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 001/621] 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 002/621] 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 377301ef36c13fe4dc44f4c816b8aeffdfd765e3 Mon Sep 17 00:00:00 2001 From: notaxeltv Date: Fri, 16 Oct 2026 00:32:24 +0200 Subject: [PATCH 003/621] fix(recompiler): fill missing pixel interpolators as DEFAULT PPSA03541 prepares a pixel shader that still reads attribute 0 while SPI_PS_IN_CONTROL.NUM_INTERP is 0; extend the CNTL table with OFFSET 0x20 defaults instead of throwing. Co-authored-by: Cursor --- .../tests/InterpolationReferenceRange.cpp | 4 ++- .../Optimization/src/ShaderInfoCollector.cpp | 32 +++++++++++++++++++ docs/dev/debt/shader.md | 1 + 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/core/libs/tests/InterpolationReferenceRange.cpp b/core/libs/tests/InterpolationReferenceRange.cpp index 7bf9c80ad3..594adbd1d2 100644 --- a/core/libs/tests/InterpolationReferenceRange.cpp +++ b/core/libs/tests/InterpolationReferenceRange.cpp @@ -42,7 +42,9 @@ int main() { } }; const auto attribute = Collect(5u, 1u, 0u, 2u); - expect(attribute.find("interpolation parameter reference is out of range (attribute 5 component 1 mode 0, 2 inputs)") != std::string::npos, "an attribute past the pixel inputs", attribute); + expect(attribute.find("interpolation parameter reference") == std::string::npos, "an attribute past the pixel inputs becomes a default", attribute); + const auto empty = Collect(0u, 0u, 0u, 0u); + expect(empty.find("interpolation parameter reference") == std::string::npos, "attribute 0 with no SPI inputs becomes a default", empty); const auto component = Collect(0u, 4u, 2u, 1u); expect(component.find("(attribute 0 component 4 mode 2, 1 inputs)") != std::string::npos, "a component past w", component); const auto mode = Collect(1u, 3u, 3u, 3u); diff --git a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp index fac95cb5d4..d89e500cb6 100644 --- a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp +++ b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp @@ -73,6 +73,35 @@ void ValidateOptions(const IrProgram& program, ShaderStageInputInfo inputInfo) { } } +void EnsurePixelInterpolatorCoverage(const IrProgram& program, ShaderPixelInputInfo& pixel) { + std::uint32_t needed = pixel.inputNum; + const auto consider = [&](std::uint32_t attribute) { + if (attribute < 32u && attribute + 1u > needed) needed = attribute + 1u; + }; + for (const auto& block : program.Blocks()) { + for (const IrValue* inst : block->Instructions()) { + switch (inst->Opcode()) { + case IrOpcode::GetAttribute: + case IrOpcode::GetInterpolationParameter: + case IrOpcode::GetInterpolationParameterF16: + case IrOpcode::InterpolateHostP1: + case IrOpcode::InterpolateHostP2: { + const IrValue* attribute = inst->Argument(0)->Resolve(); + if (attribute->HasImmediate() && attribute->Type() == IrType::U32) consider(attribute->ImmediateU32()); + break; + } + default: + break; + } + } + } + if (needed <= pixel.inputNum) return; + for (std::uint32_t input = pixel.inputNum; input < needed; ++input) { + pixel.interpolatorSettings[input] = 0x20u; + } + pixel.inputNum = needed; +} + void ValidateValueReferences(const IrProgram& program, ShaderStageInputInfo inputInfo) { for (const auto& block : program.Blocks()) { for (const IrValue* inst : block->Instructions()) { @@ -525,6 +554,9 @@ void ShaderInfoCollector::Collect(IrProgram& program, const ShaderStageInputInfo if (!program.Resources().resourceTrackingComplete || program.Metadata().shaderInfoComplete) { return Fail(!program.Resources().resourceTrackingComplete ? "shader resources were not tracked" : "shader info already collected"); } + if (program.Resources().stage == IrShaderStage::Pixel && inputInfo.pixel != nullptr) { + EnsurePixelInterpolatorCoverage(program, const_cast(*inputInfo.pixel)); + } ValidateOptions(program, inputInfo); ValidateValueReferences(program, inputInfo); auto next = program.Info(); diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index dea88ed54f..0e37433597 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -22,6 +22,7 @@ ### Functional +- [Pixel interpolators past `SPI_PS_IN_CONTROL.NUM_INTERP`](../../../core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp) (shader recompiler) - when the IR reads an attribute at or past the guest interpolator count, the missing `SPI_PS_INPUT_CNTL` slots are filled as DEFAULT (`OFFSET` 0x20, constant 0/1) before info collection, matching Mesa's treatment of a PS input with no VS output. Component and mode bounds still throw. PPSA03541 (UFC 5) prepares pixel shader `0x114073b400` with `NUM_INTERP` 0 while it still issues `v_interp` on attribute 0. Console behaviour for that pairing is not measured. - [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. - [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 From c32c26edb2beb9069dde096e5e3ff86cab3635f6 Mon Sep 17 00:00:00 2001 From: notaxeltv Date: Fri, 16 Oct 2026 05:52:28 +0200 Subject: [PATCH 004/621] fix(recompiler): emit DEFAULT for missing interpolation bindings After collector coverage, emit still rebuilt pixel input info from SPI and threw undefined attribute for the same UFC5 shader; return OFFSET 0x20 defaults from SPIR-V emission too. Co-authored-by: Cursor --- .../recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp | 9 ++++++--- docs/dev/debt/shader.md | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index 2c92bf42a0..22e6c006ac 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -183,12 +183,15 @@ std::uint32_t EmitAttributeValue(SpirvEmitterState& state, std::uint32_t attr, s } std::uint32_t EmitInterpolationParameterValue(SpirvEmitterState& state, std::uint32_t attr, std::uint32_t chan, std::uint32_t mode) { - if (PixelParameterIsDefault(state, attr)) { - return mode == 2u ? EmitAttributeValue(state, attr, chan) : ConstantU32(state, 0u); + if (state.inputInfo.pixel == nullptr) { + throw std::runtime_error("interpolation parameter refers to an undefined attribute"); + } + if (attr >= state.inputInfo.pixel->inputNum || PixelParameterIsDefault(state, attr)) { + return mode == 2u ? ConstantU32(state, state.inputInfo.pixel->InputDefaultBits(attr, chan & 3u)) : ConstantU32(state, 0u); } const auto* input = SpirvInputBindingForParameter(state, attr); if (input == nullptr) { - throw std::runtime_error("interpolation parameter refers to an undefined attribute"); + return mode == 2u ? ConstantU32(state, state.inputInfo.pixel->InputDefaultBits(attr, chan & 3u)) : ConstantU32(state, 0u); } if (!input->perVertex) { return EmitAttributeValue(state, attr, chan); diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 0e37433597..6566fda668 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -22,7 +22,7 @@ ### Functional -- [Pixel interpolators past `SPI_PS_IN_CONTROL.NUM_INTERP`](../../../core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp) (shader recompiler) - when the IR reads an attribute at or past the guest interpolator count, the missing `SPI_PS_INPUT_CNTL` slots are filled as DEFAULT (`OFFSET` 0x20, constant 0/1) before info collection, matching Mesa's treatment of a PS input with no VS output. Component and mode bounds still throw. PPSA03541 (UFC 5) prepares pixel shader `0x114073b400` with `NUM_INTERP` 0 while it still issues `v_interp` on attribute 0. Console behaviour for that pairing is not measured. +- [Pixel interpolators past `SPI_PS_IN_CONTROL.NUM_INTERP`](../../../core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp) (shader recompiler) - when the IR reads an attribute at or past the guest interpolator count, the missing `SPI_PS_INPUT_CNTL` slots are filled as DEFAULT (`OFFSET` 0x20, constant 0/1) before info collection, and SPIR-V emission of `GetInterpolationParameter` for a missing binding returns the same defaults instead of throwing, matching Mesa's treatment of a PS input with no VS output. Component and mode bounds still throw. PPSA03541 (UFC 5) prepares pixel shader `0x114073b400` with `NUM_INTERP` 0 while it still issues `v_interp` on attribute 0 (`undefined attribute` after the collector fill alone). Console behaviour for that pairing is not measured. - [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. - [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 From 4c475452d472643c7de250a855c07339d5dabf65 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sun, 18 Oct 2026 10:41:36 -0700 Subject: [PATCH 005/621] fix(agc): destroy settle aliases before an unmap releases their imports On Windows a host import under 1 MiB is settled by a batched 4-byte read. When the settle queue is in another queue family than the import's, as on AMD's Windows driver with its single graphics queue, the batch reads through an alias buffer bound to the import's memory. Reaping the batch destroys those aliases before it drops the imports retired while the batch held them. An unmap that needs such an import (releaseSettleRetired) waited for the batch and then released the import, destroying its buffer and freeing its memory while the alias was still bound to that memory; the alias went at the next reap. Destroy the batch's aliases first, as reapSettleBatches and drainSettles do. --- .../libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 3d233e6868..abe0f6738d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -386,6 +386,8 @@ bool releaseSettleRetired(HostImports& state, std::uint64_t begin, std::uint64_t ++state.settleUnmapWaits; waitSettleBatch(settle, device, batch); } + for (const auto alias : batch.aliases) settle.settleDestroyBuffer(device, alias, nullptr); + batch.aliases.clear(); for (auto& holder : batch.retired) released.push_back(std::move(holder)); batch.retired.clear(); } From e22b41c56c3e84a5a0ed38829a7bec126eaf906f Mon Sep 17 00:00:00 2001 From: LeandroLP23 <92315992+LeandroLP23@users.noreply.github.com> Date: Sun, 18 Oct 2026 17:26:54 +0000 Subject: [PATCH 006/621] fix(agc): keep a retired import with its settle batch until it is reaped forgetSettle handed back an import's settle batch only while the batch was still running. A batch the GPU had finished but that had not been reaped yet still held its aliases, so retireImport released the import at once, freeing its memory before the alias bound to it was destroyed. Hand back any busy batch. The import then stays with its batch until the batch is reaped or an unmap releases it, and both destroy the aliases first. A finished batch now keeps its imports' Vulkan objects until it is reaped (#7601). --- .../libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index abe0f6738d..a314f42abf 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -259,7 +259,7 @@ SettleBatch* forgetSettle(HostImports& state, std::uint64_t base) { SettleBatch* running = nullptr; if (const auto settle = state.settles.find(found->second.device); found->second.phase == SettlePhase::Submitted && settle != state.settles.end()) { auto& batch = settle->second.batches[found->second.batch]; - if (settleBatchRunning(settle->second, settle->first, batch)) running = &batch; + if (batch.busy) running = &batch; } state.pendingSettles.erase(found); return running; From 8a1e7371823221ea1e61d127892749e099f0f440 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sun, 18 Oct 2026 10:41:37 -0700 Subject: [PATCH 007/621] test(agc): run the host import lifetime tests on another settle family agc_driver_host_import_lifetime takes the settle alias path only on a device whose graphics family has a single queue, such as AMD's Windows driver; elsewhere the settles share the graphics family and make no alias. agc_driver_host_import_lifetime_other_family runs the same tests with the settles forced onto another queue family, as agc_import_settle_deferred_other_family does, so a device with spare graphics queues covers that path too. It is registered on Windows and skips where the device has no other queue family. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 7 +++++++ core/libs/prx/libSceAgcDriver/tests/Recorder.cpp | 9 ++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 5480263227..cb9ea09f7a 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1341,6 +1341,13 @@ if(BUILD_TESTING) endif() set_property(TEST agc_driver_host_import_lifetime APPEND PROPERTY ENVIRONMENT_MODIFICATION "APS5_RELEASE_UNDER_LOCK=unset:" "APS5_RELEASE_ON_UNLOCK=unset:") + if(WIN32) + add_test(NAME agc_driver_host_import_lifetime_other_family COMMAND agc_driver_recorder_tests --host-import-lifetime-other-family-only) + set_tests_properties(agc_driver_host_import_lifetime_other_family PROPERTIES + ENVIRONMENT "APS5_PIN_WAIT_MS=2000" SKIP_RETURN_CODE 77 TIMEOUT 30 ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_property(TEST agc_driver_host_import_lifetime_other_family APPEND PROPERTY ENVIRONMENT_MODIFICATION + "APS5_RELEASE_UNDER_LOCK=unset:" "APS5_RELEASE_ON_UNLOCK=unset:") + endif() endif() add_test_executable(agc_recorder_kept_bytes_tests EXCLUDE_FROM_ALL tests/RecorderKeptBytes.cpp) diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 2e9cd49f46..d4b369cdc7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -8763,7 +8763,7 @@ int main(int argc, char** argv) { } std::unique_ptr created; try { - created = std::make_unique(argc == 2 && (std::string_view(argv[1]) == "--import-watch-other-family-only" || std::string_view(argv[1]) == "--deferred-settle-other-family-only")); + created = std::make_unique(argc == 2 && (std::string_view(argv[1]) == "--import-watch-other-family-only" || std::string_view(argv[1]) == "--deferred-settle-other-family-only" || std::string_view(argv[1]) == "--host-import-lifetime-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'; @@ -8785,6 +8785,13 @@ int main(int argc, char** argv) { return 0; } #ifdef _WIN32 + if (argc == 2 && std::string_view(argv[1]) == "--host-import-lifetime-other-family-only") { + if (device.GetContext().settleFamily == device.GetContext().queueFamily) { + std::cout << "skipped, no other queue family to settle imports on\n"; + return 77; + } + return RunHostImportLifetimeTests(device.GetContext()); + } if (argc == 2 && (std::string_view(argv[1]) == "--deferred-settle-only" || std::string_view(argv[1]) == "--deferred-settle-other-family-only")) { if (std::string_view(argv[1]) == "--deferred-settle-other-family-only" && device.GetContext().settleFamily == device.GetContext().queueFamily) { std::cout << "skipped, no other queue family to settle imports on\n"; From c9fe9292d84e59eaa5e7bdd87c3d12a83aebb167 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Mon, 19 Oct 2026 02:45:52 +0000 Subject: [PATCH 008/621] fix(agc): draw a stencil replacement against a known stencil value as an inversion DB_STENCILREFMASK holds a test value and a separate operation value, and REPLACE_OP, ONES and REPLACE_TEST write a value that can differ from the test value on bits the compare reads. Vulkan uses one reference for both, so such a face threw. Ghost of Tsushima (PPSA02225) stops on one at its splash screen (test value 0xff, operation value 0x40, both masks 0xff). Some compares fix the stencil value wherever an operation runs: EQUAL on pass and depth fail, NOTEQUAL on fail, and LEQUAL, GEQUAL, LESS or GREATER against the largest or smallest masked value. There, if every written bit is compared, the written bits hold the test value, so the replacement is an INVERT whose write mask holds only the bits that differ from the value written. The operations of a face share its write mask, so a face whose other operations can't be drawn the same way still throws. Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/Graphics/src/State.cpp | 25 ++- .../prx/libSceAgcDriver/tests/Graphics.cpp | 34 ++++ .../tests/execution/StencilKnownReplace.cpp | 191 ++++++++++++++++++ 3 files changed, 246 insertions(+), 4 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/StencilKnownReplace.cpp diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 36293b21a8..11588edf0e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -232,7 +232,9 @@ VkStencilOpState stencilFace(std::uint32_t compare, std::uint32_t ops, std::uint const auto opValue = refMask >> 24u; auto reference = test; auto fixed = compare == 0 || compare == 7 ? 0u : face.compareMask; - const auto convert = [&](std::uint32_t op) { + std::uint32_t invertMask = 0; + std::uint32_t inverted = 0; + const auto convert = [&](std::uint32_t op, bool testFailed) { if (face.writeMask == 0) return VK_STENCIL_OP_KEEP; char detail[96]; switch (op) { @@ -247,6 +249,16 @@ VkStencilOpState stencilFace(std::uint32_t compare, std::uint32_t ops, std::uint } const auto value = op == 2 ? 0xffu : op == 3 ? test : opValue; if (((reference ^ value) & fixed & face.writeMask) != 0) { + const auto compared = test & face.compareMask; + const bool passKnown = compare == 2 || (compare == 3 && compared == face.compareMask) || (compare == 6 && compared == 0u); + const bool failKnown = compare == 5 || (compare == 1 && compared == 0u) || (compare == 4 && compared == face.compareMask); + const bool testValueKnown = testFailed ? failKnown : passKnown; + const auto flipped = (test ^ value) & face.writeMask; + if (testValueKnown && (face.writeMask & ~face.compareMask) == 0 && (invertMask == 0 || invertMask == flipped)) { + invertMask = flipped; + ++inverted; + return VK_STENCIL_OP_INVERT; + } std::snprintf(detail, sizeof(detail), "AGC graphics: stencil replacement 0x%02x against test value 0x%02x (masks 0x%02x/0x%02x) is unsupported", value, test, face.compareMask, face.writeMask); throw std::runtime_error(detail); } @@ -269,9 +281,14 @@ VkStencilOpState stencilFace(std::uint32_t compare, std::uint32_t ops, std::uint throw std::runtime_error(detail); } }; - face.failOp = convert(ops & 0xfu); - face.passOp = convert((ops >> 4u) & 0xfu); - face.depthFailOp = convert((ops >> 8u) & 0xfu); + face.failOp = convert(ops & 0xfu, true); + face.passOp = convert((ops >> 4u) & 0xfu, false); + face.depthFailOp = convert((ops >> 8u) & 0xfu, false); + if (invertMask != 0) { + const auto active = ((ops & 0xfu) != 0u) + (((ops >> 4u) & 0xfu) != 0u) + (((ops >> 8u) & 0xfu) != 0u); + if (inverted != active) throw std::runtime_error("AGC graphics: a stencil replacement drawn as an inversion beside another stencil operation is unsupported"); + face.writeMask = invertMask; + } face.reference = reference; return face; } diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index c4d4e56402..cebf2c6444 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -2180,7 +2180,41 @@ void DepthStencilTests() { queue.context[0x10b] = 0x40; queue.context[0x10c] = 0x05ffff02; queue.context[0x200] = 0x00200211; + state = AgcDriver::Graphics::DecodeState(queue); + Require(state.stencilFront.passOp == VK_STENCIL_OP_INVERT && state.stencilFront.failOp == VK_STENCIL_OP_KEEP && state.stencilFront.depthFailOp == VK_STENCIL_OP_KEEP && state.stencilFront.compareOp == VK_COMPARE_OP_EQUAL, "an equal test replaced by another value is not drawn as an inversion"); + Require(state.stencilFront.reference == 2 && state.stencilFront.compareMask == 0xff && state.stencilFront.writeMask == 7, "an inverted replacement does not keep the test value and write only the bits that differ from it"); + queue.context[0x10b] = 0x440; + state = AgcDriver::Graphics::DecodeState(queue); + Require(state.stencilFront.passOp == VK_STENCIL_OP_INVERT && state.stencilFront.depthFailOp == VK_STENCIL_OP_INVERT && state.stencilFront.writeMask == 7, "a replacement on pass and depth fail is not drawn as one inversion"); + queue.context[0x10b] = 0x4; + queue.context[0x200] = 0x00200511; + state = AgcDriver::Graphics::DecodeState(queue); + Require(state.stencilFront.failOp == VK_STENCIL_OP_INVERT && state.stencilFront.passOp == VK_STENCIL_OP_KEEP && state.stencilFront.writeMask == 7, "a not-equal test that fails is not replaced as an inversion"); + queue.context[0x10c] = 0x40ffffff; + queue.context[0x10b] = 0x40; + queue.context[0x200] = 0x00200311; + state = AgcDriver::Graphics::DecodeState(queue); + Require(state.stencilFront.passOp == VK_STENCIL_OP_INVERT && state.stencilFront.writeMask == 0xbf && state.stencilFront.reference == 0xff, "a less-or-equal test against 0xff, which passes only on 0xff, is not replaced as an inversion"); + queue.context[0x10b] = 0x4; + queue.context[0x200] = 0x00200411; + state = AgcDriver::Graphics::DecodeState(queue); + Require(state.stencilFront.failOp == VK_STENCIL_OP_INVERT && state.stencilFront.writeMask == 0xbf, "a greater test against 0xff, which fails only on 0xff, is not replaced as an inversion"); + queue.context[0x10c] = 0x40ffff7f; + queue.context[0x10b] = 0x40; + queue.context[0x200] = 0x00200311; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "stencil replacement"); + queue.context[0x10c] = 0x05ffff02; + queue.context[0x200] = 0x00200511; + queue.context[0x10b] = 0x40; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "stencil replacement"); + queue.context[0x200] = 0x00200111; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "stencil replacement"); + queue.context[0x200] = 0x00200211; + queue.context[0x10c] = 0x05ff0f02; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "stencil replacement"); + queue.context[0x10c] = 0x05ffff02; + queue.context[0x10b] = 0x41; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "beside another stencil operation"); queue.context[0x10b] = 0x50; queue.context[0x200] = 0x00700711; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "stencil add/subtract"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StencilKnownReplace.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StencilKnownReplace.cpp new file mode 100644 index 0000000000..284eb2e7d6 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StencilKnownReplace.cpp @@ -0,0 +1,191 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; + +constexpr std::byte Kept{0x40}; + +alignas(256) constexpr std::array VertexCode{ + 0x7e000d05u, 0x080200f4u, 0x10020300u, 0x100202f4u, 0x060202f3u, 0x060400f3u, 0x10040500u, 0x060404f3u, + 0x7e060280u, 0x7e0802f2u, 0xf80008cfu, 0x04030201u, 0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u, +}; +alignas(256) constexpr std::array WhitePixelCode{0x7e0e02f2u, 0xf800180fu, 0x07070707u, 0xbf810000u}; +alignas(256) std::array ColorMemory{}; +alignas(256) std::array DepthMemory{}; +alignas(256) std::array StencilMemory{}; + +struct Header { + Shader shader{}; + ShaderUserData userData{}; + std::array registers{}; +}; + +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; + if (type == 1) { + const auto address = reinterpret_cast(code); + header.registers = {{{0x008, static_cast(address >> 8u)}, {0x009, static_cast(address >> 40u)}, {0x00b, 1u << 1u}, {0x00c, std::bit_cast(1.0f)}}}; + header.shader.sh_registers = header.registers.data(); + header.shader.num_sh_registers = header.registers.size(); + } + 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); +} + +struct StencilState { + std::uint32_t depthControl; + std::uint32_t operations; + std::uint32_t refMask; +}; + +std::vector Draw(const StencilState& stencilState) { + const auto vertex = reinterpret_cast(VertexCode.data()); + const auto pixel = reinterpret_cast(WhitePixelCode.data()); + const auto color = reinterpret_cast(ColorMemory.data()); + const auto depth = reinterpret_cast(DepthMemory.data()); + const auto stencil = reinterpret_cast(StencilMemory.data()); + const std::vector> context{ + {0x2d5, 0x2000}, + {0x1b3, 0}, {0x1b4, 0}, {0x1b6, 0}, {0x207, 0}, {0x200, stencilState.depthControl}, {0x203, 0x800}, + {0x2dc, 0xaa00}, {0x2f8, 0}, {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, 9}, {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}, + {0x318, static_cast(color >> 8u)}, {0x390, static_cast(color >> 40u)}, {0x292, 2}, + {0x000, 0}, {0x002, 0}, {0x007, (3u << 16u) | 63u}, + {0x008, 0}, {0x009, std::bit_cast(1.0f)}, {0x00a, 0}, {0x00b, 0}, + {0x010, 3}, {0x011, 1}, + {0x012, static_cast(depth >> 8u)}, {0x014, static_cast(depth >> 8u)}, + {0x013, static_cast(stencil >> 8u)}, {0x015, static_cast(stencil >> 8u)}, + {0x01a, static_cast(depth >> 40u)}, {0x01c, static_cast(depth >> 40u)}, + {0x01b, static_cast(stencil >> 40u)}, {0x01d, static_cast(stencil >> 40u)}, + {0x10b, stencilState.operations}, {0x10c, stencilState.refMask}, {0x10d, stencilState.refMask}, + }; + const std::vector> shader{ + {0xc8, static_cast(vertex >> 8u)}, {0xc9, static_cast(vertex >> 40u)}, {0x8b, 1u << 1u}, {0x8c, 0}, + {0x008, static_cast(pixel >> 8u)}, {0x009, static_cast(pixel >> 40u)}, {0x00b, 1u << 1u}, {0x00c, std::bit_cast(1.0f)}, + }; + 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}); + Append(commands, 0x2d, {3, 2}); + return commands; +} + +void Submit(const StencilState& stencilState) { + ColorMemory.fill(Kept); + auto commands = Draw(stencilState); + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, "the stencil draw submission failed"); + AgcDriverWaitIdle_nid_postfix(); +} + +constexpr std::uint32_t StencilEnable = 1u; +constexpr std::uint32_t Equal = 2u << 8u; +constexpr std::uint32_t LessEqual = 3u << 8u; +constexpr std::uint32_t Greater = 4u << 8u; +constexpr std::uint32_t NotEqual = 5u << 8u; +constexpr std::uint32_t Always = 7u << 8u; + +std::uint32_t RefMask(std::uint32_t test, std::uint32_t compareMask, std::uint32_t writeMask, std::uint32_t opValue) { + return test | (compareMask << 8u) | (writeMask << 16u) | (opValue << 24u); +} + +std::array Fill(std::uint32_t value) { + Submit({StencilEnable | Always, 0x30u, RefMask(value, 0xff, 0xff, 0)}); + const auto coverage = ColorMemory; + Require(std::count(coverage.begin(), coverage.end(), Kept) != static_cast(coverage.size()), "the stencil fill covered no pixels"); + return coverage; +} + +void Expect(const std::array& coverage, std::uint32_t value, const std::string& what) { + Submit({StencilEnable | Equal, 0u, RefMask(value, 0xff, 0, 0)}); + std::size_t drawn = 0; + for (std::size_t pixel = 0; pixel < coverage.size(); pixel += 4u) { + const bool covered = coverage[pixel] != Kept; + drawn += covered; + for (std::size_t channel = 0; channel < 4u; ++channel) Require(ColorMemory[pixel + channel] == (covered ? std::byte{255} : Kept), what + ": the stencil is not " + std::to_string(value) + " exactly where the draw covered"); + } + Require(drawn != 0, what + ": the probe covered no pixels"); +} + +void Case(const std::string& what, std::uint32_t initial, const StencilState& stencilState, std::uint32_t expected, bool testPasses = true) { + const auto coverage = Fill(initial); + Submit(stencilState); + if (testPasses) Require(ColorMemory == coverage, what + ": the draw under test did not color exactly the filled pixels"); + else Require(std::all_of(ColorMemory.begin(), ColorMemory.end(), [](auto value) { return value == Kept; }), what + ": a draw whose stencil test fails wrote color"); + Expect(coverage, expected, what); +} + +} + +int main() { + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + Registered(VertexHeader, VertexCode.data(), sizeof(VertexCode), 2); + Registered(PixelHeader, WhitePixelCode.data(), sizeof(WhitePixelCode), 1); + const std::array pixelContext{{{0x203, 0x800}, {0x1c4, 9}, {0x1b6, 0}}}; + AgcDriverResolveShaderAbi_nid_postfix(&PixelHeader.shader, pixelContext, {}); + Case("an equal test replaced by the operation value", 0xff, {StencilEnable | Equal, 0x40u, RefMask(0xff, 0xff, 0xff, 0x40)}, 0x40); + Case("an equal test replaced by the operation value on pass and depth fail", 0xff, {StencilEnable | Equal, 0x440u, RefMask(0xff, 0xff, 0xff, 0x40)}, 0x40); + Case("an equal test replaced by 0xff", 0x25, {StencilEnable | Equal, 0x20u, RefMask(0x25, 0xff, 0xff, 0)}, 0xff); + Case("an equal test replaced through a partial write mask", 0xff, {StencilEnable | Equal, 0x40u, RefMask(0xff, 0xff, 0xf0, 0x40)}, 0x4f); + Case("a failed not-equal test replaced by the operation value", 0xff, {StencilEnable | NotEqual, 0x4u, RefMask(0xff, 0xff, 0xff, 0x40)}, 0x40, false); + Case("a less-or-equal test against 0xff replaced by the operation value", 0xff, {StencilEnable | LessEqual, 0x40u, RefMask(0xff, 0xff, 0xff, 0x40)}, 0x40); + Case("a failed greater test against 0xff replaced by the operation value", 0xff, {StencilEnable | Greater, 0x4u, RefMask(0xff, 0xff, 0xff, 0x40)}, 0x40, false); + AgcDriverShutdown_nid_postfix(); + std::puts("stencil replacements with a known stencil value passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + try { AgcDriverShutdown_nid_postfix(); } catch (...) {} + return 1; + } +} From 054cf422026b7ac81e37deb14db2cbcb0b53904a Mon Sep 17 00:00:00 2001 From: milescoviello Date: Mon, 19 Oct 2026 12:01:48 +0000 Subject: [PATCH 009/621] 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 82be87f1852b539fac780c93841b001c4bc75c9f Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Mon, 19 Oct 2026 09:10:23 +0000 Subject: [PATCH 010/621] fix(agc): refuse LDS split before allocation --- .../Execution/src/VulkanDevice.cpp | 14 ++++++++------ .../tests/execution/OversizedDispatch.cpp | 18 +++++++++++------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 1ebaafcc12..46d0e2d70f 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -4606,6 +4606,14 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) { auto& resources = *record.resources; const auto arguments = record.arguments; const auto* argumentImport = record.argumentImport; + const auto* limit = state->properties.limits.maxComputeWorkGroupCount; + if (argumentImport == nullptr && record.shader->program->workgroupMemoryDwords != 0 && (record.x > limit[0] || record.y > limit[1] || record.z > limit[2])) { + char counted[128] = ""; + if (record.cpuArguments != 0) std::snprintf(counted, sizeof(counted), " (read on the CPU from the indirect arguments at 0x%llx, cpuReason %d)", static_cast(record.cpuArguments), record.cpuReason); + char text[384]; + std::snprintf(text, sizeof(text), "Vulkan dispatch: workgroup count %ux%ux%u%s exceeds device limits %ux%ux%u and a split aliases the device-memory LDS slots, whose base is computed from gl_NumWorkGroups of a single chunk", record.x, record.y, record.z, counted, limit[0], limit[1], limit[2]); + throw std::runtime_error(text); + } // The record phase split (APS5_PROFILE_DRAW, rows "record: ..." of the [dispatch] totals): // opening the batch, the keeps, the data refresh, the barriers with the bind and the dispatch // itself, the pending-write notes and marks (MarkGpuWrites), and the completion registration. @@ -4718,17 +4726,11 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) { if (argumentImport != nullptr) { context.Resolved(&Graphics::DeviceFunctions::cmdDispatchIndirect, "vkCmdDispatchIndirect")(commands, indirectBuffer, indirectOffset); } else { - const auto* limit = state->properties.limits.maxComputeWorkGroupCount; if (record.x <= limit[0] && record.y <= limit[1] && record.z <= limit[2]) { context.Resolved(&Graphics::DeviceFunctions::cmdDispatch, "vkCmdDispatch")(commands, record.x, record.y, record.z); } else { char counted[128] = ""; if (record.cpuArguments != 0) std::snprintf(counted, sizeof(counted), " (read on the CPU from the indirect arguments at 0x%llx, cpuReason %d)", static_cast(record.cpuArguments), record.cpuReason); - if (record.shader->program->workgroupMemoryDwords != 0) { - char text[384]; - std::snprintf(text, sizeof(text), "Vulkan dispatch: workgroup count %ux%ux%u%s exceeds device limits %ux%ux%u and a split aliases the device-memory LDS slots, whose base is computed from gl_NumWorkGroups of a single chunk", record.x, record.y, record.z, counted, limit[0], limit[1], limit[2]); - throw std::runtime_error(text); - } const auto offsets = std::ranges::count_if(record.shader->program->specialization, [](const auto& constant) { return ShaderRecompiler::PipelineSpecialization::IsWorkgroupOffset(constant.id); }); if (offsets != ShaderRecompiler::PipelineSpecialization::WorkgroupOffsetWords) { char text[320]; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/OversizedDispatch.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/OversizedDispatch.cpp index 06b14970f4..9cc490015d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/OversizedDispatch.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/OversizedDispatch.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -135,14 +136,17 @@ void CheckLdsSplitThrows(AgcDriver::VulkanDevice& device) { request.useCache = false; const auto result = ShaderRecompiler::Recompile(request); Require(result.workgroupMemoryDwords != 0u, "oversized dispatch: the LDS shader did not take the device-memory path"); - bool threw = false; - try { - device.Dispatch(result, 1, device.WorkGroupCountLimit()[1] + 1u, 1, {}, reinterpret_cast(code.data())); - } catch (const std::runtime_error&) { - threw = true; + for (const auto z : {1u, std::numeric_limits::max()}) { + bool threw = false; + try { + device.Dispatch(result, 1, device.WorkGroupCountLimit()[1] + 1u, z, {}, reinterpret_cast(code.data())); + } catch (const std::runtime_error& error) { + Require(std::string(error.what()).find("a split aliases the device-memory LDS slots") != std::string::npos, "oversized dispatch: wrong LDS split refusal: " + std::string(error.what())); + threw = true; + } + device.WaitIdle(); + Require(threw, "oversized dispatch: a Y count over the limit with device-memory LDS did not throw"); } - device.WaitIdle(); - Require(threw, "oversized dispatch: a Y count over the limit with device-memory LDS did not throw"); } alignas(256) constexpr std::array GroupYCode{ From f6b6d79aa87c5d1a81d057cc9d45ebec1f9d4c6f Mon Sep 17 00:00:00 2001 From: Adam Franke Date: Mon, 19 Oct 2026 12:14:56 -0400 Subject: [PATCH 011/621] 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 012/621] 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 013/621] 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 014/621] 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 7ca5faf4a4672853c394cb291fac64ac5472169f Mon Sep 17 00:00:00 2001 From: David Date: Tue, 20 Oct 2026 00:39:13 +0400 Subject: [PATCH 015/621] 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 016/621] 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 017/621] 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 018/621] 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 efd895b5118d793d5a6e7c9f39481ffeebf73e87 Mon Sep 17 00:00:00 2001 From: David Date: Tue, 20 Oct 2026 01:07:33 +0400 Subject: [PATCH 019/621] 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 2b991e8751935c5f8d666b16676d9d7872ae0a0b Mon Sep 17 00:00:00 2001 From: simonbronk Date: Sat, 17 Oct 2026 19:07:22 +0200 Subject: [PATCH 020/621] 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 9664503056628359eb98f2e8f41b6b959bc22587 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Tue, 20 Oct 2026 01:39:53 +0300 Subject: [PATCH 021/621] 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 022/621] 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 023/621] 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 b4762a6583427dcc2246b0062576f0e0df914a66 Mon Sep 17 00:00:00 2001 From: ShinMalphur Date: Tue, 20 Oct 2026 09:18:31 +0630 Subject: [PATCH 024/621] 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 025/621] 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 026/621] 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 bb864e8acdc806672853bbdfd28a3dd5a2d4550b Mon Sep 17 00:00:00 2001 From: felek Date: Tue, 20 Oct 2026 01:30:25 +0200 Subject: [PATCH 027/621] 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 ca55e46f98ad85861dd7567abad0c028274daca1 Mon Sep 17 00:00:00 2001 From: dantdmdarealguy Date: Tue, 20 Oct 2026 04:31:08 -0500 Subject: [PATCH 028/621] 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 b3a38791f9812b1c62c1b2f038c05c2d30b19729 Mon Sep 17 00:00:00 2001 From: Gruzzberry Date: Tue, 20 Oct 2026 10:47:24 +0100 Subject: [PATCH 029/621] docs: add trademark and non-affiliation notice to README Co-Authored-By: Claude Sonnet 5.5 --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 5777022fb4..c06b74b83c 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,8 @@ SDL-mapped game controllers are supported, including analog sticks and triggers. This project is intended for interoperability, research, preservation, and compatibility purposes. It does not include, distribute, or require copyrighted software, firmware, cryptographic keys, or proprietary libraries. Users are responsible for ensuring that any binaries used with this project are obtained and used in accordance with applicable laws and their respective license terms. +AnyPS5 is an independent project and is not affiliated with, authorized or endorsed by Sony Interactive Entertainment. "PlayStation" and "PS5" are trademarks of Sony Interactive Entertainment Inc., used here only to describe compatibility. + ## License This project is licensed under the GNU General Public License version 2 only. From 8cae300f8df857b28e365a16141597f881687dc9 Mon Sep 17 00:00:00 2001 From: Gruzzberry Date: Tue, 20 Oct 2026 11:53:13 +0100 Subject: [PATCH 030/621] docs: drop the compatibility clause from the trademark notice Co-Authored-By: Claude Sonnet 5.5 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c06b74b83c..3e7fc3ccf4 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ SDL-mapped game controllers are supported, including analog sticks and triggers. This project is intended for interoperability, research, preservation, and compatibility purposes. It does not include, distribute, or require copyrighted software, firmware, cryptographic keys, or proprietary libraries. Users are responsible for ensuring that any binaries used with this project are obtained and used in accordance with applicable laws and their respective license terms. -AnyPS5 is an independent project and is not affiliated with, authorized or endorsed by Sony Interactive Entertainment. "PlayStation" and "PS5" are trademarks of Sony Interactive Entertainment Inc., used here only to describe compatibility. +AnyPS5 is an independent project and is not affiliated with, authorized or endorsed by Sony Interactive Entertainment. "PlayStation" and "PS5" are trademarks of Sony Interactive Entertainment Inc. ## License From 25a8636b50faa6ba809a2c350e7e837fbf6bca05 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Tue, 20 Oct 2026 14:02:33 +0200 Subject: [PATCH 031/621] fix(libkernel): never block SIGKILL and SIGSTOP in sigprocmask and sigsuspend FreeBSD 11.4 kern_sigprocmask applies SIG_CANTMASK to the set for SIG_BLOCK and SIG_SETMASK (sys/kern/kern_sig.c:1056, 1067), and sigsuspend sets its mask through the same call (kern_sig.c:1534). The guest mask kept both bits, so sigprocmask(SIG_BLOCK, {SIGKILL}) read back as blocked. Co-Authored-By: Claude Sonnet 5.5 --- .../libs/prx/libkernel/System/src/Signals.cpp | 7 +++--- core/libs/tests/GuestSignals.cpp | 24 +++++++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libkernel/System/src/Signals.cpp b/core/libs/prx/libkernel/System/src/Signals.cpp index c9211181d4..7319aae317 100644 --- a/core/libs/prx/libkernel/System/src/Signals.cpp +++ b/core/libs/prx/libkernel/System/src/Signals.cpp @@ -37,6 +37,7 @@ using GuestHandler = void (APS5_VABI *)(int); constexpr int MaxSignal = 128; constexpr int GuestSigkill = 9; constexpr int GuestSigstop = 17; +constexpr std::uint32_t UnblockableSignals = (1u << (GuestSigkill - 1)) | (1u << (GuestSigstop - 1)); constexpr int GuestSigalrm = 14; constexpr int SaRestart = 0x2; constexpr int SaSiginfo = 0x40; @@ -555,7 +556,7 @@ int APS5_VABI _sigprocmask_nid_postfix(int how, const GuestSignalSet* set, Guest if (set != nullptr) { switch (how) { case 1: - blockedMask.fetch_or(set->bits[0]); + blockedMask.fetch_or(set->bits[0] & ~UnblockableSignals); for (int word = 1; word < 4; ++word) blockedMaskExtra[word - 1].fetch_or(set->bits[word]); break; case 2: @@ -563,7 +564,7 @@ int APS5_VABI _sigprocmask_nid_postfix(int how, const GuestSignalSet* set, Guest for (int word = 1; word < 4; ++word) blockedMaskExtra[word - 1].fetch_and(~set->bits[word]); break; case 3: - blockedMask.store(set->bits[0]); + blockedMask.store(set->bits[0] & ~UnblockableSignals); for (int word = 1; word < 4; ++word) blockedMaskExtra[word - 1].store(set->bits[word]); break; } @@ -628,7 +629,7 @@ int APS5_VABI sigsuspend_nid_postfix(const GuestSignalSet* set) { GuestSignalSet previous{{0, 0, 0, 0}}; { std::lock_guard lock(registration); - previous.bits[0] = blockedMask.exchange(set->bits[0]); + previous.bits[0] = blockedMask.exchange(set->bits[0] & ~UnblockableSignals); for (int word = 1; word < 4; ++word) previous.bits[word] = blockedMaskExtra[word - 1].exchange(set->bits[word]); } WaitForDeliverableSignal(); diff --git a/core/libs/tests/GuestSignals.cpp b/core/libs/tests/GuestSignals.cpp index 016c4c5a0f..8e254fd717 100644 --- a/core/libs/tests/GuestSignals.cpp +++ b/core/libs/tests/GuestSignals.cpp @@ -64,6 +64,11 @@ void APS5_VABI Callback(int value) { received = value; handlerReturn = reinterpret_cast(__builtin_return_address(0)); } +GuestSignalSet maskInHandler{}; +void APS5_VABI MaskCallback(int value) { + received = value; + sigprocmask_nid_postfix(1, nullptr, &maskInHandler); +} static void Require(bool value) { if (!value) std::abort(); } int main() { const auto invalid = reinterpret_cast(static_cast(-1)); @@ -112,6 +117,17 @@ int main() { Require(sigprocmask_nid_postfix(1, nullptr, &previous) == 0 && previous.bits[0] == 0x20); Require(pthread_sigmask_nid_postfix(2, &blocked, &previous) == 0 && previous.bits[0] == 0x20); Require(pthread_sigmask_nid_postfix(1, nullptr, &previous) == 0 && previous.bits[0] == 0); + const GuestSignalSet withKillAndStop{{(1u << 8) | (1u << 16) | 0x20, 0, 0, 0}}; + const GuestSignalSet noSignals{{0, 0, 0, 0}}; + Require(sigprocmask_nid_postfix(1, &withKillAndStop, nullptr) == 0); + Require(sigprocmask_nid_postfix(1, nullptr, &previous) == 0 && previous.bits[0] == 0x20); + Require(sigprocmask_nid_postfix(3, &noSignals, nullptr) == 0); + Require(sigprocmask_nid_postfix(3, &withKillAndStop, nullptr) == 0); + Require(sigprocmask_nid_postfix(1, nullptr, &previous) == 0 && previous.bits[0] == 0x20); + Require(sigprocmask_nid_postfix(3, &noSignals, nullptr) == 0); + Require(pthread_sigmask_nid_postfix(1, &withKillAndStop, nullptr) == 0); + Require(pthread_sigmask_nid_postfix(1, nullptr, &previous) == 0 && previous.bits[0] == 0x20); + Require(pthread_sigmask_nid_postfix(3, &noSignals, nullptr) == 0); const auto callback = reinterpret_cast(Callback); const auto infoCallback = reinterpret_cast(InfoCallback); @@ -184,6 +200,14 @@ int main() { *__error_nid_postfix() = 0; Require(sigsuspend_nid_postfix(&empty) == -1 && *__error_nid_postfix() == 4 && received == 30); Require(sigprocmask_nid_postfix(1, nullptr, &previous) == 0 && previous.bits[0] == user.bits[0]); + Require(signal_nid_postfix(30, MaskCallback) != invalid); + const GuestSignalSet suspendWithKillAndStop{{(1u << 8) | (1u << 16) | (1u << 28), 0, 0, 0}}; + received = 0; + Require(raise_nid_postfix(30) == 0 && received == 0); + Require(sigsuspend_nid_postfix(&suspendWithKillAndStop) == -1 && received == 30); + Require(maskInHandler.bits[0] == (1u << 28)); + Require(sigprocmask_nid_postfix(1, nullptr, &previous) == 0 && previous.bits[0] == user.bits[0]); + Require(signal_nid_postfix(30, Callback) != invalid); received = 0; SuspendWait wait{&empty, 0, 0}; void* suspender = nullptr; From 529c1a0f5146f4b67267b98bca37795785466c29 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Tue, 20 Oct 2026 14:16:18 +0200 Subject: [PATCH 032/621] fix(libkernel): reject undefined sa_flags in sigaction with EINVAL FreeBSD 11.4 kern_sigaction fails with EINVAL when a handler other than SIG_DFL and SIG_IGN comes with flags outside SA_ONSTACK, SA_RESTART, SA_RESETHAND, SA_NOCLDSTOP, SA_NODEFER, SA_NOCLDWAIT and SA_SIGINFO (sys/kern/kern_sig.c:691-695). The guest accepted any flag word. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libkernel/System/src/Signals.cpp | 2 ++ core/libs/tests/GuestSignals.cpp | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/core/libs/prx/libkernel/System/src/Signals.cpp b/core/libs/prx/libkernel/System/src/Signals.cpp index c9211181d4..fe28f0e76f 100644 --- a/core/libs/prx/libkernel/System/src/Signals.cpp +++ b/core/libs/prx/libkernel/System/src/Signals.cpp @@ -40,6 +40,7 @@ constexpr int GuestSigstop = 17; constexpr int GuestSigalrm = 14; constexpr int SaRestart = 0x2; constexpr int SaSiginfo = 0x40; +constexpr int SaDefinedFlags = 0x7f; std::atomic handlers[MaxSignal + 1]{}; static_assert(std::atomic::is_always_lock_free); std::atomic blockedMask{0}; @@ -485,6 +486,7 @@ GuestHandler APS5_VABI signal_nid_postfix(int guest, GuestHandler handler) { } int APS5_VABI sigaction_nid_postfix(int guest, const GuestSigaction* action, GuestSigaction* previous) { if (guest < 1 || guest > MaxSignal) { *__error_nid_postfix() = 22; return -1; } + if (action && action->handler > 1 && (action->flags & ~SaDefinedFlags) != 0) { *__error_nid_postfix() = 22; return -1; } if (action && (guest == GuestSigkill || guest == GuestSigstop) && action->handler != 0) { *__error_nid_postfix() = 22; return -1; } std::lock_guard lock(registration); const GuestSigaction old = dispositions[guest]; diff --git a/core/libs/tests/GuestSignals.cpp b/core/libs/tests/GuestSignals.cpp index 016c4c5a0f..90293808f7 100644 --- a/core/libs/tests/GuestSignals.cpp +++ b/core/libs/tests/GuestSignals.cpp @@ -130,6 +130,15 @@ int main() { Require(signal_nid_postfix(15, Callback) == nullptr); Require(sigaction_nid_postfix(15, nullptr, ¤t) == 0 && current.handler == callback && current.flags == 0x2); Require(signal_nid_postfix(15, nullptr) == Callback); + for (const int flags : {0x80, 0x100, 0x8000, 0x40 | 0x80}) { + const GuestSigaction undefinedFlags{callback, flags, {}}; + *__error_nid_postfix() = 0; + Require(sigaction_nid_postfix(15, &undefinedFlags, nullptr) == -1 && *__error_nid_postfix() == 22); + Require(sigaction_nid_postfix(15, nullptr, ¤t) == 0 && current.handler == 0 && current.flags == 0x2); + } + const GuestSigaction ignoredWithFlags{1, 0x8000, {}}; + Require(sigaction_nid_postfix(15, &ignoredWithFlags, nullptr) == 0); + Require(sigaction_nid_postfix(15, &defaults, nullptr) == 0); const GuestSigaction crash{infoCallback, 0x40 | 0x10, {}}; for (int recorded : {10, 1, 64, 128}) { From 12785e21924857264f608455f45df8007d0708f3 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Tue, 20 Oct 2026 14:35:04 +0200 Subject: [PATCH 033/621] fix(libkernel): accept MAP_ANON without MAP_PRIVATE and refuse MAP_SHARED with MAP_PRIVATE FreeBSD 11.4 kern_mmap maps anonymous memory whenever MAP_ANON is set, with or without MAP_PRIVATE (sys/vm/vm_mmap.c:339), and fails with EINVAL when both MAP_SHARED and MAP_PRIVATE are set (vm_mmap.c:252). The guest failed the first with ENOTSUP (45) and the second with ENOTSUP as well. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp | 4 +++- core/libs/tests/GuestMemory.cpp | 8 ++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp b/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp index 7c69af40fa..35e9784538 100644 --- a/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp @@ -155,7 +155,9 @@ void* APS5_VABI mmap_nid_postfix(void* address, std::size_t length, int protecti if (!RoundLength(length, rounded) || (protection & ~7) != 0) return failed(GuestInvalid); if ((flags & (GuestMapExclusive | GuestMapFixed)) == GuestMapExclusive) return failed(GuestInvalid); - const int mappingFlags = flags & ~GuestAlignedSuper; + if ((flags & (GuestShared | GuestPrivate)) == (GuestShared | GuestPrivate)) return failed(GuestInvalid); + int mappingFlags = flags & ~GuestAlignedSuper; + if ((mappingFlags & (GuestShared | GuestPrivate | GuestAnonymous)) == GuestAnonymous) mappingFlags |= GuestPrivate; const bool fixedAnonymous = (mappingFlags & ~(GuestMapFixed | GuestMapExclusive)) == (GuestPrivate | GuestAnonymous) && (mappingFlags & GuestMapFixed) != 0; if (mappingFlags == (GuestPrivate | GuestAnonymous) || fixedAnonymous) { diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index 04fe239d47..d638a658b1 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -2985,6 +2985,14 @@ int main(int argc, char** argv) { reject(page, 3, 0x1012, -1, 0, 22); reject(page, 3, 0x2, 0, 0, 45); // file-backed reject(page, 3, 0x22, -1, 0, 45); // Linux MAP_ANON is not guest MAP_ANON + reject(page, 3, 0x1003, -1, 0, 22); + reject(page, 3, 0x3, 0, 0, 22); + auto* anonymousOnly = static_cast(mmap_nid_postfix(nullptr, page, 3, 0x1000, -1, 0)); + Require(anonymousOnly != failed && (reinterpret_cast(anonymousOnly) & (page - 1)) == 0); + anonymousOnly[page - 1] = 7; + Require(anonymousOnly[page - 1] == 7 && anonymousOnly[0] == 0); + Require(munmap_nid_postfix(const_cast(anonymousOnly), page) == 0); + reject(page, 3, 0x1000, 0, 0, 22); auto* memory = static_cast(mmap_nid_postfix(nullptr, page * 3 - 1, 3, 0x1002, -1, 0)); Require(memory != failed && (reinterpret_cast(memory) & (page - 1)) == 0); From ce8f7803ea5d830733c046e1991ed9485fa1fef2 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Tue, 20 Oct 2026 13:54:24 +0200 Subject: [PATCH 034/621] fix(libkernel): check socket address arguments before the descriptor bind, connect and sendto looked the descriptor up before they looked at the address length, so a descriptor that is not open hid an argument error. FreeBSD 11.4 sys_bind, sys_connect and sendit run getsockaddr first (ENAMETOOLONG, EINVAL, EFAULT) and only then look the descriptor up, so these arguments are now answered first and EBADF only follows for a valid address. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libkernel/Socket/src/Udp.cpp | 14 +++++++++++--- core/libs/tests/GuestSocketAddressLength.cpp | 14 ++++++++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 7b64c96aae..deb8421e4a 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -226,10 +226,13 @@ std::shared_ptr Lookup(int descriptor) { Fail(descriptor >= 0 && DescriptorIsOpen_nid_no_patch(descriptor) ? 38 : 9); return {}; } +int AddressLengthError(const void* input, std::uint32_t length) { + if (length > 255) return 63; + if (length < 2) return 22; + return input ? 0 : 14; +} bool Address(const void* input, std::uint32_t length, sockaddr_storage& native, socklen_t& size) { - if (length > 255) { Fail(63); return false; } - if (length < 2) { Fail(22); return false; } - if (!input) { Fail(14); return false; } + if (const int error = AddressLengthError(input, length)) { Fail(error); return false; } const auto* bytes = static_cast(input); if ((bytes[1] == 2 && length < 16) || (bytes[1] == 28 && length < 28)) { Fail(22); return false; } if (bytes[1] == 2 && length >= 16) { @@ -1094,6 +1097,7 @@ int APS5_VABI socketpair_nid_postfix(int family, int type, int protocol, int* de } } int APS5_VABI connect_nid_postfix(int descriptor, const void* address, std::uint32_t length) { + if (const int error = AddressLengthError(address, length)) return Fail(error); const auto socket = Lookup(descriptor); if (!socket) return -1; if (socket->family != GuestUnix && socket->type == 2 && address && length >= 2 && static_cast(address)[1] == 0 @@ -1179,6 +1183,7 @@ std::int64_t APS5_VABI recv_nid_postfix(int descriptor, void* buffer, std::uint6 return result < 0 ? Fail(NativeError()) : result; } int APS5_VABI bind_nid_postfix(int descriptor, const void* address, std::uint32_t length) { + if (const int error = AddressLengthError(address, length)) return Fail(error); const auto socket = Lookup(descriptor); if (!socket) return -1; sockaddr_storage native{}; @@ -1253,6 +1258,9 @@ int APS5_VABI _ioctl_nid_postfix(int descriptor, std::uint64_t request, void* ar } 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) { + if (address) { + if (const int error = AddressLengthError(address, addressLength)) return Fail(error); + } const auto socket = Lookup(descriptor); if (!socket) return -1; if ((flags & ~GuestSendFlags) != 0) return Fail(45); diff --git a/core/libs/tests/GuestSocketAddressLength.cpp b/core/libs/tests/GuestSocketAddressLength.cpp index 930a8626d6..d18dd5910c 100644 --- a/core/libs/tests/GuestSocketAddressLength.cpp +++ b/core/libs/tests/GuestSocketAddressLength.cpp @@ -56,6 +56,19 @@ void ShortAddressesAreInvalid() { Require(close_nid_postfix(stream) == 0 && close_nid_postfix(datagram) == 0 && close_nid_postfix(stream6) == 0, "close failed"); } +void ArgumentsAreCheckedBeforeTheDescriptor() { + const auto address = Loopback(); + constexpr int ErrorBadDescriptor = 9; + Require(Fails(bind_nid_postfix(-1, address.data(), 1), ErrorInvalid), "bind(-1) with a 1-byte address must fail with EINVAL (22) before the descriptor is looked up"); + Require(Fails(bind_nid_postfix(-1, address.data(), 256), ErrorNameTooLong), "bind(-1) with a 256-byte address must fail with ENAMETOOLONG (63)"); + Require(Fails(bind_nid_postfix(-1, nullptr, address.size()), ErrorFault), "bind(-1) with a null address must fail with EFAULT (14)"); + Require(Fails(connect_nid_postfix(-1, address.data(), 1), ErrorInvalid), "connect(-1) with a 1-byte address must fail with EINVAL (22)"); + Require(Fails(sendto_nid_postfix(-1, "x", 1, 0, address.data(), 1), ErrorInvalid), "sendto(-1) with a 1-byte address must fail with EINVAL (22)"); + Require(Fails(bind_nid_postfix(-1, address.data(), address.size()), ErrorBadDescriptor), "bind(-1) with a valid address must fail with EBADF (9)"); + Require(Fails(connect_nid_postfix(-1, address.data(), address.size()), ErrorBadDescriptor), "connect(-1) with a valid address must fail with EBADF (9)"); + Require(Fails(sendto_nid_postfix(-1, "x", 1, 0, address.data(), address.size()), ErrorBadDescriptor), "sendto(-1) with a valid address must fail with EBADF (9)"); +} + void FullAddressesStillWork() { const int stream = socket_nid_postfix(2, Stream, 0); Require(stream >= 0, "socket failed"); @@ -68,6 +81,7 @@ void FullAddressesStillWork() { int main() { ShortAddressesAreInvalid(); + ArgumentsAreCheckedBeforeTheDescriptor(); FullAddressesStillWork(); return 0; } From 26f861dd272da78edbd4d3d6626f5f39fbc86ed3 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Tue, 20 Oct 2026 15:12:33 +0200 Subject: [PATCH 035/621] feat(libkernel): accept MSG_WAITALL in recv, recvfrom and recvmsg on a stream socket soreceive_generic keeps a MSG_WAITALL receive on a stream socket waiting until the buffer is full, the peer closes or an error occurs. The three calls failed every flag but MSG_PEEK and MSG_DONTWAIT with EOPNOTSUPP. A stream socket now passes the flag to the host receive; a datagram socket ignores it, as a record is delivered whole. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libkernel/Socket/src/Udp.cpp | 16 +++++---- core/libs/tests/GuestTcp.cpp | 42 ++++++++++++++++++++++ docs/dev/debt/libkernel.md | 1 + 3 files changed, 52 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 7b64c96aae..edd764e713 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -67,10 +67,11 @@ constexpr auto Invalid = -1; #endif int Fail(int error) { *__error_nid_postfix() = error; return -1; } constexpr int GuestPeek = 0x2; +constexpr int GuestWaitAll = 0x40; constexpr int GuestDontWait = 0x80; constexpr int GuestNoSignal = 0x20000; constexpr int GuestSendFlags = GuestNoSignal | GuestDontWait; -constexpr int GuestReceiveFlags = GuestPeek | GuestDontWait; +constexpr int GuestReceiveFlags = GuestPeek | GuestWaitAll | GuestDontWait; int NativeSendFlags(int flags) { #ifdef _WIN32 static_cast(flags); @@ -79,11 +80,12 @@ int NativeSendFlags(int flags) { return (flags & GuestNoSignal ? MSG_NOSIGNAL : 0) | (flags & GuestDontWait ? MSG_DONTWAIT : 0); #endif } -int NativeReceiveFlags(int flags) { +int NativeReceiveFlags(int flags, int type) { + const int waitAll = type == 1 && (flags & GuestWaitAll) != 0 ? MSG_WAITALL : 0; #ifdef _WIN32 - return flags & GuestPeek ? MSG_PEEK : 0; + return (flags & GuestPeek ? MSG_PEEK : 0) | waitAll; #else - return (flags & GuestPeek ? MSG_PEEK : 0) | (flags & GuestDontWait ? MSG_DONTWAIT : 0); + return (flags & GuestPeek ? MSG_PEEK : 0) | (flags & GuestDontWait ? MSG_DONTWAIT : 0) | waitAll; #endif } int NativeError() { @@ -1172,7 +1174,7 @@ std::int64_t APS5_VABI recv_nid_postfix(int descriptor, void* buffer, std::uint6 if (!destination.Open()) return Fail(14); if (const int waited = WaitForTransfer(*socket, false, flags)) return Fail(waited); const auto result = ::recv(socket->value, static_cast(buffer), static_cast(length), - NativeReceiveFlags(flags)); + NativeReceiveFlags(flags, socket->type)); #ifdef _WIN32 if (result < 0 && socket->type == 2 && WSAGetLastError() == WSAEMSGSIZE) return static_cast(length); #endif @@ -1286,7 +1288,7 @@ std::int64_t APS5_VABI recvfrom_nid_postfix(int descriptor, void* buffer, std::u socklen_t size = sizeof(native); if (const int waited = WaitForTransfer(*socket, false, flags)) return Fail(waited); auto result = ::recvfrom(socket->value, static_cast(buffer), static_cast(length), - NativeReceiveFlags(flags), reinterpret_cast(&native), &size); + NativeReceiveFlags(flags, socket->type), reinterpret_cast(&native), &size); #ifdef _WIN32 if (result < 0 && socket->type == 2 && WSAGetLastError() == WSAEMSGSIZE) result = static_cast(length); #endif @@ -1434,7 +1436,7 @@ std::int64_t APS5_VABI recvmsg_nid_postfix(int descriptor, GuestMsghdr* message, socklen_t size = sizeof(native); if (const int waited = WaitForTransfer(*socket, false, flags)) return Fail(waited); std::int64_t received = ::recvfrom(socket->value, buffer.data(), static_cast(buffer.size()), - NativeReceiveFlags(flags), reinterpret_cast(&native), &size); + NativeReceiveFlags(flags, socket->type), reinterpret_cast(&native), &size); if (received < 0) { #ifdef _WIN32 if (!records || WSAGetLastError() != WSAEMSGSIZE) return Fail(NativeError()); diff --git a/core/libs/tests/GuestTcp.cpp b/core/libs/tests/GuestTcp.cpp index 2b211538ed..15f8d514a6 100644 --- a/core/libs/tests/GuestTcp.cpp +++ b/core/libs/tests/GuestTcp.cpp @@ -17,6 +17,7 @@ int APS5_VABI connect_nid_postfix(int, const void*, std::uint32_t); int APS5_VABI accept_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); +std::int64_t APS5_VABI sendto_nid_postfix(int, const void*, std::uint64_t, int, const void*, std::uint32_t); int APS5_VABI getpeername_nid_postfix(int, void*, std::uint32_t*); int APS5_VABI shutdown_nid_postfix(int, int); std::int64_t APS5_VABI recvfrom_nid_postfix(int, void*, std::uint64_t, int, void*, std::uint32_t*); @@ -137,10 +138,51 @@ static void UnboundSocketNameIsZero() { } } +static void WaitAllReceivesTheWholeRequest() { + constexpr int WaitAll = 0x40; + 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 && listen_nid_postfix(listener, 4) == 0); + std::uint32_t size = address.size(); + Require(getsockname_nid_postfix(listener, address.data(), &size) == 0); + const int client = socket_nid_postfix(2, 1, 0); + Require(connect_nid_postfix(client, address.data(), address.size()) == 0); + const int accepted = accept_nid_postfix(listener, nullptr, nullptr); + Require(accepted >= 0); + + Require(send_nid_postfix(client, "abcd", 4, 0) == 4); + std::thread sender([client] { + std::this_thread::sleep_for(std::chrono::milliseconds(150)); + Require(send_nid_postfix(client, "efgh", 4, 0) == 4); + }); + char received[8]{}; + Require(recv_nid_postfix(accepted, received, sizeof(received), WaitAll) == 8); + sender.join(); + Require(std::memcmp(received, "abcdefgh", 8) == 0); + + Require(send_nid_postfix(client, "xy", 2, 0) == 2 && shutdown_nid_postfix(client, 1) == 0); + char tail[8]{}; + Require(recvfrom_nid_postfix(accepted, tail, sizeof(tail), WaitAll, nullptr, nullptr) == 2); + Require(std::memcmp(tail, "xy", 2) == 0); + + const int datagram = socket_nid_postfix(2, 2, 0); + Require(datagram >= 0); + std::array local{16, 2, 0, 0, 127, 0, 0, 1}; + Require(bind_nid_postfix(datagram, local.data(), local.size()) == 0); + size = local.size(); + Require(getsockname_nid_postfix(datagram, local.data(), &size) == 0); + Require(sendto_nid_postfix(datagram, "dgram", 5, 0, local.data(), local.size()) == 5); + char record[16]{}; + Require(recv_nid_postfix(datagram, record, sizeof(record), WaitAll) == 5 && std::memcmp(record, "dgram", 5) == 0); + for (const int descriptor : {datagram, client, accepted, listener}) Require(close_nid_postfix(descriptor) == 0); +} + int main() { StreamReceiveReportsNoSender(); ListenersShareAPortAcrossFamilies(); AcceptedSocketsFollowListenerBlocking(); + WaitAllReceivesTheWholeRequest(); UnboundSocketNameIsZero(); const int listener = socket_nid_postfix(2, 1, 6); Require(listener >= 0); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 04342995ea..b7248ac4ef 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -179,6 +179,7 @@ - [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 - [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 +- [MSG_WAITALL](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) in `recv`, `recvfrom` and `recvmsg` on a stream socket is passed to the host, which returns when the buffer is full, the peer closes or an error occurs, as FreeBSD 11.4's [`soreceive_generic`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L1855-L1860) does; a datagram socket ignores it. It is not measured on a console, a nonblocking socket and a call that also passes `MSG_PEEK` follow the host's rules, and `SO_RCVTIMEO` bounds only the wait for the first data. Any other flag still fails with `EOPNOTSUPP`. - [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. - [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 From cd1efc9fca09c2b5f3a4e687cc21969652d7f51d Mon Sep 17 00:00:00 2001 From: Werner Date: Mon, 19 Oct 2026 13:24:04 +0200 Subject: [PATCH 036/621] 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 037/621] 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 669455d1f27cf71227c7c64aad1d99b5689e4078 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Tue, 20 Oct 2026 15:56:03 +0200 Subject: [PATCH 038/621] fix(libkernel): reject a pthread barrier count above INT_MAX like libthr _pthread_barrier_init fails a count above INT_MAX with EINVAL. pthread_barrier_init accepted any count above zero. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libkernel/Pthread/Posix/Barrier.cpp | 3 ++- core/libs/tests/GuestPthreadBarrier.cpp | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/Pthread/Posix/Barrier.cpp b/core/libs/prx/libkernel/Pthread/Posix/Barrier.cpp index 8a4c613626..4f4c7884db 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Barrier.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Barrier.cpp @@ -1,4 +1,5 @@ #include "prx/libc/include/General.hpp" +#include #include #include #include @@ -60,7 +61,7 @@ int APS5_VABI pthread_barrierattr_setpshared_nid_postfix(PthreadBarrierattrPriva int APS5_VABI pthread_barrier_init_nid_postfix(PthreadBarrierPrivate** barrier, PthreadBarrierattrPrivate* const* attr, unsigned count) { (void)attr; - if (!barrier || count == 0) return 22; + if (!barrier || count == 0 || count > INT_MAX) return 22; try { auto* value = new PthreadBarrierPrivate; value->count = count; diff --git a/core/libs/tests/GuestPthreadBarrier.cpp b/core/libs/tests/GuestPthreadBarrier.cpp index 6fbdcb4bad..163c97b14c 100644 --- a/core/libs/tests/GuestPthreadBarrier.cpp +++ b/core/libs/tests/GuestPthreadBarrier.cpp @@ -91,6 +91,8 @@ int main() { GuestBarrier* invalid = nullptr; Require(pthread_barrier_init_nid_postfix(nullptr, &attr, 1) == GUEST_EINVAL); Require(pthread_barrier_init_nid_postfix(&invalid, &attr, 0) == GUEST_EINVAL && invalid == nullptr); + Require(pthread_barrier_init_nid_postfix(&invalid, &attr, 0x80000000u) == GUEST_EINVAL && invalid == nullptr); + Require(pthread_barrier_init_nid_postfix(&invalid, &attr, 0xffffffffu) == GUEST_EINVAL && invalid == nullptr); Context withAttr; Require(pthread_barrier_init_nid_postfix(&withAttr.barrier, &attr, WAITERS) == 0 && withAttr.barrier != nullptr); From 894e9993a2265200e9bae5c093d594ba8b6aadaa Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Tue, 20 Oct 2026 15:56:07 +0200 Subject: [PATCH 039/621] fix(libkernel): reject a negative cond timedwait deadline like libthr _pthread_cond_timedwait fails an abstime with a negative tv_sec with EINVAL before it looks at the condition variable or the mutex. pthread_cond_timedwait took it as a deadline in the past and answered ETIMEDOUT. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libkernel/Pthread/Posix/Cond.cpp | 2 +- core/libs/tests/GuestCondTimedwait.cpp | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/Pthread/Posix/Cond.cpp b/core/libs/prx/libkernel/Pthread/Posix/Cond.cpp index 6dd1e09509..f357cac53a 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Cond.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Cond.cpp @@ -39,7 +39,7 @@ int APS5_VABI pthread_cond_signal_nid_postfix(PthreadCond* cond) { } int APS5_VABI pthread_cond_timedwait_nid_postfix(PthreadCond* cond, PthreadMutex* mutex, const KernelTimespec* abstime) { - if (!abstime || abstime->tv_nsec < 0 || abstime->tv_nsec >= 1000000000) return PosixThread::GUEST_EINVAL; + if (!abstime || abstime->tv_sec < 0 || abstime->tv_nsec < 0 || abstime->tv_nsec >= 1000000000) return PosixThread::GUEST_EINVAL; return toPosix(CondOperations::AbsoluteTimedwait(cond, mutex, abstime)); } diff --git a/core/libs/tests/GuestCondTimedwait.cpp b/core/libs/tests/GuestCondTimedwait.cpp index 91c18ac93e..434d257d2d 100644 --- a/core/libs/tests/GuestCondTimedwait.cpp +++ b/core/libs/tests/GuestCondTimedwait.cpp @@ -18,6 +18,7 @@ int APS5_VABI scePthreadCondTimedwait(PthreadCond* cond, PthreadMutex* mutex, un int APS5_VABI scePthreadCondSignal(PthreadCond* cond); int APS5_VABI pthread_cond_init_nid_postfix(PthreadCond* cond, const PthreadCondattr* attr); int APS5_VABI pthread_cond_destroy_nid_postfix(PthreadCond* cond); +int APS5_VABI pthread_cond_timedwait_nid_postfix(PthreadCond* cond, PthreadMutex* mutex, const KernelTimespec* abstime); int APS5_VABI pthread_cond_reltimedwait_np_nid_postfix(PthreadCond* cond, PthreadMutex* mutex, unsigned int usec); int APS5_VABI __cxa_atexit_nid_postfix(void (APS5_VABI *)(void*), void*, void*); void APS5_VABI __pthread_cxa_finalize_nid_postfix(void*); @@ -31,6 +32,7 @@ static constexpr int SCE_OK = 0; static constexpr int SCE_KERNEL_ERROR_ETIMEDOUT = static_cast(0x8002003C); static constexpr int SCE_KERNEL_ERROR_EBUSY = static_cast(0x80020010); static constexpr int POSIX_EPERM = 1; +static constexpr int POSIX_EINVAL = 22; static constexpr int POSIX_ETIMEDOUT = 60; static void Require(bool value) { if (!value) std::abort(); } @@ -131,6 +133,14 @@ int main() { Require(scePthreadMutexUnlock(&context.mutex) == SCE_OK); Require(scePthreadCondDestroy(&cond) == SCE_OK); Require(pthread_cond_init_nid_postfix(&cond, &unset) == 0); + { + const KernelTimespec negativeSeconds{-1, 0}; + const KernelTimespec negativeNanoseconds{1, -1}; + Require(scePthreadMutexLock(&context.mutex) == SCE_OK); + Require(pthread_cond_timedwait_nid_postfix(&cond, &context.mutex, &negativeSeconds) == POSIX_EINVAL); + Require(pthread_cond_timedwait_nid_postfix(&cond, &context.mutex, &negativeNanoseconds) == POSIX_EINVAL); + Require(scePthreadMutexUnlock(&context.mutex) == SCE_OK); + } Require(pthread_cond_destroy_nid_postfix(&cond) == 0); Require(scePthreadMutexDestroy(&context.mutex) == SCE_OK); 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 040/621] 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 52a64aef65bac29b85c3725ebcaae0eb66bee35f Mon Sep 17 00:00:00 2001 From: MiguelAdolfsen Date: Thu, 15 Oct 2026 03:41:22 +0200 Subject: [PATCH 041/621] perf(agc): refresh only the layers a use of an array color target covers A color view of one slice of a 2D array target refreshed the whole array image before each draw, sampled lookup and present. When the array's unused slices lie over other live targets (ps5-opengl declares every target as a two-slice array and allocates the next one over its second slice), each refresh stored the other targets' pending results, wrote back its own and uploaded the whole image from guest memory again, on every pass. StorageTexture::RefreshLayers refreshes the tracked layers that hold the given array layers alone: it flushes and compares their memory only, and results of other images pending over the other layers stay pending. A layer outside the range whose memory changed or that holds another image's pending results is left behind; GenerationFor reports it as never current, so a use over it takes the whole Refresh. It falls back to Refresh for volumes, single-layer and DCC images, an alias, or results of the image pending outside the range. A draw into an array slice, a 2D view of one layer of an array image and a present of one layer use it. The compare memo of untracked memory is kept per range, so a layer refresh's compare is memoized like the whole image's. Co-Authored-By: Claude Opus 5.5 --- .../Execution/src/VulkanDevice.cpp | 5 +- .../Graphics/include/ShaderResources.hpp | 1 + .../Graphics/include/Texture.hpp | 13 +- .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 5 +- .../Graphics/src/ShaderResources.cpp | 36 ++++-- .../libSceAgcDriver/Graphics/src/Texture.cpp | 116 +++++++++++++++--- .../tests/execution/ArrayLayerTarget.cpp | 47 +++++-- 7 files changed, 182 insertions(+), 41 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index c19c2efa29..b372d843c2 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -3182,8 +3182,9 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) { static const bool noAdopt = std::getenv("APS5_NO_PRESENT_EPOCH") != nullptr; if (!noAdopt) GuestMemory::AdoptLatestCollectEpoch(); GuestMemory::CollectWritesUncached(buffer.address, bytes); - if (!GuestMemory::UnchangedSince(buffer.address, bytes, resident->Generation()) || (buffer.dccAddress != 0 && ResidentKeysMoved(*resident))) { - resident->Refresh(); + if (!GuestMemory::UnchangedSince(buffer.address, bytes, resident->GenerationFor(buffer.address, bytes)) || (buffer.dccAddress != 0 && ResidentKeysMoved(*resident))) { + if (resident->ImageLayers() > 1) resident->RefreshLayers(layer, 1); + else resident->Refresh(); ++refreshedPresents; } ++residentPresents; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index 9999703dc6..bf3f14504e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -52,6 +52,7 @@ bool SpirvStoresThroughAddresses(std::span words); // The cached storage image of a surface (render targets use it as their resident image); brought up // to date with guest memory before it is returned. std::shared_ptr CachedStorageSurface(const Context& context, const GuestTextureResource& resource); +std::shared_ptr CachedStorageLayers(const Context& context, const GuestTextureResource& resource, std::uint32_t firstLayer, std::uint32_t layerCount); // Whether `image` is still the storage cache's image of its surface (what CachedStorageSurface // would return); a true answer counts as a use for the cache's eviction order, as a lookup would. bool StorageImageCached(const Context& context, const StorageTexture* image); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index f54483fcbd..70a910562f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -349,6 +349,7 @@ public: // validates textures copied from this image. std::uint64_t Version() const { return version; } std::uint64_t Generation() const { return generation; } + std::uint64_t GenerationFor(std::uint64_t address, std::uint64_t bytes) const; // The DCC keys the content was uploaded under and the last scan of the surface's keys // (ProvedClearKeys): Refresh's own key rule for the fast revalidation. Under // GuestMemory::GpuMutex only, as Refresh is; never from a build's stage A. @@ -363,6 +364,7 @@ public: // was still current (nothing uploaded). // Keeps the image current with guest memory (see GuestMemory::CollectWrites). bool Refresh(); + bool RefreshLayers(std::uint32_t first, std::uint32_t count); static std::uint64_t SinglePassMoves(); static std::uint64_t SlabRetiles(); static std::uint64_t RefreshesProved(); @@ -473,6 +475,8 @@ private: // layer generation (the whole-surface fast check), `dirty` whether any layer is pending. std::uint64_t layerBegin(std::uint32_t layer) const { return descriptor.baseAddress + static_cast(layer) * trackedLayerBytes; } bool anyLayerPending() const; + bool refresh(std::uint32_t firstLayer, std::uint32_t lastLayer); + void noteLayersBehind(std::uint32_t firstLayer, std::uint32_t lastLayer, const std::vector& outside); void refreshGeneration(); bool refreshProved(); void takeRefreshProof(bool aliased); @@ -549,7 +553,13 @@ private: std::uint64_t sliceLinearBytes = 0; SurfaceGeometry geometry; std::vector original; - mutable std::array comparedGuestBytes{}; + struct ComparedRange { + std::uint64_t address = 0; + std::size_t bytes = 0; + std::array stamp{}; + }; + mutable std::array comparedRanges{}; + mutable std::uint32_t nextComparedRange = 0; std::vector generationBaseline; // DCC keys the image content was uploaded under: a fast-cleared surface starts as its clear value. DccKeys uploadedKeys = DccKeys::Uncompressed; @@ -575,6 +585,7 @@ private: std::uint64_t trackedLayerBytes = 0; std::vector layerGeneration; std::vector layerPending; + std::vector layerBehind; bool blockUnits = false; // Write-back hysteresis (block units, see writeBack): partial stores in the current window of // presents, its first present, and the present until which every pending unit is stored. diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index e0a69a53ac..61f6e4690e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -2234,7 +2234,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& const auto geometry = DescribeSurface(array); for (std::uint32_t slice = 0; slice <= color.arrayLastSlice; ++slice) Require(geometry.GuestLayerOffset(slice) == slice * sliceBytes, "the array image's layer pitch differs from the color view's slice offset"); Require(color.surfaceAddress == color.arrayAddress + color.arraySlice * sliceBytes, "the array image's layer pitch differs from the color view's slice offset"); - auto resident = CachedStorageSurface(context, array); + auto resident = CachedStorageLayers(context, array, color.arraySlice, 1); Require(resident->Attachable(), "storage format cannot be a color attachment"); return resident; } @@ -2833,7 +2833,8 @@ DrawRecipeOutcome DrawWithRecipe(const Context& context, const State& state, con auto stored = recipe.targets[index].lock(); if (stored == nullptr || !StorageImageCached(context, stored.get()) || !StorageImageServesKeys(*stored, state.colors[index].dccAddress)) return miss(DrawRecipeMiss::TargetGone); auto resident = refreshResidentTarget(context, state, state.colors[index], outcome, profile, [&] { - stored->Refresh(); + if (state.colors[index].arrayLastSlice != 0) stored->RefreshLayers(state.colors[index].arraySlice, 1); + else stored->Refresh(); return stored; }); timer.phase(PhaseReadTarget); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 56936290cd..fcb90818cc 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -460,7 +460,12 @@ private: bool outermost; }; -std::shared_ptr cachedStorageTexture(const Context& context, std::span words, const GuestTextureResource& resource, std::uint32_t mip, std::uint64_t guestBytes = 0, bool written = false); +void refreshForView(StorageTexture& source, const GuestTextureResource& resource) { + if (resource.dimension == TextureDimension::k2D && source.ImageLayers() > 1) source.RefreshLayers(resource.baseArray, 1); + else source.Refresh(); +} + +std::shared_ptr cachedStorageTexture(const Context& context, std::span words, const GuestTextureResource& resource, std::uint32_t mip, std::uint64_t guestBytes = 0, bool written = false, std::optional> layers = std::nullopt); std::uint64_t heldBytes(const StorageTexture& image) { return image.BudgetBytes(); @@ -785,9 +790,9 @@ std::shared_ptr cachedTexture(const Context& context, std::spanGeneration())) { + 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. - source->Refresh(); + refreshForView(*source, resource); } // 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 @@ -855,7 +860,7 @@ std::shared_ptr cachedTexture(const Context& context, std::spansource) && (source != nullptr || *keys == DccKeys::Uncompressed) && StorageImageServesKeys(*it->source, resource.dccAddress)) { - if (!GuestMemory::UnchangedSince(address, bytes, it->source->Generation())) it->source->Refresh(); + if (!GuestMemory::UnchangedSince(address, bytes, it->source->GenerationFor(address, bytes))) refreshForView(*it->source, resource); if (it->texture->IsDepthCopy()) it->texture->RefreshDepthCopy(); it->keys = *keys; touchTexture(cache, it); @@ -1102,7 +1107,7 @@ static WrittenStorage WrittenStorageBytes(const GuestTextureResource& viewed, st } // `guestBytes` is the surface size when the caller described the surface already (0: described here). -std::shared_ptr cachedStorageTexture(const Context& context, std::span words, const GuestTextureResource& requested, std::uint32_t mip, std::uint64_t guestBytes, bool written) { +std::shared_ptr cachedStorageTexture(const Context& context, std::span words, const GuestTextureResource& requested, std::uint32_t mip, std::uint64_t guestBytes, bool written, std::optional> layers) { const auto viewed = ArrayLayerWindow(requested, context.limits.maxImageArrayLayers); if (viewed.baseAddress != requested.baseAddress) guestBytes = 0; Require(viewed.dimension != TextureDimension::k2DMsaa, "storage image access to a multisampled texture is not implemented"); @@ -1147,7 +1152,8 @@ std::shared_ptr cachedStorageTexture(const Context& context, std it = cache.entries.end(); } if (it != cache.entries.end()) { - it->texture->Refresh(); + if (layers) it->texture->RefreshLayers(layers->first, layers->second); + else it->texture->Refresh(); cache.entries.splice(cache.entries.begin(), cache.entries, it); texture = it->texture; counters.storageHits.fetch_add(1, std::memory_order_relaxed); @@ -1429,6 +1435,10 @@ std::shared_ptr CachedStorageSurface(const Context& context, con return cachedStorageTexture(context, {}, resource, 0); } +std::shared_ptr CachedStorageLayers(const Context& context, const GuestTextureResource& resource, std::uint32_t firstLayer, std::uint32_t layerCount) { + return cachedStorageTexture(context, {}, resource, 0, 0, false, std::pair{firstLayer, layerCount}); +} + bool StorageImageServesKeys(const StorageTexture& image, std::uint64_t dccAddress) { GuestTextureResource resource{}; resource.dccAddress = dccAddress; @@ -2977,7 +2987,7 @@ bool ShaderResources::fastRevalidate(std::uint64_t serialBefore, std::spanCached()) return fail(FastFail::Evicted); - queries.push_back({address, bytes, source->Generation()}); + queries.push_back({address, bytes, source->GenerationFor(address, bytes)}); images.push_back(source); } else { queries.push_back({address, bytes, surface.generation}); @@ -3005,7 +3015,7 @@ bool ShaderResources::fastRevalidate(std::uint64_t serialBefore, std::spanCached()) return fail(FastFail::Evicted); - queries.push_back({address, bytes, image->Generation()}); + queries.push_back({address, bytes, image->GenerationFor(address, bytes)}); images.push_back(image); } if (!pending.empty()) { @@ -3071,7 +3081,7 @@ bool ShaderResources::fastRevalidateEach() { if (surface.source != nullptr) { // The view follows the image: it needs the image current with guest memory, as the // lookup's Refresh would make it. - if (!StorageImageCached(context, surface.source) || !GuestMemory::UnchangedSince(address, bytes, surface.source->Generation())) return false; + if (!StorageImageCached(context, surface.source) || !GuestMemory::UnchangedSince(address, bytes, surface.source->GenerationFor(address, bytes))) return false; } else if (!GuestMemory::UnchangedSince(address, bytes, surface.generation)) { return false; } @@ -3105,7 +3115,7 @@ bool ShaderResources::fastRevalidateEach() { if (GuestMemory::CollectWrites(address, bytes) == 0) return false; if (PendingStorageOverlaps(address, bytes, image)) return false; if (!StorageImageCached(context, image)) return false; - if (!GuestMemory::UnchangedSince(address, bytes, image->Generation())) return false; + if (!GuestMemory::UnchangedSince(address, bytes, image->GenerationFor(address, bytes))) return false; } // Every check passed: snapshot textures are current at the collects issued before them. Storage // images keep their own generation: nothing was stamped above it, so the blocks a later write @@ -3139,9 +3149,9 @@ ShaderResources::OwnRefreshFallback ShaderResources::refreshOwnObjects(std::span if (found != nullptr && !Texture::CanCopyFrom(*found, surface.resource)) found.reset(); if (found != nullptr) { if (found != source) return OwnRefreshFallback::ForeignView; - if (!GuestMemory::UnchangedSince(address, bytes, source->Generation())) { + if (!GuestMemory::UnchangedSince(address, bytes, source->GenerationFor(address, bytes))) { countOwnRefresh(false); - source->Refresh(); + refreshForView(*source, surface.resource); } return OwnRefreshFallback::Count; } @@ -4514,7 +4524,7 @@ std::shared_ptr ShaderResources::fastTexture(const ImageRecord& record) // keys and stays valid while they are unchanged); a change is the full lookup's to judge. if (keys != record.entryKeys) return nullptr; if (record.source != nullptr) { - if (!StorageImageCached(context, record.source.get()) || !GuestMemory::UnchangedSince(address, bytes, record.source->Generation())) return nullptr; + if (!StorageImageCached(context, record.source.get()) || !GuestMemory::UnchangedSince(address, bytes, record.source->GenerationFor(address, bytes))) return nullptr; if (record.resource.dccAddress != 0 && IsDccClear(record.source->FilledKeys())) return nullptr; // Under fast-clear keys the view holds only while its image's results are still pending over // the surface; flushed, the clear the image cannot see makes the lookup take a snapshot. diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index a2d90b6926..2168c4a4b2 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -2016,11 +2016,31 @@ LookupOutcomes& ThreadLookupOutcomes() { } bool StorageTexture::Refresh() { + return refresh(0, trackedLayers); +} + +bool StorageTexture::RefreshLayers(std::uint32_t first, std::uint32_t count) { + if (count == 0 || geometry.thick || geometry.imageLayers <= 1 || first >= geometry.imageLayers || count > geometry.imageLayers - first || geometry.layerBytes == 0 || trackedLayerBytes == 0 || descriptor.dccAddress != 0) return Refresh(); + const auto begin = geometry.GuestLayerOffset(first); + const auto end = geometry.GuestLayerOffset(first + count - 1) + geometry.layerBytes; + const auto firstLayer = static_cast(begin / trackedLayerBytes); + const auto lastLayer = static_cast(std::min((end + trackedLayerBytes - 1) / trackedLayerBytes, trackedLayers)); + if (firstLayer >= lastLayer || (firstLayer == 0 && lastLayer == trackedLayers)) return Refresh(); + for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) { + if (layerPending[layer] && (layer < firstLayer || layer >= lastLayer)) return Refresh(); + } + if (pendingAlias() != nullptr) return Refresh(); + return refresh(firstLayer, lastLayer); +} + +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; CompletePendingCpuWrites(context, descriptor.baseAddress, static_cast(guestBytes)); if (descriptor.dccAddress != 0) CompletePendingCpuWrites(context, descriptor.dccAddress, DccKeyBytes(guestBytes)); if (refreshProved()) { + layerBehind.clear(); NoteProved(); refreshesProved.fetch_add(1, std::memory_order_relaxed); if (profile) LookupOutcomes::Add(LookupOutcomes::RefreshProved, start); @@ -2042,7 +2062,7 @@ bool StorageTexture::Refresh() { // generation, no results of this image's own there) or dead to its own write-back (tracked and // stamped); an untracked unit is stored whole by that write-back, and a unit pending in both // images takes the old order (store, then this image re-uploads it). - auto alias = pendingAlias(); + auto alias = scoped ? std::shared_ptr{} : pendingAlias(); if (alias != nullptr && !(ProvedClearKeys(descriptor, guestBytes, keyProof) == DccKeys::Uncompressed && HostImportFor(context, descriptor.baseAddress, static_cast(guestBytes)) != nullptr)) alias = nullptr; if (alias != nullptr) { std::vector aliasStamped(trackedLayers); @@ -2055,8 +2075,10 @@ bool StorageTexture::Refresh() { } } } + const auto scopeBegin = scoped ? layerBegin(firstLayer) : descriptor.baseAddress; + const auto scopeBytes = scoped ? static_cast(layerBegin(lastLayer - 1) + layerBytes(lastLayer - 1) - scopeBegin) : static_cast(guestBytes); // No publish: the upload below reads the stored units from the unit shadow itself. - const bool flushed = FlushPending(descriptor.baseAddress, static_cast(guestBytes), alias.get(), "storage refresh", PublishScope::None); + const bool flushed = FlushPending(scopeBegin, scopeBytes, alias.get(), "storage refresh", PublishScope::None); if (profile && flushed) start = LookupOutcomes::Add(LookupOutcomes::PendingFlush, start); // `original` holds the guest bytes the image was last uploaded from or written back as; while the // guest memory and the DCC keys still match, the image content is current. Pages nobody wrote @@ -2068,6 +2090,7 @@ bool StorageTexture::Refresh() { // by a CPU store; scanned once when anything changed. std::vector generations; std::vector stampedBlocks, cpuBlocks; + std::vector outside; // Units taken from the alias's image instead of guest memory. std::vector borrowed; bool anyBorrowed = false; @@ -2083,6 +2106,7 @@ bool StorageTexture::Refresh() { const auto equalsOriginal = [&] { // Named for the [hooksync] attribution: the compare goes through the flush hook. const GuestMemory::ReadSiteScope site(GuestMemory::ReadSite::TextureCompare); + if (scoped) return GuestMemory::EqualsCommitted(scopeBegin, std::span(original).subspan(static_cast(scopeBegin - descriptor.baseAddress), scopeBytes)); return GuestMemory::EqualsCommitted(descriptor.baseAddress, original); }; const auto scanBlocks = [&] { @@ -2091,13 +2115,19 @@ bool StorageTexture::Refresh() { cpuBlocks.assign(generations.size(), 0); tracked = GuestMemory::ChangedBlocks(descriptor.baseAddress, static_cast(guestBytes), generations, stampedBlocks, cpuBlocks); if (!tracked) awaitStoredSave(nullptr); - compared = !tracked && compareUntracked(descriptor.baseAddress, static_cast(guestBytes), stampedBlocks, true); + const auto firstBlock = static_cast(scopeBegin / 65536 - descriptor.baseAddress / 65536); + const auto blocks = static_cast((scopeBegin + scopeBytes - 1) / 65536 - scopeBegin / 65536 + 1); + compared = !tracked && (scoped ? compareUntracked(scopeBegin, scopeBytes, std::span(stampedBlocks).subspan(firstBlock, blocks), true) : compareUntracked(descriptor.baseAddress, static_cast(guestBytes), stampedBlocks, true)); if (compared) { for (const auto& [from, unitGeneration] : keptAtUnwatch) { const auto unit = static_cast((from - descriptor.baseAddress) / trackedLayerBytes); if (keptAtUnwatchHolds(from, unit)) stampedBlocks[static_cast(from / 65536 - descriptor.baseAddress / 65536)] = GuestMemory::BlockWritten; } cpuBlocks = stampedBlocks; + if (scoped) { + std::fill(stampedBlocks.begin(), stampedBlocks.begin() + static_cast(firstBlock), GuestMemory::BlockWritten); + std::fill(stampedBlocks.begin() + static_cast(firstBlock + blocks), stampedBlocks.end(), GuestMemory::BlockWritten); + } } }; const auto keysStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; @@ -2142,6 +2172,15 @@ bool StorageTexture::Refresh() { } unchanged = std::none_of(changed.begin(), changed.end(), [](bool value) { return value; }); } + if (scoped && !keysChanged) { + outside.assign(trackedLayers, false); + for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) { + if ((layer >= firstLayer && layer < lastLayer) || (!changed[layer] && !compared)) continue; + outside[layer] = true; + changed[layer] = false; + } + unchanged = std::none_of(changed.begin(), changed.end(), [](bool value) { return value; }); + } for (std::size_t k = 0; k < cpuBlocks.size(); ++k) { if (cpuBlocks[k] != 0 && (compared || (tracked && generations[k] != 0))) cpuWrote = true; } @@ -2152,9 +2191,10 @@ bool StorageTexture::Refresh() { for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) { if (changed[layer] && layerPending[layer]) pendingChanged = true; } - if (!unchanged && !keysChanged && !pendingChanged && originalHolds(descriptor.baseAddress, static_cast(guestBytes)) && equalsOriginal()) { + if (!unchanged && !keysChanged && !pendingChanged && (scoped ? originalHolds(scopeBegin, scopeBytes) : originalHolds(descriptor.baseAddress, static_cast(guestBytes))) && equalsOriginal()) { unchanged = true; stamped = false; + if (!scoped) outside.clear(); } // Only the direct path uploads the selected layers alone (see upload); the others replace // the whole image, so every pending layer's results are stored first. @@ -2182,11 +2222,16 @@ bool StorageTexture::Refresh() { } } } + const bool outsideChanged = std::any_of(outside.begin(), outside.end(), [](bool value) { return value; }); + if (scoped) noteLayersBehind(firstLayer, lastLayer, outside); + else layerBehind.clear(); if (unchanged) { ++Profile().storageReused; - layerGeneration.assign(trackedLayers, current); + for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) { + if (!outsideChanged || !outside[layer]) layerGeneration[layer] = current; + } refreshGeneration(); - takeRefreshProof(alias != nullptr); + if (layerBehind.empty()) takeRefreshProof(alias != nullptr); if (profile) LookupOutcomes::Add(stamped ? LookupOutcomes::RefreshUnchanged : LookupOutcomes::RefreshCompared, start); return true; } @@ -2389,7 +2434,7 @@ void traceKeyStore(const char* path, const GuestTextureResource& descriptor, std void StorageTexture::captureGuestBytes(const std::vector* layers) { original.resize(static_cast(guestBytes)); - comparedGuestBytes = {}; + comparedRanges = {}; std::vector selected(trackedLayers, true); if (layers != nullptr) selected = *layers; for (const auto& [begin, end] : unitRuns(selected)) GuestMemory::FlushGpuWrites(descriptor.baseAddress + begin, static_cast(end - begin)); @@ -2426,11 +2471,10 @@ bool StorageTexture::compareUntracked(std::uint64_t address, std::size_t bytes, Require(changed.size() == (end - 1) / blockBytes - first + 1, "texture comparison block count differs"); const auto* recorder = Recorder::Active(); const auto epoch = GuestMemory::CollectEpoch(); - const bool whole = address == descriptor.baseAddress && bytes == guestBytes; - const auto hint = memoize && epoch != 0 && whole ? GuestMemory::UntrackedHint(address, bytes) : 0; + const auto hint = memoize && epoch != 0 ? GuestMemory::UntrackedHint(address, bytes) : 0; const std::array stamp{hint != 0, hint != 0 ? hint : epoch, GuestMemory::NewestStamp(address, bytes), GuestMemory::ForgetSerial(), recorder != nullptr ? recorder->NewestWriteNote(address, bytes) : 0}; - const bool cacheable = memoize && epoch != 0 && (stamp[3] & 1u) == 0 && whole; - if (cacheable && comparedGuestBytes == stamp) { + const bool cacheable = memoize && epoch != 0 && (stamp[3] & 1u) == 0; + if (cacheable && std::any_of(comparedRanges.begin(), comparedRanges.end(), [&](const ComparedRange& entry) { return entry.address == address && entry.bytes == bytes && entry.stamp == stamp; })) { std::fill(changed.begin(), changed.end(), GuestMemory::BlockUnchanged); return true; } @@ -2438,10 +2482,14 @@ bool StorageTexture::compareUntracked(std::uint64_t address, std::size_t bytes, const auto saved = std::span(original).subspan(static_cast(address - descriptor.baseAddress), bytes); if (GuestMemory::EqualsCommittedUnsynced(address, saved)) { std::fill(changed.begin(), changed.end(), GuestMemory::BlockUnchanged); - if (cacheable) comparedGuestBytes = stamp; + if (cacheable) { + auto found = std::find_if(comparedRanges.begin(), comparedRanges.end(), [&](const ComparedRange& entry) { return entry.address == address && entry.bytes == bytes; }); + if (found == comparedRanges.end()) found = comparedRanges.begin() + nextComparedRange++ % comparedRanges.size(); + *found = {address, bytes, stamp}; + } return true; } - comparedGuestBytes = {}; + comparedRanges = {}; for (std::size_t index = 0; index < changed.size(); ++index) { const auto begin = std::max(address, (first + index) * blockBytes); const auto stop = std::min(end, (first + index + 1) * blockBytes); @@ -3541,6 +3589,44 @@ void StorageTexture::refreshGeneration() { generation = *std::min_element(layerGeneration.begin(), layerGeneration.end()); } +std::uint64_t StorageTexture::GenerationFor(std::uint64_t address, std::uint64_t bytes) const { + if (layerBehind.empty()) return generation; + std::optional oldest; + for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) { + const auto begin = layerBegin(layer); + if (address >= begin + layerBytes(layer) || begin >= address + bytes) continue; + if (layerBehind[layer]) return 0; + oldest = std::min(oldest.value_or(layerGeneration[layer]), layerGeneration[layer]); + } + return oldest.value_or(generation); +} + +void StorageTexture::noteLayersBehind(std::uint32_t firstLayer, std::uint32_t lastLayer, const std::vector& outside) { + if (layerBehind.size() != trackedLayers) layerBehind.assign(trackedLayers, false); + for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) { + if (layer >= firstLayer && layer < lastLayer) layerBehind[layer] = false; + else if (layer < outside.size() && outside[layer]) layerBehind[layer] = true; + } + const auto layersEnd = layerBegin(lastLayer - 1) + layerBytes(lastLayer - 1); + const std::array, 2> ranges{{{descriptor.baseAddress, layerBegin(firstLayer)}, {layersEnd, descriptor.baseAddress + guestBytes}}}; + for (const auto& [from, to] : ranges) { + if (to <= from) continue; + for (const auto& other : overlappingPending(from, static_cast(to - from))) { + const bool whole = other->descriptor.dccAddress != 0 && other->uploadedKeys != DccKeys::Uncompressed; + for (std::uint32_t unit = 0; unit < other->trackedLayers; ++unit) { + if (!whole && !other->layerPending[unit]) continue; + const auto begin = std::max(from, other->layerBegin(unit)); + const auto end = std::min(to, other->layerBegin(unit) + other->layerBytes(unit)); + if (end <= begin) continue; + const auto first = static_cast((begin - descriptor.baseAddress) / trackedLayerBytes); + const auto last = static_cast(std::min((end - descriptor.baseAddress + trackedLayerBytes - 1) / trackedLayerBytes, trackedLayers)); + for (auto layer = first; layer < last; ++layer) layerBehind[layer] = true; + } + } + } + if (std::none_of(layerBehind.begin(), layerBehind.end(), [](bool behind) { return behind; })) layerBehind.clear(); +} + 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(descriptor.baseAddress), first, count, static_cast(generation)); static const bool eager = std::getenv("APS5_EAGER_WRITEBACK") != nullptr || std::getenv("APS5_NO_TEXTURE_CACHE") != nullptr; @@ -3801,7 +3887,7 @@ void StorageTexture::saveBeforeUnwatch() { const auto runs = unitRuns(units); if (runs.empty()) return; original.resize(static_cast(guestBytes)); - comparedGuestBytes = {}; + comparedRanges = {}; for (const auto& [begin, end] : runs) GuestMemory::ReadCommittedUnsynced(descriptor.baseAddress + begin, std::span(original).subspan(static_cast(begin), static_cast(end - begin))); for (const auto& [begin, end] : runs) GuestMemory::CollectWritesUncached(descriptor.baseAddress + begin, static_cast(end - begin)); constexpr std::uint64_t block = 65536; @@ -3862,7 +3948,7 @@ void StorageTexture::applyStoredSave(std::uint64_t ticket, const std::vector(guestBytes)); - comparedGuestBytes = {}; + comparedRanges = {}; const auto offset = static_cast(layerBegin(unit) - descriptor.baseAddress); std::memcpy(original.data() + offset, stored.data() + offset, static_cast(layerBytes(unit))); layerOriginal[unit] = true; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp index f9e388295e..5d635c3f2c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp @@ -68,6 +68,7 @@ alignas(256) std::array PixelCode{}; alignas(256) std::array Output{}; void SampleSliceZero(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint32_t swizzleMode); +void OverlappingArrays(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint32_t swizzleMode); std::uint64_t AddressOf(const void* data) { return static_cast(reinterpret_cast(data)); @@ -170,10 +171,10 @@ void Load(AgcDriver::VulkanDevice& device, std::span code, Dispatch(device, code, userData); } -void LoadLayer(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint32_t layer, std::uint32_t swizzleMode) { +void LoadLayer(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint32_t layer, std::uint32_t swizzleMode, std::uint32_t lastLayer = Layers - 1u) { auto& code = LoadCodes[layer]; code = {0x7e020280, 0x7e040280u | layer, 0xf0001128, 0x00010300, 0x34080082, 0xbf8c3f70, 0xe0701000, 0x80000304, 0xbf810000}; - Load(device, code, SurfaceDescriptor(surface, swizzleMode, Type2DArray, Layers - 1u)); + Load(device, code, SurfaceDescriptor(surface, swizzleMode, Type2DArray, lastLayer)); } void Sample2D(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint32_t swizzleMode) { @@ -187,23 +188,23 @@ void RequireOutput(std::uint32_t inside, std::uint32_t outside, const std::strin } } -AgcDriver::Graphics::ColorTarget DecodeTarget(std::uint64_t address, std::uint32_t slice, std::uint32_t swizzleMode, bool collapsed) { +AgcDriver::Graphics::ColorTarget DecodeTarget(std::uint64_t address, std::uint32_t slice, std::uint32_t swizzleMode, bool collapsed, std::uint32_t lastSlice) { AgcDriver::Registers cx; cx[0x318] = static_cast(address >> 8u); cx[0x31b] = slice | (slice << 13u); cx[0x31c] = Color32FloatInfo; cx[0x31d] = 0; cx[0x3b0] = ((Side - 1u) << 14u) | (Side - 1u); - cx[0x3b8] = (TiledAttrib3 & ~(0x1fu << 14u)) | (swizzleMode << 14u) | (collapsed ? 0u : Layers - 1u); + cx[0x3b8] = (TiledAttrib3 & ~(0x1fu << 14u)) | (swizzleMode << 14u) | (collapsed ? 0u : lastSlice); cx[0x390] = static_cast(address >> 40u); const auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); Require(color.format == VK_FORMAT_R32_SFLOAT && color.bytes == LayerBytes && color.address == address + slice * LayerBytes, "the COLOR_32 float view did not decode as a 128x128 R32 slice of the array"); if (collapsed) Require(color.arrayAddress == 0 && color.arrayLastSlice == 0, "a collapsed slice view unexpectedly describes a whole array"); - else Require(color.arrayAddress == address && color.arrayLastSlice == Layers - 1u && color.arraySlice == slice, "the color view did not keep its array and slice"); + else Require(color.arrayAddress == address && color.arrayLastSlice == lastSlice && color.arraySlice == slice, "the color view did not keep its array and slice"); return color; } -void DrawSlice(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint32_t slice, std::uint32_t swizzleMode, bool collapsed) { +void DrawSlice(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint32_t slice, std::uint32_t swizzleMode, bool collapsed, std::uint32_t lastSlice = Layers - 1u, std::uint32_t width = ScissorWidth) { const auto target = device.Target(); constexpr std::uint32_t waveSize = 64; std::size_t at = 0; @@ -230,7 +231,7 @@ void DrawSlice(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint const auto vertexResult = ShaderRecompiler::Recompile(vertex); const auto vertexPush = static_cast(vertexResult.pushConstants.size()); - const auto color = DecodeTarget(surface, slice, swizzleMode, collapsed); + const auto color = DecodeTarget(surface, slice, swizzleMode, collapsed, lastSlice); VkPipelineColorBlendAttachmentState blend{}; blend.colorWriteMask = 0xf; AgcDriver::Graphics::State state{}; @@ -242,7 +243,7 @@ void DrawSlice(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint 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}, {ScissorWidth, Side}}; + state.scissor = {{0, 0}, {width, Side}}; state.cullMode = VK_CULL_MODE_NONE; state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; state.blends = {blend}; @@ -296,6 +297,7 @@ void Run(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint32_t sw RequireOutput(drawn, Initial, "slice " + std::to_string(slice) + " uploaded again from guest memory"); AgcDriver::Graphics::ClearCachedTextures(device.Device()); SampleSliceZero(device, texels, swizzleMode); + OverlappingArrays(device, texels, swizzleMode); } void SampleSliceZero(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint32_t swizzleMode) { @@ -354,6 +356,35 @@ void RetiredArrayLayer(AgcDriver::VulkanDevice& device) { Require(untouched, "CPU write-back stored results over a retired allocation"); } +void OverlappingArrays(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint32_t swizzleMode) { + const auto surface = AddressOf(texels); + const auto next = surface + LayerBytes; + const auto drawn = std::bit_cast(Drawn); + std::fill_n(reinterpret_cast(texels), SurfaceBytes / 4u, Initial); + AgcDriver::GuestMemory::CollectWrites(surface, SurfaceBytes); + AgcDriver::GuestMemory::BumpCollectEpoch(); + DrawSlice(device, surface, 0, swizzleMode, false, 1u); + DrawSlice(device, next, 0, swizzleMode, false, 1u, Side); + const auto neighbour = StorageTexture::FindPending(next, 2u * LayerBytes); + Require(neighbour != nullptr && neighbour->Descriptor().baseAddress == next && neighbour->Descriptor().dimension == TextureDimension::k2DArray, "the next target did not render into the image of its own array"); + DrawSlice(device, surface, 0, swizzleMode, false, 1u); + Require(StorageTexture::FindPending(next, 2u * LayerBytes) == neighbour, "a draw into slice 0 of the first array stored the next target's results, which only its unused second slice overlaps"); + Sample2D(device, surface, swizzleMode); + RequireOutput(drawn, Initial, "slice 0 of the first array sampled as a 2D texture after both draws"); + Require(StorageTexture::FindPending(next, 2u * LayerBytes) == neighbour, "a 2D texture over slice 0 of the first array stored the next target's results, which only its unused second slice overlaps"); + LoadLayer(device, next, 0, swizzleMode, 1u); + RequireOutput(drawn, drawn, "slice 0 of the next array after the draw into the first"); + LoadLayer(device, surface, 1, swizzleMode, 1u); + RequireOutput(drawn, drawn, "slice 1 of the first array (the next target's slice 0) read while the next target's results were pending"); + LoadLayer(device, surface, 0, swizzleMode, 1u); + RequireOutput(drawn, Initial, "slice 0 of the first array after both draws"); + LoadLayer(device, next, 1, swizzleMode, 1u); + RequireOutput(Initial, Initial, "slice 1 of the next array, which nothing drew"); + StorageTexture::FlushPending(surface, SurfaceBytes, nullptr, "test"); + device.WaitIdle(); + AgcDriver::Graphics::ClearCachedTextures(device.Device()); +} + } int main() { From 2d677974d144f45504b249744879b441298e2cd6 Mon Sep 17 00:00:00 2001 From: Zenodeon Date: Thu, 15 Oct 2026 18:12:28 +0400 Subject: [PATCH 042/621] test(agc): cover CPU refresh of overlapping array targets --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 7 +++++++ .../tests/execution/ArrayLayerTarget.cpp | 10 ++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 83d79012a9..8a6d7127d1 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2504,6 +2504,13 @@ if(BUILD_TESTING) endif() endforeach() endforeach() + add_test(NAME agc_driver_array_layer_target_cpu COMMAND agc_driver_array_layer_target_tests) + set_tests_properties(agc_driver_array_layer_target_cpu PROPERTIES + ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_HOST_IMPORT=1" TIMEOUT 30 SKIP_RETURN_CODE 77) + if(WIN32) + set_tests_properties(agc_driver_array_layer_target_cpu PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() foreach(preparedMode IN ITEMS indirect uncached debug debug_failure deferred deferred_undecodable registered_workgroup registered_workgroup_sync abi_deferred_compute abi_deferred_pixel abi_deferred_stages abi_deferred_rectangle abi_deferred_undecodable_state abi_deferred_registration_failure abi_deferred_vertex_inputs abi_deferred_rectangle_pairs lazy lazy_sync) set(preparedTest "agc_driver_prepared_shaders_${preparedMode}") if(preparedMode MATCHES "^abi_deferred_") diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp index 5d635c3f2c..d43fd89626 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp @@ -68,7 +68,7 @@ alignas(256) std::array PixelCode{}; alignas(256) std::array Output{}; void SampleSliceZero(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint32_t swizzleMode); -void OverlappingArrays(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint32_t swizzleMode); +void OverlappingArrays(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint32_t swizzleMode, bool cpuWrite = false); std::uint64_t AddressOf(const void* data) { return static_cast(reinterpret_cast(data)); @@ -298,6 +298,7 @@ void Run(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint32_t sw AgcDriver::Graphics::ClearCachedTextures(device.Device()); SampleSliceZero(device, texels, swizzleMode); OverlappingArrays(device, texels, swizzleMode); + OverlappingArrays(device, texels, swizzleMode, true); } void SampleSliceZero(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint32_t swizzleMode) { @@ -356,7 +357,7 @@ void RetiredArrayLayer(AgcDriver::VulkanDevice& device) { Require(untouched, "CPU write-back stored results over a retired allocation"); } -void OverlappingArrays(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint32_t swizzleMode) { +void OverlappingArrays(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint32_t swizzleMode, bool cpuWrite) { const auto surface = AddressOf(texels); const auto next = surface + LayerBytes; const auto drawn = std::bit_cast(Drawn); @@ -367,6 +368,11 @@ void OverlappingArrays(AgcDriver::VulkanDevice& device, std::uint8_t* texels, st DrawSlice(device, next, 0, swizzleMode, false, 1u, Side); const auto neighbour = StorageTexture::FindPending(next, 2u * LayerBytes); Require(neighbour != nullptr && neighbour->Descriptor().baseAddress == next && neighbour->Descriptor().dimension == TextureDimension::k2DArray, "the next target did not render into the image of its own array"); + if (cpuWrite) { + *reinterpret_cast(texels) = std::bit_cast(0.125f); + AgcDriver::GuestMemory::CollectWrites(surface, LayerBytes); + AgcDriver::GuestMemory::BumpCollectEpoch(); + } DrawSlice(device, surface, 0, swizzleMode, false, 1u); Require(StorageTexture::FindPending(next, 2u * LayerBytes) == neighbour, "a draw into slice 0 of the first array stored the next target's results, which only its unused second slice overlaps"); Sample2D(device, surface, swizzleMode); From c25566ca6da65b82e6f6f272df2d2de9c645a35c Mon Sep 17 00:00:00 2001 From: Zenodeon Date: Thu, 15 Oct 2026 18:30:56 +0400 Subject: [PATCH 043/621] perf(agc): upload selected array layers through CPU staging --- .../Graphics/include/Texture.hpp | 3 +- .../libSceAgcDriver/Graphics/src/Texture.cpp | 79 +++++++++++++++---- .../tests/execution/ArrayLayerTarget.cpp | 34 +++++++- 3 files changed, 94 insertions(+), 22 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index 70a910562f..abd019bf76 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -378,9 +378,8 @@ private: 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 CopyRegions(const std::vector* layers = nullptr) const; - // Uploads the surface, or only the tracked layers `layers` selects (the direct path; the others - // upload everything). void upload(const std::vector* layers = nullptr); + std::vector cpuUploadLayers(const std::vector& units) const; void captureGuestBytes(const std::vector* layers); bool originalHolds(std::uint64_t address, std::size_t bytes) const; bool compareUntracked(std::uint64_t address, std::size_t bytes, std::span changed, bool memoize = false) const; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 2168c4a4b2..b109717dbf 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -2196,8 +2196,6 @@ bool StorageTexture::refresh(std::uint32_t firstLayer, std::uint32_t lastLayer) stamped = false; if (!scoped) outside.clear(); } - // Only the direct path uploads the selected layers alone (see upload); the others replace - // the whole image, so every pending layer's results are stored first. direct = keys == DccKeys::Uncompressed && HostImportFor(context, descriptor.baseAddress, static_cast(guestBytes)) != nullptr; if (alias != nullptr && direct) { // The alias's pending units that nothing stamped since its generation (a CPU store or a @@ -2281,6 +2279,7 @@ bool StorageTexture::refresh(std::uint32_t firstLayer, std::uint32_t lastLayer) } std::fprintf(stderr, "[dcc-keys] refresh 0x%llx+0x%llx keys %s -> %s (dirty %d, %zu pending units, %zu cpu-stamped units%s)\n", static_cast(descriptor.baseAddress), static_cast(guestBytes), DccKeysName(uploadedKeys), DccKeysName(keys), dirty ? 1 : 0, pendingUnits, stampedUnits, keyFlip ? ", flip keeps unstamped results" : ""); } + const bool selectedCpu = !direct && keys == DccKeys::Uncompressed && !cpuUploadLayers(changed).empty(); for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) { if (!layerPending[layer]) continue; if (keyFlip && layerGeneration[layer] != 0 && stampedBlocks[layer] != GuestMemory::BlockWritten) { @@ -2299,7 +2298,7 @@ bool StorageTexture::refresh(std::uint32_t firstLayer, std::uint32_t lastLayer) dropped = true; continue; } - } else if (direct) { + } else if (direct || selectedCpu) { continue; } stored[layer] = true; @@ -2501,6 +2500,25 @@ bool StorageTexture::compareUntracked(std::uint64_t address, std::size_t bytes, constexpr std::uint64_t GenerationBaselineBytes = 1ull << 20; +std::vector StorageTexture::cpuUploadLayers(const std::vector& units) const { + if (descriptor.dimension != TextureDimension::k2DArray || descriptor.mipCount != 1 || descriptor.dccAddress != 0 || IsBlockCompressed(descriptor.format) || geometry.thick || arrayLayers <= 1 || geometry.imageLayers != arrayLayers || trackedLayerBytes == 0 || units.size() != trackedLayers) return {}; + std::vector selected(arrayLayers, false); + for (std::uint32_t layer = 0; layer < arrayLayers; ++layer) { + const auto begin = geometry.GuestLayerOffset(layer); + const auto end = begin + geometry.layerBytes; + if (end <= begin || end > guestBytes || begin % trackedLayerBytes != 0 || (end % trackedLayerBytes != 0 && end != guestBytes)) return {}; + const auto first = static_cast(begin / trackedLayerBytes); + const auto last = static_cast((end + trackedLayerBytes - 1) / trackedLayerBytes); + if (first >= last || last > trackedLayers) return {}; + selected[layer] = units[first]; + for (auto unit = first + 1; unit < last; ++unit) { + if (units[unit] != selected[layer]) return {}; + } + } + if (std::none_of(selected.begin(), selected.end(), [](bool value) { return value; }) || std::all_of(selected.begin(), selected.end(), [](bool value) { return value; })) return {}; + return selected; +} + 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{}; @@ -2509,8 +2527,6 @@ void StorageTexture::upload(const std::vector* layers) { CaptureTrace::Log("upload image=%llx bytes=%llu generation=%llu reason=%s partial=%d", static_cast(descriptor.baseAddress), static_cast(guestBytes), static_cast(generation), uploadReason, layers != nullptr); const auto elementBytes = BytesPerElement(descriptor.format); const auto linearBytes = sliceLinearBytes * arrayLayers; - // A layer selection applies to the direct path alone (the others upload everything); it names - // array layers, which the tracked layers are when there are several. if (layers != nullptr && ((!blockUnits && trackedLayers != arrayLayers) || std::all_of(layers->begin(), layers->end(), [](bool selected) { return selected; }))) layers = nullptr; const auto now = GuestMemory::CollectWrites(descriptor.baseAddress, static_cast(guestBytes)); const auto stampLayers = [&](bool selectedOnly) { @@ -2717,15 +2733,36 @@ void StorageTexture::upload(const std::vector* layers) { if (profile) LookupOutcomes::Add(LookupOutcomes::UploadDirect, start); return; } - layerOriginal.assign(trackedLayers, true); - cancelStoredSaves(); - forgetBorrowed(0, trackedLayers); - stampLayers(false); - original.resize(static_cast(guestBytes)); - GuestMemory::ReadCommitted(descriptor.baseAddress, original); + const auto cpuLayers = uploadedKeys == DccKeys::Uncompressed && layers != nullptr ? cpuUploadLayers(*layers) : std::vector{}; + const bool selectedCpu = !cpuLayers.empty(); + std::uint64_t uploadedBytes = guestBytes; + if (selectedCpu) { + captureGuestBytes(layers); + for (std::uint32_t unit = 0; unit < trackedLayers; ++unit) { + if ((*layers)[unit]) forgetBorrowed(unit, 1); + } + stampLayers(true); + uploadedBytes = 0; + for (std::uint32_t layer = 0; layer < arrayLayers; ++layer) { + if (cpuLayers[layer]) uploadedBytes += geometry.layerBytes; + } + } else { + layerOriginal.assign(trackedLayers, true); + cancelStoredSaves(); + forgetBorrowed(0, trackedLayers); + stampLayers(false); + original.resize(static_cast(guestBytes)); + GuestMemory::ReadCommitted(descriptor.baseAddress, original); + } { Buffer staging(context, original.size(), VK_BUFFER_USAGE_TRANSFER_SRC_BIT); - if (uploadedKeys == DccKeys::Uncompressed) std::memcpy(staging.Bytes().data(), original.data(), original.size()); + if (selectedCpu) { + for (std::uint32_t layer = 0; layer < arrayLayers; ++layer) { + if (!cpuLayers[layer]) continue; + const auto offset = geometry.GuestLayerOffset(layer); + std::memcpy(staging.Bytes().data() + offset, original.data() + offset, static_cast(geometry.layerBytes)); + } + } else if (uploadedKeys == DccKeys::Uncompressed) std::memcpy(staging.Bytes().data(), original.data(), original.size()); else ReadTextureSurface(descriptor, uploadedKeys, staging.Bytes().first(original.size())); DeviceBuffer tiled(context, original.size(), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT); DeviceBuffer linear(context, static_cast(linearBytes), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT); @@ -2733,9 +2770,16 @@ void StorageTexture::upload(const std::vector* layers) { CommandBatch batch(context); const auto commands = batch.Handle(); RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); - CopyBuffer(context, commands, staging.Handle(), 0, tiled.Handle(), 0, original.size()); + if (selectedCpu) { + for (std::uint32_t layer = 0; layer < arrayLayers; ++layer) { + if (!cpuLayers[layer]) continue; + const auto offset = geometry.GuestLayerOffset(layer); + CopyBuffer(context, commands, staging.Handle(), offset, tiled.Handle(), offset, geometry.layerBytes); + } + } else CopyBuffer(context, commands, staging.Handle(), 0, tiled.Handle(), 0, original.size()); RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT); for (std::uint32_t layer = 0; layer < arrayLayers; ++layer) { + if (selectedCpu && !cpuLayers[layer]) continue; for (std::uint32_t level = 0; level < mips.size(); ++level) { if (!geometry.HasLayer(level, layer)) continue; const auto& mip = mips[level]; @@ -2744,15 +2788,16 @@ void StorageTexture::upload(const std::vector* layers) { } const auto linearRead = WholeBufferBarrier(linear.Handle(), VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); VkImageMemoryBarrier toTransfer{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + toTransfer.srcAccessMask = selectedCpu ? VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT : 0u; toTransfer.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; - toTransfer.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + toTransfer.oldLayout = selectedCpu ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_UNDEFINED; toTransfer.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; toTransfer.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; toTransfer.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; toTransfer.image = image; toTransfer.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, descriptor.mipCount, 0, geometry.imageLayers}; - context.Function("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 1, &linearRead, 1, &toTransfer); - const auto regions = CopyRegions(); + context.Function("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT | (selectedCpu ? VK_PIPELINE_STAGE_ALL_COMMANDS_BIT : 0u), VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 1, &linearRead, 1, &toTransfer); + const auto regions = CopyRegions(selectedCpu ? &cpuLayers : nullptr); context.Function("vkCmdCopyBufferToImage")(commands, linear.Handle(), image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, static_cast(regions.size()), regions.data()); VkImageMemoryBarrier toGeneral = toTransfer; toGeneral.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; @@ -2764,7 +2809,7 @@ void StorageTexture::upload(const std::vector* layers) { batch.SubmitAndWait(); APS5_LOG_CHARS_OUT("StorageTexture upload done"); } - countStorageUpload(2, guestBytes); + countStorageUpload(2, uploadedBytes); ++version; if (profile) LookupOutcomes::Add(LookupOutcomes::UploadCpu, start); } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp index d43fd89626..5a578e1a4c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp @@ -69,6 +69,7 @@ alignas(256) std::array Output{}; void SampleSliceZero(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint32_t swizzleMode); void OverlappingArrays(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint32_t swizzleMode, bool cpuWrite = false); +void OverlappingArrayLastSlice(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint32_t swizzleMode); std::uint64_t AddressOf(const void* data) { return static_cast(reinterpret_cast(data)); @@ -299,6 +300,7 @@ void Run(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint32_t sw SampleSliceZero(device, texels, swizzleMode); OverlappingArrays(device, texels, swizzleMode); OverlappingArrays(device, texels, swizzleMode, true); + OverlappingArrayLastSlice(device, texels, swizzleMode); } void SampleSliceZero(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint32_t swizzleMode) { @@ -318,6 +320,32 @@ void SampleSliceZero(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std: AgcDriver::Graphics::ClearCachedTextures(device.Device()); } +void OverlappingArrayLastSlice(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint32_t swizzleMode) { + const auto neighbourAddress = AddressOf(texels); + const auto surface = neighbourAddress + LayerBytes; + const auto cpuValue = std::bit_cast(0.125f); + const auto drawn = std::bit_cast(Drawn); + std::fill_n(reinterpret_cast(texels), SurfaceBytes / sizeof(std::uint32_t), Initial); + AgcDriver::GuestMemory::CollectWrites(neighbourAddress, SurfaceBytes); + AgcDriver::GuestMemory::BumpCollectEpoch(); + DrawSlice(device, surface, 1u, swizzleMode, false, 1u); + DrawSlice(device, neighbourAddress, 1u, swizzleMode, false, 1u, Side); + const auto neighbour = StorageTexture::FindPending(neighbourAddress, 2u * LayerBytes); + Require(neighbour != nullptr && neighbour->Descriptor().baseAddress == neighbourAddress, "the preceding target has no pending results"); + std::fill_n(reinterpret_cast(texels + 2u * LayerBytes), LayerBytes / sizeof(std::uint32_t), cpuValue); + AgcDriver::GuestMemory::CollectWrites(surface + LayerBytes, LayerBytes); + AgcDriver::GuestMemory::BumpCollectEpoch(); + DrawSlice(device, surface, 1u, swizzleMode, false, 1u); + Require(StorageTexture::FindPending(neighbourAddress, 2u * LayerBytes) == neighbour, "CPU refresh of the last slice stored the preceding target's results"); + LoadLayer(device, surface, 1u, swizzleMode, 1u); + RequireOutput(drawn, cpuValue, "the refreshed last slice keeps the CPU's untouched pixels"); + LoadLayer(device, surface, 0u, swizzleMode, 1u); + RequireOutput(drawn, drawn, "the first slice sees the preceding target's pending results"); + StorageTexture::FlushPending(neighbourAddress, SurfaceBytes, nullptr, "test"); + device.WaitIdle(); + AgcDriver::Graphics::ClearCachedTextures(device.Device()); +} + void UnregisteredUnusedLayer(AgcDriver::VulkanDevice& device, std::uint32_t slice) { GuestBlock block(LayerBytes, slice * LayerBytes); auto* texels = block.Data(); @@ -369,21 +397,21 @@ void OverlappingArrays(AgcDriver::VulkanDevice& device, std::uint8_t* texels, st const auto neighbour = StorageTexture::FindPending(next, 2u * LayerBytes); Require(neighbour != nullptr && neighbour->Descriptor().baseAddress == next && neighbour->Descriptor().dimension == TextureDimension::k2DArray, "the next target did not render into the image of its own array"); if (cpuWrite) { - *reinterpret_cast(texels) = std::bit_cast(0.125f); + std::fill_n(reinterpret_cast(texels), LayerBytes / sizeof(std::uint32_t), std::bit_cast(0.125f)); AgcDriver::GuestMemory::CollectWrites(surface, LayerBytes); AgcDriver::GuestMemory::BumpCollectEpoch(); } DrawSlice(device, surface, 0, swizzleMode, false, 1u); Require(StorageTexture::FindPending(next, 2u * LayerBytes) == neighbour, "a draw into slice 0 of the first array stored the next target's results, which only its unused second slice overlaps"); Sample2D(device, surface, swizzleMode); - RequireOutput(drawn, Initial, "slice 0 of the first array sampled as a 2D texture after both draws"); + RequireOutput(drawn, cpuWrite ? std::bit_cast(0.125f) : Initial, "slice 0 of the first array sampled as a 2D texture after both draws"); Require(StorageTexture::FindPending(next, 2u * LayerBytes) == neighbour, "a 2D texture over slice 0 of the first array stored the next target's results, which only its unused second slice overlaps"); LoadLayer(device, next, 0, swizzleMode, 1u); RequireOutput(drawn, drawn, "slice 0 of the next array after the draw into the first"); LoadLayer(device, surface, 1, swizzleMode, 1u); RequireOutput(drawn, drawn, "slice 1 of the first array (the next target's slice 0) read while the next target's results were pending"); LoadLayer(device, surface, 0, swizzleMode, 1u); - RequireOutput(drawn, Initial, "slice 0 of the first array after both draws"); + RequireOutput(drawn, cpuWrite ? std::bit_cast(0.125f) : Initial, "slice 0 of the first array after both draws"); LoadLayer(device, next, 1, swizzleMode, 1u); RequireOutput(Initial, Initial, "slice 1 of the next array, which nothing drew"); StorageTexture::FlushPending(surface, SurfaceBytes, nullptr, "test"); From de0cf03efe0333d62f3aa6dbfd2b58a8d930aedf Mon Sep 17 00:00:00 2001 From: Zenodeon Date: Fri, 16 Oct 2026 01:32:36 +0400 Subject: [PATCH 044/621] test(agc): cover array slices beside uncommitted memory --- .../tests/execution/ArrayLayerTarget.cpp | 45 +++++++++++++++++-- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp index 5a578e1a4c..18a3373738 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp @@ -83,22 +83,24 @@ std::string Hex(std::uint32_t value) { class GuestBlock { public: - explicit GuestBlock(std::size_t registeredBytes = SurfaceBytes, std::size_t registeredOffset = 0) : registeredOffset(registeredOffset) { + explicit GuestBlock(std::size_t registeredBytes = SurfaceBytes, std::size_t registeredOffset = 0, bool commitAll = true) : registeredOffset(registeredOffset) { #ifdef _WIN32 block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(SurfaceBytes, LayerBytes)); - if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block, SurfaceBytes, PAGE_READWRITE, SurfaceBytes); + if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block + (commitAll ? 0 : registeredOffset), commitAll ? SurfaceBytes : registeredBytes, PAGE_READWRITE, commitAll ? SurfaceBytes : registeredBytes); #else - void* mapped = mmap(nullptr, SurfaceBytes + LayerBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + void* mapped = mmap(nullptr, SurfaceBytes + LayerBytes, commitAll ? PROT_READ | PROT_WRITE : PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (mapped != MAP_FAILED) { const auto begin = reinterpret_cast(mapped); const auto aligned = (begin + LayerBytes - 1) & ~(static_cast(LayerBytes) - 1); if (aligned != begin) munmap(mapped, aligned - begin); if (aligned + SurfaceBytes != begin + SurfaceBytes + LayerBytes) munmap(reinterpret_cast(aligned + SurfaceBytes), begin + LayerBytes - aligned); block = reinterpret_cast(aligned); - if (AgcDriver::GuestMemory::WriteWatched()) GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, SurfaceBytes); + if (!commitAll) Require(mprotect(block + registeredOffset, registeredBytes, PROT_READ | PROT_WRITE) == 0, "cannot commit the active array slice"); + if (AgcDriver::GuestMemory::WriteWatched()) GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block + (commitAll ? 0 : registeredOffset), commitAll ? SurfaceBytes : registeredBytes); } #endif Require(block != nullptr, "array layer target: cannot allocate the guest block"); + if (!commitAll) GuestAllocations::GuestAllocationsInvalidate_nid_postfix(reinterpret_cast(block), SurfaceBytes); GuestAllocations::Mutation().Add(block + registeredOffset, registeredBytes, true, true, true); } @@ -385,6 +387,39 @@ void RetiredArrayLayer(AgcDriver::VulkanDevice& device) { Require(untouched, "CPU write-back stored results over a retired allocation"); } +void UncommittedUnusedLayers(AgcDriver::VulkanDevice& device, std::uint32_t slice) { + GuestBlock block(LayerBytes, slice * LayerBytes, false); + const auto surface = AddressOf(block.Data()); + const auto active = surface + slice * LayerBytes; + auto* texels = reinterpret_cast(active); + std::fill_n(texels, LayerBytes / sizeof(std::uint32_t), Initial); + Require(AgcDriver::GuestMemory::CollectWrites(active, LayerBytes) != 0, "the committed array slice has no write history"); + Require(AgcDriver::GuestMemory::CollectWrites(surface, SurfaceBytes) == 0, "the unused array slices unexpectedly have complete write history"); + AgcDriver::GuestMemory::BumpCollectEpoch(); + DrawSlice(device, surface, slice, TileR64KBX, false); + const auto pending = StorageTexture::FindPending(surface, SurfaceBytes); + Require(pending != nullptr, "the committed array slice has no pending draw results"); + Require(pending->GenerationFor(active, LayerBytes) != 0, "an uncommitted unused slice erased the active slice's write history"); + const auto version = pending->Version(); + for (unsigned repeat = 0; repeat < 3; ++repeat) { + AgcDriver::GuestMemory::BumpCollectEpoch(); + Require(pending->RefreshLayers(slice, 1), "an unchanged committed slice was uploaded again"); + Require(pending->GenerationFor(active, LayerBytes) != 0 && pending->Version() == version, "repeated refresh erased the committed slice's write history"); + Require(StorageTexture::FindPending(surface, SurfaceBytes) == pending, "refresh settled unchanged GPU results for an uncommitted unused slice"); + } + const auto cpu = std::bit_cast(0.125f); + std::fill_n(texels, LayerBytes / sizeof(std::uint32_t), cpu); + AgcDriver::GuestMemory::BumpCollectEpoch(); + Require(!pending->RefreshLayers(slice, 1), "the active array slice ignored a genuine CPU write"); + LoadLayer(device, surface, slice, TileR64KBX); + RequireOutput(cpu, cpu, "a committed array slice after a genuine CPU write"); + DrawSlice(device, surface, slice, TileR64KBX, false); + StorageTexture::FlushPending(active, LayerBytes, nullptr, "test"); + device.WaitIdle(); + Require(std::count(texels, texels + LayerBytes / sizeof(std::uint32_t), std::bit_cast(Drawn)) == ScissorWidth * Side, "write-back lost results in the committed array slice"); + AgcDriver::Graphics::ClearCachedTextures(device.Device()); +} + void OverlappingArrays(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint32_t swizzleMode, bool cpuWrite) { const auto surface = AddressOf(texels); const auto next = surface + LayerBytes; @@ -432,6 +467,8 @@ int main() { UnregisteredUnusedLayer(*device, 0u); UnregisteredUnusedLayer(*device, Layers - 1u); RetiredArrayLayer(*device); + UncommittedUnusedLayers(*device, 0u); + UncommittedUnusedLayers(*device, Layers - 1u); std::puts("array layer target tests passed"); return 0; } catch (const std::exception& error) { From 2421f6cfa00ec25a14c673bd54aac7f2244ce1bc Mon Sep 17 00:00:00 2001 From: Zenodeon Date: Fri, 16 Oct 2026 01:41:02 +0400 Subject: [PATCH 045/621] fix(agc): retain write history for committed texture units --- .../Graphics/include/Texture.hpp | 1 + .../libSceAgcDriver/Graphics/src/Texture.cpp | 40 ++++++++++++++----- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index abd019bf76..4227e5e16f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -477,6 +477,7 @@ private: bool refresh(std::uint32_t firstLayer, std::uint32_t lastLayer); void noteLayersBehind(std::uint32_t firstLayer, std::uint32_t lastLayer, const std::vector& outside); void refreshGeneration(); + std::uint64_t collectedLayerGeneration(std::uint32_t layer, std::uint64_t whole, bool uncached = false) const; bool refreshProved(); void takeRefreshProof(bool aliased); bool otherPendingOverlaps() const; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index b109717dbf..ea981b6529 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -2084,6 +2084,16 @@ bool StorageTexture::refresh(std::uint32_t firstLayer, std::uint32_t lastLayer) // guest memory and the DCC keys still match, the image content is current. Pages nobody wrote // since `generation` need no comparison. const auto current = GuestMemory::CollectWrites(descriptor.baseAddress, static_cast(guestBytes)); + const auto scopedCurrent = current != 0 || !scoped ? current : GuestMemory::CollectWrites(scopeBegin, scopeBytes); + std::vector collected; + if (scopedCurrent == 0) { + collected.resize(trackedLayers); + for (auto layer = firstLayer; layer < lastLayer; ++layer) collected[layer] = collectedLayerGeneration(layer, 0); + } + const auto currentFor = [&](std::uint32_t layer) { + if (scoped && (layer < firstLayer || layer >= lastLayer)) return current; + return scopedCurrent != 0 ? scopedCurrent : collected[layer]; + }; // Per tracked layer: changed since its generation (a keys change makes every layer stale). std::vector changed(trackedLayers, false); // Per 64 KiB block of the surface (blockGenerations): stamped since its layer's generation, and @@ -2146,7 +2156,7 @@ bool StorageTexture::refresh(std::uint32_t firstLayer, std::uint32_t lastLayer) scanBlocks(); for (std::uint32_t unit = 0; unit < trackedLayers; ++unit) { if (stampedBlocks[unit] == 0) { - layerGeneration[unit] = current; + layerGeneration[unit] = currentFor(unit); continue; } changed[unit] = true; @@ -2226,7 +2236,7 @@ bool StorageTexture::refresh(std::uint32_t firstLayer, std::uint32_t lastLayer) if (unchanged) { ++Profile().storageReused; for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) { - if (!outsideChanged || !outside[layer]) layerGeneration[layer] = current; + if (!outsideChanged || !outside[layer]) layerGeneration[layer] = currentFor(layer); } refreshGeneration(); if (layerBehind.empty()) takeRefreshProof(alias != nullptr); @@ -2320,7 +2330,7 @@ bool StorageTexture::refresh(std::uint32_t firstLayer, std::uint32_t lastLayer) uploadReason = "alias"; borrowUnits(*alias, borrowed); for (std::uint32_t unit = 0; unit < trackedLayers; ++unit) { - if (borrowed[unit]) layerGeneration[unit] = current; + if (borrowed[unit]) layerGeneration[unit] = currentFor(unit); } refreshGeneration(); } @@ -2538,7 +2548,7 @@ void StorageTexture::upload(const std::vector* layers) { } for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) { if (!whole && !(*layers)[layer]) continue; - layerGeneration[layer] = now; + layerGeneration[layer] = collectedLayerGeneration(layer, now); if (whole || generationBaseline.size() != guestBytes) continue; const auto offset = static_cast(layerBegin(layer) - descriptor.baseAddress); GuestMemory::ReadCommitted(layerBegin(layer), std::span(generationBaseline).subspan(offset, static_cast(layerBytes(layer)))); @@ -3635,17 +3645,24 @@ void StorageTexture::refreshGeneration() { } std::uint64_t StorageTexture::GenerationFor(std::uint64_t address, std::uint64_t bytes) const { - if (layerBehind.empty()) return generation; + if (layerBehind.empty() && generation != 0) return generation; std::optional oldest; for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) { const auto begin = layerBegin(layer); if (address >= begin + layerBytes(layer) || begin >= address + bytes) continue; - if (layerBehind[layer]) return 0; + if (!layerBehind.empty() && layerBehind[layer]) return 0; oldest = std::min(oldest.value_or(layerGeneration[layer]), layerGeneration[layer]); } return oldest.value_or(generation); } +std::uint64_t StorageTexture::collectedLayerGeneration(std::uint32_t layer, std::uint64_t whole, bool uncached) const { + if (whole != 0 || trackedLayers <= 1) return whole; + const auto begin = layerBegin(layer); + const auto bytes = static_cast(layerBytes(layer)); + return uncached ? GuestMemory::CollectWritesUncached(begin, bytes) : GuestMemory::CollectWrites(begin, bytes); +} + void StorageTexture::noteLayersBehind(std::uint32_t firstLayer, std::uint32_t lastLayer, const std::vector& outside) { if (layerBehind.size() != trackedLayers) layerBehind.assign(trackedLayers, false); for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) { @@ -4630,7 +4647,8 @@ bool StorageTexture::clearByKeysFill(DccKeys keys, std::uint8_t key) { filledKeys = DccKeys::Uncompressed; keyProof = {}; forgetBorrowed(0, trackedLayers); - layerGeneration.assign(trackedLayers, GuestMemory::CollectWrites(descriptor.baseAddress, static_cast(guestBytes))); + const auto now = GuestMemory::CollectWrites(descriptor.baseAddress, static_cast(guestBytes)); + for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) layerGeneration[layer] = collectedLayerGeneration(layer, now); refreshGeneration(); ++version; if (traceKeys) std::fprintf(stderr, "[dcc-keys] key fill %s clears 0x%llx+0x%llx at once (keys 0x%llx, %zu pending units dropped, dirty %d)\n", DccKeysName(keys), static_cast(descriptor.baseAddress), static_cast(guestBytes), static_cast(descriptor.dccAddress), droppedUnits, wasDirty ? 1 : 0); @@ -4791,7 +4809,8 @@ bool StorageTexture::FillClear(std::span pattern, std::u return true; } if (!watched) captureGuestBytes(nullptr); - layerGeneration.assign(trackedLayers, GuestMemory::CollectWritesUncached(descriptor.baseAddress, static_cast(guestBytes))); + const auto now = GuestMemory::CollectWritesUncached(descriptor.baseAddress, static_cast(guestBytes)); + for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) layerGeneration[layer] = collectedLayerGeneration(layer, now, true); refreshGeneration(); forgetBorrowed(0, trackedLayers); MarkDirty(); @@ -4904,7 +4923,8 @@ bool StorageTexture::CopyFrom(StorageTexture& source, const char*& refusal) { captureGuestBytes(nullptr); } uploadedKeys = DccKeys::Uncompressed; - layerGeneration.assign(trackedLayers, GuestMemory::CollectWritesUncached(descriptor.baseAddress, static_cast(guestBytes))); + const auto now = GuestMemory::CollectWritesUncached(descriptor.baseAddress, static_cast(guestBytes)); + for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) layerGeneration[layer] = collectedLayerGeneration(layer, now, true); refreshGeneration(); forgetBorrowed(0, trackedLayers); MarkDirty(); @@ -5269,7 +5289,7 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { if (!advances && adjacent.empty() && shadowed.empty()) return; const auto now = memoizedCollect ? GuestMemory::CollectWrites(firstStored, static_cast(lastStored - firstStored)) : GuestMemory::CollectWritesUncached(firstStored, static_cast(lastStored - firstStored)); for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) { - if (layers[layer] && !skippedLayer[layer]) layerGeneration[layer] = now; + if (layers[layer] && !skippedLayer[layer]) layerGeneration[layer] = collectedLayerGeneration(layer, now, !memoizedCollect); } refreshGeneration(); advanceAdjacent(adjacent, now, firstBlock, lastBlock); From 5012f736d02792fcbdbaa8cc0fbf6fb644d82371 Mon Sep 17 00:00:00 2001 From: Zenodeon Date: Fri, 16 Oct 2026 02:12:40 +0400 Subject: [PATCH 046/621] test(agc): scope unit history checks to write-watched hosts --- .../prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp index 18a3373738..0d0c8a865e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp @@ -388,6 +388,7 @@ void RetiredArrayLayer(AgcDriver::VulkanDevice& device) { } void UncommittedUnusedLayers(AgcDriver::VulkanDevice& device, std::uint32_t slice) { + if (!AgcDriver::GuestMemory::WriteWatched()) return; GuestBlock block(LayerBytes, slice * LayerBytes, false); const auto surface = AddressOf(block.Data()); const auto active = surface + slice * LayerBytes; From b8f6d316a96799c06bd4970adfd812c91cd0a0bb Mon Sep 17 00:00:00 2001 From: Zenodeon Date: Sat, 17 Oct 2026 23:40:31 +0400 Subject: [PATCH 047/621] test(agc): expose cached write history across allocation replacement --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 11 +++++++ .../prx/libSceAgcDriver/tests/Recorder.cpp | 31 +++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 8a6d7127d1..2782537b10 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1372,6 +1372,17 @@ if(BUILD_TESTING) set_tests_properties(agc_unchanged_cpu_stamps PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + foreach(mode IN ITEMS map ring shared uncached) + add_test(NAME agc_collect_mapping_history_${mode} COMMAND agc_driver_recorder_tests --collect-mapping-history-only) + set_tests_properties(agc_collect_mapping_history_${mode} PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_collect_mapping_history_${mode} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() + set_tests_properties(agc_collect_mapping_history_ring PROPERTIES ENVIRONMENT "APS5_NO_EPOCH_COLLECT_MAP=1") + set_tests_properties(agc_collect_mapping_history_shared PROPERTIES ENVIRONMENT "APS5_SHARED_COLLECT_MEMO=1") + set_tests_properties(agc_collect_mapping_history_uncached PROPERTIES ENVIRONMENT "APS5_NO_COLLECT_MEMO=1") add_test(NAME agc_storage_kept_unit_settle COMMAND agc_driver_recorder_tests --kept-unit-settle-only) set_tests_properties(agc_storage_kept_unit_settle PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) if(WIN32) diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 3c749730b7..5edb320d3a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -117,6 +117,32 @@ void ReleaseWatched(void* block, std::size_t bytes) { #endif } +bool collectMappingHistoryTests() { + constexpr std::size_t bytes = 131072; + auto* memory = static_cast(AllocateWatched(bytes, 65536)); + if (memory == nullptr) return false; + GuestAllocations::Mutation().Add(memory, bytes, true, true, false); + struct Cleanup { + std::uint8_t* memory; + ~Cleanup() { + GuestAllocations::Mutation().Remove(memory); + ReleaseWatched(memory, bytes); + } + } cleanup{memory}; + const auto address = reinterpret_cast(memory); + std::memset(memory, 0x11, bytes); + AgcDriver::GuestMemory::BumpCollectEpoch(); + const auto before = AgcDriver::GuestMemory::CollectWrites(address, bytes); + Require(before != 0, "the mapping fixture has no complete write history"); + memory[4096] = 0x22; + GuestAllocations::Mutation().Remove(memory); + GuestAllocations::Mutation().Add(memory, bytes, true, true, false); + Require(AgcDriver::GuestMemory::CollectWrites(address, bytes) != 0, "the replacement mapping lost write tracking"); + Require(!AgcDriver::GuestMemory::UnchangedSince(address, bytes, before), "a cached collect hid a CPU store across allocation replacement"); + Require(memory[4096] == 0x22, "collecting the replacement mapping changed its CPU bytes"); + return true; +} + std::optional selectDevice(std::span devices, std::string_view request) { const auto rankDeviceType = [](VkPhysicalDeviceType type) { switch (type) { @@ -9641,6 +9667,11 @@ void Run(Context context) { int main(int argc, char** argv) { try { + if (argc == 2 && std::string_view(argv[1]) == "--collect-mapping-history-only") { + if (!collectMappingHistoryTests()) return 77; + std::cout << "collect mapping history tests passed\n"; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--device-selection-only") { deviceSelectionTests(); std::cout << "recorder device selection tests passed\n"; From c19dcecfec8954233d0f22da969422c8eed7ac4f Mon Sep 17 00:00:00 2001 From: Zenodeon Date: Sat, 17 Oct 2026 23:42:11 +0400 Subject: [PATCH 048/621] fix(agc): expire collected write proofs on allocation changes --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 8 ++++ .../Execution/src/GuestMemory.cpp | 19 +++++---- .../prx/libSceAgcDriver/tests/Recorder.cpp | 40 +++++++++++++++++++ 3 files changed, 59 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 2782537b10..3d086d3763 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1383,6 +1383,14 @@ if(BUILD_TESTING) set_tests_properties(agc_collect_mapping_history_ring PROPERTIES ENVIRONMENT "APS5_NO_EPOCH_COLLECT_MAP=1") set_tests_properties(agc_collect_mapping_history_shared PROPERTIES ENVIRONMENT "APS5_SHARED_COLLECT_MEMO=1") set_tests_properties(agc_collect_mapping_history_uncached PROPERTIES ENVIRONMENT "APS5_NO_COLLECT_MEMO=1") + if(WIN32) + foreach(mode IN ITEMS cached uncached) + add_test(NAME agc_hint_mapping_history_${mode} COMMAND agc_driver_recorder_tests --hint-mapping-history-only) + set_tests_properties(agc_hint_mapping_history_${mode} PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30 + ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endforeach() + set_tests_properties(agc_hint_mapping_history_uncached PROPERTIES ENVIRONMENT "APS5_NO_COLLECT_MEMO=1") + endif() add_test(NAME agc_storage_kept_unit_settle COMMAND agc_driver_recorder_tests --kept-unit-settle-only) set_tests_properties(agc_storage_kept_unit_settle PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) if(WIN32) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index 36d57da9c9..a937f56111 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -873,6 +873,7 @@ struct WriteTracker { std::uintptr_t end = 0; std::uint64_t epoch = 0; std::uint64_t unwatched = 0; + std::uint64_t mapping = 0; }; std::array memo{}; std::size_t nextMemo = 0; @@ -1347,19 +1348,20 @@ std::uint64_t collectWrites(std::uint64_t address, std::size_t bytes, bool memoi // must not differ between them. const auto epoch = currentCollectEpoch(); const auto unwatched = unwatchSerial.load(std::memory_order_acquire); + const auto mapping = GuestAllocations::GuestAllocationsGeneration_nid_postfix(); const bool useMemo = memoized && collectMemoEnabled() && bytes != 0; if (useMemo && !sharedCollectMemo()) { // An entry exists only for a completed walk of an in-arena range, so the tracker is // initialized and watched; nothing below the lock needs asking. if (epochCollectMapEnabled()) { const auto& map = epochCollectMap(epoch); - if (const auto found = map.ranges.find(first); found != map.ranges.end() && found->second.unwatched == unwatched && stop <= found->second.end) { + 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); } } for (const auto& entry : threadCollectMemo.entries) { - if (entry.epoch == epoch && entry.unwatched == unwatched && entry.begin <= first && stop <= entry.end) { + 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 // make every later UnchangedSince fail until the epoch ends. @@ -1374,7 +1376,7 @@ std::uint64_t collectWrites(std::uint64_t address, std::size_t bytes, bool memoi auto cursor = first; if (useMemo && sharedCollectMemo()) { for (const auto& entry : tracker.memo) { - if (entry.epoch == epoch && entry.unwatched == unwatchSerial.load(std::memory_order_relaxed) && entry.begin <= cursor && stop <= entry.end) { + if (entry.epoch == epoch && entry.unwatched == unwatchSerial.load(std::memory_order_relaxed) && entry.mapping == mapping && entry.begin <= cursor && stop <= entry.end) { collectMemoHits.fetch_add(1, std::memory_order_relaxed); return tracker.generation; } @@ -1395,12 +1397,12 @@ std::uint64_t collectWrites(std::uint64_t address, std::size_t bytes, bool memoi // Only a completed walk is remembered; a failed one (uncommitted pages) returned 0 above. if (collectMemoEnabled()) { const auto serial = unwatchSerial.load(std::memory_order_relaxed); - if (sharedCollectMemo()) tracker.memo[tracker.nextMemo++ % tracker.memo.size()] = {first, stop, epoch, serial}; + 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}; + threadCollectMemo.entries[threadCollectMemo.next++ % threadCollectMemo.entries.size()] = {first, stop, epoch, serial, mapping}; if (epochCollectMapEnabled()) { auto& entry = epochCollectMap(epoch).ranges[first]; - if (entry.unwatched != serial || entry.end < stop) entry = {first, stop, epoch, serial}; + if (entry.unwatched != serial || entry.mapping != mapping || entry.end < stop) entry = {first, stop, epoch, serial, mapping}; } } } @@ -1826,15 +1828,16 @@ std::uint64_t UntrackedHint(std::uint64_t address, std::size_t bytes) { tracker.initialize(); if (!tracker.watched || !tracker.hinted(first, stop)) return 0; const auto unwatched = unwatchSerial.load(std::memory_order_relaxed); + const auto mapping = GuestAllocations::GuestAllocationsGeneration_nid_postfix(); const bool memoized = collectMemoEnabled() && std::any_of(threadHintMemo.entries.begin(), threadHintMemo.entries.end(), [&](const WriteTracker::Memo& entry) { - return entry.epoch == epoch && entry.unwatched == unwatched && entry.begin <= first && stop <= entry.end; + return entry.epoch == epoch && entry.unwatched == unwatched && entry.mapping == mapping && entry.begin <= first && stop <= entry.end; }); if (memoized) { collectMemoHits.fetch_add(1, std::memory_order_relaxed); } else { const TimedAccess timed(CounterCollect, bytes); if (!walkHints(tracker, first, stop)) return 0; - if (collectMemoEnabled()) threadHintMemo.entries[threadHintMemo.next++ % threadHintMemo.entries.size()] = {first, stop, epoch, unwatched}; + if (collectMemoEnabled()) threadHintMemo.entries[threadHintMemo.next++ % threadHintMemo.entries.size()] = {first, stop, epoch, unwatched, mapping}; } return tracker.newestHint(first, stop); #else diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 5edb320d3a..3aee54c8fc 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -169,6 +169,41 @@ std::optional selectDevice(std::span(AllocateWatched(bytes, 65536)); + if (memory == nullptr) return false; + GuestAllocations::Mutation().Add(memory, bytes, true, true, false); + struct Cleanup { + std::uint8_t* memory; + ~Cleanup() { + GuestAllocations::Mutation().Remove(memory); + ReleaseWatched(memory, bytes); + } + } cleanup{memory}; + const auto address = reinterpret_cast(memory); + std::memset(memory, 0x11, bytes); + AgcDriver::GuestMemory::BumpCollectEpoch(); + AgcDriver::GuestMemory::Unwatch(address, bytes); + const auto before = AgcDriver::GuestMemory::UntrackedHint(address, bytes); + Require(before != 0, "the hint mapping fixture has no write hint"); + Require(AgcDriver::GuestMemory::UntrackedHint(address, bytes) == before, "an unchanged hint range advanced its history"); + memory[4096] = 0x22; + GuestAllocations::Mutation().Remove(memory); + GuestAllocations::Mutation().Add(memory, bytes, true, true, false); + const auto after = AgcDriver::GuestMemory::UntrackedHint(address, bytes); + Require(after > before, "a cached hint walk hid a CPU store across allocation replacement"); + Require(memory[4096] == 0x22 && memory[bytes - 4096] == 0x11, "collecting replacement hints changed guest bytes"); + GuestAllocations::Mutation().Remove(memory); + GuestAllocations::Mutation().Add(memory, bytes, true, true, false); + Require(AgcDriver::GuestMemory::UntrackedHint(address, bytes) == after, "allocation replacement invented a CPU write hint"); + return true; +#else + return false; +#endif +} + void deviceSelectionTests() { const auto properties = [](const char* name, VkPhysicalDeviceType type) { VkPhysicalDeviceProperties result{}; @@ -9672,6 +9707,11 @@ int main(int argc, char** argv) { std::cout << "collect mapping history tests passed\n"; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--hint-mapping-history-only") { + if (!hintMappingHistoryTests()) return 77; + std::cout << "hint mapping history tests passed\n"; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--device-selection-only") { deviceSelectionTests(); std::cout << "recorder device selection tests passed\n"; From 9fe1f963c8f1849478d86dc77e23cfcc4be1278e Mon Sep 17 00:00:00 2001 From: Zenodeon Date: Sat, 17 Oct 2026 23:42:12 +0400 Subject: [PATCH 049/621] test(agc): verify incomplete array history against the backend --- .../prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp index 0d0c8a865e..aed0ffc37a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp @@ -395,6 +395,7 @@ void UncommittedUnusedLayers(AgcDriver::VulkanDevice& device, std::uint32_t slic auto* texels = reinterpret_cast(active); std::fill_n(texels, LayerBytes / sizeof(std::uint32_t), Initial); Require(AgcDriver::GuestMemory::CollectWrites(active, LayerBytes) != 0, "the committed array slice has no write history"); + Require(AgcDriver::GuestMemory::CollectWritesUncached(surface, SurfaceBytes) == 0, "the uncommitted array fixture unexpectedly has complete backend write history"); Require(AgcDriver::GuestMemory::CollectWrites(surface, SurfaceBytes) == 0, "the unused array slices unexpectedly have complete write history"); AgcDriver::GuestMemory::BumpCollectEpoch(); DrawSlice(device, surface, slice, TileR64KBX, false); From 923a32c9c9e6352e01bde42072456ea40e862b65 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Tue, 20 Oct 2026 16:43:02 +0200 Subject: [PATCH 050/621] fix(libkernel_sys): fail link, symlink, pathconf and fchmodat through a regular file with ENOTDIR FreeBSD 11.4 vfs_cache_lookup answers ENOTDIR when the directory it searches is not a directory (sys/kern/vfs_cache.c:2104-2105), the lookup behind the calls fixed in the earlier ENOTDIR changes. These four calls answered ENOENT for a path such as file/child on Windows. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libkernel_sys/Export.cpp | 14 +++++++------- core/libs/tests/GuestKernelSys.cpp | 6 ++++++ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index a0adb45364..18c4eb733d 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -301,8 +301,8 @@ std::int64_t APS5_VABI pathconf_nid_postfix(const char* path, int name) { if (const int error = ResolvePathError_nid_no_patch(path, native)) return Fail(error); std::error_code error; const auto status = std::filesystem::status(native, error); - if (error) return Fail(GuestError(error)); - if (!std::filesystem::exists(status)) return Fail(GuestEnoent); + 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(GuestNameMax); @@ -341,10 +341,10 @@ 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(Fail(error)); std::error_code error; const auto status = std::filesystem::status(source, error); - if (!std::filesystem::exists(status)) return static_cast(Fail(GuestEnoent)); + if (!std::filesystem::exists(status)) return static_cast(Fail(HasNonDirectoryPrefix(source) ? GuestEnotdir : GuestEnoent)); if (std::filesystem::is_directory(status)) return static_cast(Fail(GuestEperm)); if (std::filesystem::exists(std::filesystem::symlink_status(destination, error))) return static_cast(Fail(GuestEexist)); - if (!std::filesystem::is_directory(destination.parent_path(), error)) return static_cast(Fail(GuestEnoent)); + if (!std::filesystem::is_directory(destination.parent_path(), error)) return static_cast(Fail(HasNonDirectoryPrefix(destination) ? GuestEnotdir : GuestEnoent)); const auto target = std::filesystem::is_symlink(source, error) ? std::filesystem::canonical(source, error) : source; if (!error) std::filesystem::create_hard_link(target, destination, error); if (error == std::errc::cross_device_link) return static_cast(Fail(GuestExdev)); @@ -404,7 +404,7 @@ int APS5_VABI symlink_nid_postfix(const char* target, const char* created) { #else if (std::filesystem::exists(std::filesystem::symlink_status(native, error))) return static_cast(Fail(GuestEexist)); #endif - if (!std::filesystem::is_directory(native.parent_path(), error)) return static_cast(Fail(GuestEnoent)); + if (!std::filesystem::is_directory(native.parent_path(), error)) return static_cast(Fail(HasNonDirectoryPrefix(native) ? GuestEnotdir : GuestEnoent)); const bool directory = std::filesystem::is_directory(native.parent_path() / contents, error); #ifdef _WIN32 DWORD flags = SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE; @@ -522,9 +522,9 @@ int APS5_VABI fchmodat_nid_postfix(int fd, const char* path, int mode, int flag) } } #ifdef _WIN32 - if (::_wchmod(native.wstring().c_str(), mode & 07777) != 0) return static_cast(Fail(GuestFileError(errno))); + if (::_wchmod(native.wstring().c_str(), mode & 07777) != 0) return static_cast(Fail(errno == ENOENT && HasNonDirectoryPrefix(native) ? GuestEnotdir : GuestFileError(errno))); #else - if (::chmod(native.c_str(), static_cast(mode & 07777)) != 0) return static_cast(Fail(GuestFileError(errno))); + if (::chmod(native.c_str(), static_cast(mode & 07777)) != 0) return static_cast(Fail(errno == ENOENT && HasNonDirectoryPrefix(native) ? GuestEnotdir : GuestFileError(errno))); #endif RecordWrittenPath_nid_no_patch(native); return 0; diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index ea71a5644b..c7e2a639ad 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -387,6 +387,12 @@ int main() { Require(chroot_nid_postfix((root / "file.txt" / "..").string().c_str()) == -1 && Error() == 20); Require(chroot_nid_postfix((root / std::string(256, 'n')).string().c_str()) == -1 && Error() == 63); Require(chroot_nid_postfix(("/" + std::string(1024, 'p')).c_str()) == -1 && Error() == 63); + const std::string throughFile = (root / "file.txt" / "child").string(); + Require(pathconf_nid_postfix(throughFile.c_str(), 4) == -1 && Error() == 20); + Require(fchmodat_nid_postfix(-100, throughFile.c_str(), 0600, 0) == -1 && Error() == 20); + Require(symlink_nid_postfix("target", throughFile.c_str()) == -1 && Error() == 20); + Require(link_nid_postfix(file.c_str(), throughFile.c_str()) == -1 && Error() == 20); + Require(link_nid_postfix(throughFile.c_str(), (root / "linked").string().c_str()) == -1 && Error() == 20); Require(ChrootThrows(root.string())); Require(ChrootThrows("/" + root.filename().string() + "/.")); const auto guestRoot = "/" + root.filename().string(); 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 051/621] 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 052/621] 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 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 053/621] 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 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 054/621] 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 89779222442c810ff4d8fd0412f2d6dd584479ea Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Tue, 20 Oct 2026 14:28:03 +0200 Subject: [PATCH 055/621] fix(libkernel): drop undefined protection bits in mprotect FreeBSD 11.4 kern_mprotect masks the argument with VM_PROT_ALL instead of refusing it (sys/vm/vm_mmap.c:613). sceKernelMprotect already does this since #5770; the POSIX mprotect export still failed with EINVAL. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp | 3 ++- core/libs/tests/GuestMemory.cpp | 12 ++++++++++++ docs/dev/debt/libkernel.md | 2 +- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp b/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp index 7c69af40fa..22d30d3596 100644 --- a/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp @@ -53,6 +53,7 @@ constexpr int GuestAdviceCore = 9; constexpr int GuestAdviceProtect = 10; constexpr int GuestLockCurrent = 0x1; constexpr int GuestLockFuture = 0x2; +constexpr int GuestDefinedProtection = 0x3F7; constexpr std::uintptr_t GuestUserAddressEnd = 0x800000000000; constexpr int GuestAlignedSuper = 0x1000000; constexpr std::size_t SuperpageSize = std::size_t{1} << 21; @@ -279,7 +280,7 @@ int APS5_VABI mprotect_nid_postfix(void* address, std::size_t length, int protec if (start == 0 || length > std::numeric_limits::max() - start) return failed(GuestInvalid); try { - if (DoMprotect(address, length, protection) != 0) return failed(GuestInvalid); + if (DoMprotect(address, length, protection & GuestDefinedProtection) != 0) return failed(GuestInvalid); return 0; } catch (const std::bad_alloc&) { return failed(GuestNoMemory); diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index 37501693fb..7eea938933 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -287,6 +287,17 @@ static void CheckStrictMemoryProtectionBits() { Require(sceKernelReleaseDirectMemory(physical, length) == 0); } +static void CheckPosixMprotectIgnoresUndefinedBits() { + constexpr std::size_t length = 0x4000; + auto* mapping = static_cast(mmap_nid_postfix(nullptr, length, 3, 0x1002, -1, 0)); + Require(mapping != reinterpret_cast(static_cast(-1))); + Require(mprotect_nid_postfix(const_cast(mapping), length, static_cast(0xfffffff2u)) == 0); + mapping[length - 1] = 5; + Require(mapping[length - 1] == 5); + Require(mprotect_nid_postfix(const_cast(mapping), length, 0x8 | 0x3) == 0); + Require(munmap_nid_postfix(const_cast(mapping), length) == 0); +} + static void CheckInternalNamedFlexibleMapping() { constexpr std::size_t length = 0x10000; std::size_t before = 0; @@ -2971,6 +2982,7 @@ int main(int argc, char** argv) { CheckAudioCoprocessorProtection(); CheckMprotectIgnoresUndefinedBits(); CheckStrictMemoryProtectionBits(); + CheckPosixMprotectIgnoresUndefinedBits(); CheckDirectMemoryFollowsPhysicalPages(); CheckSparseDirectMemoryBacking(); CheckReleaseDirectMemoryClearsMappings(); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index c27dde1e57..060ebea064 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -112,7 +112,7 @@ - [Guest thread scheduling](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - the policies and priorities that `sched_get_priority_max`, `sched_get_priority_min`, `pthread_setschedparam` and `scePthreadSetschedparam` accept, and those a created thread takes, follow a PS5 (firmware 13.60), but they are only recorded and reported: they are not applied to host threads, so a title's priority order has no effect on host scheduling - [sceKernelMprotect, sceKernelMtypeprotect](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) over a range with a gap that is not registered in the allocation tracker protect the registered parts and skip the gap, and return 0, as FreeBSD 11.4's [`vm_map_protect`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/vm/vm_map.c) does (it has no check for holes); a range whose start is unmapped (also when later pages are mapped) returns `SCE_KERNEL_ERROR_EINVAL` (`mprotect`: `EINVAL`) because an existing test pins that for an unmapped start; FreeBSD answers 0 there too (`vm_map_protect` takes the next entry), so this errno is not taken from the source and was not changed. `sceKernelMprotect` threw for a gap before, which ended the title. Every part is checked for a GPU or image pin before the first one is protected; the parts are then protected one after the other, so a host protection failure in a later part leaves the earlier parts changed (FreeBSD's `vm_map_protect` cannot fail in its second pass). The behaviour on the console is not known. None of this was measured on a console. -- [sceKernelMprotect protection bits](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - bits outside `0x3F7` are dropped instead of refused, as 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 +- [sceKernelMprotect protection bits](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - bits outside `0x3F7` are dropped instead of refused, in the POSIX `mprotect` as well (FreeBSD 11.4 masks the argument, `sys/vm/vm_mmap.c:613`; the console was measured only through `sceKernelMprotect`), 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 - [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 and `EVFILT_WRITE` on the write 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 off Linux (see the kevent EV_CLEAR entry), `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) 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 056/621] 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 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 057/621] 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 e5b66f2ddf245c61fdd85d2f58c305969b88ed14 Mon Sep 17 00:00:00 2001 From: billythemenace2 Date: Tue, 20 Oct 2026 20:12:28 +0100 Subject: [PATCH 058/621] 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 62e6ba74813ddf8d019efe894a644534e4de633c Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Tue, 20 Oct 2026 15:25:32 +0200 Subject: [PATCH 059/621] fix(libkernel): clamp long nanosleep requests like FreeBSD instead of wrapping them kern_clock_nanosleep limits the sleep to INT32_MAX / 2 seconds and sleeps for that long (sys/kern/kern_time.c:543-547). The guest multiplied tv_sec by 1e9 in 64 bits, so a request of 18446744074 seconds or more wrapped to a short sleep: nanosleep with tv_sec = INT64_MAX returned after about 0.1 s. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libkernel/CMakeLists.txt | 10 ++++ core/libs/prx/libkernel/Time/Time.cpp | 3 +- core/libs/tests/GuestNanosleepLongRequest.cpp | 58 +++++++++++++++++++ 3 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 core/libs/tests/GuestNanosleepLongRequest.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index cea1629cec..31d96c7509 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -749,6 +749,16 @@ if(BUILD_TESTING) set_tests_properties(guest_sleep_wake guest_sleep_wake_coarse PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_nanosleep_long_request_tests ${LIBS_INCLUDE_DIR}/tests/GuestNanosleepLongRequest.cpp) + target_include_directories(guest_nanosleep_long_request_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_nanosleep_long_request_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_nanosleep_long_request_tests) + add_test(NAME guest_nanosleep_long_request COMMAND guest_nanosleep_long_request_tests) + set_tests_properties(guest_nanosleep_long_request PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_nanosleep_long_request PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_pthread_sem_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadSem.cpp) target_include_directories(guest_pthread_sem_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_pthread_sem_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/Time/Time.cpp b/core/libs/prx/libkernel/Time/Time.cpp index afd6d5d40e..ad5f6f4863 100644 --- a/core/libs/prx/libkernel/Time/Time.cpp +++ b/core/libs/prx/libkernel/Time/Time.cpp @@ -290,8 +290,9 @@ static int SleepForRequest(const KernelTimespec* rqtp) { constexpr int guestInvalid = 22; if (rqtp == nullptr) return guestFault; if (rqtp->tv_nsec < 0 || rqtp->tv_nsec >= 1000000000LL) return guestInvalid; + constexpr std::int64_t longestSleepSeconds = INT32_MAX / 2; if (rqtp->tv_sec >= 0) { - CancellableSleep(static_cast(rqtp->tv_sec) * 1000000000ULL + + CancellableSleep(static_cast(std::min(static_cast(rqtp->tv_sec), longestSleepSeconds)) * 1000000000ULL + static_cast(rqtp->tv_nsec)); } return 0; diff --git a/core/libs/tests/GuestNanosleepLongRequest.cpp b/core/libs/tests/GuestNanosleepLongRequest.cpp new file mode 100644 index 0000000000..b2b6ec9cd6 --- /dev/null +++ b/core/libs/tests/GuestNanosleepLongRequest.cpp @@ -0,0 +1,58 @@ +#include "SceTypes.hpp" +#include "prx/libkernel/Pthread/include/Pthread.hpp" +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); +int APS5_VABI nanosleep_nid_postfix(const KernelTimespec* rqtp, KernelTimespec* rmtp); +int APS5_VABI sceKernelNanosleep(const KernelTimespec* rqtp, KernelTimespec* rmtp); +} + +static void Require(bool value, std::source_location location = std::source_location::current()) { + if (value) return; + std::fprintf(stderr, "%s:%u: requirement failed\n", location.file_name(), location.line()); + std::abort(); +} + +static constexpr auto ObservationTime = std::chrono::milliseconds(1500); +static std::atomic started{false}; +static std::atomic returned{false}; +static std::int64_t requestedSeconds = 0; +static bool posix = true; + +static void* APS5_VABI Sleeper(void*) { + const KernelTimespec request{requestedSeconds, 0}; + started.store(true); + if (posix) nanosleep_nid_postfix(&request, nullptr); + else sceKernelNanosleep(&request, nullptr); + returned.store(true); + return nullptr; +} + +static void SleepsPastTheObservation(std::int64_t seconds, bool usePosix) { + started.store(false); + returned.store(false); + requestedSeconds = seconds; + posix = usePosix; + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, Sleeper, nullptr, nullptr) == 0); + while (!started.load()) std::this_thread::yield(); + std::this_thread::sleep_for(ObservationTime); + Require(!returned.load()); +} + +int main() { + for (const bool usePosix : {true, false}) { + SleepsPastTheObservation(INT64_MAX, usePosix); + SleepsPastTheObservation(18446744074LL, usePosix); + SleepsPastTheObservation(std::int64_t{1} << 40, usePosix); + } + std::fflush(nullptr); + std::_Exit(0); +} From aec6bba8c4b6d6e5ab0bc1f513b99b236d4052e0 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Mon, 19 Oct 2026 23:40:42 +0000 Subject: [PATCH 060/621] test(agc): cover zero result for 16-bit color compares --- .../prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp index 1d96a16670..1b7b1c94aa 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp @@ -407,6 +407,7 @@ int main() { } } } + RunSrgbZero(*device, Format1616UNorm, {ClampEdge, FilterPoint}, "16_16 unorm comparison returns zero", false, 0u); Run(*device, {ClampEdge, FilterPoint}, "point, clamp to edge"); Run(*device, {ClampWrap, FilterPoint}, "point, wrap"); if (lodClamp) Run(*device, {ClampWrap, FilterPoint}, "point, wrap with negative offsets", true); From 4f0e4ba8e732c606385f318237cec77b6013d8a8 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Mon, 19 Oct 2026 23:47:25 +0000 Subject: [PATCH 061/621] fix(agc): return zero for measured color compares --- .../tests/execution/EmulatedColorCompare.cpp | 86 ++++++------------- .../Optimization/src/ResourceMaterializer.cpp | 21 +---- 2 files changed, 30 insertions(+), 77 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp index 1b7b1c94aa..cfe2c75b30 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp @@ -27,6 +27,7 @@ constexpr std::uint32_t Side = 2; constexpr std::uint32_t Format8888UNorm = 56; constexpr std::uint32_t Format8UNorm = 1; constexpr std::uint32_t Format8_8UNorm = 14; +constexpr std::uint32_t Format16UNorm = 7; constexpr std::uint32_t Format1616UNorm = 23; constexpr std::uint32_t Format9995Float = 132; constexpr std::uint32_t Format8Srgb = 128; @@ -178,45 +179,6 @@ void FillInput() { Extra[0] = 0x00001f20u; } -std::int32_t OffsetComponent(std::uint32_t tid, std::uint32_t component) { - const auto field = (Extra[tid * 3u] >> (component * 8u)) & 0x3fu; - return static_cast(field ^ 0x20u) - 0x20; -} - -float ReferenceTexel(int x, int y, float reference, const Sampler& sampler) { - const std::uint32_t clamp = sampler.clamp; - const int size = static_cast(Side); - if (clamp == ClampBorder && (x < 0 || x >= size || y < 0 || y >= size)) { - const float border = sampler.border == BorderWhite ? 1.0f : 0.0f; - return reference <= border ? 1.0f : 0.0f; - } - const auto address = [&](int value) { - if (clamp == ClampEdge) return std::clamp(value, 0, static_cast(Side) - 1); - const int size = static_cast(Side); - return ((value % size) + size) % size; - }; - const float red = static_cast(Red[address(y) * Side + address(x)]) / 255.0f; - return reference <= red ? 1.0f : 0.0f; -} - -float Expected(std::uint32_t tid, const Sampler& sampler, bool offsets) { - const float u = Input[tid * 3u + 1u] * static_cast(Side); - const float v = Input[tid * 3u + 2u] * static_cast(Side); - const float reference = std::clamp(Input[tid * 3u + 0u], 0.0f, 1.0f); - const int offsetX = offsets ? OffsetComponent(tid, 0u) : 0; - const int offsetY = offsets ? OffsetComponent(tid, 1u) : 0; - if (sampler.filter == FilterPoint) return ReferenceTexel(static_cast(std::floor(u)) + offsetX, static_cast(std::floor(v)) + offsetY, reference, sampler); - const float cu = u - 0.5f; - const float cv = v - 0.5f; - const int x = static_cast(std::floor(cu)) + offsetX; - const int y = static_cast(std::floor(cv)) + offsetY; - const float a = cu - std::floor(cu); - const float b = cv - std::floor(cv); - const float top = ReferenceTexel(x, y, reference, sampler) * (1.0f - a) + ReferenceTexel(x + 1, y, reference, sampler) * a; - const float bottom = ReferenceTexel(x, y + 1, reference, sampler) * (1.0f - a) + ReferenceTexel(x + 1, y + 1, reference, sampler) * a; - return top * (1.0f - b) + bottom * b; -} - ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, std::uint32_t format, const Sampler& sampler, std::span code = Code, bool useCache = false, bool nativeSampleOffsets = true, bool cube = false) { std::vector userData(24, 0u); const auto input = cube ? BufferDescriptor(CubeInput.data(), static_cast(sizeof(CubeInput))) : BufferDescriptor(Input.data(), static_cast(sizeof(Input))); @@ -254,9 +216,7 @@ std::uint64_t Run(AgcDriver::VulkanDevice& device, const Sampler& sampler, const device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); device.WaitIdle(); for (std::uint32_t tid = 0; tid < Threads; ++tid) { - const float expected = Expected(tid, sampler, offsets); - const float tolerance = sampler.filter == FilterPoint ? 0.0f : 1e-4f; - Require(std::fabs(Output[tid] - expected) <= tolerance, std::string(name) + ": thread " + std::to_string(tid) + " compared to " + std::to_string(Output[tid]) + ", expected " + std::to_string(expected)); + Require(Output[tid] == 0.0f, std::string(name) + ": thread " + std::to_string(tid) + " compared to " + std::to_string(Output[tid]) + ", expected 0"); } return result.variantId; } @@ -274,12 +234,11 @@ void RunChannels(AgcDriver::VulkanDevice& device, std::span device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); device.WaitIdle(); for (std::uint32_t tid = 0; tid < Threads; ++tid) { - const float compared = Expected(tid, sampler, false); for (std::uint32_t reg = 0; reg < 4u; ++reg) { float expected = -1.0f; if (reg < registers) { const auto select = channels[firstChannel + reg]; - expected = select == 0u ? 0.0f : select == 1u ? 1.0f : compared; + expected = select == 0u ? 0.0f : select == 1u ? 1.0f : 0.0f; } const float actual = Output[tid * 4u + reg]; Require(std::fabs(actual - expected) <= 1e-4f, std::string(name) + ": thread " + std::to_string(tid) + " register " + std::to_string(reg) + " returned " + std::to_string(actual) + ", expected " + std::to_string(expected)); @@ -304,7 +263,7 @@ void RunCube(AgcDriver::VulkanDevice& device, const Sampler& sampler, const char device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(CubeCode.data())); device.WaitIdle(); for (std::uint32_t tid = 0; tid < Threads; ++tid) { - Require(Output[tid] == 1.0f, std::string(name) + ": +X cube face comparison returned " + std::to_string(Output[tid]) + ", expected 1"); + Require(Output[tid] == 0.0f, std::string(name) + ": +X cube face comparison returned " + std::to_string(Output[tid]) + ", expected 0"); } } @@ -314,7 +273,7 @@ bool BindsDepthCompare(const ShaderRecompiler::RecompileResult& result) { }); } -void RunSrgbCubeZero(AgcDriver::VulkanDevice& device, std::uint32_t format, const Sampler& sampler, const char* name) { +void RunZeroCube(AgcDriver::VulkanDevice& device, std::uint32_t format, const Sampler& sampler, const char* name) { FillCubeTexels(); for (std::uint32_t tid = 0; tid < Threads; ++tid) { CubeInput[tid * 4u + 0u] = 0.5f; @@ -332,7 +291,7 @@ void RunSrgbCubeZero(AgcDriver::VulkanDevice& device, std::uint32_t format, cons } } -void RunSrgbZero(AgcDriver::VulkanDevice& device, std::uint32_t format, const Sampler& sampler, const char* name, bool useCache, std::uint64_t expectedVariant) { +void RunZero(AgcDriver::VulkanDevice& device, std::uint32_t format, const Sampler& sampler, const char* name, bool useCache, std::uint64_t expectedVariant) { Output.fill(-1.0f); const auto result = Compile(device, format, sampler, Code, useCache); if (useCache) Require(result.cacheHit && result.variantId == expectedVariant, std::string(name) + " did not reuse the compiled comparison artifact"); @@ -366,25 +325,34 @@ int main() { FillTexels(); FillInput(); RunCube(*device, {ClampEdge, FilterPoint}, "point cube, clamp to edge"); - Reject(*device, Format1616UNorm, {ClampEdge, FilterBilinear}, "bilinear cube comparison requires DISABLE_CUBE_WRAP", CubeCode, true, true); + RunCube(*device, {ClampEdge, FilterBilinear}, "bilinear cube without DISABLE_CUBE_WRAP"); for (std::uint32_t function = 0; function < 8u; ++function) { const auto name = "bilinear 8_8_8_8 srgb cube function " + std::to_string(function); - RunSrgbCubeZero(*device, Format8888Srgb, {ClampEdge, FilterBilinear, BorderBlack, 0u, 0u, false, function}, name.c_str()); + RunZeroCube(*device, Format8888Srgb, {ClampEdge, FilterBilinear, BorderBlack, 0u, 0u, false, function}, name.c_str()); } - RunSrgbCubeZero(*device, Format8Srgb, {ClampEdge, FilterBilinear}, "bilinear 8 srgb cube without DISABLE_CUBE_WRAP"); - RunSrgbCubeZero(*device, Format8_8Srgb, {ClampEdge, FilterBilinear}, "bilinear 8_8 srgb cube without DISABLE_CUBE_WRAP"); + RunZeroCube(*device, Format8Srgb, {ClampEdge, FilterBilinear}, "bilinear 8 srgb cube without DISABLE_CUBE_WRAP"); + RunZeroCube(*device, Format8_8Srgb, {ClampEdge, FilterBilinear}, "bilinear 8_8 srgb cube without DISABLE_CUBE_WRAP"); RunCube(*device, {ClampEdge, FilterBilinear, BorderBlack, 0u, 0u, true}, "bilinear cube with DISABLE_CUBE_WRAP"); - Reject(*device, Format9995Float, {ClampEdge, FilterPoint}, "implemented only for float, unorm and snorm formats"); + Reject(*device, Format9995Float, {ClampEdge, FilterPoint}, "implemented only for floating-point, UNORM and SNORM formats"); for (std::uint32_t function = 0; function < 8u; ++function) { const auto name = "bilinear 8_8_8_8 unorm cube function " + std::to_string(function); - RunSrgbCubeZero(*device, Format8888UNorm, {ClampEdge, FilterBilinear, BorderBlack, 0u, 0u, false, function}, name.c_str()); + RunZeroCube(*device, Format8888UNorm, {ClampEdge, FilterBilinear, BorderBlack, 0u, 0u, false, function}, name.c_str()); } - RunSrgbCubeZero(*device, Format8888UNorm, {ClampEdge, FilterAnisoBilinear, BorderBlack, 0u, 0u, false, GreaterEqual}, "anisotropic bilinear 8_8_8_8 unorm cube without DISABLE_CUBE_WRAP"); - Reject(*device, Format1616UNorm, {ClampEdge, FilterAnisoBilinear}, "point or bilinear"); - Reject(*device, Format1616UNorm, {ClampEdge, FilterAnisoPoint}, "point or bilinear"); + RunZeroCube(*device, Format8888UNorm, {ClampEdge, FilterAnisoBilinear, BorderBlack, 0u, 0u, false, GreaterEqual}, "anisotropic bilinear 8_8_8_8 unorm cube without DISABLE_CUBE_WRAP"); + RunZero(*device, Format1616UNorm, {ClampEdge, FilterAnisoBilinear}, "anisotropic bilinear 16_16 unorm", false, 0u); + RunZero(*device, Format1616UNorm, {ClampEdge, FilterAnisoPoint}, "anisotropic point 16_16 unorm", false, 0u); FillTexels(); + Require(BindsDepthCompare(Compile(*device, Format16UNorm, {ClampEdge, FilterPoint})), "an R16 unorm texture left the native comparison path"); Require(BindsDepthCompare(Compile(*device, Format32Float, {ClampEdge, FilterBilinear})), "an R32 float texture left the native comparison path"); Require(!BindsDepthCompare(Compile(*device, Format8888UNorm, {ClampEdge, FilterBilinear})), "a color texture kept a depth-compare binding"); + for (const auto format : {1u, 2u, 8u, 13u, 14u, 15u, 23u, 24u, 29u, 36u, 50u, 51u, 56u, 57u, 65u, 66u, 71u, 77u}) { + const auto name = "measured GFX10 color format " + std::to_string(format) + " returns zero"; + RunZero(*device, format, {ClampEdge, FilterPoint}, name.c_str(), false, 0u); + } + for (std::uint32_t function = 0; function < 8u; ++function) { + const auto name = "16_16 unorm compare function " + std::to_string(function) + " returns zero"; + RunZero(*device, Format1616UNorm, {ClampEdge, FilterPoint, BorderBlack, 0u, 0u, false, function}, name.c_str(), false, 0u); + } for (const bool useCache : {false, true}) { const auto variant = useCache ? Run(*device, {ClampEdge, FilterPoint}, "cached UNORM before sRGB", false, true) : 0u; if (useCache) Require(variant != 0, "the cached comparison has no compiled artifact identity"); @@ -392,7 +360,7 @@ int main() { for (std::uint32_t function = 0; function < 8u; ++function) { for (const auto filter : {FilterPoint, FilterBilinear}) { const auto name = std::string(useCache ? "cached" : "uncached") + " sRGB format " + std::to_string(format) + " function " + std::to_string(function) + (filter == FilterPoint ? " point" : " bilinear"); - RunSrgbZero(*device, format, {ClampEdge, filter, BorderBlack, 0u, 0u, false, function}, name.c_str(), useCache, variant); + RunZero(*device, format, {ClampEdge, filter, BorderBlack, 0u, 0u, false, function}, name.c_str(), useCache, variant); } } } @@ -403,11 +371,11 @@ int main() { for (const auto filter : {FilterPoint, FilterBilinear, FilterAnisoBilinear}) { if (format != Format8888UNorm && function != LessEqual) continue; const auto name = "8-bit unorm format " + std::to_string(format) + " function " + std::to_string(function) + " filter " + std::to_string(filter); - RunSrgbZero(*device, format, {ClampEdge, filter, BorderBlack, 0u, 0u, false, function}, name.c_str(), false, 0u); + RunZero(*device, format, {ClampEdge, filter, BorderBlack, 0u, 0u, false, function}, name.c_str(), false, 0u); } } } - RunSrgbZero(*device, Format1616UNorm, {ClampEdge, FilterPoint}, "16_16 unorm comparison returns zero", false, 0u); + RunZero(*device, Format1616UNorm, {ClampEdge, FilterPoint}, "16_16 unorm comparison returns zero", false, 0u); Run(*device, {ClampEdge, FilterPoint}, "point, clamp to edge"); Run(*device, {ClampWrap, FilterPoint}, "point, wrap"); if (lodClamp) Run(*device, {ClampWrap, FilterPoint}, "point, wrap with negative offsets", true); diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index 3a9a072113..5404f91580 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -1347,24 +1347,9 @@ void materializeForcedDegamma(const IrResourcePlan& plan, ResourceSnapshot& snap } std::uint32_t colorCompareReference(IrBufferFormat format) { - switch (format) { - case IrBufferFormat::Format16_16UNorm: - case IrBufferFormat::Format11_11_10UNorm: case IrBufferFormat::Format10_11_11UNorm: case IrBufferFormat::Format2_10_10_10UNorm: - case IrBufferFormat::Format10_10_10_2UNorm: case IrBufferFormat::Format16_16_16_16UNorm: - return EmulatedCompare::ReferenceUnorm; - case IrBufferFormat::Format8UNorm: case IrBufferFormat::Format8_8UNorm: case IrBufferFormat::Format8_8_8_8UNorm: - case IrBufferFormat::Format8Srgb: case IrBufferFormat::Format8_8Srgb: case IrBufferFormat::Format8_8_8_8Srgb: - return EmulatedCompare::ReferenceZero; - case IrBufferFormat::Format8SNorm: case IrBufferFormat::Format16SNorm: case IrBufferFormat::Format8_8SNorm: case IrBufferFormat::Format16_16SNorm: - case IrBufferFormat::Format11_11_10SNorm: case IrBufferFormat::Format10_11_11SNorm: case IrBufferFormat::Format2_10_10_10SNorm: - case IrBufferFormat::Format10_10_10_2SNorm: case IrBufferFormat::Format8_8_8_8SNorm: case IrBufferFormat::Format16_16_16_16SNorm: - return EmulatedCompare::ReferenceSnorm; - case IrBufferFormat::Format16Float: case IrBufferFormat::Format16_16Float: case IrBufferFormat::Format11_11_10Float: - case IrBufferFormat::Format10_11_11Float: case IrBufferFormat::Format32_32Float: case IrBufferFormat::Format16_16_16_16Float: - return EmulatedCompare::ReferenceFloat; - default: - throw std::runtime_error("comparison sampling of a color texture is implemented only for float, unorm and snorm formats (format " + std::to_string(static_cast(format)) + ")"); - } + const auto value = static_cast(format); + if (((value >= 1u && value <= 77u) || (value >= 128u && value <= 130u)) && SampledTextureNumericClass(format) == IrTextureNumericClass::Float && format != IrBufferFormat::Format11_11_10UNorm && format != IrBufferFormat::Format10_11_11Float) return EmulatedCompare::ReferenceZero; + throw std::runtime_error("comparison sampling of a color texture is implemented only for floating-point, UNORM and SNORM formats (format " + std::to_string(value) + ")"); } bool convertedSampleCompatible(const ImageResource& image) { From 092a66773a29d7d955c77d92107a0be150ccd8c5 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Mon, 19 Oct 2026 23:47:27 +0000 Subject: [PATCH 062/621] docs(shader): record measured color compare formats --- docs/dev/debt/shader.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 4353f3bd8e..e00bdf7cb6 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -14,7 +14,7 @@ - [MUBUF/MTBUF descriptors ending in VCC](../../../core/shader/recompiler/RdnaDecoder/src/RdnaMemoryOpDecoder.cpp): the four dword descriptor at s104 is s104, s105, vcc_lo and vcc_hi, as LLVM's [VCCTuple/SReg_128](https://github.com/llvm/llvm-project/blob/d8219b47422f7322db252ad29ca8ea87993fb433/llvm/lib/Target/AMDGPU/SIRegisterInfo.td#L422) and [buffer resource operands](https://github.com/llvm/llvm-project/blob/d8219b47422f7322db252ad29ca8ea87993fb433/llvm/lib/Target/AMDGPU/BUFInstructions.td) describe. Existing translation maps scalar codes106/107 to the raw VCC words. Recompiled loads/stores, both words, bounds, formats and wave32/wave64 execute on NVIDIA and AMD Vulkan; raw gfx10/gfx1013 hardware is not measured. Higher descriptor bases remain refused; the normal SGPR bound is unchanged. - [Packed pixel ancillary word](../../../core/shader/recompiler/Optimization/src/ConstantFolder.cpp) (shader recompiler) - the sample index at bits 8-11 and the render target index at bits 16-28 follow #295. The sample index is the SampleId builtin in a pixel shader the guest iterates per sample (`PS_ITER_SAMPLE` on a multisampled draw), an equality inferred, not measured. In any other pixel shader it is 0, the iterated sample number of a pixel the hardware does not iterate (also inferred), so reading it does not make the host shade per sample. The render target index is the Layer builtin when the draw exports it (a layered draw, `PA_CL_VS_OUT_CNTL` bit 18; the pipeline specialization constant `LayerExported`) and 0 otherwise, the first layer Vulkan renders when no pre-rasterization stage writes Layer. Reading the unwritten Layer builtin instead makes AMD's Windows driver drop the fragment's writes (#4800); with the constant at 0 the load is folded away. Mesa reads both fields from those bits, with the layer 14 bits wide only from GFX12 on ([ac_nir_lower_intrinsics_to_args.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/bffd91947efc1932da99aca69fc540d6d4201b0c/src/amd/common/nir/ac_nir_lower_intrinsics_to_args.c#L342-L368)). A value passed through a select, move or phi whose every read is an extract inside one of those fields is rebuilt from the two fields alone. Any other read the masked-select pass cannot drop gets the whole word: the primitive class at bits 0-1 (0 point, 1 line, 2 triangle, 3 rectangle), which the draw sets as a pipeline specialization constant ([RasterPrimitiveOf](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp)) in the shaders that read it, plus the sample field, with every other bit zero. That layout comes from the RDNA 3 and 3.5 ISA guides (prim type 1:0, VRS rates 3:2 and 5:4, eye id 7, sample 11:8, render target index 28:16). The GCN register guides give the same prim type and sample fields and put the render target index at 26:16, so the two sources agree for layers below 2048. AMD's LLPC reads the primitive class from bits 1:0 with the same encoding ([LowerGlCompatibility.cpp](https://github.com/GPUOpen-Drivers/llpc/blob/40cb8d95ad8d6f7f1652e3fd47d39667594cce08/llpc/lowering/LowerGlCompatibility.cpp#L830-L836)) and without a GFX-level check ([getPrimType](https://github.com/GPUOpen-Drivers/llpc/blob/40cb8d95ad8d6f7f1652e3fd47d39667594cce08/lgc/lowering/LowerInOut.cpp#L5307-L5315)). Nobody has checked it on gfx1013. In the whole word only bits 0-1 and 8-11 have a source. Bits 2-7, 12-15 and 29-31 are assumed zero, not measured: the VRS rates (2-5) and the eye id (7) because the driver supports neither variable-rate shading nor multiview, so a title that enables either would read other bits on the console, and bits 6, 12-15 and 29-31 because no source gives them a meaning. The sample field reads SampleId only in iterated shaders. The whole word has the same 0 at bits 16-28. A change that makes draws export a layer must restore the Layer read in both places - [Front-face input with `FRONT_FACE_ALL_BITS`](../../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) (shader recompiler) - with `SPI_BARYC_CNTL.FRONT_FACE_ALL_BITS` set, a back face reads 0 and a front face 0xffffffff. Mesa 22.0 RADV sets the bit ([radv_pipeline.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-22.0.0/src/amd/vulkan/radv_pipeline.c#L5953)) and ACO tests the input with `v_cmp_lg_u32 0` ([aco_instruction_selection.cpp](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-22.0.0/src/amd/compiler/aco_instruction_selection.cpp#L8123-L8128)), so only back = 0 and front ≠ 0 are sourced; PPSA05684's two-sided cloth shaders test it with `v_cmp_lt_u32 0`. The exact front value is not measured. Without the bit the input is ±1.0, as current Mesa reads it (`fgt 0`, `fsign`; [ac_nir_lower_intrinsics_to_args.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/bffd91947efc1932da99aca69fc540d6d4201b0c/src/amd/common/nir/ac_nir_lower_intrinsics_to_args.c)) -- [Color comparison descriptor forms](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp): emulated color comparison supports UNORM, SNORM and float formats for 2D, 2D-array and cube views. 8-bit sRGB and UNORM (`Format8Srgb`, `Format8_8Srgb`, `Format8_8_8_8Srgb`, `Format8UNorm`, `Format8_8UNorm`, `Format8_8_8_8UNorm`) return 0 for every compare function, measured with `image_sample_c_lz` on gfx1035 over references -0.5..1.5 and texel codes 0..255. Anisotropic filters are refused, except on those 8-bit formats, where every texel comparison is 0 (not measured with an anisotropic S#). Bilinear cube comparisons of float, snorm and the other unorm formats without `DISABLE_CUBE_WRAP` are refused; on that hardware a 4x4 R32 float cube blends across faces (46 of 90 edge rows differ from a per-face clamp; face 0, ref 0.1, u=3.9 v=2.5 is 0.6016 and u=1.5 v=0.1 is 0.3984), and with `DISABLE_CUBE_WRAP` the per-face clamp matches. An 8-bit sRGB or UNORM bilinear cube without that bit returns 0, since every texel comparison is already 0 for every compare function and the selected face cannot change the result. Cube-edge sampling behavior and console coverage on PS5's gfx1013 remain unverified. +- [Color comparison descriptor forms](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp): for formats AnyPS5 can sample as floating-point from GFX10 codes 1–77, `image_sample_c*` returns 0 for every comparison function except codes 7 (`16_UNORM`) and 22 (`32_FLOAT`), which use the native comparison path. The split was measured with point sampling on RDNA2 (gfx1031 and gfx1036) and with point and bilinear sampling on RDNA3 (gfx1100); the PS5's gfx1013 and 2D-array/cube comparisons across these formats remain unverified. Codes 128–130 (8-bit sRGB) also return 0, measured on gfx1035 over references -0.5..1.5 and texel codes 0..255. Scaled, integer and formats requiring converted sampling remain unsupported, although the tested hardware formats also return 0 for them. Anisotropic comparison of the remaining formats has not been measured. - [s_setkill](../../../core/shader/recompiler/RdnaDecoder/src/RdnaScalarOpDecoder.cpp) (core/shader/recompiler) - decoded as `s_endpgm` for any immediate: measured on RDNA2 hardware (gfx1035), a store before it is visible and nothing after it runs ([#771](https://github.com/boykopovar/AnyPS5/pull/771)); whether the KILL status bit it sets is observable differently from `s_endpgm` is unverified - [s_sethalt](../../../core/shader/recompiler/Translation/src/ScalarInstructions.cpp) (core/shader/recompiler) - with SIMM16[0] clear it is a no-op: the RDNA 2 ISA guide describes that value as resuming a halted wave, and a wave that executes it is not halted; measured on a gfx1035 (RDNA 2) wave: `s_sethalt 0` followed by a VALU op completes and gives the normal result. Only SIMM16[0] is read; with it set the instruction throws - [s_subvector_loop_begin and s_subvector_loop_end](../../../core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp) (core/shader/recompiler) - follow AMD's RDNA 2 instruction set reference and a gfx1036 (wave64) measurement, pinned in [SubvectorLoop.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/SubvectorLoop.cpp); `S0` in the end instruction's `elif(S0 == 0)` is read as its SGPR operand. A wave32 shader and a destination other than an SGPR throw because neither is measured. Measured on PC RDNA2 (Radeon 610M), not on a PS5. The [specialization pass](../../../core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp) keeps a constant-condition branch whose untaken target is a loop header, since folding it removes the loop's back edge, which SPIR-V requires exactly once per loop header (SPIRV-Tools rejects the module and the NVIDIA driver 615.78.08 crashes compiling it); a loop whose back-edge block becomes unreachable in another way is not handled @@ -136,7 +136,7 @@ - [FP16_OVFL](../../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) follows the shader's float mode as measured on RDNA2 (gfx1036) at IEEE=0 and IEEE=1 with f16 denormals kept and flushed: with `FP16_OVFL`=1 an f16 result that rounds to ±infinity is written as ±0x7bff unless a source operand is infinite, so `v_rcp_f16`, `v_rsq_f16` and `v_log_f16` of ±0 and `v_exp_f16` of 16 or more are clamped while an infinite input passes through, and `v_div_fixup_f16` keeps the infinity it writes for a zero denominator or an infinite numerator and clamps any other infinite quotient, a NaN one included. `v_cvt_pkrtz_f16_f32` rounds toward zero and never overflows. The sources are checked as the f32 values the translation computes with; the rule is applied to `v_fma_mix*_f16` with f32 sources and to `v_interp_p2_f16` without a measurement of infinite f32 sources or parameters. Round modes other than nearest even are not modelled, so the overflow threshold is the nearest-even one (65520). - [Directed f16 rounding](../../../core/shader/recompiler/Translation/src/TranslationContext/SubwordPacking.cpp) for `v_cvt_f16_f32`, `v_cvt_f16_i16`, `v_cvt_f16_u16` and `v_ldexp_f16` follows `FLOAT_MODE` bits 3:2 in every f16 denormal mode, measured on RDNA2 (gfx1036 with f16 denormals preserved, gfx1030 in all sixteen combinations of f16 rounding and denormal mode). When bit 7 flushes f16 results, `v_cvt_f16_f32`, `v_cvt_f16_i16` and `v_cvt_f16_u16` write a zero of the result's sign for a value that, rounded in that mode with an unbounded exponent, is below 2^-14, so a value just below 2^-14 that rounds up to it stays 0x0400; `v_ldexp_f16` keeps its rule above. The `clamp` modifier is applied before the rounding. On gfx1030 every row matches: 8,996 f32 inputs around the half denormal, normal and overflow boundaries with and without `clamp`, every 16-bit integer, and 2,470 `v_ldexp_f16` operands. - [v_cvt_pkrtz_f16_f32 and v_pack_b32_f16 denormals](../../../core/shader/recompiler/Translation/src/ConvertInstructions.cpp) follow the `FLOAT_MODE` f16 denormal field: `v_cvt_pkrtz_f16_f32` flushes each denormal half result to its signed zero when bit 1 is clear (0 and 1), and `v_pack_b32_f16` flushes each denormal half input to its signed zero when bit 0 is clear (0 and 2). Measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) at IEEE=0 and IEEE=1 with denorm16 0..3 and f32 denormals kept, over 256 pairs of 16 f32 values around the f16 denormal range for `v_cvt_pkrtz_f16_f32` and 256 pairs of 16 halves (zeros, denormals, minimum normals, ±1, the largest finite value, infinities and quiet and signaling NaNs) for `v_pack_b32_f16`. Shaders without a known float mode keep the previous lowering. The PS5's gfx1013 is not measured. -- [Comparison sampling of a color texture](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (`image_sample_c`/`image_sample_c_lz` on a texture that is not R32 float or R16 unorm) is emulated in the shader, since Vulkan compares only depth formats: the red channel is fetched, compared and, for bilinear filtering, blended (compare, then filter). Only 2D, 2D array and cube views, float/unorm/snorm formats, wrap, clamp-to-edge and clamp-to-border addressing (border red 0 for the black borders, 1 for opaque white) and point or bilinear filtering at one level are implemented, with texel offsets added to the texel indices; LOD biases and clamps are accepted, as they cannot select another level of a single-level sample. Half-border addressing, the border color table, gradients, gathers, other dimensions, anisotropy (except on the 8-bit formats below) and mip filtering throw. Under bilinear filtering, taps past a clamp-to-border edge compare the border value and blend with the texel taps, so results near the edge depend on the filter weights. AMD documentation does not say how a color format's reference is handled: it is clamped to [0, 1] for unorm and [-1, 1] for snorm, as Vulkan does for unorm depth, and not clamped for float. 8-bit sRGB and UNORM (`Format8Srgb`, `Format8_8Srgb`, `Format8_8_8_8Srgb`, `Format8UNorm`, `Format8_8UNorm`, `Format8_8_8_8UNorm`) return 0 for every compare function, measured with `image_sample_c_lz` on gfx1035 over references -0.5..1.5 and texel codes 0..255. A bilinear cube of those 8-bit formats without `DISABLE_CUBE_WRAP`, and an anisotropic S# on them, return 0 for the same reason; float, snorm and the other unorm bilinear cubes without that bit still throw. Bilinear weights are full float, not the hardware's fixed-point subtexel weights +- [Comparison sampling of a color texture](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (`image_sample_c`/`image_sample_c_lz` on a texture that is not R32 float or R16 unorm) returns 0 for every comparison function on formats AnyPS5 can sample as floating-point measured on GFX10 (codes 1–77 except native comparison codes 7 and 22) and on the corresponding GFX11 formats measured on gfx1100. The current Vulkan path still lowers these formats through the shader; the compare result is constant zero. Scaled, integer and formats requiring converted sampling remain refused. Only 2D, 2D-array and cube views are accepted; image tables, min/max reduction, unnormalized coordinates and unsupported addressing remain refused. Point and bilinear results match on gfx1100. The PS5's gfx1013, array/cube behavior for the measured formats, and anisotropic comparisons remain unverified. Bilinear weights are full float, not the hardware's fixed-point subtexel weights - [Negative color-texture comparison wrap indices](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (libSceAgcDriver) use explicit `OpSRem` normalization to implement modulo; the negative-offset point-wrap regression fails with direct `OpSMod` and passes with normalization on Mesa 22.3.6 llvmpipe (LLVM 15.0.6), but negative-index wrap has not been measured on PS5 hardware. - [Converted unorm images](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (10_11_11_UNORM, read and written through an R32_UINT view) 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. 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 063/621] 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 ee2d9a48765dc68804f19afd7bd54b9b08ebd251 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 20 Oct 2026 20:38:17 +0000 Subject: [PATCH 064/621] test(agc): avoid unsupported color image format --- .../libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp index cfe2c75b30..4538735f37 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp @@ -345,7 +345,10 @@ int main() { Require(BindsDepthCompare(Compile(*device, Format16UNorm, {ClampEdge, FilterPoint})), "an R16 unorm texture left the native comparison path"); Require(BindsDepthCompare(Compile(*device, Format32Float, {ClampEdge, FilterBilinear})), "an R32 float texture left the native comparison path"); Require(!BindsDepthCompare(Compile(*device, Format8888UNorm, {ClampEdge, FilterBilinear})), "a color texture kept a depth-compare binding"); - for (const auto format : {1u, 2u, 8u, 13u, 14u, 15u, 23u, 24u, 29u, 36u, 50u, 51u, 56u, 57u, 65u, 66u, 71u, 77u}) { + // Format 51 maps to VK_FORMAT_A2B10G10R10_SNORM_PACK32, which is not an + // image format on every Vulkan device (including RADV NAVI31). Keep this + // execution test limited to formats the test device can create. + for (const auto format : {1u, 2u, 8u, 13u, 14u, 15u, 23u, 24u, 29u, 36u, 50u, 56u, 57u, 65u, 66u, 71u, 77u}) { const auto name = "measured GFX10 color format " + std::to_string(format) + " returns zero"; RunZero(*device, format, {ClampEdge, FilterPoint}, name.c_str(), false, 0u); } From 745ff15b80e897accd9ee18c563822dde00036c9 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 20 Oct 2026 20:39:55 +0000 Subject: [PATCH 065/621] test(agc): skip unsupported sampled color images --- .../libSceAgcDriver/Execution/include/VulkanDevice.hpp | 1 + .../prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp | 6 ++++++ .../tests/execution/EmulatedColorCompare.cpp | 9 +++++---- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp index 19b1b24e22..40a62ef1d7 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp @@ -52,6 +52,7 @@ public: VulkanDevice& operator=(const VulkanDevice&) = delete; std::string DeviceName() const; std::string DriverInfo() const; + bool SupportsImageFormat(VkFormat format, VkImageUsageFlags usage) const; bool CalibrateClock(double& gpuNs, std::chrono::steady_clock::time_point& cpu) const; ShaderRecompiler::SpirvTarget Target() const; std::optional GeometryLimits() const; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 66da673ed8..9c2b02ed07 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -3630,6 +3630,12 @@ std::uint32_t VulkanDevice::VendorId() const { return state->properties.vendorID; } +bool VulkanDevice::SupportsImageFormat(VkFormat format, VkImageUsageFlags usage) const { + VkImageFormatProperties properties{}; + const auto query = state->InstanceFunction("vkGetPhysicalDeviceImageFormatProperties"); + return query(state->physical, format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, usage, 0, &properties) == VK_SUCCESS; +} + ShaderRecompiler::SpirvTarget VulkanDevice::Target() const { require(state->shaderProfile != nullptr, "shader device profile is unavailable"); auto target = state->shaderProfile->Target(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp index 4538735f37..b656cca503 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp @@ -345,10 +345,11 @@ int main() { Require(BindsDepthCompare(Compile(*device, Format16UNorm, {ClampEdge, FilterPoint})), "an R16 unorm texture left the native comparison path"); Require(BindsDepthCompare(Compile(*device, Format32Float, {ClampEdge, FilterBilinear})), "an R32 float texture left the native comparison path"); Require(!BindsDepthCompare(Compile(*device, Format8888UNorm, {ClampEdge, FilterBilinear})), "a color texture kept a depth-compare binding"); - // Format 51 maps to VK_FORMAT_A2B10G10R10_SNORM_PACK32, which is not an - // image format on every Vulkan device (including RADV NAVI31). Keep this - // execution test limited to formats the test device can create. - for (const auto format : {1u, 2u, 8u, 13u, 14u, 15u, 23u, 24u, 29u, 36u, 50u, 56u, 57u, 65u, 66u, 71u, 77u}) { + for (const auto format : {1u, 2u, 8u, 13u, 14u, 15u, 23u, 24u, 29u, 36u, 50u, 51u, 56u, 57u, 65u, 66u, 71u, 77u}) { + if (format == 51u && !device->SupportsImageFormat(VK_FORMAT_A2B10G10R10_SNORM_PACK32, VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT)) { + std::puts("measured GFX10 color format 51 returns zero: skipped, Vulkan device does not support the image format"); + continue; + } const auto name = "measured GFX10 color format " + std::to_string(format) + " returns zero"; RunZero(*device, format, {ClampEdge, FilterPoint}, name.c_str(), false, 0u); } From b5a8335fe3c74801fe2acc4d931894f691980e66 Mon Sep 17 00:00:00 2001 From: CPritch Date: Tue, 20 Oct 2026 14:22:22 +0100 Subject: [PATCH 066/621] 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 703102d6cae9850e8a06fa58df321349e3ea885f Mon Sep 17 00:00:00 2001 From: CPritch Date: Tue, 20 Oct 2026 21:59:25 +0100 Subject: [PATCH 067/621] 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 33c1f2365fbf5dc60f22be68f7631a88f5a12d0a Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 20 Oct 2026 21:06:23 +0000 Subject: [PATCH 068/621] fix(relinker): validate ELF prefix before reading full input --- core/relinker/io/include/io/FileReader.hpp | 2 ++ core/relinker/io/src/FileReader.cpp | 16 ++++++++++++++++ core/relinker/main.cpp | 1 + 3 files changed, 19 insertions(+) diff --git a/core/relinker/io/include/io/FileReader.hpp b/core/relinker/io/include/io/FileReader.hpp index bf5d2fa85a..1056ab33a1 100644 --- a/core/relinker/io/include/io/FileReader.hpp +++ b/core/relinker/io/include/io/FileReader.hpp @@ -2,11 +2,13 @@ #define IO_FILEREADER_HPP #include +#include namespace Io { class FileReader : public IFileReader { public: + std::vector ReadPrefix(const std::string& path, std::size_t size) const; std::vector Read(const std::string& path) override; }; diff --git a/core/relinker/io/src/FileReader.cpp b/core/relinker/io/src/FileReader.cpp index 228903ab48..35763b5df1 100644 --- a/core/relinker/io/src/FileReader.cpp +++ b/core/relinker/io/src/FileReader.cpp @@ -3,9 +3,25 @@ #include #include #include +#include namespace Io { +std::vector FileReader::ReadPrefix(const std::string& path, const std::size_t size) const { + if (size > static_cast(std::numeric_limits::max())) + throw Domain::RelinkerException("File prefix is too large: " + path); + std::ifstream f(path, std::ios::binary); + if (!f) + throw Domain::RelinkerException("Cannot open file: " + path); + std::vector buf(size); + f.read(reinterpret_cast(buf.data()), static_cast(size)); + const auto count = f.gcount(); + if (f.bad()) + throw Domain::RelinkerException("Cannot read file: " + path); + buf.resize(static_cast(count)); + return buf; +} + std::vector FileReader::Read(const std::string& path) { const auto native = NativePath(path); std::error_code error; diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index fe626ea38d..88c42f2797 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -41,6 +41,7 @@ struct ConvertedExecutable { ConvertedExecutable Convert(const Cli::Args& args, const std::string& inputPath, const std::string& absPath, const bool mainExecutable, const std::string& targetName, const std::vector& patches, Cli::ConsoleProgress& progress) { Io::FileReader fileReader; Relinker::ConversionInput input; + Relinker::ElfReader(fileReader.ReadPrefix(inputPath, 0x40)).ReadHeader(); input.Executable = fileReader.Read(inputPath); input.InputPath = std::filesystem::absolute(Io::NativePath(inputPath)).lexically_normal(); input.OutputPath = Io::NativePath(absPath); From fc046e621b53261073e3dd493074a3d672371d03 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 20 Oct 2026 21:06:23 +0000 Subject: [PATCH 069/621] test(relinker): reject large non-ELF inputs before allocation --- .../relinker/relinker/tests/test_input_magic.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/core/relinker/relinker/tests/test_input_magic.py b/core/relinker/relinker/tests/test_input_magic.py index e388e2d813..ace10d9119 100644 --- a/core/relinker/relinker/tests/test_input_magic.py +++ b/core/relinker/relinker/tests/test_input_magic.py @@ -1,6 +1,7 @@ """Reject inputs that are not plain ELF files with a message that names what was read.""" from pathlib import Path +import platform import subprocess import sys import tempfile @@ -23,6 +24,22 @@ def main(): convert("eboot.self.kernel", b"\x54\x14\xf5\xee" + bytes(0x1000), "The input is a SELF container, not an ELF") convert("eboot.pkg", b"\x7fCNT" + bytes(0x1000), "Invalid ELF magic number: 7f 43 4e 54") convert("eboot.short", b"\x7fEL", "File too small for ELF header") + if platform.system() == "Linux": + import resource + + source = work / "large-unsupported.pkg" + output = work / "large-unsupported.out" + with source.open("wb") as stream: + stream.write(b"\x7fFIH") + stream.truncate(8 * 1024**3) + + def limit_address_space(): + resource.setrlimit(resource.RLIMIT_AS, (512 * 1024**2, 512 * 1024**2)) + resource.setrlimit(resource.RLIMIT_CORE, (0, 0)) + + result = subprocess.run([str(relinker), "--skip-sce-module", str(source), str(output)], + capture_output=True, text=True, timeout=20, preexec_fn=limit_address_space) + assert result.returncode == 2 and "Invalid ELF magic number: 7f 46 49 48" in result.stderr and not output.exists(), result print("Input magic diagnostics tests passed") From 8625b4802bbdd97b30e52c1445555fa2e77e5412 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 20 Oct 2026 21:07:53 +0000 Subject: [PATCH 070/621] fix(relinker): normalize input prefix paths --- core/relinker/io/src/FileReader.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/core/relinker/io/src/FileReader.cpp b/core/relinker/io/src/FileReader.cpp index 35763b5df1..4f012529d3 100644 --- a/core/relinker/io/src/FileReader.cpp +++ b/core/relinker/io/src/FileReader.cpp @@ -10,7 +10,11 @@ namespace Io { std::vector FileReader::ReadPrefix(const std::string& path, const std::size_t size) const { if (size > static_cast(std::numeric_limits::max())) throw Domain::RelinkerException("File prefix is too large: " + path); - std::ifstream f(path, std::ios::binary); + const auto native = NativePath(path); + std::error_code error; + if (!std::filesystem::is_regular_file(native, error)) + throw Domain::RelinkerException("Cannot open file: " + path); + std::ifstream f(native, std::ios::binary); if (!f) throw Domain::RelinkerException("Cannot open file: " + path); std::vector buf(size); From 11adccfe15258e21e9ba6e90b947f674320ad9a8 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 20 Oct 2026 21:16:19 +0000 Subject: [PATCH 071/621] fix(relinker): preserve detailed header diagnostics --- core/relinker/main.cpp | 2 +- .../relinker/include/relinker/parsing/ElfReader.hpp | 1 + core/relinker/relinker/src/parsing/ElfReader.cpp | 6 +++++- core/relinker/relinker/tests/test_input_magic.py | 5 +++++ 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index 88c42f2797..6e10b551f5 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -41,7 +41,7 @@ struct ConvertedExecutable { ConvertedExecutable Convert(const Cli::Args& args, const std::string& inputPath, const std::string& absPath, const bool mainExecutable, const std::string& targetName, const std::vector& patches, Cli::ConsoleProgress& progress) { Io::FileReader fileReader; Relinker::ConversionInput input; - Relinker::ElfReader(fileReader.ReadPrefix(inputPath, 0x40)).ReadHeader(); + Relinker::ElfReader(fileReader.ReadPrefix(inputPath, 0x40)).ValidateMagic(); input.Executable = fileReader.Read(inputPath); input.InputPath = std::filesystem::absolute(Io::NativePath(inputPath)).lexically_normal(); input.OutputPath = Io::NativePath(absPath); diff --git a/core/relinker/relinker/include/relinker/parsing/ElfReader.hpp b/core/relinker/relinker/include/relinker/parsing/ElfReader.hpp index 692f30c43d..b8fe9513c3 100644 --- a/core/relinker/relinker/include/relinker/parsing/ElfReader.hpp +++ b/core/relinker/relinker/include/relinker/parsing/ElfReader.hpp @@ -10,6 +10,7 @@ class ElfReader : public IElfReader { public: explicit ElfReader(std::vector fileBuffer); + void ValidateMagic() const; ElfHeader ReadHeader() const override; std::vector ReadProgramHeaders() const override; std::vector ReadSectionHeaders() const override; diff --git a/core/relinker/relinker/src/parsing/ElfReader.cpp b/core/relinker/relinker/src/parsing/ElfReader.cpp index bfed4c355a..f023f3ca3c 100644 --- a/core/relinker/relinker/src/parsing/ElfReader.cpp +++ b/core/relinker/relinker/src/parsing/ElfReader.cpp @@ -76,7 +76,7 @@ std::uint64_t ElfReader::_readU64At(FileByteOffset fileByteOffset) const { return value; } -ElfHeader ElfReader::ReadHeader() const { +void ElfReader::ValidateMagic() const { if (_fileBuffer.size() < 0x40) { throw RelinkerException("File too small for ELF header"); } @@ -90,6 +90,10 @@ ElfHeader ElfReader::ReadHeader() const { _fileBuffer[2] != 'L' || _fileBuffer[3] != 'F') { throw RelinkerException("Invalid ELF magic number: " + formatMagic()); } +} + +ElfHeader ElfReader::ReadHeader() const { + ValidateMagic(); if (_fileBuffer[4] != 2 || _fileBuffer[5] != 1 || _fileBuffer[6] != 1) throw RelinkerException("Expected little-endian ELF64 version 1"); if (_readU16At(0x12) != 62) diff --git a/core/relinker/relinker/tests/test_input_magic.py b/core/relinker/relinker/tests/test_input_magic.py index ace10d9119..e26ccf337c 100644 --- a/core/relinker/relinker/tests/test_input_magic.py +++ b/core/relinker/relinker/tests/test_input_magic.py @@ -24,6 +24,11 @@ def main(): convert("eboot.self.kernel", b"\x54\x14\xf5\xee" + bytes(0x1000), "The input is a SELF container, not an ELF") convert("eboot.pkg", b"\x7fCNT" + bytes(0x1000), "Invalid ELF magic number: 7f 43 4e 54") convert("eboot.short", b"\x7fEL", "File too small for ELF header") + unsupported_version = bytearray(64) + unsupported_version[:4] = b"\x7fELF" + unsupported_version[4:7] = b"\x02\x01\x01" + unsupported_version[0x12:0x14] = b"\x3e\x00" + convert("eboot.unsupported-version", unsupported_version, "Unsupported ELF object version: expected 1") if platform.system() == "Linux": import resource 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 072/621] 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 7caad6117ed5e8f0eae0015fb8a06cb12cdd81e4 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Sat, 17 Oct 2026 18:26:35 +0000 Subject: [PATCH 073/621] 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 074/621] 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 075/621] 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 076/621] 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 077/621] 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 078/621] 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 079/621] 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 080/621] 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 081/621] 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 082/621] 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 083/621] 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 084/621] 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 085/621] 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 086/621] 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 087/621] 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 088/621] 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 089/621] 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 090/621] 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 091/621] 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 092/621] 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 093/621] 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 094/621] 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 095/621] 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 096/621] 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 097/621] 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 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 098/621] 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 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 099/621] 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 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 100/621] 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 c2b92498b566daa382d996c7928b5298304c95f7 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Tue, 20 Oct 2026 14:07:29 +0200 Subject: [PATCH 101/621] fix(libkernel): look up the descriptor of read, write, pread and pwrite before their buffer and offset A null buffer, or for pread and pwrite a negative offset, was answered with EFAULT or EINVAL before the descriptor was looked at, so a descriptor that is not open (EBADF) or a pipe (ESPIPE) was hidden. FreeBSD 11.4 sys_generic.c looks the descriptor up first (kern_readv, kern_preadv: fget, then the seekable check, then the offset) and only reaches the buffer when it copies. The check runs only when the buffer or offset is invalid, so a normal transfer costs nothing. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libkernel/File/src/Open.cpp | 4 +- core/libs/prx/libkernel/File/src/Stdio.cpp | 48 ++++++++++++++-------- core/libs/tests/GuestFilesystem.cpp | 20 +++++++++ 3 files changed, 54 insertions(+), 18 deletions(-) diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index 186ce63ff3..2aa7cb1d0d 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -313,7 +313,7 @@ std::int64_t APS5_VABI sceKernelRead(int d, void* buf, std::size_t nbytes) { return n < 0 ? SceKernelError(*__error_nid_postfix()) : n; } if (buf == nullptr) { - if (nbytes != 0) return SceIoErrorFromErrno(EFAULT); + if (nbytes != 0) return SceIoErrorFromErrno(DescriptorIsOpen_nid_no_patch(d) ? EFAULT : EBADF); char empty = 0; return sceKernelRead(d, &empty, 0); } @@ -332,7 +332,7 @@ std::int64_t APS5_VABI sceKernelWrite(int d, const void* buf, std::size_t nbytes return n < 0 ? SceKernelError(*__error_nid_postfix()) : n; } if (buf == nullptr) { - if (nbytes != 0) return SceIoErrorFromErrno(EFAULT); + if (nbytes != 0) return SceIoErrorFromErrno(DescriptorIsOpen_nid_no_patch(d) ? EFAULT : EBADF); const char empty = 0; return sceKernelWrite(d, &empty, 0); } diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 2400df7664..0b77a183ce 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -428,6 +428,28 @@ static bool FailPosixPositionedGuestSocket(int descriptor) { return true; } +static bool IsPipeDescriptor(int descriptor) { +#ifdef _WIN32 + const auto handle = reinterpret_cast(WithoutParameterHandler([descriptor] { return ::_get_osfhandle(descriptor); })); + return handle != INVALID_HANDLE_VALUE && ::GetFileType(handle) == FILE_TYPE_PIPE; +#else + struct stat status; + return ::fstat(descriptor, &status) == 0 && S_ISFIFO(status.st_mode); +#endif +} + +static int PositionedArgumentError(int descriptor, bool nullBuffer, std::int64_t offset) { + if (!nullBuffer && offset >= 0) return 0; + if (!GuestSockets::IsOpen(descriptor) && !DescriptorIsOpen_nid_no_patch(descriptor)) return GUEST_EBADF; + if (GuestSockets::IsOpen(descriptor) || IsPipeDescriptor(descriptor)) return GUEST_ESPIPE; + return offset < 0 ? GUEST_EINVAL : GUEST_EFAULT; +} + +static int TransferArgumentError(int descriptor, bool nullBuffer) { + if (!nullBuffer) return 0; + return DescriptorIsOpen_nid_no_patch(descriptor) ? GUEST_EFAULT : GUEST_EBADF; +} + static int SceErrorFromErrno(int error) { int guest = error > 0 && error <= 34 ? error : error == GUEST_ENOTEMPTY ? error : GUEST_EIO; if (error == EAGAIN) guest = GUEST_EWOULDBLOCK; @@ -957,8 +979,7 @@ int APS5_VABI _open_nid_postfix(const char* path, int flags, ...) { int64_t APS5_VABI pread_nid_postfix(int d, void* buf, size_t nbytes, int64_t offset) { if (ExceedsTransferLimit(nbytes)) return PosixFailure(GUEST_EINVAL); - if (buf == nullptr && nbytes != 0) return PosixFailure(GUEST_EFAULT); - if (offset < 0) return PosixFailure(GUEST_EINVAL); + if (const int error = PositionedArgumentError(d, buf == nullptr && nbytes != 0, offset)) return PosixFailure(error); if (FailPosixPositionedGuestSocket(d)) return -1; const GuestArena::HostWrite destination(buf, nbytes); if (!destination.Open()) return PosixFailure(GUEST_EFAULT); @@ -971,8 +992,7 @@ int64_t APS5_VABI pread_nid_postfix(int d, void* buf, size_t nbytes, int64_t off int64_t APS5_VABI pwrite_nid_disambig1_nid_postfix(int d, const void* buf, size_t nbytes, int64_t offset) { if (ExceedsTransferLimit(nbytes)) return PosixFailure(GUEST_EINVAL); - if (buf == nullptr && nbytes != 0) return PosixFailure(GUEST_EFAULT); - if (offset < 0) return PosixFailure(GUEST_EINVAL); + if (const int error = PositionedArgumentError(d, buf == nullptr && nbytes != 0, offset)) return PosixFailure(error); if (FailPosixPositionedGuestSocket(d)) return -1; GuestArena::GuestArenaPrepareHostRead_nid_no_patch(buf, nbytes); char empty = 0; @@ -983,7 +1003,7 @@ int64_t APS5_VABI pwrite_nid_disambig1_nid_postfix(int d, const void* buf, size_ int64_t APS5_VABI read_nid_postfix(int d, void* buf, uint64_t nbytes) { if (ExceedsTransferLimit(nbytes)) return PosixFailure(GUEST_EINVAL); if (GuestSockets::IsOpen(d)) return GuestSockets::Read(d, buf, static_cast(nbytes)); - if (buf == nullptr && nbytes != 0) return PosixFailure(GUEST_EFAULT); + if (const int error = TransferArgumentError(d, buf == nullptr && nbytes != 0)) return PosixFailure(error); char emptyBuffer = 0; return PosixResult(sceKernelRead(d, buf == nullptr ? &emptyBuffer : buf, static_cast(nbytes))); } @@ -991,7 +1011,7 @@ int64_t APS5_VABI read_nid_postfix(int d, void* buf, uint64_t nbytes) { std::int64_t APS5_VABI _read_nid_postfix(int descriptor, void* buffer, std::size_t count) { if (ExceedsTransferLimit(count)) return PosixFailure(GUEST_EINVAL); if (GuestSockets::IsOpen(descriptor)) return GuestSockets::Read(descriptor, buffer, count); - if (buffer == nullptr && count != 0) return PosixFailure(GUEST_EFAULT); + if (const int error = TransferArgumentError(descriptor, buffer == nullptr && count != 0)) return PosixFailure(error); char emptyBuffer = 0; return PosixResult(sceKernelRead(descriptor, buffer == nullptr ? &emptyBuffer : buffer, count)); } @@ -999,7 +1019,7 @@ std::int64_t APS5_VABI _read_nid_postfix(int descriptor, void* buffer, std::size int64_t APS5_VABI write_nid_postfix(int d, const char* str, int64_t size) { if (size < 0) return PosixFailure(GUEST_EINVAL); if (GuestSockets::IsOpen(d)) return GuestSockets::Write(d, str, static_cast(size)); - if (str == nullptr && size != 0) return PosixFailure(GUEST_EFAULT); + if (const int error = TransferArgumentError(d, str == nullptr && size != 0)) return PosixFailure(error); const char emptyBuffer = 0; return PosixResult(sceKernelWrite(d, str == nullptr ? &emptyBuffer : str, static_cast(size))); } @@ -1007,7 +1027,7 @@ int64_t APS5_VABI write_nid_postfix(int d, const char* str, int64_t size) { std::int64_t APS5_VABI _write_nid_postfix(int descriptor, const void* buffer, std::size_t count) { if (ExceedsTransferLimit(count)) return PosixFailure(GUEST_EINVAL); if (GuestSockets::IsOpen(descriptor)) return GuestSockets::Write(descriptor, buffer, count); - if (buffer == nullptr && count != 0) return PosixFailure(GUEST_EFAULT); + if (const int error = TransferArgumentError(descriptor, buffer == nullptr && count != 0)) return PosixFailure(error); const char emptyBuffer = 0; return PosixResult(sceKernelWrite(descriptor, buffer == nullptr ? &emptyBuffer : buffer, count)); } @@ -1220,8 +1240,7 @@ static void PrepareWriteIovecs(const KernelIovec* iov, int iovcnt) { #ifdef _WIN32 int64_t APS5_VABI sceKernelPread(int d, void* buf, size_t nbytes, int64_t offset) { - if (buf == nullptr && nbytes != 0) return SceErrorFromErrno(GUEST_EFAULT); - if (offset < 0) return SceErrorFromErrno(GUEST_EINVAL); + if (const int error = PositionedArgumentError(d, buf == nullptr && nbytes != 0, offset)) return SceErrorFromErrno(error); if (const auto error = PositionedGuestSocketError(d); error != 0) return error; const GuestArena::HostWrite destination(buf, nbytes); if (!destination.Open()) return SceErrorFromErrno(GUEST_EFAULT); @@ -1233,8 +1252,7 @@ int64_t APS5_VABI sceKernelPread(int d, void* buf, size_t nbytes, int64_t offset } int64_t APS5_VABI sceKernelPwrite(int d, const void* buf, size_t nbytes, int64_t offset) { - if (buf == nullptr && nbytes != 0) return SceErrorFromErrno(GUEST_EFAULT); - if (offset < 0) return SceErrorFromErrno(GUEST_EINVAL); + if (const int error = PositionedArgumentError(d, buf == nullptr && nbytes != 0, offset)) return SceErrorFromErrno(error); if (const auto error = PositionedGuestSocketError(d); error != 0) return error; GuestArena::GuestArenaPrepareHostRead_nid_no_patch(buf, nbytes); char empty = 0; @@ -1319,8 +1337,7 @@ int64_t APS5_VABI sceKernelPwritev(int d, const KernelIovec* iov, int iovcnt, in #else int64_t APS5_VABI sceKernelPread(int d, void* buf, size_t nbytes, int64_t offset) { - if (buf == nullptr && nbytes != 0) return SceErrorFromErrno(GUEST_EFAULT); - if (offset < 0) return SceErrorFromErrno(GUEST_EINVAL); + if (const int error = PositionedArgumentError(d, buf == nullptr && nbytes != 0, offset)) return SceErrorFromErrno(error); if (const auto error = PositionedGuestSocketError(d); error != 0) return error; const GuestArena::HostWrite destination(buf, nbytes); if (!destination.Open()) return SceErrorFromErrno(GUEST_EFAULT); @@ -1331,8 +1348,7 @@ int64_t APS5_VABI sceKernelPread(int d, void* buf, size_t nbytes, int64_t offset } int64_t APS5_VABI sceKernelPwrite(int d, const void* buf, size_t nbytes, int64_t offset) { - if (buf == nullptr && nbytes != 0) return SceErrorFromErrno(GUEST_EFAULT); - if (offset < 0) return SceErrorFromErrno(GUEST_EINVAL); + if (const int error = PositionedArgumentError(d, buf == nullptr && nbytes != 0, offset)) return SceErrorFromErrno(error); if (const auto error = PositionedGuestSocketError(d); error != 0) return error; GuestArena::GuestArenaPrepareHostRead_nid_no_patch(buf, nbytes); const auto result = NativePwrite(d, buf, nbytes, offset); diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index ec64c41dfb..82cd95ebdb 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -98,6 +98,8 @@ int APS5_VABI dup2_nid_postfix(int, int); std::int64_t APS5_VABI lseek_nid_postfix(int, std::int64_t, 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 pread_nid_postfix(int, void*, std::size_t, std::int64_t); +std::int64_t APS5_VABI sceKernelWrite(int, const void*, std::size_t); int APS5_VABI sceKernelDebugOutText(int, const char*); } #ifdef _WIN32 @@ -1037,6 +1039,24 @@ int main() { Require(lseek_nid_postfix(-1, 0, 7) == -1 && *__error_nid_postfix() == 9); Require(lseek_nid_postfix(0x7fffffff, 0, -1) == -1 && *__error_nid_postfix() == 9); Require(lseek_nid_postfix(locked, 0, 7) == -1 && *__error_nid_postfix() == 22); + { + char transfer[4]{}; + int transferPipe[2] = {-1, -1}; + Require(pipe_nid_postfix(transferPipe) == 0); + Require(read_nid_postfix(-1, nullptr, 1) == -1 && *__error_nid_postfix() == 9); + Require(write_nid_postfix(-1, nullptr, 1) == -1 && *__error_nid_postfix() == 9); + Require(sceKernelRead(-1, nullptr, 1) == static_cast(0x80020009u)); + Require(sceKernelWrite(-1, nullptr, 1) == static_cast(0x80020009u)); + Require(read_nid_postfix(locked, nullptr, 1) == -1 && *__error_nid_postfix() == 14); + Require(pread_nid_postfix(-1, transfer, 1, -1) == -1 && *__error_nid_postfix() == 9); + Require(pread_nid_postfix(-1, nullptr, 1, 0) == -1 && *__error_nid_postfix() == 9); + Require(sceKernelPread(-1, transfer, 1, -1) == static_cast(0x80020009u)); + Require(pread_nid_postfix(transferPipe[0], transfer, 1, -1) == -1 && *__error_nid_postfix() == 29); + Require(pread_nid_postfix(locked, transfer, 1, -1) == -1 && *__error_nid_postfix() == 22); + Require(pread_nid_postfix(locked, nullptr, 1, -1) == -1 && *__error_nid_postfix() == 22); + 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(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 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 102/621] 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 53b73b5dae17ab7005c93e766a3dc66fa2e10310 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Tue, 20 Oct 2026 19:53:17 -0400 Subject: [PATCH 103/621] fix(agc): prefer imported prefixes before sparse heap mirrors --- .../Graphics/src/GuestBufferMemory.cpp | 12 +-- .../libSceAgcDriver/tests/BdaResources.cpp | 75 +++++++++++++++++++ 2 files changed, 81 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 7944e8b199..3281d2d62e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -4638,12 +4638,6 @@ void GuestBufferMemory::UploadFinish(bool addressable) { if (profile) refreshUs += microsecondsSince(refreshStart); continue; } - if (sparseHeapCandidate(region)) { - const auto refreshStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; - const bool bound = bindSparseMirror(region, importRanges()); - if (profile) refreshUs += microsecondsSince(refreshStart); - if (bound) continue; - } bool copyOnGpu = false; if (context.hostImportAlignment != 0) { auto& state = Imports(); @@ -4730,6 +4724,12 @@ void GuestBufferMemory::UploadFinish(bool addressable) { gpuCopies.push_back(®ion); continue; } + if (sparseHeapCandidate(region)) { + const auto refreshStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; + const bool bound = bindSparseMirror(region, importRanges()); + if (profile) refreshUs += microsecondsSince(refreshStart); + if (bound) continue; + } if (region.deviceLocal) { // The staging shadow UploadPrepare made cannot take the CPU copy (no host mapping): the // region falls back to a host buffer of its own. diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index 6d625cf165..c52c6b4a02 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -2182,6 +2182,80 @@ void importWatchTests() { #endif } +void sparseImportedHeapTests(const Context& context, const BdaTestAccess& access) { +#ifndef _WIN32 + if (!GuestWriteWatch::GuestWriteWatchAvailable_nid_postfix()) { + std::cout << "write watch unavailable: sparse imported heaps not tested\n"; + return; + } + constexpr std::size_t bytes = 65536; + void* raw = mmap(nullptr, 4 * bytes, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(raw != MAP_FAILED, "cannot reserve the sparse imported heap fixture"); + const auto base = (reinterpret_cast(raw) + bytes - 1) & ~(bytes - 1); + auto* guest = reinterpret_cast(base); + Require(mprotect(guest, bytes, PROT_READ | PROT_WRITE) == 0, "cannot commit the sparse imported heap prefix"); + std::memset(guest, 0x57, bytes); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(guest, bytes); + auto importing = context; + importing.hostImportAlignment = bytes; + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, guest, bytes, true, true, true); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + const auto* imported = HostImportFor(importing, base, bytes); + Require(imported != nullptr, "the sparse heap prefix was not imported"); + const auto before = MirrorCounters(); + for (unsigned index = 0; index < 3; ++index) { + { + GuestBufferMemory described(importing); + described.AddReadable(base, 2 * bytes); + described.Upload(false); + std::uint32_t adjustment = 0; + const auto view = described.Descriptor(base, 2 * bytes, adjustment); + Require(view.buffer == imported->buffer && view.range == bytes && adjustment == 0, "the sparse descriptor did not bind its imported prefix"); + Require(described.CopiedBytes() == 0 && MirrorCounters().rebuilds == before.rebuilds, "the sparse imported descriptor rebuilt or copied its heap mirror"); + reject([&] { static_cast(described.Descriptor(base + bytes + 16, 4, adjustment)); }, "starts past its copy"); + described.WriteBack(); + } + { + GuestBufferMemory addressed(importing); + addressed.AcquireRegistered(); + addressed.Upload(true); + Require(MirrorCounters().rebuilds == before.rebuilds && MirrorCounters().heapMirrors == before.heapMirrors, "alternating descriptor and address builds rebuilt the imported heap mirror"); + addressed.WriteBack(); + } + } + { + GuestBufferMemory written(importing); + written.AddWritable(base, 2 * bytes); + written.Upload(false); + std::uint32_t adjustment = 0; + const auto view = written.Descriptor(base, 2 * bytes, adjustment); + Require(view.buffer != imported->buffer && view.range == bytes && MirrorCounters().rebuilds == before.rebuilds + 1, "a sparse writable descriptor bound a shortened import"); + access.bytes(view.buffer)[view.offset] = std::byte{0x68}; + guest[8] = 0x79; + written.WriteBack(); + Require(guest[0] == 0x68 && guest[8] == 0x79, "the sparse writable fallback lost a GPU store or rolled back a CPU store"); + } + mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, guest); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + Require(HostImportFor(importing, base, bytes) == nullptr, "the sparse heap import outlived its range"); + { + GuestBufferMemory sweep(importing); + sweep.AcquireRegistered(); + sweep.Upload(true); + sweep.WriteBack(); + } + Require(MirrorCounters().heapMirrors == before.heapMirrors, "the sparse imported heap fixture leaked its mirror"); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(guest, bytes); + munmap(raw, 4 * bytes); + std::cout << "Sparse imported heap tests passed\n"; +#else + static_cast(context); + static_cast(access); +#endif +} + void heapMirrorTests(const Context& context, const BdaTestAccess& access) { if (!GuestArena::GuestArenaAvailable_nid_postfix() || !GuestArena::GuestArenaWriteWatched_nid_postfix()) { std::cout << "guest arena unavailable or not write-watched: heap mirrors not tested\n"; @@ -3393,6 +3467,7 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { importReportFloorTests(context, access, true); importReportFloorTests(context, access, false); importReportRefusalMemoryTests(context, access); + sparseImportedHeapTests(context, access); heapMirrorTests(context, access); importedHeapMirrorTests(context, access); importWatchTests(); 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 104/621] 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 105/621] 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 106/621] 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 107/621] 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 23a8673cfbe6434867ca0529a21a0e196bc8364a Mon Sep 17 00:00:00 2001 From: frankea Date: Tue, 20 Oct 2026 21:56:44 -0400 Subject: [PATCH 108/621] 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 109/621] 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 7b07c20497015fac54bfa01735ba1eb7b7e8fecf Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Wed, 21 Oct 2026 03:41:57 +0000 Subject: [PATCH 110/621] fix(relinker): resolve kernel variant imports via canonical kernel --- .../elfpatcher/src/windows/WindowsEntryStubBuilder.cpp | 5 +++++ .../relinker/elfpatcher/src/windows/WindowsImportBuilder.cpp | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp index 175a42f755..dbcca232a2 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp @@ -172,6 +172,11 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con const bool hostModule = !import.Library.empty() && std::any_of(order.begin(), order.end(), [&](auto index) { return index >= guestModules.size(); }); if (hostModule) { + if (library == "libkernel_sys.prx" || library == "libkernel_web.prx") { + for (std::size_t index = guestModules.size(); index < libraries.size(); ++index) { + if (libraries[index] == "libkernel.prx") order.push_back(CheckedRva(index)); + } + } for (std::size_t index = guestModules.size(); index < libraries.size(); ++index) { if (libraries[index] == "libc.prx" && library != "libc.prx") order.push_back(CheckedRva(index)); } diff --git a/core/relinker/elfpatcher/src/windows/WindowsImportBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsImportBuilder.cpp index e7153da217..4f41f50b82 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsImportBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsImportBuilder.cpp @@ -55,7 +55,7 @@ std::vector WindowsImportBuilder::ReadLibraries(const Domain::SysVD // Keep the requested module first, then make its shared runtime visible. if ((unique.contains("libSceLibcInternal.prx") || unique.contains("libkernel.prx")) && !unique.contains("libc.prx")) result.push_back("libc.prx"); - if (unique.contains("libkernel_web.prx") && !unique.contains("libkernel.prx")) + if ((unique.contains("libkernel_sys.prx") || unique.contains("libkernel_web.prx")) && !unique.contains("libkernel.prx")) result.push_back("libkernel.prx"); return result; } From fef2755c76cb1f567c82dfeaaf646b88e241c3a6 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Wed, 21 Oct 2026 03:41:57 +0000 Subject: [PATCH 111/621] test(relinker): cover scoped kernel variant imports --- .../elfpatcher/tests/EntryStubTests.cpp | 29 +++++++++++++++++++ .../WindowsDependencyDiagnosticsTests.cpp | 2 ++ 2 files changed, 31 insertions(+) diff --git a/core/relinker/elfpatcher/tests/EntryStubTests.cpp b/core/relinker/elfpatcher/tests/EntryStubTests.cpp index 5b3b8a78b1..5802231e59 100644 --- a/core/relinker/elfpatcher/tests/EntryStubTests.cpp +++ b/core/relinker/elfpatcher/tests/EntryStubTests.cpp @@ -1,6 +1,8 @@ #include #include #include +#include +#include #include #include #include @@ -116,6 +118,31 @@ void windowsStubRejectsAnOverlongAllocatorPath() { throw std::runtime_error("an allocator path past the startup buffer was accepted"); } +void windowsKernelVariantImportsSearchCanonicalKernelAfterTheRequestedVariant() { + using namespace Elfpatcher::Windows; + const auto nativeImports = WindowsImportBuilder().Build(0x1000); + const std::vector libraries{"libkernel_sys.prx", "libc.prx", "libkernel.prx"}; + const std::vector imports{{.Name = "getpagesize", .TargetRva = 0x2000, .Addend = 0, .Library = "libkernel_sys.prx"}}; + const auto stub = WindowsEntryStubBuilder().Build(0x40000, 0x1000, nativeImports, libraries, imports, "$ORIGIN/libs", false, false); + const std::vector expected{0, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0}; + require(std::search(stub.Data.Data.begin(), stub.Data.Data.end(), expected.begin(), expected.end()) != stub.Data.Data.end(), + "a scoped libkernel_sys import does not search the requested variant before canonical libkernel and libc"); +} + +void windowsKernelVariantDependenciesLoadCanonicalKernel() { + using namespace Elfpatcher::Windows; + for (const auto& variant : {std::string("libkernel_sys.prx"), std::string("libkernel_web.prx")}) { + Domain::SysVDynamicSection dynamic; + dynamic.DynStrData.insert(dynamic.DynStrData.end(), variant.begin(), variant.end()); + dynamic.DynStrData.push_back(0); + dynamic.DynamicSegmentData.resize(16); + Io::WriteU64(dynamic.DynamicSegmentData, 0, 1); + Io::WriteU64(dynamic.DynamicSegmentData, 8, 0); + require(WindowsImportBuilder().ReadLibraries(dynamic) == std::vector{variant, "libkernel.prx"}, + variant + ": canonical libkernel is not loaded after the requested module"); + } +} + } int main() { @@ -126,6 +153,8 @@ int main() { windowsStubLoadsTheAllocatorOnRequest("C:/Games/Title/libs", u"C:\\Games\\Title\\libs\\", {0x48, 0x8d, 0x0d}); windowsStubLoadsTheAllocatorOnRequest("$ORIGIN/libs", u"libs\\", {0x48, 0x8d, 0x35}); windowsStubRejectsAnOverlongAllocatorPath(); + windowsKernelVariantImportsSearchCanonicalKernelAfterTheRequestedVariant(); + windowsKernelVariantDependenciesLoadCanonicalKernel(); } catch (const std::exception& error) { std::cerr << error.what() << '\n'; return 1; diff --git a/core/relinker/elfpatcher/tests/WindowsDependencyDiagnosticsTests.cpp b/core/relinker/elfpatcher/tests/WindowsDependencyDiagnosticsTests.cpp index fa6c1d4085..7a0d29e1bf 100644 --- a/core/relinker/elfpatcher/tests/WindowsDependencyDiagnosticsTests.cpp +++ b/core/relinker/elfpatcher/tests/WindowsDependencyDiagnosticsTests.cpp @@ -55,6 +55,8 @@ void checkRuntimeDependencies() { {"libc.prx", "libSceLibcInternal.prx"}); check({"libSceVideoOut.prx", "libkernel.prx", "libSceVideoOut.prx"}, {"libSceVideoOut.prx", "libkernel.prx", "libc.prx"}); + check({"libkernel_sys.prx"}, {"libkernel_sys.prx", "libkernel.prx"}); + check({"libkernel_web.prx"}, {"libkernel_web.prx", "libkernel.prx"}); check({"libSceVideoOut.prx", "libSceVideoOut.prx"}, {"libSceVideoOut.prx"}); } 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 112/621] 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 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 113/621] 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 114/621] 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 115/621] 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 116/621] 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 0c7d25c3d1574652f30965fb09e8a0df93a9afdc Mon Sep 17 00:00:00 2001 From: harisibrar1 <207127467+harisibrar1@users.noreply.github.com> Date: Wed, 21 Oct 2026 00:50:41 -0400 Subject: [PATCH 117/621] ci(conventions): check pull requests with the current base branch rules The workflow checked out pull_request.base.sha, the base recorded when the pull request was opened, and ran that revision's check_conventions.py. A rule merged after a pull request was opened never ran on it: #7005 added APS5_NO_WGI without an ENVIRONMENT.md row and passed with the checker of 07e741481, which had no env-reference rule. Check out the current tip of the base branch and pass it as --base; the checker already diffs from the merge base, so only the pull request's own changes are checked. A name documented on the base branch after the branch point now counts as documented unless the pull request removes its row. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/conventions.yml | 7 ++-- CONTRIBUTING.md | 2 +- tools/check_conventions.py | 6 ++- .../test_check_conventions_env_reference.py | 42 ++++++++++++++++--- 4 files changed, 45 insertions(+), 12 deletions(-) diff --git a/.github/workflows/conventions.yml b/.github/workflows/conventions.yml index 2a14c1c2a3..d09210cfd0 100644 --- a/.github/workflows/conventions.yml +++ b/.github/workflows/conventions.yml @@ -18,15 +18,14 @@ jobs: steps: - uses: actions/checkout@v7 with: - ref: ${{ github.event.pull_request.base.sha }} + ref: ${{ github.event.pull_request.base.ref }} fetch-depth: 0 persist-credentials: false - name: Fetch pull request objects without checking out its code env: HEAD_SHA: ${{ github.event.pull_request.head.sha }} run: git fetch --no-tags origin "$HEAD_SHA" - - name: Check conventions with the trusted base script + - name: Check conventions with the current base branch script env: - BASE_SHA: ${{ github.event.pull_request.base.sha }} HEAD_SHA: ${{ github.event.pull_request.head.sha }} - run: python3 -I tools/check_conventions.py --base "$BASE_SHA" --head "$HEAD_SHA" + run: python3 -I tools/check_conventions.py --base HEAD --head "$HEAD_SHA" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 27ccf98173..2e218f13f7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -41,7 +41,7 @@ This manual run also builds and tests the relinker on Linux, Windows and macOS w With `--field full=false`, the manual run skips the full builds and runs only the relinker matrix on Linux, Windows and macOS, if the pull request changes a file the relinker-only build uses, such as the Python tools and their tests in `tools/`. -The Conventions check runs automatically for every contributor, including first-time contributors. It uses the base branch checker and reads pull request Git objects without executing pull request code. The Conventions check runs on every pull request and fails when a rule on this page is broken. It accepts code comments only when the pull request also changes [technical debt](docs/dev/TechnicalDebt.md), the index or a file in `docs/dev/debt/`, and only UTF-8 text files. Run it locally before pushing: +The Conventions check runs automatically for every contributor, including first-time contributors. It uses the checker from the current tip of the base branch, so rules added after a pull request was opened apply to its next run, and reads pull request Git objects without executing pull request code. The Conventions check runs on every pull request and fails when a rule on this page is broken. It accepts code comments only when the pull request also changes [technical debt](docs/dev/TechnicalDebt.md), the index or a file in `docs/dev/debt/`, and only UTF-8 text files. Run it locally before pushing: ``` python3 tools/check_conventions.py --base origin/main diff --git a/tools/check_conventions.py b/tools/check_conventions.py index 9c3eea8e60..586c1d1e1e 100644 --- a/tools/check_conventions.py +++ b/tools/check_conventions.py @@ -93,6 +93,7 @@ def git(*args, text=True): class Check: def __init__(self, base, head): self.head = head + self.target = base self.base = git("merge-base", base, head).strip() self.findings = [] self.added = defaultdict(list) @@ -361,7 +362,10 @@ class Check: reads = set() for line in result.stdout.splitlines(): reads.update(match.group("name") for _, match in env_reference.line_reads(line)) - documented = set(env_reference.documented_pattern(env_reference.PREFIXES).findall(self.show(doc))) if doc in self.tree else set() + 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: + documented |= set(pattern.findall(git("show", f"{self.target}:{doc}"))) - dropped for name, (source, line) in sorted(added.items()): if name not in documented: self.report("env-reference", source, line, f"{name} is read here but has no row in {doc}") diff --git a/tools/tests/test_check_conventions_env_reference.py b/tools/tests/test_check_conventions_env_reference.py index 0ccd9bf93c..b217e80247 100644 --- a/tools/tests/test_check_conventions_env_reference.py +++ b/tools/tests/test_check_conventions_env_reference.py @@ -14,7 +14,7 @@ SAMPLE = "core/libs/prx/libSceExample/Sample.cpp" DOC_TEXT = "| `APS5_DOCUMENTED` | presence |\n| `APS5_GONE` | presence |\n" -def repository(root, base, head): +def repository(root, base, head, target=None): def git(*args): subprocess.run(["git", "-c", f"safe.directory={root}", "-c", "user.name=Conventions Test", "-c", "user.email=test@example.com", "-c", "commit.gpgsign=false", *args], @@ -34,16 +34,23 @@ def repository(root, base, head): git("add", ".") git("commit", "-qm", "docs: add base") git("tag", "base") + if target is not None: + git("branch", "pull") + apply(target) + git("add", "-A") + git("commit", "-qm", "docs: advance the target") + git("tag", "target") + git("checkout", "-q", "pull") apply(head) git("add", "-A") git("commit", "-qm", "feat: read a new name") -def findings_of(root, base, head): +def findings_of(root, base, head, ref="base"): previous = os.getcwd() os.chdir(root) try: - check = Check("base", "HEAD") + check = Check(ref, "HEAD") check.env_reference() return check.findings finally: @@ -51,12 +58,12 @@ def findings_of(root, base, head): class EnvReferenceDiffTests(unittest.TestCase): - def run_case(self, base, head): + def run_case(self, base, head, target=None): directory = tempfile.TemporaryDirectory() self.addCleanup(directory.cleanup) root = Path(directory.name) - repository(root, base, head) - return findings_of(root, base, head) + repository(root, base, head, target) + return findings_of(root, base, head, "base" if target is None else "target") def test_only_a_name_the_pull_request_adds_is_reported_at_its_line(self): base = { @@ -143,6 +150,29 @@ class EnvReferenceDiffTests(unittest.TestCase): self.assertEqual(self.run_case(base, head), [("env-reference", DOC, 0, "APS5_GONE has a row but the code no longer reads it")]) + def test_a_row_the_target_added_after_the_branch_point_documents_the_read(self): + base = {SAMPLE: "int unrelated;\n"} + target = {DOC: "| `APS5_NEW_READ` | presence |\n"} + head = {SAMPLE: 'static bool a = std::getenv("APS5_NEW_READ") != nullptr;\n'} + self.assertEqual(self.run_case(base, head, target), []) + + def test_a_read_the_target_does_not_document_is_still_reported(self): + base = {SAMPLE: "int unrelated;\n"} + target = {DOC: DOC_TEXT} + head = {SAMPLE: 'static bool a = std::getenv("APS5_NEW_READ") != nullptr;\n'} + self.assertEqual(self.run_case(base, head, target), [("env-reference", SAMPLE, 1, + f"APS5_NEW_READ is read here but has no row in {DOC}")]) + + def test_a_row_the_pull_request_removes_is_reported_although_the_target_keeps_it(self): + base = { + DOC: DOC_TEXT, + SAMPLE: 'static bool a = std::getenv("APS5_DOCUMENTED") != nullptr;\n', + } + target = {"core/libs/prx/libSceExample/Other.cpp": "int other;\n"} + head = {DOC: "| `APS5_GONE` | presence |\n"} + self.assertEqual(self.run_case(base, head, target), [("env-reference", DOC, 0, + "APS5_DOCUMENTED is still read by the code but this pull request removes its row")]) + if __name__ == "__main__": unittest.main() From 054f8ff4de13a9bcd8329e762d8f38a609d74fc4 Mon Sep 17 00:00:00 2001 From: thuanquang Date: Wed, 21 Oct 2026 11:16:03 +0700 Subject: [PATCH 118/621] 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 0d0b29cf446f035b6adf34d4d21400e8e8633d0f Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Wed, 21 Oct 2026 05:47:05 +0000 Subject: [PATCH 119/621] test(relinker): require libc for kernel variant fallback --- core/relinker/elfpatcher/tests/EntryStubTests.cpp | 4 ++-- .../elfpatcher/tests/WindowsDependencyDiagnosticsTests.cpp | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core/relinker/elfpatcher/tests/EntryStubTests.cpp b/core/relinker/elfpatcher/tests/EntryStubTests.cpp index 5802231e59..2dc67f2ec2 100644 --- a/core/relinker/elfpatcher/tests/EntryStubTests.cpp +++ b/core/relinker/elfpatcher/tests/EntryStubTests.cpp @@ -138,8 +138,8 @@ void windowsKernelVariantDependenciesLoadCanonicalKernel() { dynamic.DynamicSegmentData.resize(16); Io::WriteU64(dynamic.DynamicSegmentData, 0, 1); Io::WriteU64(dynamic.DynamicSegmentData, 8, 0); - require(WindowsImportBuilder().ReadLibraries(dynamic) == std::vector{variant, "libkernel.prx"}, - variant + ": canonical libkernel is not loaded after the requested module"); + require(WindowsImportBuilder().ReadLibraries(dynamic) == std::vector{variant, "libkernel.prx", "libc.prx"}, + variant + ": canonical kernel fallback and its libc provider are not loaded after the requested module"); } } diff --git a/core/relinker/elfpatcher/tests/WindowsDependencyDiagnosticsTests.cpp b/core/relinker/elfpatcher/tests/WindowsDependencyDiagnosticsTests.cpp index 7a0d29e1bf..15197aa868 100644 --- a/core/relinker/elfpatcher/tests/WindowsDependencyDiagnosticsTests.cpp +++ b/core/relinker/elfpatcher/tests/WindowsDependencyDiagnosticsTests.cpp @@ -55,8 +55,8 @@ void checkRuntimeDependencies() { {"libc.prx", "libSceLibcInternal.prx"}); check({"libSceVideoOut.prx", "libkernel.prx", "libSceVideoOut.prx"}, {"libSceVideoOut.prx", "libkernel.prx", "libc.prx"}); - check({"libkernel_sys.prx"}, {"libkernel_sys.prx", "libkernel.prx"}); - check({"libkernel_web.prx"}, {"libkernel_web.prx", "libkernel.prx"}); + check({"libkernel_sys.prx"}, {"libkernel_sys.prx", "libkernel.prx", "libc.prx"}); + check({"libkernel_web.prx"}, {"libkernel_web.prx", "libkernel.prx", "libc.prx"}); check({"libSceVideoOut.prx", "libSceVideoOut.prx"}, {"libSceVideoOut.prx"}); } From 7fdad075c3d25cd03120ff584a28f4d834d17c51 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Wed, 21 Oct 2026 05:47:33 +0000 Subject: [PATCH 120/621] fix(relinker): include libc for kernel variant imports --- .../elfpatcher/src/windows/WindowsImportBuilder.cpp | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/core/relinker/elfpatcher/src/windows/WindowsImportBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsImportBuilder.cpp index 4f41f50b82..f2d45060be 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsImportBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsImportBuilder.cpp @@ -53,10 +53,15 @@ std::vector WindowsImportBuilder::ReadLibraries(const Domain::SysVD // AnyPS5 implements the C runtime in libc.prx. Windows GetProcAddress // does not search a module's dependencies as ELF symbol lookup does. // Keep the requested module first, then make its shared runtime visible. - if ((unique.contains("libSceLibcInternal.prx") || unique.contains("libkernel.prx")) && !unique.contains("libc.prx")) - result.push_back("libc.prx"); - if ((unique.contains("libkernel_sys.prx") || unique.contains("libkernel_web.prx")) && !unique.contains("libkernel.prx")) - result.push_back("libkernel.prx"); + const auto addIfMissing = [&](const char* library) { + if (unique.insert(library).second) result.emplace_back(library); + }; + if (unique.contains("libSceLibcInternal.prx") || unique.contains("libkernel.prx")) + addIfMissing("libc.prx"); + if (unique.contains("libkernel_sys.prx") || unique.contains("libkernel_web.prx")) { + addIfMissing("libkernel.prx"); + addIfMissing("libc.prx"); + } return result; } 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 121/621] 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 122/621] 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 aa56b002285182763ac422483bd384d309568ee5 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Wed, 21 Oct 2026 08:06:00 +0200 Subject: [PATCH 123/621] test(libSceNet): allow more timed sends before the blocking-send test expects EAGAIN The test fills the send path with up to 16 timed sends and expects one of them to return EAGAIN. Where the loopback receive buffer can grow to 32 MiB (tcp_rmem max 33554432), 16 sends of at most 4 MiB do not fill it and the three net_dontwait_blocking_send tests fail with "peer never exhausted the send buffers". Raise the bound to 64; the loop still ends at the first EAGAIN. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/tests/NetBlockingSend.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/tests/NetBlockingSend.cpp b/core/libs/tests/NetBlockingSend.cpp index fb64d834ba..6e3b3fdebb 100644 --- a/core/libs/tests/NetBlockingSend.cpp +++ b/core/libs/tests/NetBlockingSend.cpp @@ -113,7 +113,7 @@ int main(int argc, char** argv) { Require(partial > 0 && partial < static_cast(bytes.size()), "timeout must return queued bytes"); Require(std::chrono::steady_clock::now() - started >= std::chrono::milliseconds(80), "partial send returned before its timeout"); bool emptyTimeout = false; - for (int attempt = 0; attempt < 16; ++attempt) { + for (int attempt = 0; attempt < 64; ++attempt) { const auto result = Send(api, connection.sender, bytes); if (result < 0) { Require(*sceNetErrnoLoc() == NET_EAGAIN, "timeout without progress must return EAGAIN"); 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 124/621] 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 125/621] 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 126/621] 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 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 127/621] 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 8cd100e486b9d278f8469499cae802a00229255d Mon Sep 17 00:00:00 2001 From: CPritch Date: Wed, 21 Oct 2026 07:37:09 +0100 Subject: [PATCH 128/621] 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 da5c7a895d0c6bd4d7b5246b6156b0c2c5577e55 Mon Sep 17 00:00:00 2001 From: thuanquang Date: Mon, 19 Oct 2026 23:41:56 +0700 Subject: [PATCH 129/621] fix(agc): give AMD's proprietary driver robustBufferAccess2 again enabling it broke agc_driver_layered_volume_draw, agc_driver_layered_volume_vertex and agc_driver_subvector_loop on an RX 6700 XT. With #7623 and #7828 on main, those tests pass with the feature on, in either library-mode order. This reverts 48b394d1e and keeps the record of #7623's empty-mask test in the s_subvector_loop debt entry. Co-Authored-By: Claude --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 9 ----- .../Execution/include/RobustBufferAccess.hpp | 14 -------- .../Execution/src/VulkanDevice.cpp | 10 +----- .../tests/RobustBufferAccess.cpp | 33 ------------------- docs/dev/debt/shader.md | 4 +-- 5 files changed, 3 insertions(+), 67 deletions(-) delete mode 100644 core/libs/prx/libSceAgcDriver/Execution/include/RobustBufferAccess.hpp delete mode 100644 core/libs/prx/libSceAgcDriver/tests/RobustBufferAccess.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 2782023e6a..c94391c9b4 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -712,15 +712,6 @@ if(BUILD_TESTING) add_test(NAME agc_pipeline_link_optimization COMMAND agc_pipeline_link_optimization_tests) endif() -add_test_executable(agc_robust_buffer_access_tests EXCLUDE_FROM_ALL tests/RobustBufferAccess.cpp) -target_include_directories(agc_robust_buffer_access_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) -set_target_properties(agc_robust_buffer_access_tests PROPERTIES CXX_EXTENSIONS OFF) -configure_windows_unwind(agc_robust_buffer_access_tests) -if(BUILD_TESTING) - set_target_properties(agc_robust_buffer_access_tests PROPERTIES EXCLUDE_FROM_ALL OFF) - add_test(NAME agc_robust_buffer_access COMMAND agc_robust_buffer_access_tests) -endif() - add_test_executable(agc_draw_inputs_in_place_driver_tests EXCLUDE_FROM_ALL tests/DrawInputsInPlace.cpp) target_include_directories(agc_draw_inputs_in_place_driver_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) set_target_properties(agc_draw_inputs_in_place_driver_tests PROPERTIES CXX_EXTENSIONS OFF) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/RobustBufferAccess.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/RobustBufferAccess.hpp deleted file mode 100644 index a180b23104..0000000000 --- a/core/libs/prx/libSceAgcDriver/Execution/include/RobustBufferAccess.hpp +++ /dev/null @@ -1,14 +0,0 @@ -#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_ROBUSTBUFFERACCESS_HPP -#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_ROBUSTBUFFERACCESS_HPP - -#include - -namespace AgcDriver { - -inline bool RobustBufferAccess2Usable(VkDriverId driver) { - return driver != VK_DRIVER_ID_AMD_PROPRIETARY; -} - -} - -#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 226c792301..0ed7b7bf1f 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -18,7 +18,6 @@ #include "prx/libSceAgcDriver/Execution/include/Float32FlushControl.hpp" #include "prx/libSceAgcDriver/Execution/include/PipelineLinkOptimization.hpp" #include "prx/libSceAgcDriver/Execution/include/DrawInputsInPlace.hpp" -#include "prx/libSceAgcDriver/Execution/include/RobustBufferAccess.hpp" #include "prx/libSceAgcDriver/Execution/include/SubgroupBarrier.hpp" #include "prx/libSceAgcDriver/Execution/include/SubgroupClock.hpp" #include "prx/libSceAgcDriver/Execution/include/ShaderDeviceProfile.hpp" @@ -1915,14 +1914,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : VkPhysicalDeviceFeatures2 queried{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &robustness2}; state->InstanceFunction("vkGetPhysicalDeviceFeatures2")(selected, &queried); } - VkDriverId robustnessDriver = VK_DRIVER_ID_MAX_ENUM; - if (hasExtension(VK_KHR_DRIVER_PROPERTIES_EXTENSION_NAME)) { - VkPhysicalDeviceDriverProperties driver{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRIVER_PROPERTIES}; - VkPhysicalDeviceProperties2 driverProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &driver}; - state->InstanceFunction("vkGetPhysicalDeviceProperties2")(selected, &driverProperties); - robustnessDriver = driver.driverID; - } - state->robustBufferAccess2 = robustness2.robustBufferAccess2 == VK_TRUE && enabled.robustBufferAccess == VK_TRUE && RobustBufferAccess2Usable(robustnessDriver); + state->robustBufferAccess2 = robustness2.robustBufferAccess2 == VK_TRUE && enabled.robustBufferAccess == VK_TRUE; if (robustness2.nullDescriptor == VK_TRUE || state->robustBufferAccess2) { const bool nullDescriptor = robustness2.nullDescriptor == VK_TRUE; robustness2 = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ROBUSTNESS_2_FEATURES_EXT}; diff --git a/core/libs/prx/libSceAgcDriver/tests/RobustBufferAccess.cpp b/core/libs/prx/libSceAgcDriver/tests/RobustBufferAccess.cpp deleted file mode 100644 index 6b0331109f..0000000000 --- a/core/libs/prx/libSceAgcDriver/tests/RobustBufferAccess.cpp +++ /dev/null @@ -1,33 +0,0 @@ -#include "prx/libSceAgcDriver/Execution/include/RobustBufferAccess.hpp" -#include - -namespace { - -struct Case { - VkDriverId driver; - bool usable; -}; - -constexpr Case Cases[]{ - {VK_DRIVER_ID_AMD_PROPRIETARY, false}, - {VK_DRIVER_ID_AMD_OPEN_SOURCE, true}, - {VK_DRIVER_ID_MESA_RADV, true}, - {VK_DRIVER_ID_NVIDIA_PROPRIETARY, true}, - {VK_DRIVER_ID_INTEL_PROPRIETARY_WINDOWS, true}, - {VK_DRIVER_ID_INTEL_OPEN_SOURCE_MESA, true}, - {VK_DRIVER_ID_MESA_LLVMPIPE, true}, - {VK_DRIVER_ID_MOLTENVK, true}, -}; - -} - -int main() { - int failures = 0; - for (const auto& item : Cases) { - if (AgcDriver::RobustBufferAccess2Usable(item.driver) != item.usable) { - std::fprintf(stderr, "driver %d: expected robustBufferAccess2 to be %s\n", static_cast(item.driver), item.usable ? "usable" : "unusable"); - ++failures; - } - } - return failures == 0 ? 0 : 1; -} diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 5c42726ddc..4e1b62f1de 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -17,7 +17,7 @@ - [Color comparison descriptor forms](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp): emulated color comparison supports UNORM, SNORM and float formats for 2D, 2D-array and cube views. 8-bit sRGB and UNORM (`Format8Srgb`, `Format8_8Srgb`, `Format8_8_8_8Srgb`, `Format8UNorm`, `Format8_8UNorm`, `Format8_8_8_8UNorm`) return 0 for every compare function, measured with `image_sample_c_lz` on gfx1035 over references -0.5..1.5 and texel codes 0..255. Anisotropic filters are refused, except on those 8-bit formats, where every texel comparison is 0 (not measured with an anisotropic S#). Bilinear cube comparisons of float, snorm and the other unorm formats without `DISABLE_CUBE_WRAP` are refused; on that hardware a 4x4 R32 float cube blends across faces (46 of 90 edge rows differ from a per-face clamp; face 0, ref 0.1, u=3.9 v=2.5 is 0.6016 and u=1.5 v=0.1 is 0.3984), and with `DISABLE_CUBE_WRAP` the per-face clamp matches. An 8-bit sRGB or UNORM bilinear cube without that bit returns 0, since every texel comparison is already 0 for every compare function and the selected face cannot change the result. Cube-edge sampling behavior and console coverage on PS5's gfx1013 remain unverified. - [s_setkill](../../../core/shader/recompiler/RdnaDecoder/src/RdnaScalarOpDecoder.cpp) (core/shader/recompiler) - decoded as `s_endpgm` for any immediate: measured on RDNA2 hardware (gfx1035), a store before it is visible and nothing after it runs ([#771](https://github.com/boykopovar/AnyPS5/pull/771)); whether the KILL status bit it sets is observable differently from `s_endpgm` is unverified - [s_sethalt](../../../core/shader/recompiler/Translation/src/ScalarInstructions.cpp) (core/shader/recompiler) - with SIMM16[0] clear it is a no-op: the RDNA 2 ISA guide describes that value as resuming a halted wave, and a wave that executes it is not halted; measured on a gfx1035 (RDNA 2) wave: `s_sethalt 0` followed by a VALU op completes and gives the normal result. Only SIMM16[0] is read; with it set the instruction throws -- [s_subvector_loop_begin and s_subvector_loop_end](../../../core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp) (core/shader/recompiler) - follow AMD's RDNA 2 instruction set reference and a gfx1036 (wave64) measurement, pinned in [SubvectorLoop.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/SubvectorLoop.cpp); `S0` in the end instruction's `elif(S0 == 0)` is read as its SGPR operand. A wave32 shader and a destination other than an SGPR throw because neither is measured. Measured on PC RDNA2 (Radeon 610M), not on a PS5. The [specialization pass](../../../core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp) keeps a constant-condition branch whose untaken target is a loop header, since folding it removes the loop's back edge, which SPIR-V requires exactly once per loop header (SPIRV-Tools rejects the module and the NVIDIA driver 615.78.08 crashes compiling it); a loop whose back-edge block becomes unreachable in another way is not handled +- [s_subvector_loop_begin and s_subvector_loop_end](../../../core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp) (core/shader/recompiler) - follow AMD's RDNA 2 instruction set reference and a gfx1036 (wave64) measurement, pinned in [SubvectorLoop.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/SubvectorLoop.cpp); `S0` in the end instruction's `elif(S0 == 0)` is read as its SGPR operand. A wave32 shader and a destination other than an SGPR throw because neither is measured. The begin instruction tests for an empty mask as `(exec_lo | exec_hi) == 0`: AMD's Windows driver (32.0.21045.5002 and 32.0.21042.62) compiled `exec_lo == 0 && exec_hi == 0` as a test of the whole 64-bit mask when guest loads use selects, as they do with `robustBufferAccess2` (#7376, #7623). Measured on PC RDNA2 (Radeon 610M), not on a PS5. The [specialization pass](../../../core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp) keeps a constant-condition branch whose untaken target is a loop header, since folding it removes the loop's back edge, which SPIR-V requires exactly once per loop header (SPIRV-Tools rejects the module and the NVIDIA driver 615.78.08 crashes compiling it); a loop whose back-edge block becomes unreachable in another way is not handled - [Vertex entry merged wave info](../../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) (recompiler) - `s3` of every vertex-path shader is set to `waveSize | waveSize << 8` (vertex and primitive count of a full wave, wave index 0), or to `32 | 32 << 8` for a wave64 program on a 32-lane host subgroup, which runs it at one lane per invocation and holds no lanes 32-63. On a device whose `subgroupSupportedStages` lacks the vertex stage (lavapipe, MoltenVK), vertex and local programs run as one-lane waves: `s3` is `1 | 1 << 8`, the lane id is 0, a ballot is the invocation's own bit, a shuffle or broadcast returns the invocation's value, an elect is true and a subgroup reduction is the value itself, so no `GroupNonUniform` capability is declared. Layout taken from the prologue Mesa's ACO emits for NGG vertex shaders (`s_bfe_u64 exec` from `s3`), not measured on hardware. The driver gives every graphics program user data from `s8`, so the condition does not tell NGG from legacy vertex shaders: they just never read `s3`. A draw whose last wave is partial still reports a full wave, and so does a host wave with idle invocations (an RTX 5070 ran every vertex warp of a non-indexed triangle list with 30 of its 32 invocations). - [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 @@ -171,6 +171,6 @@ - [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. +- [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. From ee1bd9fbcbdd525c3c26b429fa8ede8b5ff8e2d9 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Wed, 21 Oct 2026 08:49:56 +0200 Subject: [PATCH 130/621] docs(debt): say that snwprintf_s does not invoke the exported constraint handler Co-Authored-By: Claude Sonnet 5.5 --- 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 6f00311858..d4f3f6fd18 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -37,7 +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. -- [snwprintf_s](../../../core/libs/prx/libc/src/FormattingWide.cpp) (libc) - follows C11 K.3.9.1.3 with `RSIZE_MAX` assumed to be `SIZE_MAX >> 1`. A runtime-constraint violation only returns a negative value: `set_constraint_handler_s` is not exported and the console's default handler is unknown. An invalid multibyte `%s` argument is copied byte by byte instead of being reported as an encoding error, as in `vswprintf` +- [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. - [getw and putw](../../../core/libs/prx/libc/src/BinaryWordIo.cpp) (libc) follow FreeBSD 11.4 [getw.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/getw.c) and [putw.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/putw.c): one 32-bit little-endian word, `getw` returns -1 for EOF or an error, and `putw` returns 0 on success or -1 on error while holding the stream lock. They use the existing host-backed streams, so buffering and error timing follow the host CRT. Null streams throw and closed streams return -1 with `EBADF`, as existing byte I/O does. Console behavior is unverified. From 0a2ba6ed9a9fceceeba4f842bd5a352c16641a73 Mon Sep 17 00:00:00 2001 From: Rookie Date: Wed, 21 Oct 2026 06:23:46 +0100 Subject: [PATCH 131/621] 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 bcc36571bfc687770ec32b52b40a99bb821dbcf4 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Wed, 21 Oct 2026 04:10:43 -0400 Subject: [PATCH 132/621] test(agc): bind a misaligned sparse descriptor past its committed prefix without reading the tail Co-Authored-By: Claude Opus 5.5 --- .../prx/libSceAgcDriver/tests/BdaResources.cpp | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index c52c6b4a02..7d4ccd4701 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -2224,6 +2224,21 @@ void sparseImportedHeapTests(const Context& context, const BdaTestAccess& access addressed.WriteBack(); } } + { + auto misaligned = importing; + misaligned.limits.minStorageBufferOffsetAlignment = 256; + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + Recorder recorder(misaligned); + recorder.Activate(); + GuestBufferMemory memory(misaligned); + memory.AddReadable(base + 16, 2 * bytes - 16); + memory.Upload(false); + std::uint32_t adjustment = 0; + const auto view = memory.Descriptor(base + 16, 2 * bytes - 16, adjustment); + Require(view.buffer != imported->buffer, "a misaligned sparse descriptor bound its import in place"); + Require(view.range != 0 && view.range <= bytes && memory.CopiedBytes() <= bytes && access.bytes(view.buffer)[view.offset + adjustment] == std::byte{0x57}, "a misaligned sparse descriptor does not serve its committed prefix alone"); + memory.WriteBack(); + } { GuestBufferMemory written(importing); written.AddWritable(base, 2 * bytes); From 2baa2b5a6ee4d333a8f6dfab4747989af5315831 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Wed, 21 Oct 2026 21:16:31 +1300 Subject: [PATCH 133/621] docs(dev): describe the missing-interpolator debt entry without naming a title Name no title, title ID or shader address in the entry. The mechanism text is unchanged. --- 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 996e1becf4..1bac118a65 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -25,7 +25,7 @@ ### Functional -- [Pixel interpolators past `SPI_PS_IN_CONTROL.NUM_INTERP`](../../../core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp) (shader recompiler) - when the IR reads an attribute at or past the guest interpolator count, the missing `SPI_PS_INPUT_CNTL` slots are filled as DEFAULT (`OFFSET` 0x20, constant 0/1) before info collection, and SPIR-V emission of `GetInterpolationParameter` for a missing binding returns the same defaults instead of throwing, matching Mesa's treatment of a PS input with no VS output. Component and mode bounds still throw. PPSA03541 (UFC 5) prepares pixel shader `0x114073b400` with `NUM_INTERP` 0 while it still issues `v_interp` on attribute 0 (`undefined attribute` after the collector fill alone). Console behaviour for that pairing is not measured. +- [Pixel interpolators past `SPI_PS_IN_CONTROL.NUM_INTERP`](../../../core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp) (shader recompiler) - when the IR reads an attribute at or past the guest interpolator count, the missing `SPI_PS_INPUT_CNTL` slots are filled as DEFAULT (`OFFSET` 0x20, constant 0/1) before info collection, and SPIR-V emission of `GetInterpolationParameter` for a missing binding returns the same defaults instead of throwing, matching Mesa's treatment of a PS input with no VS output. Component and mode bounds still throw. A retail title prepares a pixel shader with `NUM_INTERP` 0 while it still issues `v_interp` on attribute 0 (`undefined attribute` after the collector fill alone). Console behaviour for that pairing is not measured. - [Windows cache housekeeping](../../../core/shader/recompiler/ShaderDiskCache.cpp): eviction of old versions and temporary files is skipped for `\\?\` and `\\.\` cache roots because the supported WinLibs directory iterator can enumerate the process working directory for long prefixed paths. The active version's `last-used` stamp update still runs; it fails for long `\\.\` aliases with the supported toolchain. Ordinary Windows directory enumeration remains subject to MAX_PATH. - [f64 compares](../../../core/shader/recompiler/Translation/src/CompareInstructions.cpp) flush denormal inputs to signed zero when bit 0 of the `FLOAT_MODE` f16/f64 denormal field is clear (modes 0 and 2); modes 1 and 3 and an unknown mode keep them. This follows the RDNA2 ISA section 6.4 and LLVM's sign-preserving AMDGPU denormal modes. The gfx1036 audit in [#981](https://github.com/boykopovar/AnyPS5/issues/981#issuecomment-6070046381) reports mode-sensitive `VopcCompareF64` results. Class compares still inspect the original bits. The full four-mode matrix has not been newly measured on AMD hardware or the PS5's gfx1013. From 6826a4bd9c295008d34efb9fd82371b428244921 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Wed, 21 Oct 2026 12:32:44 +0300 Subject: [PATCH 134/621] 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 135/621] 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 136/621] 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 137/621] 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 138/621] 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 139/621] 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 e8b9eb93234ae33214c204523e2ada920626476f Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Wed, 21 Oct 2026 12:00:17 +0200 Subject: [PATCH 140/621] test(libc): keep the abort-race flag of the call trace test alive until the process aborts The detached thread of the abort-race case reads a flag that is a local of the enclosing block, which ends before main calls std::abort(). Under AddressSanitizer that read is reported as stack-use-after-scope. Make the flag static. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/tests/GuestCallTrace.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/libs/tests/GuestCallTrace.cpp b/core/libs/tests/GuestCallTrace.cpp index 4b7e2091e9..6fc77ae658 100644 --- a/core/libs/tests/GuestCallTrace.cpp +++ b/core/libs/tests/GuestCallTrace.cpp @@ -106,8 +106,8 @@ int main(int argc, char** argv) { while (!locked.load()) std::this_thread::yield(); if (std::strcmp(argv[1], "abort-race") == 0) { for (int call = 0; call < 100; ++call) Require(first(1, 2, 3, 4, 5, 6, 7, 8) == 204); - std::atomic go{false}; - std::thread([&go] { + static std::atomic go{false}; + std::thread([] { while (!go.load()) { } std::abort(); From f57bbc936a7de6835e5ab8ea8dff42e776e1700f Mon Sep 17 00:00:00 2001 From: milescoviello Date: Wed, 21 Oct 2026 08:28:24 +0000 Subject: [PATCH 141/621] perf(agc): store only the depth plane units a flush overlaps A read of memory inside a stale D32 Z_X depth plane (FlushDepthPlanes) stored the whole plane: a copy of the depth image into the mirror, MarkWholeWritten and a write-back of every byte. In Super Meat Boy 3D (#8840) a read-only buffer ending about 3 KiB inside a stale 3840x2160 plane makes ShaderResources::Revalidate flush ~1000 times per 5 s, each one moving 31.9 MiB, bound by transfer bandwidth. A depth surface now tracks its plane as 64 KiB units, each one 128x128 tile of the mirror's block units: a stale bit and a write generation per unit replace memoryStale and the plane-wide syncedGeneration. - FlushDepthPlanes stores only the stale units the range overlaps: the depth to mirror copy takes one region per unit rectangle (UnitRects, from the mirror's slice windows), the mirror marks just those units written (MarkUnitsWritten) and the write-back covers their hull. The other units stay stale and the plane stays registered. Stale bits are cleared only after the copy was recorded, and the stored units' generations advance only when FlushPending stored them. A depth fast clear still pending at the store rewrites the whole image, so it makes every unit stale first. - SyncFromMemory copies from the mirror only the units the guest wrote since their own generation (one ChangedBlocks scan; untracked memory counts as unwritten, as WrittenSince did), so a guest write into one unit no longer replaces GPU depth that is still unstored in the others. - Flushes followed by a retire keep the whole-plane store: the color target flush in Draw and a written storage view (DepthPlaneScope::Whole). The retire of an overwritten surface in DepthSurfaceTexture first stores its remaining stale units the guest did not write. - A mirror without block units (APS5_NO_BLOCK_TRACKING, unaligned base) falls back to the whole-plane copy, and APS5_DEPTH_PLANE_WHOLE_STORE=1 tracks the plane as one unit, the previous behaviour. - DepthPlaneStoreCounts reports stores, units and bytes stored. Co-Authored-By: Claude Opus 5.5 --- .../Graphics/include/DepthSurface.hpp | 9 +- .../Graphics/include/Texture.hpp | 4 + .../Graphics/src/DepthSurface.cpp | 305 +++++++++++++++--- .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 2 +- .../Graphics/src/ShaderResources.cpp | 2 +- .../libSceAgcDriver/Graphics/src/Texture.cpp | 42 +++ docs/dev/ENVIRONMENT.md | 1 + docs/dev/debt/libSceAgcDriver.md | 2 +- 8 files changed, 314 insertions(+), 53 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp index 521c60721b..c17934d116 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp @@ -20,8 +20,15 @@ class StorageTexture; VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target, bool depthWritten = true); SampleOrderTarget DepthSurfaceOrderTarget(const Context& context, const DepthTarget& target); DepthSampleLocations DepthSurfaceSampleLocations(const Context& context, const DepthTarget& target); -std::vector> FlushDepthPlanes(std::uint64_t address, std::size_t bytes); +enum class DepthPlaneScope { Overlapped, Whole }; +std::vector> FlushDepthPlanes(std::uint64_t address, std::size_t bytes, DepthPlaneScope scope = DepthPlaneScope::Overlapped); bool DepthPlanesStale(); +struct DepthPlaneStoreStatistics { + std::uint64_t stores = 0; + std::uint64_t units = 0; + std::uint64_t bytes = 0; +}; +DepthPlaneStoreStatistics DepthPlaneStoreCounts(); 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/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index ae4f6dcaec..d79eecb2e6 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -198,6 +198,10 @@ public: // Deferred write-back (APS5_EAGER_WRITEBACK=1 stores at once instead). void MarkDirty(); void MarkWholeWritten(); + void MarkUnitsWritten(const std::vector& units); + std::vector UnitRects(const std::vector& units) const; + bool BlockUnits() const { return blockUnits; } + std::uint32_t TrackedUnits() const { return trackedLayers; } void MarkSliceDirty(std::uint32_t slice, std::uint32_t count = 1); void Flush(); bool Dirty() const; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index 3c31c3ab57..8833fef947 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -150,6 +151,21 @@ DepthClearCounters& depthClearCounters() { return counters; } +struct DepthPlaneStoreCounters { + std::atomic stores{0}; + std::atomic units{0}; + std::atomic bytes{0}; +}; + +DepthPlaneStoreCounters& depthPlaneStoreCounters() { + static DepthPlaneStoreCounters counters; + return counters; +} + +bool anyUnit(const std::vector& units) { + return std::find(units.begin(), units.end(), true) != units.end(); +} + thread_local DepthPlaneOverwrite* currentOverwrite = nullptr; std::optional decodeOverwrite(const StorageImageClear& clear) { @@ -698,42 +714,166 @@ public: else Recorder::CountBarriers(Recorder::CommandClass::Copy, 3); } - std::shared_ptr StoreDepthPlane(const GuestTextureResource& resource) { - ApplyFastClear(); + std::shared_ptr Mirror(const GuestTextureResource& resource) { if (mirrorPlane.storage == nullptr) { mirrorPlane.storage = StorageTexture::Create(context, *context.detiler, resource, 0); ++mirrorEpoch; } - copyPlane(mirrorPlane, mirrorPlane.storage, false, true, true); return mirrorPlane.storage; } - bool SyncFromMemory(const GuestTextureResource& resource) { + bool StoresUnits(const StorageTexture& mirror) const { + return mirror.BlockUnits() && mirror.TrackedUnits() == staleUnits.size(); + } + + std::shared_ptr StoreDepthPlane(const GuestTextureResource& resource, std::vector& units) { + ApplyFastClear(); + auto mirror = Mirror(resource); + auto& counters = depthPlaneStoreCounters(); + ++counters.stores; + const auto planeBytes = mirror->GuestBytes(); + if (!StoresUnits(*mirror)) { + units.assign(units.size(), true); + copyPlane(mirrorPlane, mirror, false, true, true); + mirror->MarkWholeWritten(); + counters.units += units.size(); + counters.bytes += planeBytes; + return mirror; + } + const auto rects = mirror->UnitRects(units); + copyPlane(mirrorPlane, mirror, false, true, true, &rects); + mirror->MarkUnitsWritten(units); + for (std::size_t unit = 0; unit < units.size(); ++unit) { + if (!units[unit]) continue; + ++counters.units; + counters.bytes += std::min(unitBytes, planeBytes - unit * unitBytes); + } + return mirror; + } + + void SizeUnits(std::uint64_t planeBytes) { + static const bool whole = std::getenv("APS5_DEPTH_PLANE_WHOLE_STORE") != nullptr; + const std::uint64_t bytes = !whole && target.address % 65536u == 0 ? std::uint64_t{65536} : std::max(planeBytes, 1u); + const auto count = static_cast(std::max((planeBytes + bytes - 1u) / bytes, 1u)); + if (unitBytes == bytes && staleUnits.size() == count) return; + unitBytes = bytes; + staleUnits.assign(count, false); + staleCount = 0; + unitGeneration.assign(count, 0); + } + + bool AnyStale() const { + return staleCount != 0; + } + + void MarkAllStale(std::uint64_t planeBytes) { + SizeUnits(planeBytes); + staleUnits.assign(staleUnits.size(), true); + staleCount = staleUnits.size(); + } + + void ClearStale() { + staleUnits.assign(staleUnits.size(), false); + staleCount = 0; + } + + void ClearStale(const std::vector& units) { + for (std::size_t unit = 0; unit < units.size() && unit < staleUnits.size(); ++unit) { + if (!units[unit] || !staleUnits[unit]) continue; + staleUnits[unit] = false; + --staleCount; + } + } + + std::vector StaleUnitsIn(std::uint64_t planeBytes, std::uint64_t begin, std::uint64_t end) const { + std::vector units(staleUnits.size(), false); + const auto plane = target.address; + if (units.empty() || unitBytes == 0 || begin >= end || begin >= plane + planeBytes || plane >= end) return units; + const auto from = std::max(begin, plane) - plane; + const auto to = std::min(end, plane + planeBytes) - plane; + const auto last = std::min((to - 1u) / unitBytes, units.size() - 1u); + for (auto unit = from / unitBytes; unit <= last; ++unit) units[static_cast(unit)] = staleUnits[static_cast(unit)]; + return units; + } + + std::pair UnitHull(const std::vector& units, std::uint64_t planeBytes) const { + std::uint64_t first = planeBytes; + std::uint64_t last = 0; + for (std::size_t unit = 0; unit < units.size(); ++unit) { + if (!units[unit]) continue; + first = std::min(first, unit * unitBytes); + last = std::max(last, std::min(planeBytes, (unit + 1u) * unitBytes)); + } + return {target.address + first, target.address + std::max(first, last)}; + } + + void NoteStored(const std::vector& units, std::uint64_t generation) { + for (std::size_t unit = 0; unit < units.size() && unit < unitGeneration.size(); ++unit) { + if (units[unit]) unitGeneration[unit] = generation; + } + } + + std::vector WrittenUnits(const GuestTextureResource& resource, std::uint64_t& generation) { const auto bytes = static_cast(DescribeSurface(resource).guestBytes); + SizeUnits(bytes); GuestMemory::CollectWrites(resource.baseAddress, bytes); - if (syncedGeneration == 0 || (pendingClear & VK_IMAGE_ASPECT_DEPTH_BIT) != 0 || !GuestMemory::WrittenSince(resource.baseAddress, bytes, syncedGeneration)) { - syncedGeneration = GuestMemory::TrackerGeneration(); - return false; + generation = GuestMemory::TrackerGeneration(); + std::vector changed(unitGeneration.size(), false); + if (unitGeneration.size() == 1u) { + changed[0] = GuestMemory::WrittenSince(resource.baseAddress, bytes, unitGeneration[0]); + return changed; } - memoryStale = false; - if (mirrorPlane.storage == nullptr) { - mirrorPlane.storage = StorageTexture::Create(context, *context.detiler, resource, 0); - ++mirrorEpoch; + const auto oldest = *std::min_element(unitGeneration.begin(), unitGeneration.end()); + if (oldest != 0u && !GuestMemory::WrittenSince(resource.baseAddress, bytes, oldest)) return changed; + std::vector stamped(unitGeneration.size(), GuestMemory::BlockUnchanged); + if (!GuestMemory::ChangedBlocks(resource.baseAddress, bytes, unitGeneration, stamped)) return changed; + for (std::size_t unit = 0; unit < changed.size(); ++unit) changed[unit] = unitGeneration[unit] != 0u && stamped[unit] == GuestMemory::BlockWritten; + return changed; + } + + void SyncFromMemory(const GuestTextureResource& resource) { + std::uint64_t generation = 0; + const auto changed = WrittenUnits(resource, generation); + if ((pendingClear & VK_IMAGE_ASPECT_DEPTH_BIT) != 0 || !anyUnit(changed)) { + unitGeneration.assign(unitGeneration.size(), generation); + return; + } + ClearStale(changed); + { + struct Syncing { + bool& flag; + ~Syncing() { flag = false; } + } guard{syncing}; + syncing = true; + if (mirrorPlane.storage == nullptr) { + mirrorPlane.storage = StorageTexture::Create(context, *context.detiler, resource, 0); + ++mirrorEpoch; + } else { + static_cast(mirrorPlane.storage->Refresh()); + } + } + unitGeneration.assign(unitGeneration.size(), generation); + const auto mirror = mirrorPlane.storage; + if (StoresUnits(*mirror)) { + const auto rects = mirror->UnitRects(changed); + copyPlane(mirrorPlane, mirror, false, false, true, &rects); + } else { + ClearStale(); + copyPlane(mirrorPlane, mirror, false, false, true); } - else static_cast(mirrorPlane.storage->Refresh()); - syncedGeneration = GuestMemory::TrackerGeneration(); - copyPlane(mirrorPlane, mirrorPlane.storage, false, false, true); written = ++depthWrites; - return true; } void DropMirror() { mirrorPlane = {}; } - bool memoryStale = false; + std::vector staleUnits; + std::size_t staleCount = 0; + std::vector unitGeneration; + std::uint64_t unitBytes = 0; + bool syncing = false; std::uint64_t scannedEpoch = 0; - std::uint64_t syncedGeneration = 0; const Context context; const DepthTarget target; @@ -943,33 +1083,52 @@ private: return ResolveTextureFormat(resource.format) == (stencil ? VK_FORMAT_R8_UINT : d16 ? VK_FORMAT_R16_UNORM : VK_FORMAT_R32_SFLOAT); } - void copyPlane(PlaneStorage& plane, const std::shared_ptr& storage, bool stencil, bool toStorage, bool ownBatch = false) { + void copyPlane(PlaneStorage& plane, const std::shared_ptr& storage, bool stencil, bool toStorage, bool ownBatch = false, const std::vector* rects = nullptr) { + if (rects != nullptr && rects->empty()) return; auto* recorder = Recorder::Active(); Require(recorder != nullptr || ownBatch, "depth surface storage copies need an active recorder"); std::unique_ptr batch; if (recorder == nullptr) batch = std::make_unique(context); const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT; - const auto bytes = static_cast(target.extent.width) * target.extent.height * (stencil ? 1u : d16 ? 2u : 4u); + const std::uint32_t texelBytes = stencil ? 1u : d16 ? 2u : 4u; + const auto bytes = static_cast(target.extent.width) * target.extent.height * texelBytes; if (plane.staging == nullptr) plane.staging = std::make_unique(context, bytes, VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT); const auto commands = recorder != nullptr ? recorder->Commands() : batch->Handle(); if (recorder != nullptr) recorder->Keep(storage); const std::uint32_t timing = recorder != nullptr ? recorder->BeginGpuTiming(Recorder::CommandClass::Copy) : 0u; - VkBufferImageCopy planeRegion{0, 0, 0, {stencil ? VK_IMAGE_ASPECT_STENCIL_BIT : VK_IMAGE_ASPECT_DEPTH_BIT, 0, 0, 1}, {0, 0, 0}, {target.extent.width, target.extent.height, 1}}; - VkBufferImageCopy colorRegion = planeRegion; - colorRegion.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + const VkBufferImageCopy planeRegion{0, 0, 0, {stencil ? VK_IMAGE_ASPECT_STENCIL_BIT : VK_IMAGE_ASPECT_DEPTH_BIT, 0, 0, 1}, {0, 0, 0}, {target.extent.width, target.extent.height, 1}}; + std::vector planeRegions; + std::size_t moved = bytes; + if (rects == nullptr) { + planeRegions.push_back(planeRegion); + } else { + moved = 0; + for (const auto& rect : *rects) { + auto region = planeRegion; + region.bufferOffset = (static_cast(rect.offset.y) * target.extent.width + static_cast(rect.offset.x)) * texelBytes; + region.bufferRowLength = target.extent.width; + region.imageOffset = {rect.offset.x, rect.offset.y, 0}; + region.imageExtent = {rect.extent.width, rect.extent.height, 1}; + planeRegions.push_back(region); + moved += static_cast(rect.extent.width) * rect.extent.height * texelBytes; + } + } + auto colorRegions = planeRegions; + for (auto& region : colorRegions) region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + const auto count = static_cast(planeRegions.size()); const auto sourceImage = toStorage ? image : storage->Image(); const auto destinationImage = toStorage ? storage->Image() : image; RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); - context.Function("vkCmdCopyImageToBuffer")(commands, sourceImage, VK_IMAGE_LAYOUT_GENERAL, plane.staging->Handle(), 1, toStorage ? &planeRegion : &colorRegion); + context.Function("vkCmdCopyImageToBuffer")(commands, sourceImage, VK_IMAGE_LAYOUT_GENERAL, plane.staging->Handle(), count, toStorage ? planeRegions.data() : colorRegions.data()); RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); - context.Function("vkCmdCopyBufferToImage")(commands, plane.staging->Handle(), destinationImage, VK_IMAGE_LAYOUT_GENERAL, 1, toStorage ? &colorRegion : &planeRegion); + context.Function("vkCmdCopyBufferToImage")(commands, plane.staging->Handle(), destinationImage, VK_IMAGE_LAYOUT_GENERAL, count, toStorage ? colorRegions.data() : planeRegions.data()); RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT); if (batch) { batch->SubmitAndWait(); return; } Recorder::CountBarriers(Recorder::CommandClass::Copy, 3); - recorder->EndGpuTiming(timing, bytes); + recorder->EndGpuTiming(timing, moved); } void release() noexcept { @@ -1578,15 +1737,36 @@ void retire(DepthSurface& surface) { for (auto* owners : {&depthOwners(), &stencilOwners()}) { std::erase_if(*owners, [&](const auto& entry) { return entry.second.surface == &surface; }); } - if (!surface.memoryStale) return; - surface.memoryStale = false; + if (!surface.AnyStale()) return; + surface.ClearStale(); const auto plane = stalePlaneRange(surface); - if (std::any_of(surfaces().begin(), surfaces().end(), [&](const auto& other) { return other->memoryStale && stalePlaneRange(*other) == plane; })) return; + if (std::any_of(surfaces().begin(), surfaces().end(), [&](const auto& other) { return other->AnyStale() && stalePlaneRange(*other) == plane; })) return; auto& planes = stalePlanes(); std::lock_guard lock(planes.mutex); std::erase(planes.ranges, plane); publishStaleHull(planes); } + +void retireStoringStale(DepthSurface& surface) { + if (!surface.AnyStale() || !mirrorable(surface.target)) { + retire(surface); + return; + } + const auto resource = mirrorResource(surface.target); + const auto planeBytes = DescribeSurface(resource).guestBytes; + std::uint64_t generation = 0; + const auto written = surface.WrittenUnits(resource, generation); + auto units = surface.staleUnits; + for (std::size_t unit = 0; unit < units.size() && unit < written.size(); ++unit) { + if (written[unit]) units[unit] = false; + } + std::shared_ptr mirror; + if (anyUnit(units) && (!anyUnit(written) || surface.StoresUnits(*surface.Mirror(resource)))) mirror = surface.StoreDepthPlane(resource, units); + retire(surface); + if (mirror == nullptr) return; + const auto [begin, end] = surface.UnitHull(units, planeBytes); + if (StorageTexture::FlushPending(begin, static_cast(end - begin), nullptr, "depth plane store")) surface.NoteStored(units, GuestMemory::TrackerGeneration()); +} } std::uint64_t DepthSurfacesSerial() { @@ -1749,7 +1929,7 @@ VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target, if (!created) surface->NoteHtileClear(VK_IMAGE_ASPECT_DEPTH_BIT); } if (!created) surface->NoteHtileClear(htileCleared); - if (mirrorable(target)) static_cast(surface->SyncFromMemory(mirrorResource(target))); + if (mirrorable(target)) surface->SyncFromMemory(mirrorResource(target)); surface->ApplyFastClear(); if (drawnDepth != nullptr && drawnDepth != surface) drawnDepth->written = ++depthWrites; surface->written = ++depthWrites; @@ -1761,14 +1941,15 @@ VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target, if (other.get() == surface || other->context.device != context.device || other->written >= surface->written || !mirrorable(other->target)) continue; const auto otherPlane = stalePlaneRange(*other); if (otherPlane == plane || otherPlane.first >= plane.second || plane.first >= otherPlane.second) continue; - other->memoryStale = false; + other->ClearStale(); other->DropMirror(); } } if ((depthWritten || cleared) && mirrorable(target)) { - surface->memoryStale = true; const auto plane = target.address; - noteStalePlane(plane, plane + DescribeSurface(mirrorResource(target)).guestBytes); + const auto planeBytes = DescribeSurface(mirrorResource(target)).guestBytes; + surface->MarkAllStale(planeBytes); + noteStalePlane(plane, plane + planeBytes); } drawnDepth = surface; return surface->view; @@ -1805,52 +1986,78 @@ bool DepthPlanesStale() { return planes.begin.load(std::memory_order_acquire) < planes.end.load(std::memory_order_acquire); } -std::vector> FlushDepthPlanes(std::uint64_t address, std::size_t bytes) { +std::vector> FlushDepthPlanes(std::uint64_t address, std::size_t bytes, DepthPlaneScope scope) { static thread_local bool flushing = false; const auto end = bytes > std::numeric_limits::max() - address ? std::numeric_limits::max() : address + bytes; if (flushing || !overlapsStalePlane(address, end)) return {}; + if (scope == DepthPlaneScope::Overlapped) { + std::lock_guard lock(surfacesMutex()); + const auto staleInRange = [&](const auto& surface) { return surface->AnyStale() && !surface->syncing && ((surface->pendingClear & VK_IMAGE_ASPECT_DEPTH_BIT) != 0 || anyUnit(surface->StaleUnitsIn(DescribeSurface(mirrorResource(surface->target)).guestBytes, address, end))); }; + if (std::none_of(surfaces().begin(), surfaces().end(), staleInRange)) return {}; + } flushing = true; struct Reset { ~Reset() { flushing = false; } } reset; - std::vector>> stored; + struct Stored { + DepthSurface* surface; + std::shared_ptr mirror; + std::vector units; + std::uint64_t begin; + std::uint64_t end; + bool flushed; + }; + std::vector stored; std::vector> ranges; { std::lock_guard gpu(GuestMemory::GpuMutex()); std::lock_guard lock(surfacesMutex()); std::vector> remaining; for (const auto& surface : surfaces()) { - if (!surface->memoryStale) continue; + if (!surface->AnyStale()) continue; const auto resource = mirrorResource(surface->target); const auto plane = surface->target.address; const auto planeBytes = DescribeSurface(resource).guestBytes; - if (plane >= end || address >= plane + planeBytes) { - remaining.emplace_back(plane, plane + planeBytes); - continue; + const bool overlapped = !surface->syncing && plane < end && address < plane + planeBytes; + if (overlapped && (surface->pendingClear & VK_IMAGE_ASPECT_DEPTH_BIT) != 0) surface->MarkAllStale(planeBytes); + if (overlapped && (scope == DepthPlaneScope::Whole || anyUnit(surface->StaleUnitsIn(planeBytes, address, end)))) { + surface->SyncFromMemory(resource); + auto units = scope == DepthPlaneScope::Whole ? surface->staleUnits : surface->StaleUnitsIn(planeBytes, address, end); + if (anyUnit(units)) { + auto mirror = surface->StoreDepthPlane(resource, units); + surface->ClearStale(units); + const auto hull = surface->UnitHull(units, planeBytes); + ranges.emplace_back(hull.first, hull.second - hull.first); + stored.push_back({surface.get(), std::move(mirror), std::move(units), hull.first, hull.second, false}); + } } - surface->memoryStale = false; - if (surface->SyncFromMemory(resource)) continue; - stored.emplace_back(surface.get(), surface->StoreDepthPlane(resource)); - ranges.emplace_back(plane, planeBytes); + if (surface->AnyStale()) remaining.emplace_back(plane, plane + planeBytes); } auto& planes = stalePlanes(); std::lock_guard staleLock(planes.mutex); planes.ranges = std::move(remaining); publishStaleHull(planes); } - for (const auto& entry : stored) entry.second->MarkWholeWritten(); - for (const auto& [plane, planeBytes] : ranges) StorageTexture::FlushPending(plane, static_cast(planeBytes), nullptr, "depth plane store"); + for (auto& entry : stored) entry.flushed = StorageTexture::FlushPending(entry.begin, static_cast(entry.end - entry.begin), nullptr, "depth plane store"); const auto generation = GuestMemory::TrackerGeneration(); std::lock_guard lock(surfacesMutex()); for (const auto& entry : stored) { - if (std::any_of(surfaces().begin(), surfaces().end(), [&](const auto& candidate) { return candidate.get() == entry.first; })) { - entry.first->syncedGeneration = generation; - entry.first->NoteMemory(); - } + if (!entry.flushed || std::none_of(surfaces().begin(), surfaces().end(), [&](const auto& candidate) { return candidate.get() == entry.surface; })) continue; + entry.surface->NoteStored(entry.units, generation); + entry.surface->NoteMemory(); } return ranges; } +DepthPlaneStoreStatistics DepthPlaneStoreCounts() { + const auto& counters = depthPlaneStoreCounters(); + DepthPlaneStoreStatistics statistics; + statistics.stores = counters.stores.load(); + statistics.units = counters.units.load(); + statistics.bytes = counters.bytes.load(); + return statistics; +} + std::uint64_t HtileDepthClearAddress(std::span code, std::span userData, const std::array& numThreads) { static constexpr std::array htileMaskKernel{0xd7460000u, 0x04010c06u, 0x34000084u, 0xdc388000u, 0x04020000u, 0xbf8c3f70u, 0xd7710008u, 0x00120805u, 0xd7710009u, 0x00120a05u, 0xd771000au, 0x00120c05u, 0xd771000bu, 0x00120e05u, 0xdc788000u, 0x00020800u, 0xbf810000u}; if (userData.size() < 6 || numThreads[0] != 64 || numThreads[1] != 1 || numThreads[2] != 1) return 0; @@ -2027,7 +2234,7 @@ std::shared_ptr DepthSurfaceTexture(const Context& context, std::span resource.depthOrLastArray)) && !(cube && resource.depthOrLastArray != 0 && resource.depthOrLastArray != 5) && (resource.dimension == TextureDimension::k2D || resource.dimension == TextureDimension::k2DArray || cube); if (!(sampled ? found->SampledAccepts(words, resource) : planes) && found->OverwrittenInMemory()) { - retire(*found); + retireStoringStale(*found); return nullptr; } found->ApplyFastClear(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 2438c18aa2..cb4c5b1136 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -2239,7 +2239,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& } APS5_LOG_OUT_DEBUG("Creating color target %zu address=0x%llx bytes=%llu extent=%ux%u", index, static_cast(color.address), static_cast(color.bytes), color.extent.width, color.extent.height); const ColorTargetLayout colorLayout(color.extent.width, color.extent.height, color.tileMode, color.elementBytes); - static_cast(FlushDepthPlanes(color.address, colorLayout.Bytes())); + static_cast(FlushDepthPlanes(color.address, colorLayout.Bytes(), DepthPlaneScope::Whole)); RetireDepthSurfaces(context.device, color.address, colorLayout.Bytes()); constexpr VkBufferUsageFlags copies = VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT; timer.phase(PhaseSetup); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 277eea93c5..fd39bcc21a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -1123,7 +1123,7 @@ std::shared_ptr cachedStorageTexture(const Context& context, std reportGuestAllocation(viewed, viewedBytes); } } - if (DepthPlanesStale() || written) static_cast(FlushDepthPlanes(viewed.baseAddress, static_cast(viewedBytes))); + if (DepthPlanesStale() || written) static_cast(FlushDepthPlanes(viewed.baseAddress, static_cast(viewedBytes), written ? DepthPlaneScope::Whole : DepthPlaneScope::Overlapped)); if (written && DepthSurfaceOverlaps(context.device, viewed.baseAddress, viewedBytes)) { const auto storage = WrittenStorageBytes(viewed, mip); RetireCoveredDepthSurfaces(context.device, storage.bytes, storage.extents); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index a70da26def..91970938a8 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -2837,6 +2837,16 @@ std::vector StorageTexture::sliceWindows(std::span< return windows; } +std::vector StorageTexture::UnitRects(const std::vector& units) const { + std::vector rects; + if (units.size() != trackedLayers) return rects; + const auto runs = unitRuns(units); + for (const auto& window : sliceWindows(runs)) { + for (const auto& region : window.regions) rects.push_back({{region.imageOffset.x, region.imageOffset.y}, {region.imageExtent.width, region.imageExtent.height}}); + } + return rects; +} + std::vector> StorageTexture::uncoveredBytes(std::span> runs) const { std::vector> covered; if (!geometry.thick) { @@ -4814,6 +4824,38 @@ void StorageTexture::MarkWholeWritten() { MarkDirty(); } +void StorageTexture::MarkUnitsWritten(const std::vector& units) { + if (!blockUnits || descriptor.dccAddress != 0 || units.size() != trackedLayers || std::all_of(units.begin(), units.end(), [](bool unit) { return unit; })) { + MarkWholeWritten(); + return; + } + const auto runs = unitRuns(units); + if (runs.empty()) return; + if (GuestMemory::Watched(descriptor.baseAddress, static_cast(guestBytes))) { + for (std::uint32_t unit = 0; unit < trackedLayers; ++unit) { + if (!units[unit]) continue; + layerOriginal[unit] = false; + cancelStoredSave(unit); + } + } else { + captureGuestBytes(&units); + } + uploadedKeys = DccKeys::Uncompressed; + for (const auto& [begin, end] : runs) { + const auto first = static_cast(begin / trackedLayerBytes); + const auto count = static_cast((end - begin + trackedLayerBytes - 1) / trackedLayerBytes); + const auto collected = GuestMemory::CollectWritesUncached(descriptor.baseAddress + begin, static_cast(end - begin)); + for (auto unit = first; unit < first + count; ++unit) layerGeneration[unit] = collected; + } + refreshGeneration(); + for (const auto& [begin, end] : runs) { + const auto first = static_cast(begin / trackedLayerBytes); + const auto count = static_cast((end - begin + trackedLayerBytes - 1) / trackedLayerBytes); + forgetBorrowed(first, count); + markLayersPending(first, count); + } +} + // See AdjacentGenerationEnabled above. std::vector StorageTexture::adjacentPendingUnchanged(std::uint64_t firstBlock, std::uint64_t lastBlock) const { std::vector adjacent; diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 7d6f4d537b..dd75c7f25f 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -107,6 +107,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_CPU_STORES` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp` | | writes packet stores on the CPU instead of recording them on the GPU | | `APS5_DCC_KEYS_EACH` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | records every DCC key store at once instead of batching them | | `APS5_DEPTH_COPY_MIN_KIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | 1024 | minimum surface size in KiB for a depth target to take the depth-copy path | +| `APS5_DEPTH_PLANE_WHOLE_STORE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp` | | stores a stale depth plane whole when memory over it is read, instead of only the 64 KiB units the read overlaps | | `APS5_DISPATCH_CACHE_BYTES` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchCache.cpp` | 268435456 | size of the dispatch cache in bytes (0 keeps the default of 256 MiB) | | `APS5_DISPATCH_CACHE_ENTRIES` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchCache.cpp` | 8192 | number of dispatch cache entries; 0 picks a default from the variant count | | `APS5_DISPATCH_STAMP_VALIDATE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchCache.cpp` | | validates dispatch cache hits by write stamps, forcing one variant and a 4096-entry cache | diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 05682afa75..0e12b5ca8f 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -73,7 +73,7 @@ - [Reused depth storage](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp): a storage view with another extent or a format that cannot hold the cached depth plane uses guest memory. Depth rendering is not written back to those guest bytes; this admission rule does not prove allocation reuse or preserve a depth writer through an incompatible view. A 2D storage view, or a one-layer 2D array view of layer 0, of level 0 with the plane's extent and format (32-bit float or the 32-bit depth-bits layout for the depth plane, 16-bit unorm for D16, 8-bit uint for the stencil plane) is a storage image that is [filled](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) from the depth image before every use and, when the shader writes it, copied back after the work: a whole-plane copy through a staging buffer each way. A dispatch of the 15-word 8x8x1 image clear kernel ([`MatchStorageImageClear`](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp): one colour read from the V# in `user[8..11]` stored to every texel of the T# in `user[0..7]`) that covers the whole plane and stores 0 or 1 through a 16-bit unorm view, 0 or 0xffff through a 16-bit uint view, or 0 or a normal float in (0, 1] through a 32-bit float or integer view leaves out the copy-in and replaces the copy-out by a clear of the depth image to that value; the dispatch still writes the storage image. The colour is read again when the copy-out is replaced, and a changed value keeps the copy-out. Indirect dispatches, partial last groups, dispatches that queue on the draw pipeline, other values and formats, a plane the kernel covers only partly and stencil planes keep both copies. A written view is also left pending like any other storage image, so the plane's content, drawn depth included, reaches guest memory on the next flush of that range. A 2D array view of more than one slice of the depth plane, or of a later slice, is a storage image over the slices from 0 to its last (`LayeredDepthStorage`); only the slices from its base slice to its last are copied: a slice that has a depth surface at the plane's address plus a whole depth slice per layer (`DepthSliceBytes`, as for the depth plane copies below) is copied from that surface before every use and back to it after the work, every bound slice each time, through a staging buffer; the other slices hold guest memory like any storage image. These images are kept per address, extent, slice count, base slice, format and tile mode until the device goes away, outside the storage cache's budget. Such a view needs a layer pitch equal to that slice size, and another view of the same memory (the 2D view of its first slice, a view from another base slice, or a sampled copy) is a separate image, copied in before and out after each use, so views that are written before a flush and share a slice do not merge: the later copy-out wins, so their results are not merged. A matching view still throws when it is a cube, a 2D array of more than one layer of the stencil plane or from a later slice, or another level, when the surface is multisampled, when it names the depth plane of a 16-bit depth with stencil surface the device stores in a wider format, and when it was written after another view of the same plane (another format, tile mode, address or dimension) replaced it, as two such views bound in one dispatch are. Stored depth values outside 0..1 are not clamped, and Vulkan leaves that copy undefined without `VK_EXT_depth_range_unrestricted`. `agc_driver_depth_storage_dispatch` pins these paths; none is measured on the console. - [Collapsed color array views](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) preserve the selected slice in the XOR address equation when the view is represented as a shifted 2D surface. Native Vulkan coverage checks R_X draws, array reads, publication and re-upload for slices 1-3; Lost Soul Aside PPSA26358 validates the RGBA16F cube mip chain on Windows/NVIDIA. Other devices and console comparisons remain unverified. Full-array views retain their layer addressing. - A [depth surface](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) is retired when a color target or a written storage image it does not serve overlaps it, or when it is sampled through a view it cannot serve and guest memory over it was written since it was last bound; lookups then skip it, so sampling reads the memory like any texture. Bound again, it keeps its own depth instead of the bytes written over it, except for the depth planes the next entry keeps in guest memory. Without write watching, a view it cannot serve still throws. A retired surface also gives up its depth and stencil planes, so another surface sharing them no longer copies from it. -- [Depth planes in guest memory](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver): only single-sample `D32_SFLOAT` and `D32_SFLOAT_S8_UINT` planes with the 64 KiB Z swizzle (`SW_MODE` 24) reach guest memory; D16, other swizzles, stencil and multisampled planes keep depth in the host image only. A plane drawn since its last store is stored, uncompressed, when a buffer upload, a `GuestBuffers` V#, an image lookup or a pending-store flush overlaps it; CPU reads and reads through address-based heaps are not caught. Guest writes are ordered when collected (bind, flush, collect epoch), not when made, at 64 KiB granularity: a collected write reloads the whole plane unless an HTILE clear is pending. A store outside a recorder waits for the GPU. The console keeps HTILE-compressed tiles compressed in memory until a decompress (Mesa `ac_descriptors.h`). +- [Depth planes in guest memory](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver): only single-sample `D32_SFLOAT` and `D32_SFLOAT_S8_UINT` planes with the 64 KiB Z swizzle (`SW_MODE` 24) reach guest memory; D16, other swizzles, stencil and multisampled planes keep depth in the host image only. A plane drawn since its last store is stored, uncompressed, in 64 KiB units (128x128 tiles): a buffer upload, a `GuestBuffers` V#, an image lookup or a pending-store flush stores the units it overlaps, a color target or written storage image over the plane all of them. CPU reads and reads through address-based heaps are not caught. Guest writes are ordered when collected (bind, flush, collect epoch), not when made: a collected write reloads the units it touched unless an HTILE clear is pending. A store outside a recorder waits for the GPU. The console keeps HTILE-compressed tiles compressed in memory until a decompress (Mesa `ac_descriptors.h`). - [Depth plane copies](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver): a depth plane sampled as a 2D array or a cube, or a D16 plane read as `R16_UINT`, is copied into an image of its own (`DepthPlaneCopy`), kept per base address, extent, format and slice count; a slice comes from the live depth surface when one is bound at the slice's address and from guest memory (detiled) otherwise. A draw or dispatch that binds the plane copies again only the slices that changed and records nothing when none did: a surface slice when its image or write serial differs from the one copied (the serial is bumped when the surface is created, when a draw binds it as its depth target, when a pending fast clear is applied and when a written storage view of the plane is copied back), a guest-memory slice when its bytes were written since they were last read (`GuestMemory::UnchangedSince`), a snapshot write overlaps them, or its address, size or tile mode changed. The surface of the latest depth draw is copied at every use until another depth target is bound, since that draw may still be recorded after the copy. A slice's address is the base plus a whole 64 KiB-block depth slice per layer (`DepthSliceBytes`), and the cube face order is the array order; both are inferred from the titles' use, not measured on hardware. A 2D-array T# whose `BASE_ARRAY` is past slice 0 reads slices `BASE_ARRAY` to `LAST_ARRAY` of the copy, which holds slices 0 to `LAST_ARRAY`, and a 2D binding of it reads slice `BASE_ARRAY`; that follows the colour 2D-array semantics and the titles that hit it (PPSA01556, PPSA03912), and was not measured on hardware for depth surfaces. Planes with mips, a cube or a 2D T# with a base slice other than 0, a `BASE_ARRAY` past `LAST_ARRAY`, a cube with other than 1 or 6 slices, and other views of the planes still throw, and so do these copies of a 16-bit plane with stencil that the device stores as D24 or D32 (16-bit depth with stencil, below). - [Repacked format-43 copies](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver): a sampled texture of format 43 (10-bit R, 11-bit G and B unsigned floats from bit 0) is uploaded into a `B10G11R11_UFLOAT` image with its R and B fields swapped (`RepackTextureWord`) and viewed with R and B swapped back. A texture whose results a storage image still holds is copied from it again, whole, whenever the image changed (`RefreshDepthCopy`); a 3D one is flushed to guest memory and uploaded instead. Storage reads and writes of format 43 keep the packed `R32_UINT` path. - [SW_64KB_Z_X color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp) (libSceAgcDriver) use the texture equations and 64 KiB block geometry of `SW_64KB_Z_X`, whose addresses are checked against [AMD addrlib GFX10](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp) in the texture tiling tests. Any 256-byte pipe/bank XOR below the block size is accepted, as for `SW_64KB_R_X` targets and Z_X textures; which of those bits the console honours, and the render results, are not measured. From 7bf7be589d68688bdb74e41a240b4a3ce1881f0f Mon Sep 17 00:00:00 2001 From: milescoviello Date: Wed, 21 Oct 2026 08:32:53 +0000 Subject: [PATCH 142/621] test(agc): cover range-limited depth plane stores DepthPlanePartialStore draws depth 0.5 into a 384x200 D32 Z_X plane (3x2 64 KiB tiles, the last row 72 texels high) over memory filled with 1.0 and checks the plane's memory unit by unit: - a read ending 3 KiB into the plane stores unit 0 alone (one store, one unit, 64 KiB counted) and leaves the plane stale; the same read again stores nothing; - a read of the whole plane stores the other five units, the edge tiles holding 128x72 depth values with their padding left as it was; - a guest write into unit 4 after unit 0 was stored wins in unit 4 only: a later whole read stores the four other stale units over 1.0, and a LESS draw at 0.6 passes exactly over unit 4 (the plane-wide generation synced every unit from memory instead); - the retire of a surface the guest wrote into, through a view with two levels, stores its stale units the guest did not write; - a color target over unit 5 retires the surface after storing units 1-4; - over unwatched memory a partial store leaves the other units on the GPU, so a LESS draw at 0.6 passes nowhere. TextureTiling checks that the 64KB Z_X equation of 4-byte elements reads only x and y bits below 7, so each 64 KiB unit of a 3840x2160 D32 plane is exactly one 128x128 tile. Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/tests/TextureTiling.cpp | 23 ++ .../execution/DepthPlanePartialStore.cpp | 234 ++++++++++++++++++ 2 files changed, 257 insertions(+) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/DepthPlanePartialStore.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp b/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp index cb439c6ffe..48b7f6fa98 100644 --- a/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp @@ -386,6 +386,29 @@ void RunTextureTilingTests() { Require(dense.pitchBytes == 1024 && CoveredMipBytes(TextureTileMode::kLinear, 4, dense) == std::vector>{{0, 3072}}, "linear rows without padding are not one range"); } + { + const auto* equation = FindTextureSwizzleEquation(XorSwizzleMode(TextureTileMode::kZ64KBX), 4u); + Require(equation != nullptr, "missing the 64KB Z_X swizzle equation of 4-byte elements"); + for (const auto mask : equation->bits) Require((mask & 0x00f80f80u) == 0u, "a 64KB Z_X address bit of a 4-byte element reads x or y bits past one 128x128 tile"); + GuestTextureResource plane{}; + plane.width = 3840; + plane.height = 2160; + plane.mipCount = 1; + plane.tileMode = TextureTileMode::kZ64KBX; + plane.dimension = TextureDimension::k2D; + plane.format = 22; + const auto geometry = DescribeSurface(plane); + const auto& mip = geometry.mips.front(); + const auto block = ThinBlockLayout(TextureTileMode::kZ64KBX, 4u); + Require(block[0] == 65536u && block[1] == 128u && block[2] == 128u, "a 64KB Z_X tile of 4-byte elements is not 128x128"); + Require(!mip.tail && mip.tiledOffset == 0u && mip.blocksPerRow == 30u && geometry.guestBytes == 510ull * 65536ull, "a 3840x2160 D32 Z_X plane is not 30x17 tiles"); + const std::array, 8> samples{{{0, 0}, {127, 127}, {128, 0}, {3839, 0}, {0, 128}, {1000, 700}, {2047, 1151}, {3839, 2159}}}; + for (const auto& [x, y] : samples) { + const auto offset = mip.tiledOffset + (static_cast(y / block[2]) * mip.blocksPerRow + x / block[1]) * block[0] + equationOffset(*equation, x, y, 0); + Require(offset / 65536u == static_cast(y >> 7u) * 30u + (x >> 7u), "element (" + std::to_string(x) + ", " + std::to_string(y) + ") of a 3840x2160 D32 Z_X plane lies outside the 64 KiB unit of its 128x128 tile"); + } + } + reject([] { ComputeSurfaceSize({}, 1); }, "empty mip chain"); reject([] { ComputeSurfaceSize(ComputeMipLayout(TextureTileMode::kLinear, 1, 4, 4, 1), 0); }, "zero array layers"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlanePartialStore.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlanePartialStore.cpp new file mode 100644 index 0000000000..43b8815b56 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlanePartialStore.cpp @@ -0,0 +1,234 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "DepthFastClearHarness.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 AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + +namespace { + +using AgcDriver::Graphics::DepthPlaneStoreStatistics; +using DepthFastClearHarness::Compile; +using DepthFastClearHarness::Draw; +using DepthFastClearHarness::DrawOptions; +using DepthFastClearHarness::Require; +using DepthFastClearHarness::Surface; + +constexpr std::uint32_t PlaneWidth = 384; +constexpr std::uint32_t PlaneHeight = 200; +constexpr VkExtent2D PlaneExtent{PlaneWidth, PlaneHeight}; +constexpr std::size_t Block = 65536; +constexpr std::size_t Units = 6; +constexpr std::size_t PlaneBytes = Units * Block; +constexpr std::size_t UnitTexels = Block / sizeof(float); +constexpr std::size_t EdgeTexels = 128u * (PlaneHeight - 128u); +constexpr std::size_t PlaneTexels = static_cast(PlaneWidth) * PlaneHeight; +constexpr VkExtent2D SmallExtent{64, 32}; +constexpr std::size_t SmallColorBytes = static_cast(SmallExtent.width) * SmallExtent.height * 4u; +constexpr std::uint64_t ReadBefore = 4096; +constexpr std::size_t ReadBytes = 7u * 1024u; +alignas(256) std::array Colors{}; + +void* AllocateBlocks(std::size_t bytes, bool watched) { +#ifdef _WIN32 + void* block = nullptr; + if (watched) { + block = GuestArena::GuestArenaAllocate_nid_postfix(bytes, Block); + if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block, bytes, PAGE_READWRITE, bytes); + } else { + block = VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); + } + Require(block != nullptr, "cannot allocate the depth plane blocks"); +#else + void* raw = mmap(nullptr, bytes + Block, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(raw != MAP_FAILED, "cannot map the depth plane blocks"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + Block - 1) & ~static_cast(Block - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + bytes != begin + bytes + Block) munmap(reinterpret_cast(aligned + bytes), begin + Block - aligned); + void* block = reinterpret_cast(aligned); + if (watched) GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, bytes); +#endif + GuestAllocations::Mutation().Add(block, bytes, true, true, true); + return block; +} + +void Fill(float* plane, float value) { + std::fill(plane, plane + PlaneBytes / sizeof(float), value); + AgcDriver::GuestMemory::BumpCollectEpoch(); +} + +void FillUnit(float* plane, std::size_t unit, float value) { + std::fill(plane + unit * UnitTexels, plane + (unit + 1u) * UnitTexels, value); + AgcDriver::GuestMemory::BumpCollectEpoch(); +} + +std::size_t Texels(std::size_t unit) { + return unit < 3u ? UnitTexels : EdgeTexels; +} + +void RequireUnit(const float* plane, std::size_t unit, float depth, std::size_t count, const std::string& what) { + const auto* first = plane + unit * UnitTexels; + const auto held = static_cast(std::count(first, first + UnitTexels, depth)); + Require(held == count, what + ": unit " + std::to_string(unit) + " holds " + std::to_string(held) + " texels of " + std::to_string(depth) + " instead of " + std::to_string(count)); +} + +DepthPlaneStoreStatistics Since(const DepthPlaneStoreStatistics& before) { + const auto now = AgcDriver::Graphics::DepthPlaneStoreCounts(); + DepthPlaneStoreStatistics difference; + difference.stores = now.stores - before.stores; + difference.units = now.units - before.units; + difference.bytes = now.bytes - before.bytes; + return difference; +} + +std::vector> ReadStart(AgcDriver::VulkanDevice& device, std::uint64_t base) { + auto stored = AgcDriver::Graphics::FlushDepthPlanes(base - ReadBefore, ReadBytes); + device.WaitIdle(); + return stored; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Require(AgcDriver::Graphics::DepthSliceBytes(PlaneExtent, 4u) == PlaneBytes, "the 384x200 depth plane is not six 64 KiB tiles"); + auto* blocks = static_cast(AllocateBlocks(Block + PlaneBytes, true)); + auto* plane = reinterpret_cast(blocks + Block); + const auto base = reinterpret_cast(plane); + const auto shaders = Compile(*device); + const auto swizzle = AgcDriver::Graphics::XorSwizzleMode(AgcDriver::Graphics::TextureTileMode::kZ64KBX); + const Surface depth{base, 0, 0, false, VK_FORMAT_D32_SFLOAT, 0, 0, swizzle}; + const auto draw = [&](const Surface& surface, const DrawOptions& options) { return Draw(*device, shaders, surface, options, PlaneExtent, Colors); }; + const DrawOptions writeHalf{.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS}; + + Fill(plane, 1.0f); + Require(draw(depth, writeHalf) == PlaneTexels, "the draw writing depth 0.5 did not cover the plane"); + Require(AgcDriver::Graphics::DepthPlanesStale(), "the draw writing depth left the plane unregistered"); + auto before = AgcDriver::Graphics::DepthPlaneStoreCounts(); + const auto first = ReadStart(*device, base); + auto stored = Since(before); + Require(first.size() == 1 && first[0].first == base && first[0].second == Block, "a read ending 3 KiB into the stale plane did not store exactly its first 64 KiB unit"); + Require(stored.stores == 1 && stored.units == 1 && stored.bytes == Block, "a read ending 3 KiB into the stale plane did not count one store of one unit"); + RequireUnit(plane, 0, 0.5f, UnitTexels, "after a read of the first unit"); + for (std::size_t unit = 1; unit < Units; ++unit) RequireUnit(plane, unit, 1.0f, UnitTexels, "after a read of the first unit, a unit it does not overlap"); + Require(AgcDriver::Graphics::DepthPlanesStale(), "a store of one unit left no stale plane"); + + before = AgcDriver::Graphics::DepthPlaneStoreCounts(); + Require(ReadStart(*device, base).empty(), "the same read again stored the first unit again"); + stored = Since(before); + Require(stored.stores == 0 && stored.units == 0 && stored.bytes == 0, "the same read again counted a store"); + + before = AgcDriver::Graphics::DepthPlaneStoreCounts(); + const auto rest = AgcDriver::Graphics::FlushDepthPlanes(base, PlaneBytes); + device->WaitIdle(); + stored = Since(before); + Require(rest.size() == 1 && rest[0].first == base + Block && rest[0].second == PlaneBytes - Block, "a read of the whole plane did not store the five units still stale"); + Require(stored.stores == 1 && stored.units == Units - 1u && stored.bytes == PlaneBytes - Block, "a read of the whole plane did not count one store of five units"); + for (std::size_t unit = 0; unit < Units; ++unit) RequireUnit(plane, unit, 0.5f, Texels(unit), "after a read of the whole plane"); + for (std::size_t unit = 3; unit < Units; ++unit) RequireUnit(plane, unit, 1.0f, UnitTexels - EdgeTexels, "after a read of the whole plane, the padding below the last row"); + Require(!AgcDriver::Graphics::DepthPlanesStale(), "a read of the whole plane left it stale"); + + if (!AgcDriver::GuestMemory::WriteWatched() || !AgcDriver::GuestMemory::Watched(base, PlaneBytes)) { + std::puts("guest memory is not write-watched here: guest writes into a partly stored plane are not exercised"); + } else { + Fill(plane, 1.0f); + Require(draw(depth, writeHalf) == PlaneTexels, "the draw writing depth 0.5 over the reloaded plane did not cover it"); + Require(!ReadStart(*device, base).empty(), "a read of the first unit of the drawn plane stored nothing"); + FillUnit(plane, 4, 0.75f); + draw(depth, {.depthTest = false}); + before = AgcDriver::Graphics::DepthPlaneStoreCounts(); + Require(!AgcDriver::Graphics::FlushDepthPlanes(base, PlaneBytes).empty(), "a read of the whole plane after a guest write into unit 4 stored nothing"); + device->WaitIdle(); + stored = Since(before); + Require(stored.units == 4 && stored.bytes == 4u * Block, "a read of the whole plane after a guest write into unit 4 did not store the four units still stale"); + for (const std::size_t unit : {0u, 1u, 2u, 3u, 5u}) RequireUnit(plane, unit, 0.5f, Texels(unit), "a guest write into unit 4 after unit 0 was stored, then a read of the whole plane"); + RequireUnit(plane, 4, 0.75f, UnitTexels, "a guest write into unit 4 after unit 0 was stored, then a read of the whole plane, the written unit"); + Require(draw(depth, {.minDepth = 0.6f, .maxDepth = 0.6f}) == EdgeTexels, "a draw at depth 0.6 did not pass LESS exactly over the unit the guest wrote 0.75 into"); + + Fill(plane, 1.0f); + Require(draw(depth, writeHalf) == PlaneTexels, "the draw writing depth 0.5 before a guest write and an unmatched view did not cover the plane"); + Require(!ReadStart(*device, base).empty(), "a read of the first unit before a guest write and an unmatched view stored nothing"); + FillUnit(plane, 2, 0.75f); + const std::array words{0, 0, 0, 0, PlaneWidth, PlaneHeight, 1, 0}; + AgcDriver::Graphics::GuestTextureResource levels{}; + levels.baseAddress = base; + levels.width = PlaneWidth; + levels.height = PlaneHeight; + levels.mipCount = 2; + levels.lastLevel = 1; + levels.dimension = AgcDriver::Graphics::TextureDimension::k2D; + levels.tileMode = AgcDriver::Graphics::TextureTileMode::kZ64KBX; + levels.format = 22; + Require(AgcDriver::Graphics::DepthSurfaceTexture(AgcDriver::VulkanDeviceTestAccess::ContextOf(*device), words, levels, {}) == nullptr, "a view with two levels of a plane the guest wrote was served by the depth surface"); + device->WaitIdle(); + Require(!AgcDriver::Graphics::DepthSurfaceAt(base), "a view with two levels of a plane the guest wrote left the depth surface live"); + for (const std::size_t unit : {0u, 1u, 3u, 4u, 5u}) RequireUnit(plane, unit, 0.5f, Texels(unit), "the retire of a partly stored plane the guest wrote into"); + RequireUnit(plane, 2, 0.75f, UnitTexels, "the retire of a partly stored plane the guest wrote into, the written unit"); + } + + Fill(plane, 1.0f); + Require(draw(depth, writeHalf) == PlaneTexels, "the draw writing depth 0.5 under a later color target did not cover the plane"); + Require(!ReadStart(*device, base).empty(), "a read of the first unit before a color target stored nothing"); + auto* smallPlane = AllocateBlocks(Block, true); + const Surface smallDepth{reinterpret_cast(smallPlane), 0, 0, false, VK_FORMAT_D32_SFLOAT, 0, 0, swizzle}; + const std::span overLastUnit(reinterpret_cast(plane + 5u * UnitTexels), SmallColorBytes); + Draw(*device, shaders, smallDepth, {.depthTest = false}, SmallExtent, overLastUnit); + Require(!AgcDriver::Graphics::DepthSurfaceAt(base), "a color target over the last unit left the depth surface live"); + for (std::size_t unit = 0; unit < 5u; ++unit) RequireUnit(plane, unit, 0.5f, Texels(unit), "a color target over the last unit of a partly stored plane"); + + auto* looseBlocks = static_cast(AllocateBlocks(Block + PlaneBytes, false)); + auto* loose = reinterpret_cast(looseBlocks + Block); + const auto looseBase = reinterpret_cast(loose); + if (AgcDriver::GuestMemory::Watched(looseBase, PlaneBytes)) { + std::puts("memory outside the guest arena is write-watched here: a partial store over unwatched memory is not exercised"); + } else { + const Surface unwatched{looseBase, 0, 0, false, VK_FORMAT_D32_SFLOAT, 0, 0, swizzle}; + Fill(loose, 1.0f); + Require(draw(unwatched, writeHalf) == PlaneTexels, "the draw writing depth 0.5 into an unwatched plane did not cover it"); + before = AgcDriver::Graphics::DepthPlaneStoreCounts(); + Require(ReadStart(*device, looseBase).size() == 1, "a read of the first unit of an unwatched plane did not store it"); + stored = Since(before); + Require(stored.units == 1 && stored.bytes == Block, "a read of the first unit of an unwatched plane did not count one unit"); + RequireUnit(loose, 0, 0.5f, UnitTexels, "unwatched plane after a read of the first unit"); + for (std::size_t unit = 1; unit < Units; ++unit) RequireUnit(loose, unit, 1.0f, UnitTexels, "unwatched plane after a read of the first unit, a unit it does not overlap"); + Require(draw(unwatched, {.minDepth = 0.6f, .maxDepth = 0.6f}) == 0u, "a draw at depth 0.6 passed LESS over unwatched units still holding depth 0.5 on the GPU"); + } + std::puts("depth plane partial store tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} From db3af409704a595bb22e06bc43583d652c88632b Mon Sep 17 00:00:00 2001 From: Joey Fisher Date: Mon, 19 Oct 2026 10:53:16 -0600 Subject: [PATCH 143/621] fix(agc): wait for the idle flush's unit-shadow publish before returning The idle flush (VulkanDevice::StoreIdleResults) publishes the unit shadows over guest memory into the host import and only submits the copies. publishUnits picks the units to store on the CPU, and the slab -> import copy lands when the GPU runs it, so a guest store made between the two is overwritten with the older shadow bytes. The memory-access hook waits through SyncThrough right after its flush; the idle flush has no reader that waits. FlushPending and PublishShadowsOnly now sync the recorder under the GPU mutex after an idle publish that stored units. Other flush reasons are unchanged. Test: agc_idle_publish_wait (a CPU store made after the idle flush returns survives; a label flush's publish stays recorded). --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 4 +- .../libSceAgcDriver/Graphics/src/Texture.cpp | 17 +++- .../prx/libSceAgcDriver/tests/Recorder.cpp | 81 +++++++++++++++++++ 3 files changed, 100 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 198d4a0865..350d4e703e 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1542,6 +1542,8 @@ if(BUILD_TESTING) set_tests_properties(agc_sparse_run_import_copied PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) add_test(NAME agc_unit_shadow COMMAND agc_driver_recorder_tests --unit-shadow-only) 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") 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 @@ -1575,7 +1577,7 @@ if(BUILD_TESTING) add_test(NAME agc_metadata_pass COMMAND agc_driver_recorder_tests --metadata-pass-only) set_tests_properties(agc_metadata_pass PROPERTIES SKIP_RETURN_CODE 77) if(WIN32) - set_tests_properties(agc_recorder_read_list agc_host_import_unmap agc_parked_import_unmap agc_host_import_retirement agc_sparse_run_import agc_sparse_run_import_copied agc_unit_shadow agc_descriptor_pool_recycling agc_draw_snapshot_cap agc_draw_snapshot_epoch agc_multisample_guest_access agc_draw_inputs_in_place agc_draw_inputs_copied agc_resident_present agc_slice_dirty agc_queued_write_groups agc_metadata_pass agc_tiled_resolve PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_recorder_read_list agc_host_import_unmap agc_parked_import_unmap agc_host_import_retirement agc_sparse_run_import agc_sparse_run_import_copied agc_unit_shadow agc_idle_publish_wait agc_descriptor_pool_recycling agc_draw_snapshot_cap agc_draw_snapshot_epoch agc_multisample_guest_access agc_draw_inputs_in_place agc_draw_inputs_copied agc_resident_present agc_slice_dirty agc_queued_write_groups agc_metadata_pass agc_tiled_resolve PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") add_test(NAME agc_host_import_second_mapping COMMAND agc_driver_recorder_tests --host-import-second-mapping-only) set_tests_properties(agc_host_import_second_mapping PROPERTIES SKIP_RETURN_CODE 77 ENVIRONMENT_MODIFICATION diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index a70da26def..53c21f2c17 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -3674,6 +3674,18 @@ bool StorageTexture::pendingUnitInside(std::uint64_t address, std::size_t bytes) return false; } +namespace { + +void waitForIdlePublish(const char* reason) { + if (reason == nullptr || std::strcmp(reason, "idle") != 0 || Recorder::InCompletion()) return; + if (auto* recorder = Recorder::Active()) { + Recorder::CountSync(0); + recorder->Sync(); + } +} + +} + bool StorageTexture::FlushPending(std::uint64_t address, std::size_t bytes, const StorageTexture* except, const char* reason, PublishScope scope, bool* published) { static_cast(FlushDepthPlanes(address, bytes)); if (published != nullptr) *published = false; @@ -3767,6 +3779,7 @@ bool StorageTexture::FlushPending(std::uint64_t address, std::size_t bytes, cons // 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)); + if (units != 0) waitForIdlePublish(reason); if (published != nullptr) *published = units != 0; } return true; @@ -3932,7 +3945,9 @@ void StorageTexture::awaitStoredSave(const std::vector* layers) { bool StorageTexture::PublishShadowsOnly(std::uint64_t address, std::size_t bytes, PublishScope scope, const char* reason) { if (scope == PublishScope::None || !AnyShadowedOverlaps(address, bytes)) return false; std::lock_guard gpu(GuestMemory::GpuMutex()); - return PublishShadow(address, bytes, scope, PublishReasonFor(reason)) != 0; + if (PublishShadow(address, bytes, scope, PublishReasonFor(reason)) == 0) return false; + waitForIdlePublish(reason); + return true; } namespace { diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 497adb234f..6dbf0f5d32 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -4677,6 +4677,79 @@ bool unitShadowTests(const Device& device, Recorder& recorder) { return true; } +void idlePublishWaitTests(const Device& device, Recorder& recorder) { + using namespace AgcDriver::GuestMemory; + const auto& context = device.GetContext(); + constexpr std::uint64_t unit = 65536; + constexpr std::size_t bytes = 8 * unit; + void* block = AllocateWatched(bytes, 65536); + Require(block != nullptr, "unit shadows are on without write watching"); + struct Release { + void* block; + ~Release() { ReleaseWatched(block, bytes); } + } release{block}; + std::memset(block, 0x11, bytes); + const auto address = reinterpret_cast(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{context, block, address}; + const auto* import = HostImportFor(context, address, bytes); + if (import == nullptr || !Watched(address, bytes)) { + std::cout << "host import of the idle publish test block refused or not watched: idle publish wait not tested\n"; + return; + } + recorder.Sync(); + CollectWritesUncached(address, bytes); + auto* words = static_cast(block); + const auto copyBuffer = context.Function("vkCmdCopyBuffer"); + const auto retile = [&](std::uint64_t begin, std::uint8_t value) { + auto destination = ShadowDestinationFor(context, *import, begin, begin + unit); + Require(destination.has_value() && destination->seedUnits.empty(), "a whole-unit retile piece has no destination"); + auto pattern = std::make_shared(context, static_cast(unit), VK_BUFFER_USAGE_TRANSFER_SRC_BIT); + std::memset(pattern->Bytes().data(), value, pattern->Bytes().size()); + const auto commands = recorder.Commands(); + recorder.Keep(pattern); + recorder.Keep(destination->slab); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_HOST_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + const VkBufferCopy piece{0, destination->offset, unit}; + copyBuffer(commands, pattern->Handle(), destination->buffer, 1, &piece); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + recorder.MarkCovered(VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + const ShadowedRange range{begin, begin + unit, destination->slab}; + MarkShadowed(*import, std::span(&range, 1), TrackerGeneration()); + Require(AnyShadowedOverlaps(begin, unit), "the retiled unit is not shadowed"); + }; + retile(address + 2 * unit, 0x22); + retile(address + 3 * unit, 0x33); + StorageTexture::FlushAllPending("idle"); + Require(!AnyShadowedOverlaps(address + 2 * unit, static_cast(2 * unit)), "the idle flush did not publish the shadowed units"); + Require(!recorder.PendingWriteOverlaps(address + 2 * unit, static_cast(2 * unit)), "the idle flush returned before its publish's copies completed"); + words[2 * unit] = 0x44; + words[4 * unit - 1] = 0x45; + 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"); + 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"); + recorder.Sync(); + Require(words[5 * unit] == 0x55 && words[6 * unit - 1] == 0x55, "the label flush's publish did not land"); + std::cout << "idle publish wait verified\n"; +} + void storageRefreshTests(const Device& device, Recorder& recorder, bool watched) { const auto& base = device.GetContext(); if (base.hostImportAlignment == 0) { @@ -10164,6 +10237,14 @@ int main(int argc, char** argv) { parkedImportUnmapTests(device); return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--idle-publish-wait-only") { + if (!AgcDriver::Graphics::UnitShadowEnabled() || device.GetContext().hostImportAlignment == 0) { + std::cout << "skipped, unit shadows or host imports are unavailable\n"; + return 77; + } + idlePublishWaitTests(device, recorder); + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--unit-shadow-only") { if (!AgcDriver::Graphics::UnitShadowEnabled() || device.GetContext().hostImportAlignment == 0) { std::cout << "skipped, unit shadows or host imports are unavailable\n"; From ed921e225fae906ca55d540413bf56cc41a382fe Mon Sep 17 00:00:00 2001 From: Joey Fisher Date: Sun, 18 Oct 2026 22:13:20 -0600 Subject: [PATCH 144/621] fix(agc): hash the pages of an import window instead of copying them ImportWatched kept a copy of every non-zero page of the range being imported, so that a page the write watch reported during the import could be told from one that still held its bytes. The copy had the range's footprint: a first import of a 0.5-3 GiB guest heap held that much host memory while it ran. The window now keeps a 64-bit digest per 4 KiB page (four xxHash64 lanes) and compares digests: 8 bytes per page, 1/512 of the range. Same decision for every page, same reads of guest memory. CheckImportWindowFootprint imports a 64 MiB watched range and requires the heap held during the import to stay under 1 MiB. It failed with "held 64 MiB" before this change. --- .../Execution/src/GuestMemory.cpp | 42 +++++++++++-------- .../libSceAgcDriver/tests/WriteTracking.cpp | 36 ++++++++++++++++ 2 files changed, 61 insertions(+), 17 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index 6e7a43a575..bdd6f4f160 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -813,18 +813,33 @@ constexpr std::size_t WritePagesPerBlock = WriteBlockBytes / WritePageBytes; enum class StampKind : std::uint8_t { Cpu, Driver, ImportWindow, Fresh }; -constexpr std::array ZeroPage{}; +std::uint64_t HashPage(const std::byte* page) { + constexpr std::uint64_t prime1 = 0x9E3779B185EBCA87ull; + constexpr std::uint64_t prime2 = 0xC2B2AE3D27D4EB4Full; + constexpr std::uint64_t prime3 = 0x165667B19E3779F9ull; + constexpr std::uint64_t prime4 = 0x85EBCA77C2B2AE63ull; + const auto round = [](std::uint64_t accumulator, std::uint64_t word) { return std::rotl(accumulator + word * prime2, 31) * prime1; }; + std::array lanes{prime1 + prime2, prime2, 0, 0 - prime1}; + for (std::size_t offset = 0; offset < WritePageBytes; offset += 4 * sizeof(std::uint64_t)) { + std::array words; + std::memcpy(words.data(), page + offset, sizeof(words)); + for (std::size_t lane = 0; lane < 4; ++lane) lanes[lane] = round(lanes[lane], words[lane]); + } + std::uint64_t hash = std::rotl(lanes[0], 1) + std::rotl(lanes[1], 7) + std::rotl(lanes[2], 12) + std::rotl(lanes[3], 18); + for (const auto lane : lanes) hash = (hash ^ round(0, lane)) * prime1 + prime4; + hash ^= hash >> 33; + hash *= prime2; + hash ^= hash >> 29; + hash *= prime3; + return hash ^ (hash >> 32); +} struct WindowCopy { - static constexpr std::uint32_t Zero = std::numeric_limits::max(); std::uint64_t first; - std::span bytes; - std::span slots; + std::span hashes; bool Holds(std::uintptr_t page) const { - const auto slot = slots[(page - first) / WritePageBytes]; - const auto* expected = slot == Zero ? ZeroPage.data() : bytes.data() + static_cast(slot) * WritePageBytes; - return std::memcmp(reinterpret_cast(page), expected, WritePageBytes) == 0; + return HashPage(reinterpret_cast(page)) == hashes[(page - first) / WritePageBytes]; } }; @@ -1567,23 +1582,16 @@ bool ImportWatched(std::uint64_t address, std::size_t bytes, const std::function begin = end; } } - std::vector slots(static_cast((stop - first) / page), WindowCopy::Zero); - std::size_t kept = 0; + std::vector hashes(static_cast((stop - first) / page), 0); for (const auto& [begin, end] : spans) { CheckRange(reinterpret_cast(begin), static_cast(end - begin), 1, false); for (auto cursor = begin; cursor < end; cursor += page) { - if (std::memcmp(reinterpret_cast(cursor), ZeroPage.data(), page) != 0) slots[static_cast((cursor - first) / page)] = static_cast(kept++); + hashes[static_cast((cursor - first) / page)] = HashPage(reinterpret_cast(cursor)); } } - std::vector saved; - saved.reserve(kept * page); - const auto* source = reinterpret_cast(first); - for (std::size_t index = 0; index < slots.size(); ++index) { - if (slots[index] != WindowCopy::Zero) saved.insert(saved.end(), source + index * page, source + (index + 1) * page); - } std::erase_if(spans, [&](const auto& span) { return !walkWrites(tracker, span.first, span.second, StampKind::Cpu, true); }); if (!runImport()) return false; - const WindowCopy window{first, saved, slots}; + const WindowCopy window{first, hashes}; auto unwatchedFrom = first; for (const auto& [begin, end] : spans) { if (!walkWrites(tracker, begin, end, StampKind::Cpu, true, &window)) continue; diff --git a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp index 700455ddd0..e7ad973bba 100644 --- a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp @@ -12,6 +12,9 @@ #include #include #endif +#ifdef __GLIBC__ +#include +#endif #include #include #include @@ -1153,6 +1156,38 @@ void CheckImportWindowOverUnwatchedBlock() { Require(after != 0 && !UnchangedSince(base, Block, after), "a store after an import window over an unwatched block was missed"); } +void CheckImportWindowFootprint() { +#ifdef __GLIBC__ + constexpr std::size_t bytes = std::size_t{64} << 20; + void* memory = AllocateWatched(bytes); + const auto base = reinterpret_cast(memory); + std::memset(memory, 0x5a, bytes); + Require(CollectWritesUncached(base, bytes) != 0, "the footprint import window block is not collected"); + const auto heapBytes = [] { + const auto info = mallinfo2(); + return info.uordblks + info.hblkhd; + }; + const auto before = heapBytes(); + std::size_t during = 0; + Require(ImportWatched(base, bytes, [&] { + during = heapBytes(); + return true; + }), "the footprint import window import failed"); + Require(Watched(base, bytes), "the footprint import window left its range unwatched"); + const auto held = during > before ? during - before : 0; + Require(held <= bytes / 64, "an import window over " + std::to_string(bytes >> 20) + " MiB held " + std::to_string(held >> 20) + " MiB of host memory during the import: it copies the range instead of hashing its pages"); + auto* plain = static_cast(memory); + const auto synced = CollectWritesUncached(base, bytes); + Require(ImportWatched(base, bytes, [&] { + plain[0x100] = 0x5a; + plain[Block + 0x100] = 0x5b; + return true; + }), "the footprint import window re-import failed"); + Require(UnchangedSince(base, Block, synced), "a page a hashed import window resolved without changing its bytes reads as written"); + Require(WrittenSince(base + Block, Block, synced), "a store of new bytes within a hashed import window was missed"); +#endif +} + void ProtectPage(std::uint64_t address, bool readable) { #ifdef _WIN32 DWORD previous = 0; @@ -1442,6 +1477,7 @@ int main() { CheckImportWindowOtherThread(); CheckImportWindowOverUnwatchedBlock(); CheckStoreAcrossImports(); + CheckImportWindowFootprint(); CheckUnwatchedImport(); CheckCommittedRanges(); CheckAdoptLatestEpoch(); From eb2aae7f8b58d7a38230230e6707f2157829917d Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Wed, 21 Oct 2026 23:40:11 +1300 Subject: [PATCH 145/621] fix(agc): refresh every layer a layered array color draw attaches A slice of a 2D array color target was refreshed one layer wide while its attachment view spans color.layers, so a layered draw refreshed only its first slice. Pass the attached layer count. --- 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 94c47bf27a..8f8a1dd2bf 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -2235,7 +2235,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& const auto geometry = DescribeSurface(array); for (std::uint32_t slice = 0; slice <= color.arrayLastSlice; ++slice) Require(geometry.GuestLayerOffset(slice) == slice * sliceBytes, "the array image's layer pitch differs from the color view's slice offset"); Require(color.surfaceAddress == color.arrayAddress + color.arraySlice * sliceBytes, "the array image's layer pitch differs from the color view's slice offset"); - auto resident = CachedStorageLayers(context, array, color.arraySlice, 1); + auto resident = CachedStorageLayers(context, array, color.arraySlice, color.layers); Require(resident->Attachable(), "storage format cannot be a color attachment"); return resident; } 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 146/621] 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 36226741f56ebb3a1d8902ebdc6e612a93975ff6 Mon Sep 17 00:00:00 2001 From: Retr0 Date: Tue, 20 Oct 2026 19:09:51 +0300 Subject: [PATCH 147/621] 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 99bdd60a6ed0c0b5e094d948924bfec1f1a2104e Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Wed, 21 Oct 2026 14:59:31 +0300 Subject: [PATCH 148/621] 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 149/621] 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 150/621] 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 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 151/621] 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 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 152/621] 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 b8297adab94813820d459687ba609495eb6780ab Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis Date: Fri, 16 Oct 2026 00:15:13 +0300 Subject: [PATCH 153/621] feat(agc): add fragment-preserving R_X and Z_X layouts --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 23 +++ .../include/MultisampleSwizzleEquations.hpp | 53 +++++ .../include/MultisampleTextureLayout.hpp | 54 ++++++ .../Graphics/src/MultisampleTextureLayout.cpp | 120 ++++++++++++ .../tests/MultisampleTextureLayout.cpp | 182 ++++++++++++++++++ docs/dev/debt/libSceAgcDriver.md | 1 + 6 files changed, 433 insertions(+) create mode 100644 core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleSwizzleEquations.hpp create mode 100644 core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTextureLayout.hpp create mode 100644 core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTextureLayout.cpp create mode 100644 core/libs/prx/libSceAgcDriver/tests/MultisampleTextureLayout.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 177d7b1d5c..7efd108e1d 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -334,6 +334,7 @@ set(agcDriverSources ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/TextureFormat.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/DccMetadata.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/TextureTiling.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/MultisampleTextureLayout.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/TextureDetiler.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/MultisampleReadback.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Texture.cpp @@ -1846,6 +1847,28 @@ endif() set_target_properties(agc_driver_graphics_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_driver_graphics_tests) +add_test_executable(agc_driver_msaa_layout_tests EXCLUDE_FROM_ALL + tests/MultisampleTextureLayout.cpp + Graphics/src/MultisampleTextureLayout.cpp +) +target_include_directories(agc_driver_msaa_layout_tests PRIVATE + ${LIBS_INCLUDE_DIR} + ${CMAKE_SOURCE_DIR}/core/shader/recompiler + ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR} +) +target_link_libraries(agc_driver_msaa_layout_tests PRIVATE libc) +set_target_properties(agc_driver_msaa_layout_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_driver_msaa_layout_tests) +if(BUILD_TESTING) + set_target_properties(agc_driver_msaa_layout_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_driver_msaa_layout COMMAND agc_driver_msaa_layout_tests) + set_tests_properties(agc_driver_msaa_layout PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(agc_driver_msaa_layout PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + foreach(agcVertexRangeShader IN ITEMS VertexRange.vert VertexRangeProducer.frag) string(REPLACE "." "_" agcVertexRangeName "${agcVertexRangeShader}") string(TOUPPER "${agcVertexRangeName}_SPV" agcVertexRangeSymbol) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleSwizzleEquations.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleSwizzleEquations.hpp new file mode 100644 index 0000000000..358d893eb8 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleSwizzleEquations.hpp @@ -0,0 +1,53 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_MULTISAMPLESWIZZLEEQUATIONS_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_MULTISAMPLESWIZZLEEQUATIONS_HPP + +#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" +#include +#include + +namespace AgcDriver::Graphics { + +struct MultisampleSwizzleEquation { + TextureTileMode tileMode; + std::uint32_t elementBytes; + std::uint32_t fragments; + std::array bits; +}; + +inline constexpr MultisampleSwizzleEquation kMultisampleSwizzleEquations[] = { + {TextureTileMode::kZ64KBX, 1u, 2u, {0x0001000000000000ull, 0x0000000000000001ull, 0x0000000000010000ull, 0x0000000000000002ull, 0x0000000000020000ull, 0x0000000000000004ull, 0x0000000000040000ull, 0x0000000000000008ull, 0x0000000800080008ull, 0x0000000400100010ull, 0x0000000200200040ull, 0x0000000100400020ull, 0x0000000000100000ull, 0x0000000000400000ull, 0x0000000000000040ull, 0x0000000000800000ull}}, + {TextureTileMode::kZ64KBX, 2u, 2u, {0x0000000000000000ull, 0x0001000000000000ull, 0x0000000000000001ull, 0x0000000000010000ull, 0x0000000000000002ull, 0x0000000000020000ull, 0x0000000000000004ull, 0x0000000000040000ull, 0x0000000800080008ull, 0x0000000400100010ull, 0x0000000200200040ull, 0x0000000100400020ull, 0x0000000000080000ull, 0x0000000000000010ull, 0x0000000000400000ull, 0x0000000000000040ull}}, + {TextureTileMode::kZ64KBX, 4u, 2u, {0x0000000000000000ull, 0x0000000000000000ull, 0x0001000000000000ull, 0x0000000000000001ull, 0x0000000000010000ull, 0x0000000000000002ull, 0x0000000000020000ull, 0x0000000000000004ull, 0x0000000800080008ull, 0x0000000400100010ull, 0x0000000200200040ull, 0x0000000100400020ull, 0x0000000000080000ull, 0x0000000000000010ull, 0x0000000000400000ull, 0x0000000000840000ull}}, + {TextureTileMode::kZ64KBX, 1u, 4u, {0x0001000000000000ull, 0x0002000000000000ull, 0x0000000000000001ull, 0x0000000000010000ull, 0x0000000000000002ull, 0x0000000000020000ull, 0x0000000000000004ull, 0x0000000000040000ull, 0x0000000800080008ull, 0x0000000400100010ull, 0x0000000200200040ull, 0x0000000100400020ull, 0x0000000000080000ull, 0x0000000000000010ull, 0x0000000000400000ull, 0x0000000000000040ull}}, + {TextureTileMode::kZ64KBX, 2u, 4u, {0x0000000000000000ull, 0x0001000000000000ull, 0x0002000000000000ull, 0x0000000000000001ull, 0x0000000000010000ull, 0x0000000000000002ull, 0x0000000000020000ull, 0x0000000000000004ull, 0x0000000800080008ull, 0x0000000400100010ull, 0x0000000200200040ull, 0x0000000100400020ull, 0x0000000000000008ull, 0x0000000000100000ull, 0x0000000000000040ull, 0x0000000000440000ull}}, + {TextureTileMode::kZ64KBX, 4u, 4u, {0x0000000000000000ull, 0x0000000000000000ull, 0x0001000000000000ull, 0x0002000000000000ull, 0x0000000000000001ull, 0x0000000000010000ull, 0x0000000000000002ull, 0x0000000000020000ull, 0x0000000800080008ull, 0x0000000400100010ull, 0x0000000200200040ull, 0x0000000100400020ull, 0x0000000000080000ull, 0x0000000000000010ull, 0x0000000000440000ull, 0x0000000000800004ull}}, + {TextureTileMode::kZ64KBX, 1u, 8u, {0x0001000000000000ull, 0x0002000000000000ull, 0x0004000000000000ull, 0x0000000000000001ull, 0x0000000000010000ull, 0x0000000000000002ull, 0x0000000000020000ull, 0x0000000000000004ull, 0x0000000400080008ull, 0x0000000200100010ull, 0x0000000100200040ull, 0x0000000800400020ull, 0x0000000000080000ull, 0x0000000000000010ull, 0x0000000000400000ull, 0x0000000000840000ull}}, + {TextureTileMode::kZ64KBX, 2u, 8u, {0x0000000000000000ull, 0x0001000000000000ull, 0x0002000000000000ull, 0x0004000000000000ull, 0x0000000000000001ull, 0x0000000000010000ull, 0x0000000000000002ull, 0x0000000000020000ull, 0x0000000400080008ull, 0x0000000200100010ull, 0x0000000100200040ull, 0x0000000800400020ull, 0x0000000000080000ull, 0x0000000000000010ull, 0x0000000000440000ull, 0x0000000000800004ull}}, + {TextureTileMode::kZ64KBX, 4u, 8u, {0x0000000000000000ull, 0x0000000000000000ull, 0x0001000000000000ull, 0x0002000000000000ull, 0x0004000000000000ull, 0x0000000000000001ull, 0x0000000000010000ull, 0x0000000000000002ull, 0x0000000400080008ull, 0x0000000200100010ull, 0x0000000100200040ull, 0x0000000000440020ull, 0x0000000000000008ull, 0x0000000000100000ull, 0x0000000000400004ull, 0x0000000000820000ull}}, + {TextureTileMode::kR64KBX, 1u, 2u, {0x0000000000000001ull, 0x0000000000000002ull, 0x0000000000000004ull, 0x0000000000020000ull, 0x0000000000010000ull, 0x0000000000040000ull, 0x0000000000000008ull, 0x0000000000100000ull, 0x0000000800080008ull, 0x0000000400100010ull, 0x0000000200200040ull, 0x0000000100400020ull, 0x0000000000400000ull, 0x0000000000000040ull, 0x0000000000800000ull, 0x0001000001000000ull}}, + {TextureTileMode::kR64KBX, 2u, 2u, {0x0000000000000000ull, 0x0000000000000001ull, 0x0000000000000002ull, 0x0000000000000004ull, 0x0000000000010000ull, 0x0000000000020000ull, 0x0000000000040000ull, 0x0000000000000008ull, 0x0000000800080008ull, 0x0000000400100010ull, 0x0000000200200040ull, 0x0000000100400020ull, 0x0000000000100000ull, 0x0000000000000040ull, 0x0000000000400000ull, 0x0001000000800000ull}}, + {TextureTileMode::kR64KBX, 4u, 2u, {0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000001ull, 0x0000000000000002ull, 0x0000000000010000ull, 0x0000000000020000ull, 0x0000000000040000ull, 0x0000000000000004ull, 0x0000000800080008ull, 0x0000000400100010ull, 0x0000000200200040ull, 0x0000000100400020ull, 0x0000000000080000ull, 0x0000000000000010ull, 0x0000000000400000ull, 0x0001000000800000ull}}, + {TextureTileMode::kR64KBX, 8u, 2u, {0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000001ull, 0x0000000000010000ull, 0x0000000000000002ull, 0x0000000000000004ull, 0x0000000000020000ull, 0x0000000800080008ull, 0x0000000400100010ull, 0x0000000200200040ull, 0x0000000100400020ull, 0x0000000000040000ull, 0x0000000000000008ull, 0x0000000000100000ull, 0x0001000000400000ull}}, + {TextureTileMode::kR64KBX, 16u, 2u, {0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000001ull, 0x0000000000010000ull, 0x0000000000000002ull, 0x0000000000020000ull, 0x0000000400080008ull, 0x0000000200100010ull, 0x0000000100200040ull, 0x0001000000400020ull, 0x0000000000040000ull, 0x0000000000000004ull, 0x0000000000080000ull, 0x0000000000000010ull}}, + {TextureTileMode::kR64KBX, 1u, 4u, {0x0000000000000001ull, 0x0000000000000002ull, 0x0000000000000004ull, 0x0000000000020000ull, 0x0000000000010000ull, 0x0000000000040000ull, 0x0000000000000008ull, 0x0000000000100000ull, 0x0000000800080008ull, 0x0000000400100010ull, 0x0000000200200040ull, 0x0000000100400020ull, 0x0000000000400000ull, 0x0000000000000040ull, 0x0001000000800000ull, 0x0002000001000000ull}}, + {TextureTileMode::kR64KBX, 2u, 4u, {0x0000000000000000ull, 0x0000000000000001ull, 0x0000000000000002ull, 0x0000000000000004ull, 0x0000000000010000ull, 0x0000000000020000ull, 0x0000000000040000ull, 0x0000000000000008ull, 0x0000000800080008ull, 0x0000000400100010ull, 0x0000000200200040ull, 0x0000000100400020ull, 0x0000000000100000ull, 0x0000000000000040ull, 0x0001000000400000ull, 0x0002000000800000ull}}, + {TextureTileMode::kR64KBX, 4u, 4u, {0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000001ull, 0x0000000000000002ull, 0x0000000000010000ull, 0x0000000000020000ull, 0x0000000000040000ull, 0x0000000000000004ull, 0x0000000800080008ull, 0x0000000400100010ull, 0x0000000200200040ull, 0x0000000100400020ull, 0x0000000000080000ull, 0x0000000000000010ull, 0x0001000000400000ull, 0x0002000000800000ull}}, + {TextureTileMode::kR64KBX, 8u, 4u, {0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000001ull, 0x0000000000010000ull, 0x0000000000000002ull, 0x0000000000000004ull, 0x0000000000020000ull, 0x0000000400080008ull, 0x0000000200100010ull, 0x0002000000200040ull, 0x0000000100400020ull, 0x0000000000040000ull, 0x0000000000000008ull, 0x0000000000100000ull, 0x0001000000400000ull}}, + {TextureTileMode::kR64KBX, 16u, 4u, {0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000001ull, 0x0000000000010000ull, 0x0000000000000002ull, 0x0000000000020000ull, 0x0000000200080008ull, 0x0000000100100010ull, 0x0002000000200040ull, 0x0001000000400020ull, 0x0000000000040000ull, 0x0000000000000004ull, 0x0000000000080000ull, 0x0000000000000010ull}}, + {TextureTileMode::kR64KBX, 1u, 8u, {0x0000000000000001ull, 0x0000000000000002ull, 0x0000000000000004ull, 0x0000000000020000ull, 0x0000000000010000ull, 0x0000000000040000ull, 0x0000000000000008ull, 0x0000000000100000ull, 0x0000000400080008ull, 0x0000000200100010ull, 0x0000000100200040ull, 0x0000000800400020ull, 0x0000000000400000ull, 0x0001000000800000ull, 0x0002000001000000ull, 0x0004000002000000ull}}, + {TextureTileMode::kR64KBX, 2u, 8u, {0x0000000000000000ull, 0x0000000000000001ull, 0x0000000000000002ull, 0x0000000000000004ull, 0x0000000000010000ull, 0x0000000000020000ull, 0x0000000000040000ull, 0x0000000000000008ull, 0x0000000400080008ull, 0x0000000200100010ull, 0x0000000100200040ull, 0x0000000800400020ull, 0x0000000000100000ull, 0x0001000000400000ull, 0x0002000000800000ull, 0x0004000001000000ull}}, + {TextureTileMode::kR64KBX, 4u, 8u, {0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000001ull, 0x0000000000000002ull, 0x0000000000010000ull, 0x0000000000020000ull, 0x0000000000040000ull, 0x0000000000000004ull, 0x0000000400080008ull, 0x0000000200100010ull, 0x0000000100200040ull, 0x0004000000400020ull, 0x0000000000080000ull, 0x0000000000000010ull, 0x0001000000400000ull, 0x0002000000800000ull}}, + {TextureTileMode::kR64KBX, 8u, 8u, {0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000001ull, 0x0000000000010000ull, 0x0000000000000002ull, 0x0000000000000004ull, 0x0000000000020000ull, 0x0000000200080008ull, 0x0000000100100010ull, 0x0004000000200040ull, 0x0002000000400020ull, 0x0000000000040000ull, 0x0000000000000008ull, 0x0000000000100000ull, 0x0001000000400000ull}}, + {TextureTileMode::kR64KBX, 16u, 8u, {0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000001ull, 0x0000000000010000ull, 0x0000000000000002ull, 0x0000000000020000ull, 0x0000000200080008ull, 0x0000000100100010ull, 0x0004000000200040ull, 0x0002000000400020ull, 0x0000000000000004ull, 0x0000000000040000ull, 0x0000000000000008ull, 0x0001000000400000ull}}, +}; + +constexpr const MultisampleSwizzleEquation* FindMultisampleSwizzleEquation(TextureTileMode tileMode, std::uint32_t elementBytes, std::uint32_t fragments) { + for (const auto& equation : kMultisampleSwizzleEquations) { + if (equation.tileMode == tileMode && equation.elementBytes == elementBytes && equation.fragments == fragments) return &equation; + } + return nullptr; +} + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTextureLayout.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTextureLayout.hpp new file mode 100644 index 0000000000..a7960c43dc --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTextureLayout.hpp @@ -0,0 +1,54 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_MULTISAMPLETEXTURELAYOUT_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_MULTISAMPLETEXTURELAYOUT_HPP + +#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" +#include +#include +#include +#include +#include + +namespace AgcDriver::Graphics { + +class MultisampleTextureLayout { +public: + static constexpr std::uint32_t Alignment = 65536u; + + MultisampleTextureLayout(TextureTileMode tileMode, std::uint32_t elementBytes, std::uint32_t fragments, std::uint32_t width, std::uint32_t height, std::uint32_t layers, std::uint32_t pipeBankXor = 0); + std::uint64_t Bytes() const { return sliceBytes * layers; } + std::uint64_t SliceBytes() const { return sliceBytes; } + std::uint64_t LinearBytes() const { return static_cast(width) * height * layers * fragments * elementBytes; } + std::uint32_t Pitch() const { return pitch; } + std::uint32_t PaddedHeight() const { return paddedHeight; } + std::uint32_t BlockWidth() const { return blockWidth; } + std::uint32_t BlockHeight() const { return blockHeight; } + const std::array& InverseBlockEquation() const { return inverseBlockEquation; } + std::uint64_t Offset(std::uint32_t x, std::uint32_t y, std::uint32_t layer, std::uint32_t fragment) const; + void Detile(std::span source, std::span destination) const; + void Tile(std::span source, std::span destination) const; + +private: + std::uint64_t offset(std::uint32_t x, std::uint32_t y, std::uint32_t layer, std::uint32_t fragment) const; + void transfer(std::span source, std::span destination, bool retile) const; + + std::uint32_t elementBytes; + std::uint32_t fragments; + std::uint32_t width; + std::uint32_t height; + std::uint32_t layers; + std::uint32_t pipeBankXor; + std::uint32_t blockWidth; + std::uint32_t blockHeight; + std::uint32_t pitch; + std::uint32_t paddedHeight; + std::uint64_t sliceBytes; + std::array inverseBlockEquation; + std::vector xOffsets; + std::vector yOffsets; + std::vector layerOffsets; + std::vector fragmentOffsets; +}; + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTextureLayout.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTextureLayout.cpp new file mode 100644 index 0000000000..96a5cbc02a --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTextureLayout.cpp @@ -0,0 +1,120 @@ +#include "prx/libSceAgcDriver/Graphics/include/MultisampleTextureLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/MultisampleSwizzleEquations.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Context.hpp" +#include +#include +#include +#include + +namespace AgcDriver::Graphics { +namespace { + +std::array inverseBlock(const MultisampleSwizzleEquation& equation, std::uint32_t byteBits, std::uint32_t fragmentBits, std::uint32_t widthBits) { + std::array rows{}, inverse{}; + for (std::uint32_t input = 0; input < 16u; ++input) { + std::uint32_t address = 0u; + if (input < byteBits) address = 1u << input; + else { + const auto coordinateBit = input < byteBits + fragmentBits ? 48u + input - byteBits : input < byteBits + fragmentBits + widthBits ? input - byteBits - fragmentBits : 16u + input - byteBits - fragmentBits - widthBits; + const auto coordinate = std::uint64_t{1} << coordinateBit; + for (std::uint32_t output = 0; output < 16u; ++output) address |= static_cast(std::popcount(coordinate & equation.bits[output]) & 1u) << output; + } + for (std::uint32_t output = 0; output < 16u; ++output) rows[output] |= ((address >> output) & 1u) << input; + inverse[input] = 1u << input; + } + for (std::uint32_t input = 0; input < 16u; ++input) { + auto pivot = input; + while (pivot < 16u && (rows[pivot] & (1u << input)) == 0u) ++pivot; + Require(pivot < 16u, "multisample block address equation is not invertible"); + std::swap(rows[input], rows[pivot]); + std::swap(inverse[input], inverse[pivot]); + for (std::uint32_t row = 0; row < 16u; ++row) { + if (row == input || (rows[row] & (1u << input)) == 0u) continue; + rows[row] ^= rows[input]; + inverse[row] ^= inverse[input]; + } + } + return inverse; +} + +std::vector axisOffsets(const MultisampleSwizzleEquation& equation, std::uint32_t count, std::uint32_t shift) { + std::vector offsets(count); + for (std::uint32_t value = 0; value < count; ++value) { + const auto coordinate = static_cast(value) << shift; + for (std::uint32_t bit = 0; bit < equation.bits.size(); ++bit) { + offsets[value] |= static_cast(std::popcount(coordinate & equation.bits[bit]) & 1u) << bit; + } + } + return offsets; +} + +} + +MultisampleTextureLayout::MultisampleTextureLayout(TextureTileMode tileMode, std::uint32_t elementBytes, std::uint32_t fragments, std::uint32_t width, std::uint32_t height, std::uint32_t layers, std::uint32_t pipeBankXor) : elementBytes(elementBytes), fragments(fragments), width(width), height(height), layers(layers), pipeBankXor(pipeBankXor) { + Require(width != 0 && height != 0 && width <= 16384u && height <= 16384u && layers != 0 && layers <= 8192u, "invalid multisampled surface extent or layer count"); + Require(std::has_single_bit(elementBytes) && elementBytes <= 16u, "unsupported multisampled surface element size"); + Require(fragments == 2u || fragments == 4u || fragments == 8u, "unsupported multisampled surface fragment count"); + Require((pipeBankXor & ~0xcf00u) == 0, "multisampled surface pipe/bank XOR uses bits outside its 16-pipe swizzle"); + Require(tileMode != TextureTileMode::kZ64KBX || elementBytes <= 4u, "multisampled Z surface element size exceeds 32 bits"); + const auto* equation = FindMultisampleSwizzleEquation(tileMode, elementBytes, fragments); + Require(equation != nullptr, "unsupported multisampled surface tile mode"); + + const auto fragmentBits = static_cast(std::countr_zero(fragments)); + const auto elementBits = 16u - static_cast(std::countr_zero(elementBytes)) - fragmentBits; + const auto widthBits = (elementBits + ((fragmentBits & 1u) == 0 ? 1u : 0u)) / 2u; + blockWidth = 1u << widthBits; + blockHeight = 1u << (elementBits - widthBits); + inverseBlockEquation = inverseBlock(*equation, static_cast(std::countr_zero(elementBytes)), fragmentBits, widthBits); + pitch = (width + blockWidth - 1u) / blockWidth * blockWidth; + paddedHeight = (height + blockHeight - 1u) / blockHeight * blockHeight; + sliceBytes = static_cast(pitch) * paddedHeight * fragments * elementBytes; + Require(sliceBytes <= std::numeric_limits::max() / layers, "multisampled surface size overflows"); + xOffsets = axisOffsets(*equation, width, 0u); + yOffsets = axisOffsets(*equation, height, 16u); + layerOffsets = axisOffsets(*equation, layers, 32u); + fragmentOffsets = axisOffsets(*equation, fragments, 48u); +} + +std::uint64_t MultisampleTextureLayout::offset(std::uint32_t x, std::uint32_t y, std::uint32_t layer, std::uint32_t fragment) const { + const auto block = static_cast(y / blockHeight) * (pitch / blockWidth) + x / blockWidth; + const auto withinBlock = xOffsets[x] ^ yOffsets[y] ^ layerOffsets[layer] ^ fragmentOffsets[fragment] ^ pipeBankXor; + return static_cast(layer) * sliceBytes + block * Alignment + withinBlock; +} + +std::uint64_t MultisampleTextureLayout::Offset(std::uint32_t x, std::uint32_t y, std::uint32_t layer, std::uint32_t fragment) const { + Require(x < width && y < height && layer < layers && fragment < fragments, "multisampled surface coordinate is outside its extent"); + return offset(x, y, layer, fragment); +} + +void MultisampleTextureLayout::transfer(std::span source, std::span destination, bool retile) const { + const auto sourceBytes = retile ? LinearBytes() : Bytes(); + const auto destinationBytes = retile ? Bytes() : LinearBytes(); + Require(source.size() >= sourceBytes && destination.size() >= destinationBytes, "multisampled surface transfer buffer is too small"); + const auto sourceAddress = reinterpret_cast(source.data()); + const auto destinationAddress = reinterpret_cast(destination.data()); + Require(sourceAddress <= std::numeric_limits::max() - sourceBytes && destinationAddress <= std::numeric_limits::max() - destinationBytes, "multisampled surface transfer range overflows"); + Require(sourceAddress + sourceBytes <= destinationAddress || destinationAddress + destinationBytes <= sourceAddress, "multisampled surface transfer buffers overlap"); + + std::uint64_t linear = 0; + for (std::uint32_t layer = 0; layer < layers; ++layer) { + for (std::uint32_t y = 0; y < height; ++y) { + for (std::uint32_t x = 0; x < width; ++x) { + for (std::uint32_t fragment = 0; fragment < fragments; ++fragment) { + const auto tiled = offset(x, y, layer, fragment); + std::memcpy(destination.data() + (retile ? tiled : linear), source.data() + (retile ? linear : tiled), elementBytes); + linear += elementBytes; + } + } + } + } +} + +void MultisampleTextureLayout::Detile(std::span source, std::span destination) const { + transfer(source, destination, false); +} + +void MultisampleTextureLayout::Tile(std::span source, std::span destination) const { + transfer(source, destination, true); +} + +} diff --git a/core/libs/prx/libSceAgcDriver/tests/MultisampleTextureLayout.cpp b/core/libs/prx/libSceAgcDriver/tests/MultisampleTextureLayout.cpp new file mode 100644 index 0000000000..2986c79da0 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/MultisampleTextureLayout.cpp @@ -0,0 +1,182 @@ +#include "prx/libSceAgcDriver/Graphics/include/MultisampleTextureLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Context.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace AgcDriver::Graphics; + +namespace { + +struct ReferenceCase { + TextureTileMode tileMode; + std::uint32_t elementBytes; + std::uint32_t fragments; + std::uint32_t blockWidth; + std::uint32_t blockHeight; + std::uint32_t pitch; + std::uint32_t paddedHeight; + std::uint64_t sliceBytes; + std::array addresses; +}; + +constexpr std::array, 7> kCoordinates{{ + {0u, 0u, 0u}, {128u, 66u, 16u}, {64u, 32u, 1u}, {32u, 64u, 2u}, + {63u, 31u, 7u}, {127u, 65u, 15u}, {1u, 1u, 3u} +}}; + +constexpr ReferenceCase kReferences[] = { + {TextureTileMode::kR64KBX, 1u, 2u, 128u, 256u, 256u, 256u, 131072ull, {{0ull, 2201608ull, 141312ull, 300032ull, 919295ull, 2013271ull, 396305ull}}}, + {TextureTileMode::kR64KBX, 1u, 4u, 128u, 128u, 256u, 128u, 131072ull, {{0ull, 2185224ull, 174080ull, 316416ull, 919295ull, 1996887ull, 429073ull}}}, + {TextureTileMode::kR64KBX, 1u, 8u, 64u, 128u, 192u, 128u, 196608ull, {{0ull, 3291144ull, 279552ull, 422400ull, 1413119ull, 3061847ull, 640529ull}}}, + {TextureTileMode::kR64KBX, 2u, 2u, 128u, 128u, 256u, 128u, 131072ull, {{0ull, 2213920ull, 141312ull, 312320ull, 923390ull, 2025630ull, 396306ull}}}, + {TextureTileMode::kR64KBX, 2u, 4u, 128u, 64u, 256u, 128u, 262144ull, {{0ull, 4392992ull, 305152ull, 689152ull, 1840894ull, 4073630ull, 822290ull}}}, + {TextureTileMode::kR64KBX, 2u, 8u, 64u, 64u, 192u, 128u, 393216ull, {{0ull, 6621216ull, 476160ull, 999936ull, 2793470ull, 6195358ull, 1230354ull}}}, + {TextureTileMode::kR64KBX, 4u, 2u, 64u, 128u, 192u, 128u, 196608ull, {{0ull, 3328032ull, 264192ull, 443392ull, 1390332ull, 3074204ull, 592916ull}}}, + {TextureTileMode::kR64KBX, 4u, 4u, 64u, 64u, 192u, 128u, 393216ull, {{0ull, 6621216ull, 493568ull, 1016832ull, 2766588ull, 6170780ull, 1215508ull}}}, + {TextureTileMode::kR64KBX, 4u, 8u, 32u, 64u, 160u, 128u, 655360ull, {{0ull, 11077664ull, 820224ull, 1737216ull, 4667388ull, 10365084ull, 2002452ull}}}, + {TextureTileMode::kR64KBX, 8u, 2u, 64u, 64u, 192u, 128u, 393216ull, {{0ull, 6621312ull, 460800ull, 984064ull, 2782968ull, 6170744ull, 1182744ull}}}, + {TextureTileMode::kR64KBX, 8u, 4u, 64u, 32u, 192u, 96u, 589824ull, {{0ull, 9963648ull, 855040ull, 1574400ull, 4158456ull, 9317496ull, 1773080ull}}}, + {TextureTileMode::kR64KBX, 8u, 8u, 32u, 32u, 160u, 96u, 983040ull, {{0ull, 16648320ull, 1444352ull, 2689280ull, 6979576ull, 15605880ull, 2952984ull}}}, + {TextureTileMode::kR64KBX, 16u, 2u, 32u, 64u, 160u, 128u, 655360ull, {{0ull, 11075712ull, 787456ull, 1706496ull, 4718576ull, 10397808ull, 1967664ull}}}, + {TextureTileMode::kR64KBX, 16u, 4u, 32u, 32u, 160u, 96u, 983040ull, {{0ull, 16646272ull, 1443328ull, 2690304ull, 7011312ull, 15641712ull, 2950960ull}}}, + {TextureTileMode::kR64KBX, 16u, 8u, 16u, 32u, 144u, 96u, 1769472ull, {{0ull, 30017664ull, 2624000ull, 4851968ull, 12615664ull, 28201072ull, 5312304ull}}}, + {TextureTileMode::kZ64KBX, 1u, 2u, 128u, 256u, 256u, 256u, 131072ull, {{0ull, 2172945ull, 149504ull, 271361ull, 923390ull, 1992879ull, 396294ull}}}, + {TextureTileMode::kZ64KBX, 1u, 4u, 128u, 128u, 256u, 128u, 131072ull, {{0ull, 2181153ull, 165890ull, 279555ull, 931580ull, 2025565ull, 396302ull}}}, + {TextureTileMode::kZ64KBX, 1u, 8u, 64u, 128u, 192u, 128u, 196608ull, {{0ull, 3295297ull, 263170ull, 410115ull, 1425404ull, 3041469ull, 591390ull}}}, + {TextureTileMode::kZ64KBX, 2u, 2u, 128u, 128u, 256u, 128u, 131072ull, {{0ull, 2181154ull, 165888ull, 279554ull, 931580ull, 2025566ull, 396300ull}}}, + {TextureTileMode::kZ64KBX, 2u, 4u, 128u, 64u, 256u, 128u, 262144ull, {{0ull, 4425794ull, 280580ull, 689158ull, 1881848ull, 4118714ull, 789532ull}}}, + {TextureTileMode::kZ64KBX, 2u, 8u, 64u, 64u, 192u, 128u, 393216ull, {{0ull, 6637698ull, 459780ull, 999942ull, 2818040ull, 6219898ull, 1181244ull}}}, + {TextureTileMode::kZ64KBX, 4u, 2u, 64u, 128u, 192u, 128u, 196608ull, {{0ull, 3295300ull, 264192ull, 410628ull, 1423096ull, 3041468ull, 592920ull}}}, + {TextureTileMode::kZ64KBX, 4u, 4u, 64u, 64u, 192u, 128u, 393216ull, {{0ull, 6637700ull, 460808ull, 1000460ull, 2815728ull, 6219892ull, 1182776ull}}}, + {TextureTileMode::kZ64KBX, 4u, 8u, 32u, 64u, 160u, 128u, 655360ull, {{0ull, 11126788ull, 787464ull, 1720844ull, 4716528ull, 10359028ull, 1967736ull}}}, +}; + +template +void requireRefusal(TFunction action) { + bool refused = false; + try { action(); } catch (const std::exception&) { refused = true; } + Require(refused, "multisampled layout accepted an unsupported operation"); +} + +void checkReference(const ReferenceCase& reference) { + MultisampleTextureLayout layout(reference.tileMode, reference.elementBytes, reference.fragments, 129u, 67u, 17u); + Require(layout.BlockWidth() == reference.blockWidth && layout.BlockHeight() == reference.blockHeight, "multisampled block extent differs from AMD reference"); + Require(layout.Pitch() == reference.pitch && layout.PaddedHeight() == reference.paddedHeight, "multisampled padded extent differs from AMD reference"); + Require(layout.SliceBytes() == reference.sliceBytes && layout.Bytes() == reference.sliceBytes * 17u, "multisampled allocation differs from AMD reference"); + Require(layout.LinearBytes() == 129ull * 67u * 17u * reference.fragments * reference.elementBytes, "multisampled linear region omits fragments or layers"); + for (std::uint32_t n = 0; n < kCoordinates.size(); ++n) { + const auto& coordinate = kCoordinates[n]; + const auto address = layout.Offset(coordinate[0], coordinate[1], coordinate[2], n % reference.fragments); + Require(address == reference.addresses[n], "multisampled fragment address differs from AMD reference"); + for (auto xorBytes : {0x100u, 0x800u, 0x4000u, 0x8000u, 0xcf00u}) { + MultisampleTextureLayout swizzled(reference.tileMode, reference.elementBytes, reference.fragments, 129u, 67u, 17u, xorBytes); + const auto expected = (address & ~65535ull) | ((address & 65535ull) ^ xorBytes); + Require(swizzled.Offset(coordinate[0], coordinate[1], coordinate[2], n % reference.fragments) == expected, "multisampled pipe/bank XOR moves a fragment outside its tile"); + } + } +} + +void checkTransfer(const ReferenceCase& reference) { + MultisampleTextureLayout layout(reference.tileMode, reference.elementBytes, reference.fragments, 129u, 67u, 3u, 0x8100u); + std::vector source(layout.LinearBytes() + 32u, std::byte{0xa5}); + auto linear = std::span(source).subspan(16u, layout.LinearBytes()); + for (std::size_t n = 0; n < linear.size(); ++n) linear[n] = static_cast((n * 13u + n / 251u) & 255u); + const auto savedSource = source; + std::vector tiled(layout.Bytes() + 32u, std::byte{0xa5}); + auto guest = std::span(tiled).subspan(16u, layout.Bytes()); + layout.Tile(linear, guest); + Require(source == savedSource, "multisampled retile changed its source"); + std::vector covered(guest.size()); + std::size_t position = 0; + for (std::uint32_t layer = 0; layer < 3u; ++layer) { + for (std::uint32_t y = 0; y < 67u; ++y) { + for (std::uint32_t x = 0; x < 129u; ++x) { + for (std::uint32_t fragment = 0; fragment < reference.fragments; ++fragment) { + const auto address = layout.Offset(x, y, layer, fragment); + for (std::uint32_t channel = 0; channel < reference.elementBytes; ++channel) { + Require(address + channel < guest.size() && !covered[address + channel], "multisampled element addresses overlap or exceed the allocation"); + covered[address + channel] = true; + Require(guest[address + channel] == linear[position++], "multisampled retile changed element bytes"); + } + } + } + } + } + for (std::size_t n = 0; n < guest.size(); ++n) Require(covered[n] || guest[n] == std::byte{0xa5}, "multisampled retile changed tile padding"); + Require(std::all_of(tiled.begin(), tiled.begin() + 16u, [](auto value) { return value == std::byte{0xa5}; }) && std::all_of(tiled.end() - 16u, tiled.end(), [](auto value) { return value == std::byte{0xa5}; }), "multisampled retile changed guard bytes"); + const auto savedTiled = tiled; + std::vector restored(source.size(), std::byte{0xa5}); + layout.Detile(guest, std::span(restored).subspan(16u, layout.LinearBytes())); + Require(restored == source && tiled == savedTiled, "multisampled detile changed element, source or guard bytes"); +} + +void checkInverse(const ReferenceCase& reference) { + const auto byteBits = static_cast(std::countr_zero(reference.elementBytes)); + const auto fragmentBits = static_cast(std::countr_zero(reference.fragments)); + const auto widthBits = static_cast(std::countr_zero(reference.blockWidth)); + for (const auto xorBytes : {0u, 0x4000u, 0x8000u, 0xcf00u}) { + const MultisampleTextureLayout layout(reference.tileMode, reference.elementBytes, reference.fragments, reference.blockWidth * 4u, reference.blockHeight * 4u, 8u, xorBytes); + for (const auto origin : {std::array{0u, 0u, 0u}, std::array{reference.blockWidth * 2u, reference.blockHeight * 3u, 7u}}) { + const auto base = static_cast(layout.Offset(origin[0], origin[1], origin[2], 0u) & 65535u); + const auto step = xorBytes == 0u && origin[2] == 0u ? 1u : 257u; + for (std::uint32_t packed = 0u; packed < 65536u; packed += step) { + const auto byte = packed & (reference.elementBytes - 1u); + const auto fragment = (packed >> byteBits) & (reference.fragments - 1u); + const auto x = (packed >> (byteBits + fragmentBits)) & (reference.blockWidth - 1u); + const auto y = packed >> (byteBits + fragmentBits + widthBits); + const auto physical = static_cast((layout.Offset(origin[0] + x, origin[1] + y, origin[2], fragment) + byte) & 65535u) ^ base; + std::uint32_t restored = 0u; + for (std::uint32_t bit = 0; bit < 16u; ++bit) restored |= static_cast(std::popcount(physical & layout.InverseBlockEquation()[bit]) & 1u) << bit; + Require(restored == packed, "multisampled inverse equation changed a byte, fragment or pixel coordinate"); + } + } + } +} + +void checkRefusals() { + for (const auto& extent : {std::array{0u, 3u, 1u}, std::array{5u, 0u, 1u}, std::array{16385u, 3u, 1u}, std::array{5u, 16385u, 1u}, std::array{5u, 3u, 0u}, std::array{5u, 3u, 8193u}}) { + requireRefusal([&] { MultisampleTextureLayout invalid(TextureTileMode::kR64KBX, 8u, 8u, extent[0], extent[1], extent[2]); }); + } + for (auto elementBytes : {0u, 3u, 32u}) requireRefusal([&] { MultisampleTextureLayout invalid(TextureTileMode::kR64KBX, elementBytes, 8u, 5u, 3u, 1u); }); + for (auto fragments : {0u, 1u, 3u, 16u}) requireRefusal([&] { MultisampleTextureLayout invalid(TextureTileMode::kR64KBX, 8u, fragments, 5u, 3u, 1u); }); + for (auto tileMode : {TextureTileMode::kLinear, TextureTileMode::kStandard4KB, TextureTileMode::kStandard64KB, TextureTileMode::kS64KBX, TextureTileMode::kD64KBX}) requireRefusal([&] { MultisampleTextureLayout invalid(tileMode, 8u, 8u, 5u, 3u, 1u); }); + for (auto elementBytes : {8u, 16u}) requireRefusal([&] { MultisampleTextureLayout invalid(TextureTileMode::kZ64KBX, elementBytes, 8u, 5u, 3u, 1u); }); + for (auto xorBytes : {1u, 255u, 0x1000u, 0x2000u, 0x10000u}) requireRefusal([&] { MultisampleTextureLayout invalid(TextureTileMode::kR64KBX, 8u, 8u, 5u, 3u, 1u, xorBytes); }); + + MultisampleTextureLayout layout(TextureTileMode::kR64KBX, 8u, 8u, 5u, 3u, 2u); + for (const auto& coordinate : {std::array{5u, 0u, 0u, 0u}, std::array{0u, 3u, 0u, 0u}, std::array{0u, 0u, 2u, 0u}, std::array{0u, 0u, 0u, 8u}}) requireRefusal([&] { layout.Offset(coordinate[0], coordinate[1], coordinate[2], coordinate[3]); }); + std::vector source(layout.Bytes(), std::byte{0xa5}), destination(layout.Bytes(), std::byte{0x5a}); + const auto savedSource = source, savedDestination = destination; + requireRefusal([&] { layout.Detile(std::span(source).first(layout.Bytes() - 1u), destination); }); + requireRefusal([&] { layout.Detile(source, std::span(destination).first(layout.LinearBytes() - 1u)); }); + requireRefusal([&] { layout.Tile(std::span(source).first(layout.LinearBytes() - 1u), destination); }); + requireRefusal([&] { layout.Tile(source, std::span(destination).first(layout.Bytes() - 1u)); }); + requireRefusal([&] { layout.Detile(source, source); }); + requireRefusal([&] { layout.Tile(source, source); }); + requireRefusal([&] { layout.Detile(source, std::span(source).subspan(8u)); }); + Require(source == savedSource && destination == savedDestination, "refused multisampled transfer changed a buffer"); +} + +} + +int main() { + try { + for (const auto& reference : kReferences) { + checkReference(reference); + checkTransfer(reference); + checkInverse(reference); + } + checkRefusals(); + std::cout << "Multisampled texture layout reference, fragment, layer, padding and refusal tests passed\n"; + 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 52e16ee07a..23232ad317 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -14,6 +14,7 @@ - [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. +- [Multisampled R_X and Z_X layouts](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTextureLayout.cpp) implement single-mip 2D arrays with 2, 4 or 8 equal samples and fragments, using the non-RB+ 16-pipe GFX10 equations from [AMD Addrlib](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10SwizzlePattern.h). R_X accepts 1/2/4/8/16-byte elements and Z_X accepts 1/2/4-byte elements. Tile and detile preserve element bytes and leave padding untouched; they do not synchronize resident images or publish compression metadata. Other modes, sample counts, element sizes and pipe/bank XOR bits outside the supported configuration throw. PS5 address-layout equivalence has not been measured. - [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. From b4fbf24d1e811e0518ccd9359d2c7e1109d340f7 Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis Date: Sat, 17 Oct 2026 01:18:45 +0300 Subject: [PATCH 154/621] feat(agc): upload individual multisample image samples --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 16 ++- .../Execution/src/VulkanDevice.cpp | 3 + .../Graphics/include/Context.hpp | 1 + .../Graphics/include/TextureDetiler.hpp | 3 + .../Graphics/shaders/MultisampleUpload.comp | 32 +++++ .../Graphics/src/MultisampleUpload.cpp | 68 ++++++++++ .../Graphics/src/TextureDetiler.cpp | 1 + .../tests/execution/MultisampleReadback.cpp | 128 ++++++++++++++++-- docs/dev/debt/libSceAgcDriver.md | 2 +- 9 files changed, 244 insertions(+), 10 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/Graphics/shaders/MultisampleUpload.comp create mode 100644 core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleUpload.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 7efd108e1d..969138bea5 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -79,6 +79,18 @@ foreach(agcReadbackKind IN ITEMS Uint Depth) list(APPEND agcReadbackHeaders ${agcReadbackHeader}) endforeach() +set(agcMultisampleUploadSpv ${agcTextureDetileGeneratedDir}/MultisampleUpload.spv) +set(agcMultisampleUploadHeader ${agcTextureDetileGeneratedDir}/MultisampleUpload_spv.h) +add_custom_command( + OUTPUT "${agcMultisampleUploadHeader}" + COMMAND ${CMAKE_COMMAND} -E make_directory "${agcTextureDetileGeneratedDir}" + COMMAND "$" -V --target-env vulkan1.1 -o "${agcMultisampleUploadSpv}" "${CMAKE_CURRENT_SOURCE_DIR}/Graphics/shaders/MultisampleUpload.comp" + COMMAND ${CMAKE_COMMAND} -DINPUT=${agcMultisampleUploadSpv} -DOUTPUT=${agcMultisampleUploadHeader} + -DSYMBOL=MULTISAMPLE_UPLOAD_SPV -P "${CMAKE_CURRENT_SOURCE_DIR}/embed_spirv.cmake" + DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/Graphics/shaders/MultisampleUpload.comp" glslang-standalone + VERBATIM +) + set(agcMeshArgumentsShaderSpv ${agcTextureDetileGeneratedDir}/MeshArguments.spv) set(agcMeshArgumentsShaderHeader ${agcTextureDetileGeneratedDir}/MeshArguments_spv.h) add_custom_command( @@ -337,6 +349,7 @@ set(agcDriverSources ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/MultisampleTextureLayout.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/TextureDetiler.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/MultisampleReadback.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/MultisampleUpload.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Texture.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/PassHazards.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/SampleOrder.cpp @@ -346,6 +359,7 @@ set(agcDriverSources ${agcSampleOrderShaderHeaders} ${agcTextureDetileShaderHeader} ${agcReadbackHeaders} + ${agcMultisampleUploadHeader} ${agcCmaskClearShaderHeader} ${agcDepthSampleShaderHeaders} ${agcMeshArgumentsShaderHeader} @@ -2295,7 +2309,7 @@ if(BUILD_TESTING) 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) - foreach(agcReadbackCase IN ITEMS uint8 uint16 uint64 uint128 depth) + foreach(agcReadbackCase IN ITEMS uint8 uint16 uint64 uint128 depth upload) add_test(NAME agc_driver_multisample_readback_${agcReadbackCase} COMMAND agc_driver_multisample_readback_tests ${agcReadbackCase}) set_tests_properties(agc_driver_multisample_readback_${agcReadbackCase} PROPERTIES TIMEOUT 20 ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 286da2ec1f..4077bfcfcc 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -316,6 +316,7 @@ struct VulkanDevice::State { bool geometryShader = false; bool sampleRateShading = false; bool storageImageMultisample = false; + bool storageImageWriteWithoutFormat = false; bool shaderClipDistance = false; bool shaderCullDistance = false; bool shaderClock = false; @@ -1693,6 +1694,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : if (enabled.shaderStorageImageMultisample) state->capabilities.push_back(spv::CapabilityStorageImageMultisample); state->capabilities.push_back(spv::CapabilityImageMSArray); enabled.shaderStorageImageWriteWithoutFormat = available.shaderStorageImageWriteWithoutFormat; + state->storageImageWriteWithoutFormat = enabled.shaderStorageImageWriteWithoutFormat == VK_TRUE; enabled.shaderStorageImageReadWithoutFormat = available.shaderStorageImageReadWithoutFormat; // Gathers with non-constant offsets (ImageGatherExtended). enabled.shaderImageGatherExtended = available.shaderImageGatherExtended; @@ -3833,6 +3835,7 @@ Graphics::Context VulkanDevice::buildContext() const { context.fillModeNonSolid = state->fillModeNonSolid; context.maintenance5 = state->maintenance5; context.storageImageMultisample = state->storageImageMultisample; + context.storageImageWriteWithoutFormat = state->storageImageWriteWithoutFormat; context.shaderClipDistance = state->shaderClipDistance; context.shaderCullDistance = state->shaderCullDistance; context.shaderTerminateInvocation = state->shaderTerminateInvocation; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp index afaa005b1d..1ac783d39e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp @@ -241,6 +241,7 @@ struct Context { bool fillModeNonSolid = false; bool maintenance5 = false; bool storageImageMultisample = false; + bool storageImageWriteWithoutFormat = false; bool shaderClipDistance = false; bool shaderCullDistance = false; bool nullDescriptors = false; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp index 316ae0cb63..e0dc3531e4 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp @@ -57,9 +57,11 @@ namespace AgcDriver::Graphics { std::uint32_t CmaskErrors(); void DispatchMultisampleReadback(VkCommandBuffer commands, VkFormat format, VkBuffer destination, std::uint64_t offset, std::uint64_t bytes, VkImageView arrayView, std::uint32_t width, std::uint32_t height, std::uint32_t layers, std::uint32_t fragments, std::uint32_t firstElement, std::uint32_t elements); void DispatchDepthSample(VkCommandBuffer commands, VkImageView depth, bool multisampled, VkImageView target, std::uint32_t width, std::uint32_t height, std::uint32_t sample); + void DispatchMultisampleImage(VkCommandBuffer commands, std::uint32_t elementBytes, VkBuffer source, std::uint64_t offset, std::uint64_t bytes, VkImageView arrayView, std::uint32_t width, std::uint32_t height, std::uint32_t layers, std::uint32_t fragments, std::uint32_t firstElement, std::uint32_t elements); private: VkPipeline multisampleReadbackPipeline(std::uint32_t elementBytes, bool depth); + VkPipeline multisampleUploadPipeline(std::uint32_t elementBytes); VkPipeline pipeline(TextureTileMode tileMode, std::uint32_t elementBytes, bool retile, bool thick, bool image = false); void release() noexcept; struct DescriptorRing { @@ -100,6 +102,7 @@ namespace AgcDriver::Graphics { std::array readbackModules{}; std::vector> readbackPipelines; DescriptorRing readbackDescriptorSets; + VkShaderModule multisampleUploadModule = VK_NULL_HANDLE; }; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/shaders/MultisampleUpload.comp b/core/libs/prx/libSceAgcDriver/Graphics/shaders/MultisampleUpload.comp new file mode 100644 index 0000000000..4aa972fc04 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/shaders/MultisampleUpload.comp @@ -0,0 +1,32 @@ +#version 450 +#extension GL_EXT_shader_image_load_formatted : require + +layout(constant_id = 0) const uint ELEMENT_BYTES = 4u; +layout(local_size_x = 64) in; +layout(set = 0, binding = 0, std430) readonly buffer Input { uint data[]; } sourceBuffer; +layout(set = 0, binding = 1) uniform uimage2DMSArray destinationImage; +layout(push_constant) uniform Push { + uint width; + uint height; + uint layers; + uint fragments; + uint firstElement; + uint elements; + uint rowThreads; +} params; + +void main() { + uint index = gl_GlobalInvocationID.y * params.rowThreads + gl_GlobalInvocationID.x; + if (index >= params.elements) return; + uint element = params.firstElement + index; + uint pixel = element / params.fragments; + ivec3 coordinate = ivec3(pixel % params.width, (pixel / params.width) % params.height, pixel / (params.width * params.height)); + uint address = index * ELEMENT_BYTES; + uvec4 value = uvec4(0u); + if (ELEMENT_BYTES < 4u) { + value.x = (sourceBuffer.data[address >> 2u] >> ((address & 3u) * 8u)) & (ELEMENT_BYTES == 1u ? 255u : 65535u); + } else { + for (uint component = 0u; component < ELEMENT_BYTES / 4u; ++component) value[component] = sourceBuffer.data[(address >> 2u) + component]; + } + imageStore(destinationImage, coordinate, int(element % params.fragments), value); +} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleUpload.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleUpload.cpp new file mode 100644 index 0000000000..419ed7648e --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleUpload.cpp @@ -0,0 +1,68 @@ +#include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" +#include "prx/libSceAgcDriver/Graphics/shaders/MultisampleUpload_spv.h" +#include +#include +#include +#include + +namespace AgcDriver::Graphics { + +VkPipeline TextureDetiler::multisampleUploadPipeline(std::uint32_t elementBytes) { + const auto key = (1u << 19u) | elementBytes; + for (const auto& entry : pipelines) if (entry.first == key) return entry.second; + if (multisampleUploadModule == VK_NULL_HANDLE) { + VkShaderModuleCreateInfo info{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; + info.codeSize = sizeof(MULTISAMPLE_UPLOAD_SPV); + info.pCode = MULTISAMPLE_UPLOAD_SPV; + Check(context.Function("vkCreateShaderModule")(context.device, &info, nullptr, &multisampleUploadModule), "vkCreateShaderModule multisample upload"); + } + const VkSpecializationMapEntry entry{0u, 0u, sizeof(elementBytes)}; + const VkSpecializationInfo specialization{1u, &entry, sizeof(elementBytes), &elementBytes}; + VkComputePipelineCreateInfo info{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; + info.stage = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}; + info.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT; + info.stage.module = multisampleUploadModule; + info.stage.pName = "main"; + info.stage.pSpecializationInfo = &specialization; + info.layout = imagePipelineLayout; + pipelines.reserve(pipelines.size() + 1u); + VkPipeline result = VK_NULL_HANDLE; + { + auto pipelineCacheLock = context.LockPipelineCacheForUse(); + Check(context.Function("vkCreateComputePipelines")(context.device, context.pipelineCache, 1u, &info, nullptr, &result), "vkCreateComputePipelines multisample upload"); + } + pipelines.emplace_back(key, result); + return result; +} + +void TextureDetiler::DispatchMultisampleImage(VkCommandBuffer commands, std::uint32_t elementBytes, VkBuffer source, std::uint64_t offset, std::uint64_t bytes, VkImageView arrayView, std::uint32_t width, std::uint32_t height, std::uint32_t layers, std::uint32_t fragments, std::uint32_t firstElement, std::uint32_t elements) { + Require(context.storageImageMultisample && context.storageImageWriteWithoutFormat, "device does not enable multisampled storage image writes without format"); + Require(commands != VK_NULL_HANDLE && source != VK_NULL_HANDLE && arrayView != VK_NULL_HANDLE, "multisample upload requires commands, buffer and array view"); + Require(std::has_single_bit(elementBytes) && elementBytes <= 16u, "multisample upload element size is unsupported"); + Require(width != 0u && width <= 16384u && height != 0u && height <= 16384u && layers != 0u && layers <= 8192u, "multisample upload extent is unsupported"); + Require(fragments == 2u || fragments == 4u || fragments == 8u, "multisample upload fragment count is unsupported"); + const auto total = static_cast(width) * height * layers * fragments; + Require(total <= std::numeric_limits::max() && elements != 0u && static_cast(firstElement) + elements <= total, "multisample upload element range is invalid"); + const auto required = (static_cast(elements) * elementBytes + 3u) & ~3ull; + const auto alignment = std::max(4u, context.limits.minStorageBufferOffsetAlignment); + Require(offset % alignment == 0u && bytes >= required && bytes <= context.limits.maxStorageBufferRange && bytes % 4u == 0u && offset <= std::numeric_limits::max() - bytes, "multisample upload buffer range is invalid"); + Require(context.limits.maxComputeWorkGroupInvocations >= 64u && context.limits.maxComputeWorkGroupSize[0] >= 64u && context.limits.maxComputeWorkGroupSize[1] >= 1u && context.limits.maxComputeWorkGroupSize[2] >= 1u && context.limits.maxComputeWorkGroupCount[2] >= 1u, "device does not support the multisample upload workgroup"); + const auto groups = (static_cast(elements) + 63u) / 64u; + const auto groupsX = std::min(groups, context.limits.maxComputeWorkGroupCount[0]); + Require(groupsX != 0u, "device has no multisample upload workgroups"); + const auto groupsY = (groups + groupsX - 1u) / groupsX; + Require(groupsY <= context.limits.maxComputeWorkGroupCount[1] && groupsX * groupsY * 64u <= std::numeric_limits::max(), "multisample upload dispatch exceeds the device limits"); + const auto target = multisampleUploadPipeline(elementBytes); + const auto set = allocateSet(true); + const VkDescriptorBufferInfo buffer{source, offset, bytes}; + const VkDescriptorImageInfo image{VK_NULL_HANDLE, arrayView, VK_IMAGE_LAYOUT_GENERAL}; + const std::array writes{{{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, nullptr, set, 0u, 0u, 1u, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, nullptr, &buffer, nullptr}, {VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, nullptr, set, 1u, 0u, 1u, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, &image, nullptr, nullptr}}}; + context.Function("vkUpdateDescriptorSets")(context.device, 2u, writes.data(), 0u, nullptr); + context.Function("vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, target); + context.Function("vkCmdBindDescriptorSets")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, imagePipelineLayout, 0u, 1u, &set, 0u, nullptr); + const std::array push{width, height, layers, fragments, firstElement, elements, static_cast(groupsX * 64u)}; + context.Function("vkCmdPushConstants")(commands, imagePipelineLayout, VK_SHADER_STAGE_COMPUTE_BIT, 0u, sizeof(push), push.data()); + context.Function("vkCmdDispatch")(commands, static_cast(groupsX), static_cast(groupsY), 1u); +} + +} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetiler.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetiler.cpp index d2a6964f6b..c571bd1710 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetiler.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetiler.cpp @@ -110,6 +110,7 @@ void TextureDetiler::release() noexcept { for (const auto pool : readbackDescriptorSets.pools) context.Function("vkDestroyDescriptorPool")(context.device, pool, nullptr); for (const auto& entry : readbackPipelines) context.Function("vkDestroyPipeline")(context.device, entry.second, nullptr); for (const auto shader : readbackModules) if (shader) context.Function("vkDestroyShaderModule")(context.device, shader, nullptr); + if (multisampleUploadModule) context.Function("vkDestroyShaderModule")(context.device, multisampleUploadModule, nullptr); if (readbackPipelineLayout) context.Function("vkDestroyPipelineLayout")(context.device, readbackPipelineLayout, nullptr); if (readbackDescriptorLayout) context.Function("vkDestroyDescriptorSetLayout")(context.device, readbackDescriptorLayout, nullptr); for (const auto pool : cmaskDescriptorSets.pools) context.Function("vkDestroyDescriptorPool")(context.device, pool, nullptr); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleReadback.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleReadback.cpp index 8b6cc1b256..4e27aaf2f5 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleReadback.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleReadback.cpp @@ -1,6 +1,7 @@ #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" +#include "prx/libSceAgcDriver/Graphics/include/MultisampleTextureLayout.hpp" #include "VulkanTestDevice.hpp" #include "MsaaReadbackVertex_spv.h" #include "MsaaReadbackUint_spv.h" @@ -69,8 +70,8 @@ void CheckSupportSelection() { static_assert(!SurfaceFits(VkImageFormatProperties{}, 4u)); } -bool SurfaceSupported(const Context& context, VkFormat format, std::uint32_t samples, std::uint32_t width = 17u, std::uint32_t height = 5u, std::uint32_t layers = 2u) { - const VkImageUsageFlags usage = VK_IMAGE_USAGE_SAMPLED_BIT | (format == VK_FORMAT_D32_SFLOAT ? VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT : VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT); +bool SurfaceSupported(const Context& context, VkFormat format, std::uint32_t samples, std::uint32_t width = 17u, std::uint32_t height = 5u, std::uint32_t layers = 2u, bool writable = false) { + const VkImageUsageFlags usage = VK_IMAGE_USAGE_SAMPLED_BIT | (format == VK_FORMAT_D32_SFLOAT ? VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT : VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT) | (writable ? VK_IMAGE_USAGE_STORAGE_BIT : 0u); VkImageFormatProperties supported{}; const auto result = context.imageFormatProperties(context.physical, format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, usage, 0u, &supported); if (result != VK_ERROR_FORMAT_NOT_SUPPORTED) Check(result, "readback fixture format query"); @@ -97,11 +98,11 @@ struct Surface { std::vector views; std::vector framebuffers; - Surface(const Context& context, VkFormat format, std::uint32_t elementBytes, std::uint32_t samples, std::uint32_t width = 17u, std::uint32_t height = 5u, std::uint32_t layers = 2u) : context(context), format(format), elementBytes(elementBytes), samples(samples), width(width), height(height), layers(layers) { + Surface(const Context& context, VkFormat format, std::uint32_t elementBytes, std::uint32_t samples, std::uint32_t width = 17u, std::uint32_t height = 5u, std::uint32_t layers = 2u, bool writable = false) : context(context), format(format), elementBytes(elementBytes), samples(samples), width(width), height(height), layers(layers) { try { const bool depth = format == VK_FORMAT_D32_SFLOAT; const auto aspect = depth ? VK_IMAGE_ASPECT_DEPTH_BIT : VK_IMAGE_ASPECT_COLOR_BIT; - const VkImageUsageFlags usage = VK_IMAGE_USAGE_SAMPLED_BIT | (depth ? VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT : VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT); + const VkImageUsageFlags usage = VK_IMAGE_USAGE_SAMPLED_BIT | (depth ? VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT : VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT) | (writable ? VK_IMAGE_USAGE_STORAGE_BIT : 0u); VkImageFormatProperties supported{}; Check(context.imageFormatProperties(context.physical, format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, usage, 0u, &supported), "readback fixture format query"); Require(SurfaceFits(supported, samples, width, height, layers), "readback fixture format does not support its sample count or extent"); @@ -282,8 +283,8 @@ struct Surface { } }; -void CheckReadback(TextureDetiler& reader, const Surface& surface, std::uint32_t first, std::uint32_t elements, std::uint32_t repetitions, std::size_t extraBytes = 0u) { - const auto expected = surface.Expected(first, elements); +std::vector CheckReadback(TextureDetiler& reader, const Surface& surface, std::uint32_t first, std::uint32_t elements, std::uint32_t repetitions, std::size_t extraBytes = 0u, std::span supplied = {}) { + const auto expected = supplied.empty() ? surface.Expected(first, elements) : std::vector(supplied.begin(), supplied.end()); const auto bytes = ((expected.size() + 3u) & ~std::size_t{3u}) + extraBytes; const auto alignment = std::max(4u, surface.context.limits.minStorageBufferOffsetAlignment); const auto offset = (16u + alignment - 1u) / alignment * alignment; @@ -291,7 +292,7 @@ void CheckReadback(TextureDetiler& reader, const Surface& surface, std::uint32_t std::fill(output.Bytes().begin(), output.Bytes().end(), std::byte{0xa5}); reader.BeginBatch(); CommandBatch batch(surface.context); - RecordMemoryBarrier(surface.context, batch.Handle(), VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT); + RecordMemoryBarrier(surface.context, batch.Handle(), VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT); for (std::uint32_t repeat = 0u; repeat < repetitions; ++repeat) { reader.DispatchMultisampleReadback(batch.Handle(), surface.format, output.Handle(), offset, bytes, surface.arrayView, surface.width, surface.height, surface.layers, surface.samples, first, elements); if (repeat + 1u < repetitions) RecordMemoryBarrier(surface.context, batch.Handle(), VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT); @@ -304,6 +305,7 @@ void CheckReadback(TextureDetiler& reader, const Surface& surface, std::uint32_t if (mismatch.first != expected.end()) std::printf("readback mismatch: format=%u first=%u byte=%zu expected=0x%02x actual=0x%02x\n", static_cast(surface.format), first, static_cast(mismatch.first - expected.begin()), std::to_integer(*mismatch.first), std::to_integer(*mismatch.second)); Require(std::equal(expected.begin(), expected.end(), data.begin() + offset), "multisample readback changed a rendered sample, component, layer or pixel"); for (std::size_t index = 0u; index < data.size(); ++index) Require((index >= offset && index < offset + expected.size()) || data[index] == std::byte{0xa5}, "multisample readback changed a buffer sentinel or packed tail"); + return {data.begin() + offset, data.begin() + offset + expected.size()}; } template @@ -331,6 +333,106 @@ void CheckRefusals(TextureDetiler& reader, const Surface& surface) { std::puts("multisample readback invalid-input refusals passed"); } +std::vector Upload(TextureDetiler& transfer, TextureDetiler& reader, const Surface& surface, std::vector& expected, std::uint32_t first, std::uint32_t elements, std::uint32_t repetitions = 1u, std::span supplied = {}) { + const auto bytes = static_cast(elements) * surface.elementBytes; + const auto padded = (bytes + 3u) & ~std::size_t{3}; + const auto alignment = std::max({4u, surface.elementBytes, surface.context.limits.minStorageBufferOffsetAlignment}); + const auto offset = (16u + alignment - 1u) / alignment * alignment; + Buffer source(surface.context, offset + padded + 16u, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + std::fill(source.Bytes().begin(), source.Bytes().end(), std::byte{0x5a}); + Require(supplied.empty() || supplied.size() == bytes, "multisample upload fixture source size mismatch"); + for (std::size_t index = 0; index < bytes; ++index) source.Bytes()[offset + index] = supplied.empty() ? static_cast((index * 29u + first * 17u + elements * 7u) & 255u) : supplied[index]; + const std::vector retained(source.Bytes().begin(), source.Bytes().end()); + std::copy_n(retained.begin() + offset, bytes, expected.begin() + static_cast(first) * surface.elementBytes); + const auto stride = 256u / alignment * alignment; + Require(stride != 0u, "upload fixture cannot hold an aligned chunk"); + transfer.BeginBatch(); + CommandBatch batch(surface.context); + RecordMemoryBarrier(surface.context, batch.Handle(), VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT); + for (std::uint32_t repeat = 0; repeat < repetitions; ++repeat) { + for (std::size_t done = 0; done < bytes; done += stride) { + const auto count = std::min(stride, bytes - done); + transfer.DispatchMultisampleImage(batch.Handle(), surface.elementBytes, source.Handle(), offset + done, (count + 3u) & ~std::size_t{3}, surface.arrayView, surface.width, surface.height, surface.layers, surface.samples, first + static_cast(done / surface.elementBytes), static_cast(count / surface.elementBytes)); + } + RecordMemoryBarrier(surface.context, batch.Handle(), VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT); + } + batch.SubmitAndWait(); + source.Invalidate(); + Require(std::equal(retained.begin(), retained.end(), source.Bytes().begin()), "multisample upload changed source bytes or sentinels"); + return CheckReadback(reader, surface, 0u, surface.width * surface.height * surface.layers * surface.samples, 1u, 0u, expected); +} + +void CheckUploadRefusals(TextureDetiler& transfer, const Surface& surface) { + Buffer source(surface.context, 256u, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + CommandBatch batch(surface.context); + const auto call = [&](std::uint32_t bytes, std::uint32_t fragments, std::uint32_t first, std::uint32_t elements, std::uint64_t offset = 0u, std::uint64_t range = 4u) { + transfer.DispatchMultisampleImage(batch.Handle(), bytes, source.Handle(), offset, range, surface.arrayView, 1u, 1u, 1u, fragments, first, elements); + }; + for (const auto bytes : {0u, 3u, 32u}) Refused([&] { call(bytes, 4u, 0u, 1u); }, "element size is unsupported"); + for (const auto fragments : {0u, 1u, 3u, 16u}) Refused([&] { call(4u, fragments, 0u, 1u); }, "fragment count is unsupported"); + for (const auto range : {std::array{0u, 0u}, std::array{4u, 1u}, std::array{3u, 2u}, std::array{0xffffffffu, 2u}}) Refused([&] { call(4u, 4u, range[0], range[1]); }, "element range is invalid"); + for (const auto range : {std::array{1u, 4u}, std::array{0u, 0u}, std::array{0u, 3u}, std::array{0u, 260u}, std::array{~std::uint64_t{0} - 3u, 8u}}) Refused([&] { call(4u, 4u, 0u, 1u, range[0], range[1]); }, "buffer range is invalid"); + Refused([&] { transfer.DispatchMultisampleImage(VK_NULL_HANDLE, 4u, source.Handle(), 0u, 4u, surface.arrayView, 1u, 1u, 1u, 4u, 0u, 1u); }, "requires commands, buffer and array view"); + Refused([&] { transfer.DispatchMultisampleImage(batch.Handle(), 4u, VK_NULL_HANDLE, 0u, 4u, surface.arrayView, 1u, 1u, 1u, 4u, 0u, 1u); }, "requires commands, buffer and array view"); + Refused([&] { transfer.DispatchMultisampleImage(batch.Handle(), 4u, source.Handle(), 0u, 4u, VK_NULL_HANDLE, 1u, 1u, 1u, 4u, 0u, 1u); }, "requires commands, buffer and array view"); + for (const auto extent : {std::array{0u, 1u, 1u}, std::array{1u, 0u, 1u}, std::array{1u, 1u, 0u}, std::array{16385u, 1u, 1u}, std::array{1u, 16385u, 1u}, std::array{1u, 1u, 8193u}}) Refused([&] { transfer.DispatchMultisampleImage(batch.Handle(), 4u, source.Handle(), 0u, 4u, surface.arrayView, extent[0], extent[1], extent[2], 4u, 0u, 1u); }, "extent is unsupported"); + Refused([&] { transfer.DispatchMultisampleImage(batch.Handle(), 4u, source.Handle(), 0u, 4u, surface.arrayView, 16384u, 16384u, 8192u, 8u, 0u, 1u); }, "element range is invalid"); + for (const bool multisample : {false, true}) { + auto unsupported = surface.context; + if (multisample) unsupported.storageImageMultisample = false; + else unsupported.storageImageWriteWithoutFormat = false; + TextureDetiler unavailable(unsupported); + Refused([&] { unavailable.DispatchMultisampleImage(batch.Handle(), 4u, source.Handle(), 0u, 4u, surface.arrayView, 1u, 1u, 1u, 4u, 0u, 1u); }, "does not enable multisampled storage image writes without format"); + } +} + +void CheckUpload(const Context& context) { + Require(context.storageImageMultisample && context.storageImageWriteWithoutFormat, "upload fixture requires enabled multisampled formatless storage writes"); + auto limited = context; + limited.limits.maxComputeWorkGroupCount[0] = 1u; + limited.limits.maxStorageBufferRange = 256u; + TextureDetiler transfer(limited); + TextureDetiler reader(context); + std::uint32_t exercised = 0u; + for (const auto bytes : {1u, 2u, 4u, 8u, 16u}) { + const auto format = TextureDetiler::ImageElementFormat(bytes); + for (const auto samples : {2u, 4u, 8u}) { + if (!SurfaceSupported(context, format, samples, 17u, 5u, 2u, true)) continue; + Surface surface(context, format, bytes, samples, 17u, 5u, 2u, true); + surface.Render(); + const auto elements = surface.width * surface.height * surface.layers * samples; + auto expected = surface.Expected(0u, elements); + Upload(transfer, reader, surface, expected, 3u, 1u); + Upload(transfer, reader, surface, expected, 1u, elements - 3u); + Upload(transfer, reader, surface, expected, 0u, elements); + Upload(transfer, reader, surface, expected, elements - 1u, 1u, samples == 4u ? 65u : 1u); + if (bytes == 4u && samples == 4u) CheckUploadRefusals(transfer, surface); + std::printf("multisample upload: bytes=%u samples=%u layers=2 full/partial/packed/source-sentinels/chunks/reuse=passed\n", bytes, samples); + ++exercised; + } + } + Require(exercised != 0u, "no supported multisample upload subcase was exercised"); + const MultisampleTextureLayout active(TextureTileMode::kR64KBX, 8u, 4u, 17u, 5u, 2u); + if (SurfaceSupported(context, VK_FORMAT_R32G32_UINT, 4u, active.Pitch(), active.PaddedHeight(), 2u, true)) { + for (const auto xorValue : {0u, 0x8100u}) { + const MultisampleTextureLayout layout(TextureTileMode::kR64KBX, 8u, 4u, active.Pitch(), active.PaddedHeight(), 2u, xorValue); + std::vector tiled(static_cast(layout.Bytes())); + for (std::size_t byte = 0; byte < tiled.size(); ++byte) tiled[byte] = static_cast((byte * 37u + (byte >> 8u) * 13u + (byte >> 15u) * 71u + (byte >> 16u) * 101u) & 255u); + std::vector linear(static_cast(layout.LinearBytes())); + layout.Detile(tiled, linear); + Surface surface(context, VK_FORMAT_R32G32_UINT, 8u, 4u, layout.Pitch(), layout.PaddedHeight(), 2u, true); + surface.Render(); + const auto elements = surface.width * surface.height * surface.layers * surface.samples; + auto expected = surface.Expected(0u, elements); + const auto actual = Upload(transfer, reader, surface, expected, 0u, elements, 1u, linear); + std::vector retiled(tiled.size(), std::byte{0xa5}); + layout.Tile(actual, retiled); + Require(retiled == tiled, "multisample padded initialization changed color bytes, fragments, layers, XOR or padding"); + std::printf("multisample padded upload: active=17x5 backing=%ux%u layers=2 fragments=4 bytes=%zu xor=0x%x exact roundtrip passed\n", surface.width, surface.height, retiled.size(), xorValue); + } + } +} + } int main(int argc, char** argv) { @@ -343,7 +445,7 @@ int main(int argc, char** argv) { else if (mode == "uint16") { format = VK_FORMAT_R16_UINT; bytes = 2u; } else if (mode == "uint64") { format = VK_FORMAT_R32G32_UINT; bytes = 8u; } else if (mode == "uint128") { format = VK_FORMAT_R32G32B32A32_UINT; bytes = 16u; } - else Require(mode == "uint32" || mode == "depth", "unknown multisample readback test mode"); + else Require(mode == "uint32" || mode == "depth" || mode == "upload", "unknown multisample readback test mode"); if (mode == "depth") format = VK_FORMAT_D32_SFLOAT; const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; @@ -357,6 +459,16 @@ int main(int argc, char** argv) { std::puts("skipped, the device does not support per-sample shading"); return VulkanTestSkipped; } + if (mode == "upload") { + if (!context.storageImageMultisample || !context.storageImageWriteWithoutFormat) { + std::puts("skipped, the device does not enable multisampled formatless storage writes"); + return VulkanTestSkipped; + } + CheckUpload(context); + Require(checkedPipelineCacheUses != 0u, "upload fixture did not check pipeline cache use"); + std::printf("multisample upload guarded pipeline creation checks=%u passed\n", checkedPipelineCacheUses); + return 0; + } auto limited = context; limited.limits.maxComputeWorkGroupCount[0] = 1u; TextureDetiler reader(limited); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 23232ad317..06024e03e5 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -218,7 +218,7 @@ - [Unclamped normalized color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - CB_COLOR_INFO targets with BLEND_CLAMP clear (bit 15) on UNORM and SNORM formats are accepted when blending is disabled (ENABLE clear or BLEND_BYPASS set). Vulkan converts unblended shader values and clamps to the normalized range automatically; blending into an unclamped normalized target remains unmodeled because Vulkan always clamps inputs prior to blending. - [Multisample image readback](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleReadback.cpp) fetches unsigned R8/R16/R32/R32G32/R32G32B32A32 or D32 depth samples through a 2D array view in GENERAL, in layer/y/x/fragment order. The caller owns producer completion, image transitions and resource lifetimes. It implements Vulkan per-sample fetches, not guest-memory publication, metadata expansion or PS5 layout equivalence; D32 exceptional-value preservation has not been measured. Other formats, sample counts and invalid ranges throw. - +- [Multisample image upload](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleUpload.cpp) does not model guest layouts, native ownership or CMASK/FMASK/DCC publication; console equivalence and large-title initialization costs remain unmeasured. It stores 1/2/4/8/16-byte elements from a storage buffer into an unsigned 2D multisample array view in layer/y/x/fragment order. It requires enabled multisample storage images and formatless image writes, 2/4/8 fragments and valid aligned buffer and dispatch ranges. The caller supplies the matching storage view in GENERAL and owns barriers, completion and resource lifetimes. - [Thick SW_4KB_S_X volumes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp) use AMD addrlib's GFX10 non-RB+ 16-pipe `GFX10_SW_4K_S3_X_PATINFO` equations for 1, 2, 4, 8 and 16-byte elements, including packed mip tails. The layout follows [AMD PAL c5e80007](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp); it has not been measured on console hardware. Other thick swizzle modes outside the supported families still throw. Mipmapped 3D color targets remain refused by the color-register decoder. - [Viewport depth range remap](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - on a device without `VK_EXT_depth_range_unrestricted` a viewport depth range outside [0, 1] (Minecraft PPSA17221 draws its world with `PA_CL_VPORT_ZSCALE` 1, `ZOFFSET` 0 in negative-one-to-one clip space) is folded into the vertex shader: `RemapPositionDepth` scales clip z so z/w is the console's depth, the pipeline's depth clamp applies the console's `PA_SC_VPORT_ZMIN/ZMAX` clipped to [0, 1] (exact for a normalized depth target or none; Minecraft clamps its float depth target to [-1, 1], so a depth below 0 is stored as 0 there, which only a later test or read at exactly 0 can tell, and the first such draw is reported once), and two extra `gl_ClipDistance` elements restore the near and far planes depth clamping removes. The mode is a pipeline specialization constant (`PipelineSpecialization::DepthRemapBase`); scale and offset are read from the draw's push block (two words at byte 104, `DepthRemapPushOffsetBytes`, so a remapped draw's stage push data ends before it), so draws that differ only in them share one vertex module and one pipeline. The remap serves the vertex path only; mesh and tessellation draws with such a range still throw. Whether the console clamps before or after its depth bias and how it treats a clamp range narrower than the transform's are not measured. - [Benchmark rewrite failure test off Windows](../../../core/libs/prx/libSceAgcDriver/tests/PerformanceTrace.cpp) (libSceAgcDriver) - `ReplaceGzipFile` checks that a write failing at `.tmp` keeps the previous `aps5-bench.json.gz` only on non-Windows builds: the `std::ios_base::failure` thrown inside the MinGW ofstream did not unwind to the test's catch, so Windows CI covers only the failed move. From ab1e6a7a8987379da2ff4de0b0c42d9328a699be Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis Date: Fri, 16 Oct 2026 19:13:19 +0300 Subject: [PATCH 155/621] test(agc): connect multisample readback to R_X and Z_X retile --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 2 +- .../tests/execution/MultisampleReadback.cpp | 63 ++++++++++++++++++- 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 969138bea5..d4a7c32860 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2309,7 +2309,7 @@ if(BUILD_TESTING) 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) - foreach(agcReadbackCase IN ITEMS uint8 uint16 uint64 uint128 depth upload) + foreach(agcReadbackCase IN ITEMS uint8 uint16 uint64 uint128 depth upload retile) add_test(NAME agc_driver_multisample_readback_${agcReadbackCase} COMMAND agc_driver_multisample_readback_tests ${agcReadbackCase}) set_tests_properties(agc_driver_multisample_readback_${agcReadbackCase} PROPERTIES TIMEOUT 20 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/MultisampleReadback.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleReadback.cpp index 4e27aaf2f5..62a82c3421 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleReadback.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleReadback.cpp @@ -308,6 +308,60 @@ std::vector CheckReadback(TextureDetiler& reader, const Surface& surf return {data.begin() + offset, data.begin() + offset + expected.size()}; } +void CheckRetile(TextureDetiler& reader, const Surface& surface) { + const auto elements = surface.width * surface.height * surface.layers * surface.samples; + const auto linear = CheckReadback(reader, surface, 0u, elements, 1u); + const auto expected = surface.Expected(0u, elements); + constexpr std::array, 7> coordinates{{ + {0u, 0u, 0u}, {128u, 66u, 16u}, {64u, 32u, 1u}, {32u, 64u, 2u}, + {63u, 31u, 7u}, {127u, 65u, 15u}, {1u, 1u, 3u} + }}; + constexpr std::array modes{TextureTileMode::kR64KBX, TextureTileMode::kZ64KBX}; + constexpr std::array, 2> references{{ + {0ull, 6621216ull, 493568ull, 1016832ull, 2766588ull, 6170780ull, 1215508ull}, + {0ull, 6637700ull, 460808ull, 1000460ull, 2815728ull, 6219892ull, 1182776ull} + }}; + for (std::size_t mode = 0u; mode < modes.size(); ++mode) { + for (const auto pipeBankXor : {0u, 0x8100u}) { + const MultisampleTextureLayout layout(modes[mode], surface.elementBytes, surface.samples, surface.width, surface.height, surface.layers, pipeBankXor); + std::vector tiled(static_cast(layout.Bytes()) + 32u, std::byte{0xa5}); + auto payload = std::span(tiled).subspan(16u, layout.Bytes()); + layout.Tile(linear, payload); + std::vector covered(payload.size()); + std::size_t position = 0u; + for (std::uint32_t layer = 0u; layer < surface.layers; ++layer) { + for (std::uint32_t y = 0u; y < surface.height; ++y) { + for (std::uint32_t x = 0u; x < surface.width; ++x) { + for (std::uint32_t fragment = 0u; fragment < surface.samples; ++fragment) { + const auto address = layout.Offset(x, y, layer, fragment); + Require(address + surface.elementBytes <= payload.size(), "readback retile exceeds its allocation"); + for (std::uint32_t byte = 0u; byte < surface.elementBytes; ++byte) { + Require(!covered[address + byte], "readback retile elements overlap"); + covered[address + byte] = true; + Require(payload[address + byte] == expected[position++], "readback and retile disagree on layer, pixel or fragment order"); + } + } + } + } + } + for (std::size_t index = 0u; index < covered.size(); ++index) Require(covered[index] || payload[index] == std::byte{0xa5}, "readback retile changed tile padding"); + for (std::size_t index = 0u; index < coordinates.size(); ++index) { + const auto& coordinate = coordinates[index]; + const auto fragment = static_cast(index) % surface.samples; + const auto source = (((coordinate[2] * surface.height + coordinate[1]) * surface.width + coordinate[0]) * surface.samples + fragment) * surface.elementBytes; + const auto reference = references[mode][index]; + const auto address = (reference & ~65535ull) | ((reference & 65535ull) ^ pipeBankXor); + Require(address + surface.elementBytes <= payload.size() && std::equal(expected.begin() + source, expected.begin() + source + surface.elementBytes, payload.begin() + address), "rendered readback differs at an AMD reference fragment address"); + } + std::vector restored(expected.size()); + layout.Detile(payload, restored); + Require(restored == expected && linear == expected, "readback retile roundtrip changed rendered element bits"); + for (std::size_t index = 0u; index < tiled.size(); ++index) Require((index >= 16u && index < 16u + payload.size()) || tiled[index] == std::byte{0xa5}, "readback retile changed an allocation sentinel"); + std::printf("multisample readback retile: mode=%s xor=0x%x samples=%u layers=%u elements=%u reference-points=7 padding/sentinels/roundtrip=passed\n", mode == 0u ? "R_X" : "Z_X", pipeBankXor, surface.samples, surface.layers, elements); + } + } +} + template void Refused(TAction action, const char* expected) { std::string reason; @@ -445,7 +499,7 @@ int main(int argc, char** argv) { else if (mode == "uint16") { format = VK_FORMAT_R16_UINT; bytes = 2u; } else if (mode == "uint64") { format = VK_FORMAT_R32G32_UINT; bytes = 8u; } else if (mode == "uint128") { format = VK_FORMAT_R32G32B32A32_UINT; bytes = 16u; } - else Require(mode == "uint32" || mode == "depth" || mode == "upload", "unknown multisample readback test mode"); + else Require(mode == "uint32" || mode == "depth" || mode == "upload" || mode == "retile", "unknown multisample readback test mode"); if (mode == "depth") format = VK_FORMAT_D32_SFLOAT; const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; @@ -472,6 +526,13 @@ int main(int argc, char** argv) { auto limited = context; limited.limits.maxComputeWorkGroupCount[0] = 1u; TextureDetiler reader(limited); + if (mode == "retile") { + if (!SurfaceSupported(context, format, 4u, 129u, 67u, 17u)) return VulkanTestSkipped; + Surface surface(context, format, bytes, 4u, 129u, 67u, 17u); + surface.Render(); + CheckRetile(reader, surface); + return 0; + } std::uint32_t checked = 0u; for (const auto samples : {2u, 4u, 8u}) { if (!SurfaceSupported(context, format, samples)) continue; From b65a553db6d8bbfeb158ecacc81305a5a16f2cb8 Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis Date: Fri, 16 Oct 2026 20:34:33 +0300 Subject: [PATCH 156/621] test(agc): retile distinct D32 sample readback --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 2 +- .../tests/execution/MultisampleReadback.cpp | 12 +++++++----- .../tests/shaders/MultisampleReadback.frag | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index d4a7c32860..8c80040c35 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2309,7 +2309,7 @@ if(BUILD_TESTING) 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) - foreach(agcReadbackCase IN ITEMS uint8 uint16 uint64 uint128 depth upload retile) + foreach(agcReadbackCase IN ITEMS uint8 uint16 uint64 uint128 depth upload retile depth_retile) add_test(NAME agc_driver_multisample_readback_${agcReadbackCase} COMMAND agc_driver_multisample_readback_tests ${agcReadbackCase}) set_tests_properties(agc_driver_multisample_readback_${agcReadbackCase} PROPERTIES TIMEOUT 20 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/MultisampleReadback.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleReadback.cpp index 62a82c3421..a6c84ad27f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleReadback.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleReadback.cpp @@ -276,7 +276,7 @@ struct Surface { const auto layer = pixel / (width * height); const auto value = x + y * width + layer * width * height + sample * 31u; std::array words{0x91352700u ^ value, 0x45219b00u ^ (value * 3u), 0xc7391d00u ^ (value * 5u), 0x6b43e100u ^ (value * 7u)}; - if (format == VK_FORMAT_D32_SFLOAT) words[0] = std::bit_cast(0.125f + static_cast(element + 1u) / 65536.0f); + if (format == VK_FORMAT_D32_SFLOAT) words[0] = std::bit_cast(0.125f + static_cast(element + 1u) / 1048576.0f); std::memcpy(result.data() + static_cast(index) * elementBytes, words.data(), elementBytes); } return result; @@ -357,7 +357,7 @@ void CheckRetile(TextureDetiler& reader, const Surface& surface) { layout.Detile(payload, restored); Require(restored == expected && linear == expected, "readback retile roundtrip changed rendered element bits"); for (std::size_t index = 0u; index < tiled.size(); ++index) Require((index >= 16u && index < 16u + payload.size()) || tiled[index] == std::byte{0xa5}, "readback retile changed an allocation sentinel"); - std::printf("multisample readback retile: mode=%s xor=0x%x samples=%u layers=%u elements=%u reference-points=7 padding/sentinels/roundtrip=passed\n", mode == 0u ? "R_X" : "Z_X", pipeBankXor, surface.samples, surface.layers, elements); + std::printf("multisample readback retile: format=%u mode=%s xor=0x%x samples=%u layers=%u elements=%u reference-points=7 padding/sentinels/roundtrip=passed\n", static_cast(surface.format), mode == 0u ? "R_X" : "Z_X", pipeBankXor, surface.samples, surface.layers, elements); } } } @@ -499,8 +499,8 @@ int main(int argc, char** argv) { else if (mode == "uint16") { format = VK_FORMAT_R16_UINT; bytes = 2u; } else if (mode == "uint64") { format = VK_FORMAT_R32G32_UINT; bytes = 8u; } else if (mode == "uint128") { format = VK_FORMAT_R32G32B32A32_UINT; bytes = 16u; } - else Require(mode == "uint32" || mode == "depth" || mode == "upload" || mode == "retile", "unknown multisample readback test mode"); - if (mode == "depth") format = VK_FORMAT_D32_SFLOAT; + else Require(mode == "uint32" || mode == "depth" || mode == "upload" || mode == "retile" || mode == "depth_retile", "unknown multisample readback test mode"); + if (mode == "depth" || mode == "depth_retile") format = VK_FORMAT_D32_SFLOAT; const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); @@ -526,11 +526,13 @@ int main(int argc, char** argv) { auto limited = context; limited.limits.maxComputeWorkGroupCount[0] = 1u; TextureDetiler reader(limited); - if (mode == "retile") { + if (mode == "retile" || mode == "depth_retile") { if (!SurfaceSupported(context, format, 4u, 129u, 67u, 17u)) return VulkanTestSkipped; Surface surface(context, format, bytes, 4u, 129u, 67u, 17u); surface.Render(); CheckRetile(reader, surface); + Require(checkedPipelineCacheUses != 0u, "retile fixture did not check pipeline cache use"); + std::printf("multisample retile guarded pipeline creation checks=%u passed\n", checkedPipelineCacheUses); return 0; } std::uint32_t checked = 0u; diff --git a/core/libs/prx/libSceAgcDriver/tests/shaders/MultisampleReadback.frag b/core/libs/prx/libSceAgcDriver/tests/shaders/MultisampleReadback.frag index 3a72da6058..2f0e104c90 100644 --- a/core/libs/prx/libSceAgcDriver/tests/shaders/MultisampleReadback.frag +++ b/core/libs/prx/libSceAgcDriver/tests/shaders/MultisampleReadback.frag @@ -10,7 +10,7 @@ void main() { uint y = uint(gl_FragCoord.y); #if READ_DEPTH uint element = (x + y * params.width + params.layer * params.width * params.height) * params.samples + uint(gl_SampleID); - gl_FragDepth = 0.125 + float(element + 1u) / 65536.0; + gl_FragDepth = 0.125 + float(element + 1u) / 1048576.0; #else uint value = x + y * params.width + params.layer * params.width * params.height + uint(gl_SampleID) * 31u; color = uvec4(0x91352700u ^ value, 0x45219b00u ^ (value * 3u), 0xc7391d00u ^ (value * 5u), 0x6b43e100u ^ (value * 7u)); 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 157/621] 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 0bd8f6188a39e65c05b709d9bf59ba5e613de789 Mon Sep 17 00:00:00 2001 From: thuanquang Date: Mon, 19 Oct 2026 15:30:53 +0700 Subject: [PATCH 158/621] fix(agc): draw triangle strips through a geometry program with the last provoking vertex An RX 6700 XT hands a geometry shader odd strip triangle i as (i+1, i, i+2) with the last provoking vertex, which is the order the translator already uses, and since #7374 last mode takes the flat outputs of the program's primitives from their last index. Keep the last-mode refusal for fans only, which reach the program as (i+1, i+2, 0) while the hardware gives (0, i+1, i+2). TEKKEN DARK RESURRECTION (PPSA31371) stops on such a strip draw. Co-Authored-By: Claude --- .../libSceAgcDriver/Graphics/src/Pipeline.cpp | 2 +- .../prx/libSceAgcDriver/tests/Graphics.cpp | 10 ++++-- .../tests/execution/MeshProvokingVertex.cpp | 36 +++++++++++++++---- docs/dev/debt/libSceAgcDriver.md | 2 +- 4 files changed, 39 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index 20ce2e4e8b..faae8726e5 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -122,7 +122,7 @@ void ValidateProvokingVertex(const Context& context, const State& state, std::sp 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"); - Require(!meshPrimitives || (state.topology != VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP && state.topology != VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN), "last provoking vertex is unsupported for triangle strips or fans assembled for a geometry program"); + Require(!meshPrimitives || state.topology != VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN, "last provoking vertex is unsupported for triangle fans assembled for a geometry program"); Require(state.rectList || state.topology == VK_PRIMITIVE_TOPOLOGY_POINT_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_LINE_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_LINE_STRIP || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN, "last provoking vertex is unsupported for this primitive topology"); for (const auto& shader : shaders) { if (shader.stage != ShaderRecompiler::ShaderStage::Fragment) continue; diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 12b11af2df..a7feeeac37 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -9161,10 +9161,16 @@ void ProvokingVertexTests() { meshDraw.topology = topology; for (const auto& drawn : {std::span{}, std::span(flatShaders), std::span(flatP10Shaders), std::span(smoothShaders), std::span(slotShaders)}) ValidateProvokingVertex(supported, meshDraw, drawn); } + meshDraw.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP; + for (const auto& drawn : {std::span{}, std::span(flatShaders), std::span(flatP10Shaders), std::span(smoothShaders)}) ValidateProvokingVertex(supported, meshDraw, drawn); + fragment.fragmentParameters = {{0, 0, false, true, false, false, false}, {1, 1, false, true, false, false, false}, {2, 2, false, true, false, false, false}, {3, 3, false, true, false, false, false}}; + ValidateProvokingVertex(supported, meshDraw, shaders); + expectFailure([&] { ValidateProvokingVertex(supported, meshDraw, slotShaders); }, "last provoking vertex with explicit per-vertex triangle-strip interpolation is unsupported"); + meshDraw.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN; + expectFailure([&] { ValidateProvokingVertex(supported, meshDraw, flatShaders); }, "last provoking vertex is unsupported for triangle fans assembled for a geometry program"); for (const auto topology : {VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN}) { - meshDraw.topology = topology; - expectFailure([&] { ValidateProvokingVertex(supported, meshDraw, flatShaders); }, "last provoking vertex is unsupported for triangle strips or fans assembled for a geometry program"); auto firstDraw = meshDraw; + firstDraw.topology = topology; firstDraw.provokingVertexMode = VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT; ValidateProvokingVertex(supported, firstDraw, flatShaders); } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/MeshProvokingVertex.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/MeshProvokingVertex.cpp index 3c7b2a9333..1cb6bea34a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/MeshProvokingVertex.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/MeshProvokingVertex.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -23,9 +24,9 @@ constexpr std::uint32_t Width = 64; constexpr std::uint32_t Height = 64; using Image = std::array; alignas(256) Image Pixels{}; -alignas(256) constexpr std::array Indices{0, 1, 2, 0}; -alignas(256) constexpr std::array, 3> Vertices{{ - {-0.9f, -0.8f, 0, 1, 1, 0, 0, 1}, {-0.1f, -0.8f, 0, 1, 0, 1, 0, 1}, {-0.5f, 0.8f, 0, 1, 0, 0, 1, 1} +alignas(256) constexpr std::array Indices{0, 1, 2, 3}; +alignas(256) constexpr std::array, 4> Vertices{{ + {-0.9f, -0.8f, 0, 1, 1, 0, 0, 1}, {-0.1f, -0.8f, 0, 1, 0, 1, 0, 1}, {-0.5f, 0.8f, 0, 1, 0, 0, 1, 1}, {0.3f, 0.8f, 0, 1, 1, 1, 0, 1} }}; alignas(256) constexpr std::array FlatPixelCode{0xc8120002u, 0xc8160102u, 0xc81a0202u, 0xc81e0302u, 0xf800180fu, 0x07060504u, 0xbf810000u}; alignas(256) constexpr std::array SmoothPixelCode{0xc8100000u, 0xc8110001u, 0xc8140100u, 0xc8150101u, 0xc8180200u, 0xc8190201u, 0xc81c0300u, 0xc81d0301u, 0xf800180fu, 0x07060504u, 0xbf810000u}; @@ -64,7 +65,7 @@ void Write(std::vector& commands, std::uint32_t opcode, std::uint commands.insert(commands.end(), {0xc0010000u | (opcode << 8u), offset, value}); } -Image Draw(const std::uint32_t* pixelCode, bool flat, bool last) { +Image Draw(const std::uint32_t* pixelCode, bool flat, bool last, bool strip = false) { Pixels.fill(std::byte{0}); const auto color = reinterpret_cast(Pixels.data()); const auto geometry = reinterpret_cast(GeometryCode.data()); @@ -98,13 +99,14 @@ Image Draw(const std::uint32_t* pixelCode, bool flat, bool last) { {0x8, static_cast(pixel >> 8u)}, {0x9, static_cast(pixel >> 40u)}, {0xb, 0} }; std::vector commands; - Write(commands, 0x79, 0x242, 4); + Write(commands, 0x79, 0x242, strip ? 6u : 4u); Write(commands, 0x79, 0x25b, (12u << 9u) | 4u); for (const auto& [offset, value] : context) Write(commands, 0x69, offset, value); for (const auto& [offset, value] : shaders) Write(commands, 0x76, offset, value); commands.insert(commands.end(), {0xc0002a00u, 0u}); const auto address = reinterpret_cast(Indices.data()); - commands.insert(commands.end(), {0xc0042700u, 3u, static_cast(address), static_cast(address >> 32u), 3u, 0u}); + const std::uint32_t count = strip ? 4u : 3u; + commands.insert(commands.end(), {0xc0042700u, count, static_cast(address), static_cast(address >> 32u), count, 0u}); Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; Require(sceAgcDriverSubmitDcb(&packet) == 0, "mesh submission failed"); AgcDriverWaitIdle_nid_postfix(); @@ -132,6 +134,25 @@ void FlatInputs() { } } +constexpr std::array, 4> SecondInside{{{26, 48}, {32, 48}, {29, 44}, {29, 52}}}; + +void CheckFlat(const Image& image, std::span> pixels, const std::array& expected, const std::string& what) { + for (const auto& [x, y] : pixels) { + for (std::uint32_t channel = 0; channel < 4; ++channel) { + Require(Channel(image, x, y, channel) == expected[channel], what + ": pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") channel " + std::to_string(channel) + " is " + std::to_string(Channel(image, x, y, channel)) + ", expected " + std::to_string(expected[channel])); + } + } +} + +void StripFlatInputs() { + const auto last = Draw(FlatPixelCode.data(), true, true, true); + CheckFlat(last, Inside, {0u, 0u, 255u, 255u}, "triangle strip, last provoking vertex, triangle 0 (the color of vertex 2)"); + CheckFlat(last, SecondInside, {255u, 255u, 0u, 255u}, "triangle strip, last provoking vertex, triangle 1 (the color of vertex 3)"); + Require(Channel(last, 48, 48, 3) == 0, "triangle strip, last provoking vertex: the draw wrote outside its triangles"); + const auto first = Draw(FlatPixelCode.data(), true, false, true); + CheckFlat(first, Inside, {255u, 0u, 0u, 255u}, "triangle strip, first provoking vertex, triangle 0 (the color of vertex 0)"); +} + void SmoothInputs() { const auto first = Draw(SmoothPixelCode.data(), false, false); for (const auto& [x, y] : Inside) { @@ -169,9 +190,10 @@ int main() { Register(FlatPixelHeader, FlatPixelCode.data(), sizeof(FlatPixelCode), 1); Register(SmoothPixelHeader, SmoothPixelCode.data(), sizeof(SmoothPixelCode), 1); FlatInputs(); + StripFlatInputs(); if (smooth) SmoothInputs(); AgcDriverShutdown_nid_postfix(); - std::cout << "geometry-program triangles take flat inputs from vertex 0 with the first provoking vertex and from vertex 2 with the last" << (smooth ? ", and interpolate smooth inputs the same in both modes\n" : "; smooth inputs skipped without fragment shader barycentrics\n"); + std::cout << "geometry-program triangles take flat inputs from vertex 0 with the first provoking vertex and from vertex 2 with the last, and the second triangle of a strip takes them from vertex 3 with the last" << (smooth ? ", and interpolate smooth inputs the same in both modes\n" : "; smooth inputs skipped without fragment shader barycentrics\n"); return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index c4ea080793..1dd66f3d53 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -134,7 +134,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) 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`, except for the [generated barycentric tessellation stages](../../../core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp), which write the last vertex's flat values at every corner, 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. +- [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). - [Dual-source blending](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) takes the MRT1 export as the second source of the MRT0 blend, as RADV and radeonsi program it; what the CB does with other export or target layouts is unknown, so they throw. - [Blending into reversed-order color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - 8_8_8_8 and 16_16_16_16 targets with `COMP_SWAP` 2 or 3 keep the exported alpha in their first channel and red or blue in their fourth, which Vulkan would blend with the color and the alpha equation respectively. The color block is taken to blend in shader export order and apply `COMP_SWAP` afterwards: the alpha equation and every source-alpha factor use the exported alpha, and the destination alpha is the first channel. This follows the `CB_TARGET_MASK` text of the AMD [SI](https://xorg.freedesktop.org/docs/AMD/old/SI_3D_registers.pdf) (p. 229) and [CIK](https://xorg.freedesktop.org/docs/AMD/old/CIK_3D_registers_v2.pdf) (p. 238) register references ("the order of components after blending and before COMP_SWAP is applied"). RADV builds `CB_BLENDn_CONTROL` without regard to the swap ([radv_pipeline_graphics.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/vulkan/radv_pipeline_graphics.c#L650-L709)) and renders R4G4B4A4, B4G4R4A4, R5G5B5A1 and B5G5R5A1 with `SWAP_STD_REV` and `SWAP_ALT_REV` ([vk_format.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/vulkan/util/vk_format.c#L43-L48), [ac_formats.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/common/ac_formats.c#L609-L624)). Mesa CI's Navi10 (gfx1010) CTS run ([job 112381553](https://gitlab.freedesktop.org/mesa/mesa/-/jobs/112381553), [112381554](https://gitlab.freedesktop.org/mesa/mesa/-/jobs/112381554)) passes all 400 `blend.format..states` cases of each of those formats, including separate alpha and destination-alpha factors. It is not measured on gfx1013 or on 8_8_8_8 and 16_16_16_16 targets. Factor pairs Vulkan applies per channel are used directly. Otherwise the pixel shader only copies values: it writes the exported alpha, each channel's own export or the constants 0 and 1 into a second color at location 0 index 1 (and 1 into a channel of the first color whose source factor is ONE, moving its export into the factor), and the blend forms every factor, `1 -` and product from those copies through `SRC1_COLOR`/`SRC1_ALPHA` factors. The blend unit therefore rounds them as it rounds a plain Vulkan blend of the same export; Vulkan does not require a second-source color to be converted like the first, but on an RTX 5080 and lavapipe every reversed-order case of the execution test is bit-identical to the same draw into a `SWAP_STD` target, which the test checks. Factor combinations that copies cannot express (for example `ONE_MINUS_SRC_ALPHA` in the alpha equation with `SRC_COLOR` in the color equation), sRGB, SNORM and integer targets, constant and second-source factors, destination-alpha and `SRC_ALPHA_SATURATE` factors in the color equation, different color and alpha operations, MIN or MAX with factors other than ONE, other color exports when the blend needs the second color, and devices without `dualSrcBlend` throw. Gran Turismo 7 (PPSA01317) blends `CB_BLEND0_CONTROL` 0x65010504 into such a target (#4782). - [DB_SHADER_CONTROL.CONSERVATIVE_Z_EXPORT](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp) (libSceAgcDriver) becomes `DepthLess`/`DepthGreater`. For a pixel shader that breaks the promise, the host driver decides whether its interpolated Z is depth-tested before shading; what the console does then was not measured. From f40e1b68c88bec2007ceeaef67742902f2deae87 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Thu, 22 Oct 2026 03:57:05 +1300 Subject: [PATCH 159/621] test(avplayer): wait for the frame in the sync tolerance check The sync tolerance test asked for the next frame once, without waiting, so on a loaded runner a decoder that had not queued frame 1 yet failed the delivery check, and the premature-delivery check passed for the same reason. Poll for the frame in both checks: with a bounded wait for the premature case and the usual wait for delivery. --- core/libs/tests/GuestAvPlayer.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/libs/tests/GuestAvPlayer.cpp b/core/libs/tests/GuestAvPlayer.cpp index efa5ff1e00..68ef734c2f 100644 --- a/core/libs/tests/GuestAvPlayer.cpp +++ b/core/libs/tests/GuestAvPlayer.cpp @@ -1596,9 +1596,9 @@ void TestVideoSyncTolerance() { Check(WaitFor([&] { return sceAvPlayerGetVideoDataEx(player, &frame) != 0; }), "sync tolerance first frame missing"); Check(FrameIndex(frame.timestamp) == 0, "first frame not index 0"); AvPlayer::SetAvPlayerClockForTest_nid_no_patch(player, 5.0); - Check(!sceAvPlayerGetVideoDataEx(player, &frame), "frame beyond sync tolerance delivered prematurely"); + Check(!WaitFor([&] { return sceAvPlayerGetVideoDataEx(player, &frame) != 0; }, 200), "frame beyond sync tolerance delivered prematurely"); AvPlayer::SetAvPlayerClockForTest_nid_no_patch(player, 10.0); - Check(sceAvPlayerGetVideoDataEx(player, &frame), "frame within sync tolerance was not delivered"); + Check(WaitFor([&] { return sceAvPlayerGetVideoDataEx(player, &frame) != 0; }), "frame within sync tolerance was not delivered"); Check(FrameIndex(frame.timestamp) == 1, "unexpected second frame index"); Check(sceAvPlayerClose(player) == 0, "sync tolerance close failed"); std::lock_guard lock(allocations.mutex); From 6586741a7dc9dcfa4ac0564235d9f9e71bc8b00f Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Wed, 21 Oct 2026 18:04:23 +0300 Subject: [PATCH 160/621] 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 161/621] 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 162/621] 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 163/621] 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 164/621] 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 165/621] 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 166/621] 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 0b411ccee0c07a670f85e11df1f7c39b2368a72e Mon Sep 17 00:00:00 2001 From: Maz Date: Wed, 21 Oct 2026 19:08:57 +0400 Subject: [PATCH 167/621] docs(architecture): record main-module entry register contracts Answer #9048 from the host stubs: Linux, Windows and macOS differ, and the console is not measured. --- docs/dev/ARCHITECTURE.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index 574244a6da..0c74e50345 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -45,6 +45,18 @@ flowchart LR - [`core/relinker/main.cpp`](../../core/relinker/main.cpp) runs the steps in this order. `--to-intel` is optional, see [USAGE.md](../user/USAGE.md). - Each library in [`core/libs/prx`](../../core/libs/prx) builds as a shared library. After the build, `nid_patcher` ([`core/libs/nid`](../../core/libs/nid)) renames every export to its NID, computed from the function name. `APS5_EXPORT("", func)` sets the NID directly when the name is unknown. Every other library keeps the names libc exports; it reads them from `libc.exports`, which `nid_patcher --write-export-list` rewrites only when that list changes. On Windows the link writes each import library with `SOURCE_DATE_EPOCH=0` and `ranlib -D` and replaces it only when its bytes change. A source edit that keeps the exports therefore relinks and re-patches only its own library. +## Main-module entry registers + +The host stubs that transfer control to the guest `e_entry` are not the same on every platform, and the console's register contract at that point is not measured here (#9048). What the stubs establish today: + +| Host | Source | `rdi` | `rsi` | `rdx` | `rbp` | Return from entry | +|---|---|---|---|---|---|---| +| Linux | [`EntryStubBuilder.cpp`](../../core/relinker/elfpatcher/src/general/EntryStubBuilder.cpp) | process stack: qword `argc`, then argv pointers, then a null | 0 | left as the host dynamic linker left it (often `rtld_fini`) | zero frame `{0, 0}` | `ud2` | +| Windows | [`WindowsEntryStubBuilder.cpp`](../../core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp) | `VirtualAlloc` block: qword `argc`, UTF-8 argv pointers, null | address of an exit trampoline that finalizes guest modules and returns | not set by the stub (last host call is `FreeLibrary`) | zero frame in the stub's stack | `ExitProcess` with the entry's return code | +| macOS | [`MacOsImage.cpp`](../../core/relinker/elfpatcher/src/macos/MacOsImage.cpp) | guest entry address into `_Aps5StartGuest_nid_no_patch` | stack block `{argc, argv…, 0, envp…, 0}` | — | — | `Aps5StartGuest` has no implementation on `main`, so the final guest-entry registers are unset | + +On Linux the argument block is the process stack itself (`mov rdi, rsp`). On Windows it is a separate allocation the stub fills with UTF-8 argument strings. Titles that read only `argc`/`argv` through `rdi` see the same layout shape on both; titles that call through `rsi` on Windows get a host exit path that Linux does not provide (`rsi` is zero there). + ## Game patches [`GamePatch.cpp`](../../core/relinker/relinker/src/patching/GamePatch.cpp) parses the patch file, a strict subset of TOML. `Conversion::Convert` applies the entries of its own target to the executable bytes right after reading them, and to each bundled module before `GuestModuleBuilder` reads it, so every later stage sees patched bytes. Entry virtual addresses are translated through the `PT_LOAD` segments, and a range may not touch the ELF header or program header table, which the output writers rewrite. The patched file ranges are then checked against every in-place writer: the `--to-intel` reports and trampoline sites, `RelinkPatch` records and syscall trap ranges. The patched virtual address ranges are checked against the 8-byte targets of the `RelaData` and `RelaPltData` relocations, which the loader or the output writers overwrite. `main.cpp` builds the manifest text before it writes any output. `main.cpp` matches targets after the conversions, aggregates the applied records into `.patches.toml`, and runs `--verify-patches` through `GamePatch::VerifyPatches`, which reads only bytes. 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 168/621] 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 169/621] 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 170/621] 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 171/621] 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 172/621] 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 4bd46aa9efd6bbe3c594d2e14fc99aeb5d468c0b Mon Sep 17 00:00:00 2001 From: Nixitov Date: Tue, 20 Oct 2026 00:09:11 +0200 Subject: [PATCH 173/621] 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 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 174/621] 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: Mon, 19 Oct 2026 21:04:11 +0300 Subject: [PATCH 175/621] 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 486eb18dc115a7bf3b64fc0937963de34a5a1b36 Mon Sep 17 00:00:00 2001 From: emirplus Date: Wed, 21 Oct 2026 19:36:15 +0300 Subject: [PATCH 176/621] 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 177/621] 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 178/621] 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 a9b14023dd2c4023a9da426366a3c81b02a7206b Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Mon, 19 Oct 2026 10:55:14 +0000 Subject: [PATCH 179/621] fix(agc): retire host imports before range cuts --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 6 +- .../Graphics/src/GuestBufferMemory.cpp | 136 ++---------------- .../tests/HostImportLifetime.cpp | 5 +- .../prx/libSceAgcDriver/tests/Recorder.cpp | 51 +++---- .../tests/execution/GuestMemoryLifetime.cpp | 4 +- 5 files changed, 41 insertions(+), 161 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 177d7b1d5c..8680fd9f39 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1450,9 +1450,9 @@ if(BUILD_TESTING) endif() if(WIN32) add_test(NAME agc_reimport_bench COMMAND agc_driver_recorder_tests --reimport-bench) - add_test(NAME agc_trimmed_import COMMAND agc_driver_recorder_tests --trimmed-import-only) - set_tests_properties(agc_reimport_bench agc_trimmed_import PROPERTIES SKIP_RETURN_CODE 77) - set_tests_properties(agc_reimport_bench agc_trimmed_import PROPERTIES ENVIRONMENT_MODIFICATION + add_test(NAME agc_host_import_cut_lifetime COMMAND agc_driver_recorder_tests --host-import-cut-only) + set_tests_properties(agc_reimport_bench agc_host_import_cut_lifetime PROPERTIES SKIP_RETURN_CODE 77) + set_tests_properties(agc_reimport_bench agc_host_import_cut_lifetime PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test(NAME agc_unchanged_cpu_stamps COMMAND agc_driver_recorder_tests --unchanged-cpu-stamps-only) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 610c437369..e66a4dc0b2 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -232,7 +232,6 @@ struct HostImports { PFN_vkDestroyBuffer destroyBuffer = nullptr; PFN_vkFreeMemory freeMemory = nullptr; std::map imports; - std::map trimmed; std::map>> parked; std::uint64_t liveBytes = 0; std::map failed; @@ -458,64 +457,6 @@ void shareDmaBufImport(const Context& context, VkBufferCreateInfo& info, ImportF } #endif -void releaseTrimmed(HostImports& state, ImportNode node) { - state.liveBytes -= node.mapped().pinnedBytes; - auto holder = std::make_shared(state.device, state.destroyBuffer, state.freeMemory, std::move(node)); - auto* recorder = Recorder::Active(); - if (recorder != nullptr) recorder->KeepThroughHold(std::move(holder)); -} - -void dropTrimmed(HostImports& state, std::uint64_t begin, std::uint64_t end) { - for (auto it = state.trimmed.begin(); it != state.trimmed.end();) { - const auto next = std::next(it); - if (it->first < end && begin < it->first + it->second.bytes) releaseTrimmed(state, state.trimmed.extract(it)); - it = next; - } -} - -void retireOrTrim(VkDevice device, HostImports& state, std::map::iterator it, const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end) { - const auto& entry = it->second; - const auto entryEnd = entry.base + entry.bytes; - std::uint64_t first = 0; - std::uint64_t last = 0; - if (begin <= entry.base && end > entry.base && end < entryEnd) { - first = end; - last = entryEnd; - } else if (begin > entry.base && begin < entryEnd && end >= entryEnd) { - first = entry.base; - last = begin; - } else if (begin > entry.base && begin < end && end < entryEnd) { - const bool tail = entryEnd - end >= begin - entry.base; - first = tail ? end : entry.base; - last = tail ? entryEnd : begin; - } - const bool plain = entry.alias == nullptr && !entry.dmaBuf && entry.chunk == nullptr && !entry.joined && entry.ranges.size() == 1 && entry.backingSerial != 0 && entry.memoryType != std::numeric_limits::max() && entry.base == entry.range->address && entry.bytes == entry.range->bytes; -#ifdef _WIN32 - const bool settling = state.pendingSettles.contains(it->first); -#else - const bool settling = false; -#endif - if (first == last || !plain || settling || state.trimmed.contains(first)) { - retireImport(device, state, it, lease, false); - return; - } - RetireShadow(device, entry, [&lease](std::uint64_t from, std::uint64_t to) { return runCovers(lease, from, to); }); - ++state.epoch; - auto node = state.imports.extract(it); - auto& kept = node.mapped(); - std::shared_ptr holder(nullptr, [device = state.device, destroy = state.destroyBuffer, buffer = std::exchange(kept.buffer, VK_NULL_HANDLE)](void*) { destroy(device, buffer, nullptr); }); - kept.range.reset(); - kept.ranges.clear(); - kept.base = first; - kept.bytes = last - first; - kept.address = 0; - kept.serial = 0; - node.key() = first; - state.trimmed.insert(std::move(node)); - auto* recorder = Recorder::Active(); - if (recorder != nullptr && !recorder->Idle()) recorder->Keep(std::move(holder)); -} - VkResult createImportBuffer(const Context& context, VkExternalMemoryHandleTypeFlagBits handleType, VkDeviceSize size, VkBuffer* buffer) { const VkExternalMemoryBufferCreateInfo external{VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_BUFFER_CREATE_INFO, nullptr, static_cast(handleType)}; VkBufferCreateInfo info{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, &external}; @@ -1407,9 +1348,6 @@ bool sameRun(const Entry& entry, const ImportRun& run) { bool wouldRetireImport(const HostImports& state, const ImportRun& run) { if (run.ranges.empty()) return false; - for (const auto& [base, entry] : state.trimmed) { - if (base < run.base + run.bytes && run.base < base + entry.bytes) return true; - } const auto next = state.imports.lower_bound(run.base); if (next != state.imports.end()) { if (next->first == run.base && sameRun(next->second, run)) return false; @@ -1920,46 +1858,6 @@ const GuestAllocations::Range* containingRange(const GuestAllocations::Lease& le return range->readable && begin >= range->address && end <= range->address + range->bytes ? range.get() : nullptr; } -bool reattachTrimmed(const Context& context, HostImports& state, HostImport& entry, const GuestAllocations::Lease& lease) { - const auto first = entry.base; - const auto last = entry.base + entry.bytes; - std::shared_ptr kept; - for (auto it = std::lower_bound(lease.begin(), lease.end(), first, [](const auto& range, std::uint64_t value) { return range->address < value; }); it != lease.end() && (*it)->address < last; ++it) { - if ((*it)->backingSerial == entry.backingSerial && (*it)->address + (*it)->bytes <= last && (kept == nullptr || (*it)->bytes > kept->bytes)) kept = *it; - } - if (kept == nullptr) return false; - const auto next = state.imports.lower_bound(kept->address); - if (next != state.imports.end() && next->first < kept->address + kept->bytes) return false; - if (next != state.imports.begin() && std::prev(next)->second.base + std::prev(next)->second.bytes > kept->address) return false; - HostImport candidate{kept->address, kept->bytes, VK_NULL_HANDLE, VK_NULL_HANDLE, 0}; - candidate.range = kept; - candidate.ranges = {kept}; - if (!sameRange(candidate, lease)) return false; - const auto offset = kept->address - entry.memoryBase; - VkBuffer buffer = VK_NULL_HANDLE; - if (createImportBuffer(context, VK_EXTERNAL_MEMORY_HANDLE_TYPE_HOST_ALLOCATION_BIT_EXT, kept->bytes, &buffer) != VK_SUCCESS) return false; - const auto refuse = [&] { - context.Function("vkDestroyBuffer")(context.device, buffer, nullptr); - return false; - }; - VkMemoryRequirements requirements{}; - context.Function("vkGetBufferMemoryRequirements")(context.device, buffer, &requirements); - if (((requirements.memoryTypeBits >> entry.memoryType) & 1u) == 0 || requirements.alignment == 0 || offset % requirements.alignment != 0 || requirements.size > entry.pinnedBytes - offset) return refuse(); - if (context.Function("vkBindBufferMemory")(context.device, buffer, entry.memory, offset) != VK_SUCCESS) return refuse(); - const VkBufferDeviceAddressInfo addressInfo{VK_STRUCTURE_TYPE_BUFFER_DEVICE_ADDRESS_INFO, nullptr, buffer}; - const auto address = context.Function("vkGetBufferDeviceAddressKHR")(context.device, &addressInfo); - if (address == 0) return refuse(); - entry.base = kept->address; - entry.bytes = kept->bytes; - entry.buffer = buffer; - entry.address = address; - entry.range = kept; - entry.ranges = {kept}; - entry.serial = 0; - entry.lastUse = ++state.useCounter; - return true; -} - // Drops imports whose registered range changed or disappeared: their pages may no longer back the // guest addresses. Walks the imports only when the registry changed since the last walk. void refreshImports(const Context& context, HostImports& state, const GuestAllocations::Lease& lease) { @@ -1967,17 +1865,15 @@ void refreshImports(const Context& context, HostImports& state, const GuestAlloc #ifdef _WIN32 drainSettles(state); #endif - for (auto* held : {&state.imports, &state.trimmed}) { - while (!held->empty()) { - auto node = held->extract(held->begin()); - if (state.device != VK_NULL_HANDLE && state.destroyBuffer != nullptr && state.freeMemory != nullptr) { - state.parked[state.device].push_back(std::make_shared(state.device, state.destroyBuffer, state.freeMemory, std::move(node))); - continue; - } -#ifdef _WIN32 - GuestArena::GuestArenaUnmapAlias_nid_postfix(node.mapped().alias); -#endif + while (!state.imports.empty()) { + auto node = state.imports.extract(state.imports.begin()); + if (state.device != VK_NULL_HANDLE && state.destroyBuffer != nullptr && state.freeMemory != nullptr) { + state.parked[state.device].push_back(std::make_shared(state.device, state.destroyBuffer, state.freeMemory, std::move(node))); + continue; } +#ifdef _WIN32 + GuestArena::GuestArenaUnmapAlias_nid_postfix(node.mapped().alias); +#endif } state.liveBytes = 0; state.failed.clear(); @@ -2008,16 +1904,6 @@ void refreshImports(const Context& context, HostImports& state, const GuestAlloc }; std::erase_if(state.sparseRunEnds, [&](const auto& entry) { return !starts(entry.first); }); std::erase_if(state.sparseRunJoins, [&](const auto& entry) { return !starts(entry.first); }); - while (!state.trimmed.empty()) { - auto node = state.trimmed.extract(state.trimmed.begin()); - if (!reattachTrimmed(context, state, node.mapped(), lease)) { - releaseTrimmed(state, std::move(node)); - continue; - } - node.key() = node.mapped().base; - ++state.epoch; - state.imports.insert(std::move(node)); - } const auto budget = effectiveBudget(context, state); for (auto it = state.imports.begin(); it != state.imports.end();) { if (sameRange(it->second, lease, budget)) { @@ -2895,7 +2781,7 @@ bool WaitForLeasesAndImports(std::uintptr_t address, std::size_t bytes) { continue; } const auto next = std::next(it); - retireOrTrim(imports.device, imports, it, lease, address, end); + retireImport(imports.device, imports, it, lease, false); it = next; retiredImport = true; } @@ -2961,7 +2847,7 @@ bool WaitForLeasesAndImports(std::uintptr_t address, std::size_t bytes) { continue; } const auto next = std::next(it); - retireOrTrim(imports.device, imports, it, lease, address, end); + retireImport(imports.device, imports, it, lease, false); it = next; retiredImport = true; } @@ -3141,9 +3027,7 @@ void ClearHostImports(VkDevice device) { GuestArena::GuestArenaUnmapAlias_nid_postfix(entry.alias); #endif } - for (const auto& [address, entry] : state.trimmed) state.freeMemory(state.device, entry.memory, nullptr); state.imports.clear(); - state.trimmed.clear(); state.liveBytes = 0; state.failed.clear(); state.failedJoins.clear(); diff --git a/core/libs/prx/libSceAgcDriver/tests/HostImportLifetime.cpp b/core/libs/prx/libSceAgcDriver/tests/HostImportLifetime.cpp index 36d9aba45e..603d4bb0a2 100644 --- a/core/libs/prx/libSceAgcDriver/tests/HostImportLifetime.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/HostImportLifetime.cpp @@ -517,7 +517,7 @@ void partialUnmap(Fixture& fixture) { { GuestAllocations::Mutation mutation; mutation.Unmap(middle, part, [&](const void* piece, std::size_t bytes, const void* original, bool last) { - Require(!imported->freed.load(), "an unmap inside a host import freed the memory of the side it keeps"); + requireFreed(imported); Require(!other->freed.load(), "partial unmap retired a disjoint host import"); Require(piece == middle && bytes == part && original == allocation.data && !last, "partial unmap changed the wrong guest extent"); }); @@ -525,9 +525,8 @@ void partialUnmap(Fixture& fixture) { Require(!RegisteredReadableCovers(reinterpret_cast(middle), part), "the middle of a partial unmap remains registered"); const auto low = fixture.Import(allocation.Address(), part); const auto high = fixture.Import(allocation.Address() + 2 * part, part); - Require(high->memory == imported->memory && !imported->freed.load(), "the side kept from an unmap inside the host import was imported again"); + Require(imported->freed.load(), "the side kept from an unmap inside the host import reused memory imported across the unmapped range"); Require(high->address == allocation.Address() + 2 * part && high->bytes == part, "the kept side of the host import does not cover its remaining range"); - Require(low->memory != imported->memory, "the smaller side of an unmap inside the host import was not imported again"); Require(!low->freed.load() && !high->freed.load() && !other->freed.load(), "remaining partial mappings could not be imported"); { GuestAllocations::Mutation mutation; diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 497adb234f..e3a76c5ed2 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -2619,15 +2619,15 @@ int reimportBench(const Device& device) { throw; } gpu.lock(); - Require(importedAgain.empty(), "the kept side was imported again instead of trimmed after the " + importedAgain + " cuts"); - std::printf("re-import bench: every cut at the head, at the tail and inside the range kept the larger side by trimming the first import\n"); + Require(importedAgain == "head, tail, inside", "a cut kept memory imported across the changed host range instead of importing the remaining side again: " + importedAgain); + std::printf("re-import bench: every cut at the head, at the tail and inside the range retired the original import before importing the remaining side\n"); return 0; } -int trimmedImportTests(const Device& device) { +int hostImportCutTests(const Device& device) { const auto& context = device.GetContext(); if (context.hostImportAlignment == 0 || context.settleQueue == VK_NULL_HANDLE || !AgcDriver::GuestMemory::WriteWatched()) { - std::cout << "skipped, the trimmed import tests require host imports, a settle queue and write watching\n"; + std::cout << "skipped, the host import cut tests require host imports, a settle queue and write watching\n"; return 77; } constexpr std::size_t bytes = 32ull << 20u; @@ -2644,14 +2644,13 @@ int trimmedImportTests(const Device& device) { std::size_t keptEnd; std::size_t smaller; std::size_t smallerEnd; - bool trimmed; }; constexpr std::array cases{{ - {"a page made inaccessible inside the import, nearer its start", inside + guard, guard, false, inside + 2 * guard, bytes, 0, inside + guard, true}, - {"a page made inaccessible inside the import, nearer its end", bytes - inside - 2 * guard, guard, false, 0, bytes - inside - 2 * guard, bytes - inside - guard, bytes, true}, - {"an unmap of a 2 MiB run inside the import", inside, run, true, inside + run, bytes, 0, inside, true}, - {"an unmap of the import's first 2 MiB run", 0, run, true, run, bytes, 0, 0, true}, - {"an unmap of 1440 KiB at the start of the import's first 2 MiB run", 0, 1440ull << 10u, true, 1440ull << 10u, bytes, 0, 0, false}, + {"a page made inaccessible inside the import, nearer its start", inside + guard, guard, false, inside + 2 * guard, bytes, 0, inside + guard}, + {"a page made inaccessible inside the import, nearer its end", bytes - inside - 2 * guard, guard, false, 0, bytes - inside - 2 * guard, bytes - inside - guard, bytes}, + {"an unmap of a 2 MiB run inside the import", inside, run, true, inside + run, bytes, 0, inside}, + {"an unmap of the import's first 2 MiB run", 0, run, true, run, bytes, 0, 0}, + {"an unmap of 1440 KiB at the start of the import's first 2 MiB run", 0, 1440ull << 10u, true, 1440ull << 10u, bytes, 0, 0}, }}; struct Backing { void* mapping = nullptr; @@ -2679,15 +2678,14 @@ int trimmedImportTests(const Device& device) { const auto check = [&](const Case& test) { const std::string name = test.name; Backing backing; - Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, bytes, run, 0, &backing.physical) == 0, "allocate the trimmed import test backing"); - Require(sceKernelMapDirectMemory(&backing.mapping, bytes, 0x33, 0, backing.physical, run) == 0, "map the trimmed import test backing"); + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, bytes, run, 0, &backing.physical) == 0, "allocate the host import cut test backing"); + Require(sceKernelMapDirectMemory(&backing.mapping, bytes, 0x33, 0, backing.physical, run) == 0, "map the host import cut test backing"); const auto address = reinterpret_cast(backing.mapping); auto* memory = static_cast(backing.mapping); for (std::size_t offset = 0; offset < bytes; offset += page) std::memset(memory + offset, static_cast(0x40u + (offset / page) % 64u), 64); gpu.lock(); const auto* whole = HostImportFor(context, address, bytes); Require(whole != nullptr && whole->base == address && whole->bytes == bytes && whole->alias == nullptr && !whole->unwatched, name + ": the range was not imported whole, settled and watched"); - const auto original = whole->memory; auto* cut = memory + test.offset; gpu.unlock(); if (test.unmap) { @@ -2702,15 +2700,13 @@ int trimmedImportTests(const Device& device) { const auto end = address + test.keptEnd; const auto* kept = HostImportFor(context, begin, static_cast(end - begin)); Require(kept != nullptr && kept->base == begin && kept->base + kept->bytes == end, name + ": the larger side is not imported whole"); - const auto keptMemory = kept->memory; - if (test.trimmed) Require(kept->memory == original, "the larger side of " + name + " was imported again instead of kept"); - else Require(kept->memory != original, "the side of " + name + " whose 2 MiB run the unmap replaced is served from the import that pinned the run's old pages"); + Require(kept->memoryBase == begin && kept->pinnedBytes == end - begin, "the side of " + name + " is served from the import that pinned the changed host range"); requireImportServesGuest(context, recorder, *kept, begin, end, ++salt, "the larger side of " + name); if (test.smallerEnd != test.smaller) { const auto* smaller = HostImportFor(context, address + test.smaller, test.smallerEnd - test.smaller); - Require(smaller != nullptr && smaller->base == address + test.smaller && smaller->bytes == test.smallerEnd - test.smaller && smaller->memory != original, "the smaller side of " + name + " is not imported apart from the larger one"); + Require(smaller != nullptr && smaller->base == address + test.smaller && smaller->bytes == test.smallerEnd - test.smaller && smaller->memoryBase == address + test.smaller && smaller->pinnedBytes == test.smallerEnd - test.smaller, "the smaller side of " + name + " is not imported apart from the larger one"); requireImportServesGuest(context, recorder, *smaller, address + test.smaller, address + test.smallerEnd, ++salt, "the smaller side of " + name); - Require(HostImportFor(context, begin, static_cast(end - begin)) == kept && kept->memory == original, "importing the smaller side of " + name + " dropped the larger one"); + Require(HostImportFor(context, begin, static_cast(end - begin)) == kept && kept->memoryBase == begin && kept->pinnedBytes == end - begin, "importing the smaller side of " + name + " replaced the safe import of the larger one"); } if (test.unmap) { gpu.unlock(); @@ -2722,26 +2718,25 @@ int trimmedImportTests(const Device& device) { const auto from = reinterpret_cast(cut); const auto* remapped = HostImportFor(context, address, bytes); Require(remapped != nullptr && remapped->base == address && remapped->bytes == bytes, name + ": the range mapped again is not imported whole"); - Require(remapped->memory != keptMemory, "after " + name + " the pages mapped again are served from the import made before they were mapped again"); + Require(remapped->memoryBase == address && remapped->pinnedBytes == bytes, "after " + name + " the pages mapped again are served from the import that pinned the old ones"); requireImportServesGuest(context, recorder, *remapped, from, from + test.length, ++salt, "the pages mapped again after " + name); requireImportServesGuest(context, recorder, *remapped, begin, end, ++salt, "the larger side after " + name + " and a new mapping"); } cleanup(); gpu.unlock(); - std::cout << "trimmed import: " << name << ": the larger side was " << (test.trimmed ? "kept" : "imported again") << " and the GPU and the write watch see the guest's bytes" << (test.unmap ? ", also after the pages were mapped again" : "") << '\n'; + std::cout << "host import after cut: " << name << ": the changed import was retired before the remaining range was imported, and the GPU and write watch see the guest's bytes" << (test.unmap ? ", also after the pages were mapped again" : "") << '\n'; }; const auto remapWhileTrimmed = [&] { - const std::string name = "an unmap and a new mapping of the kept side while the import waits trimmed"; + const std::string name = "an unmap and a new mapping of the retained side while the old import is retired"; Backing backing; - Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, bytes, run, 0, &backing.physical) == 0, "allocate the trimmed import test backing"); - Require(sceKernelMapDirectMemory(&backing.mapping, bytes, 0x33, 0, backing.physical, run) == 0, "map the trimmed import test backing"); + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, bytes, run, 0, &backing.physical) == 0, "allocate the host import cut test backing"); + Require(sceKernelMapDirectMemory(&backing.mapping, bytes, 0x33, 0, backing.physical, run) == 0, "map the host import cut test backing"); const auto address = reinterpret_cast(backing.mapping); auto* memory = static_cast(backing.mapping); for (std::size_t offset = 0; offset < bytes; offset += page) std::memset(memory + offset, static_cast(0x40u + (offset / page) % 64u), 64); gpu.lock(); const auto* whole = HostImportFor(context, address, bytes); Require(whole != nullptr && whole->base == address && whole->bytes == bytes && whole->alias == nullptr && !whole->unwatched, name + ": the range was not imported whole, settled and watched"); - const auto original = whole->memory; gpu.unlock(); Require(sceKernelMprotect(memory, run, 0x03) == 0, name + ": the protection of the head failed"); Require(sceKernelMunmap(memory + run, bytes - run) == 0, name + ": the unmap of the kept side failed"); @@ -2757,10 +2752,10 @@ int trimmedImportTests(const Device& device) { const auto* remapped = HostImportFor(context, begin, static_cast(end - begin)); Require(remapped != nullptr && remapped->base == begin && remapped->base + remapped->bytes == end, name + ": the pages mapped again are not imported whole"); requireImportServesGuest(context, recorder, *remapped, begin, end, ++salt, "the pages mapped again after " + name); - Require(remapped->memory != original, "after " + name + " the pages mapped again are served from the import that pinned the old ones"); + Require(remapped->memoryBase == begin && remapped->pinnedBytes == end - begin, "after " + name + " the pages mapped again are served from the import that pinned the old ones"); cleanup(); gpu.unlock(); - std::cout << "trimmed import: " << name << ": the pages mapped again were imported again and the GPU and the write watch see the guest's bytes\n"; + std::cout << "host import cut: " << name << ": the pages mapped again were imported again and the GPU and the write watch see the guest's bytes\n"; }; gpu.unlock(); try { @@ -2771,7 +2766,7 @@ int trimmedImportTests(const Device& device) { throw; } gpu.lock(); - std::cout << "Trimmed host import tests passed\n"; + std::cout << "Host import cut lifetime tests passed\n"; return 0; } #endif @@ -9999,7 +9994,7 @@ int main(int argc, char** argv) { if (argc == 2 && std::string_view(argv[1]) == "--settle-retire-only") return settleRetireTests(device) ? 0 : 77; if (argc == 2 && std::string_view(argv[1]) == "--settle-storage-image-only") return settleStorageImageTests(device) ? 0 : 77; if (argc == 2 && std::string_view(argv[1]) == "--reimport-bench") return reimportBench(device); - if (argc == 2 && std::string_view(argv[1]) == "--trimmed-import-only") return trimmedImportTests(device); + if (argc == 2 && std::string_view(argv[1]) == "--host-import-cut-only") return hostImportCutTests(device); #endif if (argc == 2 && std::string_view(argv[1]) == "--host-import-retirement-only") { const auto alignment = device.GetContext().hostImportAlignment; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/GuestMemoryLifetime.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/GuestMemoryLifetime.cpp index 6c92e2171d..ed32384e66 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/GuestMemoryLifetime.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/GuestMemoryLifetime.cpp @@ -112,7 +112,9 @@ void CheckMappings(AgcDriver::VulkanDevice& device) { CheckPattern(mapping); Fill(device, mapping); Require(munmap_nid_postfix(middle, BlockBytes) == 0, "guest middle unmap failed"); - Require(Imported(device, mapping), "unrelated prefix import was retired by middle unmap"); + Require(!Imported(device, mapping), "middle unmap left the original host import alive across the unmapped range"); + Fill(device, mapping); + Require(Imported(device, mapping), "the retained prefix was not imported again after middle unmap"); Require(munmap_nid_postfix(mapping, BlockBytes) == 0, "guest prefix unmap failed"); Require(!Imported(device, mapping), "prefix host import survived guest munmap"); Require(munmap_nid_postfix(middle + BlockBytes, BlockBytes) == 0, "guest suffix unmap failed"); From 787a7bc39d759d1d101c67f150242ff4c7853d2e Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Mon, 19 Oct 2026 17:32:10 +0000 Subject: [PATCH 180/621] fix(agc): retire imports after dropping cached address spaces --- .../Graphics/src/GuestBufferMemory.cpp | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index e66a4dc0b2..6720fa9c8e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -2743,6 +2743,7 @@ bool WaitForLeasesAndImports(std::uintptr_t address, std::size_t bytes) { bool synced = false; bool drained = false; bool retiredImport = false; + bool droppedAddressSpace = false; // A range pinned only by the cached address space is released by dropping the cache's // reference: no GPU wait, no device lock (the mutating thread may be a driver thread holding // it). The registry rescans; a range still pinned by a build or a batch holding the space comes @@ -2751,12 +2752,24 @@ bool WaitForLeasesAndImports(std::uintptr_t address, std::size_t bytes) { Spaces().droppedByWaiter.store(true, std::memory_order_relaxed); Spaces().waiterDrops.fetch_add(1, std::memory_order_relaxed); dropped.reset(); - auto& state = Leases(); - std::lock_guard lock(state.mutex); - ++state.stats.contentionWaits; - ++state.stats.cacheDrops; - state.stats.contentionMs += std::chrono::duration(std::chrono::steady_clock::now() - start).count(); - return true; + droppedAddressSpace = true; + auto& imports = Imports(); + const auto end = static_cast(address) + bytes; + bool overlapsImport = false; + { + std::lock_guard importsLock(imports.mutex); + const auto next = imports.imports.lower_bound(address); + const auto previous = next == imports.imports.begin() ? imports.imports.end() : std::prev(next); + overlapsImport = (next != imports.imports.end() && next->first < end) || (previous != imports.imports.end() && previous->first + previous->second.bytes > address); + } + if (!overlapsImport) { + auto& state = Leases(); + std::lock_guard lock(state.mutex); + ++state.stats.contentionWaits; + ++state.stats.cacheDrops; + state.stats.contentionMs += std::chrono::duration(std::chrono::steady_clock::now() - start).count(); + return true; + } } if (Recorder::InCompletion()) throw std::runtime_error("guest allocation mutation cannot wait for an active GPU completion"); if (GuestMemory::GpuMutex().HeldByThisThread()) { @@ -2866,6 +2879,7 @@ bool WaitForLeasesAndImports(std::uintptr_t address, std::size_t bytes) { auto& state = Leases(); std::lock_guard lock(state.mutex); ++state.stats.contentionWaits; + if (droppedAddressSpace) ++state.stats.cacheDrops; if (synced) ++state.stats.contentionSyncs; if (drained) ++state.stats.contentionDrains; state.stats.contentionMs += std::chrono::duration(std::chrono::steady_clock::now() - start).count(); From 2a12152f2b55792bd362c34552ec66496bfe66af Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Mon, 19 Oct 2026 19:15:48 +0000 Subject: [PATCH 181/621] test(agc): clarify imports after partial unmap --- .../tests/execution/GuestMemoryLifetime.cpp | 28 ++++++++++++------- docs/dev/ARCHITECTURE.md | 2 +- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/GuestMemoryLifetime.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/GuestMemoryLifetime.cpp index ed32384e66..a7511c3012 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/GuestMemoryLifetime.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/GuestMemoryLifetime.cpp @@ -63,16 +63,16 @@ void* Map(std::size_t bytes = BlockBytes) { return result; } -void CheckPattern(const void* pointer) { +void CheckPattern(const void* pointer, std::size_t bytes = FillBytes) { const auto* data = static_cast(pointer); - for (std::size_t offset = 0; offset < FillBytes; offset += sizeof(Pattern)) + for (std::size_t offset = 0; offset < bytes; offset += sizeof(Pattern)) Require(std::memcmp(data + offset, Pattern.data(), sizeof(Pattern)) == 0, "guest mutation lost the pending GPU fill"); } -void Fill(AgcDriver::VulkanDevice& device, void* pointer) { +void Fill(AgcDriver::VulkanDevice& device, void* pointer, std::size_t bytes = FillBytes) { std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); - Require(device.FillBuffer(Address(pointer), FillBytes, Pattern), "Vulkan device did not import the guest allocation"); - Require(Imported(device, pointer), "GPU fill did not create a host import"); + Require(device.FillBuffer(Address(pointer), bytes, Pattern), "Vulkan device did not import the guest allocation"); + Require(Imported(device, pointer, bytes), "GPU fill did not create a host import"); } void CheckMappings(AgcDriver::VulkanDevice& device) { @@ -105,14 +105,22 @@ void CheckMappings(AgcDriver::VulkanDevice& device) { Require(!Imported(device, mapping), "host import survived guest munmap"); mapping = Map(3 * BlockBytes); - Fill(device, mapping); + Fill(device, mapping, 3 * BlockBytes); auto* middle = static_cast(mapping) + BlockBytes; Require(mprotect_nid_postfix(middle, BlockBytes, 1) == 0, "guest partial protection failed"); - Require(!Imported(device, mapping), "whole host import survived partial protection change"); - CheckPattern(mapping); - Fill(device, mapping); + Require(!Imported(device, mapping, 3 * BlockBytes), "whole host import survived partial protection change"); + CheckPattern(mapping, 3 * BlockBytes); + Require(munmap_nid_postfix(middle, BlockBytes) == 0, "guest middle unmap after protection change failed"); + Require(!Imported(device, mapping, 3 * BlockBytes), "host import still covered the protected and unmapped middle range"); + Require(munmap_nid_postfix(mapping, BlockBytes) == 0, "guest prefix unmap after protection change failed"); + Require(munmap_nid_postfix(middle + BlockBytes, BlockBytes) == 0, "guest suffix unmap after protection change failed"); + + mapping = Map(3 * BlockBytes); + Fill(device, mapping, 3 * BlockBytes); + Require(Imported(device, mapping, 3 * BlockBytes), "whole host import did not cover the three-block mapping before middle unmap"); + middle = static_cast(mapping) + BlockBytes; Require(munmap_nid_postfix(middle, BlockBytes) == 0, "guest middle unmap failed"); - Require(!Imported(device, mapping), "middle unmap left the original host import alive across the unmapped range"); + Require(!Imported(device, mapping, 3 * BlockBytes), "middle unmap left a host import spanning the unmapped range"); Fill(device, mapping); Require(Imported(device, mapping), "the retained prefix was not imported again after middle unmap"); Require(munmap_nid_postfix(mapping, BlockBytes) == 0, "guest prefix unmap failed"); diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index 574244a6da..14c028bcc0 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -130,7 +130,7 @@ flowchart LR - A draw whose `PA_SC_AA_CONFIG` selects 2, 4 or 8 samples renders into host-only multisampled images. [`MultisampleTarget.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) keeps one image per device, surface address, format, extent and sample count (`MultisampleTargetView`); the pipeline, render pass and library keys carry `State::samples`, and a multisampled depth target is a multisampled depth surface. The guest sample planes and FMASK are never read or written, so the registry also guards the target's guest range: a CPU read (`GuestMemory::SetReadGuard`), a DMA copy, and a sampled or storage image read of it throw. A `CB_COLOR_CONTROL` resolve mode over a multisampled CB0 runs as a render pass whose resolve attachment is CB1's resident image (`ResolveMultisampleTarget`). A zero fill of the target's CMASK (`NoteColorMetadataFill`, from fill kernels and CP DMA fills) clears the image at its next use. - A texture over a depth surface samples the depth image itself when it views one 2D slice of the plane's own format (`DepthSurfaceTexture` in [`DepthSurface.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp)). A view of several slices (a 2D array or a cube) or an integer view of a D16 plane is served by a `DepthPlaneCopy`, an image the slices are copied into at every use: from the depth surface bound at each slice's address, else from guest memory. Such a texture is never kept in the resource templates (`RefreshedPerUse`). - Guest memory reaches the GPU through host imports (`VK_EXT_external_memory_host`) where the driver accepts them, and through copies otherwise ([`GuestBufferMemory.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp)). A registered range larger than the import window (64 MiB) is imported per window for buffer and fill accesses and whole for an address-based build. Live imports stay under a budget, with the least recently used import retired first (see TechnicalDebt, Host import windows and budget). -- A guest protect or unmap over part of a plain host-pointer import (one registered range, no alias, dma-buf, chunked or joined import, no pending settle) trims the import instead of retiring it (`retireOrTrim` in [`GuestBufferMemory.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp)): its buffer is destroyed and it waits in `HostImports::trimmed` with the side of its range the change left, the larger side for a change strictly inside the range, while its `VkDeviceMemory` keeps the whole range it was made for pinned (`HostImport::memoryBase` and `pinnedBytes`, which `liveBytes` and the eviction count), also after the guest protects, unmaps or maps again the pages that were cut off. The next `refreshImports` binds a new `VkBuffer` to the same memory at the offset of the largest registered range inside the kept side (`reattachTrimmed`) when that range still has the import's backing serial, overlaps no other import, is what a new import would cover and the buffer accepts the offset; otherwise, or when an import of a range overlapping the kept side is made first, the trimmed import is released and held through the recorder (`KeepThroughHold`) as a retired import is. `GuestAllocations::Range::backingSerial` tells whether a range's pages are still the ones that were imported: a new range gets a fresh serial, the untouched pieces of a split keep theirs, and every span a mutation passes to `RequireUnpinned` (a protect or unmap, a fixed mapping over registered memory, the other mappings of remapped direct memory, and for an unmap or remap the whole 2 MiB resident runs it touches, which it may demote to shared pages) gets a fresh one when the mutation ends. +- 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. From fa8115ce6351ec567faea190743d6c45ae5e4ebc Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 20 Oct 2026 08:07:56 +0000 Subject: [PATCH 182/621] test(agc): assert whole import retires on middle unmap --- .../prx/libSceAgcDriver/tests/execution/GuestMemoryLifetime.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/GuestMemoryLifetime.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/GuestMemoryLifetime.cpp index a7511c3012..2d737ebc4e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/GuestMemoryLifetime.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/GuestMemoryLifetime.cpp @@ -121,6 +121,7 @@ void CheckMappings(AgcDriver::VulkanDevice& device) { middle = static_cast(mapping) + BlockBytes; Require(munmap_nid_postfix(middle, BlockBytes) == 0, "guest middle unmap failed"); Require(!Imported(device, mapping, 3 * BlockBytes), "middle unmap left a host import spanning the unmapped range"); + Require(!Imported(device, mapping), "middle unmap retained the prefix of the retired host import"); Fill(device, mapping); Require(Imported(device, mapping), "the retained prefix was not imported again after middle unmap"); Require(munmap_nid_postfix(mapping, BlockBytes) == 0, "guest prefix unmap failed"); From 349a25f7ff03ee4b2f11c57813d3d01712026958 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 20 Oct 2026 08:30:27 +0000 Subject: [PATCH 183/621] fix(agc): remove stale trimmed-import call --- core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 6720fa9c8e..77e76cc5cd 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -1624,7 +1624,6 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import if (sameRun(failed->second, run)) return nullptr; failures.erase(failed); } - dropTrimmed(state, base, base + bytes); const auto budget = effectiveBudget(context, state); const auto containing = std::find_if(run.ranges.begin(), run.ranges.end(), [address](const auto& range) { return address >= range->address && address < range->address + range->bytes; }); if (const auto remembered = state.evictingRefusals.find({base, bytes}); evict && remembered != state.evictingRefusals.end()) { From c566d31d1f472506518ad9490c5347141cdebf1a Mon Sep 17 00:00:00 2001 From: MarcZX10 Date: Wed, 21 Oct 2026 16:53:05 +0000 Subject: [PATCH 184/621] test(agc): skip reimport bench below host budget --- .../libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp | 1 + .../prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp | 7 +++++++ core/libs/prx/libSceAgcDriver/tests/Recorder.cpp | 6 ++++++ 3 files changed, 14 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp index ceec04cf3e..fa552ac524 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp @@ -114,6 +114,7 @@ void SetHostImportDeferredSettles(bool enabled); // 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); std::uint64_t HostImportLiveBytes(); +std::uint64_t HostImportBudgetRemaining(const Context& context); struct HostImportSettleCounts { std::uint64_t submitted = 0; std::uint64_t waited = 0; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 77e76cc5cd..fd46544f36 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -3333,6 +3333,13 @@ std::uint64_t HostImportLiveBytes() { return state.liveBytes; } +std::uint64_t HostImportBudgetRemaining(const Context& context) { + auto& state = Imports(); + std::lock_guard lock(state.mutex); + const auto budget = effectiveBudget(context, state); + return budget > state.liveBytes ? budget - state.liveBytes : 0; +} + const char* ImportRequesterName(ImportRequester requester) { switch (requester) { case ImportRequester::Map: return "map"; diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index e3a76c5ed2..e78f79d58b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -2505,6 +2505,12 @@ int reimportBench(const Device& device) { constexpr std::size_t moves = 5; constexpr std::size_t page = 4096; constexpr std::size_t alignment = 2ull << 20u; + constexpr std::size_t requiredImportBytes = bytes + inside; + const auto availableImportBytes = HostImportBudgetRemaining(context); + if (availableImportBytes < requiredImportBytes) { + std::cout << "skipped, the re-import bench needs " << (requiredImportBytes >> 20u) << " MiB of simultaneous host imports; the current budget has " << (availableImportBytes >> 20u) << " MiB available\n"; + return 77; + } struct Release { void* mapping = nullptr; std::int64_t physical = -1; From 89da7e6c0ae7d1230010dde754089f51c523bc5e Mon Sep 17 00:00:00 2001 From: ShinMalphur Date: Wed, 21 Oct 2026 23:26:44 +0630 Subject: [PATCH 185/621] 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 186/621] 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 187/621] 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 58ad4eda77577412aab42a180a82fc164d1cfab1 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Thu, 22 Oct 2026 04:06:03 +1300 Subject: [PATCH 188/621] 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 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 189/621] 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 190/621] 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 191/621] 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 192/621] 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 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 193/621] 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 194/621] 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 be51ab7bae405612f27bd0eb6e56c4dedb4209b1 Mon Sep 17 00:00:00 2001 From: mossyanwar <moezanwar1505@gmail.com> Date: Wed, 21 Oct 2026 16:49:04 +0100 Subject: [PATCH 195/621] 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 ba8b881fa63e26d86d7d3c48f94ae2252a30e026 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Tue, 20 Oct 2026 15:43:36 +0200 Subject: [PATCH 196/621] fix(libkernel): answer a receive after SHUT_RD or with no buffer without the host soreceive_generic returns 0 for a socket whose receive side is shut and for a zero-length receive on a connected stream socket (ENOTCONN when it is not connected), without waiting. Winsock fails the first with WSAESHUTDOWN, which reached the guest as EPIPE, and blocks the second until data arrives. recv and recvfrom now answer both themselves. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --- core/libs/prx/libkernel/Socket/src/Udp.cpp | 16 ++++++++++ core/libs/tests/GuestTcp.cpp | 34 ++++++++++++++++++++++ docs/dev/debt/libkernel.md | 1 + 3 files changed, 51 insertions(+) diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index a9805e0391..50f4c309e9 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -673,6 +673,16 @@ int SetNonBlocking(Socket& socket) { socket.options.nonblocking = true; return 0; } +int ReceiveAnsweredHere(Socket& socket, std::uint64_t length) { + { + std::lock_guard lock(socket.modeMutex); + if (socket.receiveShut) return 0; + } + if (length != 0 || socket.type != 1) return -1; + sockaddr_storage peer{}; + socklen_t size = sizeof(peer); + return ::getpeername(socket.value, reinterpret_cast<sockaddr*>(&peer), &size) == 0 ? 0 : NativeError(); +} int SocketSendFlags(Socket& socket, int flags) { std::lock_guard lock(socket.modeMutex); return NativeSendFlags(socket.options.noSignal ? flags | GuestNoSignal : flags); @@ -1256,6 +1266,7 @@ std::int64_t APS5_VABI recv_nid_postfix(int descriptor, void* buffer, std::uint6 if (!buffer && length) return Fail(14); const GuestArena::HostWrite destination(length == 0 ? nullptr : buffer, static_cast<std::size_t>(length)); if (!destination.Open()) return Fail(14); + if (const int answered = ReceiveAnsweredHere(*socket, length); answered >= 0) return answered == 0 ? 0 : Fail(answered); if (const int waited = WaitForTransfer(*socket, false, flags)) return Fail(waited); const auto result = ::recv(socket->value, static_cast<char*>(buffer), static_cast<int>(length), NativeReceiveFlags(flags, socket->type)); @@ -1368,6 +1379,11 @@ std::int64_t APS5_VABI recvfrom_nid_postfix(int descriptor, void* buffer, std::u if ((!buffer && length) || (address && !addressLength)) return Fail(14); const GuestArena::HostWrite destination(length == 0 ? nullptr : buffer, static_cast<std::size_t>(length)); if (!destination.Open()) return Fail(14); + if (const int answered = ReceiveAnsweredHere(*socket, length); answered >= 0) { + if (answered > 0) return Fail(answered); + if (address) *addressLength = 0; + return 0; + } sockaddr_storage native{}; socklen_t size = sizeof(native); if (const int waited = WaitForTransfer(*socket, false, flags)) return Fail(waited); diff --git a/core/libs/tests/GuestTcp.cpp b/core/libs/tests/GuestTcp.cpp index 15f8d514a6..80322f4979 100644 --- a/core/libs/tests/GuestTcp.cpp +++ b/core/libs/tests/GuestTcp.cpp @@ -178,11 +178,45 @@ static void WaitAllReceivesTheWholeRequest() { for (const int descriptor : {datagram, client, accepted, listener}) Require(close_nid_postfix(descriptor) == 0); } +static void ReceiveAnsweredWithoutTheHost() { + constexpr int DontWait = 0x80; + const int listener = socket_nid_postfix(2, 1, 0); + Require(listener >= 0); + std::array<std::uint8_t, 16> address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(bind_nid_postfix(listener, address.data(), address.size()) == 0 && listen_nid_postfix(listener, 4) == 0); + std::uint32_t size = address.size(); + Require(getsockname_nid_postfix(listener, address.data(), &size) == 0); + const int unconnected = socket_nid_postfix(2, 1, 0); + char byte; + Require(recv_nid_postfix(unconnected, &byte, 0, DontWait) == -1 && *__error_nid_postfix() == 57); + const int client = socket_nid_postfix(2, 1, 0); + Require(connect_nid_postfix(client, address.data(), address.size()) == 0); + const int accepted = accept_nid_postfix(listener, nullptr, nullptr); + Require(accepted >= 0); + + Require(recv_nid_postfix(accepted, &byte, 0, DontWait) == 0); + Require(recv_nid_postfix(accepted, &byte, 0, 0) == 0); + std::array<std::uint8_t, 16> sender; + sender.fill(0xaa); + std::uint32_t senderSize = sender.size(); + Require(recvfrom_nid_postfix(accepted, &byte, 0, 0, sender.data(), &senderSize) == 0 && senderSize == 0); + + Require(shutdown_nid_postfix(accepted, 0) == 0); + Require(send_nid_postfix(client, "x", 1, 0) == 1); + char received[4]; + Require(recv_nid_postfix(accepted, received, sizeof(received), 0) == 0); + Require(recv_nid_postfix(accepted, received, sizeof(received), DontWait) == 0); + senderSize = sender.size(); + Require(recvfrom_nid_postfix(accepted, received, sizeof(received), 0, sender.data(), &senderSize) == 0 && senderSize == 0); + for (const int descriptor : {unconnected, client, accepted, listener}) Require(close_nid_postfix(descriptor) == 0); +} + int main() { StreamReceiveReportsNoSender(); ListenersShareAPortAcrossFamilies(); AcceptedSocketsFollowListenerBlocking(); WaitAllReceivesTheWholeRequest(); + ReceiveAnsweredWithoutTheHost(); UnboundSocketNameIsZero(); const int listener = socket_nid_postfix(2, 1, 6); Require(listener >= 0); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index cc3fef20ac..640ad4bc97 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -189,6 +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 - [MSG_WAITALL](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) in `recv`, `recvfrom` and `recvmsg` on a stream socket is passed to the host, which returns when the buffer is full, the peer closes or an error occurs, as FreeBSD 11.4's [`soreceive_generic`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L1855-L1860) does; a datagram socket ignores it. It is not measured on a console, a nonblocking socket and a call that also passes `MSG_PEEK` follow the host's rules, and `SO_RCVTIMEO` bounds only the wait for the first data. Any other flag still fails with `EOPNOTSUPP`. +- [recv and recvfrom](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) answer without the host a receive on a socket whose read side `shutdown` closed (0, as [`soreceive_generic`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L1556-L1562) returns at `SBS_CANTRCVMORE`) and a zero-length receive on a stream socket (0, or `ENOTCONN` when it is not connected, [L1566-L1578](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L1566-L1578)); Winsock fails the first with `EPIPE` and blocks on the second. `recvmsg` still asks the host in both cases. Not measured on a console. - [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. - [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 From c02a5d8034d8ea197718169eaec7cdb596921ccb Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Wed, 21 Oct 2026 12:52:14 +0200 Subject: [PATCH 197/621] fix(libkernel): keep the receive-after-SHUT_RD answer on Windows only On Linux the host keeps data that was buffered before shutdown(SHUT_RD) and guest_socket_pair expects to read it, so the shortcut that answers 0 for any receive after SHUT_RD broke that test there. Limit the shortcut to Windows, where the host cannot answer it, and run the post-shutdown receives of guest_tcp on Windows only. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --- core/libs/prx/libkernel/Socket/src/Udp.cpp | 2 ++ core/libs/tests/GuestTcp.cpp | 2 ++ 2 files changed, 4 insertions(+) diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 50f4c309e9..593cac22fd 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -674,10 +674,12 @@ int SetNonBlocking(Socket& socket) { return 0; } int ReceiveAnsweredHere(Socket& socket, std::uint64_t length) { +#ifdef _WIN32 { std::lock_guard lock(socket.modeMutex); if (socket.receiveShut) return 0; } +#endif if (length != 0 || socket.type != 1) return -1; sockaddr_storage peer{}; socklen_t size = sizeof(peer); diff --git a/core/libs/tests/GuestTcp.cpp b/core/libs/tests/GuestTcp.cpp index 80322f4979..332b9ebede 100644 --- a/core/libs/tests/GuestTcp.cpp +++ b/core/libs/tests/GuestTcp.cpp @@ -201,6 +201,7 @@ static void ReceiveAnsweredWithoutTheHost() { std::uint32_t senderSize = sender.size(); Require(recvfrom_nid_postfix(accepted, &byte, 0, 0, sender.data(), &senderSize) == 0 && senderSize == 0); +#ifdef _WIN32 Require(shutdown_nid_postfix(accepted, 0) == 0); Require(send_nid_postfix(client, "x", 1, 0) == 1); char received[4]; @@ -208,6 +209,7 @@ static void ReceiveAnsweredWithoutTheHost() { Require(recv_nid_postfix(accepted, received, sizeof(received), DontWait) == 0); senderSize = sender.size(); Require(recvfrom_nid_postfix(accepted, received, sizeof(received), 0, sender.data(), &senderSize) == 0 && senderSize == 0); +#endif for (const int descriptor : {unconnected, client, accepted, listener}) Require(close_nid_postfix(descriptor) == 0); } From 32f8205390ddfc601183db4f63d54544e5c6dcf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= <adriadam10@gmail.com> Date: Wed, 21 Oct 2026 20:01:26 +0200 Subject: [PATCH 198/621] docs(libkernel): say the receive-after-SHUT_RD answer is Windows only Co-Authored-By: Claude <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 640ad4bc97..a4988be219 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 - [MSG_WAITALL](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) in `recv`, `recvfrom` and `recvmsg` on a stream socket is passed to the host, which returns when the buffer is full, the peer closes or an error occurs, as FreeBSD 11.4's [`soreceive_generic`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L1855-L1860) does; a datagram socket ignores it. It is not measured on a console, a nonblocking socket and a call that also passes `MSG_PEEK` follow the host's rules, and `SO_RCVTIMEO` bounds only the wait for the first data. Any other flag still fails with `EOPNOTSUPP`. -- [recv and recvfrom](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) answer without the host a receive on a socket whose read side `shutdown` closed (0, as [`soreceive_generic`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L1556-L1562) returns at `SBS_CANTRCVMORE`) and a zero-length receive on a stream socket (0, or `ENOTCONN` when it is not connected, [L1566-L1578](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L1566-L1578)); Winsock fails the first with `EPIPE` and blocks on the second. `recvmsg` still asks the host in both cases. Not measured on a console. +- [recv and recvfrom](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) answer without the host a zero-length receive on a stream socket (0, or `ENOTCONN` when it is not connected, [L1566-L1578](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L1566-L1578)) and, on Windows only, a receive on a socket whose read side `shutdown` closed (0, as [`soreceive_generic`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L1556-L1562) returns at `SBS_CANTRCVMORE`); Winsock fails the second with `EPIPE` and blocks on the first. Linux and macOS still return data buffered before the `shutdown`, where FreeBSD discards it. `recvmsg` still asks the host in both cases. Not measured on a console. - [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. - [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 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 199/621] 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 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 200/621] 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 201/621] 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 202/621] 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 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 203/621] 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 204/621] 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 205/621] 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 5266e7f770ac22a0e28e1602605a7aea06d3e1b8 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 11:16:25 +0300 Subject: [PATCH 206/621] 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 207/621] 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 208/621] 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 209/621] 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 210/621] 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 211/621] 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 212/621] 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 213/621] 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 214/621] 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 215/621] 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 216/621] 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 217/621] 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 218/621] 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 219/621] 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 220/621] 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 221/621] 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 222/621] 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 223/621] 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 224/621] 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 225/621] 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 226/621] 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 227/621] 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 228/621] 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 229/621] 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 230/621] 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 231/621] 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 232/621] 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 233/621] 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 234/621] 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 235/621] 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 236/621] 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 237/621] 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 238/621] 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 239/621] 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 240/621] 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 241/621] 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 242/621] 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 243/621] 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 244/621] 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 245/621] 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 246/621] 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 247/621] 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 248/621] 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 249/621] 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 250/621] 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 251/621] 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 252/621] 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 253/621] 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 254/621] 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 255/621] 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 256/621] 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 257/621] 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 258/621] 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 259/621] 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 260/621] 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 261/621] 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 262/621] 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 263/621] 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 264/621] 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 265/621] 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 266/621] 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 267/621] 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 268/621] 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 269/621] 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 270/621] 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 271/621] 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 272/621] 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 273/621] 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 274/621] 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 275/621] 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 276/621] 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 277/621] 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 278/621] 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 279/621] 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 280/621] 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 281/621] 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 282/621] 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 283/621] 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 284/621] 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 285/621] 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 286/621] 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 287/621] 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 288/621] 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 289/621] 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 290/621] 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 291/621] 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 292/621] 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 293/621] 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 294/621] 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 295/621] 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 296/621] 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 297/621] 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 298/621] 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 299/621] 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 300/621] 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 301/621] 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 302/621] 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 303/621] 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 304/621] 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 305/621] 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 306/621] 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 307/621] 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 308/621] 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 309/621] 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 310/621] 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 311/621] 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 312/621] 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 313/621] 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 314/621] 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 315/621] 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 316/621] 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 317/621] 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 318/621] 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 319/621] 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 320/621] 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 321/621] 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 322/621] 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 323/621] 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 324/621] 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 325/621] 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 326/621] 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 327/621] 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 328/621] 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 329/621] 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 330/621] 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 331/621] 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 332/621] 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 333/621] 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 334/621] 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 335/621] 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 336/621] 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 337/621] 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 338/621] 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 339/621] 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 340/621] 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 341/621] 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 342/621] 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 343/621] 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 344/621] 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 345/621] 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 346/621] 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 347/621] 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 348/621] 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 349/621] 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 350/621] 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 351/621] 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 352/621] 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 353/621] 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 354/621] 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 355/621] 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 356/621] 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 357/621] 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 358/621] 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 359/621] 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 360/621] 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 361/621] 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 362/621] 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 363/621] 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 364/621] 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 365/621] 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 366/621] 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 367/621] 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 368/621] 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 369/621] 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 370/621] 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 371/621] 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 372/621] 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 373/621] 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 374/621] 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 375/621] 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 376/621] 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 377/621] 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 378/621] 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 379/621] 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 380/621] 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 381/621] 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 382/621] 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 383/621] 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 384/621] 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 385/621] 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 386/621] 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 387/621] 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 388/621] 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 389/621] 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 390/621] 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 391/621] 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 392/621] 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 393/621] 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 394/621] 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 395/621] 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 396/621] 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 397/621] 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 398/621] 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 399/621] 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 400/621] 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 401/621] 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 402/621] 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 403/621] 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 404/621] 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 405/621] 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 406/621] 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 6703b32a3788600e89bd9c9fa6e927e74af0b3ac Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Tue, 20 Oct 2026 23:41:53 +0300 Subject: [PATCH 407/621] 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 408/621] 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 409/621] 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 410/621] 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 411/621] 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 412/621] 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 413/621] 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 414/621] 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 415/621] 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 416/621] 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 417/621] 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 418/621] 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 419/621] 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 420/621] 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 462d45adf099622f3380f52f931ed30f5b1952b4 Mon Sep 17 00:00:00 2001 From: boykopovar <boykopovar@gmail.com> Date: Wed, 21 Oct 2026 00:09:21 +0300 Subject: [PATCH 421/621] 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 422/621] 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 423/621] 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 424/621] 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 425/621] 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 426/621] 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 427/621] 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 428/621] 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 429/621] 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 430/621] 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 431/621] 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 432/621] 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 433/621] 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 434/621] 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 435/621] 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 436/621] 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 437/621] 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 438/621] 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 439/621] 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 440/621] 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 441/621] 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 442/621] 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 443/621] 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 444/621] 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 445/621] 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 446/621] 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 447/621] 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 448/621] 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 449/621] 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 450/621] 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 451/621] 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 452/621] 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 453/621] 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 454/621] 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 455/621] 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 456/621] 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 457/621] 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 458/621] 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 459/621] 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 460/621] 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 461/621] 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 462/621] 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 463/621] 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 464/621] 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 465/621] 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 466/621] 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 467/621] 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 468/621] 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 469/621] 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 470/621] 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 471/621] 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 472/621] 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 473/621] 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 474/621] 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 475/621] 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 476/621] 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 477/621] 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 478/621] 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 479/621] 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 480/621] 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 481/621] 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 482/621] 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 483/621] 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 484/621] 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 485/621] 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 486/621] 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 487/621] 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 488/621] 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 489/621] 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 490/621] 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 491/621] 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 492/621] 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 493/621] 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 494/621] 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 495/621] 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 496/621] 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 497/621] 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 498/621] 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 499/621] 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 500/621] 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 501/621] 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 502/621] 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 503/621] 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 504/621] 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 505/621] 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 506/621] 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 507/621] 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 508/621] 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 509/621] 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 510/621] 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 511/621] 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 512/621] 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 513/621] 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 514/621] 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 515/621] 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 516/621] 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 517/621] 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 518/621] 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 519/621] 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 520/621] 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 521/621] 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 522/621] 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 523/621] 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 524/621] 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 525/621] 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 526/621] 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 527/621] 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 528/621] 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 529/621] 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 530/621] 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 531/621] 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 532/621] 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 533/621] 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 534/621] 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 535/621] 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 536/621] 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 537/621] 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 538/621] 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 539/621] 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 540/621] 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 541/621] 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 542/621] 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 543/621] 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 544/621] 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 545/621] 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 546/621] 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 547/621] 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 548/621] 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 549/621] 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 550/621] 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 551/621] 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 552/621] 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 553/621] 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 554/621] 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 555/621] 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 556/621] 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 557/621] 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 558/621] 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 559/621] 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 560/621] 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 561/621] 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 562/621] 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 563/621] 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 564/621] 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 565/621] 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 566/621] 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 567/621] 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 568/621] 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 569/621] 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 570/621] 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 571/621] 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 572/621] 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 573/621] 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 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 574/621] 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 575/621] 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 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 576/621] 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 577/621] 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 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 578/621] 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 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 579/621] 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 580/621] 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 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 581/621] 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 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 582/621] 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 583/621] 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 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 584/621] 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 585/621] 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 586/621] 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 587/621] 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 588/621] 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 589/621] 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 590/621] 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 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 591/621] 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 592/621] 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 593/621] 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 594/621] 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 595/621] 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 596/621] 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 597/621] 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 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 598/621] 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 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 599/621] 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 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 600/621] 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 601/621] 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 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 602/621] 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 603/621] 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 604/621] 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 605/621] 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 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 606/621] 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 607/621] 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 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 608/621] 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 4561711b753a913f44d9114398e24a0c79fb4ada Mon Sep 17 00:00:00 2001 From: emirplus <gmsplemir@gmail.com> Date: Mon, 19 Oct 2026 19:45:31 +0300 Subject: [PATCH 609/621] 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 610/621] 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 611/621] 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 612/621] 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 1bf43f9b02a7e544cc332185598e4deb99035361 Mon Sep 17 00:00:00 2001 From: LeandroLP23 <leandroplacidi@gmail.com> Date: Tue, 20 Oct 2026 17:10:37 +0200 Subject: [PATCH 613/621] 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 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 614/621] 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 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 615/621] 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 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 616/621] 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 617/621] 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 618/621] 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 619/621] 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 620/621] 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 621/621] 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));