From 0ae412394e9cb1793d8986a924a4144c3df5681d Mon Sep 17 00:00:00 2001 From: David Date: Thu, 15 Oct 2026 18:53:40 +0400 Subject: [PATCH 001/269] fix(relinker): convert stack-guard and thread-pointer reads with an empty PT_TLS for macOS --- core/relinker/elfpatcher/src/macos/MacOsImage.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/relinker/elfpatcher/src/macos/MacOsImage.cpp b/core/relinker/elfpatcher/src/macos/MacOsImage.cpp index 838d0d6c8b..47d0211f15 100644 --- a/core/relinker/elfpatcher/src/macos/MacOsImage.cpp +++ b/core/relinker/elfpatcher/src/macos/MacOsImage.cpp @@ -361,7 +361,7 @@ std::vector WriteMacOsImage(MacOsImageInput& input) { auto& image = *input.Image; const auto& headers = *input.Headers; const auto tls = ScanTls(*input.Source, headers, image); - const bool hasTls = tls.Tls != nullptr && tls.Tls->MemorySize != 0; + const bool hasTls = tls.Tls != nullptr && (tls.Tls->MemorySize != 0 || !tls.Accesses.empty()); if (!hasTls && !tls.Accesses.empty()) throw Domain::RelinkerException("Guest TLS access without a non-empty PT_TLS"); if (!hasTls && !input.TlsModuleSlots.empty()) throw Domain::RelinkerException("Guest TLS module relocation without a non-empty PT_TLS"); From f6a903a8d478c24e452b07c1c8793eb6bb73f96d Mon Sep 17 00:00:00 2001 From: David Date: Thu, 15 Oct 2026 18:53:42 +0400 Subject: [PATCH 002/269] test(relinker): convert stack-guard and thread-pointer reads with an empty PT_TLS for macOS --- core/relinker/CMakeLists.txt | 3 ++ .../relinker/tests/test_macos_empty_tls.py | 51 +++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 core/relinker/relinker/tests/test_macos_empty_tls.py diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 4660de2490..fcabf845d3 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -435,6 +435,9 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_linux_executable_exports.py $) add_test(NAME executable_tls_relocations COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_executable_tls_relocations.py $) + add_test(NAME macos_empty_tls COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_macos_empty_tls.py $) + set_tests_properties(macos_empty_tls PROPERTIES TIMEOUT 60) add_test(NAME linux_entry_argv COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_linux_entry_argv.py $) add_test(NAME linux_fixed_base COMMAND ${Python3_EXECUTABLE} diff --git a/core/relinker/relinker/tests/test_macos_empty_tls.py b/core/relinker/relinker/tests/test_macos_empty_tls.py new file mode 100644 index 0000000000..c0c20be29d --- /dev/null +++ b/core/relinker/relinker/tests/test_macos_empty_tls.py @@ -0,0 +1,51 @@ +"""Check `relinker --macos` on guests whose PT_TLS is empty.""" +from pathlib import Path +import struct +import subprocess +import sys +import tempfile + +from test_macos_output import MachO +from test_tls_function_coverage import make_image + + +def empty_tls_image(body): + image = make_image("register", "unwind", body=body + b"\xc3") + struct.pack_into(" Date: Thu, 15 Oct 2026 18:53:44 +0400 Subject: [PATCH 003/269] test(relinker): keep refusing thread-local data reads with an empty PT_TLS for macOS --- core/relinker/relinker/tests/test_macos_empty_tls.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/relinker/relinker/tests/test_macos_empty_tls.py b/core/relinker/relinker/tests/test_macos_empty_tls.py index c0c20be29d..989b1a47be 100644 --- a/core/relinker/relinker/tests/test_macos_empty_tls.py +++ b/core/relinker/relinker/tests/test_macos_empty_tls.py @@ -39,11 +39,19 @@ def check_converted(relinker, directory, name, instruction): raise SystemExit(f"{name}: the %fs access was left in the code") +def check_tls_data_refused(relinker, directory): + result, output = convert(relinker, directory, "empty-tls-data", empty_tls_image(bytes.fromhex("64 48 8b 04 25 f8 ff ff ff"))) + message = result.stdout + result.stderr + if result.returncode != 2 or "is not in the thread TLS block" not in message or output.exists(): + raise SystemExit(f"empty-tls-data: a read of thread-local data with an empty PT_TLS was not refused: {message}") + + def main(): relinker = Path(sys.argv[1]).resolve() with tempfile.TemporaryDirectory(prefix="anyps5-macos-empty-tls-") as directory: check_converted(relinker, directory, "empty-guard", bytes.fromhex("64 48 8b 04 25 28 00 00 00")) check_converted(relinker, directory, "empty-thread-pointer", bytes.fromhex("64 48 8b 04 25 00 00 00 00")) + check_tls_data_refused(relinker, directory) print("macOS empty TLS checks passed") From 14fb5709dea088aa0e1b608f19a9f4e7107e577d Mon Sep 17 00:00:00 2001 From: David Date: Thu, 15 Oct 2026 18:53:46 +0400 Subject: [PATCH 004/269] test(relinker): give an empty PT_TLS without accesses no macOS TLS descriptor --- core/relinker/relinker/tests/test_macos_empty_tls.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/core/relinker/relinker/tests/test_macos_empty_tls.py b/core/relinker/relinker/tests/test_macos_empty_tls.py index 989b1a47be..7d4619ec23 100644 --- a/core/relinker/relinker/tests/test_macos_empty_tls.py +++ b/core/relinker/relinker/tests/test_macos_empty_tls.py @@ -46,12 +46,23 @@ def check_tls_data_refused(relinker, directory): raise SystemExit(f"empty-tls-data: a read of thread-local data with an empty PT_TLS was not refused: {message}") +def check_no_access(relinker, directory): + result, output = convert(relinker, directory, "empty-no-access", empty_tls_image(b"")) + if result.returncode != 0: + raise SystemExit(f"empty-no-access: relinker failed: {result.stdout}{result.stderr}") + data = output.read_bytes() + meta = struct.unpack_from("<12Q", data, MachO(data).sections[("__APS5DATA", "__meta")][2]) + if meta[4] != 0: + raise SystemExit(f"empty-no-access: an empty PT_TLS without accesses got a TLS descriptor {meta[4:9]}") + + def main(): relinker = Path(sys.argv[1]).resolve() with tempfile.TemporaryDirectory(prefix="anyps5-macos-empty-tls-") as directory: check_converted(relinker, directory, "empty-guard", bytes.fromhex("64 48 8b 04 25 28 00 00 00")) check_converted(relinker, directory, "empty-thread-pointer", bytes.fromhex("64 48 8b 04 25 00 00 00 00")) check_tls_data_refused(relinker, directory) + check_no_access(relinker, directory) print("macOS empty TLS checks passed") From 0f0197c16d09ca16e205676cbfa7655158dbfd4f Mon Sep 17 00:00:00 2001 From: AlxcBTW <187763517+AlxcBTW@users.noreply.github.com> Date: Thu, 15 Oct 2026 20:02:38 +0200 Subject: [PATCH 005/269] fix(agc): rate-limit the deferred report of an unaligned BDA access A shader that records an unaligned BDA access on every dispatch formatted and printed one line per completion. CheckFault now throws BdaUnalignedAccess for that fault, and the recorder prints the first 8 deferred reports of it whole, then every 1024th with the count. Every other deferred failure is still printed each time. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 6 ++ .../Graphics/include/Context.hpp | 4 ++ .../Graphics/src/BdaResources.cpp | 1 + .../libSceAgcDriver/Graphics/src/Recorder.cpp | 6 ++ .../prx/libSceAgcDriver/tests/Recorder.cpp | 66 +++++++++++++++++++ 5 files changed, 83 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 051af9c6e5..e30690305b 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1105,6 +1105,12 @@ if(BUILD_TESTING) endif() add_test(NAME agc_recorder_read_list COMMAND agc_driver_recorder_tests --read-list-only) set_tests_properties(agc_recorder_read_list PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + add_test(NAME agc_recorder_deferred_failure_report COMMAND agc_driver_recorder_tests --deferred-failure-report-only) + set_tests_properties(agc_recorder_deferred_failure_report PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_recorder_deferred_failure_report PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test(NAME agc_space_pending_writes COMMAND agc_driver_recorder_tests --space-pending-writes-only) set_tests_properties(agc_space_pending_writes PROPERTIES SKIP_RETURN_CODE 77) if(WIN32) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp index cba5321c23..51d48efa9b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp @@ -78,6 +78,10 @@ struct DescriptorRejected : std::runtime_error { using std::runtime_error::runtime_error; }; +struct BdaUnalignedAccess : std::runtime_error { + using std::runtime_error::runtime_error; +}; + // Device entry points resolved once per device (VulkanDevice's State fills it after setup): the // loader's vkGetDeviceProcAddr is a name lookup under its global mutex per call, paid at every // record site otherwise. Null members (tests, APS5_NO_PROC_TABLE=1) resolve per call (Resolved). diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp index 148b5e4f55..2adfde222f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp @@ -292,6 +292,7 @@ void BdaResources::CheckFault() const { break; } } + if (report.reason == ShaderRecompiler::BdaAbi::FaultReason::Unaligned) throw BdaUnalignedAccess(message.str()); throw std::runtime_error(message.str()); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp index b494d51ffe..80b7feca40 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp @@ -3827,6 +3827,12 @@ void Recorder::finish(std::unique_ptr batch, bool wait, int source, bool for (auto& action : batch->completions) { try { action(); + } catch (const BdaUnalignedAccess& error) { + static std::atomic failures{0}; + constexpr std::uint64_t kDeferredFailureFirst = 8, kDeferredFailureEvery = 1024; + const auto count = failures.fetch_add(1, std::memory_order_relaxed) + 1; + if (count <= kDeferredFailureFirst) std::fprintf(stderr, "[gpu] deferred write-back failed: %s\n", error.what()); + else if (count % kDeferredFailureEvery == 0) std::fprintf(stderr, "[gpu] deferred write-back failed with an unaligned BDA access %llu times so far; latest: %s\n", static_cast(count), error.what()); } catch (const std::exception& error) { std::fprintf(stderr, "[gpu] deferred write-back failed: %s\n", error.what()); } diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 040c2dd195..5285a5d67c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -41,6 +41,7 @@ #ifndef NOMINMAX #define NOMINMAX #endif +#include #include #else #include @@ -53,6 +54,7 @@ #include #include #include +#include #include #include #include @@ -1024,6 +1026,65 @@ void completionCountTests(const Device& device, Recorder& recorder) { Require(memory[0] == 1 && memory[2] == (storeAlways ? 1u : 7u) && memory[16] == 1, "completion stores ran for the wrong labels (overlapped and not-imported ones store, the untouched GPU-stored one skips)"); } +class StderrCapture { +public: + StderrCapture() { + std::fflush(stderr); + file = std::tmpfile(); + Require(file != nullptr, "cannot open a temporary file for stderr"); +#ifdef _WIN32 + saved = _dup(_fileno(stderr)); + _dup2(_fileno(file), _fileno(stderr)); +#else + saved = dup(fileno(stderr)); + dup2(fileno(file), fileno(stderr)); +#endif + } + + std::string Finish() { + std::fflush(stderr); +#ifdef _WIN32 + _dup2(saved, _fileno(stderr)); + _close(saved); +#else + dup2(saved, fileno(stderr)); + close(saved); +#endif + std::rewind(file); + std::string text; + char buffer[4096]; + for (std::size_t read = 0; (read = std::fread(buffer, 1, sizeof(buffer), file)) != 0;) text.append(buffer, read); + std::fclose(file); + std::fwrite(text.data(), 1, text.size(), stderr); + return text; + } + +private: + std::FILE* file = nullptr; + int saved = -1; +}; + +std::size_t DeferredFailureLines(Recorder& recorder, std::size_t count, const std::function& failure, std::string* log = nullptr) { + StderrCapture capture; + for (std::size_t index = 0; index < count; ++index) recorder.OnComplete(failure); + recorder.Sync(); + const auto text = capture.Finish(); + if (log != nullptr) *log = text; + std::size_t lines = 0; + for (auto at = text.find("deferred write-back failed"); at != std::string::npos; at = text.find("deferred write-back failed", at + 1)) ++lines; + return lines; +} + +void deferredFailureReportTests(Recorder& recorder) { + const auto plain = [] { throw std::runtime_error("image table: poisoned entry"); }; + const auto unaligned = [] { throw BdaUnalignedAccess("BDA access failed: unaligned"); }; + Require(DeferredFailureLines(recorder, 12, plain) == 12, "a deferred failure other than an unaligned BDA access was not printed every time"); + Require(DeferredFailureLines(recorder, 12, unaligned) == 8, "the first 8 deferred reports of an unaligned BDA access were not the only ones printed"); + std::string log; + Require(DeferredFailureLines(recorder, 1012, unaligned, &log) == 1 && log.find("1024 times so far") != std::string::npos, "the 1024th deferred report of an unaligned BDA access was not the only one printed"); + Require(DeferredFailureLines(recorder, 12, plain) == 12, "unaligned BDA reports limited the printing of other deferred failures"); +} + void directCompletionLabelTests(Recorder& recorder) { using namespace AgcDriver::GuestMemory; constexpr std::size_t size = 65536; @@ -8113,6 +8174,11 @@ int main(int argc, char** argv) { std::cout << "Read list tests passed\n"; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--deferred-failure-report-only") { + deferredFailureReportTests(recorder); + std::cout << "Deferred failure report tests passed\n"; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--host-import-unmap-only") { gpu.unlock(); hostImportUnmapTests(device, recorder); From c0588d274720d468fef49d241f6fcf1295429715 Mon Sep 17 00:00:00 2001 From: AlxcBTW <187763517+AlxcBTW@users.noreply.github.com> Date: Thu, 15 Oct 2026 20:02:38 +0200 Subject: [PATCH 006/269] fix(agc): write copied buffers back before reporting an unaligned BDA access WriteBackBuffers threw the fault before guestMemory.WriteBack(), so a dispatch that recorded an unaligned BDA access lost the stores of its other invocations, although the shader only skips that access. The write-back now happens first and the report follows. A fault of any other kind still fails the command before anything is published. --- .../Graphics/src/ShaderResources.cpp | 14 ++++++++++--- .../libSceAgcDriver/tests/BdaResources.cpp | 20 +++++++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index e31084e0c4..50b05e2cc1 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" @@ -4698,9 +4699,16 @@ void ShaderResources::CheckFaults() const { } void ShaderResources::WriteBackBuffers() { - if (bda) bda->CheckFault(); - if (SkipWriteBack()) return; - guestMemory.WriteBack(); + std::exception_ptr unaligned; + if (bda) { + try { + bda->CheckFault(); + } catch (const BdaUnalignedAccess&) { + unaligned = std::current_exception(); + } + } + if (!SkipWriteBack()) guestMemory.WriteBack(); + if (unaligned) std::rethrow_exception(unaligned); } bool ShaderResources::ReadsOverlap(std::uint64_t address, std::size_t bytes) const { diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index 2fe20f100c..c05abd1460 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -1689,6 +1689,26 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { reject([&] { resources.WriteBack(); }, "rect-list requires"); Require(guest[0] == 123, "failed GPU command published writes"); } + { + const auto fail = [&](ShaderRecompiler::BdaAbi::FaultReason reason, std::uint32_t word, std::uint32_t value) { + ShaderResources resources(context, compiled); + std::memcpy(access.bytes(access.descriptor(6).buffer).data() + word * sizeof(std::uint32_t), &value, sizeof(value)); + const ShaderRecompiler::BdaAbi::Fault report{ShaderRecompiler::BdaAbi::FaultState::Ready, reason, 0x7fff99880002ULL, 4, 0, 0x5c8, 0}; + std::memcpy(access.bytes(access.descriptor(5).buffer).data(), &report, sizeof(report)); + try { + resources.WriteBack(); + } catch (const BdaUnalignedAccess& error) { + Require(std::string(error.what()).find("BDA access failed") != std::string::npos, "an unaligned BDA access did not report the failed access"); + return true; + } catch (const std::runtime_error& error) { + Require(std::string(error.what()).find("BDA access failed") != std::string::npos, "a BDA fault did not report the failed access"); + return false; + } + throw std::runtime_error("a recorded BDA fault was not reported"); + }; + Require(fail(ShaderRecompiler::BdaAbi::FaultReason::Unaligned, 2, 777) && guest[2] == 777, "an unaligned BDA access lost the stores of the dispatch's other invocations"); + Require(!fail(ShaderRecompiler::BdaAbi::FaultReason::Unmapped, 3, 888) && guest[3] == 0, "a failed GPU command published writes"); + } { auto aligned = context; aligned.limits.minStorageBufferOffsetAlignment = 16; From 20a3a38844fe5b143dfc6aaa3452ddad3ac64f9a Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Thu, 15 Oct 2026 22:25:27 +0200 Subject: [PATCH 007/269] fix(decoder): release decoded pixels when allocation fails --- core/Decoder/Jpeg/src/Jpeg.cpp | 7 ++++--- core/Decoder/Png/src/Png.cpp | 6 +++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/core/Decoder/Jpeg/src/Jpeg.cpp b/core/Decoder/Jpeg/src/Jpeg.cpp index d446841cc4..7bd331ba91 100644 --- a/core/Decoder/Jpeg/src/Jpeg.cpp +++ b/core/Decoder/Jpeg/src/Jpeg.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #define STB_IMAGE_STATIC @@ -97,12 +98,12 @@ std::optional Decode(std::span jpeg) { int width = 0; int height = 0; int channels = 0; - stbi_uc* decoded = stbi_load_from_memory(jpeg.data(), static_cast(jpeg.size()), &width, &height, &channels, 0); + const std::unique_ptr decoded( + stbi_load_from_memory(jpeg.data(), static_cast(jpeg.size()), &width, &height, &channels, 0), stbi_image_free); if (!decoded) return std::nullopt; Image image{static_cast(width), static_cast(height), static_cast(channels), {}}; - image.pixels.assign(decoded, decoded + static_cast(image.width) * image.height * image.channels); - stbi_image_free(decoded); + image.pixels.assign(decoded.get(), decoded.get() + static_cast(image.width) * image.height * image.channels); return image; } diff --git a/core/Decoder/Png/src/Png.cpp b/core/Decoder/Png/src/Png.cpp index cb04f25bd5..434a589d6c 100644 --- a/core/Decoder/Png/src/Png.cpp +++ b/core/Decoder/Png/src/Png.cpp @@ -152,12 +152,12 @@ std::optional Decode(std::span png) { int width = 0; int height = 0; int channels = 0; - stbi_uc* decoded = stbi_load_from_memory(png.data(), static_cast(png.size()), &width, &height, &channels, 4); + const std::unique_ptr decoded( + stbi_load_from_memory(png.data(), static_cast(png.size()), &width, &height, &channels, 4), stbi_image_free); if (!decoded) return std::nullopt; Image image{static_cast(width), static_cast(height), {}}; - image.pixels.assign(decoded, decoded + static_cast(image.width) * image.height * 4); - stbi_image_free(decoded); + image.pixels.assign(decoded.get(), decoded.get() + static_cast(image.width) * image.height * 4); return image; } From 3659e9a99a1b221bf4701bd5e43620f99e89be0f Mon Sep 17 00:00:00 2001 From: XairRrR Date: Fri, 16 Oct 2026 00:54:44 +0200 Subject: [PATCH 008/269] fix(agc): rasterize nothing for an attachment-free draw with an empty screen scissor A draw without color or depth targets takes its framebuffer extent from PA_SC_SCREEN_SCISSOR_BR. When that rectangle is empty the hardware still runs the vertex stage but produces no pixels; it no longer throws. The framebuffer is one pixel in the empty dimension and the screen scissor clips it to nothing. AI-assisted: yes --- core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp | 3 +-- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 7 ++++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index b767129e68..ae22d84b6f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -1134,9 +1134,8 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { } else { const auto screenBottomRight = read(cx, 0xd); APS5_LOG_OUT_DEBUG("No color target, screen BR register=0x%x", screenBottomRight); - result.renderExtent = {screenBottomRight & 0xffffu, screenBottomRight >> 16u}; + result.renderExtent = {std::max(screenBottomRight & 0xffffu, 1u), std::max(screenBottomRight >> 16u, 1u)}; APS5_LOG_OUT_DEBUG("Render extent from screen=%ux%u", result.renderExtent.width, result.renderExtent.height); - Require(result.renderExtent.width != 0 && result.renderExtent.height != 0, "empty framebuffer extent for a draw without color writes"); } result.quadSampleMask = decodeQuadSampleMask(cx, result.samples); result.pixelEarlyZ = !PixelProgramSkipped(queue) && PixelEarlyZ(read(cx, 0x203)); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 2e1d26e11f..6e4d2cf51a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -1309,7 +1309,12 @@ void DisabledColorTests() { Require(!state.hasColorTarget && state.color.address == 0 && state.color.bytes == 0, "disabled color writes accessed a color surface"); Require(state.renderExtent.width == 64 && state.renderExtent.height == 4, "attachment-free framebuffer lost screen scissor extent"); queue.context[0xd] = 0; - expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "empty framebuffer extent"); + const auto emptyScreen = AgcDriver::Graphics::DecodeState(queue); + Require(emptyScreen.renderExtent.width == 1 && emptyScreen.renderExtent.height == 1, "an empty screen scissor without targets did not get a one-pixel framebuffer"); + Require(emptyScreen.scissor.extent.width == 0 && emptyScreen.scissor.extent.height == 0, "an empty screen scissor without targets rasterized pixels"); + queue.context[0xd] = 64u; + const auto emptyRows = AgcDriver::Graphics::DecodeState(queue); + Require(emptyRows.renderExtent.width == 64 && emptyRows.scissor.extent.height == 0, "a screen scissor without rows rasterized pixels"); queue = makeState(); queue.context[0x8e] = 3; const auto partial = AgcDriver::Graphics::DecodeState(queue); From d8e7dd825648c1eed4882493c18e379ce5f3fd6a Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Fri, 16 Oct 2026 16:34:08 +0000 Subject: [PATCH 009/269] fix(agc): keep a data hit's live words with the regions that span them A dispatch-cache data hit copies the variant's words into a local liveWords, patches the data words and builds its captured regions as spans over that buffer. When the dispatch is resolved ahead (every compute queue by default), the parked entry copied both captured and liveWords: the copied regions still pointed into the local buffer, which was freed on return. The adopting dispatch then built its snapshots from those regions, and an address-based program compared the freed words in AddSnapshot (an ASan heap-use-after-free; the plain build reports "guest snapshot differs from registered memory"). Moving both into the parked entry keeps the buffer the regions point into. The pipelined dispatch item had the same shape: its snapshots span the local liveWords but the closure did not keep it. Capturing it by move keeps the item self-contained. DispatchDataHitWords submits an address-based dispatch whose constant changes every round on a compute queue, so each data hit is resolved ahead and adopted. --- .../src/Driver/Dispatch/Dispatch.cpp | 6 +- .../tests/execution/DispatchDataHitWords.cpp | 147 ++++++++++++++++++ 2 files changed, 150 insertions(+), 3 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/DispatchDataHitWords.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp index b506cff5c1..13defd021f 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp @@ -383,8 +383,8 @@ void Driver::dispatch(QueueState& queue, std::span packet, parked.keepVariant = keepVariant; parked.attachVariant = attachVariant; parked.shaderMemory = shaderMemory; - parked.captured = captured; - parked.liveWords = liveWords; + parked.captured = std::move(captured); + parked.liveWords = std::move(liveWords); parked.capture = capture; parked.dataHit = dataHit; parked.cached = cached; @@ -444,7 +444,7 @@ 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), 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] { DrawPipeline::FollowEpoch(epoch); GuestMemory::SetCurrentPacket(0x15, queueId); const auto& compiledCommit = *result; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DispatchDataHitWords.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DispatchDataHitWords.cpp new file mode 100644 index 0000000000..82eb381216 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DispatchDataHitWords.cpp @@ -0,0 +1,147 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Submit/include/Acb.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/Shutdown.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr std::uint32_t Lanes = 32; +constexpr std::uint32_t BufferBytes = Lanes * 4u; +constexpr std::uint32_t ComputeQueue = 0x20; +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t Unwritten = 0xcafebabeu; + +std::uint32_t* Constants = nullptr; +std::uint32_t* Input = nullptr; +std::uint32_t* Output = nullptr; + +alignas(256) constexpr std::array AddressCode{0xf4000200u, 0xfa000000u, 0x34020082u, 0xdc308000u, 0x03020001u, 0xbf8cc07fu, 0x7e040208u, 0x4a040481u, 0xbf8c3f70u, 0x4a040503u, 0xe0701000u, 0x80010201u, 0xbf810000u}; + +std::uint32_t* NewBlock() { +#ifdef _WIN32 + auto* block = static_cast(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + auto* block = static_cast(std::aligned_alloc(BlockBytes, BlockBytes)); +#endif + if (block == nullptr) throw std::runtime_error("dispatch data hit words: cannot allocate a guest block"); + std::memset(block, 0, BlockBytes); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + return block; +} + +void check(bool condition, const std::string& what) { + if (!condition) throw std::runtime_error("dispatch data hit words: " + what); +} + +template +void WaitUntil(Ready ready, const std::string& what) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + while (!ready()) { + if (std::chrono::steady_clock::now() >= deadline) throw std::runtime_error("dispatch data hit words: timed out waiting for " + what); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } +} + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::vector Dispatch() { + const auto codeAddress = reinterpret_cast(AddressCode.data()); + const auto constants = reinterpret_cast(Constants); + const auto input = reinterpret_cast(Input); + const auto output = BufferDescriptor(Output, BufferBytes); + return { + 0xc0027600u, 0x20cu, static_cast(codeAddress >> 8u), static_cast(codeAddress >> 40u), + 0xc0037600u, 0x207u, Lanes, 1u, 1u, + 0xc0017600u, 0x213u, 8u << 1u, + 0xc0087600u, 0x240u, static_cast(constants), static_cast(constants >> 32u), static_cast(input), static_cast(input >> 32u), output[0], output[1], output[2], output[3], + 0xc0031500u, 1u, 1u, 1u, 0x8041u, + }; +} + +void SetConstant(std::uint32_t value) { + for (std::uint32_t lane = 0; lane < Lanes; ++lane) Constants[lane] = value; + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(Constants), BufferBytes); +} + +void CheckOutput(std::uint32_t expected, const std::string& what) { + WaitUntil([] { + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(Output), BufferBytes); + for (std::uint32_t lane = 0; lane < Lanes; ++lane) { + if (std::atomic_ref(Output[lane]).load(std::memory_order_acquire) == Unwritten) return false; + } + return true; + }, what + " to store"); + for (std::uint32_t lane = 0; lane < Lanes; ++lane) check(Output[lane] == expected, what + ": lane " + std::to_string(lane) + " is " + std::to_string(Output[lane]) + ", expected " + std::to_string(expected)); +} + +void Round(std::uint32_t value, const std::string& what) { + SetConstant(value); + for (std::uint32_t lane = 0; lane < Lanes; ++lane) Output[lane] = Unwritten; + auto words = Dispatch(); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + check(sceAgcDriverSubmitAcb(ComputeQueue, &packet) == 0, "submission failed"); + AgcDriverWaitIdle_nid_postfix(); + CheckOutput(value + 1u, what); +} + +} + +int main() { + try { + { + const auto probe = OpenVulkanTestDevice(); + if (!probe) return VulkanTestSkipped; + } + + Shader shader{}; + shader.file_header = 0x34333231; + shader.version = 0x18; + shader.header_size = sizeof(Shader); + shader.shader_size = sizeof(AddressCode); + shader.code = AddressCode.data(); + AgcDriverRegisterShader_nid_postfix(&shader); + + Constants = NewBlock(); + Input = NewBlock(); + Output = NewBlock(); + + Round(0x11112222u, "the first dispatch"); + const std::array values{0x33334444u, 0x55556666u, 0x7a7b7c7du, 0x0f1e2d3cu}; + for (std::size_t round = 0; round < values.size(); ++round) { + Round(values[round], "data hit " + std::to_string(round + 1) + ", resolved ahead on the compute queue"); + } + std::printf("dispatch data hit words: %zu data hits resolved ahead read their own live words\n", values.size()); + + LibcRunShutdown_nid_postfix(); + std::puts("dispatch data hit words tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + try { LibcRunShutdown_nid_postfix(); } catch (...) {} + return 1; + } +} From f5fd23ee3b53b8bd5fcfb765d51b5244a968cb46 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Fri, 16 Oct 2026 17:02:18 +0000 Subject: [PATCH 010/269] test(agc): fail the data hit test on the snapshot mismatches it causes A plain build passes DispatchDataHitWords without the fix: AddSnapshot compares the freed words with the live region, prints "guest snapshot differs from registered memory" and lets the live region serve the GPU, so the output is right. With APS5_SNAPSHOT_CHECK_FATAL=1 the throw lands in the resolved-ahead rethrow, which re-runs the dispatch, and it still passes. AddSnapshot already counts those mismatches for its report. The count now lives with the other snapshot compare counters, and SnapshotMismatches() reads it. The test requires each data hit to add none. --- .../Graphics/include/GuestBufferMemory.hpp | 1 + .../libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp | 8 ++++++-- .../tests/execution/DispatchDataHitWords.cpp | 7 ++++++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp index 7f932aad60..08bc494e8a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp @@ -191,6 +191,7 @@ struct MirrorStats { std::uint64_t sparseBinds = 0; }; MirrorStats MirrorCounters(); +std::uint64_t SnapshotMismatches(); void ClearSnapshotCopies(VkDevice device); std::vector AddressTableKey(std::span> writes, std::span own); void ClearImageMirrors(VkDevice device); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 3aa4c9b378..a13d7a1ed3 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -2031,6 +2031,7 @@ LeaseState& Leases() { struct SnapshotStats { std::atomic checked{0}; std::atomic skipped{0}; + std::atomic mismatched{0}; }; SnapshotStats& Snapshots() { @@ -2256,6 +2257,10 @@ MirrorStats MirrorCounters() { return stats; } +std::uint64_t SnapshotMismatches() { + return Snapshots().mismatched.load(std::memory_order_relaxed); +} + std::vector AddressTableKey(std::span> writes, std::span own) { std::vector key; key.reserve(writes.size() * 2u + 1u + own.size() * 4u); @@ -3155,10 +3160,9 @@ void GuestBufferMemory::AddSnapshot(const GuestMemorySnapshot& snapshot) { static const bool fatal = std::getenv("APS5_SNAPSHOT_CHECK_FATAL") != nullptr; if (!live || fatal) throw std::runtime_error(text); static std::mutex reportMutex; - static std::uint64_t mismatches = 0; static auto lastReport = std::chrono::steady_clock::now() - std::chrono::seconds(60); std::lock_guard lock(reportMutex); - ++mismatches; + const auto mismatches = Snapshots().mismatched.fetch_add(1, std::memory_order_relaxed) + 1; const auto now = std::chrono::steady_clock::now(); if (now - lastReport >= std::chrono::seconds(10)) { lastReport = now; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DispatchDataHitWords.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DispatchDataHitWords.cpp index 82eb381216..c0d7cc48b9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DispatchDataHitWords.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DispatchDataHitWords.cpp @@ -1,5 +1,6 @@ #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" #include "prx/libSceAgcDriver/Submit/include/Acb.hpp" #include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" #include "prx/libc/include/GuestAllocations.hpp" @@ -132,7 +133,11 @@ int main() { Round(0x11112222u, "the first dispatch"); const std::array values{0x33334444u, 0x55556666u, 0x7a7b7c7du, 0x0f1e2d3cu}; for (std::size_t round = 0; round < values.size(); ++round) { - Round(values[round], "data hit " + std::to_string(round + 1) + ", resolved ahead on the compute queue"); + const auto mismatches = AgcDriver::Graphics::SnapshotMismatches(); + const auto what = "data hit " + std::to_string(round + 1) + ", resolved ahead on the compute queue"; + Round(values[round], what); + const auto added = AgcDriver::Graphics::SnapshotMismatches() - mismatches; + check(added == 0, what + ": its snapshots differ from the registered memory serving them (" + std::to_string(added) + " snapshot mismatches)"); } std::printf("dispatch data hit words: %zu data hits resolved ahead read their own live words\n", values.size()); From 6b14c31437d57b7267512d1f39b5de74a6ad4033 Mon Sep 17 00:00:00 2001 From: Zinebyte <197974009+Zinebyte@users.noreply.github.com> Date: Fri, 16 Oct 2026 13:30:33 -0400 Subject: [PATCH 011/269] fix(relinker): infer guest dependencies from referenced imports --- core/relinker/CMakeLists.txt | 4 + .../relinker/src/guest/GuestModuleBuilder.cpp | 9 +- .../test_guest_unused_import_dependencies.py | 119 ++++++++++++++++++ 3 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 core/relinker/relinker/tests/test_guest_unused_import_dependencies.py diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index d227fc017d..75484ae962 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -517,6 +517,10 @@ if(BUILD_TESTING) add_test(NAME guest_deferred_dependencies COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_deferred_dependencies.py $ $) + add_test(NAME guest_unused_import_dependencies COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_unused_import_dependencies.py + $ $) + set_property(TEST guest_unused_import_dependencies APPEND PROPERTY LABELS relinker) add_test(NAME guest_absolute_symbols COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_absolute_symbols.py $) add_test(NAME guest_module_identity COMMAND ${Python3_EXECUTABLE} diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index 33cc5bdfc2..ab8ff81d0f 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -219,6 +219,13 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path const auto found = findGuest(name); if (found != guestNames.end() && found->second != index) dependencies[index].insert(found->second); } + std::set referencedSymbols; + for (const auto* table : {&images[index].Dynamic.RelaData, &images[index].Dynamic.RelaPltData}) { + for (std::size_t offset = 0; offset < table->size(); offset += 24) { + const auto info = Io::ReadU64(*table, offset + 8); + if ((info & 0xffffffffu) != 0) referencedSymbols.insert(info >> 32); + } + } for (std::size_t symbolIndex = 0; symbolIndex < images[index].Symbols.size(); ++symbolIndex) { const auto& symbol = images[index].Symbols[symbolIndex]; if (symbol.Section != 0 || symbol.Name.empty()) continue; @@ -241,7 +248,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path } if (provider != nullptr) { if ((symbol.Info & 15) != 0 && (symbol.Info & 15) != provider->Type) throw Domain::RelinkerException("Guest import/export type mismatch: " + symbol.Name); - if (provider->Provider != index) dependencies[index].insert(provider->Provider); + if (provider->Provider != index && referencedSymbols.contains(symbolIndex)) dependencies[index].insert(provider->Provider); } else if (windows && (symbol.Info & 15) == 6) throw Domain::RelinkerException("Windows guest TLS import requires a guest TLS export: " + symbol.Name); if (provider == nullptr) systemImports[index].insert(symbolIndex); } diff --git a/core/relinker/relinker/tests/test_guest_unused_import_dependencies.py b/core/relinker/relinker/tests/test_guest_unused_import_dependencies.py new file mode 100644 index 0000000000..f3054ee7bd --- /dev/null +++ b/core/relinker/relinker/tests/test_guest_unused_import_dependencies.py @@ -0,0 +1,119 @@ +from pathlib import Path +import os +import shutil +import struct +import subprocess +import sys +import tempfile + +from guest_import_fixtures import dynamic +from test_guest_deferred_dependencies import page_aligned +from test_guest_intel_trampolines import main_fixture +from test_guest_module_args import args_module +from test_guest_module_directories import module_with_symbol, needed_libraries +from test_guest_module_dir import pe_export_names +from test_guest_needed_modules import module_with_needed +from test_guest_symbol_names import dynamic_tags +from test_macos_output import MachO + + +def consumer(kind): + image = module_with_symbol(False) + tags = dynamic_tags(image) + if kind in ('unreferenced', 'weak-unreferenced', 'needed'): + tags[8] = 0 + elif kind == 'noop': + struct.pack_into(' Date: Fri, 16 Oct 2026 19:56:19 +0200 Subject: [PATCH 012/269] fix(agc): publish after an unaligned BDA access only when it is the only fault The fault record keeps the first fault of a dispatch, so an early unaligned access could hide a later unmapped or permission fault, and the write-back then published the stores of a dispatch that had a real access fault. The shader now also sets a sticky flag in the record for every fault of another kind. An unaligned record with that flag set fails the command like any other fault, before anything is published. --- .../Graphics/include/Context.hpp | 3 ++- .../Graphics/src/BdaResources.cpp | 9 ++++++--- .../prx/libSceAgcDriver/tests/BdaExecution.cpp | 18 ++++++++++++++++-- .../prx/libSceAgcDriver/tests/BdaResources.cpp | 5 +++-- .../prx/libSceAgcDriver/tests/BdaShader.cpp | 8 ++++++++ .../prx/libSceAgcDriver/tests/BdaShader.hpp | 3 +++ core/shader/recompiler/BdaAbi.hpp | 4 ++-- .../SpirvBackend/src/SpirvBdaFault.cpp | 6 +++++- 8 files changed, 45 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp index 51d48efa9b..7c12bc43c2 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp @@ -79,7 +79,8 @@ struct DescriptorRejected : std::runtime_error { }; struct BdaUnalignedAccess : std::runtime_error { - using std::runtime_error::runtime_error; + BdaUnalignedAccess(const std::string& message, std::uint32_t instruction) : std::runtime_error(message), instruction(instruction) {} + std::uint32_t instruction; }; // Device entry points resolved once per device (VulkanDevice's State fills it after setup): the diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp index 2adfde222f..bbeead8532 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp @@ -243,10 +243,10 @@ void BdaResources::CheckFault() const { ShaderRecompiler::BdaAbi::Fault report{}; std::memcpy(&report, fault->Bytes().data(), sizeof(report)); if (report.state == ShaderRecompiler::BdaAbi::FaultState::Empty) { - Require(static_cast(report.reason) == 0 && report.address == 0 && report.bytes == 0 && report.stage == 0 && report.instruction == 0 && report.reserved == 0, "BDA fault record has data without publication"); + Require(static_cast(report.reason) == 0 && report.address == 0 && report.bytes == 0 && report.stage == 0 && report.instruction == 0 && report.otherFaults == 0, "BDA fault record has data without publication"); return; } - Require(report.state == ShaderRecompiler::BdaAbi::FaultState::Ready && report.reserved == 0, "incomplete or invalid BDA fault record"); + Require(report.state == ShaderRecompiler::BdaAbi::FaultState::Ready && report.otherFaults <= 1, "incomplete or invalid BDA fault record"); Require(report.reason != ShaderRecompiler::BdaAbi::FaultReason::InvalidRectangle, "rect-list requires finite nondegenerate axis-aligned positions with equal positive W"); if (report.reason == ShaderRecompiler::BdaAbi::FaultReason::LoopLimit) { // APS5_LOOP_GUARD: the shader left a loop that ran past the guard; the dispatch result is kept. @@ -292,7 +292,10 @@ void BdaResources::CheckFault() const { break; } } - if (report.reason == ShaderRecompiler::BdaAbi::FaultReason::Unaligned) throw BdaUnalignedAccess(message.str()); + if (report.reason == ShaderRecompiler::BdaAbi::FaultReason::Unaligned) { + if (report.otherFaults == 0) throw BdaUnalignedAccess(message.str(), report.instruction); + message << "; a fault of another kind was recorded in the same dispatch"; + } throw std::runtime_error(message.str()); } diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaExecution.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaExecution.cpp index 2804e7e81b..a01b102037 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaExecution.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaExecution.cpp @@ -332,6 +332,20 @@ void RunBdaExecutionTests(const Context& context) { } ranges[0].permissions = 0; run(guest, 8, 0, Abi::FaultReason::Permission); + const auto sequence = [&](std::initializer_list reasons, Abi::FaultReason recorded, std::uint32_t otherFaults) { + const Abi::Header header{Abi::Version, 2, sizeof(Abi::Range), 0}; + std::memcpy(table.Bytes().data(), &header, sizeof(header)); + std::memcpy(table.Bytes().data() + sizeof(header), ranges.data(), sizeof(ranges)); + std::memset(fault.Bytes().data(), 0, fault.Bytes().size()); + Pipeline pipeline(context, MakeBdaFaultSequenceTestShader(reasons), {&table, &fault, &output}); + pipeline.Run(1); + Abi::Fault report{}; + std::memcpy(&report, fault.Bytes().data(), sizeof(report)); + Require(report.state == Abi::FaultState::Ready && report.reason == recorded && report.otherFaults == otherFaults, "BDA faults of one dispatch were not recorded with the first reason and the sticky other-fault flag"); + }; + sequence({Abi::FaultReason::Unaligned, Abi::FaultReason::Unaligned}, Abi::FaultReason::Unaligned, 0); + sequence({Abi::FaultReason::Unaligned, Abi::FaultReason::Unmapped}, Abi::FaultReason::Unaligned, 1); + sequence({Abi::FaultReason::Unmapped, Abi::FaultReason::Unaligned}, Abi::FaultReason::Unmapped, 1); RunBdaDwordWriteTests(context); } @@ -364,9 +378,9 @@ void RunBdaStoreExecutionTests(const Context& context) { Abi::Fault report{}; std::memcpy(&report, fault.Bytes().data(), sizeof(report)); if (reason == Abi::FaultReason{}) { - Require(report.state == Abi::FaultState::Empty && report.reason == Abi::FaultReason{} && report.address == 0 && report.bytes == 0 && report.stage == 0 && report.instruction == 0 && report.reserved == 0, label + "mapped store published a fault"); + Require(report.state == Abi::FaultState::Empty && report.reason == Abi::FaultReason{} && report.address == 0 && report.bytes == 0 && report.stage == 0 && report.instruction == 0 && report.otherFaults == 0, label + "mapped store published a fault"); } else { - Require(report.state == Abi::FaultState::Ready && report.reason == reason && report.address == faultAddress && report.bytes == faultBytes && report.stage == static_cast(ShaderRecompiler::IrShaderStage::Compute) && report.instruction == 0x1234 && report.reserved == 0, label + "incorrect first fault metadata"); + Require(report.state == Abi::FaultState::Ready && report.reason == reason && report.address == faultAddress && report.bytes == faultBytes && report.stage == static_cast(ShaderRecompiler::IrShaderStage::Compute) && report.instruction == 0x1234 && report.otherFaults == 1, label + "incorrect first fault metadata"); } std::array words{}; std::memcpy(words.data(), fault.Bytes().data(), sizeof(words)); diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index c05abd1460..17ea9fcda0 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -1690,10 +1690,10 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { Require(guest[0] == 123, "failed GPU command published writes"); } { - const auto fail = [&](ShaderRecompiler::BdaAbi::FaultReason reason, std::uint32_t word, std::uint32_t value) { + const auto fail = [&](ShaderRecompiler::BdaAbi::FaultReason reason, std::uint32_t word, std::uint32_t value, std::uint32_t otherFaults = 0) { ShaderResources resources(context, compiled); std::memcpy(access.bytes(access.descriptor(6).buffer).data() + word * sizeof(std::uint32_t), &value, sizeof(value)); - const ShaderRecompiler::BdaAbi::Fault report{ShaderRecompiler::BdaAbi::FaultState::Ready, reason, 0x7fff99880002ULL, 4, 0, 0x5c8, 0}; + const ShaderRecompiler::BdaAbi::Fault report{ShaderRecompiler::BdaAbi::FaultState::Ready, reason, 0x7fff99880002ULL, 4, 0, 0x5c8, otherFaults}; std::memcpy(access.bytes(access.descriptor(5).buffer).data(), &report, sizeof(report)); try { resources.WriteBack(); @@ -1708,6 +1708,7 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { }; Require(fail(ShaderRecompiler::BdaAbi::FaultReason::Unaligned, 2, 777) && guest[2] == 777, "an unaligned BDA access lost the stores of the dispatch's other invocations"); Require(!fail(ShaderRecompiler::BdaAbi::FaultReason::Unmapped, 3, 888) && guest[3] == 0, "a failed GPU command published writes"); + Require(!fail(ShaderRecompiler::BdaAbi::FaultReason::Unaligned, 5, 999, 1) && guest[5] == 0, "an unaligned BDA access that hid a fault of another kind published writes"); } { auto aligned = context; diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaShader.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaShader.cpp index 60a2e87853..a0d6df0079 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaShader.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaShader.cpp @@ -133,3 +133,11 @@ std::vector MakeBdaWriteTestShader(std::uint64_t address, std::ui #endif return shader; } + +std::vector MakeBdaFaultSequenceTestShader(std::initializer_list reasons) { + using namespace ShaderRecompiler; + return MakeShader(0u, false, true, [&](SpirvValueEmitContext& ctx, const IrValue&, std::uint32_t base) { + for (const auto reason : reasons) RecordBdaFault(ctx.state, base, ConstantU32(ctx.state, 4u), ConstantU32(ctx.state, 0x1234u), reason); + return std::vector{ConstantU32(ctx.state, 1u)}; + }); +} diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaShader.hpp b/core/libs/prx/libSceAgcDriver/tests/BdaShader.hpp index 62a3adcb19..b863d17917 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaShader.hpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaShader.hpp @@ -2,12 +2,15 @@ #define CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_BDASHADER_HPP #include "prx/libSceAgcDriver/Graphics/include/Context.hpp" +#include "BdaAbi.hpp" #include +#include #include std::vector MakeBdaTestShader(std::uint64_t address, std::uint32_t bits, std::int64_t offset = 0); std::vector MakeBdaDwordReadTestShader(std::uint64_t address, std::uint32_t dwords, bool coherent, bool stops); std::vector MakeBdaSpanReadTestShader(std::uint64_t address, std::uint32_t offset, std::uint32_t extracted, bool coherent, bool stops); +std::vector MakeBdaFaultSequenceTestShader(std::initializer_list reasons); std::vector MakeBdaDwordWriteTestShader(std::uint64_t address, std::uint32_t dwords, const std::uint32_t* values); std::vector MakeBdaStoreTestShader(std::uint64_t address, std::uint32_t value, std::uint32_t bits, bool coherent); std::vector MakeBdaWriteTestShader(std::uint64_t address, std::uint32_t value, std::uint32_t bits, bool coherent); diff --git a/core/shader/recompiler/BdaAbi.hpp b/core/shader/recompiler/BdaAbi.hpp index 7a79928c76..36a99d1885 100644 --- a/core/shader/recompiler/BdaAbi.hpp +++ b/core/shader/recompiler/BdaAbi.hpp @@ -37,14 +37,14 @@ struct Fault { std::uint32_t bytes; std::uint32_t stage; std::uint32_t instruction; - std::uint32_t reserved; + std::uint32_t otherFaults; }; static_assert(std::is_standard_layout_v
&& std::is_trivially_copyable_v
&& sizeof(Header) == 16); static_assert(std::is_standard_layout_v && std::is_trivially_copyable_v && sizeof(Range) == 32); static_assert(offsetof(Range, deviceAddress) == 16 && offsetof(Range, permissions) == 24); static_assert(std::is_standard_layout_v && std::is_trivially_copyable_v && sizeof(Fault) == 32); -static_assert(offsetof(Fault, address) == 8 && offsetof(Fault, instruction) == 24); +static_assert(offsetof(Fault, address) == 8 && offsetof(Fault, instruction) == 24 && offsetof(Fault, otherFaults) == 28); inline constexpr std::uint32_t WrittenPageShift = 12; inline constexpr std::uint32_t WrittenChunkShift = 17; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvBdaFault.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvBdaFault.cpp index 4b0d60d921..13acc3fbcb 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvBdaFault.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvBdaFault.cpp @@ -1,6 +1,7 @@ #include "SpirvBackend/SpirvBda.hpp" #include "SpirvBackend/SpirvMemory/SpirvTypes.hpp" #include "SpirvBackend/SpirvMemory/SpirvConstants.hpp" +#include #include namespace ShaderRecompiler { @@ -50,6 +51,10 @@ void DefineBdaFaultFunction(SpirvEmitterState& state) { const auto previous = state.module.AllocateId(); const auto scope = ConstantU32(state, spv::ScopeDevice); const auto relaxed = ConstantU32(state, spv::MemorySemanticsMaskNone); + const auto notUnaligned = Binary(state, spv::OpINotEqual, TypeBool(state), reason, ConstantU32(state, static_cast(BdaAbi::FaultReason::Unaligned))); + EmitIfCondition(state, notUnaligned, [&] { + state.module.AddFunction(spv::OpAtomicOr, u32, state.module.AllocateId(), BdaWord(state, state.faultBufferVariable, ConstantU32(state, static_cast(offsetof(BdaAbi::Fault, otherFaults) / sizeof(std::uint32_t)))), scope, relaxed, ConstantU32(state, 1u)); + }); state.module.AddFunction(spv::OpAtomicCompareExchange, TypeU32(state), previous, pointer, scope, relaxed, relaxed, ConstantU32(state, static_cast(BdaAbi::FaultState::Writing)), ConstantU32(state, 0u)); const auto won = Binary(state, spv::OpIEqual, TypeBool(state), previous, ConstantU32(state, 0u)); EmitIfCondition(state, won, [&] { @@ -62,7 +67,6 @@ void DefineBdaFaultFunction(SpirvEmitterState& state) { store(4, bytes); store(5, ConstantU32(state, static_cast(state.program.Resources().stage))); store(6, instruction); - store(7, ConstantU32(state, 0u)); const auto release = ConstantU32(state, spv::MemorySemanticsReleaseMask | spv::MemorySemanticsUniformMemoryMask); state.module.AddFunction(spv::OpAtomicStore, pointer, scope, release, ConstantU32(state, static_cast(BdaAbi::FaultState::Ready))); }); From e6cb236d67f47868b84922fe0acdd8a768f66d3f Mon Sep 17 00:00:00 2001 From: AlxcBTW <187763517+AlxcBTW@users.noreply.github.com> Date: Fri, 16 Oct 2026 19:56:19 +0200 Subject: [PATCH 013/269] fix(agc): write storage images back before reporting an unaligned BDA access The synchronous write-back threw the unaligned access out of WriteBackBuffers before it marked the written storage images dirty, so their stores were lost although the shader only skips that access. The images are now handled first and the report follows, as in the deferred path. --- .../Graphics/src/ShaderResources.cpp | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 50b05e2cc1..11865043db 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -4647,13 +4647,20 @@ bool SkipWriteBack() { } void ShaderResources::WriteBack() { - WriteBackBuffers(); - if (SkipWriteBack()) return; - for (std::size_t index = 0; index < storageTextures.size(); ++index) { - if (!storageWritten[index]) continue; - storageTextures[index]->MarkDirty(); - static_cast(DepthSurfaceStoreBack(context, *storageTextures[index])); + std::exception_ptr unaligned; + try { + WriteBackBuffers(); + } catch (const BdaUnalignedAccess&) { + unaligned = std::current_exception(); } + if (!SkipWriteBack()) { + for (std::size_t index = 0; index < storageTextures.size(); ++index) { + if (!storageWritten[index]) continue; + storageTextures[index]->MarkDirty(); + static_cast(DepthSurfaceStoreBack(context, *storageTextures[index])); + } + } + if (unaligned) std::rethrow_exception(unaligned); } void ShaderResources::MarkGpuWrites(Recorder& recorder) { From 69b683a90196991285f04718d2b1f063a59fbbe3 Mon Sep 17 00:00:00 2001 From: AlxcBTW <187763517+AlxcBTW@users.noreply.github.com> Date: Fri, 16 Oct 2026 19:56:19 +0200 Subject: [PATCH 014/269] fix(agc): rate-limit the deferred unaligned BDA reports per instruction One process-wide counter throttled the reports of every instruction together, so a second faulting instruction stayed silent after the first eight reports of the first. The count is kept per program counter and the periodic line names it. --- .../prx/libSceAgcDriver/Graphics/src/Recorder.cpp | 11 ++++++++--- core/libs/prx/libSceAgcDriver/tests/Recorder.cpp | 7 ++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp index 80b7feca40..b2ab392746 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include "prx/libc/include/general/AtomicSharedPtr.hpp" namespace AgcDriver::Graphics { @@ -3828,11 +3829,15 @@ void Recorder::finish(std::unique_ptr batch, bool wait, int source, bool try { action(); } catch (const BdaUnalignedAccess& error) { - static std::atomic failures{0}; + static std::mutex failuresMutex; + static std::unordered_map failures; constexpr std::uint64_t kDeferredFailureFirst = 8, kDeferredFailureEvery = 1024; - const auto count = failures.fetch_add(1, std::memory_order_relaxed) + 1; + const auto count = [&] { + std::lock_guard lock(failuresMutex); + return ++failures[error.instruction]; + }(); if (count <= kDeferredFailureFirst) std::fprintf(stderr, "[gpu] deferred write-back failed: %s\n", error.what()); - else if (count % kDeferredFailureEvery == 0) std::fprintf(stderr, "[gpu] deferred write-back failed with an unaligned BDA access %llu times so far; latest: %s\n", static_cast(count), error.what()); + else if (count % kDeferredFailureEvery == 0) std::fprintf(stderr, "[gpu] deferred write-back failed with an unaligned BDA access at pc 0x%x %llu times so far; latest: %s\n", error.instruction, static_cast(count), error.what()); } catch (const std::exception& error) { std::fprintf(stderr, "[gpu] deferred write-back failed: %s\n", error.what()); } diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 5285a5d67c..647b9075bf 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -1077,11 +1077,12 @@ std::size_t DeferredFailureLines(Recorder& recorder, std::size_t count, const st void deferredFailureReportTests(Recorder& recorder) { const auto plain = [] { throw std::runtime_error("image table: poisoned entry"); }; - const auto unaligned = [] { throw BdaUnalignedAccess("BDA access failed: unaligned"); }; + const auto unaligned = [](std::uint32_t instruction) { return [instruction] { throw BdaUnalignedAccess("BDA access failed: unaligned", instruction); }; }; Require(DeferredFailureLines(recorder, 12, plain) == 12, "a deferred failure other than an unaligned BDA access was not printed every time"); - Require(DeferredFailureLines(recorder, 12, unaligned) == 8, "the first 8 deferred reports of an unaligned BDA access were not the only ones printed"); + Require(DeferredFailureLines(recorder, 12, unaligned(0x5c8)) == 8, "the first 8 deferred reports of an unaligned BDA access were not the only ones printed"); std::string log; - Require(DeferredFailureLines(recorder, 1012, unaligned, &log) == 1 && log.find("1024 times so far") != std::string::npos, "the 1024th deferred report of an unaligned BDA access was not the only one printed"); + Require(DeferredFailureLines(recorder, 1012, unaligned(0x5c8), &log) == 1 && log.find("pc 0x5c8 1024 times so far") != std::string::npos, "the 1024th deferred report of an unaligned BDA access was not the only one printed"); + Require(DeferredFailureLines(recorder, 12, unaligned(0x7a0)) == 8, "unaligned BDA reports at one instruction limited the printing of the reports at another"); Require(DeferredFailureLines(recorder, 12, plain) == 12, "unaligned BDA reports limited the printing of other deferred failures"); } From 61fe1795fa5c07124f01093f01294745f131087e Mon Sep 17 00:00:00 2001 From: Zinebyte <197974009+Zinebyte@users.noreply.github.com> Date: Fri, 16 Oct 2026 14:53:45 -0400 Subject: [PATCH 015/269] test(relinker): avoid preinit registration overlap --- core/relinker/CMakeLists.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 75484ae962..a941d625a9 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -517,14 +517,14 @@ if(BUILD_TESTING) add_test(NAME guest_deferred_dependencies COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_deferred_dependencies.py $ $) - add_test(NAME guest_unused_import_dependencies COMMAND ${Python3_EXECUTABLE} - ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_unused_import_dependencies.py - $ $) - set_property(TEST guest_unused_import_dependencies APPEND PROPERTY LABELS relinker) add_test(NAME guest_absolute_symbols COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_absolute_symbols.py $) add_test(NAME guest_module_identity COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_module_identity.py $) + add_test(NAME guest_unused_import_dependencies COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_unused_import_dependencies.py + $ $) + set_property(TEST guest_unused_import_dependencies APPEND PROPERTY LABELS relinker) add_test(NAME guest_export_lookup COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_export_lookup.py $) set_tests_properties(guest_export_lookup PROPERTIES TIMEOUT 30) From d3d362583b23951e653ab7466323b2c6deedc023 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Fri, 16 Oct 2026 05:43:36 +0000 Subject: [PATCH 016/269] fix(agc): accept any fragment export where the draw has no color target The fragment output check demanded a float4 (or uint4 for integer and packed targets) at every location below the draw's MRT count, at least one. A location with no color target, a draw without color targets or a gap between written slots, has no attachment there, and Vulkan discards the output whatever its type. Crimson Desert's first draw binds no color target and exports a uint4 at location 0, and stopped there. Co-Authored-By: Claude Opus 5.5 --- .../prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp | 2 +- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp index a1408f084f..7ebc855085 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp @@ -675,7 +675,7 @@ std::set ValidateShaders(std::span shaders, const auto color = std::find_if(state.colors.begin(), state.colors.end(), [&](const ColorTarget& target) { return target.exportIndex == location; }); const bool uintExport = color != state.colors.end() && color->uintExport; const bool packed = color != state.colors.end() && color->packing != ShaderRecompiler::ColorExportPacking::None; - Require(signature == (uintExport || packed ? "vertex:u32x4" : "vertex:f32x4"), packed ? "fragment shader must export uint4 words to its packed color attachments" : uintExport ? "fragment shader must export uint4 colors to its unsigned integer attachments" : "fragment shader must export float4 colors to its attachments"); + Require(color == state.colors.end() || signature == (uintExport || packed ? "vertex:u32x4" : "vertex:f32x4"), packed ? "fragment shader must export uint4 words to its packed color attachments" : uintExport ? "fragment shader must export uint4 colors to its unsigned integer attachments" : "fragment shader must export float4 colors to its attachments"); locations.insert(location); } Require((!state.dualSourceBlend && state.reversedBlend == 0) || previous.secondSource == "f32x4", "dual-source blending needs a float4 second color at location 0 index 1"); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 03d66ccacb..4e439b0cf6 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -7150,6 +7150,12 @@ void unorm10_11_11TargetTests() { auto plainState = state; plainState.colors[0].packing = ColorExportPacking::None; expectFailure([&] { validate(packed, plainState); }, "float4 colors"); + auto targetless = plainState; + targetless.colors.clear(); + targetless.blends.clear(); + Require(validate(packed, targetless) == std::set{0u}, "a uint4 export from a draw with no color target was refused"); + targetless.blends.resize(1); + Require(validate(packed, targetless) == std::set{0u}, "a uint4 export into an unused color slot was refused"); auto uintExport = request; uintExport.context.pixel->targetOutputMode[0] = 7; expectFailure([&] { static_cast(ShaderRecompiler::Recompile(uintExport)); }, "fragment output 0 has no color export packing path"); From 3d5335ab3a111fe865b89d07afbe205ce26978f5 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Fri, 16 Oct 2026 06:36:56 +0000 Subject: [PATCH 017/269] test(agc): check the export check with a gap below a color target The unused-slot case had no colors and one blend, which the pipeline refuses before it validates shaders. A target at slot 1 with slot 0 unused is the gap a draw can have. Co-Authored-By: Claude Opus 5.5 --- 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 4e439b0cf6..f3250867b2 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -7154,8 +7154,10 @@ void unorm10_11_11TargetTests() { targetless.colors.clear(); targetless.blends.clear(); Require(validate(packed, targetless) == std::set{0u}, "a uint4 export from a draw with no color target was refused"); - targetless.blends.resize(1); - Require(validate(packed, targetless) == std::set{0u}, "a uint4 export into an unused color slot was refused"); + auto gap = plainState; + gap.blends.insert(gap.blends.begin(), gap.blends.front()); + gap.colors[0].exportIndex = 1; + Require(validate(packed, gap) == std::set{0u}, "a uint4 export into the unused slot below a color target was refused"); auto uintExport = request; uintExport.context.pixel->targetOutputMode[0] = 7; expectFailure([&] { static_cast(ShaderRecompiler::Recompile(uintExport)); }, "fragment output 0 has no color export packing path"); From 3fd2a2fe232f99e0561b8e9d9d901d7cb5929f6e Mon Sep 17 00:00:00 2001 From: T3cm0 Date: Sun, 18 Oct 2026 03:50:03 -0500 Subject: [PATCH 018/269] feat(libSceVrTracker2): answer as the console's module before initialization Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceVrTracker2/CMakeLists.txt | 16 ++ core/libs/prx/libSceVrTracker2/Export.cpp | 267 +++++++++++++++--- core/libs/tests/GuestVrTracker2.cpp | 115 ++++++++ docs/dev/debt/libSceHmd2.md | 4 +- docs/dev/debt/libSceVrTracker2.md | 11 + 5 files changed, 374 insertions(+), 39 deletions(-) create mode 100644 core/libs/prx/libSceVrTracker2/CMakeLists.txt create mode 100644 core/libs/tests/GuestVrTracker2.cpp create mode 100644 docs/dev/debt/libSceVrTracker2.md diff --git a/core/libs/prx/libSceVrTracker2/CMakeLists.txt b/core/libs/prx/libSceVrTracker2/CMakeLists.txt new file mode 100644 index 0000000000..aaf8e867d1 --- /dev/null +++ b/core/libs/prx/libSceVrTracker2/CMakeLists.txt @@ -0,0 +1,16 @@ +add_library(libSceVrTracker2 SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceVrTracker2 PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(libSceVrTracker2 PRIVATE libc) +configure_windows_unwind(libSceVrTracker2) + +if(BUILD_TESTING) + add_test_executable(guest_vr_tracker2_tests ${LIBS_INCLUDE_DIR}/tests/GuestVrTracker2.cpp) + target_include_directories(guest_vr_tracker2_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_vr_tracker2_tests PRIVATE libSceVrTracker2 libc) + configure_windows_unwind(guest_vr_tracker2_tests) + add_test(NAME guest_vr_tracker2 COMMAND guest_vr_tracker2_tests) + if(WIN32) + set_tests_properties(guest_vr_tracker2 PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceVrTracker2/Export.cpp b/core/libs/prx/libSceVrTracker2/Export.cpp index 9e3639df41..e323b3c719 100644 --- a/core/libs/prx/libSceVrTracker2/Export.cpp +++ b/core/libs/prx/libSceVrTracker2/Export.cpp @@ -1,67 +1,260 @@ -#include #include -#include "SceTypes.hpp" +#include +#include +#include +#include #include "prx/libc/include/General.hpp" +namespace { + +constexpr std::int32_t SCE_OK = 0; +constexpr std::int32_t VR_TRACKER2_ERROR_NOT_INITIALIZED = static_cast(0x8A740001u); +constexpr std::int32_t VR_TRACKER2_ERROR_INVALID_ARGUMENT = static_cast(0x8A740006u); + +std::int32_t ClearThenReportNotInitialized(void* output, std::size_t size) { + if (output == nullptr) return VR_TRACKER2_ERROR_INVALID_ARGUMENT; + std::memset(output, 0, size); + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +} + extern "C" { -int APS5_VABI sceVrTracker2Finalize() { +std::int32_t APS5_VABI sceVrTracker2Finalize() { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +std::int32_t APS5_VABI sceVrTracker2GetCoordinateSystem() { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +std::int32_t APS5_VABI sceVrTracker2GetPlayAreaBoundaryGeometry() { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +std::int32_t APS5_VABI sceVrTracker2GetPlayAreaOrientedBoundingBox() { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +std::int32_t APS5_VABI sceVrTracker2GetResult() { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +std::int32_t APS5_VABI sceVrTracker2LocateCoordinateSystem() { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +std::int32_t APS5_VABI sceVrTracker2RegisterDevice() { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +std::int32_t APS5_VABI sceVrTracker2ResetLocalCoordinate() { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +std::int32_t APS5_VABI sceVrTracker2SetCoordinateSystem() { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +std::int32_t APS5_VABI sceVrTracker2UnregisterDevice() { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +std::int32_t APS5_VABI sceVrTracker2QueryMemory(const void* parameter, void* requirement) { + if (parameter == nullptr || requirement == nullptr) return VR_TRACKER2_ERROR_INVALID_ARGUMENT; + const auto* bytes = static_cast(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); + return SCE_OK; +} + +std::int32_t APS5_VABI sceVrTracker2Initialize() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceVrTracker2GetCoordinateSystem() { - NotImplemented_nid_no_patch(__func__); +APS5_EXPORT("1m6DD9BBmSE", VrTracker2NotInitialized); +APS5_EXPORT("SgunslIO-C0", VrTracker2NotInitialized); +APS5_EXPORT("KMIToVnHrhc", VrTracker2NotInitialized); +APS5_EXPORT("XfEBVemeJyE", VrTracker2NotInitialized); +APS5_EXPORT("y79S3-zrJKg", VrTracker2NotInitialized); +APS5_EXPORT("b777Kpfe2J8", VrTracker2NotInitialized); +APS5_EXPORT("UfUYgiT7VK0", VrTracker2NotInitialized); +APS5_EXPORT("MB7B6396K6U", VrTracker2NotInitialized); +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("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("pNXc6ZlLq-8", VrTracker2NotInitialized); +APS5_EXPORT("omJ7b8FqAN0", VrTracker2NotInitialized); +APS5_EXPORT("G2PWj1HLqn8", VrTracker2NotInitialized); +APS5_EXPORT("73zcxAFawYk", VrTracker2NotInitialized); +APS5_EXPORT("vegrWFNiQwc", VrTracker2NotInitialized); +APS5_EXPORT("6r01IawmypA", VrTracker2NotInitialized); +APS5_EXPORT("zeof7sF2fqE", VrTracker2NotInitialized); +APS5_EXPORT("Z0rfHOHQV1w", VrTracker2NotInitialized); +APS5_EXPORT("4l7x+uy3dWw", VrTracker2NotInitialized); +APS5_EXPORT("ZXlt-Xb3da4", VrTracker2NotInitialized); +APS5_EXPORT("BSwAdB3GM3o", VrTracker2NotInitialized); +APS5_EXPORT("9ATuhVfn9Dw", VrTracker2NotInitialized); +APS5_EXPORT("wjmVbmf58rE", VrTracker2NotInitialized); +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("LQV4nz6wteY", VrTracker2NotInitialized); +APS5_EXPORT("x0pGthEwf88", VrTracker2NotInitialized); +APS5_EXPORT("d5Fjs0rHkBg", VrTracker2NotInitialized); +APS5_EXPORT("Yed3ZONcuMk", VrTracker2NotInitialized); +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("-dDyvzd2jf0", VrTracker2NotInitialized); +APS5_EXPORT("k8JDLkCrOgs", VrTracker2NotInitialized); +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("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("HAJB7irjl+0", VrTracker2NotInitialized); +APS5_EXPORT("x2mN50nKScs", VrTracker2NotInitialized); +APS5_EXPORT("cCKExge5d3A", VrTracker2NotInitialized); +APS5_EXPORT("gn6e5I4g9bw", VrTracker2NotInitialized); +APS5_EXPORT("OYTKt9diFOY", VrTracker2NotInitialized); +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("JnSmj+5E8WY", VrTracker2NotInitialized); +APS5_EXPORT("N1P+melAgb8", VrTracker2NotInitialized); +APS5_EXPORT("-e7A3rAM3Yk", VrTracker2NotInitialized); +APS5_EXPORT("RefT1spBleg", VrTracker2NotInitialized); +APS5_EXPORT("DsOfryhbm4U", VrTracker2NotInitialized); +std::int32_t APS5_VABI VrTracker2NotInitialized() { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("XknY0IDcOwU", VrTracker2ClearWideResult); +std::int32_t APS5_VABI VrTracker2ClearWideResult(void* result) { + return ClearThenReportNotInitialized(result, 24); +} + +APS5_EXPORT("ark13GkiskA", VrTracker2ClearNarrowResult); +std::int32_t APS5_VABI VrTracker2ClearNarrowResult(void* result) { + return ClearThenReportNotInitialized(result, 16); +} + +APS5_EXPORT("-YvjOUph8B0", VrTracker2CheckSecondThenReportNotInitialized); +std::int32_t APS5_VABI VrTracker2CheckSecondThenReportNotInitialized(const void* first, const void* second) { + (void)first; + return second == nullptr ? VR_TRACKER2_ERROR_INVALID_ARGUMENT : VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("QyZvFyf-QEk", VrTracker2Accept); +APS5_EXPORT("fTYZOZepkqE", VrTracker2Accept); +std::int32_t APS5_VABI VrTracker2Accept() { + return SCE_OK; +} + +APS5_EXPORT("TZIUIk9id3c", VrTracker2FillDefaults); +std::int32_t APS5_VABI VrTracker2FillDefaults(void* output) { + if (output == nullptr) APS5_INVALID_ARG_EX; + auto* bytes = static_cast(output); + const std::uint32_t first = 0xC000; + const std::uint32_t second = 0; + const std::uint16_t third = 0x40; + std::memcpy(bytes + 4, &first, sizeof first); + std::memcpy(bytes + 8, &second, sizeof second); + std::memcpy(bytes + 12, &third, sizeof third); + std::memcpy(bytes + 14, &third, sizeof third); + return SCE_OK; +} + +APS5_EXPORT("IB0qcDVu8vU", VrTracker2ClearWord); +std::int32_t APS5_VABI VrTracker2ClearWord(void* output) { + if (output == nullptr) APS5_INVALID_ARG_EX; + std::memset(output, 0, sizeof(std::uint32_t)); + return SCE_OK; +} + +APS5_EXPORT("jRmuXjsknsI", VrTracker2Unknown01); +std::int32_t APS5_VABI VrTracker2Unknown01() { + NotImplemented_nid_no_patch("jRmuXjsknsI"); return 0; } -int APS5_VABI sceVrTracker2GetPlayAreaBoundaryGeometry() { - NotImplemented_nid_no_patch(__func__); +APS5_EXPORT("NBhWy+Gl0tY", VrTracker2Unknown02); +std::int32_t APS5_VABI VrTracker2Unknown02() { + NotImplemented_nid_no_patch("NBhWy+Gl0tY"); return 0; } -int APS5_VABI sceVrTracker2ResetLocalCoordinate() { - NotImplemented_nid_no_patch(__func__); +APS5_EXPORT("9R6W33Au4B4", VrTracker2Unknown03); +std::int32_t APS5_VABI VrTracker2Unknown03() { + NotImplemented_nid_no_patch("9R6W33Au4B4"); return 0; } -int APS5_VABI sceVrTracker2GetPlayAreaOrientedBoundingBox() { - NotImplemented_nid_no_patch(__func__); +APS5_EXPORT("O8ZkTFtvUL4", VrTracker2Unknown04); +std::int32_t APS5_VABI VrTracker2Unknown04() { + NotImplemented_nid_no_patch("O8ZkTFtvUL4"); return 0; } -int APS5_VABI sceVrTracker2GetResult() { - NotImplemented_nid_no_patch(__func__); +APS5_EXPORT("940WPg+WwIk", VrTracker2Unknown05); +std::int32_t APS5_VABI VrTracker2Unknown05() { + NotImplemented_nid_no_patch("940WPg+WwIk"); return 0; } -int APS5_VABI sceVrTracker2Initialize() { - NotImplemented_nid_no_patch(__func__); +APS5_EXPORT("oZDm1bynCaA", VrTracker2Unknown06); +std::int32_t APS5_VABI VrTracker2Unknown06() { + NotImplemented_nid_no_patch("oZDm1bynCaA"); return 0; } -int APS5_VABI sceVrTracker2LocateCoordinateSystem() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceVrTracker2QueryMemory() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceVrTracker2RegisterDevice() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceVrTracker2SetCoordinateSystem() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceVrTracker2UnregisterDevice() { - NotImplemented_nid_no_patch(__func__); +APS5_EXPORT("U1VYczwqukY", VrTracker2Unknown07); +std::int32_t APS5_VABI VrTracker2Unknown07() { + NotImplemented_nid_no_patch("U1VYczwqukY"); return 0; } diff --git a/core/libs/tests/GuestVrTracker2.cpp b/core/libs/tests/GuestVrTracker2.cpp new file mode 100644 index 0000000000..f8d5899a66 --- /dev/null +++ b/core/libs/tests/GuestVrTracker2.cpp @@ -0,0 +1,115 @@ +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include +#include +#include +#include + +extern "C" { +std::int32_t APS5_VABI VrTracker2NotInitialized(); +std::int32_t APS5_VABI sceVrTracker2Finalize(); +std::int32_t APS5_VABI VrTracker2ClearWideResult(void* result); +std::int32_t APS5_VABI VrTracker2ClearNarrowResult(void* result); +std::int32_t APS5_VABI VrTracker2CheckSecondThenReportNotInitialized(const void* first, const void* second); +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(); +} + +namespace { + +constexpr std::int32_t NotInitialized = static_cast(0x8A740001u); +constexpr std::int32_t InvalidArgument = static_cast(0x8A740006u); + +void Require(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "VrTracker2: %s\n", message); + std::abort(); + } +} + +std::array Filled() { + std::array buffer{}; + buffer.fill(0xAA); + return buffer; +} + +bool AllEqual(const std::uint8_t* begin, const std::uint8_t* end, std::uint8_t value) { + for (; begin != end; ++begin) + if (*begin != value) return false; + return true; +} + +template +bool Throws(TCall call) { + try { + call(); + } catch (const std::exception&) { + return true; + } + return false; +} + +} + +int main() { + Require(VrTracker2NotInitialized() == NotInitialized, "plain export"); + Require(sceVrTracker2Finalize() == NotInitialized, "named export"); + + auto wide = Filled(); + Require(VrTracker2ClearWideResult(wide.data()) == NotInitialized, "wide result return"); + Require(AllEqual(wide.data(), wide.data() + 24, 0) && AllEqual(wide.data() + 24, wide.data() + 32, 0xAA), "wide result clears 24 bytes"); + Require(VrTracker2ClearWideResult(nullptr) == InvalidArgument, "wide result null"); + + auto narrow = Filled(); + Require(VrTracker2ClearNarrowResult(narrow.data()) == NotInitialized, "narrow result return"); + Require(AllEqual(narrow.data(), narrow.data() + 16, 0) && AllEqual(narrow.data() + 16, narrow.data() + 32, 0xAA), "narrow result clears 16 bytes"); + Require(VrTracker2ClearNarrowResult(nullptr) == InvalidArgument, "narrow result null"); + + auto second = Filled(); + Require(VrTracker2CheckSecondThenReportNotInitialized(nullptr, second.data()) == NotInitialized, "second checked, first null"); + Require(VrTracker2CheckSecondThenReportNotInitialized(second.data(), nullptr) == InvalidArgument, "second null"); + Require(AllEqual(second.data(), second.data() + 32, 0xAA), "second untouched"); + + Require(VrTracker2Accept() == 0, "accepting export"); + + auto defaults = Filled(); + Require(VrTracker2FillDefaults(defaults.data()) == 0, "defaults return"); + const std::array expected{0xAA, 0xAA, 0xAA, 0xAA, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x40, 0x00}; + Require(std::memcmp(defaults.data(), expected.data(), expected.size()) == 0, "defaults layout"); + Require(AllEqual(defaults.data() + 16, defaults.data() + 32, 0xAA), "defaults stop at 16 bytes"); + Require(Throws([] { VrTracker2FillDefaults(nullptr); }), "defaults null"); + + auto word = Filled(); + Require(VrTracker2ClearWord(word.data()) == 0, "clear word return"); + Require(AllEqual(word.data(), word.data() + 4, 0) && AllEqual(word.data() + 4, word.data() + 32, 0xAA), "clear word clears 4 bytes"); + Require(Throws([] { VrTracker2ClearWord(nullptr); }), "clear word null"); + + std::array parameter{}; + parameter[0] = 0x20; + parameter.back() = 0xAA; + std::array requirement{}; + requirement.fill(0xAA); + Require(sceVrTracker2QueryMemory(parameter.data(), requirement.data()) == 0, "query return"); + const std::array fields{0x20, 0, 0, 0, 0, 0, 0, 0x02, 0, 0, 0x01, 0}; + Require(std::memcmp(requirement.data(), fields.data(), fields.size()) == 0, "query fields"); + Require(AllEqual(requirement.data() + 12, requirement.data() + 32, 0) && AllEqual(requirement.data() + 32, requirement.data() + 40, 0xAA), "query writes 32 bytes"); + Require(sceVrTracker2QueryMemory(nullptr, requirement.data()) == InvalidArgument, "query null parameter"); + Require(sceVrTracker2QueryMemory(parameter.data(), nullptr) == InvalidArgument, "query null requirement"); + for (std::size_t offset = 4; offset < 32; ++offset) { + auto rejected = parameter; + rejected[offset] = 1; + requirement.fill(0xAA); + Require(sceVrTracker2QueryMemory(rejected.data(), requirement.data()) == InvalidArgument, "query reserved byte"); + Require(AllEqual(requirement.data(), requirement.data() + 40, 0xAA), "query rejection leaves output"); + } + parameter[0] = 0x21; + Require(sceVrTracker2QueryMemory(parameter.data(), requirement.data()) == InvalidArgument, "query size"); + + Require(Throws([] { sceVrTracker2Initialize(); }), "unknown export"); + return 0; +} diff --git a/docs/dev/debt/libSceHmd2.md b/docs/dev/debt/libSceHmd2.md index 050b0f7c9d..7de17e8644 100644 --- a/docs/dev/debt/libSceHmd2.md +++ b/docs/dev/debt/libSceHmd2.md @@ -2,12 +2,12 @@ ### Silent stubs -- [libSceHmd2](../../../core/libs/prx/libSceHmd2/Export.cpp) reports a console without a PS VR2 headset, as firmware 13.20's libSceHmd2 does with the `/dev/hmd2_cmd` state measured on a retail PS5 with none connected (connected flag 0, status 2, user -1): initialization succeeds, `sceHmd2Open` returns a handle, device information reports `NotDetected` with the firmware's fixed panel, latency and image distance values, both field-of-view calls return the firmware's default tangents and `sceHmd2SetVibration` succeeds without vibrating. The reprojection and gaze exports and every [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp) export throw. PPSA01317, PPSA07411, PPSA09030, PPSA15878 and PPSA29866, which skipped their VR set-up while initialization failed, have not been run with initialization succeeding +- [libSceHmd2](../../../core/libs/prx/libSceHmd2/Export.cpp) reports a console without a PS VR2 headset, as firmware 13.20's libSceHmd2 does with the `/dev/hmd2_cmd` state measured on a retail PS5 with none connected (connected flag 0, status 2, user -1): initialization succeeds, `sceHmd2Open` returns a handle, device information reports `NotDetected` with the firmware's fixed panel, latency and image distance values, both field-of-view calls return the firmware's default tangents and `sceHmd2SetVibration` succeeds without vibrating. The reprojection and gaze exports throw, and [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp) never initializes. PPSA01317, PPSA07411, PPSA09030, PPSA15878 and PPSA29866, which skipped their VR set-up while initialization failed, have not been run with initialization succeeding ### Unknown function info - PPSA29866 imports declared without parameters, signatures unknown, added without an implementation because the title fails to load without them: [sceHmd2ReprojectionQuerySeeThroughBufferSizeAlign](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2), [sceVideoOutSetFlipMaster and sceVideoOutClearFlipMaster](../../../core/libs/prx/libSceVideoOut/Unimplemented.cpp) (libSceVideoOut) -- PPSA09030 imports from its `Media/Plugins/psvr2.prx` declared without parameters, signatures unknown, added without an implementation because the title fails to load the plugin without them: [sceHmd2ReprojectionGetStatus](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2), [sceVrTracker2GetCoordinateSystem and sceVrTracker2GetPlayAreaBoundaryGeometry](../../../core/libs/prx/libSceVrTracker2/Export.cpp) (libSceVrTracker2) +- 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 diff --git a/docs/dev/debt/libSceVrTracker2.md b/docs/dev/debt/libSceVrTracker2.md new file mode 100644 index 0000000000..42938b9e61 --- /dev/null +++ b/docs/dev/debt/libSceVrTracker2.md @@ -0,0 +1,11 @@ +# libSceVrTracker2 technical debt + +### 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 + +### 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) - 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 a605f9473223f8560d3e26fe572951973606097a Mon Sep 17 00:00:00 2001 From: lakhantheboss <61951686+lakhantheboss@users.noreply.github.com> Date: Sun, 18 Oct 2026 18:06:26 +0530 Subject: [PATCH 019/269] fix(savedata): return parameter error on null arguments in setParam --- core/libs/prx/libSceSaveData.native/Export.cpp | 2 +- core/libs/tests/GuestSaveDataMountStatus.cpp | 15 +++++++++++++++ docs/dev/debt/libSceSaveData.native.md | 2 +- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index 03a86db6dd..c10ccf31ce 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -608,7 +608,7 @@ int APS5_VABI sceSaveDataSaveIconByPath(const SaveDataMountPoint* mount_point, c static int setParam(const SaveDataMountPoint* mount_point, uint32_t param_type, const void* param_buf, size_t param_buf_size) { if (mount_point == nullptr || param_buf == nullptr) { - throw std::runtime_error("sceSaveDataSetParam: null argument"); + return SAVE_DATA_ERROR_PARAMETER; } std::lock_guard lock(g_slots_mutex); const int slot = find_slot_by_mount_point(mount_point->data); diff --git a/core/libs/tests/GuestSaveDataMountStatus.cpp b/core/libs/tests/GuestSaveDataMountStatus.cpp index 5e7786a5dd..7d40fb6266 100644 --- a/core/libs/tests/GuestSaveDataMountStatus.cpp +++ b/core/libs/tests/GuestSaveDataMountStatus.cpp @@ -14,6 +14,7 @@ int APS5_VABI sceSaveDataMount3(const SaveDataMount3*, SaveDataMountResult*); int APS5_VABI sceSaveDataUmount2(std::uint32_t, const SaveDataMountPoint*); int APS5_VABI sceSaveDataGetMountInfo(const SaveDataMountPoint*, SaveDataMountInfo*); int APS5_VABI sceSaveDataGetParam(const SaveDataMountPoint*, std::uint32_t, void*, std::size_t, std::size_t*); +int APS5_VABI sceSaveDataSetParam(const SaveDataMountPoint*, std::uint32_t, const void*, std::size_t); } namespace { @@ -46,6 +47,9 @@ void CheckUnmountedCalls() { Check(sceSaveDataGetParam(nullptr, 1, buf, sizeof(buf), &gotSize) == SaveDataErrorParameter, "getParam null point is PARAMETER"); Check(sceSaveDataGetParam(&point, 1, nullptr, sizeof(buf), &gotSize) == SaveDataErrorParameter, "getParam null buf is PARAMETER"); Check(sceSaveDataGetParam(&point, 1, buf, sizeof(buf), &gotSize) == SaveDataErrorNotMounted, "getParam unmounted point is NOT_MOUNTED"); + Check(sceSaveDataSetParam(nullptr, 1, buf, sizeof(buf)) == SaveDataErrorParameter, "setParam null point is PARAMETER"); + Check(sceSaveDataSetParam(&point, 1, nullptr, sizeof(buf)) == SaveDataErrorParameter, "setParam null buf is PARAMETER"); + Check(sceSaveDataSetParam(&point, 1, buf, sizeof(buf)) == SaveDataErrorNotMounted, "setParam unmounted point is NOT_MOUNTED"); } void CheckMountInfoAndParam(const SaveDataMountPoint* point) { @@ -55,6 +59,8 @@ void CheckMountInfoAndParam(const SaveDataMountPoint* point) { std::size_t gotSize = 0; SaveDataMountPoint unmounted{}; std::memcpy(unmounted.data, "unmounted", sizeof("unmounted")); + const char newTitle[] = "Mounted Title"; + char readTitle[128]{}; Check(sceSaveDataGetMountInfo(nullptr, &info) == SaveDataErrorParameter, "getMountInfo null point is PARAMETER"); Check(sceSaveDataGetMountInfo(point, nullptr) == SaveDataErrorParameter, "getMountInfo null info is PARAMETER"); Check(sceSaveDataGetMountInfo(&unmounted, &info) == SaveDataErrorNotMounted, "getMountInfo unmounted point is NOT_MOUNTED"); @@ -66,6 +72,15 @@ void CheckMountInfoAndParam(const SaveDataMountPoint* point) { Check(sceSaveDataGetParam(point, 1, buf, sizeof(buf), &gotSize) == 0, "getParam on mounted save succeeds"); Check(gotSize == sizeof(buf), "getParam reports expected size"); Check(sceSaveDataGetParam(point, 1, buf, sizeof(buf), nullptr) == 0, "getParam with null got_size on mounted save succeeds"); + Check(sceSaveDataSetParam(nullptr, 1, newTitle, sizeof(newTitle)) == SaveDataErrorParameter, "setParam null point is PARAMETER"); + Check(sceSaveDataSetParam(point, 1, nullptr, sizeof(newTitle)) == SaveDataErrorParameter, "setParam null buf is PARAMETER"); + Check(sceSaveDataSetParam(point, 999, newTitle, sizeof(newTitle)) == SaveDataErrorParameter, "setParam invalid type is PARAMETER"); + Check(sceSaveDataSetParam(point, 1, newTitle, 0) == SaveDataErrorParameter, "setParam zero size is PARAMETER"); + Check(sceSaveDataSetParam(point, 4, smallBuf, 1) == SaveDataErrorParameter, "setParam short user param is PARAMETER"); + Check(sceSaveDataSetParam(&unmounted, 1, newTitle, sizeof(newTitle)) == SaveDataErrorNotMounted, "setParam unmounted point is NOT_MOUNTED"); + Check(sceSaveDataSetParam(point, 1, newTitle, sizeof(newTitle)) == 0, "setParam on mounted save succeeds"); + Check(sceSaveDataGetParam(point, 1, readTitle, sizeof(readTitle), &gotSize) == 0, "getParam reads back set title"); + Check(std::strcmp(readTitle, newTitle) == 0, "readTitle matches newTitle"); } void MountAndCheck(const char* name, std::uint32_t mode, std::uint32_t expectedStatus, const std::string& what) { diff --git a/docs/dev/debt/libSceSaveData.native.md b/docs/dev/debt/libSceSaveData.native.md index 042e5e2109..380169570f 100644 --- a/docs/dev/debt/libSceSaveData.native.md +++ b/docs/dev/debt/libSceSaveData.native.md @@ -17,5 +17,5 @@ - [sceSaveDataDirNameSearch2](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) runs the same search as `sceSaveDataDirNameSearch` with the same condition and result structures, as [SharpEmu](https://github.com/sharpemu/sharpemu/blob/main/src/SharpEmu.Libs/SaveData/SaveDataExports.cs) does, its only source; what distinguishes it on the console is unknown. - [sceSaveDataMount3](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - a failed mount (BUSY, EXISTS, NOT_FOUND, MOUNT_FULL) leaves the result as it was, as shadPS4 does; not checked on hardware. Animal Well (PPSA22520) depends on it: it checks for its save with a mount it never unmounts, then saves with a second mount of the same directory, ignores its BUSY and keeps using the first mount point for `sceSaveDataPrepare`, its file and `sceSaveDataUmount2` - [sceSaveDataMount3](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - an unterminated or empty directory name, `.`, `..`, or a name containing `/`, `\` or `:` returns `SAVE_DATA_ERROR_PARAMETER`, which the [SDK 12.000 reference](https://github.com/peterpuppy/bm/blob/3f9d1ba6b896a53080460c903f5c15af8affb80d/features/ps5/document/output/psn_12/System/Save_Data/SaveData-Reference/sce-save-data-mount3.md#L196-L204) lists for an invalid parameter, and leaves the result as it was, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/569ec04d1a77127c601eed522ad5bba187173f9d/src/libs/libSaveData.cpp#L652-L658) does. The [SDK](https://github.com/peterpuppy/bm/blob/3f9d1ba6b896a53080460c903f5c15af8affb80d/features/ps5/document/output/psn_12/System/Save_Data/SaveData-Reference/sce-save-data-dir-name.md#L32-L36) lets an application use only letters, digits, `-`, `.` and `@` in a name, and KytyPS5 also accepts `_` ([`valid_path_component`](https://github.com/KytyPS5/KytyPS5/blob/569ec04d1a77127c601eed522ad5bba187173f9d/src/libs/libSaveData.cpp#L276-L287)); which other names the console's mount rejects is not checked on hardware -- [sceSaveDataGetMountInfo, sceSaveDataGetParam](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - null pointer arguments return `SAVE_DATA_ERROR_PARAMETER` instead of throwing C++ exceptions; unmounted targets return `SAVE_DATA_ERROR_NOT_MOUNTED`, and parameter type or buffer size violations return `SAVE_DATA_ERROR_PARAMETER` after slot lookup +- [sceSaveDataGetMountInfo, sceSaveDataGetParam, sceSaveDataSetParam](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - null pointer arguments return `SAVE_DATA_ERROR_PARAMETER` instead of throwing C++ exceptions; unmounted targets return `SAVE_DATA_ERROR_NOT_MOUNTED`, and parameter type or buffer size violations return `SAVE_DATA_ERROR_PARAMETER` after slot lookup From 6f2a61ade037899700062a744ec8d2c658a26488 Mon Sep 17 00:00:00 2001 From: mugurc Date: Sat, 17 Oct 2026 18:01:02 +0300 Subject: [PATCH 020/269] fix(libkernel): recognize listening sockets on macOS hosts macOS does not support getsockopt with SO_ACCEPTCONN; it fails with ENOPROTOOPT. So on macOS the guest's getsockopt of SO_ACCEPTCONN failed, connect did not see that a stream socket was listening (an address of another family failed with EAFNOSUPPORT instead of EOPNOTSUPP), and the read event of a listening socket reported FIONREAD's 0 instead of the waiting connection and did not ignore its low watermark. On macOS the listening state is read from the SO_ACCEPTCONN flag of the socket options that proc_pidfdinfo reports, and the three places that ask go through NativeListening. Other hosts keep getsockopt. --- .../Socket/include/SocketOptions.hpp | 1 + .../libkernel/Socket/src/SocketOptions.cpp | 25 ++++++++++++++++--- core/libs/prx/libkernel/Socket/src/Udp.cpp | 9 +++---- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libkernel/Socket/include/SocketOptions.hpp b/core/libs/prx/libkernel/Socket/include/SocketOptions.hpp index c81d93d46b..cfe594b32e 100644 --- a/core/libs/prx/libkernel/Socket/include/SocketOptions.hpp +++ b/core/libs/prx/libkernel/Socket/include/SocketOptions.hpp @@ -11,6 +11,7 @@ struct SocketOptionState { std::int64_t receiveTimeout = 0; }; +bool NativeListening(std::uintptr_t native, bool& listening); int SetSocketLevelOption(std::uintptr_t native, SocketOptionState& state, int option, const void* value, std::uint32_t length); int GetSocketLevelOption(std::uintptr_t native, SocketOptionState& state, int type, int option, void* value, std::uint32_t* length); int SetTcpLevelOption(std::uintptr_t native, bool tcp, int option, const void* value, std::uint32_t length); diff --git a/core/libs/prx/libkernel/Socket/src/SocketOptions.cpp b/core/libs/prx/libkernel/Socket/src/SocketOptions.cpp index 184219224d..6ffe7966eb 100644 --- a/core/libs/prx/libkernel/Socket/src/SocketOptions.cpp +++ b/core/libs/prx/libkernel/Socket/src/SocketOptions.cpp @@ -7,6 +7,10 @@ #include #include #include +#ifdef __APPLE__ +#include +#include +#endif #endif #include "prx/libkernel/Socket/include/SocketError.hpp" #include "prx/libkernel/Socket/include/SocketOptions.hpp" @@ -159,6 +163,20 @@ int SetSocketLevelOption(std::uintptr_t native, SocketOptionState& state, int op return ErrorNoProtocolOption; } +bool NativeListening(std::uintptr_t native, bool& listening) { +#ifdef __APPLE__ + socket_fdinfo info{}; + if (proc_pidfdinfo(getpid(), static_cast(native), PROC_PIDFDSOCKETINFO, &info, sizeof(info)) != static_cast(sizeof(info))) return false; + listening = (info.psi.soi_options & SO_ACCEPTCONN) != 0; +#else + int accepting = 0; + socklen_t size = sizeof(accepting); + if (::getsockopt(static_cast(native), SOL_SOCKET, SO_ACCEPTCONN, reinterpret_cast(&accepting), &size)) return false; + listening = accepting != 0; +#endif + return true; +} + int GetSocketLevelOption(std::uintptr_t native, SocketOptionState& state, int type, int option, void* value, std::uint32_t* length) { const auto handle = static_cast(native); if (option == GuestLinger) { @@ -179,10 +197,9 @@ int GetSocketLevelOption(std::uintptr_t native, SocketOptionState& state, int ty if (::getsockopt(handle, SOL_SOCKET, nativeOption, reinterpret_cast(&enabled), &size)) return HostError(); number = enabled != 0 ? option : 0; } else if (option == GuestAcceptConnection) { - int listening = 0; - socklen_t size = sizeof(listening); - if (::getsockopt(handle, SOL_SOCKET, SO_ACCEPTCONN, reinterpret_cast(&listening), &size)) return HostError(); - number = listening != 0 ? GuestAcceptConnection : 0; + bool listening = false; + if (!NativeListening(native, listening)) return HostError(); + number = listening ? GuestAcceptConnection : 0; } else if (option == GuestType) { number = type; } else if (option == GuestNoSigpipe) { diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 799c7b8b9b..d81e62166e 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -253,9 +253,8 @@ std::uint16_t& PortOf(sockaddr_storage& address) { return address.ss_family == AF_INET ? reinterpret_cast(address).sin_port : reinterpret_cast(address).sin6_port; } bool Listening(const Socket& socket) { - int listening = 0; - socklen_t size = sizeof(listening); - return ::getsockopt(socket.value, SOL_SOCKET, SO_ACCEPTCONN, reinterpret_cast(&listening), &size) == 0 && listening != 0; + bool listening = false; + return GuestSockets::NativeListening(static_cast(socket.value), listening) && listening; } int Disconnect(const Socket& socket) { sockaddr_storage peer{}; @@ -419,9 +418,7 @@ bool GuestSockets::Listening(int descriptor) { if (found == sockets.end()) return false; socket = found->second; } - int accepting = 0; - socklen_t length = sizeof(accepting); - return getsockopt(socket->value, SOL_SOCKET, SO_ACCEPTCONN, reinterpret_cast(&accepting), &length) == 0 && accepting != 0; + return Listening(*socket); } extern "C" bool GuestSocketIsOpen_nid_no_patch(int descriptor) { From 23aa711e7fab0f9687903d9c3e32d1484d62248d Mon Sep 17 00:00:00 2001 From: lakhantheboss <61951686+lakhantheboss@users.noreply.github.com> Date: Sun, 18 Oct 2026 18:45:15 +0530 Subject: [PATCH 021/269] fix(pad): return invalid argument error on null buffer in scePadRead and scePadReadState scePadRead_nid_postfix and scePadReadState threw APS5_INVALID_ARG_EX (std::invalid_argument) when passed a null output buffer or a non-positive buffer count, terminating the guest process across the PRX ABI boundary. Both functions now return PAD_ERROR_INVALID_ARG (-2137915391) on invalid arguments, matching the error return contract of all sibling libScePad APIs. --- core/libs/prx/libScePad/Export.cpp | 4 ++-- core/libs/tests/GuestPad.cpp | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index 3dd4c0650a..032f3708df 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -179,13 +179,13 @@ int APS5_VABI scePadOutputReport() { NotImplemented_nid_no_patch(__func__); retu int APS5_VABI scePadRead_nid_postfix(int handle, PadData* data, int num) { if (handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE; - if (data == nullptr || num <= 0) APS5_INVALID_ARG_EX; + if (data == nullptr || num <= 0) return PAD_ERROR_INVALID_ARG; return Pad::Read(data, num); } int APS5_VABI scePadReadState(int handle, PadData* data) { if (handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE; - if (data == nullptr) APS5_INVALID_ARG_EX; + if (data == nullptr) return PAD_ERROR_INVALID_ARG; *data = Pad::ReadState(); diff --git a/core/libs/tests/GuestPad.cpp b/core/libs/tests/GuestPad.cpp index 84b0ad50ab..f4dc717ec9 100644 --- a/core/libs/tests/GuestPad.cpp +++ b/core/libs/tests/GuestPad.cpp @@ -206,6 +206,10 @@ static void CheckReadStateHandle(int handle) { Require(scePadReadState(handle + 1, &data) == PAD_ERROR_INVALID_HANDLE); Require(scePadRead_nid_postfix(0, &data, 1) == PAD_ERROR_INVALID_HANDLE); Require(scePadReadState(handle, &data) == PAD_OK); + Require(scePadRead_nid_postfix(handle, nullptr, 1) == PAD_ERROR_INVALID_ARG); + Require(scePadRead_nid_postfix(handle, &data, 0) == PAD_ERROR_INVALID_ARG); + Require(scePadRead_nid_postfix(handle, &data, -1) == PAD_ERROR_INVALID_ARG); + Require(scePadReadState(handle, nullptr) == PAD_ERROR_INVALID_ARG); } static void CheckRemoteController(int handle) { From e79cf0f754588d2b2b4ad95508c80445a1262ad1 Mon Sep 17 00:00:00 2001 From: Maz Date: Sun, 18 Oct 2026 17:19:02 +0400 Subject: [PATCH 022/269] fix(libkernel): stop O_NOFOLLOW from opening a Windows directory junction lstat reports the junction as a symbolic link, but open with O_NOFOLLOW still opened the target. --- core/libs/prx/libkernel/File/src/Open.cpp | 8 ++++++++ core/libs/tests/GuestFilesystem.cpp | 5 +++++ docs/dev/debt/libkernel.md | 2 +- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index 23fd92fc59..f67625d5ff 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -28,6 +28,7 @@ #include #include #include +#include "prx/libkernel/File/include/WindowsSymbolicLink.hpp" static int NativeOpen(const std::filesystem::path& p, int nativeFlags, std::uint16_t mode) { const int access = nativeFlags & (_O_RDONLY | _O_WRONLY | _O_RDWR); const DWORD desired = access == _O_RDONLY ? GENERIC_READ : access == _O_WRONLY ? GENERIC_WRITE : GENERIC_READ | GENERIC_WRITE; @@ -207,6 +208,13 @@ static int OpenNative(const std::filesystem::path& native, int flags, std::uint1 constexpr int GuestEmlink = 31; std::error_code error; if (std::filesystem::is_symlink(native, error)) return SceErrorFromErrno(GuestEmlink); +#ifdef _WIN32 + const DWORD attributes = GetFileAttributesW(native.c_str()); + if (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) { + std::string junctionTarget; + if (File::WindowsSymbolicLink::ReadJunction(native, junctionTarget, "open")) return SceErrorFromErrno(GuestEmlink); + } +#endif } int fd = NativeOpen(native, MapFlags(flags), mode); #ifdef _WIN32 diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index 238ae84299..8903e256db 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -793,6 +793,11 @@ int main() { Require((junctionStatus.st_mode & 0170000) == 0120000 && junctionStatus.st_size > 0); FileStat junctionFollowed{}; Require(stat_nid_postfix(junction.string().c_str(), &junctionFollowed) == 0 && (junctionFollowed.st_mode & 0170000) == 0040000); + constexpr int noFollow = 0x100; + Require(sceKernelOpen(junction.string().c_str(), noFollow, 0) == static_cast(0x8002001Fu)); + Require(open_nid_postfix(junction.string().c_str(), noFollow, 0) == -1 && *__error_nid_postfix() == 31); + 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)); #endif FileStat identity{}; diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index baef0291c7..17230fcb45 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -116,7 +116,7 @@ - [Windows file timestamps](../../../core/libs/prx/libkernel/File/include/WindowsFileTime.hpp) use the host filesystem's timestamp range and resolution. `utimes`, `futimes`, and `lutimes` reject timestamps outside nonnegative signed 64-bit Windows file-time ticks with `EINVAL`, including exactly 1601-01-01 00:00:00 UTC, whose zero value means "leave unchanged" to `SetFileTime`. Guest directory descriptors still resolve through their recorded path. Filesystems with coarser timestamp resolution cannot retain every guest microsecond - [EVFILT_VNODE on macOS](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) (libkernel) registers `EVFILT_VNODE` on a duplicate of the descriptor in a host kqueue, whose notes have FreeBSD's values, and drops the `NOTE_LINK` that XNU adds to `NOTE_DELETE` when the watched file or directory is removed, since FreeBSD's `vop_remove_post` and `vop_rmdir_post` report `NOTE_DELETE` alone. As on Windows, a directory registered with `NOTE_EXTEND` is accepted but never receives it: XNU reports a move between directories as `NOTE_WRITE` alone, with `NOTE_LINK` for a subdirectory - [Windows file identity](../../../core/libs/prx/libkernel/File/src/NativeStat.cpp) (libkernel) - `stat` and `fstat` report the volume serial number as `st_dev` and the 64-bit file index folded into the guest's 32-bit `st_ino` (never 0), so two different files can in principle share an `st_ino`; the fold is not taken from a reference. `st_nlink` is clamped to 65535, the width of the guest field. The call keeps the `_wstat64` values when `GetFileInformationByHandle` fails -- [lstat of a Windows symbolic link](../../../core/libs/prx/libkernel/File/src/NativeStat.cpp) (libkernel) reports a link whose target is relative, as `symlink` creates them, with the link's own times, `st_dev` and `st_ino`, mode `S_IFLNK | 0777` and the length of the target `readlink` returns as `st_size`. FreeBSD gives a new link `0777` less the creating process's umask, which the host does not keep, so the permission bits are not the console's. A link with an absolute target 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` +- [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 - [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 From e9a7b8ea5498c5fc8eff47f42e33914c0ce38cc4 Mon Sep 17 00:00:00 2001 From: iAlturki <120382911+iAlturki@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:27:26 +0300 Subject: [PATCH 023/269] fix(tools): accept UTF-8 BOMs in trace digest inputs --- tools/tests/test_trace_digest.py | 43 ++++++++++++++++++++++++++++++++ tools/trace_digest.py | 8 +++--- 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/tools/tests/test_trace_digest.py b/tools/tests/test_trace_digest.py index 5864781302..9734a60332 100644 --- a/tools/tests/test_trace_digest.py +++ b/tools/tests/test_trace_digest.py @@ -128,6 +128,41 @@ class TraceDigestTests(unittest.TestCase): return trace_digest.digest(trace_digest.load_trace(self.trace), trace_digest.load_summary(None, self.trace), trace_digest.load_stacks(self.stacks), worst=2, top=3) + def json_output(self, trace, *arguments): + result = subprocess.run([sys.executable, trace_digest.__file__, str(trace), "--json", *arguments], + capture_output=True, text=True, encoding="utf-8", timeout=20) + self.assertEqual(result.returncode, 0, result.stderr) + return json.loads(result.stdout) + + def test_utf8_bom_plain_and_compressed_traces_preserve_the_digest(self): + events = [dict(event, args={"name": "worker \u03a9"}) if event["name"] == "thread_name" and event["tid"] == 1 + else event for event in EVENTS] + text = json.dumps({"traceEvents": events}, ensure_ascii=False) + self.trace.write_bytes(gzip.compress(text.encode("utf-8"))) + expected = self.json_output(self.trace) + for trace in (self.trace, self.trace.with_suffix("")): + for encoding in ("utf-8", "utf-8-sig"): + with self.subTest(trace=trace.name, encoding=encoding): + data = text.encode(encoding) + trace.write_bytes(gzip.compress(data) if trace.suffix == ".gz" else data) + self.assertEqual(self.json_output(trace), expected) + + def test_utf8_bom_summary_preserves_the_digest(self): + expected = self.json_output(self.trace) + summary = self.trace.with_name("aps5-summary.jsonl") + summary.write_text(summary.read_text(encoding="utf-8"), encoding="utf-8-sig") + self.assertEqual(self.json_output(self.trace), expected) + + def test_utf8_bom_stacks_preserve_comments_and_frame_names(self): + for text in (STACKS, "root;module\u03a9+0x1 2\n"): + with self.subTest(comments=text.startswith("#")): + self.stacks.write_text(text, encoding="utf-8") + expected = self.json_output(self.trace, "--stacks", str(self.stacks)) + expected_stacks = trace_digest.load_stacks(self.stacks) + self.stacks.write_text(text, encoding="utf-8-sig") + self.assertEqual(self.json_output(self.trace, "--stacks", str(self.stacks)), expected) + self.assertEqual(trace_digest.load_stacks(self.stacks), expected_stacks) + def test_summary_is_the_line_of_this_trace(self): self.assertEqual(self.result()["summary"]["trace"], "aps5-trace-abc-7.json.gz") @@ -464,6 +499,14 @@ class IntervalDigestTests(unittest.TestCase): def test_window_lines_are_skipped(self): self.assertEqual(len(trace_digest.load_intervals(self.path)), 4) + def test_utf8_bom_intervals_preserve_the_command_line_digest(self): + expected = trace_digest.digest_intervals(trace_digest.load_intervals(self.path)) + self.path.write_text(self.path.read_text(encoding="utf-8"), encoding="utf-8-sig") + result = subprocess.run([sys.executable, trace_digest.__file__, str(self.path), "--json"], + capture_output=True, text=True, encoding="utf-8", timeout=20) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(json.loads(result.stdout), expected) + def test_rows(self): result = trace_digest.digest_intervals(trace_digest.load_intervals(self.path)) first, second, third, _ = result["intervals"] diff --git a/tools/trace_digest.py b/tools/trace_digest.py index 08504ca43a..96885639fc 100644 --- a/tools/trace_digest.py +++ b/tools/trace_digest.py @@ -72,7 +72,7 @@ class Trace: def load_trace(path): opener = gzip.open if str(path).endswith(".gz") else open - with opener(path, "rt", encoding="utf-8") as handle: + with opener(path, "rt", encoding="utf-8-sig") as handle: return Trace(json.load(handle)) @@ -80,7 +80,7 @@ def load_summary(path, trace_path): candidate = Path(path) if path else Path(trace_path).with_name("aps5-summary.jsonl") if not candidate.exists(): return None - lines = [json.loads(line) for line in candidate.read_text(encoding="utf-8").splitlines() if line.strip()] + lines = [json.loads(line) for line in candidate.read_text(encoding="utf-8-sig").splitlines() if line.strip()] name = Path(trace_path).name alternate = name[:-3] if name.endswith(".gz") else name + ".gz" for filename in (name, alternate): @@ -92,7 +92,7 @@ def load_summary(path, trace_path): def load_stacks(path): stacks = [] - for number, line in enumerate(Path(path).read_text(encoding="utf-8").splitlines(), 1): + for number, line in enumerate(Path(path).read_text(encoding="utf-8-sig").splitlines(), 1): if line.startswith("#") or not line.strip(): continue frames, _, count = line.rpartition(" ") @@ -264,7 +264,7 @@ def markdown(result): def load_intervals(path): - lines = [json.loads(line) for line in Path(path).read_text(encoding="utf-8").splitlines() if line.strip()] + lines = [json.loads(line) for line in Path(path).read_text(encoding="utf-8-sig").splitlines() if line.strip()] return [line for line in lines if "interval_start_s" in line] From 0029bddb55435741bfca8045e5e0f8ed357c44ea Mon Sep 17 00:00:00 2001 From: slopggobblercobbler81 <336610228+slopggobblercobbler81@users.noreply.github.com> Date: Sun, 18 Oct 2026 00:15:57 +0200 Subject: [PATCH 024/269] fix(libSceAppContent): return NOT_MOUNTED for invalid download mount point Return SCE_APP_CONTENT_ERROR_NOT_MOUNTED instead of throwing APS5_INVALID_ARG_EX when a mount point other than /download0 is passed to sceAppContentDownloadDataFormat or sceAppContentDownloadDataGetAvailableSpaceKb, aligning with sceAppContentTemporaryDataFormat. Check null mount_point in sceAppContentDownloadDataGetAvailableSpaceKb with SCE_APP_CONTENT_ERROR_PARAMETER. --- core/libs/prx/libSceAppContent/Export.cpp | 6 +++--- core/libs/tests/GuestAppContent.cpp | 4 ++++ docs/dev/debt/libSceAppContent.md | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAppContent/Export.cpp b/core/libs/prx/libSceAppContent/Export.cpp index be7cad24d9..b65bbb9a00 100644 --- a/core/libs/prx/libSceAppContent/Export.cpp +++ b/core/libs/prx/libSceAppContent/Export.cpp @@ -95,7 +95,7 @@ int APS5_VABI sceAppContentAppParamGetInt(uint32_t param_id, int32_t* value) { int APS5_VABI sceAppContentDownloadDataFormat(const AppContentMountPoint* mount_point) { if (!mount_point) return SCE_APP_CONTENT_ERROR_PARAMETER; - if (std::strncmp(mount_point->data, DOWNLOAD_MOUNT_POINT, sizeof(mount_point->data)) != 0) APS5_INVALID_ARG_EX; + 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); if (std::filesystem::create_directories(directory)) RecordWrittenPath_nid_no_patch(directory); ClearDirectory(directory); @@ -103,8 +103,8 @@ int APS5_VABI sceAppContentDownloadDataFormat(const AppContentMountPoint* mount_ } int APS5_VABI sceAppContentDownloadDataGetAvailableSpaceKb(const AppContentMountPoint* mount_point, size_t* available_space_kb) { - if (!available_space_kb) return SCE_APP_CONTENT_ERROR_PARAMETER; - if (!mount_point || std::strncmp(mount_point->data, DOWNLOAD_MOUNT_POINT, sizeof(mount_point->data)) != 0) APS5_INVALID_ARG_EX; + 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; const std::uint64_t quotaKb = GetAppDownloadDataSizeMiB_nid_postfix() * 1024u; if (quotaKb == 0) { *available_space_kb = 0; diff --git a/core/libs/tests/GuestAppContent.cpp b/core/libs/tests/GuestAppContent.cpp index fef723221e..6d3745cd70 100644 --- a/core/libs/tests/GuestAppContent.cpp +++ b/core/libs/tests/GuestAppContent.cpp @@ -51,6 +51,9 @@ int main() { Require(sceAppContentDownloadDataGetAvailableSpaceKb(&download, &availableKb) == 0); Require(availableKb == 0); Require(sceAppContentDownloadDataGetAvailableSpaceKb(&download, nullptr) == ErrorParameter); + Require(sceAppContentDownloadDataGetAvailableSpaceKb(nullptr, &availableKb) == ErrorParameter); + AppContentMountPoint invalidMount{}; + Require(sceAppContentDownloadDataGetAvailableSpaceKb(&invalidMount, &availableKb) == ErrorNotMounted); const auto downloadData = ResolvePath_nid_no_patch("/download0"); Require(sceAppContentDownloadDataFormat(&download) == 0); Require(std::filesystem::is_directory(downloadData) && std::filesystem::is_empty(downloadData)); @@ -58,6 +61,7 @@ int main() { { std::ofstream file(downloadData / "cache" / "old.bin"); file << "downloaded"; } { std::ofstream file(downloadData / "index.dat"); file << "downloaded"; } Require(sceAppContentDownloadDataFormat(nullptr) == ErrorParameter); + Require(sceAppContentDownloadDataFormat(&invalidMount) == ErrorNotMounted); Require(std::filesystem::exists(downloadData / "index.dat")); Require(sceAppContentDownloadDataFormat(&download) == 0); Require(std::filesystem::is_directory(downloadData) && std::filesystem::is_empty(downloadData)); diff --git a/docs/dev/debt/libSceAppContent.md b/docs/dev/debt/libSceAppContent.md index 5639476d66..bf943bde0e 100644 --- a/docs/dev/debt/libSceAppContent.md +++ b/docs/dev/debt/libSceAppContent.md @@ -3,7 +3,7 @@ ### Unknown function info - [sceAppContentDownloadDataGetAvailableSpaceKb](../../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) - a title whose `param.json` declares `downloadDataSize` 0 gets 0 KB and OK; the console's behaviour for a title without a download data area is unverified (it may return an error instead). KytyPS5 does not special-case it, and shadPS4 stubs 1048576 KB -- [sceAppContentDownloadDataFormat](../../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) - empties the `/download0` directory, creating it if needed. Imported by PPSA01317. A null mount point returns `PARAMETER` as `sceAppContentTemporaryDataFormat` does, and another mount point throws as in `sceAppContentDownloadDataGetAvailableSpaceKb`; the console's error codes for both and its behaviour with files still open are unverified +- [sceAppContentDownloadDataFormat](../../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) - empties the `/download0` directory, creating it if needed. Imported by PPSA01317. A null mount point returns `PARAMETER` and another mount point returns `NOT_MOUNTED` as in `sceAppContentTemporaryDataFormat` and `sceAppContentDownloadDataGetAvailableSpaceKb`; the console's error codes for both and its behaviour with files still open are unverified ### Functional From 9895d130c4bbd83d9a4677f1534e8b2db9f46e62 Mon Sep 17 00:00:00 2001 From: hugodearaujo Date: Sat, 17 Oct 2026 13:11:12 -0300 Subject: [PATCH 025/269] fix(libSceAgc): accept register range values at any byte address, as the console does --- .../libs/prx/libSceAgc/Command/src/Packet.cpp | 6 ++-- core/libs/prx/libSceAgc/tests/Commands.cpp | 10 ++++-- .../prx/libSceAgc/tests/RegisterCommands.cpp | 33 +++++++++++++++++-- 3 files changed, 43 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAgc/Command/src/Packet.cpp b/core/libs/prx/libSceAgc/Command/src/Packet.cpp index 743fed53f2..6ec0e12165 100644 --- a/core/libs/prx/libSceAgc/Command/src/Packet.cpp +++ b/core/libs/prx/libSceAgc/Command/src/Packet.cpp @@ -1,7 +1,9 @@ #include "prx/libSceAgc/Command/include/Packet.hpp" #include +#include #include +#include #include #include #include @@ -96,8 +98,8 @@ std::uint32_t* WriteRegisterRange(CommandBuffer* buffer, std::uint32_t opcode, s Require(count != 0 && count <= 0x3fffu && offset <= 0xffffu && count <= 0x10000u - offset, function, "invalid register range"); std::vector snapshot; if (values != nullptr) { - CheckAddress(reinterpret_cast(values), 4, function); - snapshot.assign(values, values + count); + snapshot.resize(count); + std::memcpy(snapshot.data(), values, static_cast(count) * sizeof(std::uint32_t)); } auto* packet = Allocate(buffer, count + 2u, function); packet[0] = Header(opcode, count + 2u); diff --git a/core/libs/prx/libSceAgc/tests/Commands.cpp b/core/libs/prx/libSceAgc/tests/Commands.cpp index 1f86645fe8..ba4e85f5b9 100644 --- a/core/libs/prx/libSceAgc/tests/Commands.cpp +++ b/core/libs/prx/libSceAgc/tests/Commands.cpp @@ -462,8 +462,14 @@ void testRegisterRange() { check(packet == storage.words.data() + 6 && storage.buffer.cursor_up == storage.words.data() + 12, "incorrect populated register range allocation"); check(storage.words == expected, "register values were not copied correctly"); const auto* misaligned = reinterpret_cast(reinterpret_cast(values.data()) + 1); - expectFailure([&] { sceAgcCbSetShRegisterRangeDirect(&storage.buffer, 0x8c, misaligned, 4); }); - check(storage.words == expected && storage.buffer.cursor_up == storage.words.data() + 12, "misaligned register values modified command buffer"); + packet = sceAgcCbSetShRegisterRangeDirect(&storage.buffer, 0x8c, misaligned, 3); + expected[12] = 0xc0037600u; + expected[13] = 0x8c; + expected[14] = 0x02000000u; + expected[15] = 0x03000000u; + expected[16] = 0x04000000u; + check(packet == storage.words.data() + 12 && storage.buffer.cursor_up == storage.words.data() + 17, "incorrect misaligned register range allocation"); + check(storage.words == expected, "misaligned register values were not read byte for byte"); } void testPacketPayloadAddress() { diff --git a/core/libs/prx/libSceAgc/tests/RegisterCommands.cpp b/core/libs/prx/libSceAgc/tests/RegisterCommands.cpp index a1e0295daf..5e6be0c709 100644 --- a/core/libs/prx/libSceAgc/tests/RegisterCommands.cpp +++ b/core/libs/prx/libSceAgc/tests/RegisterCommands.cpp @@ -8,6 +8,8 @@ extern "C" { std::uint32_t* APS5_VABI sceAgcDcbSetCfRegisterDirect(CommandBuffer*, ShaderRegister); std::uint32_t* APS5_VABI sceAgcDcbSetCfRegisterRangeDirect(CommandBuffer*, std::uint32_t, const std::uint32_t*, std::uint32_t); +std::uint32_t* APS5_VABI sceAgcCbSetShRegisterRangeDirect(CommandBuffer*, std::uint32_t, const std::uint32_t*, std::uint32_t); +std::uint32_t* APS5_VABI sceAgcCbSetUcRegisterRangeDirect(CommandBuffer*, std::uint32_t, const std::uint32_t*, std::uint32_t); std::uint32_t* APS5_VABI sceAgcDcbSetCxRegisterDirect(CommandBuffer*, ShaderRegister); std::uint32_t* APS5_VABI sceAgcDcbSetShRegisterDirect(CommandBuffer*, ShaderRegister); std::uint32_t* APS5_VABI sceAgcDcbSetUcRegisterDirect(CommandBuffer*, ShaderRegister); @@ -92,11 +94,37 @@ void testConfig() { expectFailure([&] { sceAgcDcbSetCfRegisterDirect(nullptr, {0, 1}); }); expectFailure([&] { sceAgcDcbSetCfRegisterRangeDirect(&storage.buffer, 0x100u, values.data(), 0); }); expectFailure([&] { sceAgcDcbSetCfRegisterRangeDirect(&storage.buffer, 0xffffu, values.data(), 2); }); - const auto* misaligned = reinterpret_cast(reinterpret_cast(values.data()) + 1); - expectFailure([&] { sceAgcDcbSetCfRegisterRangeDirect(&storage.buffer, 0x100u, misaligned, 1); }); check(storage.words == before && storage.buffer.cursor_up == reserved + 4, "invalid config register write modified buffer"); } +void testRangeFromAnyByte() { + const std::array writers{sceAgcCbSetShRegisterRangeDirect, sceAgcCbSetUcRegisterRangeDirect, sceAgcDcbSetCfRegisterRangeDirect}; + const std::array headers{0xc0047600u, 0xc0047900u, 0xc0046800u}; + alignas(16) std::array bytes{}; + for (std::size_t i = 0; i < bytes.size(); ++i) bytes[i] = static_cast(0x10u + i); + struct Case { + std::size_t offset; + std::array values; + }; + const std::array cases{ + Case{1, {0x14131211u, 0x18171615u, 0x1c1b1a19u, 0x201f1e1du}}, + Case{2, {0x15141312u, 0x19181716u, 0x1d1c1b1au, 0x21201f1eu}}, + Case{3, {0x16151413u, 0x1a191817u, 0x1e1d1c1bu, 0x2221201fu}}, + Case{13, {0x201f1e1du, 0x24232221u, 0x28272625u, 0x2c2b2a29u}}, + }; + for (std::size_t i = 0; i < writers.size(); ++i) { + for (const auto& entry : cases) { + Storage storage; + storage.words.fill(0xabcdef01u); + const auto* values = reinterpret_cast(bytes.data() + entry.offset); + auto* packet = writers[i](&storage.buffer, 0x40u, values, 4); + check(packet == storage.words.data() && storage.buffer.cursor_up == packet + 6, "unaligned register range allocation mismatch"); + check(packet[0] == headers[i] && packet[1] == 0x40u && std::equal(entry.values.begin(), entry.values.end(), packet + 2), "unaligned register values were not read byte for byte"); + check(packet[6] == 0xabcdef01u, "unaligned register range overwrote the following word"); + } + } +} + void testIndirect() { const std::array writers{sceAgcDcbSetCxRegistersIndirect, sceAgcDcbSetShRegistersIndirect, sceAgcDcbSetUcRegistersIndirect}; const std::array sizes{sceAgcDcbSetCxRegistersIndirectGetSize, sceAgcDcbSetShRegistersIndirectGetSize, sceAgcDcbSetUcRegistersIndirectGetSize}; @@ -210,6 +238,7 @@ int main() { try { testDirect(); testConfig(); + testRangeFromAnyByte(); testIndirect(); testIndirectPlaceholder(); testDirectList(); From e5b4bd72dde2ea97670259961be935b56a54f1a4 Mon Sep 17 00:00:00 2001 From: HQasper <52220032+HQasper@users.noreply.github.com> Date: Sun, 18 Oct 2026 18:15:06 +0200 Subject: [PATCH 026/269] fix(recompiler): write a coherent dwordx2 as one 64-bit atomic store A glc or dlc buffer_store_dwordx2 is one 64-bit write on the GPU, so a workgroup that polls a {flag, value} pair never sees the flag without its value. It was emitted as two dword stores, and the coherent dwordx2 load was one 64-bit read only in programs with 64-bit buffer atomics, the only ones that had the u64 storage-buffer view. GTA V builds each BLAS with a single-pass decoupled look-back scan that publishes its tiles' pairs with buffer_store_dwordx2 and has no 64-bit atomics. A tile read an earlier tile's new flag with its old value 0, the scan came out short, the BVH got overlapping nodes and the refit kernel looped until the device was lost. A program with a coherent raw two-dword buffer load or store now has the u64 view whenever the device has shaderBufferInt64Atomics. Such a store whose dwords are in bounds, adjacent and 8-byte aligned is one device-scope OpAtomicStore. The load takes the same condition, which now also checks the second dword's bounds and that the dwords are adjacent. All other accesses keep the per-dword path. LookBackScan also runs its kernel with the two buffer_atomic_swap_x2 replaced by glc buffer_store_dwordx2. Without this change that variant fails its first whole scan. Co-Authored-By: Claude Opus 5.5 --- .../tests/execution/LookBackScan.cpp | 39 +++++++++---- .../include/SpirvBackend/SpirvAnalysis.hpp | 2 + .../SpirvBackend/src/SpirvAnalysis.cpp | 2 + .../SpirvBackend/src/SpirvEmitter.cpp | 2 + .../src/SpirvMemory/SpirvEmitterHelpers.cpp | 2 +- .../src/SpirvMemory/SpirvModuleSetup.cpp | 2 + .../SpirvBackend/src/SpirvRuntimeBuffer.cpp | 55 +++++++++++++++---- 7 files changed, 80 insertions(+), 24 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LookBackScan.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LookBackScan.cpp index 210d8e681d..8ec6a58bd2 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/LookBackScan.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LookBackScan.cpp @@ -24,6 +24,13 @@ alignas(256) constexpr std::array ScanCode{ 0xe0701000, 0x80030802, 0xbf810000, }; +constexpr auto StoreScanCode = [] { + auto code = ScanCode; + code[16] = 0xe0745000; + code[38] = 0xe0745000; + return code; +}(); + constexpr std::uint32_t Lanes = 64; constexpr std::uint32_t Blocks = 4096; @@ -41,9 +48,17 @@ std::array BufferDescriptor(const void* data, std::uint32_t by return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; } -void Run(AgcDriver::VulkanDevice& device, std::uint32_t groups) { - alignas(256) static std::array code{}; - code = ScanCode; +struct Variant { + const char* name; + const std::array& words; +}; + +constexpr std::array Variants{{{"buffer_atomic_swap_x2", ScanCode}, {"buffer_store_dwordx2", StoreScanCode}}}; + +void Run(AgcDriver::VulkanDevice& device, std::size_t variant, std::uint32_t groups) { + alignas(256) static std::array, Variants.size()> programs{}; + auto& code = programs[variant]; + code = Variants[variant].words; std::vector userData; for (const auto& descriptor : {BufferDescriptor(Counter.data(), sizeof(Counter)), BufferDescriptor(Aggregates.data(), sizeof(Aggregates)), BufferDescriptor(Pairs.data(), sizeof(Pairs)), BufferDescriptor(Prefixes.data(), sizeof(Prefixes))}) { userData.insert(userData.end(), descriptor.begin(), descriptor.end()); @@ -64,8 +79,8 @@ void Run(AgcDriver::VulkanDevice& device, std::uint32_t groups) { device.WaitIdle(); } -void CheckLookBack(AgcDriver::VulkanDevice& device, std::uint32_t block) { - const auto name = "look-back from block " + std::to_string(block); +void CheckLookBack(AgcDriver::VulkanDevice& device, std::size_t variant, std::uint32_t block) { + const auto name = std::string(Variants[variant].name) + " look-back from block " + std::to_string(block); Pairs.fill(0u); std::uint32_t prefix = 0; for (std::uint32_t b = 0; b < Blocks; ++b) Aggregates[b] = Value(b, block); @@ -77,20 +92,20 @@ void CheckLookBack(AgcDriver::VulkanDevice& device, std::uint32_t block) { Counter.fill(0u); Counter[0] = block; Prefixes.fill(0xdeadbeefu); - Run(device, 1); + Run(device, variant, 1); Require(Counter[0] == block + 1, name + ": the counter is " + std::to_string(Counter[0])); Require(Prefixes[block] == prefix, name + ": its prefix is " + std::to_string(Prefixes[block]) + ", expected " + std::to_string(prefix)); Require(Pairs[2 * block] == 2u, name + ": its flag is " + std::to_string(Pairs[2 * block]) + ", expected 2"); Require(Pairs[2 * block + 1] == prefix + Aggregates[block], name + ": it published " + std::to_string(Pairs[2 * block + 1]) + ", expected " + std::to_string(prefix + Aggregates[block])); } -void CheckWhole(AgcDriver::VulkanDevice& device, std::uint32_t seed) { - const auto name = "whole scan " + std::to_string(seed); +void CheckWhole(AgcDriver::VulkanDevice& device, std::size_t variant, std::uint32_t seed) { + const auto name = std::string(Variants[variant].name) + " whole scan " + std::to_string(seed); for (std::uint32_t b = 0; b < Blocks; ++b) Aggregates[b] = Value(b, seed); Counter.fill(0u); Pairs.fill(0u); Prefixes.fill(0xdeadbeefu); - Run(device, Blocks); + Run(device, variant, Blocks); std::uint32_t running = 0; for (std::uint32_t b = 0; b < Blocks; ++b) { Require(Prefixes[b] == running, name + ": block " + std::to_string(b) + "'s prefix is " + std::to_string(Prefixes[b]) + ", expected " + std::to_string(running)); @@ -113,8 +128,10 @@ int main() { std::printf("skipped, subgroup size %u cannot hold a wave32\n", device->Target().subgroupSize); return VulkanTestSkipped; } - for (const std::uint32_t block : {0u, 1u, 2u, 63u, 64u, 1000u, Blocks - 1}) CheckLookBack(*device, block); - for (std::uint32_t seed = 0; seed < 50; ++seed) CheckWhole(*device, seed); + for (std::size_t variant = 0; variant < Variants.size(); ++variant) { + for (const std::uint32_t block : {0u, 1u, 2u, 63u, 64u, 1000u, Blocks - 1}) CheckLookBack(*device, variant, block); + for (std::uint32_t seed = 0; seed < 50; ++seed) CheckWhole(*device, variant, seed); + } std::puts("look-back scan tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvAnalysis.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvAnalysis.hpp index ebfd8888e3..52ac6c6940 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvAnalysis.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvAnalysis.hpp @@ -25,6 +25,8 @@ struct SpirvRequirements { bool functionScratch = false; bool pixelValidMask = false; bool bufferInt64Atomics = false; + bool coherentBufferPairs = false; + bool bufferU64View = false; bool imageInt64Atomics = false; bool sharedInt64Atomics = false; bool float64 = false; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp index 60295387c7..89178ce940 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp @@ -373,6 +373,8 @@ SpirvRequirements AnalyzeProgramRequirements(const IrProgram& program) { } const auto& memory = program.Resources().memoryInfo.at(memoryIndex); requirements.coherentBuffers = requirements.coherentBuffers || (memory.coherent && !memory.gpuDescriptor); + const bool pair = inst->Opcode() == IrOpcode::LoadBufferU32x2 || inst->Opcode() == IrOpcode::StoreBufferU32x2; + requirements.coherentBufferPairs = requirements.coherentBufferPairs || (pair && memory.coherent && !memory.gpuDescriptor); if (!memory.planningOnly) { requirements.subgroupLocalInvocationId = requirements.subgroupLocalInvocationId || program.Resources().stage == IrShaderStage::Compute; } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp index 050316a6f7..0018960a3f 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp @@ -266,6 +266,8 @@ std::vector SpirvEmitter::Emit(const IrProgram& program, const Sh state.requirements.ldsLock = false; state.requirements.ldsBufferInt64Atomics = true; } + const bool int64Atomics = std::ranges::find(state.supportedCapabilities, static_cast(spv::CapabilityInt64Atomics)) != state.supportedCapabilities.end(); + state.requirements.bufferU64View = state.requirements.bufferInt64Atomics || (state.requirements.coherentBufferPairs && int64Atomics); if (HostSubgroupSingleLane(state)) { state.requirements.subgroupBallot = false; state.requirements.subgroupShuffle = false; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp index 7a704ea52d..d4b27836aa 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp @@ -440,7 +440,7 @@ void DefineDescriptors(SpirvEmitterState& state) { case DescriptorBindingKind::Buffers: state.storageBufferVariable = Define(ArrayType(StorageBufferBlockType(state)), "buffers"); if (state.requirements.coherentBufferAlias) state.storageBufferCoherentVariable = Define(ArrayType(StorageBufferBlockType(state)), "buffers_coherent"); - if (state.requirements.bufferInt64Atomics) state.storageBufferU64Variable = Define(ArrayType(StorageBufferU64BlockType(state)), "buffers_u64"); + if (state.requirements.bufferU64View) state.storageBufferU64Variable = Define(ArrayType(StorageBufferU64BlockType(state)), "buffers_u64"); if (state.storageBufferU64Variable != 0u || state.storageBufferCoherentVariable != 0u) { for (const auto variable : {state.storageBufferVariable, state.storageBufferCoherentVariable, state.storageBufferU64Variable}) { if (variable != 0u) state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationAliased); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp index ef3e1ac42d..c419a928c6 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp @@ -259,6 +259,8 @@ void DefineModule(SpirvEmitterState& state) { if (std::find(state.supportedCapabilities.begin(), state.supportedCapabilities.end(), static_cast(spv::CapabilityInt64Atomics)) == state.supportedCapabilities.end()) { FailEmit("64-bit buffer atomics need shaderBufferInt64Atomics"); } + } + if (state.requirements.bufferU64View) { state.module.EmitCapability(spv::CapabilityInt64); state.module.EmitCapability(spv::CapabilityInt64Atomics); } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp index 7820a803c3..c127beb77f 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp @@ -723,6 +723,27 @@ std::uint32_t formattedGpuCall(SpirvValueEmitContext& context, const IrValue& in return value; } +struct CoherentPair { + MemoryResourceAccess wide; + std::uint32_t index; + std::uint32_t usable; +}; + +std::optional coherentPair(SpirvValueEmitContext& context, const IrValue& instruction, const RuntimeDescriptor& resource, std::uint32_t components) { + auto& state = context.state; + const auto& memory = context.Memory(instruction); + if (components != 2u || !memory.coherent || memory.gpuDescriptor || memory.dataBits != 32u || state.storageBufferU64Variable == 0u) return std::nullopt; + const auto low = address(context, instruction, resource, 0u, 4u); + const auto high = address(context, instruction, resource, 4u, 4u); + const auto wide = directAccess(context, instruction, true); + const auto byte = EmitAddU32(state, low.guest, wide.byteOffset); + const auto index = Binary(state, spv::OpShiftRightLogical, TypeU32(state), byte, ConstantU32(state, 3u)); + const auto aligned = both(state, equal(state, baseMisalignment(context, instruction), ConstantU32(state, 0u)), equal(state, EmitAndConstant(state, byte, 7u), ConstantU32(state, 0u))); + const auto adjacent = equal(state, high.guest, EmitAddU32(state, low.guest, ConstantU32(state, 4u))); + const auto inBounds = both(state, both(state, low.inBounds, high.inBounds), EmitMemoryElementInBounds(state, wide, index)); + return CoherentPair{wide, index, both(state, both(state, aligned, adjacent), inBounds)}; +} + } std::uint32_t EmitRuntimeBufferLoad(SpirvValueEmitContext& context, const IrValue& instruction, std::uint32_t components) { @@ -742,15 +763,10 @@ std::uint32_t EmitRuntimeBufferLoad(SpirvValueEmitContext& context, const IrValu } return composite(state, components, values); }; - if (components != 2u || !memory.coherent || memory.gpuDescriptor || state.storageBufferU64Variable == 0u) return readComponents(); - const auto runtime = address(context, instruction, resource, 0u, 8u); - const auto wide = directAccess(context, instruction, true); - const auto byte = EmitAddU32(state, runtime.guest, wide.byteOffset); - const auto index = Binary(state, spv::OpShiftRightLogical, TypeU32(state), byte, ConstantU32(state, 3u)); - const auto aligned = both(state, equal(state, baseMisalignment(context, instruction), ConstantU32(state, 0u)), equal(state, EmitAndConstant(state, byte, 7u), ConstantU32(state, 0u))); - const auto inBounds = both(state, runtime.inBounds, EmitMemoryElementInBounds(state, wide, index)); - return EmitValueIfElse(state, both(state, aligned, inBounds), type, [&] { - const auto pointer = EmitStorageBufferElementPointer(state, wide, index, TypeStorageBufferU64ElementPointer(state)); + const auto pair = coherentPair(context, instruction, resource, components); + if (!pair) return readComponents(); + return EmitValueIfElse(state, pair->usable, type, [&] { + const auto pointer = EmitStorageBufferElementPointer(state, pair->wide, pair->index, TypeStorageBufferU64ElementPointer(state)); const auto value = state.module.AllocateId(); state.module.AddFunction(spv::OpAtomicLoad, TypeScalarU64(state), value, pointer, ConstantU32(state, spv::ScopeDevice), ConstantU32(state, spv::MemorySemanticsMaskNone)); const auto high = Binary(state, spv::OpShiftRightLogical, TypeScalarU64(state), value, BdaConstant(state, 32u)); @@ -769,10 +785,25 @@ void EmitRuntimeBufferStore(SpirvValueEmitContext& context, const IrValue& instr return; } const auto values = storeValues(state, storeData(context, instruction), components); - for (std::uint32_t component = 0; component < components; ++component) { - const auto access = address(context, instruction, resource, component * 4u, memory.dataBits / 8u); - EmitIfCondition(state, access.inBounds, [&] { writeBuffer(context, instruction, access.guest, values[component], memory.dataBits); }); + const auto writeComponents = [&] { + for (std::uint32_t component = 0; component < components; ++component) { + const auto access = address(context, instruction, resource, component * 4u, memory.dataBits / 8u); + EmitIfCondition(state, access.inBounds, [&] { writeBuffer(context, instruction, access.guest, values[component], memory.dataBits); }); + } + }; + const auto pair = coherentPair(context, instruction, resource, components); + if (!pair) { + writeComponents(); + return; } + EmitIfCondition(state, pair->usable, [&] { + const auto u64 = TypeScalarU64(state); + const auto high = Binary(state, spv::OpShiftLeftLogical, u64, Unary(state, spv::OpUConvert, u64, values[1]), BdaConstant(state, 32u)); + const auto value = Binary(state, spv::OpBitwiseOr, u64, Unary(state, spv::OpUConvert, u64, values[0]), high); + const auto pointer = EmitStorageBufferElementPointer(state, pair->wide, pair->index, TypeStorageBufferU64ElementPointer(state)); + state.module.AddFunction(spv::OpAtomicStore, pointer, ConstantU32(state, spv::ScopeDevice), ConstantU32(state, spv::MemorySemanticsMaskNone), value); + }); + EmitIfCondition(state, Unary(state, spv::OpLogicalNot, TypeBool(state), pair->usable), writeComponents); }); } From 1f9ac40808daa04c7f8576c1c92cf2a48811a56d Mon Sep 17 00:00:00 2001 From: milescoviello Date: Sun, 18 Oct 2026 16:54:31 +0000 Subject: [PATCH 027/269] fix(shader): print the shader hash of a SPIR-V emission failure in hex The SPIR-V emission failures printed the shader hash with std::to_string after "hash=0x", so the hash read as a hex number that was the decimal one. Shader 0x1173ae1000 was reported as hash=0x74955231232. Both now print it in hex, as the driver's own messages print the shader address. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 13 ++++ .../tests/SpirvFailureText.cpp | 75 +++++++++++++++++++ .../SpirvBackend/src/SpirvEmitter.cpp | 11 ++- 3 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/SpirvFailureText.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 5480263227..99413270d6 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -741,6 +741,19 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_spirv_failure_text_tests EXCLUDE_FROM_ALL tests/SpirvFailureText.cpp) +target_link_libraries(agc_spirv_failure_text_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_spirv_failure_text_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_spirv_failure_text_tests) +if(BUILD_TESTING) + set_target_properties(agc_spirv_failure_text_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_spirv_failure_text COMMAND agc_spirv_failure_text_tests) + set_tests_properties(agc_spirv_failure_text PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1") + if(WIN32) + set_tests_properties(agc_spirv_failure_text PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_spirv_lds_atomic_lock_tests EXCLUDE_FROM_ALL tests/SpirvLdsAtomicLock.cpp) target_link_libraries(agc_spirv_lds_atomic_lock_tests PRIVATE shader_recompiler_agc_driver libc) set_target_properties(agc_spirv_lds_atomic_lock_tests PROPERTIES CXX_EXTENSIONS OFF) diff --git a/core/libs/prx/libSceAgcDriver/tests/SpirvFailureText.cpp b/core/libs/prx/libSceAgcDriver/tests/SpirvFailureText.cpp new file mode 100644 index 0000000000..10559775ea --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/SpirvFailureText.cpp @@ -0,0 +1,75 @@ +#include "IntermediateRepresentation/IrProgram.hpp" +#include "Optimization/ShaderStageInputInfo.hpp" +#include "SpirvBackend/SpirvEmitter.hpp" +#include "PipelineSpecialization.hpp" +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace ShaderRecompiler; + +void require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +enum class Failure { MissingComputeInput, UnloweredInstruction }; + +std::string emissionFailure(Failure failure) { + IrProgram program; + program.SetWaveSize(32); + program.Resources().stage = IrShaderStage::Compute; + program.Resources().shaderHash = 0x1173ae1000ull; + program.Resources().srtPlanComplete = true; + program.Resources().resourceTrackingComplete = true; + program.Metadata().shaderInfoComplete = true; + program.Metadata().bindingLayoutComplete = true; + auto& block = program.CreateBlock(); + program.SetEntryBlock(block); + program.BlockOrder().push_back(&block); + program.Metadata().blockInfo.emplace_back(); + if (failure == Failure::UnloweredInstruction) block.AppendInstruction(&program.CreateValue(IrOpcode::Unreachable, IrType::Void)); + ShaderComputeInputInfo compute; + compute.threadsNum[0] = 32; + compute.threadsNum[1] = 1; + compute.threadsNum[2] = 1; + compute.hostSubgroupSize = 32; + compute.waveSize = 32; + ShaderStageInputInfo inputs; + if (failure != Failure::MissingComputeInput) inputs.compute = &compute; + CompiledBindingLayout bindings; + bindings.layout = program.Metadata().bindings; + SpirvTargetOptions target{}; + target.vulkanVersion = 0x00401000; + target.spirvVersion = 0x00010300; + target.subgroupSize = 32; + try { + static_cast(SpirvEmitter().Emit(program, inputs, bindings, target)); + } catch (const std::runtime_error& error) { + return error.what(); + } + throw std::runtime_error("the program emitted without a failure"); +} + +void requireFailure(Failure failure, std::string_view prefix, std::string_view reason) { + const auto text = emissionFailure(failure); + require(text.starts_with(prefix) && text.find(reason) != std::string::npos, "expected \"" + std::string(prefix) + "...\" with \"" + std::string(reason) + "\", got \"" + text + "\""); +} + +} + +int main() { + try { + requireFailure(Failure::MissingComputeInput, "SPIR-V emission failed: hash=0x1173ae1000 stage=", " reason=compute input info is missing"); + requireFailure(Failure::UnloweredInstruction, "SPIR-V emission failed: hash=0x1173ae1000 stage=", " opcode=Unreachable reason="); + } catch (const std::exception& error) { + std::fprintf(stderr, "SpirvFailureText: %s\n", error.what()); + return 1; + } + std::puts("SpirvFailureText: emission failures name the shader hash in hex"); + return 0; +} diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp index 050316a6f7..1f1f6d76b8 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -18,8 +19,14 @@ namespace ShaderRecompiler { namespace { +std::string FailureText(const IrProgram& program) { + char hash[24]; + std::snprintf(hash, sizeof(hash), "%llx", static_cast(program.Resources().shaderHash)); + return std::string("SPIR-V emission failed: hash=0x") + hash + " stage=" + std::to_string(static_cast(program.Resources().stage)); +} + [[noreturn]] void FailProgram(const IrProgram& program, const char* reason) { - throw std::runtime_error("SPIR-V emission failed: hash=0x" + std::to_string(program.Resources().shaderHash) + " stage=" + std::to_string(static_cast(program.Resources().stage)) + " reason=" + reason); + throw std::runtime_error(FailureText(program) + " reason=" + reason); } const ShaderWorkgroupInputInfo* ShaderWorkgroupInputFor(const SpirvEmitterState& state) { @@ -233,7 +240,7 @@ std::uint32_t SpirvValueEmitContext::Label(const IrBlock* block) const { } [[noreturn]] void SpirvValueEmitContext::Fail(const IrValue& inst, const char* reason) const { - throw std::runtime_error("SPIR-V emission failed: hash=0x" + std::to_string(state.program.Resources().shaderHash) + " stage=" + std::to_string(static_cast(state.program.Resources().stage)) + " opcode=" + std::string(IrOpcodeName(inst.Opcode())) + " reason=" + reason); + throw std::runtime_error(FailureText(state.program) + " opcode=" + std::string(IrOpcodeName(inst.Opcode())) + " reason=" + reason); } std::vector SpirvEmitter::Emit(const IrProgram& program, const CompiledBindingLayout& bindings, const SpirvTargetOptions& target) const { From 0730563b1c1cbb8bb3cc474b72d2e1806f9e6de3 Mon Sep 17 00:00:00 2001 From: AlxcBTW <187763517+AlxcBTW@users.noreply.github.com> Date: Fri, 16 Oct 2026 21:36:48 +0200 Subject: [PATCH 028/269] test(agc): keep the deferred failure report test independent of log coalescing Identical deferred write-back failure lines are now coalesced, so the test throws a distinct message per failure to count every printed line. --- core/libs/prx/libSceAgcDriver/tests/Recorder.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index ebb82761f2..91757db654 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -1148,7 +1148,8 @@ std::size_t DeferredFailureLines(Recorder& recorder, std::size_t count, const st } void deferredFailureReportTests(Recorder& recorder) { - const auto plain = [] { throw std::runtime_error("image table: poisoned entry"); }; + std::uint32_t serial = 0; + const auto plain = [&serial] { throw std::runtime_error("image table: poisoned entry " + std::to_string(serial++)); }; const auto unaligned = [](std::uint32_t instruction) { return [instruction] { throw BdaUnalignedAccess("BDA access failed: unaligned", instruction); }; }; Require(DeferredFailureLines(recorder, 12, plain) == 12, "a deferred failure other than an unaligned BDA access was not printed every time"); Require(DeferredFailureLines(recorder, 12, unaligned(0x5c8)) == 8, "the first 8 deferred reports of an unaligned BDA access were not the only ones printed"); From be1287b66c01891f9f1a6f24a58f4ec94b42c7b9 Mon Sep 17 00:00:00 2001 From: WallisSkill Date: Mon, 19 Oct 2026 01:02:01 +0700 Subject: [PATCH 029/269] fix(libSceVideoOut): coalesce controller output updates Every change of the pad output state sent a rumble, light bar and trigger effect update to SDL, and the trigger effect was sent again even when it had not changed. Titles that animate the light bar or drive rumble from haptic audio change that state every frame, so at 120 Hz about 110 reports a second were queued while SDL's HIDAPI rumble thread sends at most one per 10 ms. Different report kinds do not merge in its queue, so the backlog grew: rumble arrived ever later, and disconnecting the controller blocked the presenter until the queue drained. Updates now go out at most every 30 ms with the latest state, and the trigger effect is sent only when it changes or a controller connects. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceVideoOut/include/PadInput.hpp | 4 ++++ core/libs/prx/libSceVideoOut/src/PadInput.cpp | 15 +++++++++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceVideoOut/include/PadInput.hpp b/core/libs/prx/libSceVideoOut/include/PadInput.hpp index de1c799e0e..c7216c2587 100644 --- a/core/libs/prx/libSceVideoOut/include/PadInput.hpp +++ b/core/libs/prx/libSceVideoOut/include/PadInput.hpp @@ -5,6 +5,7 @@ #include "SDL_gamecontroller.h" #include "prx/libScePad/include/KeyboardMouse.hpp" #include "prx/libScePad/include/PadState.hpp" +#include #include class DisplayWindow; @@ -35,6 +36,9 @@ private: PadOutputState outputState{}; bool outputPending = false; std::chrono::steady_clock::time_point nextRumbleRefresh{}; + std::chrono::steady_clock::time_point nextOutputAllowed{}; + std::array sentTriggerEffect{}; + bool triggerEffectSent = false; }; #endif diff --git a/core/libs/prx/libSceVideoOut/src/PadInput.cpp b/core/libs/prx/libSceVideoOut/src/PadInput.cpp index 5478ece435..e7e3f553c6 100644 --- a/core/libs/prx/libSceVideoOut/src/PadInput.cpp +++ b/core/libs/prx/libSceVideoOut/src/PadInput.cpp @@ -59,6 +59,7 @@ void PadInput::openController(int deviceIndex) { SDL_GameControllerGetNumTouchpads(controller), SDL_GameControllerHasLED(controller) == SDL_TRUE, SDL_GameControllerHasRumbleTriggers(controller) == SDL_TRUE); enableSensors(); outputPending = true; + triggerEffectSent = false; } void PadInput::enableSensors() { @@ -94,8 +95,10 @@ void PadInput::applyOutput() { if ((rumbling || (triggerRumble && !isPs5)) && now >= nextRumbleRefresh) outputPending = true; else return; } + if (now < nextOutputAllowed) return; outputPending = false; nextRumbleRefresh = now + std::chrono::milliseconds(700); + nextOutputAllowed = now + std::chrono::milliseconds(30); constexpr Uint32 rumbleMs = 2000; SDL_GameControllerRumble(controller, static_cast(large * 257), static_cast(small * 257), rumbling ? rumbleMs : 0); if (SDL_GameControllerHasLED(controller) == SDL_TRUE) { @@ -104,11 +107,15 @@ void PadInput::applyOutput() { } if (outputState.triggerTouched) { if (isPs5) { - Uint8 effect[47] = {}; + std::array effect{}; effect[0] = 0x04 | 0x08; - std::memcpy(effect + 10, outputState.trigger[1].effect, 11); - std::memcpy(effect + 21, outputState.trigger[0].effect, 11); - SDL_GameControllerSendEffect(controller, effect, sizeof(effect)); + std::memcpy(effect.data() + 10, outputState.trigger[1].effect, 11); + std::memcpy(effect.data() + 21, outputState.trigger[0].effect, 11); + if (!triggerEffectSent || effect != sentTriggerEffect) { + SDL_GameControllerSendEffect(controller, effect.data(), static_cast(effect.size())); + sentTriggerEffect = effect; + triggerEffectSent = true; + } } else if (SDL_GameControllerHasRumbleTriggers(controller) == SDL_TRUE) { SDL_GameControllerRumbleTriggers(controller, static_cast(outputState.trigger[0].fallback * 257), static_cast(outputState.trigger[1].fallback * 257), triggerRumble ? rumbleMs : 0); } From eaa6d89ff981f558f8213ac1b3dd5f9abd51eb6f Mon Sep 17 00:00:00 2001 From: JJenkx Date: Sun, 18 Oct 2026 13:59:56 -0400 Subject: [PATCH 030/269] fix(shader): follow scheduled jump table index writes --- .../prx/libSceAgcDriver/tests/ControlFlow.cpp | 26 +++++++++++++++++++ .../tests/execution/ScalarSetpc.cpp | 24 +++++++++++++++++ .../ControlFlow/src/GraphBuilder.cpp | 13 +++++++--- 3 files changed, 60 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp b/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp index a5d0d015da..92a22769c4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp @@ -471,6 +471,31 @@ void verifyLongBranch(const std::string& name, std::span co require(source != graph.blocks.end() && source->terminator.kind == TerminatorKind::Branch && graph.FindBlock(source->terminator.trueBlock).startProgramCounter == target, name + ": the jump does not branch to " + std::to_string(target)); } +void verifyScheduledJumpTable() { + const std::vector code{ + 0x83888208u, 0x7e020280u, 0x8f088308u, 0x7e040280u, 0xbe8c1f00u, 0x800cff0cu, 0x00000048u, + 0x820d800du, 0xf4040386u, 0x10000000u, 0xbf8cc07fu, 0xbe901f00u, 0x80100e10u, 0x82110f11u, + 0xbe802010u, 0x7e020281u, 0xbf820003u, 0x7e020282u, 0xbf820001u, 0x7e020283u, 0xe0700000u, + 0x80000100u, 0xbf810000u, 0x0000000cu, 0u, 0x00000014u, 0u, 0x0000001cu, 0u, + }; + verifyJumpTable("scheduled two-dword table", code, 0x20u, {0xcu, 0x14u, 0x1cu}, {0x3cu, 0x44u, 0x4cu}); + static_cast(recompile("scheduled two-dword table", code, 32u)); + static_cast(recompile("scheduled two-dword table", code, 64u)); + auto vcc = patched(code, 0, {0x83ea826au}); + vcc = patched(std::move(vcc), 2, {0x8f6a836au}); + vcc = patched(std::move(vcc), 9, {0xd4000000u}); + verifyJumpTable("scheduled two-dword table through vcc", vcc, 0x20u, {0xcu, 0x14u, 0x1cu}, {0x3cu, 0x44u, 0x4cu}); + static_cast(recompile("scheduled two-dword table through vcc", vcc, 32u)); + static_cast(recompile("scheduled two-dword table through vcc", vcc, 64u)); + const std::vector> refused{ + patched(code, 1, {0xbe880380u}), + patched(code, 3, {0xbe880380u}), + patched(code, 1, {0xbf820001u}), + patched(code, 3, {0xbf850000u}), + }; + for (const auto& rejected : refused) require(refusal(rejected).find("unsupported dynamic s_setpc_b64") != std::string::npos, "a rewritten or conditional two-dword table index was accepted"); +} + void verifyNullSwappc() { const std::array jump{0xbefd210cu, 0xbf810000u}; const auto swappc = RdnaInstructionDecoder{}.Decode(jump).instructions.front(); @@ -1369,6 +1394,7 @@ done: } try { verifyNullSwappc(); + verifyScheduledJumpTable(); VerifyUserDataTail(); VerifyConstantHighTail(); verifyLargeLoopExitFallback(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ScalarSetpc.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ScalarSetpc.cpp index 669e125986..41069b2013 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ScalarSetpc.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ScalarSetpc.cpp @@ -274,6 +274,22 @@ void Run(AgcDriver::VulkanDevice& device, std::span code, s device.WaitIdle(); } +std::vector Scheduled(std::span original) { + std::vector code(original.begin(), original.end()); + code.insert(code.begin() + 25, 0x7e3c0280u); + code.insert(code.begin() + 24, 0x7e3e0280u); + const auto relocated = [](std::int32_t word) { return word + (word >= 24 ? 1 : 0) + (word >= 25 ? 1 : 0); }; + for (std::int32_t word = 0; word < static_cast(original.size()); ++word) { + const auto instruction = original[static_cast(word)]; + const auto opcode = instruction & 0xffff0000u; + if (opcode != 0xbf820000u && opcode != 0xbf850000u) continue; + const auto target = word + 1 + static_cast(instruction & 0xffffu); + const auto offset = relocated(target) - relocated(word) - 1; + code[static_cast(relocated(word))] = opcode | (static_cast(offset) & 0xffffu); + } + return code; +} + void CheckReversedSubtractRefused(AgcDriver::VulkanDevice& device) { std::string refusal; try { @@ -306,6 +322,14 @@ int main() { Check(Expected64); Run(*device, Wave64Code, 64, device->ComputeTarget(32)); Check(Expected64); + const auto scheduled32 = Scheduled(Wave32Code); + const auto scheduled64 = Scheduled(Wave64Code); + Run(*device, scheduled32, 32, device->ComputeTarget(32)); + Check(Expected32); + Run(*device, scheduled64, 64, device->Target()); + Check(Expected64); + Run(*device, scheduled64, 64, device->ComputeTarget(32)); + Check(Expected64); CheckReversedSubtractRefused(*device); std::puts("setpc tests passed"); return 0; diff --git a/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp b/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp index fe2c3565a3..73e98b1341 100644 --- a/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp +++ b/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp @@ -195,6 +195,8 @@ bool addsImmediate(const RdnaInstruction& instruction, std::uint32_t reg, std::u (isRegister(instruction.source1, RdnaOperandKind::ScalarRegister, reg) && isImmediate(instruction.source0, immediate))); } +bool findLastWriter(const RdnaProgram& program, std::uint32_t end, std::initializer_list indices, std::uint32_t& writer, std::uint32_t waveSize = 0u, std::uint32_t skipBranchesPast = 0u); + bool resolveJumpTable(const RdnaProgram& program, std::uint32_t index, BoundedJumpTable& result) { if (index < 9u) return false; const auto& branch = program.instructions[index]; @@ -223,8 +225,13 @@ bool resolveJumpTable(const RdnaProgram& program, std::uint32_t index, BoundedJu const auto& basePc = program.instructions[loadIndex - 3u]; const auto& baseLow = program.instructions[loadIndex - 2u]; const auto& baseHigh = program.instructions[loadIndex - 1u]; - const auto& shift = program.instructions[loadIndex - 4u]; - const auto& bound = program.instructions[loadIndex - 5u]; + const auto offsetReg = scalarIndex(load.source1); + std::uint32_t shiftIndex = 0, boundIndex = 0; + if (offsetReg == NoScalarRegister || + !findLastWriter(program, loadIndex - 3u, {offsetReg}, shiftIndex) || + !findLastWriter(program, shiftIndex, {offsetReg}, boundIndex)) return false; + const auto& shift = program.instructions[shiftIndex]; + const auto& bound = program.instructions[boundIndex]; const auto baseReg = load.source0.reg; std::uint32_t displacement = 0, carry = 0, shiftAmount = 0, maximum = 0; if (basePc.op != RdnaOpcode::SGetpcB64 || !isRegister(basePc.destination, RdnaOperandKind::ScalarRegister, baseReg) || @@ -294,7 +301,7 @@ bool writesScalar(const RdnaInstruction& instruction, std::uint32_t index, std:: return covers(instruction.destination) || covers(instruction.destination2); } -bool findLastWriter(const RdnaProgram& program, std::uint32_t end, std::initializer_list indices, std::uint32_t& writer, std::uint32_t waveSize = 0u, std::uint32_t skipBranchesPast = 0u) { +bool findLastWriter(const RdnaProgram& program, std::uint32_t end, std::initializer_list indices, std::uint32_t& writer, std::uint32_t waveSize, std::uint32_t skipBranchesPast) { for (auto position = end; position-- > 0u;) { const auto& instruction = program.instructions[position]; const bool skipped = skipBranchesPast != 0u && IsDirectBranchOpcode(instruction.op) && instruction.op != RdnaOpcode::SBranch && instruction.branchTarget > skipBranchesPast; From ffa63e8fec5b7d0037474f8673c56981267b5ecc Mon Sep 17 00:00:00 2001 From: WallisSkill Date: Mon, 19 Oct 2026 01:09:43 +0700 Subject: [PATCH 031/269] fix(libSceVideoOut): rumble a DualSense from haptic audio without a sound card AudioOut2 turns controller haptic PCM into motor rumble only when no controller sound card plays it, which is the case for a DualSense over Bluetooth. PadInput then dropped that rumble for every DualSense, so titles that vibrate through haptic audio never rumbled one over Bluetooth. A DualSense now takes it like any other controller; with a sound card no haptic rumble is submitted, so nothing plays twice. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceVideoOut/src/PadInput.cpp | 4 ++-- docs/dev/TechnicalDebt.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceVideoOut/src/PadInput.cpp b/core/libs/prx/libSceVideoOut/src/PadInput.cpp index 5478ece435..5cb167e2f4 100644 --- a/core/libs/prx/libSceVideoOut/src/PadInput.cpp +++ b/core/libs/prx/libSceVideoOut/src/PadInput.cpp @@ -86,8 +86,8 @@ void PadInput::applyOutput() { if (controller == nullptr) return; const auto now = std::chrono::steady_clock::now(); const bool isPs5 = SDL_GameControllerGetType(controller) == SDL_CONTROLLER_TYPE_PS5; - const auto large = isPs5 ? outputState.vibrationLarge : std::max(outputState.vibrationLarge, outputState.audioVibrationLarge); - const auto small = isPs5 ? outputState.vibrationSmall : std::max(outputState.vibrationSmall, outputState.audioVibrationSmall); + const auto large = std::max(outputState.vibrationLarge, outputState.audioVibrationLarge); + const auto small = std::max(outputState.vibrationSmall, outputState.audioVibrationSmall); const bool rumbling = large != 0 || small != 0; const bool triggerRumble = outputState.trigger[0].fallback != 0 || outputState.trigger[1].fallback != 0; if (!outputPending) { diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index cfbf2e14f0..be43a0d2e8 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -24,6 +24,6 @@ Throughout the project, every function at every stage either **does exactly what ### Functional -- Controller haptic PCM drives Xbox-style motor rumble from its per-channel RMS (large/small motor, scale 2x, below 0.01 ignored, 80 ms hold); the mapping approximates waveform energy and is not measured against console hardware +- Controller haptic PCM that no controller sound card plays (a DualSense over Bluetooth, or any other controller) drives motor rumble from its per-channel RMS (large/small motor, scale 2x, below 0.01 ignored, 80 ms hold); the mapping approximates waveform energy and is not measured against console hardware - [Cancellation coverage](ARCHITECTURE.md#thread-cancellation) checks deferred request handling and recovery, not full asynchronous support or console ABI compatibility. Cancellation during file/socket I/O, sleep interruption, cancellation versus exit/detach, condition signal conservation under cancellation, and safe rejection of non-null invalid or reclaimed thread handles remain unverified. No console title or homebrew establishes the cleanup NIDs or buffer layout. Accepted asynchronous mode currently has no diagnostic for its deferred behavior. - `--to-intel` guest module trampolines are covered only by a synthetic relinker test; no game title has been verified with them on Linux or Windows. From 8e2f71263c7c16612dd36f04052a1ab555a24449 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Sun, 18 Oct 2026 13:49:15 +0000 Subject: [PATCH 032/269] fix(libkernel): return ENOENT for measured sysctl MIBs --- .../System/src/SystemConfiguration.cpp | 21 +++++++++ core/libs/tests/GuestSystemConfiguration.cpp | 44 +++++++++++++++++++ docs/dev/debt/libkernel.md | 1 + 3 files changed, 66 insertions(+) diff --git a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp index 3205d75fce..dd16dd67d9 100644 --- a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp +++ b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp @@ -47,6 +47,22 @@ bool IsLeafMib(const int* name) { return (name[0] == 6 && (name[1] == 2 || name[1] == 3 || name[1] == 5 || name[1] == 7)) || (name[0] == 1 && name[1] == 37); } +bool IsMeasuredAbsentMib(const int* name, std::uint32_t nameLength) { + static constexpr struct { + int mib[3]; + std::uint32_t length; + } absent[] = { + {{1, 2147482816, 2147482814}, 3}, + {{1, 2147483151}, 2}, + {{1, 2147483182, 2147483181}, 3}, + {{1, 2147482838, 2147482834}, 3}, + }; + for (const auto& entry : absent) { + if (nameLength == entry.length && std::memcmp(name, entry.mib, entry.length * sizeof(int)) == 0) return true; + } + return false; +} + } extern "C" { @@ -100,6 +116,11 @@ int APS5_VABI sysctl_nid_postfix(const int* name, std::uint32_t nameLength, void *__error_nid_postfix() = 14; return -1; } + if (IsMeasuredAbsentMib(name, nameLength)) { + if (newValue != nullptr) throw std::runtime_error(std::string(__func__) + ": writing an unmeasured absent MIB"); + *__error_nid_postfix() = 2; + return -1; + } if (nameLength == 2 && name[0] == 6 && name[1] == 7) { if (newValue != nullptr) { *__error_nid_postfix() = 1; diff --git a/core/libs/tests/GuestSystemConfiguration.cpp b/core/libs/tests/GuestSystemConfiguration.cpp index 5c3f0922f4..e22964c6ea 100644 --- a/core/libs/tests/GuestSystemConfiguration.cpp +++ b/core/libs/tests/GuestSystemConfiguration.cpp @@ -209,6 +209,49 @@ static void CheckLeafWithTrailingComponent() { Require(length == sizeof(buffer)); } } +static void CheckMeasuredAbsentSysctlMibs() { + static constexpr struct { + int mib[3]; + std::uint32_t length; + } cases[] = { + {{1, 2147482816, 2147482814}, 3}, + {{1, 2147483151}, 2}, + {{1, 2147483182, 2147483181}, 3}, + {{1, 2147482838, 2147482834}, 3}, + }; + for (const auto& entry : cases) { + unsigned char buffer[64]; + std::memset(buffer, 0xaa, sizeof(buffer)); + std::size_t length = sizeof(buffer); + *__error_nid_postfix() = 77; + Require(sysctl_nid_postfix(entry.mib, entry.length, buffer, &length, nullptr, 0) == -1); + Require(*__error_nid_postfix() == 2 && length == sizeof(buffer)); + for (const auto byte : buffer) Require(byte == 0xaa); + + length = 77; + *__error_nid_postfix() = 77; + Require(sysctl_nid_postfix(entry.mib, entry.length, nullptr, &length, nullptr, 0) == -1); + Require(*__error_nid_postfix() == 2 && length == 77); + + std::memset(buffer, 0xaa, sizeof(buffer)); + *__error_nid_postfix() = 77; + Require(sysctl_nid_postfix(entry.mib, entry.length, buffer, nullptr, nullptr, 0) == -1); + Require(*__error_nid_postfix() == 2); + for (const auto byte : buffer) Require(byte == 0xaa); + + *__error_nid_postfix() = 77; + Require(sysctl_nid_postfix(entry.mib, entry.length, nullptr, nullptr, nullptr, 0) == -1); + Require(*__error_nid_postfix() == 2); + + bool threw = false; + try { + sysctl_nid_postfix(entry.mib, entry.length, buffer, &length, buffer, sizeof(buffer)); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw); + } +} static void CheckSystemSwVersion() { *__error_nid_postfix() = 13; Require(sceKernelGetSystemSwVersion_nid_postfix(nullptr) == SCE_KERNEL_ERROR_EFAULT && *__error_nid_postfix() == 13); @@ -427,6 +470,7 @@ int main() { CheckMachdepCachelineSizeSysctl(); CheckPathnameSysctl(); CheckLeafWithTrailingComponent(); + CheckMeasuredAbsentSysctlMibs(); CheckSystemSwVersion(); CheckPseudoRandomSysctl(); CheckSdkVersionSysctl(); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index b4adfa1c64..fbaf065c1f 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -210,3 +210,4 @@ - [sceKernelVirtualQuery](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) returns [VirtualQueryInfo](../../../core/libs/SceTypes.hpp) with a one-byte flag allocation unit so Windows and SysV hosts return the same 72-byte representation, with the name at byte 33 and GPU mask at byte 65. This preserves the existing SysV representation; the console offsets beyond the address, protection and size fields observed in Astro Bot's AGC calls are not hardware-measured. Size and offset assertions plus a raw-byte named-mapping regression guard the host ABI. - [app0.zar files](../../../core/libs/prx/libkernel/File/src/ArchivedFile.cpp) (libkernel) - `sceKernelOpen`, `open`, `sceKernelStat` and `sceKernelCheckReachability` resolve a path missing from `app0/` in `app0.zar`. An archived file is a read-only host null device whose reads, positioned and vectored reads, AIO reads, seeks, `fstat` and `FIONREAD` are served from the archive, with the position shared by `dup`, `dup2` and `F_DUPFD`; writes fail with `EBADF` as on a read-only descriptor, opening one for writing or truncation fails with `EROFS` and `O_CREAT | O_EXCL` with `EEXIST`. `fstat` and `sceKernelStat` report mode `0444` (`0555` for a directory), the node index as inode and the archive file's device and times. Opening an archived directory or a directory present both on disk and in the archive throws, as `getdents` would miss entries. On an archived descriptor `mmap`, `flock`, record locks, `fstatfs`, `ftruncate`, `fsync`, `fdatasync`, `getdirentries`, `fchmod` and `futimes` throw instead of acting on the null device; `mmap` is `noexcept`, so its refusal terminates the process. `lstat` and the other path calls on archived files are not implemented - [param.sfo](../../../core/libs/prx/libkernel/AppMetadata/src/ParamSfoParser.cpp) (libkernel) is read when `param.json` is absent, for `TITLE`, `TITLE_ID`, `DOWNLOAD_DATA_SIZE` and `USER_DEFINED_PARAM_1` to `_4`, and mounts `/data` and `/download0` like `param.json`; the localized `TITLE_00` to `TITLE_30` are not selected by the console language. +- [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. The names' `sysctlnametomib` mappings are not implemented here. From b42de6cf9e83e8435389d16b36836e7bc67172af Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Sun, 18 Oct 2026 14:45:59 +0000 Subject: [PATCH 033/269] docs: clarify sysctlnametomib MIB coverage --- 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 fbaf065c1f..1e6a2f3143 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -210,4 +210,4 @@ - [sceKernelVirtualQuery](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) returns [VirtualQueryInfo](../../../core/libs/SceTypes.hpp) with a one-byte flag allocation unit so Windows and SysV hosts return the same 72-byte representation, with the name at byte 33 and GPU mask at byte 65. This preserves the existing SysV representation; the console offsets beyond the address, protection and size fields observed in Astro Bot's AGC calls are not hardware-measured. Size and offset assertions plus a raw-byte named-mapping regression guard the host ABI. - [app0.zar files](../../../core/libs/prx/libkernel/File/src/ArchivedFile.cpp) (libkernel) - `sceKernelOpen`, `open`, `sceKernelStat` and `sceKernelCheckReachability` resolve a path missing from `app0/` in `app0.zar`. An archived file is a read-only host null device whose reads, positioned and vectored reads, AIO reads, seeks, `fstat` and `FIONREAD` are served from the archive, with the position shared by `dup`, `dup2` and `F_DUPFD`; writes fail with `EBADF` as on a read-only descriptor, opening one for writing or truncation fails with `EROFS` and `O_CREAT | O_EXCL` with `EEXIST`. `fstat` and `sceKernelStat` report mode `0444` (`0555` for a directory), the node index as inode and the archive file's device and times. Opening an archived directory or a directory present both on disk and in the archive throws, as `getdents` would miss entries. On an archived descriptor `mmap`, `flock`, record locks, `fstatfs`, `ftruncate`, `fsync`, `fdatasync`, `getdirentries`, `fchmod` and `futimes` throw instead of acting on the null device; `mmap` is `noexcept`, so its refusal terminates the process. `lstat` and the other path calls on archived files are not implemented - [param.sfo](../../../core/libs/prx/libkernel/AppMetadata/src/ParamSfoParser.cpp) (libkernel) is read when `param.json` is absent, for `TITLE`, `TITLE_ID`, `DOWNLOAD_DATA_SIZE` and `USER_DEFINED_PARAM_1` to `_4`, and mounts `/data` and `/download0` like `param.json`; the localized `TITLE_00` to `TITLE_30` are not selected by the console language. -- [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. The names' `sysctlnametomib` mappings are not implemented here. +- [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. From 854032843172a09e96323aadd39539a712ebafc3 Mon Sep 17 00:00:00 2001 From: WallisSkill Date: Mon, 19 Oct 2026 02:14:44 +0700 Subject: [PATCH 034/269] fix(libSceVideoOut): pace only the light bar and resend refused effects The 30 ms limit gated all controller output, so a rumble pulse shorter than the interval could be lost and a stop could wait up to 30 ms. Rumble goes out on every output change again, as on main; SDL already skips repeated values and merges consecutive rumble reports. Only the light bar is paced: a changed colour is sent at most every 30 ms, always the latest one, and a colour SDL refuses is retried after the interval. The trigger effect is cached only when SDL_GameControllerSendEffect succeeds, so a refused effect is sent again on the next update as it was before. LightBarPacer and TriggerEffectCache hold these decisions; pad_output_pacing covers changes, retries, a new controller and a light bar animated every frame at 120 Hz. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceVideoOut/CMakeLists.txt | 9 ++ .../prx/libSceVideoOut/include/PadInput.hpp | 7 +- .../include/PadOutputPacing.hpp | 63 +++++++++++++ core/libs/prx/libSceVideoOut/src/PadInput.cpp | 23 +++-- .../libSceVideoOut/tests/PadOutputPacing.cpp | 89 +++++++++++++++++++ 5 files changed, 175 insertions(+), 16 deletions(-) create mode 100644 core/libs/prx/libSceVideoOut/include/PadOutputPacing.hpp create mode 100644 core/libs/prx/libSceVideoOut/tests/PadOutputPacing.cpp diff --git a/core/libs/prx/libSceVideoOut/CMakeLists.txt b/core/libs/prx/libSceVideoOut/CMakeLists.txt index b8e4ba2743..4528be4350 100644 --- a/core/libs/prx/libSceVideoOut/CMakeLists.txt +++ b/core/libs/prx/libSceVideoOut/CMakeLists.txt @@ -140,6 +140,15 @@ if(BUILD_TESTING) set_tests_properties(pad_input_mapping PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(pad_output_pacing_tests tests/PadOutputPacing.cpp) + target_include_directories(pad_output_pacing_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(pad_output_pacing_tests PRIVATE libc) + configure_windows_unwind(pad_output_pacing_tests) + add_test(NAME pad_output_pacing COMMAND pad_output_pacing_tests) + if(WIN32) + set_tests_properties(pad_output_pacing PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() if(BUILD_TESTING) diff --git a/core/libs/prx/libSceVideoOut/include/PadInput.hpp b/core/libs/prx/libSceVideoOut/include/PadInput.hpp index c7216c2587..03c2a49f59 100644 --- a/core/libs/prx/libSceVideoOut/include/PadInput.hpp +++ b/core/libs/prx/libSceVideoOut/include/PadInput.hpp @@ -5,7 +5,7 @@ #include "SDL_gamecontroller.h" #include "prx/libScePad/include/KeyboardMouse.hpp" #include "prx/libScePad/include/PadState.hpp" -#include +#include "prx/libSceVideoOut/include/PadOutputPacing.hpp" #include class DisplayWindow; @@ -36,9 +36,8 @@ private: PadOutputState outputState{}; bool outputPending = false; std::chrono::steady_clock::time_point nextRumbleRefresh{}; - std::chrono::steady_clock::time_point nextOutputAllowed{}; - std::array sentTriggerEffect{}; - bool triggerEffectSent = false; + LightBarPacer lightBar; + TriggerEffectCache triggerEffect; }; #endif diff --git a/core/libs/prx/libSceVideoOut/include/PadOutputPacing.hpp b/core/libs/prx/libSceVideoOut/include/PadOutputPacing.hpp new file mode 100644 index 0000000000..6061fcf618 --- /dev/null +++ b/core/libs/prx/libSceVideoOut/include/PadOutputPacing.hpp @@ -0,0 +1,63 @@ +#ifndef CORE_LIBS_PRX_LIBSCEVIDEOOUT_PADOUTPUTPACING_HPP +#define CORE_LIBS_PRX_LIBSCEVIDEOOUT_PADOUTPUTPACING_HPP + +#include +#include +#include + +class LightBarPacer { +public: + using Clock = std::chrono::steady_clock; + using Color = std::array; + static constexpr std::chrono::milliseconds Interval{30}; + + bool Pending(const Color& color) const { + return !sent || color != last; + } + + bool Due(const Color& color, Clock::time_point now) const { + return Pending(color) && now >= next; + } + + void Result(const Color& color, Clock::time_point now, bool succeeded) { + next = now + Interval; + if (!succeeded) return; + last = color; + sent = true; + } + + void Reset() { + sent = false; + next = {}; + } + +private: + Color last{}; + bool sent = false; + Clock::time_point next{}; +}; + +class TriggerEffectCache { +public: + using Effect = std::array; + + bool NeedsSend(const Effect& effect) const { + return !sent || effect != last; + } + + void Result(const Effect& effect, bool succeeded) { + if (!succeeded) return; + last = effect; + sent = true; + } + + void Reset() { + sent = false; + } + +private: + Effect last{}; + bool sent = false; +}; + +#endif diff --git a/core/libs/prx/libSceVideoOut/src/PadInput.cpp b/core/libs/prx/libSceVideoOut/src/PadInput.cpp index e7e3f553c6..2b71356e45 100644 --- a/core/libs/prx/libSceVideoOut/src/PadInput.cpp +++ b/core/libs/prx/libSceVideoOut/src/PadInput.cpp @@ -59,7 +59,8 @@ void PadInput::openController(int deviceIndex) { SDL_GameControllerGetNumTouchpads(controller), SDL_GameControllerHasLED(controller) == SDL_TRUE, SDL_GameControllerHasRumbleTriggers(controller) == SDL_TRUE); enableSensors(); outputPending = true; - triggerEffectSent = false; + lightBar.Reset(); + triggerEffect.Reset(); } void PadInput::enableSensors() { @@ -91,30 +92,28 @@ void PadInput::applyOutput() { const auto small = isPs5 ? outputState.vibrationSmall : std::max(outputState.vibrationSmall, outputState.audioVibrationSmall); const bool rumbling = large != 0 || small != 0; const bool triggerRumble = outputState.trigger[0].fallback != 0 || outputState.trigger[1].fallback != 0; + if (SDL_GameControllerHasLED(controller) == SDL_TRUE) { + const LightBarPacer::Color color = outputState.lightBarValid + ? LightBarPacer::Color{outputState.lightBar[0], outputState.lightBar[1], outputState.lightBar[2]} + : LightBarPacer::Color{0, 64, 255}; + if (lightBar.Due(color, now)) lightBar.Result(color, now, SDL_GameControllerSetLED(controller, color[0], color[1], color[2]) == 0); + } if (!outputPending) { if ((rumbling || (triggerRumble && !isPs5)) && now >= nextRumbleRefresh) outputPending = true; else return; } - if (now < nextOutputAllowed) return; outputPending = false; nextRumbleRefresh = now + std::chrono::milliseconds(700); - nextOutputAllowed = now + std::chrono::milliseconds(30); constexpr Uint32 rumbleMs = 2000; SDL_GameControllerRumble(controller, static_cast(large * 257), static_cast(small * 257), rumbling ? rumbleMs : 0); - if (SDL_GameControllerHasLED(controller) == SDL_TRUE) { - if (outputState.lightBarValid) SDL_GameControllerSetLED(controller, outputState.lightBar[0], outputState.lightBar[1], outputState.lightBar[2]); - else SDL_GameControllerSetLED(controller, 0, 64, 255); - } if (outputState.triggerTouched) { if (isPs5) { - std::array effect{}; + TriggerEffectCache::Effect effect{}; effect[0] = 0x04 | 0x08; std::memcpy(effect.data() + 10, outputState.trigger[1].effect, 11); std::memcpy(effect.data() + 21, outputState.trigger[0].effect, 11); - if (!triggerEffectSent || effect != sentTriggerEffect) { - SDL_GameControllerSendEffect(controller, effect.data(), static_cast(effect.size())); - sentTriggerEffect = effect; - triggerEffectSent = true; + if (triggerEffect.NeedsSend(effect)) { + triggerEffect.Result(effect, SDL_GameControllerSendEffect(controller, effect.data(), static_cast(effect.size())) == 0); } } else if (SDL_GameControllerHasRumbleTriggers(controller) == SDL_TRUE) { SDL_GameControllerRumbleTriggers(controller, static_cast(outputState.trigger[0].fallback * 257), static_cast(outputState.trigger[1].fallback * 257), triggerRumble ? rumbleMs : 0); diff --git a/core/libs/prx/libSceVideoOut/tests/PadOutputPacing.cpp b/core/libs/prx/libSceVideoOut/tests/PadOutputPacing.cpp new file mode 100644 index 0000000000..ab624ef5a4 --- /dev/null +++ b/core/libs/prx/libSceVideoOut/tests/PadOutputPacing.cpp @@ -0,0 +1,89 @@ +#include "prx/libSceVideoOut/include/PadOutputPacing.hpp" + +#include +#include +#include + +namespace { + +using namespace std::chrono_literals; + +void Require(bool condition, const char* message) { + if (condition) return; + std::fprintf(stderr, "pad_output_pacing: %s\n", message); + std::exit(1); +} + +void LightBarSendsChangesOnly() { + LightBarPacer pacer; + const auto start = LightBarPacer::Clock::time_point{} + 1s; + const LightBarPacer::Color blue{0, 64, 255}; + Require(pacer.Due(blue, start), "the first colour is sent at once"); + pacer.Result(blue, start, true); + Require(!pacer.Pending(blue), "a colour that was sent is not sent again"); + Require(!pacer.Due(blue, start + 1s), "an unchanged colour stays quiet"); + const LightBarPacer::Color red{255, 0, 0}; + Require(pacer.Pending(red) && !pacer.Due(red, start + 10ms), "a new colour waits for the interval"); + Require(pacer.Due(red, start + LightBarPacer::Interval), "a new colour goes out once the interval has passed"); +} + +void LightBarRetriesFailures() { + LightBarPacer pacer; + const auto start = LightBarPacer::Clock::time_point{} + 1s; + const LightBarPacer::Color green{0, 255, 0}; + pacer.Result(green, start, false); + Require(pacer.Pending(green), "a colour SDL refused is still pending"); + Require(!pacer.Due(green, start + 1ms), "a refused colour is not retried on the next frame"); + Require(pacer.Due(green, start + LightBarPacer::Interval), "a refused colour is retried after the interval"); + pacer.Result(green, start + LightBarPacer::Interval, true); + Require(!pacer.Pending(green), "a retried colour that was sent is done"); + pacer.Reset(); + Require(pacer.Due(green, start + LightBarPacer::Interval), "a new controller gets the current colour"); +} + +void LightBarAnimationIsPaced() { + LightBarPacer pacer; + const auto start = LightBarPacer::Clock::time_point{} + 1s; + constexpr int frames = 120; + int sent = 0; + LightBarPacer::Color color{}; + for (int frame = 0; frame < frames; ++frame) { + const auto now = start + std::chrono::microseconds(frame * 8333); + color = {static_cast(frame), 32, 32}; + if (!pacer.Due(color, now)) continue; + pacer.Result(color, now, true); + ++sent; + } + Require(sent >= 30 && sent <= 34, "a colour that changes every frame at 120 Hz goes out about 33 times a second"); + const auto end = start + 1s; + Require(pacer.Pending(color), "the last colour of the animation is still owed"); + Require(pacer.Due(color, end + LightBarPacer::Interval), "the last colour of the animation is sent when the interval allows"); +} + +void TriggerEffectRetriesFailures() { + TriggerEffectCache cache; + TriggerEffectCache::Effect effect{}; + effect[0] = 0x0C; + effect[10] = 0x21; + Require(cache.NeedsSend(effect), "the first effect is sent"); + cache.Result(effect, false); + Require(cache.NeedsSend(effect), "an effect SDL refused is sent again"); + cache.Result(effect, true); + Require(!cache.NeedsSend(effect), "an effect that was sent is not repeated"); + auto changed = effect; + changed[21] = 0x26; + Require(cache.NeedsSend(changed), "a changed effect is sent"); + cache.Reset(); + Require(cache.NeedsSend(effect), "a new controller gets the current effect"); +} + +} + +int main() { + LightBarSendsChangesOnly(); + LightBarRetriesFailures(); + LightBarAnimationIsPaced(); + TriggerEffectRetriesFailures(); + std::puts("pad_output_pacing: ok"); + return 0; +} From 2e363d375a50af6e77c5c61ecba7c581ed0f4449 Mon Sep 17 00:00:00 2001 From: billythemenace2 Date: Sun, 18 Oct 2026 21:24:44 +0100 Subject: [PATCH 035/269] fix(libkernel): redirect a raise only once the target leaves host code On Windows, sceKernelRaiseException suspends the target and redirects it to the guest handler wherever it stopped, except inside NtContinue or winpthreads. A target stopped inside host code (a C runtime call, a host library) can hold a host lock there; a handler that then waits, as a collector's thread-stop handler does, keeps it held, and every host thread that needs the lock stops with it. SONIC SUPERSTARS stopped at frame 65 this way: its "UDS" thread was redirected inside the stderr write of a trophy message and parked in sceKernelWaitEventFlag holding the stream lock, and the driver's logging threads blocked on it. A target running code of a module other than the executable or a guest module is now resumed and suspended again until it is back in guest code, for up to the existing retry limit; a target blocked in an ntdll wait is still redirected there, as before. Co-Authored-By: Claude Opus 5.5 (1M context) --- core/libs/prx/libkernel/CMakeLists.txt | 7 ++ .../prx/libkernel/System/src/Exception.cpp | 38 +++++++++- core/libs/tests/GuestRaiseInHostCode.cpp | 69 +++++++++++++++++++ 3 files changed, 112 insertions(+), 2 deletions(-) create mode 100644 core/libs/tests/GuestRaiseInHostCode.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 1bb3b681c8..bed3d86510 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -1597,6 +1597,13 @@ if(BUILD_TESTING) add_test(NAME guest_mutex_raise_exception COMMAND guest_mutex_raise_exception_tests) set_tests_properties(guest_mutex_raise_exception PROPERTIES TIMEOUT 20 ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + add_test_executable(guest_raise_in_host_code_tests ${LIBS_INCLUDE_DIR}/tests/GuestRaiseInHostCode.cpp) + target_include_directories(guest_raise_in_host_code_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_raise_in_host_code_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_raise_in_host_code_tests) + add_test(NAME guest_raise_in_host_code COMMAND guest_raise_in_host_code_tests) + set_tests_properties(guest_raise_in_host_code PROPERTIES TIMEOUT 30 ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") add_test_executable(guest_mutex_contention_tests ${LIBS_INCLUDE_DIR}/tests/GuestMutexContention.cpp) target_include_directories(guest_mutex_contention_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_mutex_contention_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/System/src/Exception.cpp b/core/libs/prx/libkernel/System/src/Exception.cpp index 6dbef36c89..be680ca0f0 100644 --- a/core/libs/prx/libkernel/System/src/Exception.cpp +++ b/core/libs/prx/libkernel/System/src/Exception.cpp @@ -4,6 +4,8 @@ #include "prx/libc/include/General.hpp" #include "prx/libkernel/KernelErrors.hpp" #include "prx/libkernel/Pthread/include/Pthread.hpp" +#include +#include #include #include #include @@ -12,6 +14,7 @@ #include #include #ifdef _WIN32 +#include #include #else #include @@ -249,6 +252,36 @@ bool InWinpthread(DWORD64 rip) { return winpthread != nullptr && VirtualQuery(reinterpret_cast(rip), &info, sizeof(info)) == sizeof(info) && info.AllocationBase == winpthread; } +bool InHostModule(DWORD64 rip) { + HMODULE module = nullptr; + if (!GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, reinterpret_cast(rip), &module) || module == nullptr) return false; + if (module == GetModuleHandleW(nullptr)) return false; + wchar_t path[MAX_PATH]{}; + const auto length = GetModuleFileNameW(module, path, MAX_PATH); + const std::wstring_view name(path, length); + constexpr std::wstring_view guestSuffix = L".guest.prx"; + return !(name.size() >= guestSuffix.size() && name.substr(name.size() - guestSuffix.size()) == guestSuffix); +} + +bool BlockedInWait(DWORD64 rip) { + static const std::vector waits = [] { + std::vector stubs; + const HMODULE ntdll = GetModuleHandleW(L"ntdll.dll"); + for (const char* name : {"NtWaitForSingleObject", "NtWaitForMultipleObjects", "NtWaitForAlertByThreadId", "NtDelayExecution", "NtRemoveIoCompletion", "NtRemoveIoCompletionEx", "NtWaitForKeyedEvent", "NtSignalAndWaitForSingleObject", "NtWaitForWorkViaWorkerFactory"}) { + if (const auto address = reinterpret_cast(GetProcAddress(ntdll, name)); address != 0) stubs.push_back(address); + } + if (const HMODULE win32u = GetModuleHandleW(L"win32u.dll"); win32u != nullptr) { + if (const auto address = reinterpret_cast(GetProcAddress(win32u, "NtUserMsgWaitForMultipleObjectsEx")); address != 0) stubs.push_back(address); + } + return stubs; + }(); + return std::any_of(waits.begin(), waits.end(), [rip](DWORD64 stub) { return rip - stub < 0x20; }); +} + +bool RunningHostCode(DWORD64 rip) { + return InHostModule(rip) && !BlockedInWait(rip); +} + bool RaiseOn(Pthread thread, GuestExceptionHandler handler, int signum) { if (thread == scePthreadSelf()) { CONTEXT context{}; @@ -281,9 +314,10 @@ bool RaiseOn(Pthread thread, GuestExceptionHandler handler, int signum) { queued.release(); return true; } - if (!RestoringContext(delivery.context.Rip) && !InWinpthread(delivery.context.Rip)) break; + const bool restoring = RestoringContext(delivery.context.Rip) || InWinpthread(delivery.context.Rip); + if (!restoring && (!RunningHostCode(delivery.context.Rip) || std::chrono::steady_clock::now() >= retryDeadline)) break; ResumeThread(native); - if (std::chrono::steady_clock::now() >= retryDeadline) throw std::runtime_error("sceKernelRaiseException: the target thread stayed inside NtContinue or winpthreads for 1 s"); + if (restoring && std::chrono::steady_clock::now() >= retryDeadline) throw std::runtime_error("sceKernelRaiseException: the target thread stayed inside NtContinue or winpthreads for 1 s"); SwitchToThread(); } const DWORD64 slot = (delivery.context.Rsp - RedZone - sizeof(Delivery)) & ~static_cast(15); diff --git a/core/libs/tests/GuestRaiseInHostCode.cpp b/core/libs/tests/GuestRaiseInHostCode.cpp new file mode 100644 index 0000000000..a3f728545d --- /dev/null +++ b/core/libs/tests/GuestRaiseInHostCode.cpp @@ -0,0 +1,69 @@ +#include "SceTypes.hpp" +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI scePthreadCreate(Pthread*, const PthreadAttr*, PthreadEntry, void*, const char*); +int APS5_VABI scePthreadJoin(Pthread, void**); +int APS5_VABI sceKernelInstallExceptionHandler(int, void*); +int APS5_VABI sceKernelRemoveExceptionHandler(int); +int APS5_VABI sceKernelRaiseException(Pthread, int); +} + +static constexpr int SigUsr1 = 30; +static constexpr int Raises = 20; +static void Require(bool value) { if (!value) std::abort(); } + +static std::FILE* shared = nullptr; +static std::atomic deliveries{0}; +static std::atomic released{0}; +static std::atomic stop{false}; +static std::atomic started{false}; + +static void APS5_VABI Handler(int, void*) { + const auto delivery = deliveries.fetch_add(1) + 1; + while (released.load() < delivery) std::this_thread::yield(); +} + +static void* APS5_VABI Writer(void*) { + const std::string block(1u << 20u, 'x'); + started.store(true); + while (!stop.load()) { + Require(std::fwrite(block.data(), 1, block.size(), shared) == block.size()); + Require(std::fseek(shared, 0, SEEK_SET) == 0); + volatile std::uint64_t spin = 0; + for (std::uint64_t i = 0; i < 200000; ++i) spin = spin + i; + } + return nullptr; +} + +int main() { + std::atomic done{false}; + std::thread watchdog([&done] { + for (int i = 0; i < 20000 && !done.load(); ++i) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + if (!done.load()) std::abort(); + }); + shared = std::tmpfile(); + Require(shared != nullptr); + Require(sceKernelInstallExceptionHandler(SigUsr1, reinterpret_cast(&Handler)) == 0); + Pthread writer = nullptr; + Require(scePthreadCreate(&writer, nullptr, Writer, nullptr, "host writer") == 0); + while (!started.load()) std::this_thread::yield(); + for (int raise = 1; raise <= Raises; ++raise) { + Require(sceKernelRaiseException(writer, SigUsr1) == 0); + while (deliveries.load() < raise) std::this_thread::yield(); + Require(std::fputc('y', shared) == 'y'); + released.store(raise); + } + stop.store(true); + Require(scePthreadJoin(writer, nullptr) == 0); + Require(sceKernelRemoveExceptionHandler(SigUsr1) == 0); + std::fclose(shared); + done.store(true); + watchdog.join(); +} From e10616b17bd8f2faa12633d80fb8dcda28a040b0 Mon Sep 17 00:00:00 2001 From: Amr Yasser <154547867+AMRYB@users.noreply.github.com> Date: Wed, 14 Oct 2026 04:14:51 +0300 Subject: [PATCH 036/269] fix(web): publish converter and libraries from one release --- .github/workflows/pages.yml | 16 +- .github/workflows/release.yml | 36 +++- CMakeLists.txt | 3 + docs/dev/BUILD.md | 8 + tools/build_web.py | 68 +++++++- tools/package_release.py | 43 ++++- tools/tests/test_web_release.py | 291 ++++++++++++++++++++++++++++++++ 7 files changed, 440 insertions(+), 25 deletions(-) create mode 100644 tools/tests/test_web_release.py diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 29891c5c96..0dc28284b0 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -9,6 +9,7 @@ on: - core/relinker/** - demo/web/** - tools/build_web.py + - tools/package_release.py - tools/nid_names.py - tools/progress.py - tools/progress_history.py @@ -42,21 +43,10 @@ jobs: with: ref: main fetch-depth: 0 - - name: Build web converter - shell: bash - run: | - git clone --depth 1 https://github.com/emscripten-core/emsdk.git "$RUNNER_TEMP/emsdk" - "$RUNNER_TEMP/emsdk/emsdk" install 6.0.3 - "$RUNNER_TEMP/emsdk/emsdk" activate 6.0.3 - source "$RUNNER_TEMP/emsdk/emsdk_env.sh" - emcmake cmake -S . -B build-wasm -DANYPS5_BUILD_WASM=ON -DBUILD_TESTING=OFF -DCMAKE_BUILD_TYPE=Release - cmake --build build-wasm --target relinker_web --parallel 2 - - name: Test web converter - run: python3 tools/test_web.py --build build-wasm - - name: Download latest release libraries + - name: Download matching release converter and libraries env: GITHUB_TOKEN: ${{ github.token }} - run: python3 tools/build_web.py --build build-wasm --libraries + run: python3 tools/build_web.py --build build-wasm --release - run: python3 tools/progress.py build-wasm/web - run: python3 tools/progress_history.py build-wasm/web - uses: actions/upload-pages-artifact@v5 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7fccd9c6a6..15448f4f8f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -41,6 +41,7 @@ jobs: steps: - uses: actions/checkout@v7 with: + ref: ${{ github.sha }} submodules: recursive - name: Point the WinLibs CMake at the CA bundle of Git for Windows if: matrix.platform == 'windows' @@ -110,9 +111,38 @@ jobs: if-no-files-found: error compression-level: 0 + web: + name: Build and test web converter + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ github.sha }} + - name: Build web converter + shell: bash + run: | + git clone --depth 1 https://github.com/emscripten-core/emsdk.git "$RUNNER_TEMP/emsdk" + "$RUNNER_TEMP/emsdk/emsdk" install 6.0.3 + "$RUNNER_TEMP/emsdk/emsdk" activate 6.0.3 + source "$RUNNER_TEMP/emsdk/emsdk_env.sh" + emcmake cmake -S . -B build-wasm -DANYPS5_BUILD_WASM=ON -DBUILD_TESTING=OFF -DCMAKE_BUILD_TYPE=Release + cmake --build build-wasm --target relinker_web --parallel 2 + - name: Test web converter + run: python3 tools/test_web.py --build build-wasm + - name: Package web converter + shell: bash + run: python3 tools/package_release.py --platform web --build build-wasm --output dist --version "$RELEASE_TAG" --revision "$(git rev-parse HEAD)" + - uses: actions/upload-artifact@v7 + with: + name: release-web + path: dist/* + if-no-files-found: error + compression-level: 0 + publish: name: Publish release - needs: build + needs: [build, web] runs-on: ubuntu-latest timeout-minutes: 10 permissions: @@ -122,6 +152,8 @@ jobs: GH_REPO: ${{ github.repository }} steps: - uses: actions/checkout@v7 + with: + ref: ${{ github.sha }} - uses: actions/download-artifact@v8 with: pattern: release-* @@ -130,7 +162,7 @@ jobs: - name: Check release files and collect documentation shell: bash run: | - for file in "prx-windows-$RELEASE_TAG.zip" "prx-windows-$RELEASE_TAG.tar.gz" "prx-linux-$RELEASE_TAG.zip" "prx-linux-$RELEASE_TAG.tar.gz" "relinker-$RELEASE_TAG.exe" "relinker-$RELEASE_TAG"; do + for file in "prx-windows-$RELEASE_TAG.zip" "prx-windows-$RELEASE_TAG.tar.gz" "prx-linux-$RELEASE_TAG.zip" "prx-linux-$RELEASE_TAG.tar.gz" "relinker-$RELEASE_TAG.exe" "relinker-$RELEASE_TAG" "web-$RELEASE_TAG.zip"; do test -s "dist/$file" done python tools/package_release.py --docs docs/user --output dist diff --git a/CMakeLists.txt b/CMakeLists.txt index 39e489e674..832c1b1a4b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -186,6 +186,9 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_nid_names.py) add_test(NAME release_package COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_package_release.py) + add_test(NAME web_release COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_web_release.py) + set_tests_properties(web_release PROPERTIES TIMEOUT 20) add_test(NAME web_library_assets COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_build_web.py) set_tests_properties(web_library_assets PROPERTIES TIMEOUT 20) diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index 03372da27e..11c6eae28e 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -35,6 +35,14 @@ git submodule update --init --recursive - 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. +## Web converter releases + +The Release workflow builds the web converter and the Linux and Windows libraries from the same commit. It tests the converter and publishes its frontend and WASM files as `web-.zip`, with the tag and commit recorded in `web-release.json`. + +Pages downloads the converter and both library archives from one release selected through GitHub's latest-release API. `release.json` records the same tag and commit. Missing, corrupt or mismatched assets fail the Pages job before deployment. Releases made before the web asset was added need a new release; Pages does not combine them with a converter built from `main`. + +Changes to the converter or its frontend reach Pages through a release. Progress reports still come from `main`. For local development, build `relinker_web` with Emscripten and run `python3 tools/test_web.py --build build-wasm`. To stage a complete published release without compiling, run `python3 tools/build_web.py --build build-wasm --release`; `--libraries` is an alias for this mode. + ## Commands ```sh diff --git a/tools/build_web.py b/tools/build_web.py index cd19113603..c62f3a080e 100644 --- a/tools/build_web.py +++ b/tools/build_web.py @@ -1,13 +1,17 @@ import argparse import hashlib +import io import json import os from pathlib import Path import re import shutil +import tempfile import urllib.request import zipfile +from package_release import WEB_FILES + ROOT = Path(__file__).resolve().parent.parent MAX_EXPANDED_BYTES = 512 * 1024 * 1024 @@ -28,9 +32,10 @@ def request(url): return urllib.request.urlopen(urllib.request.Request(url, headers=headers), timeout=60) -def publish_libraries(output): - with request("https://api.github.com/repos/boykopovar/AnyPS5/releases/latest") as response: - release = json.load(response) +def publish_libraries(output, release=None): + if release is None: + with request("https://api.github.com/repos/boykopovar/AnyPS5/releases/latest") as response: + release = json.load(response) tag = release["tag_name"] if not re.fullmatch(r"v[0-9A-Za-z][0-9A-Za-z._-]*", tag): raise ValueError("Invalid release tag") @@ -90,18 +95,69 @@ def publish_libraries(output): print(f"Published library assets from {tag}") +def publish_release(output): + with request("https://api.github.com/repos/boykopovar/AnyPS5/releases/latest") as response: + release = json.load(response) + tag = release["tag_name"] + if not re.fullmatch(r"v[0-9A-Za-z][0-9A-Za-z._-]*", tag) or tag.endswith("."): + raise ValueError("Invalid release tag") + name = f"web-{tag}.zip" + matches = [asset for asset in release["assets"] if asset["name"] == name] + if len(matches) != 1: + raise ValueError(f"Missing or ambiguous release asset: {name}; publish a release with the web converter") + asset = matches[0] + if not 0 < asset["size"] <= 128 * 1024 * 1024: + raise ValueError("Invalid web release archive size") + with request(asset["browser_download_url"]) as response: + data = response.read(asset["size"] + 1) + if len(data) != asset["size"]: + raise ValueError("Incomplete or oversized web release archive") + checksum = hashlib.sha256(data).hexdigest() + if asset.get("digest") and asset["digest"] != "sha256:" + checksum: + raise ValueError("Web release archive checksum mismatch") + output.parent.mkdir(parents=True, exist_ok=True) + with tempfile.TemporaryDirectory(prefix=".web-release-", dir=output.parent) as temporary: + staging = Path(temporary) + with zipfile.ZipFile(io.BytesIO(data)) as archive: + files = archive.infolist() + expected = {*WEB_FILES, "web-release.json"} + if len(files) != len(expected) or {file.filename for file in files} != expected: + raise ValueError("Invalid web release archive layout") + if any(file.file_size == 0 for file in files) or sum(file.file_size for file in files) > 128 * 1024 * 1024: + raise ValueError("Invalid expanded web release archive size") + metadata = json.loads(archive.read("web-release.json")) + if (not isinstance(metadata, dict) or metadata.get("tag") != tag + or not isinstance(metadata.get("revision"), str) + or not re.fullmatch(r"[0-9a-f]{40}", metadata["revision"])): + raise ValueError("Web converter does not match the selected release") + for file in files: + target = staging / file.filename + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(archive.read(file)) + publish_libraries(staging, release) + manifest = json.loads((staging / "release.json").read_text(encoding="utf-8")) + manifest["revision"] = metadata["revision"] + (staging / "release.json").write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8") + (staging / ".nojekyll").touch() + shutil.copytree(staging, output, dirs_exist_ok=True) + print(f"Published web converter and libraries from {tag} ({metadata['revision']})") + + def main(): parser = argparse.ArgumentParser() parser.add_argument("--build", type=Path, required=True) - parser.add_argument("--libraries", action="store_true") + parser.add_argument("--release", "--libraries", dest="release", action="store_true", + help="Download the matching converter and libraries from the latest release") args = parser.parse_args() output = args.build.resolve() / "web" + if args.release: + publish_release(output) + print(output) + return for name in ("relinker_wasm.mjs", "relinker_wasm.wasm"): if not (output / "native" / name).is_file(): raise ValueError(f"Build relinker_wasm first: missing {name}") copy_sources(output) - if args.libraries: - publish_libraries(output) print(output) diff --git a/tools/package_release.py b/tools/package_release.py index 20daabb45e..93063ceb65 100644 --- a/tools/package_release.py +++ b/tools/package_release.py @@ -1,4 +1,5 @@ import argparse +import json import os import re import shutil @@ -9,6 +10,8 @@ import zipfile from pathlib import Path ROOT = Path(__file__).resolve().parent.parent +WEB_FILES = ("index.html", "app.css", "app.mjs", "archive.mjs", "batch.mjs", "output-directory.mjs", "worker.mjs", + "native/relinker_wasm.mjs", "native/relinker_wasm.wasm") def windows_runtime(build): @@ -94,22 +97,54 @@ def collect_docs(source, output): shutil.copy2(document, output / document.name) +def package_web(build, output, version, revision): + if not re.fullmatch(r"v[0-9A-Za-z][0-9A-Za-z._-]*", version) or version.endswith("."): + raise ValueError(f"Invalid release tag for asset filenames: {version}") + if not re.fullmatch(r"[0-9a-f]{40}", revision): + raise ValueError("Web release revision must be a full commit SHA") + source = build / "web" + for name in WEB_FILES: + file = source / name + if not file.is_file() or file.stat().st_size == 0: + raise RuntimeError(f"Missing or empty web release file: {file}") + name = f"web-{version}.zip" + destination = output / name + if destination.exists() and not destination.is_file(): + raise RuntimeError(f"Release destination is not a file: {destination}") + output.mkdir(parents=True, exist_ok=True) + with tempfile.TemporaryDirectory(prefix=".release-", dir=output) as temporary: + asset = Path(temporary) / name + with zipfile.ZipFile(asset, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as archive: + for file in WEB_FILES: + archive.write(source / file, arcname=file) + archive.writestr("web-release.json", json.dumps({"tag": version, "revision": revision}) + "\n") + asset.replace(destination) + + def main(): parser = argparse.ArgumentParser() - parser.add_argument("--platform", choices=("linux", "windows")) + parser.add_argument("--platform", choices=("linux", "windows", "web")) parser.add_argument("--build", type=Path) parser.add_argument("--version") + parser.add_argument("--revision") parser.add_argument("--docs", type=Path) parser.add_argument("--output", required=True, type=Path) args = parser.parse_args() if args.docs is not None: - if args.platform is not None or args.build is not None or args.version is not None: - parser.error("--docs cannot be combined with --platform, --build or --version") + if args.platform is not None or args.build is not None or args.version is not None or args.revision is not None: + parser.error("--docs cannot be combined with --platform, --build, --version or --revision") collect_docs(args.docs, args.output) else: if args.platform is None or args.build is None or args.version is None: parser.error("--platform, --build and --version are required when --docs is not specified") - package(args.platform, args.build, args.output, args.version) + if args.platform == "web": + if args.revision is None: + parser.error("--revision is required for --platform web") + package_web(args.build, args.output, args.version, args.revision) + else: + if args.revision is not None: + parser.error("--revision requires --platform web") + package(args.platform, args.build, args.output, args.version) if __name__ == "__main__": diff --git a/tools/tests/test_web_release.py b/tools/tests/test_web_release.py new file mode 100644 index 0000000000..e314acd160 --- /dev/null +++ b/tools/tests/test_web_release.py @@ -0,0 +1,291 @@ +import hashlib +import io +import json +import subprocess +import sys +import tempfile +import unittest +import zipfile +from pathlib import Path +from unittest.mock import patch + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import build_web +import package_release + +FILES = ("index.html", "app.css", "app.mjs", "archive.mjs", "batch.mjs", "output-directory.mjs", "worker.mjs", + "native/relinker_wasm.mjs", "native/relinker_wasm.wasm") +REVISION = "a" * 40 +LATEST = "https://api.github.com/repos/boykopovar/AnyPS5/releases/latest" + + +def zipped(entries): + data = io.BytesIO() + with zipfile.ZipFile(data, "w", zipfile.ZIP_DEFLATED) as archive: + for name, content in entries: + archive.writestr(name, content) + return data.getvalue() + + +class WebReleaseTests(unittest.TestCase): + def setUp(self): + self.directory = tempfile.TemporaryDirectory() + self.addCleanup(self.directory.cleanup) + self.root = Path(self.directory.name) + self.build = self.root / "build" + self.output = self.build / "web" + self.output.mkdir(parents=True) + self.contents = {name: ("release " + name).encode() for name in FILES} + self.metadata = {"tag": "v1", "revision": REVISION} + self.entries = [*self.contents.items(), ("web-release.json", json.dumps(self.metadata).encode())] + self.responses = {} + self.release = {"tag_name": "v1", "html_url": "https://github.com/boykopovar/AnyPS5/releases/tag/v1", "assets": []} + self.set_asset("web-v1.zip", zipped(self.entries)) + for platform in ("windows", "linux"): + self.set_asset(f"prx-{platform}-v1.zip", zipped([("libs/libkernel.prx", platform.encode())])) + self.requests = [] + + def set_asset(self, name, content): + url = "https://example.test/" + name + self.responses[url] = content + asset = {"name": name, "browser_download_url": url, "size": len(content), + "digest": "sha256:" + hashlib.sha256(content).hexdigest()} + self.release["assets"] = [item for item in self.release["assets"] if item["name"] != name] + [asset] + return asset + + def request(self, url): + self.requests.append(url) + if url == LATEST: + if self.requests.count(LATEST) > 1: + raise AssertionError("Release selection changed during publication") + return io.BytesIO(json.dumps(self.release).encode()) + return io.BytesIO(self.responses[url]) + + def publish(self): + with patch.object(build_web, "request", self.request): + build_web.publish_release(self.output) + + def snapshot(self): + return {path.relative_to(self.output).as_posix(): path.read_bytes() + for path in self.output.rglob("*") if path.is_file()} + + def seed_previous(self): + for name, content in {"index.html": b"old page", "native/relinker_wasm.wasm": b"newer main wasm", + "release.json": b"old manifest", "libraries/old.zip": b"old libraries"}.items(): + path = self.output / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(content) + + def assert_rejected(self, message): + self.seed_previous() + previous = self.snapshot() + with self.assertRaisesRegex(ValueError, message): + self.publish() + self.assertEqual(self.snapshot(), previous) + self.assertEqual(list(self.build.glob(".web-release-*")), []) + + def test_release_replaces_main_converter_with_matching_frontend_and_libraries(self): + self.seed_previous() + self.publish() + for name, content in self.contents.items(): + self.assertEqual((self.output / name).read_bytes(), content) + self.assertEqual(json.loads((self.output / "web-release.json").read_text()), self.metadata) + manifest = json.loads((self.output / "release.json").read_text()) + self.assertEqual((manifest["tag"], manifest["revision"], manifest["url"]), + ("v1", REVISION, self.release["html_url"])) + for platform in ("windows", "linux"): + asset = manifest["platforms"][platform] + data = (self.output / asset["path"]).read_bytes() + self.assertEqual(len(data), asset["size"]) + self.assertEqual(hashlib.sha256(data).hexdigest(), asset["sha256"]) + with zipfile.ZipFile(io.BytesIO(data)) as archive: + self.assertEqual(archive.read("libs/libkernel.prx"), platform.encode()) + self.assertTrue((self.output / ".nojekyll").is_file()) + self.assertEqual(self.requests.count(LATEST), 1) + + def test_libraries_flag_also_replaces_an_incompatible_local_converter(self): + self.seed_previous() + source = self.root / "source/demo/web" + source.mkdir(parents=True) + (source / "index.html").write_bytes(b"main frontend") + (self.output / "native/relinker_wasm.mjs").write_bytes(b"main converter") + with patch.object(build_web, "ROOT", self.root / "source"), \ + patch.object(build_web, "request", self.request), \ + patch.object(sys, "argv", ["build_web.py", "--build", str(self.build), "--libraries"]): + build_web.main() + self.assertEqual((self.output / "native/relinker_wasm.wasm").read_bytes(), self.contents["native/relinker_wasm.wasm"]) + self.assertEqual((self.output / "index.html").read_bytes(), self.contents["index.html"]) + + def test_release_mode_does_not_require_a_local_wasm_build(self): + with patch.object(build_web, "request", self.request), \ + patch.object(sys, "argv", ["build_web.py", "--build", str(self.build), "--release"]): + build_web.main() + self.assertEqual((self.output / "native/relinker_wasm.mjs").read_bytes(), self.contents["native/relinker_wasm.mjs"]) + + def test_local_build_still_copies_sources_without_network(self): + source = self.root / "source/demo/web" + source.mkdir(parents=True) + (source / "index.html").write_bytes(b"local frontend") + (self.output / "native").mkdir() + for name in ("relinker_wasm.mjs", "relinker_wasm.wasm"): + (self.output / "native" / name).write_bytes(b"local wasm") + with patch.object(build_web, "ROOT", self.root / "source"), \ + patch.object(build_web, "request", side_effect=AssertionError("Unexpected network request")), \ + patch.object(sys, "argv", ["build_web.py", "--build", str(self.build)]): + build_web.main() + self.assertEqual((self.output / "index.html").read_bytes(), b"local frontend") + self.assertEqual((self.output / "native/relinker_wasm.wasm").read_bytes(), b"local wasm") + + def test_old_release_without_web_asset_preserves_the_previous_site(self): + self.release["assets"] = [asset for asset in self.release["assets"] if asset["name"] != "web-v1.zip"] + self.assert_rejected("publish a release with the web converter") + + def test_ambiguous_web_asset_is_rejected(self): + self.release["assets"].append(dict(self.release["assets"][0])) + self.assert_rejected("ambiguous release asset") + + def test_wrong_tag_or_invalid_revision_is_rejected(self): + for metadata in ({"tag": "v2", "revision": REVISION}, {"tag": "v1", "revision": "main"}, + {"tag": "v1", "revision": None}, []): + with self.subTest(metadata=metadata): + self.requests.clear() + self.set_asset("web-v1.zip", zipped([*self.contents.items(), ("web-release.json", json.dumps(metadata))])) + self.assert_rejected("does not match") + + def test_unsafe_duplicate_missing_or_empty_files_are_rejected(self): + for entries in (self.entries + [("../escape", b"bad")], self.entries + [self.entries[0]], + self.entries[1:], [(name, b"" if name == "app.mjs" else data) for name, data in self.entries]): + with self.subTest(names=[name for name, data in entries]): + self.requests.clear() + self.set_asset("web-v1.zip", zipped(entries)) + self.assert_rejected("Invalid.*web release archive") + self.assertFalse((self.build / "escape").exists()) + + def test_web_size_and_checksum_failures_preserve_previous_site(self): + for failure in ("short", "long", "checksum", "limit"): + with self.subTest(failure=failure): + self.requests.clear() + asset = self.set_asset("web-v1.zip", zipped(self.entries)) + if failure == "short": + self.responses[asset["browser_download_url"]] = b"short" + elif failure == "long": + self.responses[asset["browser_download_url"]] += b"x" + elif failure == "checksum": + asset["digest"] = "sha256:" + "0" * 64 + else: + asset["size"] = 128 * 1024 * 1024 + 1 + self.assert_rejected("(archive|checksum)") + + def test_missing_or_corrupt_library_never_publishes_the_new_converter(self): + for failure in ("missing", "corrupt"): + with self.subTest(failure=failure): + self.requests.clear() + self.set_asset("prx-linux-v1.zip", zipped([("libs/libkernel.prx", b"linux")])) + if failure == "missing": + self.release["assets"] = [asset for asset in self.release["assets"] if asset["name"] != "prx-linux-v1.zip"] + else: + self.set_asset("prx-linux-v1.zip", zipped([("../escape", b"bad")])) + self.assert_rejected("(release asset|archive layout)") + + def test_library_download_failure_preserves_previous_site_and_removes_staging(self): + self.seed_previous() + previous = self.snapshot() + + def failed_request(url): + if url.endswith("prx-linux-v1.zip"): + raise OSError("connection lost") + return self.request(url) + + with patch.object(build_web, "request", failed_request), self.assertRaisesRegex(OSError, "connection lost"): + build_web.publish_release(self.output) + self.assertEqual(self.snapshot(), previous) + self.assertEqual(list(self.build.glob(".web-release-*")), []) + + def test_invalid_release_tag_is_rejected_before_downloading_assets(self): + for tag in ("../v1", "v1."): + with self.subTest(tag=tag): + self.requests.clear() + self.release["tag_name"] = tag + self.assert_rejected("Invalid release tag") + self.assertEqual(self.requests, [LATEST]) + + def test_packaged_web_archive_is_accepted_by_the_publisher(self): + local = self.root / "local/web" + for name, content in self.contents.items(): + path = local / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(content) + dist = self.root / "dist" + result = subprocess.run([sys.executable, str(Path(package_release.__file__)), "--platform", "web", + "--build", str(local.parent), "--output", str(dist), "--version", "v1", + "--revision", REVISION], capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stderr) + self.set_asset("web-v1.zip", (dist / "web-v1.zip").read_bytes()) + self.publish() + for name, content in self.contents.items(): + self.assertEqual((self.output / name).read_bytes(), content) + + +class WebPackageTests(unittest.TestCase): + def setUp(self): + self.directory = tempfile.TemporaryDirectory() + self.addCleanup(self.directory.cleanup) + self.root = Path(self.directory.name) + self.build = self.root / "build" + self.output = self.root / "dist" + self.output.mkdir() + self.asset = self.output / "web-v1.zip" + self.asset.write_bytes(b"previous asset") + for name in FILES: + path = self.build / "web" / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(name.encode()) + + def test_missing_or_empty_web_file_preserves_previous_archive(self): + file = self.build / "web/native/relinker_wasm.wasm" + for empty in (False, True): + with self.subTest(empty=empty): + if empty: + file.write_bytes(b"") + else: + file.unlink() + with self.assertRaisesRegex(RuntimeError, "Missing or empty web release file"): + package_release.package_web(self.build, self.output, "v1", REVISION) + self.assertEqual(self.asset.read_bytes(), b"previous asset") + + def test_package_includes_every_frontend_source(self): + frontend = {file.name: file.read_bytes() for file in (package_release.ROOT / "demo/web").iterdir() if file.is_file()} + for name, content in frontend.items(): + (self.build / "web" / name).write_bytes(content) + package_release.package_web(self.build, self.output, "v1", REVISION) + with zipfile.ZipFile(self.asset) as archive: + self.assertEqual(set(archive.namelist()), {*frontend, "native/relinker_wasm.mjs", "native/relinker_wasm.wasm", "web-release.json"}) + for name, content in frontend.items(): + self.assertEqual(archive.read(name), content) + + def test_invalid_tag_or_revision_does_not_replace_existing_archive(self): + for tag, revision in (("../v1", REVISION), ("v1.", REVISION), ("v1", "HEAD")): + with self.subTest(tag=tag, revision=revision), self.assertRaises(ValueError): + package_release.package_web(self.build, self.output, tag, revision) + self.assertEqual(self.asset.read_bytes(), b"previous asset") + + def test_archive_write_failure_preserves_previous_archive(self): + with patch.object(package_release.zipfile.ZipFile, "write", side_effect=OSError("disk full")), \ + self.assertRaisesRegex(OSError, "disk full"): + package_release.package_web(self.build, self.output, "v1", REVISION) + self.assertEqual(self.asset.read_bytes(), b"previous asset") + self.assertEqual(list(self.output.glob(".release-*")), []) + + def test_cli_rejects_missing_or_misplaced_revision_without_writing_assets(self): + base = [sys.executable, str(Path(package_release.__file__)), "--build", str(self.build), + "--output", str(self.output), "--version", "v1"] + for arguments in (["--platform", "web"], ["--platform", "linux", "--revision", REVISION]): + with self.subTest(arguments=arguments): + result = subprocess.run([*base, *arguments], capture_output=True, text=True) + self.assertEqual(result.returncode, 2) + self.assertIn("--revision", result.stderr) + self.assertEqual(self.asset.read_bytes(), b"previous asset") + + +if __name__ == "__main__": + unittest.main() From cdccc34523fb1a185f4fe69c3aba9b249fca4bd2 Mon Sep 17 00:00:00 2001 From: Faryan Rezagholi Date: Sun, 18 Oct 2026 22:26:45 +0200 Subject: [PATCH 037/269] feat(libkernel): log guest exception context --- .../prx/libkernel/System/src/Exception.cpp | 58 ++++++++++++++++++- 1 file changed, 56 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libkernel/System/src/Exception.cpp b/core/libs/prx/libkernel/System/src/Exception.cpp index 6dbef36c89..8b1bce22d4 100644 --- a/core/libs/prx/libkernel/System/src/Exception.cpp +++ b/core/libs/prx/libkernel/System/src/Exception.cpp @@ -123,9 +123,63 @@ void SetFpWord(GuestMcontext& m, std::size_t offset, std::uint32_t value) { std::memcpy(reinterpret_cast(m.fpstate) + offset, &value, sizeof(value)); } +char* AppendText(char* output, const char* text) { + while (*text != '\0') *output++ = *text++; + return output; +} + +char* AppendHex(char* output, std::uint64_t value, std::size_t digits) { + static constexpr char hexDigits[] = "0123456789abcdef"; + while (digits != 0) { + --digits; + *output++ = hexDigits[(value >> (digits * 4)) & 0xf]; + } + return output; +} + +char* AppendHexField(char* output, const char* name, std::uint64_t value, std::size_t digits) { + return AppendHex(AppendText(output, name), value, digits); +} + +void LogGuestExceptionContext(int signum, const GuestMcontext& context) { + char buffer[768]; + char* output = AppendText(buffer, "[exception] guest context"); + output = AppendHexField(output, " signal=0x", static_cast(signum), 2); + output = AppendHexField(output, " rip=0x", context.rip, 16); + output = AppendHexField(output, " rsp=0x", context.rsp, 16); + output = AppendHexField(output, " rflags=0x", context.rflags, 16); + output = AppendHexField(output, " trap=0x", context.trapno, 8); + output = AppendHexField(output, " error=0x", context.err, 16); + output = AppendHexField(output, " fault=0x", context.addr, 16); + output = AppendHexField(output, " rax=0x", context.rax, 16); + output = AppendHexField(output, " rbx=0x", context.rbx, 16); + output = AppendHexField(output, " rcx=0x", context.rcx, 16); + output = AppendHexField(output, " rdx=0x", context.rdx, 16); + output = AppendHexField(output, " rsi=0x", context.rsi, 16); + output = AppendHexField(output, " rdi=0x", context.rdi, 16); + output = AppendHexField(output, " rbp=0x", context.rbp, 16); + output = AppendHexField(output, " r8=0x", context.r8, 16); + output = AppendHexField(output, " r9=0x", context.r9, 16); + output = AppendHexField(output, " r10=0x", context.r10, 16); + output = AppendHexField(output, " r11=0x", context.r11, 16); + output = AppendHexField(output, " r12=0x", context.r12, 16); + output = AppendHexField(output, " r13=0x", context.r13, 16); + output = AppendHexField(output, " r14=0x", context.r14, 16); + output = AppendHexField(output, " r15=0x", context.r15, 16); + *output++ = '\n'; +#ifdef _WIN32 + DWORD written = 0; + WriteFile(GetStdHandle(STD_ERROR_HANDLE), buffer, static_cast(output - buffer), &written, nullptr); +#else + const auto written = write(STDERR_FILENO, buffer, static_cast(output - buffer)); + (void)written; +#endif +} + void CallHandler(GuestExceptionHandler handler, int signum, GuestUcontext& ucontext) { auto& m = ucontext.mcontext; const std::uint32_t mxcsrMask = FpWord(m, MxcsrMaskOffset); + LogGuestExceptionContext(signum, m); handler(signum, &ucontext); SetFpWord(m, MxcsrOffset, FpWord(m, MxcsrOffset) & (mxcsrMask != 0 ? mxcsrMask : DefaultMxcsrMask)); SetFpWord(m, MxcsrMaskOffset, mxcsrMask); @@ -515,11 +569,11 @@ int APS5_VABI sceKernelRaiseException(Pthread thread, int signum) { } void APS5_VABI sceKernelDebugRaiseException(int c1, int c2) { - APS5_LOG_OUT("sceKernelDebugRaiseException c1=%d c2=%d", c1, c2); + APS5_LOG_OUT("sceKernelDebugRaiseException c1=0x%08x c2=0x%08x caller=%p", static_cast(c1), static_cast(c2), __builtin_return_address(0)); } void APS5_VABI sceKernelDebugRaiseExceptionOnReleaseMode(int c1, int c2) { - APS5_LOG_OUT("sceKernelDebugRaiseExceptionOnReleaseMode c1=%d c2=%d", c1, c2); + APS5_LOG_OUT("sceKernelDebugRaiseExceptionOnReleaseMode c1=0x%08x c2=0x%08x caller=%p", static_cast(c1), static_cast(c2), __builtin_return_address(0)); } } From 388527d6b25c7a8353f3217295dbe4fb3fa347b9 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Sun, 18 Oct 2026 20:29:54 +0000 Subject: [PATCH 038/269] fix(agc): reimport a multisampled target recreated with another pipe-bank XOR RADV gives each image its own pipe-bank XOR when it creates it. When Ruffle recreates a 4x render target on the same memory, the next draw names the same surface, DCC and CMASK with another XOR, and the resident target refused it as a changed layout ("multisampled DCC target layout or metadata changed"). A use that differs from a resident DCC target only in that XOR now keeps the target's image, views and cached textures, takes the new layout, and reimports its contents from the current DCC and CMASK metadata the way a first use does, clearing the image to that color after the work already recorded against it. Any other layout change is still refused. Reusing the image keeps memory flat: Ruffle's cache_as_bitmap__avm2_button recreates its 864x864 4x target 28 times in 32 s. Co-Authored-By: Claude Opus 5.5 --- .../Graphics/src/MultisampleTarget.cpp | 33 +++++++++++++ .../tests/execution/AlphaToCoverage.cpp | 46 +++++++++++++++++++ 2 files changed, 79 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp index 4ca7a22552..e05bea8676 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -29,6 +29,10 @@ 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; } +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; +} + VkClearColorValue fastClearColor(const ColorTarget& target) { Require(target.elementBytes != 0 && target.elementBytes <= sizeof(target.clearWords) && 16u % target.elementBytes == 0, "the fast clear of a multisampled target over 64-bit texels is not modeled"); std::array texels{}; @@ -189,6 +193,24 @@ public: }); } + void Recreate(const ColorTarget& use) { + SettleStorageWrites(); + target = use; + cmask = MultisampledCmask(use.cmaskAddress, use.cmaskBytes); + colorFill = MultisampledColorFill(use); + fromTexture = false; + dccRanges.clear(); + metadataValid = std::make_shared>(true); + const auto initial = initialDccClear(use); + recordCommands(context, [&](VkCommandBuffer commands, Recorder* recorder) { + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + context.Resolved(&DeviceFunctions::cmdClearColorImage, "vkCmdClearColorImage")(commands, image, VK_IMAGE_LAYOUT_GENERAL, &initial, 1, &range); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT); + if (recorder != nullptr) Recorder::CountBarriers(Recorder::CommandClass::Draw, 2); + }); + } + void SettleStorageWrites() { if (!storageWritten) return; storageWritten = false; @@ -442,6 +464,17 @@ MultisampleTarget& acquire(const Context& context, const ColorTarget& use) { for (const auto& target : targets()) { if (!target->active->load(std::memory_order_acquire)) continue; if (target->context.device != context.device || !sameTarget(target->target, use)) continue; + if (recreated(target->target, use)) { + { + std::unique_lock lock(guardedMutex()); + std::erase_if(guarded(), [&](const auto& range) { return range.active == target->active; }); + anyGuarded().store(!guarded().empty(), std::memory_order_release); + } + target->Recreate(use); + guard(context, use, target->active, target->metadataValid); + target->ApplyFastClear(use); + return *target; + } if (target->fromTexture && use.cmaskAddress != 0) { target->fromTexture = false; target->cmask = MultisampledCmask(use.cmaskAddress, use.cmaskBytes); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp index 4f6288141b..c7de927072 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp @@ -599,6 +599,52 @@ void DccFastClearTests(AgcDriver::VulkanDevice& device, std::uint32_t selectedSa } } } + for (const auto samples : {2u, 4u, 8u}) { + if (selectedSamples != 0 && samples != selectedSamples) continue; + blocks.push_back(std::make_unique(layout.Bytes() * samples)); + auto& source = *blocks.back(); + std::memset(source.data, 0, source.bytes); + blocks.push_back(std::make_unique(BlockBytes)); + auto& metadata = *blocks.back(); + std::memset(metadata.data, 0, BlockBytes); + auto first = DecodeTarget(source, samples); + first.dccAddress = metadata.Address(); + first.dccPipeAligned = true; + const Color written{0.25f, 0.25f, 0.25f, 0.25f}; + Draw(device, first, false, Float32Export, Program(written, Float32Export, 1u), false, true); + const std::vector ones(AgcDriver::Graphics::MultisampledDccBytes(first), std::byte{0xc0}); + AgcDriver::GuestMemory::Write(metadata.Address(), ones); + auto second = first; + second.pipeBankXor = 0x4000u; + Draw(device, second, false, Float32Export, Program(written, Float32Export, 1u), false, true); + blocks.push_back(std::make_unique(layout.Bytes())); + auto& resolved = *blocks.back(); + std::memset(resolved.data, 0, resolved.bytes); + AgcDriver::Graphics::ColorMetadataPass pass{}; + pass.mode = AgcDriver::Graphics::ColorMetadataPass::Mode::Resolve; + pass.source = second; + pass.targets = {DecodeTarget(resolved)}; + device.ColorMetadataPass(pass); + const auto stored = ReadBack(device, resolved); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + for (std::uint32_t channel = 0; channel < 4; ++channel) { + const auto expected = x < ScissorWidth ? 1.0f + (written[channel] - 1.0f) / static_cast(samples) : 1.0f; + Require(FromHalf(stored[layout.Offset(x, y) / 2u + channel]) == expected, "a multisampled DCC target recreated with another pipe-bank XOR did not start from its new fast clear"); + } + } + } + auto third = second; + third.dccAlphaOnMsb = true; + bool refused = false; + try { + Draw(device, third, false, Float32Export, Program(written, Float32Export, 1u), false, true); + } catch (const std::runtime_error& error) { + refused = std::string_view(error.what()).find("layout or metadata changed") != std::string_view::npos; + } + Require(refused, "a resident multisampled DCC target accepted a use with other DCC metadata"); + programCount = 0; + } for (const auto samples : {2u, 4u, 8u}) { if (selectedSamples != 0 && samples != selectedSamples) continue; for (const auto mode : {0u, 1u, 2u, 3u, 4u, 5u, 6u, 7u}) { From 45a2e3dd4a99e7a9c4d34c73c5ab17943c14992f Mon Sep 17 00:00:00 2001 From: Savvanis Spyros Date: Sun, 18 Oct 2026 23:35:50 +0300 Subject: [PATCH 039/269] fix(relinker): resume scans after undecodable bytes between PS4 unwind functions In PS4 executables and modules the bytes between unwind functions are 16-byte slots of function tails and padding, and a tail can begin inside an instruction, such as a lone REX byte before a 15-byte NOP. The syscall and call-site scans decode the executable segment linearly, so such a slot failed the relink with "Cannot decode instruction". For images with a PT_SCE_DYNLIBDATA segment the scans now receive the unwind function ranges. Outside every range an undecodable byte is skipped and decoding resumes at the next byte, and a decoding that would cross into a range ends at its start, so gap bytes cannot change how a function is decoded. Inside a range a decode failure still fails the relink, and images without unwind ranges are scanned as before. Co-Authored-By: Claude Opus 5.5 --- core/relinker/CMakeLists.txt | 2 + .../include/codegen/IInstructionScanner.hpp | 2 +- .../codegen/src/InstructionScanner.cpp | 43 +++++++-- .../codegen/tests/Amd64OnlyConverterTests.cpp | 13 +++ core/relinker/domain/include/domain/Types.hpp | 6 ++ .../relinker/analysis/SceCodeExtent.hpp | 1 + .../relinker/domain/ICallSiteResolver.hpp | 3 +- .../relinker/domain/ISyscallScanner.hpp | 2 +- .../include/relinker/domain/Types.hpp | 1 + .../src/analysis/CallSiteResolver.cpp | 10 ++- .../relinker/src/analysis/SceCodeExtent.cpp | 8 ++ .../relinker/src/analysis/SyscallScanner.cpp | 8 +- .../relinker/src/guest/GuestModuleBuilder.cpp | 3 +- .../src/pipeline/RelinkerPipeline.cpp | 15 ++-- .../relinker/tests/test_sce_code_gaps.py | 88 +++++++++++++++++++ docs/dev/debt/relinker.md | 1 + 16 files changed, 182 insertions(+), 24 deletions(-) create mode 100644 core/relinker/relinker/tests/test_sce_code_gaps.py diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index fb33a63019..aba9318a4d 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -611,6 +611,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_sce_relro.py $) add_test(NAME sce_module_code_extent COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_sce_module_code_extent.py $) + add_test(NAME sce_code_gaps COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_sce_code_gaps.py $) add_test(NAME call_registry COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_call_registry.py $) add_test(NAME jmprel_table_bounds COMMAND ${Python3_EXECUTABLE} diff --git a/core/relinker/codegen/include/codegen/IInstructionScanner.hpp b/core/relinker/codegen/include/codegen/IInstructionScanner.hpp index 2ea39a4e4f..e5314b276a 100644 --- a/core/relinker/codegen/include/codegen/IInstructionScanner.hpp +++ b/core/relinker/codegen/include/codegen/IInstructionScanner.hpp @@ -17,7 +17,7 @@ public: virtual void VisitCodeSection(const std::vector& codeSection, Domain::FileByteOffset codeSectionOffset, Domain::FileByteOffset codeSectionSize, const std::function& visit) const = 0; }; -std::unique_ptr MakeInstructionScanner(std::function progress = {}); +std::unique_ptr MakeInstructionScanner(std::function progress = {}, std::vector unwindRanges = {}); } diff --git a/core/relinker/codegen/src/InstructionScanner.cpp b/core/relinker/codegen/src/InstructionScanner.cpp index 328e1e532b..6a6c0018b1 100644 --- a/core/relinker/codegen/src/InstructionScanner.cpp +++ b/core/relinker/codegen/src/InstructionScanner.cpp @@ -8,28 +8,50 @@ namespace Codegen { +namespace { + +std::vector MergeCodeRanges(std::vector ranges) { + std::erase_if(ranges, [](const Domain::CodeRange& range) { return range.End <= range.Begin; }); + std::sort(ranges.begin(), ranges.end(), [](const Domain::CodeRange& left, const Domain::CodeRange& right) { return left.Begin < right.Begin; }); + std::vector merged; + for (const auto& range : ranges) { + if (!merged.empty() && range.Begin <= merged.back().End) + merged.back().End = std::max(merged.back().End, range.End); + else + merged.push_back(range); + } + return merged; +} + +} + class InstructionScanner : public IInstructionScanner { public: - explicit InstructionScanner(std::function progress) : progress(std::move(progress)) {} + InstructionScanner(std::function progress, std::vector unwindRanges) : progress(std::move(progress)), unwindRanges(MergeCodeRanges(std::move(unwindRanges))) {} [[nodiscard]] std::vector ScanCodeSection(const std::vector& codeSection, Domain::FileByteOffset codeSectionOffset, Domain::FileByteOffset codeSectionSize) const override; void VisitCodeSection(const std::vector& codeSection, Domain::FileByteOffset codeSectionOffset, Domain::FileByteOffset codeSectionSize, const std::function& visit) const override; private: std::function progress; + std::vector unwindRanges; }; template -void ScanCodeSection(const std::vector& codeSection, const Domain::FileByteOffset codeSectionOffset, const Domain::FileByteOffset codeSectionSize, const std::function& progress, TVisitor&& visit) { +void ScanCodeSection(const std::vector& codeSection, const Domain::FileByteOffset codeSectionOffset, const Domain::FileByteOffset codeSectionSize, const std::function& progress, const std::vector& unwindRanges, TVisitor&& visit) { if (codeSectionSize > codeSection.size()) throw CodegenException("Code section exceeds the input buffer", codeSectionOffset); if (codeSectionSize > std::numeric_limits::max() - codeSectionOffset) throw CodegenException("Code section offset overflow", codeSectionOffset); const auto limit = static_cast(codeSectionSize); const X64InstructionDecoder decoder; std::size_t i = 0; + std::size_t range = 0; if (progress) progress(0, limit); std::size_t lastReport = 0; while (i < limit) { + while (range < unwindRanges.size() && unwindRanges[range].End <= i) ++range; + const bool outsideUnwindRanges = !unwindRanges.empty() && (range == unwindRanges.size() || i < unwindRanges[range].Begin); + const std::size_t nextUnwindRange = outsideUnwindRanges && range < unwindRanges.size() ? unwindRanges[range].Begin : limit; const std::uint8_t* cursor = codeSection.data() + i; const std::size_t available = limit - i; @@ -38,7 +60,14 @@ void ScanCodeSection(const std::vector& codeSection, const Domain: length = decoder.Decode(cursor, available); } catch (const CodegenException& e) { if (std::all_of(cursor, cursor + available, [](const std::uint8_t b) { return b == 0; })) break; - throw CodegenException(std::string("Cannot decode instruction: ") + e.what(), codeSectionOffset + i); + if (!outsideUnwindRanges) throw CodegenException(std::string("Cannot decode instruction: ") + e.what(), codeSectionOffset + i); + ++i; + continue; + } + + if (length > nextUnwindRange - i) { + i = nextUnwindRange; + continue; } const InstructionMatch match{codeSectionOffset + i, length}; @@ -56,16 +85,16 @@ void ScanCodeSection(const std::vector& codeSection, const Domain: std::vector InstructionScanner::ScanCodeSection(const std::vector& codeSection, const Domain::FileByteOffset codeSectionOffset, const Domain::FileByteOffset codeSectionSize) const { std::vector matches; - Codegen::ScanCodeSection(codeSection, codeSectionOffset, codeSectionSize, progress, [&](const InstructionMatch& match) { matches.push_back(match); }); + Codegen::ScanCodeSection(codeSection, codeSectionOffset, codeSectionSize, progress, unwindRanges, [&](const InstructionMatch& match) { matches.push_back(match); }); return matches; } void InstructionScanner::VisitCodeSection(const std::vector& codeSection, const Domain::FileByteOffset codeSectionOffset, const Domain::FileByteOffset codeSectionSize, const std::function& visit) const { - Codegen::ScanCodeSection(codeSection, codeSectionOffset, codeSectionSize, progress, visit); + Codegen::ScanCodeSection(codeSection, codeSectionOffset, codeSectionSize, progress, unwindRanges, visit); } -std::unique_ptr MakeInstructionScanner(std::function progress) { - return std::make_unique(std::move(progress)); +std::unique_ptr MakeInstructionScanner(std::function progress, std::vector unwindRanges) { + return std::make_unique(std::move(progress), std::move(unwindRanges)); } } diff --git a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp index 89bc80f07d..23faa55e9b 100644 --- a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp +++ b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp @@ -1792,6 +1792,18 @@ void scannerZeroTail() { requireFailure([&] { (void)scanner->ScanCodeSection(truncated, 0, truncated.size()); }, "Truncated non-zero tail must still fail"); } +void scannerUnwindRanges() { + const Bytes slot{0x48, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x2E, 0x0F, 0x1F, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC3}; + const auto gap = Codegen::MakeInstructionScanner({}, {{16, 17}})->ScanCodeSection(slot, 0x1000, slot.size()); + require(gap.size() == 2 && gap[0].Offset == 0x1001 && gap[0].Length == 15 && gap[1].Offset == 0x1010 && gap[1].Length == 1, "An undecodable byte outside every unwind range must resume the scan at the next byte"); + requireFailure([&] { (void)Codegen::MakeInstructionScanner({}, {{0, 17}})->ScanCodeSection(slot, 0, slot.size()); }, "An undecodable byte inside an unwind range must still fail"); + requireFailure([&] { (void)Codegen::MakeInstructionScanner()->ScanCodeSection(slot, 0, slot.size()); }, "An undecodable byte without unwind ranges must still fail"); + const Bytes crossing{0xB8, 0x0F, 0x05, 0xC3, 0x90}; + const auto cut = Codegen::MakeInstructionScanner({}, {{3, 4}, {1, 3}})->ScanCodeSection(crossing, 0, crossing.size()); + require(cut.size() == 3 && cut[0].Offset == 1 && cut[0].Length == 2 && cut[1].Offset == 3 && cut[2].Offset == 4, "A gap instruction that crosses into an unwind range must end at the range"); + require(Codegen::MakeInstructionScanner()->ScanCodeSection(crossing, 0, crossing.size()).front().Length == 5, "Without unwind ranges the crossing instruction must decode whole"); +} + int main() { try { decoderLengths(); @@ -1837,6 +1849,7 @@ int main() { linuxPlacement(); linuxRipRelativePlacement(); scannerZeroTail(); + scannerUnwindRanges(); std::cout << "AMD64-only converter tests passed\n"; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; diff --git a/core/relinker/domain/include/domain/Types.hpp b/core/relinker/domain/include/domain/Types.hpp index f505333ec7..dd181853e0 100644 --- a/core/relinker/domain/include/domain/Types.hpp +++ b/core/relinker/domain/include/domain/Types.hpp @@ -16,6 +16,12 @@ using FileByteOffset = std::uint64_t; using VirtualAddress = std::uint64_t; using ByteCount = std::uint64_t; +struct CodeRange { + ByteCount Begin; + ByteCount End; + bool operator==(const CodeRange&) const = default; +}; + struct ElfHeader { std::uint16_t Machine; std::uint16_t Type; diff --git a/core/relinker/relinker/include/relinker/analysis/SceCodeExtent.hpp b/core/relinker/relinker/include/relinker/analysis/SceCodeExtent.hpp index c43fd18cd6..4e897a7a0b 100644 --- a/core/relinker/relinker/include/relinker/analysis/SceCodeExtent.hpp +++ b/core/relinker/relinker/include/relinker/analysis/SceCodeExtent.hpp @@ -8,6 +8,7 @@ namespace Relinker { std::optional FindSceCodeEnd(std::span segment, VirtualAddress segmentVAddr, const std::vector& functions, VirtualAddress pltGot, ByteCount pltRelocationsSize); +std::vector SceUnwindRanges(std::span segment, VirtualAddress segmentVAddr, const std::vector& functions); } diff --git a/core/relinker/relinker/include/relinker/domain/ICallSiteResolver.hpp b/core/relinker/relinker/include/relinker/domain/ICallSiteResolver.hpp index 93e53570cd..8a3e5df7dc 100644 --- a/core/relinker/relinker/include/relinker/domain/ICallSiteResolver.hpp +++ b/core/relinker/relinker/include/relinker/domain/ICallSiteResolver.hpp @@ -14,7 +14,8 @@ public: const std::vector& textSection, FileByteOffset textSectionVAddr, VirtualAddress targetGotOrPltAddress, - ByteCount targetGotOrPltSize + ByteCount targetGotOrPltSize, + const std::vector& unwindRanges = {} ) = 0; }; diff --git a/core/relinker/relinker/include/relinker/domain/ISyscallScanner.hpp b/core/relinker/relinker/include/relinker/domain/ISyscallScanner.hpp index 71a9160da0..a4adf4a77c 100644 --- a/core/relinker/relinker/include/relinker/domain/ISyscallScanner.hpp +++ b/core/relinker/relinker/include/relinker/domain/ISyscallScanner.hpp @@ -12,7 +12,7 @@ class ISyscallScanner { public: virtual ~ISyscallScanner() = default; - virtual std::vector ScanCodeSectionForSyscalls(const std::vector& codeSection, FileByteOffset codeSectionOffset, FileByteOffset codeSectionSize, std::string_view input = {}) = 0; + virtual std::vector ScanCodeSectionForSyscalls(const std::vector& codeSection, FileByteOffset codeSectionOffset, FileByteOffset codeSectionSize, std::string_view input = {}, const std::vector& unwindRanges = {}) = 0; }; } diff --git a/core/relinker/relinker/include/relinker/domain/Types.hpp b/core/relinker/relinker/include/relinker/domain/Types.hpp index 0cae3ed174..856d6a0154 100644 --- a/core/relinker/relinker/include/relinker/domain/Types.hpp +++ b/core/relinker/relinker/include/relinker/domain/Types.hpp @@ -8,6 +8,7 @@ namespace Relinker { using FileByteOffset = Domain::FileByteOffset; using VirtualAddress = Domain::VirtualAddress; using ByteCount = Domain::ByteCount; +using CodeRange = Domain::CodeRange; using ElfHeader = Domain::ElfHeader; using ProgramHeader = Domain::ProgramHeader; using SectionHeader = Domain::SectionHeader; diff --git a/core/relinker/relinker/src/analysis/CallSiteResolver.cpp b/core/relinker/relinker/src/analysis/CallSiteResolver.cpp index 63a1b2450c..91fe531f9c 100644 --- a/core/relinker/relinker/src/analysis/CallSiteResolver.cpp +++ b/core/relinker/relinker/src/analysis/CallSiteResolver.cpp @@ -51,21 +51,22 @@ std::optional ripRelativeDispOffset(const std::vector class CallSiteResolver : public ICallSiteResolver { public: - std::vector ResolveCallSites(const std::vector& textSection, FileByteOffset textSectionVAddr, VirtualAddress targetGotOrPltAddress, ByteCount targetGotOrPltSize) override; + std::vector ResolveCallSites(const std::vector& textSection, FileByteOffset textSectionVAddr, VirtualAddress targetGotOrPltAddress, ByteCount targetGotOrPltSize, const std::vector& unwindRanges) override; private: const std::vector* _cachedTextPtr = nullptr; std::vector> _cachedTargetToInstr; FileByteOffset _cachedTextVAddr = 0; + std::vector _cachedUnwindRanges; }; -std::vector CallSiteResolver::ResolveCallSites(const std::vector& textSection, const FileByteOffset textSectionVAddr, const VirtualAddress targetGotOrPltAddress, const ByteCount targetGotOrPltSize) { +std::vector CallSiteResolver::ResolveCallSites(const std::vector& textSection, const FileByteOffset textSectionVAddr, const VirtualAddress targetGotOrPltAddress, const ByteCount targetGotOrPltSize, const std::vector& unwindRanges) { if (targetGotOrPltSize > std::numeric_limits::max() - targetGotOrPltAddress) throw RelinkerException("Relocation target range exceeds the address space"); if (textSection.size() > std::numeric_limits::max() - textSectionVAddr) throw RelinkerException("Text section range exceeds the address space"); if (textSection.empty()) return {}; - if (_cachedTextPtr != &textSection || _cachedTextVAddr != textSectionVAddr) { - auto scanner = Codegen::MakeInstructionScanner(); + if (_cachedTextPtr != &textSection || _cachedTextVAddr != textSectionVAddr || _cachedUnwindRanges != unwindRanges) { + auto scanner = Codegen::MakeInstructionScanner({}, unwindRanges); _cachedTargetToInstr.clear(); const auto candidateCount = std::count_if(textSection.begin(), textSection.end(), [](std::uint8_t b) { return b == OneByteGrp5Rm || b == OneByteModRmRangeJMax; @@ -83,6 +84,7 @@ std::vector CallSiteResolver::ResolveCallSites(const std::vector std::sort(_cachedTargetToInstr.begin(), _cachedTargetToInstr.end()); _cachedTextPtr = &textSection; _cachedTextVAddr = textSectionVAddr; + _cachedUnwindRanges = unwindRanges; } std::vector sites; diff --git a/core/relinker/relinker/src/analysis/SceCodeExtent.cpp b/core/relinker/relinker/src/analysis/SceCodeExtent.cpp index 106528e542..0c53fee8c8 100644 --- a/core/relinker/relinker/src/analysis/SceCodeExtent.cpp +++ b/core/relinker/relinker/src/analysis/SceCodeExtent.cpp @@ -37,4 +37,12 @@ std::optional FindSceCodeEnd(std::span segme return segmentVAddr + *plt + pltSize; } +std::vector SceUnwindRanges(std::span segment, VirtualAddress segmentVAddr, const std::vector& functions) { + std::vector ranges; + for (const auto& function : functions) + if (function.Begin >= segmentVAddr && function.Begin - segmentVAddr < segment.size() && function.End > function.Begin) + ranges.push_back({function.Begin - segmentVAddr, std::min(function.End - segmentVAddr, segment.size())}); + return ranges; +} + } diff --git a/core/relinker/relinker/src/analysis/SyscallScanner.cpp b/core/relinker/relinker/src/analysis/SyscallScanner.cpp index 77ee6dd08c..86b3647863 100644 --- a/core/relinker/relinker/src/analysis/SyscallScanner.cpp +++ b/core/relinker/relinker/src/analysis/SyscallScanner.cpp @@ -173,19 +173,19 @@ class SyscallScanner : public ISyscallScanner { public: SyscallScanner(Domain::IProgress& progress, const bool trapSdkStubs) : progress(progress), trapSdkStubs(trapSdkStubs) {} - std::vector ScanCodeSectionForSyscalls(const std::vector& codeSection, FileByteOffset codeSectionOffset, FileByteOffset codeSectionSize, std::string_view input) override; + std::vector ScanCodeSectionForSyscalls(const std::vector& codeSection, FileByteOffset codeSectionOffset, FileByteOffset codeSectionSize, std::string_view input, const std::vector& unwindRanges) override; private: Domain::IProgress& progress; bool trapSdkStubs; }; -std::vector SyscallScanner::ScanCodeSectionForSyscalls(const std::vector& codeSection, const FileByteOffset codeSectionOffset, const FileByteOffset codeSectionSize, std::string_view input) { +std::vector SyscallScanner::ScanCodeSectionForSyscalls(const std::vector& codeSection, const FileByteOffset codeSectionOffset, const FileByteOffset codeSectionSize, std::string_view input, const std::vector& unwindRanges) { if (codeSectionSize > codeSection.size()) throw RelinkerException("Code section exceeds the input buffer", codeSectionOffset); const auto limit = static_cast(codeSectionSize); const std::string source(input); - const auto scanner = Codegen::MakeInstructionScanner([&](auto completed, auto total) { progress.Report({Domain::ProgressStage::Decode, source, completed, total}); }); + const auto scanner = Codegen::MakeInstructionScanner([&](auto completed, auto total) { progress.Report({Domain::ProgressStage::Decode, source, completed, total}); }, unwindRanges); RecentInstructions recent; std::vector trapped; scanner->VisitCodeSection(codeSection, codeSectionOffset, codeSectionSize, [&](const Codegen::InstructionMatch& match) { @@ -228,7 +228,7 @@ std::unique_ptr MakeSyscallScanner(Domain::IProgress& progress, class NullSyscallScanner : public ISyscallScanner { public: - std::vector ScanCodeSectionForSyscalls(const std::vector&, FileByteOffset, FileByteOffset, std::string_view) override { return {}; } + std::vector ScanCodeSectionForSyscalls(const std::vector&, FileByteOffset, FileByteOffset, std::string_view, const std::vector&) override { return {}; } }; std::unique_ptr MakeNullSyscallScanner() { diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index 2d94dcf5dc..2a09e1a55e 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -345,7 +345,8 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path if (sceImage) if (const auto end = FindSceCodeEnd(code, header.MappedAddress, unwindFunctions, image.Got, image.Dynamic.RelaPltData.size())) code.resize(*end - header.MappedAddress); - for (const auto offset : syscallScanner.ScanCodeSectionForSyscalls(code, header.MappedAddress, code.size(), Io::Utf8Path(image.SourcePath))) { + const auto unwindRanges = SceUnwindRanges(code, header.MappedAddress, unwindFunctions); + for (const auto offset : syscallScanner.ScanCodeSectionForSyscalls(code, header.MappedAddress, code.size(), Io::Utf8Path(image.SourcePath), unwindRanges)) { if (offset > code.size() || code.size() - offset < 2u) throw Domain::RelinkerException("Syscall trap patch exceeds guest executable segment", header.MappedAddress + offset); image.Bytes[header.Offset + offset] = 0x0F; diff --git a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp index cf7f7a80ae..51cfffb403 100644 --- a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp +++ b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp @@ -362,18 +362,22 @@ RelinkResult RelinkerPipeline::Relink() { for (const auto& ref : nidRefs) validationPolicy->ValidateRelocationTypeSupported(ref.RelocationTypeValue, ref.RelocationTableOffset); + std::vector> unwindRanges(executableSegments.size()); if (std::any_of(programHeaders.begin(), programHeaders.end(), [](const ProgramHeader& ph) { return ph.Type == PT_SCE_DYNLIBDATA; })) { const auto functions = UnusedNidFilter::ReadExceptionFunctions(raw, programHeaders, {}, {}); - for (auto& [segment, segmentVAddr, segmentFileOffset] : executableSegments) { + for (std::size_t index = 0; index < executableSegments.size(); ++index) { + auto& [segment, segmentVAddr, segmentFileOffset] = executableSegments[index]; static_cast(segmentFileOffset); if (const auto codeEnd = FindSceCodeEnd(segment, segmentVAddr, functions, gotVAddr, dynJmpRelSize)) segment.resize(*codeEnd - segmentVAddr); + unwindRanges[index] = SceUnwindRanges(segment, segmentVAddr, functions); } } std::vector syscallPatches; - for (auto& [segment, segmentVAddr, segmentFileOffset] : executableSegments) { - for (const auto offset : syscallScanner->ScanCodeSectionForSyscalls(segment, segmentVAddr, segment.size())) { + for (std::size_t index = 0; index < executableSegments.size(); ++index) { + auto& [segment, segmentVAddr, segmentFileOffset] = executableSegments[index]; + for (const auto offset : syscallScanner->ScanCodeSectionForSyscalls(segment, segmentVAddr, segment.size(), {}, unwindRanges[index])) { if (offset > segment.size() || segment.size() - offset < 2u) throw RelinkerException("Syscall trap patch exceeds executable segment", segmentVAddr + offset); segment[offset] = 0x0F; @@ -536,10 +540,11 @@ RelinkResult RelinkerPipeline::Relink() { entries.push_back(std::move(entry)); } - for (const auto& [segment, segmentVAddr, segmentFileOffset] : executableSegments) { + for (std::size_t index = 0; index < executableSegments.size(); ++index) { + const auto& [segment, segmentVAddr, segmentFileOffset] = executableSegments[index]; static_cast(segmentFileOffset); for (auto& entry : entries) { - auto segmentSites = callSiteResolver->ResolveCallSites(segment, segmentVAddr, entry.TargetOffset, 8); + auto segmentSites = callSiteResolver->ResolveCallSites(segment, segmentVAddr, entry.TargetOffset, 8, unwindRanges[index]); entry.CallSites.insert(entry.CallSites.end(), segmentSites.begin(), segmentSites.end()); entry.CallSitesResolved = !entry.CallSites.empty(); } diff --git a/core/relinker/relinker/tests/test_sce_code_gaps.py b/core/relinker/relinker/tests/test_sce_code_gaps.py new file mode 100644 index 0000000000..5dc8164f13 --- /dev/null +++ b/core/relinker/relinker/tests/test_sce_code_gaps.py @@ -0,0 +1,88 @@ +from pathlib import Path +import json +import struct +import subprocess +import sys +import tempfile + +from test_guest_symbol_names import importing_executable +from test_sce_code_extent import FUNCTION, FUNCTION_SIZE, fixture +from test_sce_module_code_extent import sce_module, unwind_table + +SLOT = bytes.fromhex("48 66 66 66 66 66 66 2e 0f 1f 84 00 00 00 00 00") +SYSCALL = bytes.fromhex("0f 05 c3") +GAP = FUNCTION + FUNCTION_SIZE +TARGETS = (("linux", []), ("windows", ["--windows"]), ("macos", ["--macos"])) + + +def executable(gap_bytes=b"", sce_block=True, function=b"", before_function=b""): + image, plt = fixture(sce_block=sce_block, gap=0x20) + image[GAP:GAP + len(gap_bytes)] = gap_bytes + image[FUNCTION:FUNCTION + len(function)] = function + image[FUNCTION - len(before_function):FUNCTION] = before_function + return image, plt + + +def module(slot_in_function): + image = sce_module() + text = memoryview(image)[0x400:0x500] + text[0:len(SLOT)] = SLOT + if not slot_in_function: + text[0x10:0x10 + FUNCTION_SIZE] = b"\xc3" + b"\x90" * (FUNCTION_SIZE - 1) + unwind_table(text, 0x1000, 0x1010, FUNCTION_SIZE, 0x1080, 0x10A0) + struct.pack_into(" Date: Sun, 18 Oct 2026 23:33:46 +0200 Subject: [PATCH 040/269] perf(shader): fold masks, shifts and field extracts through bits they do not use The constant folder now knows which bits a value can hold (masks, merges, shifts, field extracts, 16- and 8-bit widenings, selects) and which bits a user reads: - a mask that clears no bit the value can hold is dropped, and so is a merged part with no bit under the mask; - And(And(x, a), m) becomes And(x, m) when m keeps no bit outside a, and a mask rewrite is folded again; - a field extract, shift, mask or 16-bit truncation reads through masks that keep the bits it reads and through merged parts that hold none; - a right shift of a left shift by the same amount becomes a mask; - BitFieldUExtract(x, 0, n) is x when x has no bit at or above n; - Or of two masked parts of one word becomes the word under the union of the masks (the word itself when they cover it); - a field extract of a left shift reads the unshifted word. These patterns come from 16-bit halves of 32-bit registers (f16 and packed math), which the translator splits and merges per instruction. Test: agc_constant_bit_test gets six cases (repeated, narrower and wider masks, a mask under a merge, half extractions, halves that rejoin); all six fail on main. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01XBXM4pgkMHYEtqN1EQni2P --- .../libSceAgcDriver/tests/ConstantBitTest.cpp | 102 ++++++++++ .../Optimization/src/ConstantFolder.cpp | 190 +++++++++++++++++- 2 files changed, 290 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ConstantBitTest.cpp b/core/libs/prx/libSceAgcDriver/tests/ConstantBitTest.cpp index d660dc354d..0c44fbf067 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ConstantBitTest.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ConstantBitTest.cpp @@ -29,6 +29,102 @@ bool foldsTo(std::uint32_t word, bool expected, bool maskedShift) { return result->HasImmediate() && result->ImmediateBool() == expected; } +bool masksFold(std::uint32_t inner, std::uint32_t outer) { + IrProgram program; + auto& block = program.CreateBlock(); + program.SetEntryBlock(block); + program.BlockOrder().push_back(&block); + IrBuilder builder(program); + builder.SetInsertionPoint(block); + auto& lane = builder.Emit(IrOpcode::LaneId, IrType::U32, {}); + auto& sum = builder.Emit(IrOpcode::IAdd32, IrType::U32, {&builder.Constant(1u), &lane}); + auto& first = builder.Emit(IrOpcode::BitwiseAnd32, IrType::U32, {&sum, &builder.Constant(inner)}); + auto& second = builder.Emit(IrOpcode::BitwiseAnd32, IrType::U32, {&first, &builder.Constant(outer)}); + static_cast(builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&first})); + auto& user = builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&second}); + static_cast(builder.Emit(IrOpcode::Return, IrType::Void, {})); + ConstantFolder().Fold(program); + const IrValue* result = user.Argument(0)->Resolve(); + if ((inner & ~outer) == 0u) return result == &first; + return result->Opcode() == IrOpcode::BitwiseAnd32 && result->Argument(0)->Resolve() == &sum && result->Argument(1)->Resolve()->ImmediateU32() == outer; +} + +bool mergedHalfMask() { + IrProgram program; + auto& block = program.CreateBlock(); + program.SetEntryBlock(block); + program.BlockOrder().push_back(&block); + IrBuilder builder(program); + builder.SetInsertionPoint(block); + auto& lane = builder.Emit(IrOpcode::LaneId, IrType::U32, {}); + auto& sum = builder.Emit(IrOpcode::IAdd32, IrType::U32, {&builder.Constant(1u), &lane}); + auto& low = builder.Emit(IrOpcode::BitwiseAnd32, IrType::U32, {&sum, &builder.Constant(0xffffu)}); + auto& high = builder.Emit(IrOpcode::ShiftLeftLogical32, IrType::U32, {&lane, &builder.Constant(16u)}); + auto& merged = builder.Emit(IrOpcode::BitwiseOr32, IrType::U32, {&low, &high}); + auto& masked = builder.Emit(IrOpcode::BitwiseAnd32, IrType::U32, {&merged, &builder.Constant(0x7fffu)}); + static_cast(builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&low})); + auto& user = builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&masked}); + static_cast(builder.Emit(IrOpcode::Return, IrType::Void, {})); + ConstantFolder().Fold(program); + const IrValue* result = user.Argument(0)->Resolve(); + return result->Opcode() == IrOpcode::BitwiseAnd32 && result->Argument(0)->Resolve() == ∑ +} + +bool halfExtractions() { + IrProgram program; + auto& block = program.CreateBlock(); + program.SetEntryBlock(block); + program.BlockOrder().push_back(&block); + IrBuilder builder(program); + builder.SetInsertionPoint(block); + auto& lane = builder.Emit(IrOpcode::LaneId, IrType::U32, {}); + auto& low = builder.Emit(IrOpcode::IAdd32, IrType::U32, {&builder.Constant(3u), &lane}); + auto& high = builder.Emit(IrOpcode::IAdd32, IrType::U32, {&builder.Constant(5u), &lane}); + auto& lowHalf = builder.Emit(IrOpcode::BitwiseAnd32, IrType::U32, {&low, &builder.Constant(0xffffu)}); + auto& highHalf = builder.Emit(IrOpcode::ShiftLeftLogical32, IrType::U32, {&high, &builder.Constant(16u)}); + auto& merged = builder.Emit(IrOpcode::BitwiseOr32, IrType::U32, {&lowHalf, &highHalf}); + auto& top = builder.Emit(IrOpcode::ShiftRightLogical32, IrType::U32, {&merged, &builder.Constant(16u)}); + auto& field = builder.Emit(IrOpcode::BitFieldUExtract, IrType::U32, {&merged, &builder.Constant(16u), &builder.Constant(16u)}); + auto& bottom = builder.Emit(IrOpcode::BitFieldUExtract, IrType::U32, {&merged, &builder.Constant(0u), &builder.Constant(16u)}); + auto& topUser = builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&top}); + auto& fieldUser = builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&field}); + auto& bottomUser = builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&bottom}); + static_cast(builder.Emit(IrOpcode::Return, IrType::Void, {})); + ConstantFolder().Fold(program); + const IrValue* topResult = topUser.Argument(0)->Resolve(); + const IrValue* fieldResult = fieldUser.Argument(0)->Resolve(); + const IrValue* bottomResult = bottomUser.Argument(0)->Resolve(); + const bool topMasked = topResult->Opcode() == IrOpcode::BitwiseAnd32 && topResult->Argument(0)->Resolve() == &high && topResult->Argument(1)->Resolve()->ImmediateU32() == 0xffffu; + const bool fieldThrough = fieldResult->Opcode() == IrOpcode::BitFieldUExtract && fieldResult->Argument(0)->Resolve() == &high && fieldResult->Argument(1)->Resolve()->ImmediateU32() == 0u; + const bool bottomThrough = bottomResult->Opcode() == IrOpcode::BitFieldUExtract && bottomResult->Argument(0)->Resolve() == &low; + return topMasked && fieldThrough && bottomThrough; +} + +bool halvesRejoin() { + IrProgram program; + auto& block = program.CreateBlock(); + program.SetEntryBlock(block); + program.BlockOrder().push_back(&block); + IrBuilder builder(program); + builder.SetInsertionPoint(block); + auto& lane = builder.Emit(IrOpcode::LaneId, IrType::U32, {}); + auto& word = builder.Emit(IrOpcode::IAdd32, IrType::U32, {&builder.Constant(7u), &lane}); + auto& low = builder.Emit(IrOpcode::BitFieldUExtract, IrType::U32, {&word, &builder.Constant(0u), &builder.Constant(16u)}); + auto& high = builder.Emit(IrOpcode::BitFieldUExtract, IrType::U32, {&word, &builder.Constant(16u), &builder.Constant(16u)}); + auto& raised = builder.Emit(IrOpcode::ShiftLeftLogical32, IrType::U32, {&high, &builder.Constant(16u)}); + auto& joined = builder.Emit(IrOpcode::BitwiseOr32, IrType::U32, {&low, &raised}); + auto& shifted = builder.Emit(IrOpcode::ShiftLeftLogical32, IrType::U32, {&word, &builder.Constant(16u)}); + auto& back = builder.Emit(IrOpcode::BitFieldUExtract, IrType::U32, {&shifted, &builder.Constant(16u), &builder.Constant(16u)}); + auto& joinedUser = builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&joined}); + auto& backUser = builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&back}); + static_cast(builder.Emit(IrOpcode::Return, IrType::Void, {})); + ConstantFolder().Fold(program); + const IrValue* rejoined = joinedUser.Argument(0)->Resolve(); + const IrValue* field = backUser.Argument(0)->Resolve(); + const bool fieldBack = field->Opcode() == IrOpcode::BitFieldUExtract && field->Argument(0)->Resolve() == &word && field->Argument(1)->Resolve()->ImmediateU32() == 0u; + return rejoined == &word && fieldBack; +} + bool reversesTo(std::uint32_t word, std::uint32_t expected) { IrProgram program; auto& block = program.CreateBlock(); @@ -118,6 +214,12 @@ int main() { passed &= run("a reversed high bit must fold to bit zero", reversesTo(0x80000000u, 0x00000001u)); passed &= run("a reversed mixed word must fold to its mirror image", reversesTo(0x12345678u, 0x1e6a2c48u)); passed &= run("a reversed descriptor dword must fold to its mirror image", reversesTo(0xdeadbeefu, 0xf77db57bu)); + passed &= run("a repeated mask folds into the first one", masksFold(0xffffu, 0xffffu)); + passed &= run("a narrower mask reads through the wider one", masksFold(0xffffu, 0x7fffu)); + passed &= run("a wider mask after a narrower one is dropped", masksFold(0x00ffu, 0xffffu)); + passed &= run("a mask under a merge of the halves reads through it", mergedHalfMask()); + passed &= run("half extractions read the half that was merged in", halfExtractions()); + passed &= run("the two halves of one word rejoin into the word", halvesRejoin()); return passed ? 0 : 1; } catch (const std::exception& error) { std::fprintf(stderr, "%s\n", error.what()); diff --git a/core/shader/recompiler/Optimization/src/ConstantFolder.cpp b/core/shader/recompiler/Optimization/src/ConstantFolder.cpp index 8d78231fb3..c646cf5b53 100644 --- a/core/shader/recompiler/Optimization/src/ConstantFolder.cpp +++ b/core/shader/recompiler/Optimization/src/ConstantFolder.cpp @@ -82,6 +82,147 @@ bool isLaneIndex(const IrValue& value) { return value.Opcode() == IrOpcode::LaneId; } +std::uint32_t possibleBits(const IrValue& value, unsigned depth = 0) { + if (value.HasImmediate() && value.Type() == IrType::U32) return value.ImmediateU32(); + if (depth >= 8u || value.Type() != IrType::U32) return UINT32_MAX; + const auto arg = [&](std::size_t index) -> const IrValue& { return *value.Argument(index)->Resolve(); }; + const auto immediate = [&](std::size_t index) { return arg(index).HasImmediate() && arg(index).Type() == IrType::U32; }; + switch (value.Opcode()) { + case IrOpcode::BitwiseAnd32: return possibleBits(arg(0), depth + 1) & possibleBits(arg(1), depth + 1); + case IrOpcode::BitwiseOr32: + case IrOpcode::BitwiseXor32: return possibleBits(arg(0), depth + 1) | possibleBits(arg(1), depth + 1); + case IrOpcode::BitFieldUExtract: + if (immediate(2) && arg(2).ImmediateU32() < 32u) return (std::uint32_t{1} << arg(2).ImmediateU32()) - 1u; + return UINT32_MAX; + case IrOpcode::ShiftLeftLogical32: + if (immediate(1) && arg(1).ImmediateU32() < 32u) return possibleBits(arg(0), depth + 1) << arg(1).ImmediateU32(); + return UINT32_MAX; + case IrOpcode::ShiftRightLogical32: + if (immediate(1) && arg(1).ImmediateU32() < 32u) return possibleBits(arg(0), depth + 1) >> arg(1).ImmediateU32(); + return UINT32_MAX; + case IrOpcode::ConvertU32U16: return 0xffffu; + case IrOpcode::ConvertU32U8: return 0xffu; + case IrOpcode::SelectU32: return possibleBits(arg(1), depth + 1) | possibleBits(arg(2), depth + 1); + default: return UINT32_MAX; + } +} + +bool readThroughUnusedBits(IrValue& value, std::size_t index, std::uint32_t demanded) { + bool changed = false; + for (unsigned step = 0; step < 16u; ++step) { + auto& operand = resolveArg(value, index); + if (operand.Type() != IrType::U32) break; + IrValue* next = nullptr; + for (std::size_t side = 0; side < 2u && next == nullptr; ++side) { + if (operand.Opcode() == IrOpcode::BitwiseAnd32) { + auto& mask = resolveArg(operand, side); + if (isImmediate(mask, IrType::U32) && (demanded & ~mask.ImmediateU32()) == 0u) next = &resolveArg(operand, 1u - side); + } else if (operand.Opcode() == IrOpcode::BitwiseOr32 || operand.Opcode() == IrOpcode::BitwiseXor32) { + if ((possibleBits(resolveArg(operand, side)) & demanded) == 0u) next = &resolveArg(operand, 1u - side); + } + } + if (next == nullptr) break; + value.ReplaceArgument(index, next); + changed = true; + } + return changed; +} + +bool maskedPart(IrValue& value, IrValue*& source, std::uint32_t& mask) { + const auto immediate = [&](IrValue& owner, std::size_t index, std::uint32_t& result) { + auto& argument = resolveArg(owner, index); + if (!isImmediate(argument, IrType::U32)) return false; + result = argument.ImmediateU32(); + return true; + }; + std::uint32_t first = 0; + std::uint32_t second = 0; + switch (value.Opcode()) { + case IrOpcode::BitwiseAnd32: + for (std::size_t side = 0; side < 2u; ++side) { + if (immediate(value, side, mask)) { + source = &resolveArg(value, 1u - side); + return !isImmediate(*source, IrType::U32); + } + } + return false; + case IrOpcode::BitFieldUExtract: + if (!immediate(value, 1, first) || !immediate(value, 2, second) || first != 0u || second == 0u || second >= 32u) return false; + source = &resolveArg(value, 0); + mask = (std::uint32_t{1} << second) - 1u; + return true; + case IrOpcode::ShiftLeftLogical32: { + if (!immediate(value, 1, first) || first == 0u || first >= 32u) return false; + auto& inner = resolveArg(value, 0); + std::uint32_t offset = 0; + std::uint32_t count = 0; + if (inner.Opcode() == IrOpcode::BitFieldUExtract && immediate(inner, 1, offset) && immediate(inner, 2, count) && offset == first && count > 0u && count <= 32u - offset) { + source = &resolveArg(inner, 0); + mask = (count == 32u - offset ? UINT32_MAX : (std::uint32_t{1} << (offset + count)) - 1u) & (UINT32_MAX << offset); + return true; + } + if (inner.Opcode() == IrOpcode::ShiftRightLogical32 && immediate(inner, 1, offset) && offset == first) { + source = &resolveArg(inner, 0); + mask = UINT32_MAX << first; + return true; + } + return false; + } + default: return false; + } +} + +bool mergeMaskedParts(IrProgram& program, IrValue& value) { + IrValue* left = nullptr; + IrValue* right = nullptr; + std::uint32_t leftMask = 0; + std::uint32_t rightMask = 0; + if (!maskedPart(resolveArg(value, 0), left, leftMask) || !maskedPart(resolveArg(value, 1), right, rightMask) || left != right) return false; + if ((leftMask | rightMask) == UINT32_MAX) { + replaceWith(value, *left); + return true; + } + if (value.Opcode() != IrOpcode::BitwiseOr32) return false; + IrBuilder builder(program); + value.ReplaceOpcode(IrOpcode::BitwiseAnd32); + value.ReplaceArgument(0, left); + value.ReplaceArgument(1, &builder.Constant(leftMask | rightMask)); + return true; +} + +bool foldRedundantMask(IrValue& value) { + for (std::size_t side = 0; side < 2u; ++side) { + auto& mask = resolveArg(value, side); + auto& operand = resolveArg(value, 1u - side); + if (!isImmediate(mask, IrType::U32)) continue; + const auto keep = mask.ImmediateU32(); + if ((possibleBits(operand) & ~keep) == 0u) { + replaceWith(value, operand); + return true; + } + if (operand.Opcode() == IrOpcode::BitwiseAnd32) { + for (std::size_t part = 0; part < 2u; ++part) { + auto& inner = resolveArg(operand, part); + if (isImmediate(inner, IrType::U32) && (keep & ~inner.ImmediateU32()) == 0u) { + value.ReplaceArgument(1u - side, &resolveArg(operand, 1u - part)); + foldRedundantMask(value); + return true; + } + } + } + if (operand.Opcode() == IrOpcode::BitwiseOr32) { + for (std::size_t part = 0; part < 2u; ++part) { + if ((possibleBits(resolveArg(operand, part)) & keep) == 0u) { + value.ReplaceArgument(1u - side, &resolveArg(operand, 1u - part)); + foldRedundantMask(value); + return true; + } + } + } + } + return false; +} + bool isImmediateU32(const IrValue& value, std::uint32_t immediate) { return isImmediate(value, IrType::U32) && value.ImmediateU32() == immediate; } @@ -446,6 +587,20 @@ bool ConstantFolder::tryFoldValue(IrProgram& program, IrValue& value) const { } case IrOpcode::BitFieldUExtract: case IrOpcode::BitFieldSExtract: { + if (isImmediate(resolveArg(value, 1), IrType::U32) && isImmediate(resolveArg(value, 2), IrType::U32)) { + const auto fieldOffset = resolveArg(value, 1).ImmediateU32(); + const auto fieldCount = resolveArg(value, 2).ImmediateU32(); + if (fieldCount > 0u && fieldCount < 32u && fieldOffset < 32u && fieldCount <= 32u - fieldOffset) readThroughUnusedBits(value, 0, ((std::uint32_t{1} << fieldCount) - 1u) << fieldOffset); + } + if (resolveArg(value, 0).Opcode() == IrOpcode::ShiftLeftLogical32 && isImmediate(resolveArg(value, 1), IrType::U32) && isImmediate(resolveArg(value, 2), IrType::U32)) { + auto& shifted = resolveArg(value, 0); + auto& amount = resolveArg(shifted, 1); + const auto fieldOffset = resolveArg(value, 1).ImmediateU32(); + if (isImmediate(amount, IrType::U32) && amount.ImmediateU32() > 0u && amount.ImmediateU32() < 32u && fieldOffset >= amount.ImmediateU32() && fieldOffset < 32u) { + value.ReplaceArgument(1, &builder.Constant(fieldOffset - amount.ImmediateU32())); + value.ReplaceArgument(0, &resolveArg(shifted, 0)); + } + } auto& source = resolveArg(value, 0); auto& offset = resolveArg(value, 1); auto& count = resolveArg(value, 2); @@ -456,6 +611,10 @@ bool ConstantFolder::tryFoldValue(IrProgram& program, IrValue& value) const { return true; } } + if (value.Opcode() == IrOpcode::BitFieldUExtract && isImmediateU32(offset, 0u) && isImmediate(count, IrType::U32) && count.ImmediateU32() < 32u && !isImmediate(source, IrType::U32) && (possibleBits(source) >> count.ImmediateU32()) == 0u) { + replaceWith(value, source); + return true; + } if (!isImmediate(source, IrType::U32) || !isImmediate(offset, IrType::U32) || !isImmediate(count, IrType::U32) || offset.ImmediateU32() > 32u || count.ImmediateU32() > 32u - offset.ImmediateU32()) { return false; } @@ -478,6 +637,7 @@ bool ConstantFolder::tryFoldValue(IrProgram& program, IrValue& value) const { case IrOpcode::BitCastU32F32: return foldBitCast(builder, value, IrOpcode::BitCastF32U32); case IrOpcode::BitCastF32U32: return foldBitCast(builder, value, IrOpcode::BitCastU32F32); case IrOpcode::ConvertU16U32: { + readThroughUnusedBits(value, 0, 0xffffu); auto& operand = resolveArg(value, 0); if (!isImmediate(operand, IrType::U32)) { return false; @@ -615,6 +775,7 @@ bool ConstantFolder::tryFoldValue(IrProgram& program, IrValue& value) const { if (foldU32(builder, value, [](std::uint32_t a, std::uint32_t b) { return a << (b & 31u); })) { return true; } + if (isImmediate(resolveArg(value, 1), IrType::U32)) readThroughUnusedBits(value, 0, UINT32_MAX >> (resolveArg(value, 1).ImmediateU32() & 31u)); auto& shift = resolveArg(value, 1); if (isImmediate(shift, IrType::U32) && (shift.ImmediateU32() & 31u) == 0u) { replaceWith(value, resolveArg(value, 0)); @@ -631,9 +792,26 @@ bool ConstantFolder::tryFoldValue(IrProgram& program, IrValue& value) const { replaceWith(value, resolveArg(value, 0)); return true; } + if (!isImmediate(shift, IrType::U32)) return false; + const auto amount = shift.ImmediateU32() & 31u; + readThroughUnusedBits(value, 0, UINT32_MAX << amount); + auto& shifted = resolveArg(value, 0); + if (shifted.Opcode() == IrOpcode::ShiftLeftLogical32 && isImmediate(resolveArg(shifted, 1), IrType::U32) && (resolveArg(shifted, 1).ImmediateU32() & 31u) == amount) { + auto& inner = resolveArg(shifted, 0); + if ((possibleBits(inner) & ~(UINT32_MAX >> amount)) == 0u) { + replaceWith(value, inner); + return true; + } + value.ReplaceOpcode(IrOpcode::BitwiseAnd32); + value.ReplaceArgument(0, &inner); + value.ReplaceArgument(1, &builder.Constant(UINT32_MAX >> amount)); + foldRedundantMask(value); + return true; + } return false; } case IrOpcode::ShiftRightArithmetic32: { + if (isImmediate(resolveArg(value, 1), IrType::U32)) readThroughUnusedBits(value, 0, UINT32_MAX << (resolveArg(value, 1).ImmediateU32() & 31u)); return foldU32(builder, value, [](std::uint32_t a, std::uint32_t b) { return static_cast(std::bit_cast(a) >> (b & 31u)); }); } case IrOpcode::ShiftLeftLogical64: { @@ -665,7 +843,14 @@ bool ConstantFolder::tryFoldValue(IrProgram& program, IrValue& value) const { if (foldU32(builder, value, [](std::uint32_t a, std::uint32_t b) { return a & b; })) { return true; } - return replaceBinaryIdentity(value, IrType::U32, 0xffffffffu); + if (replaceBinaryIdentity(value, IrType::U32, 0xffffffffu)) return true; + if (foldRedundantMask(value)) return true; + bool changed = false; + for (std::size_t side = 0; side < 2u; ++side) { + if (isImmediate(resolveArg(value, side), IrType::U32)) changed |= readThroughUnusedBits(value, 1u - side, resolveArg(value, side).ImmediateU32()); + } + if (changed) foldRedundantMask(value); + return changed; } case IrOpcode::BitwiseAnd64: { if (foldU64(builder, value, [](std::uint64_t a, std::uint64_t b) { return a & b; })) { @@ -679,7 +864,8 @@ bool ConstantFolder::tryFoldValue(IrProgram& program, IrValue& value) const { if (foldU32(builder, value, [opcode](std::uint32_t a, std::uint32_t b) { return opcode == IrOpcode::BitwiseOr32 ? a | b : a ^ b; })) { return true; } - return replaceBinaryIdentity(value, IrType::U32, 0u); + if (replaceBinaryIdentity(value, IrType::U32, 0u)) return true; + return opcode == IrOpcode::BitwiseOr32 && mergeMaskedParts(program, value); } case IrOpcode::BitwiseNot32: { auto& operand = resolveArg(value, 0); From e191a132ab19fb348dac9fea953f782f26da77fd Mon Sep 17 00:00:00 2001 From: T3cm0 Date: Sun, 18 Oct 2026 18:57:14 -0500 Subject: [PATCH 041/269] feat(libSceHmd2): return the console's display and mirroring buffer sizes Co-Authored-By: Claude Fable 5.1 --- core/libs/prx/libSceHmd2/Export.cpp | 20 ++++++++------------ core/libs/prx/libSceHmd2/Hmd2.hpp | 11 +++++++++++ core/libs/tests/GuestHmd2.cpp | 12 ++++++++++++ docs/dev/debt/libSceHmd2.md | 2 +- docs/dev/debt/libSceVideoOut.md | 1 + 5 files changed, 33 insertions(+), 13 deletions(-) diff --git a/core/libs/prx/libSceHmd2/Export.cpp b/core/libs/prx/libSceHmd2/Export.cpp index 55d518e133..8fd2c63674 100644 --- a/core/libs/prx/libSceHmd2/Export.cpp +++ b/core/libs/prx/libSceHmd2/Export.cpp @@ -47,9 +47,8 @@ int APS5_VABI sceHmd2ReprojectionGetStatus() { return 0; } -int APS5_VABI sceHmd2ReprojectionQuerySeeThroughBufferSizeAlign() { - NotImplemented_nid_no_patch(__func__); - return 0; +Hmd2::SizeAlign APS5_VABI sceHmd2ReprojectionQuerySeeThroughBufferSizeAlign() { + return {0x1106000, 0x100}; } std::int32_t APS5_VABI sceHmd2Initialize(const void* param) { @@ -151,14 +150,12 @@ int APS5_VABI sceHmd2ReprojectionEnableVrMode() { return 0; } -int APS5_VABI sceHmd2ReprojectionGetMirroringDisplayBufferSizeAlign() { - NotImplemented_nid_no_patch(__func__); - return 0; +Hmd2::SizeAlign APS5_VABI sceHmd2ReprojectionGetMirroringDisplayBufferSizeAlign() { + return {0x5FA0000, 0x10000}; } -int APS5_VABI sceHmd2ReprojectionGetMirroringWorkMemorySizeAlign() { - NotImplemented_nid_no_patch(__func__); - return 0; +Hmd2::SizeAlign APS5_VABI sceHmd2ReprojectionGetMirroringWorkMemorySizeAlign() { + return {0x4000, 0x100}; } int APS5_VABI sceHmd2ReprojectionGetPredictedDisplayTime() { @@ -176,9 +173,8 @@ int APS5_VABI sceHmd2ReprojectionQueryBufferSizeAlign() { return 0; } -int APS5_VABI sceHmd2ReprojectionQueryDisplayBufferSizeAlign() { - NotImplemented_nid_no_patch(__func__); - return 0; +Hmd2::SizeAlign APS5_VABI sceHmd2ReprojectionQueryDisplayBufferSizeAlign() { + return {0x6000000, 0x10000}; } int APS5_VABI sceHmd2ReprojectionSetAllowPositionalReprojection() { diff --git a/core/libs/prx/libSceHmd2/Hmd2.hpp b/core/libs/prx/libSceHmd2/Hmd2.hpp index 3594b32bd1..5e978e686f 100644 --- a/core/libs/prx/libSceHmd2/Hmd2.hpp +++ b/core/libs/prx/libSceHmd2/Hmd2.hpp @@ -49,6 +49,13 @@ static_assert(offsetof(DeviceInformation, hmuMount) == 20); static_assert(offsetof(DeviceInformation, virtualImageDistance) == 24); static_assert(sizeof(FieldOfView) == 16); +struct SizeAlign { + std::uint64_t size; + std::uint64_t align; +}; + +static_assert(sizeof(SizeAlign) == 16); + } extern "C" { @@ -61,6 +68,10 @@ 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 sceHmd2ReprojectionQueryDisplayBufferSizeAlign(); +Hmd2::SizeAlign APS5_VABI sceHmd2ReprojectionQuerySeeThroughBufferSizeAlign(); +Hmd2::SizeAlign APS5_VABI sceHmd2ReprojectionGetMirroringDisplayBufferSizeAlign(); +Hmd2::SizeAlign APS5_VABI sceHmd2ReprojectionGetMirroringWorkMemorySizeAlign(); } #endif diff --git a/core/libs/tests/GuestHmd2.cpp b/core/libs/tests/GuestHmd2.cpp index 8ec9cbeb30..303c58109c 100644 --- a/core/libs/tests/GuestHmd2.cpp +++ b/core/libs/tests/GuestHmd2.cpp @@ -130,7 +130,19 @@ void TestInitialized() { } +void SizeQueriesReturnTheConsoleSizes() { + const auto display = sceHmd2ReprojectionQueryDisplayBufferSizeAlign(); + Require(display.size == 0x6000000 && display.align == 0x10000, "display buffer size"); + const auto seeThrough = sceHmd2ReprojectionQuerySeeThroughBufferSizeAlign(); + Require(seeThrough.size == 0x1106000 && seeThrough.align == 0x100, "see-through buffer size"); + const auto mirroring = sceHmd2ReprojectionGetMirroringDisplayBufferSizeAlign(); + Require(mirroring.size == 0x5FA0000 && mirroring.align == 0x10000, "mirroring display buffer size"); + const auto work = sceHmd2ReprojectionGetMirroringWorkMemorySizeAlign(); + Require(work.size == 0x4000 && work.align == 0x100, "mirroring work memory size"); +} + int main() { + SizeQueriesReturnTheConsoleSizes(); InstallProcessParameters(); TestBeforeInitialize(); TestInitialized(); diff --git a/docs/dev/debt/libSceHmd2.md b/docs/dev/debt/libSceHmd2.md index 7de17e8644..bb7360c2b1 100644 --- a/docs/dev/debt/libSceHmd2.md +++ b/docs/dev/debt/libSceHmd2.md @@ -6,7 +6,6 @@ ### Unknown function info -- PPSA29866 imports declared without parameters, signatures unknown, added without an implementation because the title fails to load without them: [sceHmd2ReprojectionQuerySeeThroughBufferSizeAlign](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2), [sceVideoOutSetFlipMaster and sceVideoOutClearFlipMaster](../../../core/libs/prx/libSceVideoOut/Unimplemented.cpp) (libSceVideoOut) - PPSA09030 imports from its `Media/Plugins/psvr2.prx` declared without parameters, signatures unknown, added without an implementation because the title fails to load the plugin without them: [sceHmd2ReprojectionGetStatus](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - [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 @@ -16,3 +15,4 @@ ### 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 diff --git a/docs/dev/debt/libSceVideoOut.md b/docs/dev/debt/libSceVideoOut.md index 8a5caa007a..6dcc6c7d31 100644 --- a/docs/dev/debt/libSceVideoOut.md +++ b/docs/dev/debt/libSceVideoOut.md @@ -9,6 +9,7 @@ ### 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 - [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. From a51bb03cc8b4c777fa7b8b1cc90cfe7fd78aa73d Mon Sep 17 00:00:00 2001 From: Jota <139649225+jotitadev@users.noreply.github.com> Date: Thu, 15 Oct 2026 16:55:28 -0300 Subject: [PATCH 042/269] fix(agc): ignore Finder .DS_Store files in capture directories --- .../Execution/src/ComputePipelineCapture.cpp | 7 ++++++- .../prx/libSceAgcDriver/tests/ComputePipelineCapture.cpp | 9 +++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/ComputePipelineCapture.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/ComputePipelineCapture.cpp index 9d9fa84300..dd11ceea45 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/ComputePipelineCapture.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/ComputePipelineCapture.cpp @@ -296,8 +296,13 @@ std::filesystem::path ComputePipelineCapture::Write(const std::filesystem::path& CheckCaptureIo(error, "cannot enumerate capture directory"); while (iterator != std::filesystem::directory_iterator{}) { const auto& entry = *iterator; - RequireCaptureCapacity(++count < 128, "capture directory reached 128 entries"); const auto entryStatus = CaptureStatus(entry.path()); + if (std::filesystem::is_regular_file(entryStatus) && entry.path().filename() == ".DS_Store") { + iterator.increment(error); + CheckCaptureIo(error, "cannot advance capture directory"); + continue; + } + RequireCaptureCapacity(++count < 128, "capture directory reached 128 entries"); RequireCapture(std::filesystem::is_directory(entryStatus) && !std::filesystem::is_symlink(entryStatus), "capture directory contains an unexpected entry"); const auto capture = entry.path() / "capture.aps5cp"; const auto captureStatus = CaptureStatus(capture); diff --git a/core/libs/prx/libSceAgcDriver/tests/ComputePipelineCapture.cpp b/core/libs/prx/libSceAgcDriver/tests/ComputePipelineCapture.cpp index b27be01d0e..eb6a6b104b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ComputePipelineCapture.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ComputePipelineCapture.cpp @@ -260,8 +260,17 @@ void CheckOptionalInvalid() { Reject([&] { capture.WriteOptional(directory.path); }, "optional capture swallowed a non-directory existing record"); Require(AgcDriver::ComputePipelineCapture::OptionalEnabled() && std::filesystem::file_size(path.parent_path()) == 1u, "non-directory record disabled optional capture or was changed"); Directory unexpected; + const auto finder = unexpected.path / ".DS_Store"; + std::ofstream(finder, std::ios::binary).put('x'); + capture.specialization[0].value = 1u; + Require(!capture.Write(unexpected.path).empty(), "strict capture rejected a Finder metadata file"); + capture.specialization[0].value = 2u; + Require(!capture.WriteOptional(unexpected.path).empty() && AgcDriver::ComputePipelineCapture::OptionalEnabled(), "optional capture rejected a Finder metadata file"); + Require(std::filesystem::file_size(finder) == 1u, "Finder metadata file was changed"); + capture.specialization[0].value = 3u; const auto foreign = unexpected.path / "foreign"; std::ofstream(foreign, std::ios::binary).put('x'); + RejectWithMessage([&] { capture.Write(unexpected.path); }, "capture directory contains an unexpected entry"); Reject([&] { capture.WriteOptional(unexpected.path); }, "optional capture swallowed an unexpected directory entry"); Require(AgcDriver::ComputePipelineCapture::OptionalEnabled() && std::filesystem::file_size(foreign) == 1u, "unexpected entry disabled optional capture or was changed"); } From 42fb02cdbafda892c379d14db5ab44e06c1ab430 Mon Sep 17 00:00:00 2001 From: Jota <139649225+jotitadev@users.noreply.github.com> Date: Sun, 18 Oct 2026 18:18:39 -0300 Subject: [PATCH 043/269] fix(libc): let the macOS write watch fault handler go first The macOS write watch serialized its fault handler and collections with a ticket lock whose waiters spin on sched_yield. When the thread holding the next turn is not running, every other thread waits behind it even though the lock is free, so with more runnable threads than cores the handoffs stall. write_watch_threads took 6.75 s and then timed out at 30 s on the macos-15-intel runner in run 39151604364, a run whose pull request does not touch the write watch. The lock is now a pthread mutex with the first-fit policy: a waiter blocks in the kernel, and the mutex goes to whichever thread takes it first instead of to the next turn in line. The ticket lock was there so that a thread collecting in a loop cannot starve writers waiting in the handler. That is kept by giving the handler precedence: a collection and every other entry point wait on a condition variable while a fault is waiting for the mutex or holding it. Faults cannot starve them in turn, since a fault lifts the protection of a page and only those entry points protect pages again. --- core/libs/prx/libc/src/GuestWriteWatch.cpp | 43 ++++++++++++++++------ 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libc/src/GuestWriteWatch.cpp b/core/libs/prx/libc/src/GuestWriteWatch.cpp index 52907e1cb0..e0a4344e67 100644 --- a/core/libs/prx/libc/src/GuestWriteWatch.cpp +++ b/core/libs/prx/libc/src/GuestWriteWatch.cpp @@ -29,7 +29,7 @@ #include #include #include -#include +#include #include #include #endif @@ -484,20 +484,41 @@ private: std::vector pages; }; - struct Turns { - std::atomic next{0}; - std::atomic serving{0}; + struct Mutex { + Mutex() { + pthread_mutexattr_t attributes; + pthread_mutexattr_init(&attributes); + pthread_mutexattr_setpolicy_np(&attributes, PTHREAD_MUTEX_POLICY_FIRSTFIT_NP); + pthread_mutex_init(&mutex, &attributes); + pthread_mutexattr_destroy(&attributes); + pthread_cond_init(&idle, nullptr); + } + pthread_mutex_t mutex; + pthread_cond_t idle; + std::atomic faulting{0}; }; struct Lock { - explicit Lock(Turns& turns) : _turns(turns), _turn(turns.next.fetch_add(1, std::memory_order_relaxed)) { - while (_turns.serving.load(std::memory_order_acquire) != _turn) ::sched_yield(); + explicit Lock(Mutex& mutex) : _mutex(mutex) { + pthread_mutex_lock(&_mutex.mutex); + while (_mutex.faulting.load(std::memory_order_acquire) != 0) pthread_cond_wait(&_mutex.idle, &_mutex.mutex); } ~Lock() { - _turns.serving.store(_turn + 1, std::memory_order_release); + pthread_mutex_unlock(&_mutex.mutex); } - Turns& _turns; - std::uint64_t _turn; + Mutex& _mutex; + }; + + struct FaultLock { + explicit FaultLock(Mutex& mutex) : _mutex(mutex) { + _mutex.faulting.fetch_add(1, std::memory_order_acq_rel); + pthread_mutex_lock(&_mutex.mutex); + } + ~FaultLock() { + if (_mutex.faulting.fetch_sub(1, std::memory_order_acq_rel) == 1) pthread_cond_broadcast(&_mutex.idle); + pthread_mutex_unlock(&_mutex.mutex); + } + Mutex& _mutex; }; Watch() { @@ -530,7 +551,7 @@ private: bool record(std::uintptr_t address) { const auto page = pageStart(address); - const Lock lock(_lock); + const FaultLock lock(_lock); auto it = _ranges.upper_bound(page); if (it == _ranges.begin()) return false; --it; @@ -623,7 +644,7 @@ private: const std::uintptr_t _page = static_cast(::getpagesize()); bool _available = false; - Turns _lock; + Mutex _lock; std::map _ranges; std::vector> _hostWrites; struct sigaction _previousBus {}; From 3018bc659f4a126b4b379079456df6004e2b6aed Mon Sep 17 00:00:00 2001 From: JJenkx Date: Thu, 15 Oct 2026 07:43:20 -0400 Subject: [PATCH 044/269] fix(agc): order raw shader reads after queued code writes --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 1 + .../src/Driver/Shaders/ShaderRegistry.cpp | 8 +++ .../tests/execution/QueuedRawShader.cpp | 66 +++++++++++++++++++ 3 files changed, 75 insertions(+) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/QueuedRawShader.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 7cb916999f..cfab76c093 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1875,6 +1875,7 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_draw_pipeline_trace_disabled PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + set_tests_properties(agc_driver_queued_raw_shader PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_PIPELINED_DRAWS=8" TIMEOUT 30) add_test(NAME agc_driver_partially_resident_pending_stores COMMAND agc_driver_partially_resident_snapshot_tests --pending-stores) set_tests_properties(agc_driver_partially_resident_pending_stores PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30) if(WIN32) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp index 22bee51c49..ae349b1a8d 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -2,6 +2,7 @@ #include "ControlFlow/GraphBuilder.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" @@ -69,6 +70,11 @@ std::shared_ptr PrepareShaderWithDiagnosti } std::shared_ptr ReadRawShader(std::uint64_t address) { + const auto drain = [](std::uint64_t first, std::size_t bytes) { + if (!DrawPipeline::Active()) return; + auto& pipeline = DrawPipeline::Queue0(); + if (pipeline.Busy() && pipeline.Overlaps(first, bytes)) pipeline.Drain(DrawPipeline::DrainReason::Capture); + }; GuestMemory::CheckRange(reinterpret_cast(address), sizeof(std::uint32_t), 256); static std::mutex cacheMutex; static std::list> cache; @@ -81,6 +87,7 @@ std::shared_ptr ReadRawShader(std::uint64_t address) { } if (cached) { const auto code = std::as_bytes(std::span(cached->code)); + drain(address, code.size()); GuestMemory::FlushGpuWrites(address, code.size()); if (GuestMemory::CompareMapped(address, code) == GuestMemory::Compare::Equal) { std::lock_guard lock(cacheMutex); @@ -101,6 +108,7 @@ std::shared_ptr ReadRawShader(std::uint64_t address) { while (snapshot.code.size() < available) { const auto previous = snapshot.code.size(); snapshot.code.resize(std::min(available, std::max(64, previous * 2))); + drain(address + previous * sizeof(std::uint32_t), (snapshot.code.size() - previous) * sizeof(std::uint32_t)); GuestMemory::Read(address + previous * sizeof(std::uint32_t), std::as_writable_bytes(std::span(snapshot.code).subspan(previous)), alignof(std::uint32_t)); try { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/QueuedRawShader.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/QueuedRawShader.cpp new file mode 100644 index 0000000000..703f7523d7 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/QueuedRawShader.cpp @@ -0,0 +1,66 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void QueuedCode(bool cached) { + using namespace AgcDriver::DriverDetail; + alignas(256) static std::array, 2> programs{}; + auto& code = programs[cached ? 0 : 1]; + code.fill(0xbf810000u); + code[0] = 0x7e000200u; + const auto address = reinterpret_cast(code.data()); + const auto before = cached ? ReadRawShader(address) : nullptr; + std::atomic finished{false}; + std::atomic started{false}; + struct Restore { + std::atomic& finished; + bool active = DrawPipeline::Active(); + ~Restore() { + finished.store(true, std::memory_order_release); + DrawPipeline::Active() = active; + DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Submission); + } + } restore{finished}; + DrawPipeline::Queue0().Enqueue([&, address] { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(200); + started.store(true, std::memory_order_release); + while (!finished.load(std::memory_order_acquire) && std::chrono::steady_clock::now() < deadline) std::this_thread::yield(); + const std::uint32_t replacement = 0x7e0002f2u; + AgcDriver::GuestMemory::Write(address, std::as_bytes(std::span(&replacement, 1)), alignof(std::uint32_t)); + }, {{address, address + sizeof(std::uint32_t)}}); + while (!started.load(std::memory_order_acquire)) std::this_thread::yield(); + DrawPipeline::Active() = true; + const auto after = ReadRawShader(address); + finished.store(true, std::memory_order_release); + DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Submission); + if (after->code.front() != 0x7e0002f2u || (before != nullptr && (before->code.front() != 0x7e000200u || before == after))) { + throw std::runtime_error(std::string(cached ? "cached" : "fresh") + " raw shader read ignored a queued write to its code"); + } +} + +} + +int main(int argc, char** argv) { + try { + if (AgcDriver::DriverDetail::DrawPipeline::Depth() == 0) return 77; + if (argc == 1 || std::string(argv[1]) == "cached") QueuedCode(true); + if (argc == 1 || std::string(argv[1]) == "fresh") QueuedCode(false); + std::cout << "Raw shader reads observed queued code writes\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} From 2ab8deac1d1bfdbfb94552b8d4edd16f229fce93 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Fri, 16 Oct 2026 14:10:27 +0000 Subject: [PATCH 045/269] fix(agc): dump a failing shader capture's own request With APS5_DUMP_SHADERS, a dispatch or draw stage that failed named shader_
.req, which is written once per address by the first call that captures the shader: a later call's failure pointed at an earlier call's user data and memory, and a failing capture's memory was never in the dump. A failure now writes shader_
_failed.req with the failing call's request and the memory its capture read, and names that file. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 1 + .../src/Driver/Dispatch/Dispatch.cpp | 8 +- .../Execution/src/Driver/Draw/DrawCapture.cpp | 11 +- .../tests/execution/FailedCaptureDump.cpp | 142 ++++++++++++++++++ 4 files changed, 157 insertions(+), 5 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/FailedCaptureDump.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 7cb916999f..93e73d100e 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1775,6 +1775,7 @@ target_include_directories(agc_driver_buffer_descriptor_runtime_tests PRIVATE $< target_include_directories(agc_driver_shutdown_threads_tests PRIVATE $) target_include_directories(agc_driver_image_converted_unorm_tests PRIVATE $) +target_include_directories(agc_driver_failed_capture_dump_tests PRIVATE $) target_include_directories(agc_driver_prepared_publication_tests PRIVATE $) 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 565d951689..565a0fe0d6 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp @@ -358,7 +358,13 @@ void Driver::dispatch(QueueState& queue, std::span packet, totalMs += elapsed; if (profile && elapsed > 200) std::fprintf(stderr, "[gpu] compute shader 0x%llx recompile took %.0f ms (%zu SPIR-V words, %zu captured regions, total %.1f s)\n", static_cast(address), elapsed, compiledResult->spirv.size(), captured.size(), totalMs / 1000); } catch (const std::exception& error) { - const auto dump = dumpShaders ? dumpRequest(address, request) : std::string{}; + std::string dump; + if (dumpShaders) { + const auto regions = shaderMemory->Regions(); + auto failed = request; + failed.context.memory = regions; + dump = dumpRequest(address, failed, "_failed"); + } std::string reason = error.what(); if (const auto newline = reason.find('\n'); newline != std::string::npos) reason.resize(newline); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp index c761433da8..d7c1419102 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -32,12 +33,14 @@ std::shared_ptr Driver::materializeDraw auto& stageCapture = stageCaptures[i]; stageCapture.forgetSerial = GuestMemory::ForgetSerial(); stageCapture.pushOffset = pushOffset; - const auto named = [&](const std::exception& error) { + const auto named = [&](const std::exception& error, std::span regions) { static const bool dumpShaders = std::getenv("APS5_DUMP_SHADERS") != nullptr; std::string dump; if (dumpShaders) { try { - dump = dumpRequest(program.binary.codeAddress, request); + auto failed = request; + failed.context.memory = regions; + dump = dumpRequest(program.binary.codeAddress, failed, "_failed"); } catch (...) {} } std::ostringstream where; @@ -51,7 +54,7 @@ std::shared_ptr Driver::materializeDraw try { return CaptureInvocation(shaderMemory, *program.snapshot, program.codeOffset, request, invocation); } catch (const std::exception& error) { - throw named(error); + throw named(error, shaderMemory.Regions()); } }(); @@ -98,7 +101,7 @@ std::shared_ptr Driver::materializeDraw try { stageCapture.compiled = invocation.Materialize(*capture); } catch (const std::exception& error) { - throw named(error); + throw named(error, request.context.memory); } stageCapture.capture = capture; timing.Mark("materialize"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/FailedCaptureDump.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/FailedCaptureDump.cpp new file mode 100644 index 0000000000..1a8e78f0dd --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/FailedCaptureDump.cpp @@ -0,0 +1,142 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "ControlFlow/RequestSerializer.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; + +alignas(256) constexpr std::array Code{ + 0xf4080200u, 0xfa000000u, 0xbf8cc07fu, 0x7e020280u, 0xe0702000u, 0x80020100u, 0xbf810000u, 0u, +}; +constexpr std::uint32_t Untouched = 0xcafebabeu; +alignas(256) std::array Output{}; +alignas(16) std::array Valid{}; +alignas(16) std::array Unsupported{}; + +std::array BufferDescriptor(std::uint32_t word3) { + const auto address = reinterpret_cast(Output.data()); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (4u << 16u), static_cast(Output.size()), word3}; +} + +std::array Pointer(const void* descriptor) { + const auto address = reinterpret_cast(descriptor); + return {static_cast(address), static_cast(address >> 32u)}; +} + +void Dispatch(const std::array& userData) { + std::vector words; + const auto registers = [&](std::uint32_t first, std::span values) { + words.push_back(0xc0007600u | (static_cast(values.size()) << 16u)); + words.push_back(first); + words.insert(words.end(), values.begin(), values.end()); + }; + const std::array shape{64, 1, 1}; + registers(0x207, shape); + const auto address = reinterpret_cast(Code.data()); + const std::array program{static_cast(address >> 8u), static_cast(address >> 40u)}; + registers(0x20c, program); + const std::array resource{static_cast(userData.size()) << 1u}; + registers(0x213, resource); + registers(0x240, userData); + words.insert(words.end(), {0xc0031500u, 1, 1, 1, 0x41u}); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + AgcDriver::Submit(&packet, 0); + AgcDriverWaitIdle_nid_postfix(); +} + +std::string Read(const std::filesystem::path& path) { + std::ifstream file(path, std::ios::binary); + Require(file.is_open(), "shader request " + path.string() + " was not written"); + return {std::istreambuf_iterator(file), std::istreambuf_iterator()}; +} + +std::string Replay(const std::string& text) { + const auto replay = ShaderRecompiler::RequestSerializer{}.Deserialize(text); + try { + static_cast(ShaderRecompiler::Recompile(replay.request)); + } catch (const std::exception& error) { + return error.what(); + } + return {}; +} + +std::vector UserData(const std::string& text) { + const auto replay = ShaderRecompiler::RequestSerializer{}.Deserialize(text); + return {replay.request.context.userData.begin(), replay.request.context.userData.end()}; +} + +void Run() { + char prefix[64]; + std::snprintf(prefix, sizeof(prefix), "shader_%llx", static_cast(reinterpret_cast(Code.data()))); + const std::string first = std::string(prefix) + ".req"; + const std::string failed = std::string(prefix) + "_failed.req"; + Valid = BufferDescriptor(0x00014204u); + Unsupported = BufferDescriptor(0xa1b00facu); + Output.fill(Untouched); + const auto valid = Pointer(Valid.data()); + Dispatch(valid); + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(Output.data()), sizeof(Output)); + for (const auto word : Output) Require(word == 0u, "the dispatch through the valid descriptor stored nothing"); + const auto unsupported = Pointer(Unsupported.data()); + std::string message; + try { + Dispatch(unsupported); + } catch (const std::exception& error) { + message = error.what(); + } + Require(message.find("unsupported type") != std::string::npos, "the dispatch through a type 2 descriptor failed for another reason: " + message); + Require(message.find("(" + failed + ")") != std::string::npos, "the failure does not name the failing dispatch's request: " + message); + const auto firstText = Read(first); + const auto failedText = Read(failed); + Require(UserData(firstText) == std::vector(valid.begin(), valid.end()), "the first dispatch's request lost its user data"); + Require(UserData(failedText) == std::vector(unsupported.begin(), unsupported.end()), "the failed request holds another dispatch's user data"); + Require(Replay(firstText).empty(), "the first dispatch's request does not replay: " + Replay(firstText)); + const auto replayed = Replay(failedText); + Require(replayed.find("unsupported type") != std::string::npos, "the failed request does not replay its failure: " + replayed); +} + +} + +int main() { + const auto directory = std::filesystem::temp_directory_path() / "agc_failed_capture_dump"; + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + std::filesystem::remove_all(directory); + std::filesystem::create_directories(directory); + std::filesystem::current_path(directory); +#ifdef _WIN32 + Require(_putenv_s("APS5_DUMP_SHADERS", "1") == 0, "cannot enable shader capture"); +#else + Require(setenv("APS5_DUMP_SHADERS", "1", 1) == 0, "cannot enable shader capture"); +#endif + Run(); + try { AgcDriverShutdown_nid_postfix(); } catch (const std::exception&) {} + std::filesystem::current_path(directory.parent_path()); + std::filesystem::remove_all(directory); + std::puts("failed capture dump tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + try { AgcDriverShutdown_nid_postfix(); } catch (const std::exception&) {} + return 1; + } +} From 79874b9c6d0d64088cfa7391d2543541a58d975d Mon Sep 17 00:00:00 2001 From: milescoviello Date: Sat, 17 Oct 2026 14:34:02 +0000 Subject: [PATCH 046/269] test(agc): give each failed-capture dump run its own folder Two copies of agc_driver_failed_capture_dump that ran at once shared temp_directory_path()/agc_failed_capture_dump, and one removed the folder while the other was writing its captures there. The folder name now carries a std::random_device value, as the pipeline journal test's does. Co-Authored-By: Claude Opus 5.5 --- .../prx/libSceAgcDriver/tests/execution/FailedCaptureDump.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/FailedCaptureDump.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/FailedCaptureDump.cpp index 1a8e78f0dd..0382270371 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/FailedCaptureDump.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/FailedCaptureDump.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -114,7 +115,7 @@ void Run() { } int main() { - const auto directory = std::filesystem::temp_directory_path() / "agc_failed_capture_dump"; + const auto directory = std::filesystem::temp_directory_path() / ("agc_failed_capture_dump-" + std::to_string(std::random_device{}())); try { { const auto device = OpenVulkanTestDevice(); From 193c540162683175b74a1e8963a5b85f29d58096 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Mon, 19 Oct 2026 01:22:37 +0000 Subject: [PATCH 047/269] fix(shader): point-filter direct samplers paired with unsigned integer images A direct sampled image of the unsigned integer class kept its sampler's linear filter, while signed integer, converted and depth views got the point-filtered copy. Vulkan formats like R32_UINT have no linear filter support, so the sample was undefined (NVIDIA returned the point sample, lavapipe did not) and the validation layer reported VUID-vkCmdDispatch-magFilter-04553 for agc_driver_image_table. Unsigned integer images now get the point-filtered copy too, as image table entries already do. Fixes #7699 Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp | 2 +- .../recompiler/Optimization/src/DescriptorBindingBuilder.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp index cf78250330..d353e82b93 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp @@ -873,7 +873,7 @@ void RunTests(AgcDriver::VulkanDevice& device) { const auto sintControl = RunDirect(device, LinearWrap, sintImage); const auto floatControl = RunDirect(device, LinearWrap, floatImage); const auto uintControl = RunDirect(device, LinearWrap, uintImage); - Require(floatControl.samplers == std::vector{LinearWrap} && uintControl.samplers == std::vector{LinearWrap} && sintControl.samplers == std::vector{PointWrap}, "image table: the direct bindings under a linear S# changed their sampler copies"); + Require(floatControl.samplers == std::vector{LinearWrap} && uintControl.samplers == std::vector{PointWrap} && sintControl.samplers == std::vector{PointWrap}, "image table: the direct bindings under a linear S# changed their sampler copies"); Require(Group(integerLinear, 0) == Group(sintControl, 0) && Group(integerLinear, 1) == Group(floatControl, 0), "image table: an entry under a linear direct S# differs from the direct binding"); RequireRed(integerLinear, 0, Lanes, [](std::uint32_t x) { return static_cast(SintTexels[x]); }, "sint texels through a linear direct sampler"); RequireRed(integerLinear, 2, Lanes, [](std::uint32_t x) { return UintTexels[x]; }, "uint texels through a linear direct sampler"); diff --git a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp index 82904cf201..fee428cac9 100644 --- a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp +++ b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp @@ -364,7 +364,7 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l std::vector samplerIntegerModes(info.samplers.size(), 0u); std::vector samplerFloatModes(info.samplers.size(), 0u); std::vector directSamplerModes(info.samplers.size(), 0u); - const auto samplerMode = [](const ImageResource& image) { return !image.constantSwizzle && (image.numericClass == IrTextureNumericClass::Sint || image.conversionFormat != IrBufferFormat::Invalid || image.depthBits) ? 2u : 1u; }; + const auto samplerMode = [](const ImageResource& image) { return !image.constantSwizzle && (image.numericClass == IrTextureNumericClass::Uint || image.numericClass == IrTextureNumericClass::Sint || image.conversionFormat != IrBufferFormat::Invalid || image.depthBits) ? 2u : 1u; }; const auto pairSampler = [&](std::uint32_t sampler, const ImageResource& image) { const auto mode = samplerMode(image); samplerModes.at(sampler) |= mode; From 907c9cd1b6b5925e7cebc64ff874db96de61224b Mon Sep 17 00:00:00 2001 From: XairRrR Date: Thu, 15 Oct 2026 04:38:03 +0200 Subject: [PATCH 048/269] fix(agc): keep the Vulkan pipeline cache file with its shader cache version The pipeline cache file lived in the shader cache root and every run merged its pipelines into it, so pipelines built from SPIR-V of earlier recompiler versions were never dropped. The file only grew (965 MB for PPSA05684 after a few recompiler changes) and the driver loads all of it into host memory at device creation. The file now sits in the directory of the current shader cache source version, next to the variants it was built from. AI-assisted: yes --- core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp | 5 ++++- core/libs/prx/libSceAgcDriver/tests/Recorder.cpp | 4 ++++ docs/user/USAGE.md | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp index a3cf852d6a..29d4dca505 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp @@ -1,5 +1,6 @@ #include "prx/libSceAgcDriver/Graphics/include/PipelineCache.hpp" #include "ShaderCacheDirectory.hpp" +#include "ShaderDiskCache.hpp" #include #include #include @@ -45,9 +46,11 @@ PipelineCache::PipelineCache(const Context& context, const VkPhysicalDevicePrope std::snprintf(digits, sizeof(digits), "%02x", static_cast(byte)); uuid += digits; } + char version[17]; + std::snprintf(version, sizeof(version), "%016llx", static_cast(ShaderRecompiler::ShaderDiskCache::SourceVersion())); char name[160]; std::snprintf(name, sizeof(name), "vk-pipelines-%04x-%04x-%08x-%s.bin", properties.vendorID, properties.deviceID, properties.driverVersion, uuid.c_str()); - path = directory / name; + path = directory / version / name; } std::vector initialData; if (!path.empty()) { diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 65f350d63b..e63edeaa7a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -8310,6 +8310,10 @@ int pipelineCacheFileTests(const Device& device) { const auto cacheFile = [&] { std::vector found; for (const auto& entry : std::filesystem::directory_iterator(directory)) found.push_back(entry.path()); + Require(found.size() == 1 && std::filesystem::is_directory(found.front()), "the pipeline cache file does not live in the shader cache version's directory alone"); + const auto version = found.front(); + found.clear(); + for (const auto& entry : std::filesystem::directory_iterator(version)) found.push_back(entry.path()); Require(found.size() == 1 && found.front().filename().string().starts_with("vk-pipelines-"), "the pipeline cache directory does not hold exactly one cache file"); return found.front(); }; diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 6f299ab99b..6e1318bd71 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -265,7 +265,7 @@ HDR output is available on Windows only. Titles see it when the monitor showing ### Shader cache -Recompiled shaders and the Vulkan pipeline cache are kept in a `shader_cache/` directory beside the output executable, so later runs skip recompilation. Set `ANYPS5_SHADER_CACHE_DIR` to use another directory, or set `ANYPS5_NO_SHADER_CACHE` to any non-empty value other than `0` to disable both caches. Shaders are stored in a subdirectory named after the recompiler's source version; subdirectories of other versions unused for 14 days are removed the first time a run looks up a shader. The pipeline cache is one `vk-pipelines-*.bin` file per GPU and driver version. Entries of the current version are never removed and the directory has no size limit, so a title that compiles many shaders can add several GB within minutes. Deleting the directory is safe: it is rebuilt on the next run. +Recompiled shaders and the Vulkan pipeline cache are kept in a `shader_cache/` directory beside the output executable, so later runs skip recompilation. Set `ANYPS5_SHADER_CACHE_DIR` to use another directory, or set `ANYPS5_NO_SHADER_CACHE` to any non-empty value other than `0` to disable both caches. Shaders are stored in a subdirectory named after the recompiler's source version; subdirectories of other versions unused for 14 days are removed the first time a run looks up a shader. The pipeline cache is one `vk-pipelines-*.bin` file per GPU and driver version in the same subdirectory, so it goes with the shaders it was built from. Entries of the current version are never removed and the directory has no size limit, so a title that compiles many shaders can add several GB within minutes. Deleting the directory is safe: it is rebuilt on the next run. ### Pipeline journal From ca421072df5fad1514e8b1b403bb6670bc5019c8 Mon Sep 17 00:00:00 2001 From: XairRrR Date: Sat, 17 Oct 2026 01:13:45 +0200 Subject: [PATCH 049/269] test(agc): find the pipeline cache file in the current version's directory The test helper took the only entry of the shader cache root as the version directory. It now opens the directory named after the current shader cache source version, requires exactly one pipeline cache file there, and only requires the root to hold no files, so other version directories do not fail it. AI-assisted: yes --- core/libs/prx/libSceAgcDriver/tests/Recorder.cpp | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index e63edeaa7a..8cf874db83 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -39,6 +39,7 @@ #include "VertexRangeProducer_frag_spv.h" #include "prx/libSceAgcDriver/Graphics/include/ColorTargetTransfer.hpp" #include "BdaAbi.hpp" +#include "ShaderCacheSourceVersion.hpp" #include #ifdef _WIN32 #ifndef NOMINMAX @@ -57,6 +58,7 @@ #include #include #include +#include #include #include #include @@ -8308,13 +8310,14 @@ int pipelineCacheFileTests(const Device& device) { return bytes(cache) == before; }; const auto cacheFile = [&] { + for (const auto& entry : std::filesystem::directory_iterator(directory)) Require(entry.is_directory(), "a pipeline cache file lives in the shader cache root"); + char version[17]; + std::snprintf(version, sizeof(version), "%016llx", static_cast(ShaderRecompiler::ShaderDiskCache::Generated::SourceVersion)); + const auto versionDirectory = directory / version; + Require(std::filesystem::is_directory(versionDirectory), "the current shader cache version has no directory"); std::vector found; - for (const auto& entry : std::filesystem::directory_iterator(directory)) found.push_back(entry.path()); - Require(found.size() == 1 && std::filesystem::is_directory(found.front()), "the pipeline cache file does not live in the shader cache version's directory alone"); - const auto version = found.front(); - found.clear(); - for (const auto& entry : std::filesystem::directory_iterator(version)) found.push_back(entry.path()); - Require(found.size() == 1 && found.front().filename().string().starts_with("vk-pipelines-"), "the pipeline cache directory does not hold exactly one cache file"); + for (const auto& entry : std::filesystem::directory_iterator(versionDirectory)) found.push_back(entry.path()); + Require(found.size() == 1 && found.front().filename().string().starts_with("vk-pipelines-"), "the current shader cache version's directory does not hold exactly one pipeline cache file"); return found.front(); }; const auto contents = [](const std::filesystem::path& path) { From 42f4382bcf05443e2cb5250ba4696da79912d7ed Mon Sep 17 00:00:00 2001 From: milescoviello Date: Mon, 19 Oct 2026 02:27:00 +0000 Subject: [PATCH 050/269] fix(libScePlayGo): list chunks 0 to 999 when only a scenario file is present A dump with sce_sys/playgo-scenario.json and no chunk table already answers every chunk id as installed, but scePlayGoGetChunkId listed only chunk 0. Ghost of Tsushima (PPSA02225) builds its set of installed chunks from that list and never reads its English audio, two of its music archives or its opening movie. scePlayGoGetChunkId now lists ids 0 to 999, the room that title passes. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libScePlayGo/Export.cpp | 2 ++ core/libs/tests/GuestPlayGo.cpp | 11 ++++++++++- docs/dev/debt/libScePlayGo.md | 2 +- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libScePlayGo/Export.cpp b/core/libs/prx/libScePlayGo/Export.cpp index 2bc5bfc67c..289ea2f40c 100644 --- a/core/libs/prx/libScePlayGo/Export.cpp +++ b/core/libs/prx/libScePlayGo/Export.cpp @@ -22,6 +22,7 @@ static constexpr int8_t PLAYGO_LOCUS_LOCAL_FAST = 3; static constexpr uint64_t PLAYGO_LANGUAGE_MASK_ALL = ~0ull; static constexpr uint64_t PLAYGO_OPTIONAL_CHUNK_MASK_ALL = ~0ull; static constexpr int32_t PLAYGO_INSTALL_SPEED_FULL = 2; +static constexpr uint32_t PLAYGO_SCENARIO_CHUNK_COUNT = 1000; static int32_t g_installSpeed = PLAYGO_INSTALL_SPEED_FULL; @@ -68,6 +69,7 @@ static const ChunkTable& Chunks() { if (ReadPackageChunks(table)) return table; table.scenario = std::ifstream(ResolvePath_nid_no_patch("/app0/sce_sys/playgo-scenario.json")).good(); if (!table.scenario) RejectUnreadablePlayGoFiles(); + else for (uint32_t id = 1; id < PLAYGO_SCENARIO_CHUNK_COUNT; ++id) table.ids.insert(static_cast(id)); return table; } table.defined = true; diff --git a/core/libs/tests/GuestPlayGo.cpp b/core/libs/tests/GuestPlayGo.cpp index 0d24636d4b..4dd0d6a5f7 100644 --- a/core/libs/tests/GuestPlayGo.cpp +++ b/core/libs/tests/GuestPlayGo.cpp @@ -10,6 +10,7 @@ #include #include #include +#include extern "C" { int APS5_VABI scePlayGoInitialize(const PlayGoInitParams*); @@ -71,7 +72,15 @@ static int CheckScenarioWithoutChunkTable(const char* root) { const PlayGoToDo todo[2] = {{0, 3, 0}, {0xFFFF, 3, 0}}; Require(scePlayGoSetToDoList(handle, todo, 2) == 0); std::uint32_t chunkCount = 0; - Require(scePlayGoGetChunkId(handle, nullptr, 0, &chunkCount) == 0 && chunkCount == 1); + Require(scePlayGoGetChunkId(handle, nullptr, 0, &chunkCount) == 0 && chunkCount == 1000); + std::vector ids(chunkCount + 1, 0xFFFF); + Require(scePlayGoGetChunkId(handle, ids.data(), static_cast(ids.size()), &chunkCount) == 0 && chunkCount == 1000); + for (std::uint32_t index = 0; index < chunkCount; ++index) Require(ids[index] == index); + std::vector listedLoci(chunkCount, 0); + Require(scePlayGoGetLocus(handle, ids.data(), chunkCount, listedLoci.data()) == 0); + for (const auto locus : listedLoci) Require(locus == 3); + std::uint16_t firstTwo[2] = {0xFFFF, 0xFFFF}; + Require(scePlayGoGetChunkId(handle, firstTwo, 2, &chunkCount) == 0 && chunkCount == 2 && firstTwo[0] == 0 && firstTwo[1] == 1); Require(scePlayGoClose(handle) == 0); return 0; } diff --git a/docs/dev/debt/libScePlayGo.md b/docs/dev/debt/libScePlayGo.md index ef94e4f939..749b0204ba 100644 --- a/docs/dev/debt/libScePlayGo.md +++ b/docs/dev/debt/libScePlayGo.md @@ -2,5 +2,5 @@ ### Unknown function info -- [libScePlayGo](../../../core/libs/prx/libScePlayGo/Export.cpp) - the chunk set comes from `playgo-chunkdefs.xml` (every listed chunk plus chunks 0 through the default chunk), otherwise from `sce_sys/playgo-chunk.dat`, whose `plgx` header is read as holding the chunk count as a 16-bit value at offset 0x0A and ids 0 to count - 1 are taken as valid. The field was identified on four dumps, where exactly that many 0x20-byte entries follow from offset 0x100 (RIDE 4 PPSA01599: 35, Persona 5 Royal PPSA05110: 25, LEGO Horizon Adventures PPSA14632 and Kena PPSA01802: 1); the rest of the format is not decoded and ids are assumed to be contiguous. Every listed chunk is reported as installed (`LOCAL_FAST`). Without either file, a package with `sce_sys/playgo-scenario.json` (PPSA01748, whose dump lists no chunks) accepts every chunk id as installed while `scePlayGoGetChunkId` lists only chunk 0. A package with no `playgo-*` file in `sce_sys` (PPSA03001, PPSA03490 and PPSA01285 here) accepts chunk 0; querying any other id throws with a message naming the missing `playgo-chunk.dat` and `playgo-scenario.json`, since the incomplete dump cannot establish whether that id exists; `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) - 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 and Ghost of Tsushima PPSA02225, whose dumps list no chunks) accepts every chunk id as installed, and `scePlayGoGetChunkId` lists ids 0 to 999. PPSA02225 builds its set of installed chunks from that list, and with only chunk 0 listed it never reads its English audio, two of its music archives or its opening movie. 1000 is the room that title passes; 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. From 7e8aa10ae676f9681eb747bedc51ec837e55e9b0 Mon Sep 17 00:00:00 2001 From: To3akaRin <131595336+To3akaRin@users.noreply.github.com> Date: Mon, 19 Oct 2026 10:40:01 +0800 Subject: [PATCH 051/269] fix(tools): strip dependency options before syntax checks --- docs/dev/BUILD.md | 10 +++++++++ tools/check_compile.py | 4 ++-- tools/tests/test_check_compile.py | 34 +++++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index 03372da27e..3c290710dd 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -84,6 +84,16 @@ SDL and FreeType settings forced by the root `CMakeLists.txt` cannot be overridd With `ANYPS5_USE_SYSTEM_DEPS=ON` those libraries come from the system (on Debian and Ubuntu: `libsdl2-dev`, `libfreetype-dev`, `libvulkan-dev`, `spirv-headers`, `glslang-dev`, `glslang-tools` and `spirv-tools`). A missing package, Vulkan-Headers older than 1.4.310 (the agc driver uses `VK_KHR_maintenance8`), or an SDL2 without the HIDAPI joystick driver (sdl2-compat over SDL3, or a build with HIDAPI disabled, which breaks the pad and disk-audio tests), fails the configure with an error and never falls back to the submodule. The forced SDL and FreeType settings from the root `CMakeLists.txt` do not apply with the flag ON: the system SDL2 comes with whatever its distribution enabled. The prx libraries are shared objects, so SDL2 is linked from the system as a shared library; the default build keeps its own static SDL2. `ffmpeg-core`, `libjpeg-turbo`, `stb`, `VulkanMemoryAllocator` and `LibAtrac9` always stay bundled and still need `git submodule update --init`. +## 快速语法检查 + +配置构建目录后,可使用 `tools/check_compile.py` 检查指定源文件。工具从 `compile_commands.json` 或 Ninja 读取编译命令,保留编译选项,并去掉对象文件及依赖文件输出参数。`-MF`、`-MT`、`-MQ` 和 `-MJ` 支持分离及连写形式,避免在 `-Werror` 下因无效的依赖输出参数误报失败。 + +```sh +python3 tools/check_compile.py --build build-relinker core/relinker/main.cpp +``` + +真实编译错误仍返回失败;语法检查不替代完整构建和测试。 + ## Write watch tests The macOS write watch can be built and tested separately, without submodules or the prx libraries: diff --git a/tools/check_compile.py b/tools/check_compile.py index 47076b046b..ce10c3856a 100644 --- a/tools/check_compile.py +++ b/tools/check_compile.py @@ -10,7 +10,7 @@ from pathlib import Path CPP = (".cpp", ".cc", ".cxx", ".c") DROP = ("-c", "-MD", "-MMD", "-MP") -DROP_WITH_VALUE = ("-o", "-MT", "-MF", "-MJ") +DROP_WITH_VALUE = ("-o", "-MT", "-MF", "-MQ", "-MJ") def capture(command, cwd=None): @@ -111,7 +111,7 @@ def syntax_arguments(arguments, directory, source): if argument in DROP_WITH_VALUE: index += 2 continue - if argument in DROP or argument.startswith("-MJ"): + if argument in DROP or argument.startswith(("-MT", "-MF", "-MQ", "-MJ")): index += 1 continue if not argument.startswith("-"): diff --git a/tools/tests/test_check_compile.py b/tools/tests/test_check_compile.py index 3b83764f0a..1a31f04df2 100644 --- a/tools/tests/test_check_compile.py +++ b/tools/tests/test_check_compile.py @@ -14,6 +14,40 @@ TOOL = Path(__file__).resolve().parents[1] / "check_compile.py" class CheckCompileTests(unittest.TestCase): + def test_dependency_options_do_not_make_syntax_checks_fail_or_rewrite_outputs(self): + compiler = shutil.which("clang") + if compiler is None: + self.skipTest("Clang is required to check unused dependency options with -Werror") + source = self.source("unit.c") + object_file = self.build / "unit.o" + dependency_file = self.build / "unit.d" + for option in ("-MF", "-MT", "-MQ"): + for joined in (False, True): + with self.subTest(option=option, joined=joined): + source.write_text("int x;\n", encoding="utf-8") + value = str(dependency_file) if option == "-MF" else "target with spaces.o" + flags = [option + value] if joined else [option, value] + if option != "-MF": + flags += ["-MF", str(dependency_file)] + arguments = [compiler, "-Werror", "-MMD", *flags, + "-c", str(source), "-o", str(object_file)] + original = subprocess.run(arguments, cwd=self.build, capture_output=True, text=True) + self.assertEqual(original.returncode, 0, original.stderr) + outputs = {path: path.read_bytes() for path in (object_file, dependency_file)} + self.manifest([{"directory": str(self.build), "file": str(source), + "arguments": arguments}]) + result = self.tool("unit.c") + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("1 translation units compile", result.stdout) + for path, content in outputs.items(): + self.assertEqual(path.read_bytes(), content) + source.write_text("#error syntax_check_must_fail\n", encoding="utf-8") + result = self.tool("unit.c") + self.assertEqual(result.returncode, 1, result.stdout) + self.assertIn("syntax_check_must_fail", result.stderr) + for path, content in outputs.items(): + self.assertEqual(path.read_bytes(), content) + def test_ninja_response_file_is_expanded_before_syntax_rewriting(self): compiler = shutil.which("clang") or shutil.which("gcc") if compiler is None or shutil.which("ninja") is None: From 879b9e65c5765b15c754bf065d9e2eec6d7f3ca7 Mon Sep 17 00:00:00 2001 From: Jota <139649225+jotitadev@users.noreply.github.com> Date: Mon, 19 Oct 2026 02:44:28 +0000 Subject: [PATCH 052/269] fix(agc): keep each descriptor set slot in one of a split pipeline library pair With independent sets, a pipeline linked from a pre-rasterization library and a fragment shader library must not leave the same set slot VK_NULL_HANDLE in both libraries' layouts (VUID-VkGraphicsPipelineCreateInfo-pLibraries-06681). Every pipeline with a fragment stage is split when the device has graphics pipeline libraries, and each part's layout held only the sets its own stages bind, so a fragment shader that binds nothing in set 1, or a draw where no stage binds set 0, left that slot null in both parts. A slot neither stage binds now goes to the part that owns it: set 0 to the pre-rasterization layout, set 1 to the fragment layout. --- core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index 5bfd9b37bf..219be7ebf7 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -217,6 +217,11 @@ bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& } } } + if (resources.split) { + const auto unused = 3u & ~(keys.preRasterizationSets | keys.fragmentSets); + keys.preRasterizationSets |= unused & 1u; + keys.fragmentSets |= unused & 2u; + } for (const auto& binding : input.bindings) { appendKey(keys.vertexInput, binding.binding); appendKey(keys.vertexInput, binding.stride); From 21325fba43416d17d01012576845ba3a1a911610 Mon Sep 17 00:00:00 2001 From: Jota <139649225+jotitadev@users.noreply.github.com> Date: Mon, 19 Oct 2026 04:51:37 +0000 Subject: [PATCH 053/269] test(relinker): give the desktop entry test its own temporary directory --- core/relinker/elfpatcher/tests/LinuxDesktopEntryTests.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/relinker/elfpatcher/tests/LinuxDesktopEntryTests.cpp b/core/relinker/elfpatcher/tests/LinuxDesktopEntryTests.cpp index bfbaf19eb6..0c473271cd 100644 --- a/core/relinker/elfpatcher/tests/LinuxDesktopEntryTests.cpp +++ b/core/relinker/elfpatcher/tests/LinuxDesktopEntryTests.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -131,7 +132,8 @@ void run(const std::filesystem::path& root) { } int main() { - const auto root = std::filesystem::temp_directory_path() / "anyps5-desktop-entry-tests"; + const auto root = std::filesystem::temp_directory_path() / + ("anyps5-desktop-entry-tests-" + std::to_string(std::random_device{}())); std::filesystem::remove_all(root); try { run(root); From 31483262cfeca5636d84530046e576988947dab6 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Mon, 19 Oct 2026 04:52:44 +0000 Subject: [PATCH 054/269] fix(libc): avoid coarse Windows C++ condition waits --- core/libs/prx/libc/src/Threads.cpp | 76 ++++++++++++++++++++++- core/libs/tests/GuestLibcCndTimedWait.cpp | 54 ++++++++++++++++ 2 files changed, 128 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libc/src/Threads.cpp b/core/libs/prx/libc/src/Threads.cpp index 167360e72c..c1e056f0fd 100644 --- a/core/libs/prx/libc/src/Threads.cpp +++ b/core/libs/prx/libc/src/Threads.cpp @@ -14,6 +14,12 @@ #include #include #include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif namespace { @@ -54,19 +60,68 @@ constexpr int mutexRecursive = 0x100; constexpr std::uint64_t mutexTag = 0x5854'4D5F'4D54'5844ull; constexpr std::uint64_t conditionTag = 0x5854'4E43'5F44'4E43ull; +#ifdef _WIN32 +struct DinkumwareNativeMutex { + CRITICAL_SECTION value; + + DinkumwareNativeMutex() { + if (!InitializeCriticalSectionAndSpinCount(&value, 0)) + throw std::system_error(static_cast(GetLastError()), std::system_category(), "_Mtx_init: initializing critical section failed"); + } + + ~DinkumwareNativeMutex() { DeleteCriticalSection(&value); } + void lock() { EnterCriticalSection(&value); } + void unlock() { LeaveCriticalSection(&value); } + bool try_lock() { return TryEnterCriticalSection(&value) != FALSE; } +}; + +struct DinkumwareNativeCondition { + CONDITION_VARIABLE value = CONDITION_VARIABLE_INIT; +}; +#else +using DinkumwareNativeMutex = std::mutex; +using DinkumwareNativeCondition = std::condition_variable; +#endif + struct DinkumwareMutex { std::uint64_t tag = mutexTag; int type = 0; int count = 0; std::atomic owner{}; - std::mutex native; + DinkumwareNativeMutex native; }; struct DinkumwareCondition { std::uint64_t tag = conditionTag; - std::condition_variable native; + DinkumwareNativeCondition native; }; +#ifdef _WIN32 +DWORD WaitMillisecondsUntil(std::chrono::system_clock::time_point deadline) { + const auto now = std::chrono::system_clock::now(); + if (now >= deadline) + return 0; + const auto remaining = std::chrono::ceil(deadline - now).count(); + constexpr auto largest = static_cast(std::numeric_limits::max() - 1U); + return static_cast(std::min(remaining, largest)); +} + +bool WaitUntil(DinkumwareCondition* condition, DinkumwareMutex* mutex, std::chrono::system_clock::time_point deadline) { + for (;;) { + const DWORD milliseconds = WaitMillisecondsUntil(deadline); + if (milliseconds == 0) + return false; + if (SleepConditionVariableCS(&condition->native.value, &mutex->native.value, milliseconds) != FALSE) + return true; + const DWORD error = GetLastError(); + if (error != ERROR_TIMEOUT) + throw std::system_error(static_cast(error), std::system_category(), "_Cnd_timedwait: condition wait failed"); + if (std::chrono::system_clock::now() >= deadline) + return false; + } +} +#endif + DinkumwareMutex* ResolveMutex(void** handle, const char* function) { if (!handle) throw std::invalid_argument(std::string(function) + ": null mutex handle pointer"); @@ -213,7 +268,12 @@ int APS5_VABI _Cnd_wait_nid_postfix(void** conditionHandle, void** mutexHandle) mutex->owner.store(std::thread::id{}, std::memory_order_release); std::unique_lock lock(mutex->native, std::adopt_lock); try { +#ifdef _WIN32 + if (SleepConditionVariableCS(&condition->native.value, &mutex->native.value, INFINITE) == FALSE) + throw std::system_error(static_cast(GetLastError()), std::system_category(), "_Cnd_wait: condition wait failed"); +#else condition->native.wait(lock); +#endif } catch (const std::system_error&) { lock.release(); mutex->owner.store(std::this_thread::get_id(), std::memory_order_release); @@ -252,7 +312,11 @@ int APS5_VABI _Cnd_timedwait_nid_postfix(void** conditionHandle, void** mutexHan const auto deadlineTime = std::chrono::system_clock::time_point( std::chrono::seconds(deadline.tv_sec) + std::chrono::ceil(std::chrono::nanoseconds(deadline.tv_nsec))); +#ifdef _WIN32 + timeout = !WaitUntil(condition, mutex, deadlineTime); +#else timeout = (condition->native.wait_until(lock, deadlineTime) == std::cv_status::timeout); +#endif } catch (const std::system_error&) { lock.release(); mutex->owner.store(std::this_thread::get_id(), std::memory_order_release); @@ -266,12 +330,20 @@ int APS5_VABI _Cnd_timedwait_nid_postfix(void** conditionHandle, void** mutexHan } int APS5_VABI _Cnd_broadcast_nid_postfix(void** handle) { +#ifdef _WIN32 + WakeAllConditionVariable(&ResolveCondition(handle, "_Cnd_broadcast")->native.value); +#else ResolveCondition(handle, "_Cnd_broadcast")->native.notify_all(); +#endif return threadSuccess; } int SignalCondition_nid_no_patch(void** handle) { +#ifdef _WIN32 + WakeConditionVariable(&ResolveCondition(handle, "_Cnd_signal")->native.value); +#else ResolveCondition(handle, "_Cnd_signal")->native.notify_one(); +#endif return threadSuccess; } diff --git a/core/libs/tests/GuestLibcCndTimedWait.cpp b/core/libs/tests/GuestLibcCndTimedWait.cpp index 467d2e6ed5..e85144f158 100644 --- a/core/libs/tests/GuestLibcCndTimedWait.cpp +++ b/core/libs/tests/GuestLibcCndTimedWait.cpp @@ -1,10 +1,18 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include #include #include #include #include +#include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif struct Xtime { std::int64_t sec; @@ -18,12 +26,30 @@ int APS5_VABI _Mtx_lock_nid_postfix(void** handle); int APS5_VABI _Mtx_unlock_nid_postfix(void** handle); int APS5_VABI _Cnd_init_nid_postfix(void** handle); void APS5_VABI _Cnd_destroy_nid_postfix(void** handle); +int APS5_VABI _Cnd_wait_nid_postfix(void** condition, void** mutex); int APS5_VABI _Cnd_broadcast_nid_postfix(void** handle); int APS5_VABI _Cnd_timedwait_nid_postfix(void** condition, void** mutex, const Xtime* target); } static void Require(bool value) { if (!value) std::abort(); } +#ifdef _WIN32 +struct TimerResolution { + using SetTimerResolution = LONG(WINAPI*)(ULONG, BOOLEAN, PULONG); + SetTimerResolution set = nullptr; + ULONG current = 0; + + TimerResolution() { + const HMODULE ntdll = GetModuleHandleW(L"ntdll.dll"); + if (ntdll) + set = reinterpret_cast(GetProcAddress(ntdll, "NtSetTimerResolution")); + Require(set != nullptr && set(5000, TRUE, ¤t) >= 0); + } + + ~TimerResolution() { set(5000, FALSE, ¤t); } +}; +#endif + template static void RequireThrows(TCall call) { try { @@ -72,6 +98,22 @@ int main() { Require(result == success); } +#ifdef _WIN32 + TimerResolution timerResolution; + std::vector shortWaits; + for (int i = 0; i < 31; ++i) { + const auto start = std::chrono::steady_clock::now(); + const auto deadline = std::chrono::system_clock::now() + std::chrono::milliseconds(1); + const auto seconds = std::chrono::duration_cast(deadline.time_since_epoch()).count(); + const auto nanoseconds = std::chrono::duration_cast(deadline.time_since_epoch()).count() % 1000000000; + const Xtime shortTimeout{seconds, nanoseconds}; + Require(_Cnd_timedwait_nid_postfix(&condition, &mutex, &shortTimeout) == timedout); + shortWaits.push_back(std::chrono::steady_clock::now() - start); + } + std::sort(shortWaits.begin(), shortWaits.end()); + Require(shortWaits[shortWaits.size() / 2] < std::chrono::milliseconds(12)); +#endif + const auto unnormalizedDeadline = std::chrono::system_clock::now() + std::chrono::milliseconds(20); const auto unnormalizedSec = std::chrono::duration_cast(unnormalizedDeadline.time_since_epoch()).count(); const auto unnormalizedNsec = std::chrono::duration_cast(unnormalizedDeadline.time_since_epoch()).count() % 1000000000; @@ -99,6 +141,18 @@ int main() { } signaler.join(); + ready = false; + std::thread untimedSignaler([&] { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + Require(_Mtx_lock_nid_postfix(&mutex) == success); + ready = true; + Require(_Cnd_broadcast_nid_postfix(&condition) == success); + Require(_Mtx_unlock_nid_postfix(&mutex) == success); + }); + Require(_Cnd_wait_nid_postfix(&condition, &mutex) == success); + Require(ready); + untimedSignaler.join(); + constexpr std::int64_t farSeconds = 10000000000; const Xtime farTime{farSeconds, 999999999}; constexpr auto clockSeconds = std::chrono::duration_cast(std::chrono::system_clock::duration::max()).count(); From 8f93589389f727f6dbdec8b36e19339330ca51ed Mon Sep 17 00:00:00 2001 From: ionut27001 <194206737+ionut27001@users.noreply.github.com> Date: Mon, 19 Oct 2026 07:48:35 +0300 Subject: [PATCH 055/269] test(libkernel): pick a foreign process group guest_kill cannot own guest_kill requires killpg(1000, ...) to fail with ESRCH, but getpid returns the host process id. When Windows gives the test process id 1000, group 1000 is the caller's own, killpg succeeds as the test itself requires for killpg(self, 0), and Require aborts with 0xc0000409. Use self + 1000, which is never the caller's group. --- core/libs/tests/GuestKill.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/tests/GuestKill.cpp b/core/libs/tests/GuestKill.cpp index 7a1f380d3c..7b75694ea5 100644 --- a/core/libs/tests/GuestKill.cpp +++ b/core/libs/tests/GuestKill.cpp @@ -32,7 +32,7 @@ int main() { Fails(target, 0, 3); Fails(target, 15, 3); } - for (int target : {self + 1, 1000}) { + for (int target : {self + 1, self + 1000}) { KillpgFails(target, 0, 3); KillpgFails(target, 15, 3); } From ca4303a155407aefa2baa705cda248931e8bc8ac Mon Sep 17 00:00:00 2001 From: eth-j17 Date: Fri, 16 Oct 2026 01:16:43 -0500 Subject: [PATCH 056/269] feat(trace): record host, title and per-frame benchmark data The summary line names the CPU, its thread count, physical memory and, with param.json, the title ID, name and content version. APS5_TRACE_BENCH=1 with APS5_TRACE_SUMMARY_EVERY rewrites aps5-bench--.json.gz at each interval with the time between every two presents, so reports can be recomputed and checked from raw frame times. param.json's contentVersion is read when present. Co-Authored-By: Claude Opus 5.5 --- .../Execution/include/PerformanceTrace.hpp | 104 +++++++++++++++++- .../Execution/src/PerformanceTrace.cpp | 97 ++++++++++++++++ .../tests/PerformanceTrace.cpp | 62 +++++++++++ .../AppMetadata/include/AppMetadata.hpp | 6 + .../AppMetadata/include/ParamJsonParser.hpp | 1 + .../libkernel/AppMetadata/src/AppMetadata.cpp | 13 +++ .../AppMetadata/src/ParamJsonParser.cpp | 1 + core/libs/tests/ParamJsonParser.cpp | 9 ++ docs/dev/BUILD.md | 4 +- docs/dev/ENVIRONMENT.md | 1 + docs/user/USAGE.md | 4 + 11 files changed, 300 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp index 5c7fb7d2a6..a3aa660629 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -267,6 +268,18 @@ private: Clock::duration folded{}; }; +struct TraceHost { + std::string cpu; + std::uint32_t threads = 0; + std::uint64_t memoryMiB = 0; +}; + +struct TraceTitle { + std::string id; + std::string name; + std::string contentVersion; +}; + struct TraceSummary { std::string commit; std::optional dirty; @@ -281,6 +294,30 @@ struct TraceSummary { double gpuBusyMs = 0; std::string trace; std::optional> interval; + std::optional host; + std::optional title; +}; + +struct TraceBenchInterval { + double startS = 0; + double endS = 0; + std::uint64_t firstPresent = 0; + std::uint32_t width = 0; + std::uint32_t height = 0; + double gpuBusyMs = 0; + std::vector frameUs; +}; + +class TraceBench { +public: + static constexpr std::size_t MaximumIntervals = 240; + + static bool Parse(const char* bench, bool summaryEvery) { + if (bench == nullptr) return false; + if (std::string_view(bench) != "1") throw std::invalid_argument("APS5_TRACE_BENCH=" + std::string(bench) + " is not 1"); + if (!summaryEvery) throw std::invalid_argument("APS5_TRACE_BENCH=1 needs APS5_TRACE_SUMMARY_EVERY"); + return true; + } }; struct TraceGpuRange { @@ -429,6 +466,7 @@ public: AppendString(output, summary.driver); output += ",\"os\":"; AppendString(output, summary.os); + AppendMachine(output, summary); output += ",\"resolution\":"; AppendString(output, std::to_string(summary.width) + "x" + std::to_string(summary.height)); output += ",\"window\":{\"spec\":"; @@ -502,7 +540,71 @@ public: return output; } + static std::vector FrameMicroseconds(std::span presents) { + std::vector frames; + for (std::size_t index = 1; index < presents.size(); ++index) { + const auto elapsed = std::chrono::duration_cast(presents[index] - presents[index - 1]).count(); + frames.push_back(static_cast(std::clamp(elapsed, 0, std::numeric_limits::max()))); + } + return frames; + } + + static std::string Bench(const TraceSummary& summary, std::span intervals, std::uint64_t intervalsDropped) { + std::string output = "{\"format\":\"aps5-bench/1\",\"commit\":"; + AppendString(output, summary.commit); + output += ",\"dirty\":"; + output += summary.dirty.has_value() ? (*summary.dirty ? "true" : "false") : "null"; + output += ",\"env\":{"; + for (std::size_t index = 0; index < summary.environment.size(); ++index) { + if (index != 0) output += ','; + AppendString(output, summary.environment[index].first); + output += ':'; + AppendString(output, summary.environment[index].second); + } + output += "},\"gpu\":"; + AppendString(output, summary.gpu); + output += ",\"driver\":"; + AppendString(output, summary.driver); + output += ",\"os\":"; + AppendString(output, summary.os); + AppendMachine(output, summary); + output += ",\"intervals_dropped\":" + std::to_string(intervalsDropped) + ",\"intervals\":["; + for (std::size_t index = 0; index < intervals.size(); ++index) { + const auto& interval = intervals[index]; + if (index != 0) output += ','; + output += "\n{\"start_s\":" + number(interval.startS) + ",\"end_s\":" + number(interval.endS); + output += ",\"first_present\":" + std::to_string(interval.firstPresent); + output += ",\"resolution\":"; + AppendString(output, std::to_string(interval.width) + "x" + std::to_string(interval.height)); + output += ",\"gpu_busy_ms\":" + number(interval.gpuBusyMs) + ",\"frame_us\":["; + for (std::size_t frame = 0; frame < interval.frameUs.size(); ++frame) { + if (frame != 0) output += ','; + output += std::to_string(interval.frameUs[frame]); + } + output += "]}"; + } + output += "\n]}\n"; + return output; + } + private: + static void AppendMachine(std::string& output, const TraceSummary& summary) { + if (summary.host) { + output += ",\"cpu\":"; + AppendString(output, summary.host->cpu); + output += ",\"cpu_threads\":" + std::to_string(summary.host->threads); + output += ",\"memory_mib\":" + std::to_string(summary.host->memoryMiB); + } + if (summary.title) { + output += ",\"title_id\":"; + AppendString(output, summary.title->id); + output += ",\"title\":"; + AppendString(output, summary.title->name); + output += ",\"content_version\":"; + AppendString(output, summary.title->contentVersion); + } + } + static double microseconds(Clock::duration elapsed) { return std::chrono::duration(elapsed).count(); } @@ -521,7 +623,7 @@ private: class PerformanceTrace { public: static bool Configured() { - static const bool configured = std::getenv("APS5_TRACE_FRAMES") != nullptr || std::getenv("APS5_TRACE_SECONDS") != nullptr || std::getenv("APS5_TRACE_SUMMARY_EVERY") != nullptr; + static const bool configured = std::getenv("APS5_TRACE_FRAMES") != nullptr || std::getenv("APS5_TRACE_SECONDS") != nullptr || std::getenv("APS5_TRACE_SUMMARY_EVERY") != nullptr || std::getenv("APS5_TRACE_BENCH") != nullptr; return configured; } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTrace.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTrace.cpp index c913b43155..8bd62468ca 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTrace.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTrace.cpp @@ -3,11 +3,15 @@ #include "prx/libSceAgcDriver/Execution/include/TraceFile.hpp" #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" #include "prx/libSceAgcDriver/TraceRevision.hpp" +#include "prx/libkernel/AppMetadata/include/AppMetadata.hpp" +#include #include +#include #include #include #include #include +#include #ifdef _WIN32 #include #else @@ -15,6 +19,11 @@ #include #include #endif +#if defined(_MSC_VER) && !defined(__clang__) +#include +#elif defined(__x86_64__) || defined(__i386__) +#include +#endif #ifndef _WIN32 extern char** environ; @@ -46,6 +55,9 @@ struct Session { double gpuBusyAtOpen = 0; std::optional gpuClock; std::vector presents; + bool bench = false; + std::vector benchIntervals; + std::uint64_t benchDropped = 0; }; Session& session() { @@ -63,6 +75,11 @@ std::unique_ptr& summaryWriter() { return output; } +std::unique_ptr& benchWriter() { + static std::unique_ptr output; + return output; +} + std::filesystem::path traceDirectory() { const char* directory = std::getenv("APS5_TRACE_DIR"); return std::filesystem::path(directory != nullptr ? directory : "."); @@ -91,6 +108,60 @@ std::string operatingSystem() { #endif } +std::string processorName() { + std::array words{}; +#if defined(_MSC_VER) && !defined(__clang__) + int registers[4]{}; + __cpuid(registers, static_cast(0x80000000u)); + if (static_cast(registers[0]) < 0x80000004u) throw std::runtime_error("the CPU reports no brand string"); + for (unsigned leaf = 0; leaf < 3; ++leaf) { + __cpuid(registers, static_cast(0x80000002u + leaf)); + std::memcpy(words.data() + leaf * 4, registers, sizeof registers); + } +#elif defined(__x86_64__) || defined(__i386__) + if (__get_cpuid_max(0x80000000u, nullptr) < 0x80000004u) throw std::runtime_error("the CPU reports no brand string"); + for (unsigned leaf = 0; leaf < 3; ++leaf) { + unsigned eax = 0, ebx = 0, ecx = 0, edx = 0; + if (__get_cpuid(0x80000002u + leaf, &eax, &ebx, &ecx, &edx) == 0) throw std::runtime_error("cpuid brand string leaf failed"); + words[leaf * 4] = eax; + words[leaf * 4 + 1] = ebx; + words[leaf * 4 + 2] = ecx; + words[leaf * 4 + 3] = edx; + } +#else + throw std::runtime_error("reading the CPU name needs an x86 host"); +#endif + std::string name(reinterpret_cast(words.data()), sizeof words); + name.resize(std::strlen(name.c_str())); + const auto first = name.find_first_not_of(' '); + if (first == std::string::npos) throw std::runtime_error("the CPU brand string is empty"); + return name.substr(first, name.find_last_not_of(' ') - first + 1); +} + +std::uint64_t memoryMiB() { +#ifdef _WIN32 + MEMORYSTATUSEX status{}; + status.dwLength = sizeof(status); + if (::GlobalMemoryStatusEx(&status) == 0) throw std::system_error(static_cast(::GetLastError()), std::system_category(), "GlobalMemoryStatusEx"); + return status.ullTotalPhys / (1024 * 1024); +#else + const long pages = ::sysconf(_SC_PHYS_PAGES); + const long size = ::sysconf(_SC_PAGE_SIZE); + if (pages <= 0 || size <= 0) throw std::system_error(errno, std::generic_category(), "sysconf physical memory"); + return static_cast(pages) * static_cast(size) / (1024 * 1024); +#endif +} + +const TraceHost& host() { + static const TraceHost value{processorName(), std::thread::hardware_concurrency(), memoryMiB()}; + return value; +} + +std::optional title() { + if (!HasAppParamJson_nid_no_patch()) return std::nullopt; + return TraceTitle{GetAppTitleId_nid_postfix().value, GetAppTitle_nid_postfix().value, GetAppContentVersion_nid_no_patch().value}; +} + std::vector> environment() { #ifdef _WIN32 char** entries = _environ; @@ -120,6 +191,9 @@ TraceSummary identity(std::uint32_t width, std::uint32_t height, const VulkanDev summary.os = operatingSystem(); summary.width = width; summary.height = height; + summary.host = host(); + static const auto running = title(); + summary.title = running; return summary; } @@ -185,6 +259,27 @@ void PerformanceTrace::FlushInterval(Clock::time_point now, std::uint32_t width, summary.gpuBusyMs = VulkanDevice::PresentCounts().gpuBusyMs - state.intervalGpuBusy; summary.interval = std::make_pair(seconds(state.intervalPresents.front() - state.first), seconds(now - state.first)); summaryWriter()->Write(TraceFormat::Summary(summary, snapshots, state.intervalPresents)); + if (!state.bench) return; + state.benchIntervals.push_back({summary.interval->first, summary.interval->second, summary.firstPresent, width, height, summary.gpuBusyMs, TraceFormat::FrameMicroseconds(state.intervalPresents)}); + if (state.benchIntervals.size() > TraceBench::MaximumIntervals) { + state.benchIntervals.erase(state.benchIntervals.begin()); + ++state.benchDropped; + } + if (!benchWriter()) { + const auto name = "aps5-bench-" + summary.commit + "-" + std::to_string(processId()) + ".json.gz"; + benchWriter() = std::make_unique([directory = traceDirectory(), name](std::string_view text) { + try { + std::filesystem::create_directories(directory); + TraceFile::WriteGzip(directory / name, text); + } catch (const std::exception& error) { + std::fprintf(stderr, "[trace] writing %s failed: %s\n", name.c_str(), error.what()); + std::fflush(stderr); + throw; + } + }, std::numeric_limits::max()); + ProfilePrint_nid_no_patch("[trace] benchmark frames go to %s, rewritten every interval\n", name.c_str()); + } + benchWriter()->Write(TraceFormat::Bench(summary, state.benchIntervals, state.benchDropped)); } void PerformanceTrace::Present(std::uint32_t width, std::uint32_t height, const VulkanDevice* device) { @@ -195,6 +290,7 @@ void PerformanceTrace::Present(std::uint32_t width, std::uint32_t height, const if (!state.started) { state.window = TraceWindow::Parse(std::getenv("APS5_TRACE_FRAMES"), std::getenv("APS5_TRACE_SECONDS")); state.interval = TraceInterval::Parse(std::getenv("APS5_TRACE_SUMMARY_EVERY")); + state.bench = TraceBench::Parse(std::getenv("APS5_TRACE_BENCH"), state.interval.has_value()); state.first = now; state.started = true; } @@ -287,6 +383,7 @@ void PerformanceTrace::Present(std::uint32_t width, std::uint32_t height, const void PerformanceTrace::StopOutput() { if (writer()) writer()->Stop(); if (summaryWriter()) summaryWriter()->Stop(); + if (benchWriter()) benchWriter()->Stop(); } } diff --git a/core/libs/prx/libSceAgcDriver/tests/PerformanceTrace.cpp b/core/libs/prx/libSceAgcDriver/tests/PerformanceTrace.cpp index 41824053d6..37e9bc2e5c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/PerformanceTrace.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/PerformanceTrace.cpp @@ -346,6 +346,64 @@ void IntervalDroppedThreads() { Check(line.find("\"queue worker 0x0\":{\"events_lost\":5,\"totals_full\":false}") != std::string::npos && line.find("events_full_after_s") == std::string::npos, "an interval without presents printed a loss time"); } +void SummaryMachineFields() { + const auto origin = Clock::now(); + AgcDriver::TraceSummary summary; + summary.commit = "abc"; + summary.dirty = false; + summary.gpu = "GPU"; + summary.driver = "Driver 1"; + summary.os = "OS 1"; + summary.width = 1920; + summary.height = 1080; + summary.window = "summary_every 30"; + summary.host = AgcDriver::TraceHost{"CPU \"X\"", 16, 32000}; + summary.title = AgcDriver::TraceTitle{"PPSA00000", "Example", "01.002.000"}; + const std::vector presents{origin, origin + milliseconds(16)}; + const auto line = TraceFormat::Summary(summary, {}, presents); + Check(line.find("\"os\":\"OS 1\",\"cpu\":\"CPU \\\"X\\\"\",\"cpu_threads\":16,\"memory_mib\":32000,\"title_id\":\"PPSA00000\",\"title\":\"Example\",\"content_version\":\"01.002.000\",\"resolution\":\"1920x1080\"") != std::string::npos, "the summary line did not name the machine and title"); + summary.title.reset(); + Check(TraceFormat::Summary(summary, {}, presents).find("title_id") == std::string::npos, "a summary without param.json named a title"); +} + +void FrameMicroseconds() { + const auto origin = Clock::now(); + const std::vector presents{origin, origin + microseconds(16667), origin + microseconds(50000), origin + microseconds(50000)}; + Check(TraceFormat::FrameMicroseconds(presents) == std::vector{16667, 33333, 0}, "frame intervals in microseconds changed"); + Check(TraceFormat::FrameMicroseconds({}).empty(), "no presents gave frame intervals"); +} + +void BenchFormat() { + AgcDriver::TraceSummary summary; + summary.commit = "abc"; + summary.environment = {{"APS5_TRACE_BENCH", "1"}}; + summary.gpu = "GPU"; + summary.driver = "Driver 1"; + summary.os = "OS 1"; + summary.host = AgcDriver::TraceHost{"CPU", 8, 16000}; + const std::vector intervals{ + {0, 30.5, 0, 1920, 1080, 120.25, {16667, 16666}}, + {30.5, 61, 1830, 2560, 1440, 98.5, {}}, + }; + const std::string expected = + "{\"format\":\"aps5-bench/1\",\"commit\":\"abc\",\"dirty\":null,\"env\":{\"APS5_TRACE_BENCH\":\"1\"},\"gpu\":\"GPU\",\"driver\":\"Driver 1\",\"os\":\"OS 1\"," + "\"cpu\":\"CPU\",\"cpu_threads\":8,\"memory_mib\":16000,\"intervals_dropped\":3,\"intervals\":[\n" + "{\"start_s\":0.000,\"end_s\":30.500,\"first_present\":0,\"resolution\":\"1920x1080\",\"gpu_busy_ms\":120.250,\"frame_us\":[16667,16666]},\n" + "{\"start_s\":30.500,\"end_s\":61.000,\"first_present\":1830,\"resolution\":\"2560x1440\",\"gpu_busy_ms\":98.500,\"frame_us\":[]}\n]}\n"; + Check(TraceFormat::Bench(summary, intervals, 3) == expected, "benchmark file changed"); +} + +void BenchSwitch() { + Check(!AgcDriver::TraceBench::Parse(nullptr, false), "the benchmark file was enabled without its switch"); + Check(AgcDriver::TraceBench::Parse("1", true), "APS5_TRACE_BENCH=1 was not accepted"); + for (const auto& [text, every] : {std::pair{"1", false}, std::pair{"0", true}, std::pair{"yes", true}, std::pair{"", true}}) { + bool rejected = false; + try { AgcDriver::TraceBench::Parse(text, every); } + catch (const std::invalid_argument&) { rejected = true; } + Check(rejected, "a malformed or unsupported APS5_TRACE_BENCH was accepted"); + } +} + void GzipFile() { std::string text; for (int index = 0; index < 20000; ++index) text += "{\"name\":\"Driver.Packet\",\"ph\":\"X\",\"ts\":" + std::to_string(index) + "}\n"; @@ -413,6 +471,10 @@ int main() { TotalsSince(); IntervalSummaryFields(); IntervalDroppedThreads(); + SummaryMachineFields(); + FrameMicroseconds(); + BenchFormat(); + BenchSwitch(); GzipFile(); AppendLines(); std::cout << "performance trace tests passed\n"; diff --git a/core/libs/prx/libkernel/AppMetadata/include/AppMetadata.hpp b/core/libs/prx/libkernel/AppMetadata/include/AppMetadata.hpp index 0cceb906ff..ef97a2c7cf 100644 --- a/core/libs/prx/libkernel/AppMetadata/include/AppMetadata.hpp +++ b/core/libs/prx/libkernel/AppMetadata/include/AppMetadata.hpp @@ -11,6 +11,10 @@ struct AppTitleId { char value[12]; }; +struct AppContentVersion { + char value[16]; +}; + struct AppIconData { const std::uint8_t* bytes; std::uint64_t size; @@ -20,6 +24,8 @@ extern "C" { AppTitle GetAppTitle_nid_postfix(); AppTitleId GetAppTitleId_nid_postfix(); +bool HasAppParamJson_nid_no_patch(); +AppContentVersion GetAppContentVersion_nid_no_patch(); bool HasAppIcon_nid_postfix(); AppIconData GetAppIconData_nid_postfix(); std::uint64_t GetAppDownloadDataSizeMiB_nid_postfix(); diff --git a/core/libs/prx/libkernel/AppMetadata/include/ParamJsonParser.hpp b/core/libs/prx/libkernel/AppMetadata/include/ParamJsonParser.hpp index 22f615c522..65e2c6c0a8 100644 --- a/core/libs/prx/libkernel/AppMetadata/include/ParamJsonParser.hpp +++ b/core/libs/prx/libkernel/AppMetadata/include/ParamJsonParser.hpp @@ -9,6 +9,7 @@ struct ParsedParamJson { std::string title; std::string titleId; + std::string contentVersion; std::uint64_t downloadDataSizeMiB = 0; std::uint32_t attribute3 = 0; std::optional flexibleMemorySizeBytes; diff --git a/core/libs/prx/libkernel/AppMetadata/src/AppMetadata.cpp b/core/libs/prx/libkernel/AppMetadata/src/AppMetadata.cpp index f078143320..6598f040e5 100644 --- a/core/libs/prx/libkernel/AppMetadata/src/AppMetadata.cpp +++ b/core/libs/prx/libkernel/AppMetadata/src/AppMetadata.cpp @@ -20,6 +20,7 @@ bool g_iconAttempted = false; bool g_iconLoaded = false; char g_title[128] = {}; char g_titleId[12] = {}; +char g_contentVersion[16] = {}; std::uint64_t g_downloadDataSizeMiB = 0; std::uint32_t g_attribute3 = 0; std::int32_t g_userDefinedParams[4] = {}; @@ -47,6 +48,7 @@ void ensureTitleLoaded() { const auto parsed = parseParams(ResolvePath_nid_no_patch(AppMetadataParamJsonGuestPath), ResolvePath_nid_no_patch(AppMetadataParamSfoGuestPath)); copyToFixedBuffer(g_title, sizeof(g_title), parsed.title); copyToFixedBuffer(g_titleId, sizeof(g_titleId), parsed.titleId); + if (!parsed.contentVersion.empty()) copyToFixedBuffer(g_contentVersion, sizeof(g_contentVersion), parsed.contentVersion); g_downloadDataSizeMiB = parsed.downloadDataSizeMiB; g_attribute3 = parsed.attribute3; std::memcpy(g_userDefinedParams, parsed.userDefinedParams, sizeof(g_userDefinedParams)); @@ -133,6 +135,17 @@ AppTitleId GetAppTitleId_nid_postfix() { return result; } +bool HasAppParamJson_nid_no_patch() { + return std::filesystem::exists(ResolvePath_nid_no_patch(AppMetadataParamJsonGuestPath)); +} + +AppContentVersion GetAppContentVersion_nid_no_patch() { + ensureTitleLoaded(); + AppContentVersion result{}; + std::memcpy(result.value, g_contentVersion, sizeof(result.value)); + return result; +} + bool HasAppIcon_nid_postfix() { ensureIconLoaded(); return g_iconLoaded; diff --git a/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp b/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp index e8ac7d212a..849b5323d2 100644 --- a/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp +++ b/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp @@ -322,6 +322,7 @@ ParsedParamJson parseParamJson(const std::filesystem::path& paramJsonPath) { ParsedParamJson result; result.title = title; result.titleId = titleId; + if (const JsonValue* contentVersion = findObjectMember(root, "contentVersion")) result.contentVersion = asString(*contentVersion, "contentVersion"); if (const JsonValue* attribute = findObjectMember(root, "attribute3")) { const auto& number = attribute->numberText; if (attribute->type != JsonType::Number || number.find_first_of("-.eE") != std::string::npos) throw std::runtime_error("param.json attribute3 is not an unsigned integer"); diff --git a/core/libs/tests/ParamJsonParser.cpp b/core/libs/tests/ParamJsonParser.cpp index 8d9a2ffbd9..fe68a16d2d 100644 --- a/core/libs/tests/ParamJsonParser.cpp +++ b/core/libs/tests/ParamJsonParser.cpp @@ -81,6 +81,14 @@ static void CheckUserDefinedParams(const std::filesystem::path& path) { } } +static void CheckContentVersion(const std::filesystem::path& path) { + Require(ParseText(path, R"({"titleId":"PPSA00000","contentVersion":"01.002.000","localizedParameters":{"en-US":{"titleName":"Example"}}})").contentVersion == "01.002.000", "Incorrect content version"); + Require(ParseText(path, R"({"titleId":"PPSA00000","localizedParameters":{"en-US":{"titleName":"Example"}}})").contentVersion.empty(), "A missing content version was not empty"); + bool rejected = false; + try { ParseText(path, R"({"titleId":"PPSA00000","contentVersion":1,"localizedParameters":{"en-US":{"titleName":"Example"}}})"); } catch (const std::exception&) { rejected = true; } + Require(rejected, "Accepted a content version that is not a string"); +} + static void CheckNesting(const std::filesystem::path& path) { const std::string prefix = R"({"titleId":"PPSA00000","localizedParameters":{"en-US":{"titleName":"Example"}},"extra":)"; const auto nested = [&](std::size_t arrays) { @@ -226,6 +234,7 @@ int main() { } CheckLanguages(path); CheckUserDefinedParams(path); + CheckContentVersion(path); CheckOutputPermission(path); CheckNesting(path); } catch (const std::exception& error) { diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index 03372da27e..f1f98ec822 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -155,7 +155,7 @@ Set `APS5_TRACE_EVENTS=` to change the queue-worker and presenter buffers Inside the window the queue workers and the presenter record every `PerformanceTimer` scope and stage as a complete event named after its frame timing key: `Driver.WorkerWait`, `Driver.Packet`, `Driver.Packet.draw`, `Graphics.Draw`, `Driver.Present`, `Driver.Present.inflight_wait`, `Vulkan.Retire` and so on. A slice shorter than 20 us is only counted: its time is in the thread's counter ` folded` (milliseconds per present) and in the summary. A thread writes a present's counter sample at its first slice after the next present, so the last present of the window has no sample; its time is still in the summary. By default, each queue worker and presenter allocates a buffer for 262,144 events when it starts; a full buffer stops that thread's recording, sets `trace_dropped` and puts an instant event `trace buffer full` on the thread's track at the start of the first lost slice (slices are appended when their outer scope ends, so that can precede the last recorded slice by up to one outer-scope length), with the number of lost slices in `args.events_lost`. When the window closes, the presenter formats the trace once and a `ProfileOutput` writer thread writes `aps5-trace--.json.gz` (Chrome trace-event JSON, opened by ui.perfetto.dev or chrome://tracing) and appends one line to `aps5-summary.jsonl`, both in `APS5_TRACE_DIR` or the working directory. A window that is still open when the title exits writes nothing. -The summary line has `commit` and `dirty` of the build, `env` (every `APS5_*` variable; a value containing `/` or `\` is written as ``), `gpu`, `driver`, `os`, `resolution` (the video output's), `window`, `presents`, `frame_ms` (p50, p90, p99 and max of the intervals between presents), `gpu_busy_ms_per_present` (the batch timestamps behind `[present]`), `worker_busy_pct` (share of the window each queue worker spends in `Driver.Packet`, `Driver.Suspend`, `Driver.EndSubmission`, `Driver.Rewind` and `Driver.Completion`), `top_phases_ms_per_frame` (the eight largest scopes and stages, without `Driver.WorkerWait`), `trace_dropped`, `trace_dropped_threads` and `trace`. `trace_dropped` is `true` when any thread lost a slice, any thread's totals table (512 scope and stage entries) was full, or the `gpu` track was full. `trace_dropped_threads` is present only then and has one entry per such thread name (threads that share a name, such as the shader prepare threads, share one entry: `events_lost` is summed, `events_full_after_s` is the earliest, `totals_full` is true if any is): `events_full_after_s` (seconds from the first present of the window to the start of the first lost slice; absent for `gpu` and, in every summary line (windowed `APS5_TRACE_FRAMES`/`APS5_TRACE_SECONDS` and `APS5_TRACE_SUMMARY_EVERY`), whenever the first lost slice starts outside the line's span of presents, including a slice whose scope began before the window opened or one lost after the window's last present), `events_lost` (slices not recorded; absent when 0; in `APS5_TRACE_SUMMARY_EVERY` lines it counts the thread buffer's losses since it started, not only those of the line's span) and `totals_full` (`true` when the thread's totals table overflowed, which makes the summary numbers incomplete; a thread with `events_lost` only has complete totals). A trace holds timings, scope names, counts and thread names; no guest memory, paths or strings of the title. +The summary line has `commit` and `dirty` of the build, `env` (every `APS5_*` variable; a value containing `/` or `\` is written as ``), `gpu`, `driver`, `os`, `cpu` (the CPU brand string), `cpu_threads` and `memory_mib` (physical memory) of the host, `title_id`, `title` and `content_version` from `app0/sce_sys/param.json` (only when the title has one; `content_version` is empty when the file has no `contentVersion`), `resolution` (the video output's), `window`, `presents`, `frame_ms` (p50, p90, p99 and max of the intervals between presents), `gpu_busy_ms_per_present` (the batch timestamps behind `[present]`), `worker_busy_pct` (share of the window each queue worker spends in `Driver.Packet`, `Driver.Suspend`, `Driver.EndSubmission`, `Driver.Rewind` and `Driver.Completion`), `top_phases_ms_per_frame` (the eight largest scopes and stages, without `Driver.WorkerWait`), `trace_dropped`, `trace_dropped_threads` and `trace`. `trace_dropped` is `true` when any thread lost a slice, any thread's totals table (512 scope and stage entries) was full, or the `gpu` track was full. `trace_dropped_threads` is present only then and has one entry per such thread name (threads that share a name, such as the shader prepare threads, share one entry: `events_lost` is summed, `events_full_after_s` is the earliest, `totals_full` is true if any is): `events_full_after_s` (seconds from the first present of the window to the start of the first lost slice; absent for `gpu` and, in every summary line (windowed `APS5_TRACE_FRAMES`/`APS5_TRACE_SECONDS` and `APS5_TRACE_SUMMARY_EVERY`), whenever the first lost slice starts outside the line's span of presents, including a slice whose scope began before the window opened or one lost after the window's last present), `events_lost` (slices not recorded; absent when 0; in `APS5_TRACE_SUMMARY_EVERY` lines it counts the thread buffer's losses since it started, not only those of the line's span) and `totals_full` (`true` when the thread's totals table overflowed, which makes the summary numbers incomplete; a thread with `events_lost` only has complete totals). A trace holds timings, scope names, counts and thread names; no guest memory, paths or strings of the title. The shader preparation threads are traced as well, `shader prepare`, with a buffer for 32,768 events each. A wait for the GPU mutex on a traced thread is a `GpuMutex.wait.` slice (`dispatch`, `draw`, `present`, `flush` and so on, as on the `[lock]` line). @@ -171,6 +171,8 @@ Set `APS5_TRACE_SUMMARY_EVERY=` (a whole number above 0; a malformed va The switch combines with `APS5_TRACE_FRAMES` or `APS5_TRACE_SECONDS`. The intervals run for the whole session and the window records its full trace on top: the event buffers are allocated and filled only while the window is open, the window's own summary line (with `trace` set and no `interval_*` fields) holds only the time inside the window, and the interval lines continue unchanged around it. Without a window switch no buffer is allocated at all. The two switches do not share a clock origin: `interval_*` is relative to the first present, as the window's `APS5_TRACE_SECONDS` start is. +Set `APS5_TRACE_BENCH=1` together with `APS5_TRACE_SUMMARY_EVERY` to also keep every interval's frame times for a benchmark report. At each interval the presenter rewrites `aps5-bench--.json.gz` in `APS5_TRACE_DIR` or the working directory: one JSON object with `format` (`aps5-bench/1`), the identity fields of the summary line (`commit`, `dirty`, `env`, `gpu`, `driver`, `os`, `cpu`, `cpu_threads`, `memory_mib` and, with `param.json`, `title_id`, `title` and `content_version`), `intervals_dropped` and `intervals`. Each interval has `start_s`, `end_s`, `first_present`, `resolution`, `gpu_busy_ms` (the interval's total) and `frame_us`, the whole microseconds between consecutive presents. The file keeps the last 240 intervals; `intervals_dropped` counts older ones it no longer holds. Any other value, or the switch without `APS5_TRACE_SUMMARY_EVERY`, fails the first present. The frame times cost 4 bytes per present in memory, and the file is formatted and compressed on a writer thread. + `python tools/trace_digest.py aps5-summary.jsonl` prints the interval lines of such a file as one row per interval (frame p50, p90, p99, max, GPU busy per present, the busiest queue worker and the largest phase) and a text sparkline of the frame p99 and of the GPU busy time; lines of window captures in the file are skipped, and a change of commit, environment, GPU, driver or OS between intervals is reported. `--json` prints the rows as JSON. With `APS5_TRACE_SUMMARY_EVERY` set, the recorder writes a timestamp pair per batch for the whole run and every `PerformanceTimer` scope adds its time to the thread's totals, two clock reads per scope. A thread's totals are read while it runs, so a scope that straddles an interval boundary is counted in one interval or the other. diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index d241fa9f5f..0ad3cb7690 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -373,6 +373,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_THREAD_RT` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/ThreadPriority.cpp` | 10 | realtime priority the raised threads ask rtkit for (clamped to 1..99) | | `APS5_THREAD_RTTIME_US` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/ThreadPriority.cpp` | 200000 | hard RLIMIT_RTTIME the realtime plan lowers to, in microseconds | | `APS5_TRACE_BARRIERS` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | prints that many simulated hazard barriers before stopping | +| `APS5_TRACE_BENCH` | string | `core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp` | | `1` with `APS5_TRACE_SUMMARY_EVERY` also rewrites `aps5-bench--.json.gz` with every interval's frame times; any other value fails the first present | | `APS5_TRACE_BIGBUF` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | lists every copied guest region of at least 1 MiB | | `APS5_TRACE_CAPSYNC` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WriteEvidence.cpp` | | prints every unlocked hook wait | | `APS5_TRACE_CONTEXT_STATE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp` | | logs context-state operations and the context registers written | diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 6f299ab99b..05c7dcb28f 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -283,6 +283,10 @@ GPU results in storage images and render targets stay on the GPU until something A title's own CPU reads of that memory are not seen, so a title that waits for a GPU label and then reads the results reads stale memory. `APS5_STORE_AT_LABEL=1` stores every pending result before each label the GPU writes (`RELEASE_MEM`, `WRITE_DATA`, `DMA_DATA`), at the cost of a write-back of every pending image per label. Any other value stops the title at its first label. +### Benchmark reports + +To measure how a title runs on a machine, start it with `APS5_TRACE_SUMMARY_EVERY=30 APS5_TRACE_BENCH=1`, play the scene for a few minutes and quit. Every 30 seconds the runtime rewrites `aps5-bench--.json.gz` in `APS5_TRACE_DIR` or the working directory and appends a line to `aps5-summary.jsonl`. The file holds the time between every two presents, the commit, GPU, driver, OS, CPU name, thread count and physical memory, and, when `app0/sce_sys/param.json` is present, the title ID, name and content version. It holds no game data. The first interval includes shader compilation; compare later intervals. The interval still open when the title exits is not written. The fields are described under [summaries for a long session](../dev/BUILD.md#summaries-for-a-long-session). + ### Tracing system library calls Windows only. An executable converted with `--trace-imports` records the calls it makes to the libraries named in `APS5_TRACE_CALLS`, a comma-separated list such as `libkernel,libSceSaveData`, or `all`. Without the variable nothing is recorded. The crash and abort reports then end with the last 64 calls: sequence number, thread, library, import name as the executable imports it (a NID) and the caller's address. They are followed by the 32 most called imports since start-up, with their number of calls. From f085363ef22b4335d0385444c246d64876705465 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Mon, 19 Oct 2026 02:05:16 -0400 Subject: [PATCH 057/269] fix(agc): reject late profiling output after shutdown --- .../Execution/include/ProfileOutput.hpp | 3 +- .../libSceAgcDriver/tests/ProfileOutput.cpp | 49 ++++++++++++++++--- 2 files changed, 43 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp index 617a1399e0..533685ed98 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp @@ -42,7 +42,7 @@ public: { std::lock_guard lock(mutex); if (failure) std::rethrow_exception(failure); - if (stopping) throw std::runtime_error("profile output is stopping"); + if (stopping) return false; if (text.empty()) return true; if (text.size() > capacity - bytes) { ++dropped; @@ -69,6 +69,7 @@ public: } else if (static_cast(size) > capacity) { std::lock_guard lock(mutex); if (failure) std::rethrow_exception(failure); + if (stopping) return; ++dropped; changed.notify_one(); } else { diff --git a/core/libs/prx/libSceAgcDriver/tests/ProfileOutput.cpp b/core/libs/prx/libSceAgcDriver/tests/ProfileOutput.cpp index 72fb292b4e..0314674b2a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ProfileOutput.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ProfileOutput.cpp @@ -90,13 +90,17 @@ void ConcurrentWriters() { } void WriterFailure() { - AgcDriver::ProfileOutput output([](std::string_view) { throw std::runtime_error("test writer failure"); }); + AgcDriver::ProfileOutput output([](std::string_view) { throw std::runtime_error("test writer failure"); }, 8192); output.Write("test"); - for (int operation = 0; operation < 2; ++operation) { + for (int operation = 0; operation < 6; ++operation) { bool failed = false; try { if (operation == 0) output.Flush(); - else output.Write("next"); + else if (operation == 1) output.Write("next"); + else if (operation == 2) { output.Stop(); output.Write("after failed stop"); } + else if (operation == 3) output.Print("late failure"); + else if (operation == 4) output.Print("%s", std::string(6000, 'x').c_str()); + else output.Print("%s", std::string(10000, 'x').c_str()); } catch (const std::runtime_error& error) { failed = std::string(error.what()) == "test writer failure"; } @@ -106,16 +110,44 @@ void WriterFailure() { void ExplicitStop() { std::string written; - AgcDriver::ProfileOutput output([&](std::string_view text) { written += text; }); + AgcDriver::ProfileOutput output([&](std::string_view text) { written += text; }, 8192); output.Write("pending output"); output.Stop(); output.Stop(); output.Flush(); Check(written == "pending output", "explicit stop did not drain pending output"); - bool failed = false; - try { output.Write("after stop"); } - catch (const std::runtime_error& error) { failed = std::string(error.what()) == "profile output is stopping"; } - Check(failed, "write after stop was accepted"); + Check(!output.Write("after stop"), "write after stop was accepted"); + Check(!output.Write(""), "empty write after stop was accepted"); + output.Print("late small diagnostic %d", 1); + output.Print("%s", std::string(6000, 'x').c_str()); + output.Print("%s", std::string(10000, 'x').c_str()); + output.Flush(); + Check(written == "pending output", "late output reached the stopped writer"); +} + +void StopWithProducers() { + std::string written; + AgcDriver::ProfileOutput output([&](std::string_view text) { written += text; }); + std::atomic accepted{0}; + std::atomic start{false}; + std::vector producers; + for (int thread = 0; thread < 8; ++thread) { + producers.emplace_back([&] { + start.wait(false); + for (int item = 0; item < 1000; ++item) { + if (output.Write("x")) ++accepted; + } + }); + } + Check(output.Write("x"), "initial write was rejected"); + ++accepted; + start.store(true); + start.notify_all(); + output.Stop(); + for (auto& producer : producers) producer.join(); + output.Flush(); + Check(written == std::string(accepted.load(), 'x'), "stop did not drain exactly the accepted output"); + Check(!output.Write("late"), "concurrent stop left the writer open"); } } @@ -127,6 +159,7 @@ int main() { ConcurrentWriters(); WriterFailure(); ExplicitStop(); + StopWithProducers(); std::cout << "profile output tests passed\n"; return 0; } catch (const std::exception& error) { From 2798e6a6fde80b09fc981333aa552b7104afb7d0 Mon Sep 17 00:00:00 2001 From: Adam Franke Date: Mon, 19 Oct 2026 02:10:22 -0400 Subject: [PATCH 058/269] fix(libSceSsl): follow the codes and defaults measured on a PS5 (13.40) Every unverified point of the libSceSsl debt entry was measured on a PS5 on system software 13.40 with a ps5-payload-dev payload against a TLS 1.2 test server presenting leaves issued by the test fixture root. The code follows the console where it differed: initial verify options 0x25 with the date checks off, bits outside 0x3F rejected, the verify callback called with the context id and the bitmask of failed checks and re-run from every later call after a negative return, sceSslRead flags 0 reading until the requested length, streams that end without close_notify reading as 0, resets reported with the libSceNet codes, receive timeouts as SSL_ERROR_WANT_POLLIN, a fatal alert ending the transport, sceSslClose as a repeatable no-op before the handshake, read and write before connect running the handshake, SSL_ERROR_INVALID_ID for unknown or deleted handles and terminated contexts, sceSslSetMinSslVersion accepting 2 to 5, an empty ALPN list rejected, long attribute names from sceSslGetNameEntryInfo and sceSslGetSerialNumber leaving the caller's length. The debt entries record the measured values and what stays unmeasured. --- core/libs/prx/libSceSsl/Export.cpp | 327 ++++++++++++++-------- core/libs/tests/GuestSsl.cpp | 59 ++-- core/libs/tests/GuestSslLoad.cpp | 15 +- core/libs/tests/GuestSslPs5Readers.cpp | 4 +- core/libs/tests/GuestSslTransport.cpp | 182 +++++++++--- core/libs/tests/SslCertificateFixture.hpp | 38 +++ docs/dev/debt/libSceSsl.md | 8 +- 7 files changed, 442 insertions(+), 191 deletions(-) diff --git a/core/libs/prx/libSceSsl/Export.cpp b/core/libs/prx/libSceSsl/Export.cpp index 5d7394fdb3..636719b717 100644 --- a/core/libs/prx/libSceSsl/Export.cpp +++ b/core/libs/prx/libSceSsl/Export.cpp @@ -1,7 +1,6 @@ #ifdef _WIN32 #include #else -#include #include #include #endif @@ -52,15 +51,21 @@ constexpr int ErrorFatalAlert = static_cast(0x8095f010); constexpr int ErrorWantPollIn = static_cast(0x8095f013); constexpr int ErrorWantPollOut = static_cast(0x8095f014); constexpr int ErrorSocketClosed = static_cast(0x8095170d); -constexpr int ErrorReadTimeout = static_cast(0x80951710); constexpr int ErrorCallbackCheck = static_cast(0x8095f0fd); -constexpr int ReadPartial = 0x1; +constexpr int ErrorInvalidValue = static_cast(0x8095f007); +constexpr int ErrorBadLength = static_cast(0x80951773); +constexpr int ErrorBadRecord = static_cast(0x80951d55); +constexpr int ErrorNetBadDescriptor = static_cast(0x80410109); +constexpr int ErrorNetPipe = static_cast(0x80410120); +constexpr int ErrorNetReset = static_cast(0x80410136); +constexpr int ErrorKernelInvalid = static_cast(0x80020016); constexpr std::uint32_t FlagServerVerify = 0x1; constexpr std::uint32_t FlagCnCheck = 0x4; constexpr std::uint32_t FlagNotAfterCheck = 0x8; constexpr std::uint32_t FlagNotBeforeCheck = 0x10; constexpr std::uint32_t FlagKnownCaCheck = 0x20; -constexpr std::uint32_t InitialVerifyOptions = FlagServerVerify | FlagCnCheck | FlagNotAfterCheck | FlagNotBeforeCheck | FlagKnownCaCheck; +constexpr std::uint32_t VerifyOptionMask = 0x3f; +constexpr std::uint32_t InitialVerifyOptions = FlagServerVerify | FlagCnCheck | FlagKnownCaCheck; constexpr std::int64_t TicksPerSecond = 1000000; constexpr std::int64_t TicksPerMinute = 60 * TicksPerSecond; @@ -263,18 +268,19 @@ bool HostReset(int error) { #endif } -bool HostTimedOut(std::uintptr_t socket, int error) { +bool HostTimedOut(int error) { #ifdef _WIN32 - (void)socket; return error == WSAETIMEDOUT; #else - if (!HostWouldBlock(error)) return false; - const int flags = ::fcntl(static_cast(socket), F_GETFL); - return flags >= 0 && !(flags & O_NONBLOCK); + (void)error; + return false; #endif } +enum class Operation { Connect, Read, Write, Close }; + struct Session { + int contextHandle; std::shared_ptr context; std::shared_ptr identity; std::shared_ptr peer; @@ -291,15 +297,17 @@ struct Session { bool verifying = false; bool verified = false; bool closed = false; - bool timedOut = false; + bool reset = false; + bool gone = false; int verifyResult = 0; + unsigned int failedOptions = 0; Random random; mbedtls_x509_crt authorities; mbedtls_ssl_config config; mbedtls_ssl_context ssl; - Session(std::shared_ptr owner, std::shared_ptr socketHolder, std::uintptr_t native) - : context(std::move(owner)), identity(context->identity), socketOwner(std::move(socketHolder)), socket(native) { + Session(int handle, std::shared_ptr owner, std::shared_ptr socketHolder, std::uintptr_t native) + : contextHandle(handle), context(std::move(owner)), identity(context->identity), socketOwner(std::move(socketHolder)), socket(native) { mbedtls_x509_crt_init(&authorities); mbedtls_ssl_config_init(&config); mbedtls_ssl_init(&ssl); @@ -346,7 +354,6 @@ struct Session { static int Send(void* argument, const unsigned char* buffer, std::size_t length) { auto& session = *static_cast(argument); const auto chunk = static_cast(std::min(length, INT_MAX)); - session.timedOut = false; for (;;) { #ifdef _WIN32 const auto sent = ::send(static_cast(session.socket), reinterpret_cast(buffer), chunk, 0); @@ -356,8 +363,7 @@ struct Session { if (sent >= 0) return static_cast(sent); const int error = HostError(); if (HostInterrupted(error)) continue; - session.timedOut = HostTimedOut(session.socket, error); - if (session.timedOut || HostWouldBlock(error)) return MBEDTLS_ERR_SSL_WANT_WRITE; + if (HostWouldBlock(error) || HostTimedOut(error)) return MBEDTLS_ERR_SSL_WANT_WRITE; if (HostReset(error)) return MBEDTLS_ERR_NET_CONN_RESET; session.hostError = error; return MBEDTLS_ERR_NET_SEND_FAILED; @@ -367,7 +373,6 @@ struct Session { static int Receive(void* argument, unsigned char* buffer, std::size_t length) { auto& session = *static_cast(argument); const auto chunk = static_cast(std::min(length, INT_MAX)); - session.timedOut = false; for (;;) { #ifdef _WIN32 const auto received = ::recv(static_cast(session.socket), reinterpret_cast(buffer), chunk, 0); @@ -377,41 +382,49 @@ struct Session { if (received >= 0) return static_cast(received); const int error = HostError(); if (HostInterrupted(error)) continue; - session.timedOut = HostTimedOut(session.socket, error); - if (session.timedOut || HostWouldBlock(error)) return MBEDTLS_ERR_SSL_WANT_READ; + if (HostWouldBlock(error) || HostTimedOut(error)) return MBEDTLS_ERR_SSL_WANT_READ; if (HostReset(error)) return MBEDTLS_ERR_NET_CONN_RESET; session.hostError = error; return MBEDTLS_ERR_NET_RECV_FAILED; } } - int VerifyCode() const { + void Verify() { const auto flags = mbedtls_ssl_get_verify_result(&ssl); - if (!(verifyOptions & FlagServerVerify)) return 0; - if ((verifyOptions & FlagKnownCaCheck) && (flags & MBEDTLS_X509_BADCERT_NOT_TRUSTED)) return ErrorUnknownCa; - if ((verifyOptions & FlagCnCheck) && (flags & MBEDTLS_X509_BADCERT_CN_MISMATCH)) return ErrorCnCheck; - if ((verifyOptions & FlagNotAfterCheck) && (flags & MBEDTLS_X509_BADCERT_EXPIRED)) return ErrorNotAfterCheck; - if ((verifyOptions & FlagNotBeforeCheck) && (flags & MBEDTLS_X509_BADCERT_FUTURE)) return ErrorNotBeforeCheck; + failedOptions = 0; + verifyResult = 0; + if (!(verifyOptions & FlagServerVerify)) return; + if ((verifyOptions & FlagKnownCaCheck) && (flags & MBEDTLS_X509_BADCERT_NOT_TRUSTED)) failedOptions |= FlagKnownCaCheck; + if ((verifyOptions & FlagCnCheck) && (flags & MBEDTLS_X509_BADCERT_CN_MISMATCH)) failedOptions |= FlagCnCheck; + if ((verifyOptions & FlagNotAfterCheck) && (flags & MBEDTLS_X509_BADCERT_EXPIRED)) failedOptions |= FlagNotAfterCheck; + if ((verifyOptions & FlagNotBeforeCheck) && (flags & MBEDTLS_X509_BADCERT_FUTURE)) failedOptions |= FlagNotBeforeCheck; constexpr std::uint32_t optional = MBEDTLS_X509_BADCERT_NOT_TRUSTED | MBEDTLS_X509_BADCERT_CN_MISMATCH | MBEDTLS_X509_BADCERT_EXPIRED | MBEDTLS_X509_BADCERT_FUTURE; - return (flags & ~optional) ? ErrorInvalidCert : 0; + if (failedOptions & FlagKnownCaCheck) verifyResult = ErrorUnknownCa; + else if (failedOptions & FlagCnCheck) verifyResult = ErrorCnCheck; + else if (failedOptions & FlagNotAfterCheck) verifyResult = ErrorNotAfterCheck; + else if (failedOptions & FlagNotBeforeCheck) verifyResult = ErrorNotBeforeCheck; + else if (flags & ~optional) verifyResult = ErrorInvalidCert; } - int Code(int result) const { + int Code(int result, Operation operation) { switch (result) { - case MBEDTLS_ERR_SSL_WANT_READ: return timedOut ? ErrorReadTimeout : ErrorWantPollIn; - case MBEDTLS_ERR_SSL_WANT_WRITE: return timedOut ? 0 : ErrorWantPollOut; - case MBEDTLS_ERR_SSL_CONN_EOF: - case MBEDTLS_ERR_NET_CONN_RESET: return ErrorSocketClosed; + case MBEDTLS_ERR_SSL_WANT_READ: return ErrorWantPollIn; + case MBEDTLS_ERR_SSL_WANT_WRITE: return ErrorWantPollOut; + case MBEDTLS_ERR_NET_CONN_RESET: + reset = true; + if (operation == Operation::Write) return ErrorNetPipe; + if (operation == Operation::Connect) return ErrorSocketClosed; + return ErrorNetReset; + case MBEDTLS_ERR_SSL_CONN_EOF: return operation == Operation::Connect ? ErrorSocketClosed : 0; case MBEDTLS_ERR_SSL_FATAL_ALERT_MESSAGE: return ErrorFatalAlert; + case MBEDTLS_ERR_SSL_INVALID_MAC: return operation == Operation::Read ? ErrorBadRecord : 0; default: return 0; } } [[noreturn]] void Fail(const char* function, int error) const { if (currentPoolExhausted || error == MBEDTLS_ERR_SSL_ALLOC_FAILED) throw std::bad_alloc(); - if (error == MBEDTLS_ERR_SSL_WANT_WRITE && timedOut) - throw std::runtime_error(std::string(function) + ": the socket's send timeout expired; the console's error code is unknown"); char text[160]; mbedtls_strerror(error, text, sizeof(text)); auto message = std::string(function) + ": " + text; @@ -426,7 +439,7 @@ struct Session { using SslVerifyCallback = int (APS5_VABI *)(int, unsigned int, void**, int, void*); -constexpr int SslVersionNone = 0; +constexpr int SslVersionSsl30 = 2; constexpr int SslVersionTls12 = 5; struct SslConnection { @@ -436,7 +449,7 @@ struct SslConnection { SslVerifyCallback verifyCallback; void* verifyArgument; std::uint32_t verifyOptions = InitialVerifyOptions; - int minimumVersion = SslVersionNone; + int minimumVersion = SslVersionTls12; std::string alpn; std::shared_ptr session; }; @@ -463,26 +476,22 @@ Context& GetContext(int handle) { return *found->second; } -SslConnection& RequireConnection(const char* function, int sslConnectionId) { +SslConnection* FindConnection(int sslConnectionId) { const auto connection = State().connections.find(sslConnectionId); - if (connection == State().connections.end()) throw std::invalid_argument(std::string(function) + ": unknown connection"); - if (!State().contexts.contains(connection->second.context)) { - throw std::invalid_argument(std::string(function) + ": the connection's context was terminated"); - } - return connection->second; + if (connection == State().connections.end()) return nullptr; + if (!State().contexts.contains(connection->second.context)) return nullptr; + return &connection->second; } -std::shared_ptr RequireSession(const char* function, int sslConnectionId) { - std::lock_guard lock(State().mutex); - auto& connection = RequireConnection(function, sslConnectionId); - if (!connection.session) throw std::logic_error(std::string(function) + ": the connection is not connected"); - return connection.session; +SslConnection& RequireConnection(const char* function, int sslConnectionId) { + auto* connection = FindConnection(sslConnectionId); + if (!connection) throw std::invalid_argument(std::string(function) + ": unknown connection"); + return *connection; } -void RequireVerified(const char* function, const Session& session) { - if (!session.verified) throw std::logic_error(std::string(function) + ": the connection is not connected"); - if (session.verifyResult != 0) throw std::logic_error(std::string(function) + ": the connection failed verification"); - if (session.closed) throw std::logic_error(std::string(function) + ": the connection was closed"); +void RequireUsable(const char* function, const Session& session) { + if (session.verifyResult != 0 && session.verifyResult != ErrorCallbackCheck) + throw std::logic_error(std::string(function) + ": the connection failed verification; the console's behaviour is unknown"); } void RegisterPeer(Session& session) { @@ -554,6 +563,17 @@ const mbedtls_x509_crt* RequireCertificate(const char* function, int sslCtxId, v throw std::invalid_argument(std::string(function) + ": unknown certificate"); } +const char* LongAttributeName(const char* shortName) { + static constexpr std::pair names[] = { + {"CN", "commonName"}, {"C", "countryName"}, {"L", "localityName"}, {"ST", "stateOrProvinceName"}, + {"O", "organizationName"}, {"OU", "organizationalUnitName"}, {"SN", "surname"}, {"GN", "givenName"}, + {"DC", "domainComponent"}, {"uid", "userId"}, + }; + for (const auto& [name, longName] : names) + if (std::strcmp(shortName, name) == 0) return longName; + return shortName; +} + void* ExtractCertName(const char* function, int sslCtxId, void* sslCert, bool subject) { std::lock_guard lock(State().mutex); auto& context = GetContext(sslCtxId); @@ -565,7 +585,7 @@ void* ExtractCertName(const char* function, int sslCtxId, void* sslCert, bool su std::string oid; const char* shortName = nullptr; if (mbedtls_oid_get_attr_short_name(&node->oid, &shortName) == 0 && shortName) { - oid = shortName; + oid = LongAttributeName(shortName); } else { char numeric[64] = {}; if (mbedtls_oid_get_numeric_string(numeric, sizeof(numeric), &node->oid) > 0) { @@ -588,6 +608,7 @@ void* ExtractCertName(const char* function, int sslCtxId, void* sslCert, bool su extern "C" { int APS5_VABI sceSslInit_nid_postfix(std::uint64_t poolSize) { + if (poolSize == 0) return ErrorKernelInvalid; auto& state = State(); std::lock_guard lock(state.mutex); if (state.nextHandle > static_cast(std::numeric_limits::max())) throw std::overflow_error("SSL context handles exhausted"); @@ -600,18 +621,18 @@ int APS5_VABI sceSslInit_nid_postfix(std::uint64_t poolSize) { int APS5_VABI sceSslTerm_nid_postfix(int sslCtxId) { auto& state = State(); std::lock_guard lock(state.mutex); - state.contexts.erase(sslCtxId); + if (!state.contexts.erase(sslCtxId)) return ErrorInvalidId; return 0; } int APS5_VABI sceSslGetMemoryPoolStats(int sslCtxId, SceSsl::MemoryPoolStats* stats) { - if (!stats) return static_cast(0x8095F007); + if (!stats) return ErrorInvalidValue; auto& state = State(); std::lock_guard lock(state.mutex); const auto found = state.contexts.find(sslCtxId); if (found == state.contexts.end()) { std::memset(stats, 0, sizeof(*stats)); - return static_cast(0x8095F006); + return ErrorInvalidId; } auto& pool = found->second->pool; std::lock_guard poolLock(pool.mutex); @@ -757,7 +778,7 @@ int APS5_VABI sceSslFreeCaCerts(int sslCtxId, SceSsl::CaCerts* certificates) { } int APS5_VABI sceSslCreateConnection(int sslCtxId, int sockId, const char* host) { - if (!host) throw std::invalid_argument("sceSslCreateConnection: null host"); + if (!host) return ErrorInvalidValue; std::lock_guard lock(State().mutex); if (!State().contexts.contains(sslCtxId)) return ErrorInvalidId; auto& state = State(); @@ -771,58 +792,74 @@ int APS5_VABI sceSslCreateConnection(int sslCtxId, int sockId, const char* host) int APS5_VABI sceSslDeleteConnection(int sslConnectionId) { std::shared_ptr session; std::lock_guard lock(State().mutex); - const auto found = State().connections.find(sslConnectionId); - if (found == State().connections.end()) throw std::invalid_argument("sceSslDeleteConnection: unknown connection"); - session = std::move(found->second.session); - State().connections.erase(found); + auto* connection = FindConnection(sslConnectionId); + if (!connection) return ErrorInvalidId; + session = std::move(connection->session); + State().connections.erase(sslConnectionId); return 0; } int APS5_VABI sceSslSetVerifyCallback(int sslConnectionId, SslVerifyCallback cbfunc, void* userArg) { std::lock_guard lock(State().mutex); - SslConnection& connection = RequireConnection(__func__, sslConnectionId); - connection.verifyCallback = cbfunc; - connection.verifyArgument = userArg; + auto* connection = FindConnection(sslConnectionId); + if (!connection) return ErrorInvalidId; + connection->verifyCallback = cbfunc; + connection->verifyArgument = userArg; return 0; } -int APS5_VABI sceSslConnect(int sslConnectionId) { - std::shared_ptr session; +} + +namespace { + +int RunCallback(Session& session, std::unique_lock& sessionLock, SslVerifyCallback callback, void* argument) { + session.verifying = true; + sessionLock.unlock(); + const int decision = callback(session.contextHandle, session.failedOptions, session.peerHandles.data(), + static_cast(session.peerHandles.size()), argument); + sessionLock.lock(); + session.verifying = false; + return decision < 0 ? ErrorCallbackCheck : 0; +} + +int Establish(const char* function, int sslConnectionId, std::shared_ptr& session) { SslVerifyCallback callback = nullptr; void* callbackArgument = nullptr; { auto& state = State(); std::lock_guard lock(state.mutex); - auto& connection = RequireConnection(__func__, sslConnectionId); - if (!connection.session) { - auto context = state.contexts.at(connection.context); + auto* connection = FindConnection(sslConnectionId); + if (!connection) return ErrorInvalidId; + if (!connection->session) { + auto context = state.contexts.at(connection->context); std::shared_ptr owner; std::uintptr_t native = 0; - if (!KernelLookupSocketNative_nid_no_patch(connection.socket, &owner, &native)) - throw std::invalid_argument("sceSslConnect: descriptor " + std::to_string(connection.socket) + " is not a socket"); + if (!KernelLookupSocketNative_nid_no_patch(connection->socket, &owner, &native)) return ErrorNetBadDescriptor; PoolScope scope(context->pool); - session = std::make_shared(std::move(context), std::move(owner), native); - session->Configure(connection.host, connection.alpn, connection.verifyOptions); - connection.session = session; + session = std::make_shared(connection->context, std::move(context), std::move(owner), native); + session->Configure(connection->host, connection->alpn, connection->verifyOptions); + connection->session = session; } else { - session = connection.session; + session = connection->session; } - callback = connection.verifyCallback; - callbackArgument = connection.verifyArgument; + callback = connection->verifyCallback; + callbackArgument = connection->verifyArgument; } std::unique_lock sessionLock(session->mutex); - if (session->verified && session->verifyResult != 0) return session->verifyResult; - if (session->closed) throw std::logic_error("sceSslConnect: the connection was closed"); - if (session->verified) return 0; - if (session->verifying) throw std::logic_error("sceSslConnect: the verify callback is running"); - int code = 0; + if (session->verifying) throw std::logic_error(std::string(function) + ": the verify callback is running"); + if (session->verified) { + if (session->verifyResult == ErrorCallbackCheck && callback) session->verifyResult = RunCallback(*session, sessionLock, callback, callbackArgument); + return session->verifyResult; + } + if (session->gone) return 0; { PoolScope scope(session->context->pool); if (!session->handshaken) { const int result = mbedtls_ssl_handshake(&session->ssl); - if (result == MBEDTLS_ERR_SSL_WANT_READ || result == MBEDTLS_ERR_SSL_WANT_WRITE) { - if (const int pending = session->Code(result)) return pending; - session->Fail(__func__, result); + if (result == MBEDTLS_ERR_SSL_WANT_READ || result == MBEDTLS_ERR_SSL_WANT_WRITE) return session->Code(result, Operation::Connect); + if (result == MBEDTLS_ERR_SSL_FATAL_ALERT_MESSAGE) { + session->gone = true; + return ErrorFatalAlert; } if (result != 0) { { @@ -830,55 +867,85 @@ int APS5_VABI sceSslConnect(int sslConnectionId) { const auto found = State().connections.find(sslConnectionId); if (found != State().connections.end() && found->second.session == session) found->second.session.reset(); } - if (const int failure = session->Code(result)) return failure; - session->Fail(__func__, result); + if (const int failure = session->Code(result, Operation::Connect)) return failure; + session->Fail(function, result); } session->handshaken = true; const char* selected = mbedtls_ssl_get_alpn_protocol(&session->ssl); session->alpnSelected = selected ? selected : ""; } - code = session->VerifyCode(); + session->Verify(); + if (callback && session->verifyResult == ErrorInvalidCert) + throw std::runtime_error(std::string(function) + ": the chain failed a check no verify option names; what the console passes to the verify callback is unknown"); if (callback) RegisterPeer(*session); } - if (callback) { - session->verifying = true; - sessionLock.unlock(); - const int decision = callback(sslConnectionId, static_cast(code), session->peerHandles.data(), - static_cast(session->peerHandles.size()), callbackArgument); - sessionLock.lock(); - session->verifying = false; - code = decision < 0 ? ErrorCallbackCheck : 0; - } + if (callback) session->verifyResult = RunCallback(*session, sessionLock, callback, callbackArgument); session->verified = true; - session->verifyResult = code; - if (code != 0 && !session->closed) { + if (session->verifyResult != 0 && session->verifyResult != ErrorCallbackCheck) { PoolScope scope(session->context->pool); mbedtls_ssl_close_notify(&session->ssl); session->closed = true; } - return code; + return session->verifyResult; +} + +} + +extern "C" { + +int APS5_VABI sceSslConnect(int sslConnectionId) { + std::shared_ptr session; + return Establish(__func__, sslConnectionId, session); } int APS5_VABI sceSslRead(int sslConnectionId, void* buf, size_t len, int flags) { - if (flags != 0 && flags != ReadPartial) throw std::invalid_argument("sceSslRead: unsupported flags " + std::to_string(flags)); if (!buf && len) throw std::invalid_argument("sceSslRead: null buffer"); - const auto session = RequireSession(__func__, sslConnectionId); + std::shared_ptr session; + if (const int code = Establish(__func__, sslConnectionId, session)) { + if (session) { + std::lock_guard sessionLock(session->mutex); + RequireUsable(__func__, *session); + } + return code; + } std::lock_guard sessionLock(session->mutex); - RequireVerified(__func__, *session); + if (session->gone || session->closed) return 0; + if (session->reset) return ErrorNetReset; PoolScope scope(session->context->pool); - const int result = mbedtls_ssl_read(&session->ssl, static_cast(buf), std::min(len, INT_MAX)); - if (result >= 0) return result; - if (result == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY) return 0; - if (const int code = session->Code(result)) return code; - session->Fail(__func__, result); + auto* bytes = static_cast(buf); + const auto total = std::min(len, INT_MAX); + std::size_t received = 0; + while (received < total) { + const int result = mbedtls_ssl_read(&session->ssl, bytes + received, total - received); + if (result > 0) { + received += static_cast(result); + if (flags != 0) break; + continue; + } + if (result == 0 || result == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY || result == MBEDTLS_ERR_SSL_CONN_EOF) break; + const int code = session->Code(result, Operation::Read); + if (code == 0) session->Fail(__func__, result); + if (received) break; + return code; + } + return static_cast(received); } int APS5_VABI sceSslWrite(int sslConnectionId, void* buf, size_t len, int flags) { - if (flags != 0) throw std::invalid_argument("sceSslWrite: unsupported flags " + std::to_string(flags)); + if (flags != 0) return ErrorInvalidValue; if (!buf && len) throw std::invalid_argument("sceSslWrite: null buffer"); - const auto session = RequireSession(__func__, sslConnectionId); + std::shared_ptr session; + if (const int code = Establish(__func__, sslConnectionId, session)) { + if (session) { + std::lock_guard sessionLock(session->mutex); + RequireUsable(__func__, *session); + } + return code; + } std::lock_guard sessionLock(session->mutex); - RequireVerified(__func__, *session); + if (session->gone) return ErrorSocketClosed; + if (session->closed) throw std::logic_error("sceSslWrite: the connection was closed; the console's behaviour is unknown"); + if (session->reset) return ErrorNetPipe; PoolScope scope(session->context->pool); const auto* bytes = static_cast(buf); const auto total = std::min(len, INT_MAX); @@ -886,9 +953,10 @@ int APS5_VABI sceSslWrite(int sslConnectionId, void* buf, size_t len, int flags) while (written < total) { const int result = mbedtls_ssl_write(&session->ssl, bytes + written, total - written); if (result < 0) { + const int code = session->Code(result, Operation::Write); + if (code == 0) session->Fail(__func__, result); if (written) break; - if (const int code = session->Code(result)) return code; - session->Fail(__func__, result); + return code; } written += static_cast(result); } @@ -899,15 +967,20 @@ int APS5_VABI sceSslClose(int sslConnectionId) { std::shared_ptr session; { std::lock_guard lock(State().mutex); - session = RequireConnection(__func__, sslConnectionId).session; + auto* connection = FindConnection(sslConnectionId); + if (!connection) return ErrorInvalidId; + session = connection->session; } if (!session) return 0; std::lock_guard sessionLock(session->mutex); - if (session->closed) return 0; - session->closed = true; - if (!session->handshaken) return 0; + if (session->gone || session->closed || !session->handshaken) return 0; + if (session->reset) return ErrorNetReset; PoolScope scope(session->context->pool); - mbedtls_ssl_close_notify(&session->ssl); + const int result = mbedtls_ssl_close_notify(&session->ssl); + if (result == MBEDTLS_ERR_SSL_WANT_WRITE || result == MBEDTLS_ERR_SSL_WANT_READ) return session->Code(result, Operation::Close); + if (result == MBEDTLS_ERR_NET_CONN_RESET) return session->Code(result, Operation::Close); + if (result != 0) session->Fail(__func__, result); + session->closed = true; return 0; } @@ -925,7 +998,6 @@ int APS5_VABI sceSslGetSerialNumber(int sslCtxId, void* sslCert, unsigned char* return ErrorInvalidArg; } std::memcpy(sboData, crt->serial.p, reqLen); - *sboLen = reqLen; return 0; } @@ -942,13 +1014,19 @@ int APS5_VABI sceSslFreeSslCertName(int sslCtxId, void* certName) { int APS5_VABI sceSslEnableVerifyOption(int sslConnectionId, std::uint32_t option) { std::lock_guard lock(State().mutex); - RequireConnection(__func__, sslConnectionId).verifyOptions |= option; + auto* connection = FindConnection(sslConnectionId); + if (!connection) return ErrorInvalidId; + if (option & ~VerifyOptionMask) return ErrorInvalidValue; + connection->verifyOptions |= option; return 0; } int APS5_VABI sceSslDisableVerifyOption(int sslConnectionId, std::uint32_t option) { std::lock_guard lock(State().mutex); - RequireConnection(__func__, sslConnectionId).verifyOptions &= ~option; + auto* connection = FindConnection(sslConnectionId); + if (!connection) return ErrorInvalidId; + if (option & ~VerifyOptionMask) return ErrorInvalidValue; + connection->verifyOptions &= ~option; return 0; } @@ -967,16 +1045,19 @@ 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); - RequireConnection(__func__, sslConnectionId).alpn = proto; + auto* connection = FindConnection(sslConnectionId); + if (!connection) return ErrorInvalidId; + if (!*proto) return ErrorBadLength; + connection->alpn = proto; return 0; } int APS5_VABI sceSslSetMinSslVersion(int sslConnectionId, int version) { - if (version < SslVersionNone || version > SslVersionTls12) { - throw std::invalid_argument("sceSslSetMinSslVersion: unknown version " + std::to_string(version)); - } std::lock_guard lock(State().mutex); - RequireConnection(__func__, sslConnectionId).minimumVersion = version; + auto* connection = FindConnection(sslConnectionId); + if (!connection) return ErrorInvalidId; + if (version < SslVersionSsl30 || version > SslVersionTls12) return ErrorInvalidValue; + connection->minimumVersion = version; return 0; } diff --git a/core/libs/tests/GuestSsl.cpp b/core/libs/tests/GuestSsl.cpp index efda5060a0..4becc68d8c 100644 --- a/core/libs/tests/GuestSsl.cpp +++ b/core/libs/tests/GuestSsl.cpp @@ -18,6 +18,7 @@ int APS5_VABI sceSslSetVerifyCallback(int, int (APS5_VABI *)(int, unsigned int, int APS5_VABI sceSslConnect(int); int APS5_VABI sceSslRead(int, void*, std::size_t, int); int APS5_VABI sceSslWrite(int, void*, std::size_t, int); +int APS5_VABI sceSslClose(int); int APS5_VABI sceSslEnableVerifyOption(int, std::uint32_t); int APS5_VABI sceSslDisableVerifyOption(int, std::uint32_t); int APS5_VABI sceSslSetMinSslVersion(int, int); @@ -101,7 +102,11 @@ int main() { Require(list.certs == nullptr && list.num == 0); constexpr int invalidId = static_cast(0x8095F006); - Require(Throws([&] { sceSslCreateConnection(context, 0, nullptr); })); + constexpr int invalidValue = static_cast(0x8095F007); + constexpr int badLength = static_cast(0x80951773); + constexpr int badDescriptor = static_cast(0x80410109); + Require(sceSslInit_nid_postfix(0) == static_cast(0x80020016)); + Require(sceSslCreateConnection(context, 0, nullptr) == invalidValue); Require(sceSslCreateConnection(context + 1000, 0, "example.com") == invalidId); const int connection = sceSslCreateConnection(context, 0, "example.com"); @@ -113,39 +118,51 @@ int main() { Require(sceSslEnableVerifyOption(connection, serverVerify | cnCheck) == 0); Require(sceSslDisableVerifyOption(connection, cnCheck) == 0); Require(sceSslDisableVerifyOption(connection, serverVerify | cnCheck) == 0); - Require(sceSslSetMinSslVersion(connection, 0) == 0); + Require(sceSslEnableVerifyOption(connection, 0x40) == invalidValue); + Require(sceSslDisableVerifyOption(connection, 0x1000) == invalidValue); + Require(sceSslSetMinSslVersion(connection, 0) == invalidValue); + Require(sceSslSetMinSslVersion(connection, 1) == invalidValue); + Require(sceSslSetMinSslVersion(connection, 2) == 0); Require(sceSslSetMinSslVersion(connection, 5) == 0); - Require(Throws([&] { sceSslSetMinSslVersion(connection, -1); })); - Require(Throws([&] { sceSslSetMinSslVersion(connection, 6); })); + Require(sceSslSetMinSslVersion(connection, -1) == invalidValue); + Require(sceSslSetMinSslVersion(connection, 6) == invalidValue); Require(sceSslGetAlpnSelected(connection) == nullptr); Require(sceSslSetAlpn(connection, "http/1.1") == 0); - Require(sceSslSetAlpn(connection, "") == 0); + Require(sceSslSetAlpn(connection, "") == badLength); Require(Throws([&] { sceSslSetAlpn(connection, nullptr); })); Require(sceSslGetAlpnSelected(connection) == nullptr); char buffer[4]{}; - Require(Throws([&] { sceSslConnect(connection); })); - Require(Throws([&] { sceSslRead(connection, buffer, sizeof(buffer), 0); })); - Require(Throws([&] { sceSslWrite(connection, buffer, sizeof(buffer), 0); })); + Require(sceSslConnect(connection) == badDescriptor); + Require(sceSslRead(connection, buffer, sizeof(buffer), 0) == badDescriptor); + Require(sceSslWrite(connection, buffer, sizeof(buffer), 0) == badDescriptor); + Require(sceSslWrite(connection, buffer, sizeof(buffer), 1) == invalidValue); + Require(sceSslClose(connection) == 0); const int unknown = connection + 1000; - Require(Throws([&] { sceSslSetVerifyCallback(unknown, nullptr, nullptr); })); - Require(Throws([&] { sceSslEnableVerifyOption(unknown, serverVerify); })); - Require(Throws([&] { sceSslDisableVerifyOption(unknown, serverVerify); })); - Require(Throws([&] { sceSslSetMinSslVersion(unknown, 5); })); - Require(Throws([&] { sceSslSetAlpn(unknown, "http/1.1"); })); + Require(sceSslSetVerifyCallback(unknown, nullptr, nullptr) == invalidId); + Require(sceSslEnableVerifyOption(unknown, serverVerify) == invalidId); + Require(sceSslDisableVerifyOption(unknown, serverVerify) == invalidId); + Require(sceSslSetMinSslVersion(unknown, 5) == invalidId); + Require(sceSslSetAlpn(unknown, "http/1.1") == invalidId); Require(Throws([&] { sceSslGetAlpnSelected(unknown); })); - Require(Throws([&] { sceSslDeleteConnection(unknown); })); + Require(sceSslConnect(unknown) == invalidId); + Require(sceSslRead(unknown, buffer, sizeof(buffer), 0) == invalidId); + Require(sceSslWrite(unknown, buffer, sizeof(buffer), 0) == invalidId); + Require(sceSslClose(unknown) == invalidId); + Require(sceSslDeleteConnection(unknown) == invalidId); Require(sceSslTerm_nid_postfix(context) == 0); - Require(Throws([&] { sceSslSetVerifyCallback(connection, nullptr, nullptr); })); - Require(Throws([&] { sceSslEnableVerifyOption(connection, serverVerify); })); - Require(Throws([&] { sceSslDisableVerifyOption(connection, serverVerify); })); - Require(Throws([&] { sceSslSetMinSslVersion(connection, 5); })); - Require(Throws([&] { sceSslSetAlpn(connection, "http/1.1"); })); + Require(sceSslTerm_nid_postfix(context) == invalidId); + Require(sceSslSetVerifyCallback(connection, nullptr, nullptr) == invalidId); + Require(sceSslEnableVerifyOption(connection, serverVerify) == invalidId); + Require(sceSslDisableVerifyOption(connection, serverVerify) == invalidId); + Require(sceSslSetMinSslVersion(connection, 5) == invalidId); + Require(sceSslSetAlpn(connection, "http/1.1") == invalidId); Require(Throws([&] { sceSslGetAlpnSelected(connection); })); + Require(sceSslConnect(connection) == invalidId); + Require(sceSslClose(connection) == invalidId); Require(sceSslCreateConnection(context, 0, "example.com") == invalidId); - Require(sceSslDeleteConnection(connection) == 0); - Require(Throws([&] { sceSslDeleteConnection(connection); })); + Require(sceSslDeleteConnection(connection) == invalidId); Require(sceSslGetCaList(context, nullptr) == invalidArg); Require(sceSslFreeCaList(context, nullptr) == invalidArg); diff --git a/core/libs/tests/GuestSslLoad.cpp b/core/libs/tests/GuestSslLoad.cpp index 95e12521f7..28b894ddb4 100644 --- a/core/libs/tests/GuestSslLoad.cpp +++ b/core/libs/tests/GuestSslLoad.cpp @@ -39,6 +39,7 @@ int APS5_VABI sceSslGetPem(int, void*, char*, std::size_t*); namespace { constexpr int NotFound = static_cast(0x8095F004); +constexpr int InvalidId = static_cast(0x8095F006); void Check(bool condition, const char* message) { if (!condition) throw std::runtime_error(message); @@ -120,8 +121,8 @@ void ConnectionLifetime() { Check(sceSslSetVerifyCallback(retained, nullptr, nullptr) == 0, "credential unload invalidated the connection"); Check(sceSslFreeCaCerts(id, &bytes) == 0 && Stats(id).currentInuseSize == 0, "CA snapshot leaked with a retained connection"); Check(sceSslTerm_nid_postfix(id) == 0, "context termination failed with a retained connection"); - Throws([&] { sceSslSetVerifyCallback(retained, nullptr, nullptr); }); - Check(sceSslDeleteConnection(retained) == 0, "terminated context's connection cannot be released"); + Check(sceSslSetVerifyCallback(retained, nullptr, nullptr) == InvalidId, "terminated context's connection did not report an invalid id"); + Check(sceSslDeleteConnection(retained) == InvalidId, "terminated context's connection was still deletable"); Check(sceSslGetCaCerts(other, &empty) == NotFound, "unrelated context was invalidated"); Check(sceSslTerm_nid_postfix(other) == 0, "unrelated context termination failed"); } @@ -188,7 +189,7 @@ void CertificateReaders() { unsigned char serialData[8]{}; std::size_t serialLen = sizeof(serialData); Check(sceSslGetSerialNumber(id, cert, serialData, &serialLen) == 0, "get serial failed"); - Check(serialLen == 3, "serial length mismatch"); + Check(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(0x8095177a), "null serial length accepted"); Throws([&] { sceSslGetSerialNumber(id, nullptr, serialData, &serialLen); }); @@ -217,11 +218,11 @@ void CertificateReaders() { char val[64]{}; std::size_t vlen = 0; Check(sceSslGetNameEntryInfo(id, subject, 0, oid, sizeof(oid), val, sizeof(val), &vlen) == 0, "entry 0 info failed"); - Check(std::strcmp(oid, "C") == 0 && std::strcmp(val, "GB") == 0 && vlen == 2, "entry 0 mismatch"); + Check(std::strcmp(oid, "countryName") == 0 && std::strcmp(val, "GB") == 0 && vlen == 2, "entry 0 mismatch"); Check(sceSslGetNameEntryInfo(id, subject, 1, oid, sizeof(oid), val, sizeof(val), &vlen) == 0, "entry 1 info failed"); - Check(std::strcmp(oid, "O") == 0 && std::strcmp(val, "AnyPS5 Tests") == 0 && vlen == 12, "entry 1 mismatch"); + Check(std::strcmp(oid, "organizationName") == 0 && std::strcmp(val, "AnyPS5 Tests") == 0 && vlen == 12, "entry 1 mismatch"); Check(sceSslGetNameEntryInfo(id, subject, 2, oid, sizeof(oid), val, sizeof(val), &vlen) == 0, "entry 2 info failed"); - Check(std::strcmp(oid, "CN") == 0 && std::strcmp(val, "AnyPS5 fixture root") == 0 && vlen == 19, "entry 2 mismatch"); + Check(std::strcmp(oid, "commonName") == 0 && std::strcmp(val, "AnyPS5 fixture root") == 0 && vlen == 19, "entry 2 mismatch"); char smallVal[4]{}; std::size_t fullLen = 0; @@ -296,7 +297,7 @@ void CertificateReaders() { Throws([&] { sceSslGetPem(id, nullptr, pemBuf.data(), &writtenLen); }); Throws([&] { sceSslGetPem(id, fake, pemBuf.data(), &writtenLen); }); - Throws([&] { sceSslClose(1); }); + Check(sceSslClose(1) == InvalidId, "closing an unknown connection did not report an invalid id"); Check(sceSslFreeCaList(id, &handles) == 0, "free ca list failed"); Check(sceSslTerm_nid_postfix(id) == 0, "term failed"); diff --git a/core/libs/tests/GuestSslPs5Readers.cpp b/core/libs/tests/GuestSslPs5Readers.cpp index 7f7bf6e909..caffe1cf3e 100644 --- a/core/libs/tests/GuestSslPs5Readers.cpp +++ b/core/libs/tests/GuestSslPs5Readers.cpp @@ -66,10 +66,10 @@ void Readers() { Throws([&] { sceSslGetSubjectName(other, certificate); }); Throws([&] { sceSslGetNameEntryCount(other, subject); }); Throws([&] { sceSslFreeSslCertName(other, subject); }); - std::array oid{}, value{}; + std::array 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"); - Check(std::strcmp(oid.data(), "O") == 0 && std::strcmp(value.data(), "AnyPS5 Tests") == 0 && length == 12, "name output or full-width length changed"); + Check(std::strcmp(oid.data(), "organizationName") == 0 && std::strcmp(value.data(), "AnyPS5 Tests") == 0 && length == 12, "name output or full-width length changed"); length = SIZE_MAX; Check(sceSslGetPem(context, certificate, nullptr, &length) == 0 && length == std::strlen(RootPem) + 1, "PS5 PEM size query failed"); diff --git a/core/libs/tests/GuestSslTransport.cpp b/core/libs/tests/GuestSslTransport.cpp index 9b5bb60bc5..93fcb1b901 100644 --- a/core/libs/tests/GuestSslTransport.cpp +++ b/core/libs/tests/GuestSslTransport.cpp @@ -63,14 +63,22 @@ int APS5_VABI sceNetSocketClose(int); namespace { constexpr int ParallelConnections = 4; +constexpr int ErrorInvalidId = static_cast(0x8095f006); +constexpr int ErrorInvalidValue = static_cast(0x8095f007); constexpr int ErrorCnCheck = static_cast(0x8095f00b); constexpr int ErrorUnknownCa = static_cast(0x8095f00c); +constexpr int ErrorNotAfterCheck = static_cast(0x8095f00d); +constexpr int ErrorNotBeforeCheck = static_cast(0x8095f00e); +constexpr int ErrorFatalAlert = static_cast(0x8095f010); constexpr int ErrorWantPollIn = static_cast(0x8095f013); constexpr int ErrorWantPollOut = static_cast(0x8095f014); constexpr int ErrorCallbackCheck = static_cast(0x8095f0fd); constexpr int ErrorSocketClosed = static_cast(0x8095170d); -constexpr int ErrorReadTimeout = static_cast(0x80951710); -constexpr std::uint32_t ServerVerify = 0x1, CnCheck = 0x4; +constexpr int ErrorNetBadDescriptor = static_cast(0x80410109); +constexpr int ErrorNetPipe = static_cast(0x80410120); +constexpr int ErrorNetReset = static_cast(0x80410136); +constexpr int ErrorBadLength = static_cast(0x80951773); +constexpr std::uint32_t ServerVerify = 0x1, CnCheck = 0x4, NotAfterCheck = 0x8, NotBeforeCheck = 0x10, KnownCaCheck = 0x20; constexpr int VersionTls12 = 5; constexpr int GuestSocketLevel = 0xffff; constexpr int GuestNonBlocking = 0x1200; @@ -78,7 +86,8 @@ constexpr int GuestReceiveTimeout = 0x1006; constexpr int WaitRounds = 2000; const char* ServerProtocols[] = {"http/1.1", nullptr}; -enum class Serve { Close, Exchange, Drop, Reset }; +enum class Serve { Close, Exchange, Drop, Reset, Alert }; +enum class Leaf { Valid, Expired, Future }; struct GuestTimeval { std::int64_t seconds; @@ -106,18 +115,20 @@ void Settle() { std::this_thread::sleep_for(std::chrono::milliseconds(5)); } struct CallbackRecord { int context; int result; - int connection = 0; + int id = 0; unsigned int error = 0; int count = 0; + int calls = 0; void* leaf = nullptr; std::string commonName; }; -int APS5_VABI RecordingCallback(int connection, unsigned int error, void** certificates, int count, void* argument) { +int APS5_VABI RecordingCallback(int id, unsigned int error, void** certificates, int count, void* argument) { auto& record = *static_cast(argument); - record.connection = connection; + record.id = id; record.error = error; record.count = count; + ++record.calls; if (count > 0 && certificates && certificates[0]) { record.leaf = certificates[0]; void* name = sceSslGetSubjectName(record.context, record.leaf); @@ -140,9 +151,13 @@ struct Server { mbedtls_entropy_context entropy; mbedtls_ctr_drbg_context drbg; mbedtls_x509_crt certificate; + mbedtls_x509_crt expiredCertificate; + mbedtls_x509_crt futureCertificate; mbedtls_x509_crt authority; mbedtls_pk_context key; - mbedtls_ssl_config config; + mbedtls_pk_context expiredKey; + mbedtls_pk_context futureKey; + mbedtls_ssl_config configs[3]; std::uint16_t port = 0; std::string failure; @@ -151,9 +166,13 @@ struct Server { mbedtls_entropy_init(&entropy); mbedtls_ctr_drbg_init(&drbg); mbedtls_x509_crt_init(&certificate); + mbedtls_x509_crt_init(&expiredCertificate); + mbedtls_x509_crt_init(&futureCertificate); mbedtls_x509_crt_init(&authority); mbedtls_pk_init(&key); - mbedtls_ssl_config_init(&config); + mbedtls_pk_init(&expiredKey); + mbedtls_pk_init(&futureKey); + for (auto& config : configs) mbedtls_ssl_config_init(&config); Require(mbedtls_ctr_drbg_seed(&drbg, mbedtls_entropy_func, &entropy, nullptr, 0) == 0, "seed server random generator"); Require(mbedtls_x509_crt_parse(&certificate, reinterpret_cast(ServerPem), sizeof(ServerPem)) == 0, "parse server certificate"); @@ -161,13 +180,26 @@ struct Server { "parse client authority"); Require(mbedtls_pk_parse_key(&key, reinterpret_cast(ServerKey), sizeof(ServerKey), nullptr, 0, mbedtls_ctr_drbg_random, &drbg) == 0, "parse server key"); - Require(mbedtls_ssl_config_defaults(&config, MBEDTLS_SSL_IS_SERVER, MBEDTLS_SSL_TRANSPORT_STREAM, MBEDTLS_SSL_PRESET_DEFAULT) == 0, - "configure server"); - mbedtls_ssl_conf_rng(&config, mbedtls_ctr_drbg_random, &drbg); - Require(mbedtls_ssl_conf_own_cert(&config, &certificate, &key) == 0, "set server certificate"); - mbedtls_ssl_conf_authmode(&config, MBEDTLS_SSL_VERIFY_REQUIRED); - mbedtls_ssl_conf_ca_chain(&config, &authority, nullptr); - Require(mbedtls_ssl_conf_alpn_protocols(&config, ServerProtocols) == 0, "set server protocols"); + Require(mbedtls_x509_crt_parse(&expiredCertificate, reinterpret_cast(ExpiredServerPem), sizeof(ExpiredServerPem)) == 0, + "parse expired server certificate"); + Require(mbedtls_pk_parse_key(&expiredKey, reinterpret_cast(ExpiredServerKey), sizeof(ExpiredServerKey), nullptr, 0, + mbedtls_ctr_drbg_random, &drbg) == 0, "parse expired server key"); + Require(mbedtls_x509_crt_parse(&futureCertificate, reinterpret_cast(FutureServerPem), sizeof(FutureServerPem)) == 0, + "parse future server certificate"); + Require(mbedtls_pk_parse_key(&futureKey, reinterpret_cast(FutureServerKey), sizeof(FutureServerKey), nullptr, 0, + mbedtls_ctr_drbg_random, &drbg) == 0, "parse future server key"); + mbedtls_x509_crt* leaves[] = {&certificate, &expiredCertificate, &futureCertificate}; + mbedtls_pk_context* keys[] = {&key, &expiredKey, &futureKey}; + for (int index = 0; index < 3; ++index) { + auto& config = configs[index]; + Require(mbedtls_ssl_config_defaults(&config, MBEDTLS_SSL_IS_SERVER, MBEDTLS_SSL_TRANSPORT_STREAM, MBEDTLS_SSL_PRESET_DEFAULT) == 0, + "configure server"); + mbedtls_ssl_conf_rng(&config, mbedtls_ctr_drbg_random, &drbg); + Require(mbedtls_ssl_conf_own_cert(&config, leaves[index], keys[index]) == 0, "set server certificate"); + mbedtls_ssl_conf_authmode(&config, MBEDTLS_SSL_VERIFY_REQUIRED); + mbedtls_ssl_conf_ca_chain(&config, &authority, nullptr); + Require(mbedtls_ssl_conf_alpn_protocols(&config, ServerProtocols) == 0, "set server protocols"); + } Require(mbedtls_net_bind(&listener, "127.0.0.1", "0", MBEDTLS_NET_PROTO_TCP) == 0, "bind loopback listener"); sockaddr_in address{}; #ifdef _WIN32 @@ -180,16 +212,21 @@ struct Server { } ~Server() { - mbedtls_ssl_config_free(&config); + for (auto& config : configs) mbedtls_ssl_config_free(&config); + mbedtls_pk_free(&futureKey); + mbedtls_pk_free(&expiredKey); mbedtls_pk_free(&key); mbedtls_x509_crt_free(&authority); + mbedtls_x509_crt_free(&futureCertificate); + mbedtls_x509_crt_free(&expiredCertificate); mbedtls_x509_crt_free(&certificate); mbedtls_ctr_drbg_free(&drbg); mbedtls_entropy_free(&entropy); mbedtls_net_free(&listener); } - void Serve(Serve mode, bool requireClientCertificate = true) { + void Serve(Serve mode, bool requireClientCertificate = true, Leaf leaf = Leaf::Valid) { + auto& config = configs[static_cast(leaf)]; mbedtls_ssl_conf_authmode(&config, requireClientCertificate ? MBEDTLS_SSL_VERIFY_REQUIRED : MBEDTLS_SSL_VERIFY_NONE); mbedtls_net_context client; mbedtls_net_init(&client); @@ -197,6 +234,8 @@ struct Server { mbedtls_ssl_init(&ssl); if (mbedtls_net_accept(&listener, &client, nullptr, 0, nullptr) != 0 || mbedtls_ssl_setup(&ssl, &config) != 0) { failure = "server accept or setup failed"; + } else if (mode == Serve::Alert) { + Alert(client); } else { mbedtls_ssl_set_bio(&ssl, &client, mbedtls_net_send, mbedtls_net_recv, nullptr); const int handshake = mbedtls_ssl_handshake(&ssl); @@ -209,6 +248,16 @@ struct Server { mbedtls_net_free(&client); } + void Alert(mbedtls_net_context& client) { + std::array hello{}; + if (mbedtls_net_recv(&client, hello.data(), hello.size()) <= 0) { + failure = "server did not receive the client hello before the alert"; + return; + } + const unsigned char alert[] = {0x15, 0x03, 0x03, 0x00, 0x02, 0x02, 0x28}; + if (mbedtls_net_send(&client, alert, sizeof(alert)) != sizeof(alert)) failure = "server could not send the alert"; + } + void Reset(mbedtls_ssl_context& ssl, mbedtls_net_context& client) { unsigned char signal = 0; if (mbedtls_ssl_read(&ssl, &signal, 1) != 1) { @@ -291,14 +340,21 @@ int main(int argc, char** argv) { server.Serve(Serve::Close); server.Serve(Serve::Close); server.Serve(Serve::Close); + server.Serve(Serve::Close); server.Serve(Serve::Exchange); server.Serve(Serve::Exchange); server.Serve(Serve::Drop); server.Serve(Serve::Reset); + server.Serve(Serve::Alert); + server.Serve(Serve::Close, true, Leaf::Expired); + server.Serve(Serve::Close, true, Leaf::Expired); + server.Serve(Serve::Close, true, Leaf::Future); + server.Serve(Serve::Close, true, Leaf::Future); if (sockets.net) server.Serve(Serve::Exchange); for (int index = 0; index < ParallelConnections; ++index) server.Serve(Serve::Exchange); }); + Require(sceSslInit_nid_postfix(0) == static_cast(0x80020016), "an empty pool was accepted"); const int empty = sceSslInit_nid_postfix(256 * 1024); const int untrustedSocket = sockets.Open(server.port); const int untrusted = sceSslCreateConnection(empty, untrustedSocket, "localhost"); @@ -308,6 +364,12 @@ int main(int argc, char** argv) { Require(Throws([&] { sceSslRead(untrusted, scratch.data(), 1, 0); }), "a connection that failed verification was readable"); Require(sceSslClose(untrusted) == 0, "close a connection that failed verification"); Require(sceSslDeleteConnection(untrusted) == 0 && sockets.Close(untrustedSocket) == 0, "release the untrusted connection"); + Require(sceSslDeleteConnection(untrusted) == ErrorInvalidId && sceSslClose(untrusted) == ErrorInvalidId + && sceSslConnect(untrusted) == ErrorInvalidId && sceSslRead(untrusted, scratch.data(), 1, 0) == ErrorInvalidId + && sceSslWrite(untrusted, scratch.data(), 1, 0) == ErrorInvalidId && sceSslSetAlpn(untrusted, "h2") == ErrorInvalidId + && sceSslEnableVerifyOption(untrusted, CnCheck) == ErrorInvalidId && sceSslDisableVerifyOption(untrusted, CnCheck) == ErrorInvalidId + && sceSslSetMinSslVersion(untrusted, VersionTls12) == ErrorInvalidId && sceSslSetVerifyCallback(untrusted, nullptr, nullptr) == ErrorInvalidId, + "a deleted connection did not report an invalid id"); const int unverifiedSocket = sockets.Open(server.port); const int unverified = sceSslCreateConnection(empty, unverifiedSocket, "localhost"); @@ -322,13 +384,14 @@ int main(int argc, char** argv) { const int accepted = sceSslCreateConnection(empty, acceptedSocket, "localhost"); Require(sceSslSetVerifyCallback(accepted, RecordingCallback, &accepting) == 0, "set the accepting verify callback"); Require(sceSslConnect(accepted) == 0, "a verify callback that returns 0 did not accept the connection"); - Require(accepting.connection == accepted && accepting.error == static_cast(ErrorUnknownCa), - "the verify callback did not receive the connection and its verification result"); - Require(accepting.count == 1 && accepting.commonName == "CN=localhost", "the verify callback did not receive the peer chain"); + Require(accepting.id == empty && accepting.error == KnownCaCheck, "the verify callback did not receive the context id and the failed option"); + Require(accepting.count == 1 && accepting.commonName == "commonName=localhost", "the verify callback did not receive the peer chain"); + Require(sceSslConnect(accepted) == 0 && accepting.calls == 1, "a second connect after an accepted callback ran the callback again"); Require(sceSslRead(accepted, scratch.data(), scratch.size(), 0) == 0, "an accepted connection was not readable"); Require(sceSslDeleteConnection(accepted) == 0 && sockets.Close(acceptedSocket) == 0, "release the accepted connection"); Require(Throws([&] { sceSslGetSubjectName(empty, accepting.leaf); }), "a peer certificate handle outlived its connection"); Require(sceSslTerm_nid_postfix(empty) == 0, "release the context without CA"); + Require(sceSslTerm_nid_postfix(empty) == ErrorInvalidId, "a second context termination did not report an invalid id"); const int context = sceSslInit_nid_postfix(1024 * 1024); std::string root = RootPem; @@ -340,8 +403,9 @@ int main(int argc, char** argv) { Require(sceSslLoadCert(context, 1, &authorities, &certificate, &key) == 0, "load the fixture root and client certificate"); const auto loaded = InUse(context); + Require(sceSslCreateConnection(context, 1000, nullptr) == ErrorInvalidValue, "a null host was accepted"); const int closed = sceSslCreateConnection(context, 1000, "localhost"); - Require(Throws([&] { sceSslConnect(closed); }), "a handshake over a closed descriptor was attempted"); + Require(sceSslConnect(closed) == ErrorNetBadDescriptor, "a handshake over a closed descriptor did not report the bad descriptor"); Require(sceSslClose(closed) == 0, "close a connection that never connected"); Require(sceSslDeleteConnection(closed) == 0, "delete connection over a closed descriptor"); @@ -363,32 +427,45 @@ int main(int argc, char** argv) { const int rejected = sceSslCreateConnection(context, rejectedSocket, "localhost"); Require(sceSslSetVerifyCallback(rejected, RecordingCallback, &rejecting) == 0, "set the rejecting verify callback"); Require(sceSslConnect(rejected) == ErrorCallbackCheck, "a negative verify callback result did not fail the connect"); - Require(rejecting.error == 0 && rejecting.count == 1, "the verify callback did not see a passed chain"); - Require(sceSslConnect(rejected) == ErrorCallbackCheck, "a second connect did not repeat the callback result"); - Require(Throws([&] { sceSslRead(rejected, scratch.data(), 1, 0); }), "a rejected connection was readable"); + Require(rejecting.error == 0 && rejecting.count == 1 && rejecting.id == context, "the verify callback did not see a passed chain"); + Require(sceSslConnect(rejected) == ErrorCallbackCheck && rejecting.calls == 2, "a second connect did not run the callback again"); + Require(sceSslRead(rejected, scratch.data(), 1, 0) == ErrorCallbackCheck && rejecting.calls == 3, "a read after the rejection did not run the callback again"); + Require(sceSslWrite(rejected, scratch.data(), 1, 0) == ErrorCallbackCheck && rejecting.calls == 4, "a write after the rejection did not run the callback again"); + Require(sceSslClose(rejected) == 0, "close the rejected connection"); Require(sceSslDeleteConnection(rejected) == 0 && sockets.Close(rejectedSocket) == 0, "release the rejected connection"); + const int earlySocket = sockets.Open(server.port); + const int early = sceSslCreateConnection(context, earlySocket, "localhost"); + Require(sceSslClose(early) == 0, "close before the handshake"); + Require(sceSslRead(early, scratch.data(), scratch.size(), 0) == 0, "a read before the handshake did not run it and reach the close notify"); + Require(sceSslConnect(early) == 0, "a connect after the read-driven handshake did not return 0"); + Require(sceSslDeleteConnection(early) == 0 && sockets.Close(earlySocket) == 0, "release the early connection"); + const int descriptor = sockets.Open(server.port); const int connection = sceSslCreateConnection(context, descriptor, "localhost"); std::array buffer{}; + Require(sceSslSetMinSslVersion(connection, 0) == ErrorInvalidValue && sceSslSetMinSslVersion(connection, 6) == ErrorInvalidValue + && sceSslSetMinSslVersion(connection, 2) == 0, "the accepted minimum versions changed"); + Require(sceSslEnableVerifyOption(connection, 0x40) == ErrorInvalidValue && sceSslDisableVerifyOption(connection, 0x1000) == ErrorInvalidValue + && sceSslEnableVerifyOption(connection, 0x3f) == 0, "the verify option mask changed"); + Require(sceSslSetAlpn(connection, "") == ErrorBadLength, "an empty ALPN list was accepted"); Require(sceSslSetMinSslVersion(connection, VersionTls12) == 0 && sceSslSetAlpn(connection, "h2,http/1.1") == 0, "set version and ALPN"); Require(sceSslGetAlpnSelected(connection) == nullptr, "a protocol was selected before the handshake"); - Require(Throws([&] { sceSslRead(connection, buffer.data(), buffer.size(), 0); }), "read before the handshake"); - Require(Throws([&] { sceSslWrite(connection, buffer.data(), buffer.size(), 0); }), "write before the handshake"); Require(sceSslConnect(connection) == 0, "handshake with the loopback server"); Require(InUse(context) > loaded, "the connection is not charged to the context pool"); Require(sceSslConnect(connection) == 0, "a second connect after success did not return the same result"); const char* selected = sceSslGetAlpnSelected(connection); Require(selected && std::strcmp(selected, "http/1.1") == 0, "the negotiated ALPN protocol is not reported"); - Require(Throws([&] { sceSslWrite(connection, buffer.data(), 1, 1); }), "unknown write flags were accepted"); - Require(Throws([&] { sceSslRead(connection, buffer.data(), 1, 3); }), "peek was accepted"); + 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(), 0) == 4 && std::memcmp(buffer.data(), "pong", 4) == 0, "read the reply"); + 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"); Require(sceSslClose(connection) == 0, "close the connection"); - Require(Throws([&] { sceSslRead(connection, buffer.data(), buffer.size(), 0); }), "a closed connection was readable"); - Require(Throws([&] { sceSslConnect(connection); }), "a closed connection reconnected"); + Require(sceSslRead(connection, buffer.data(), buffer.size(), 0) == 0, "a read after the close did not return 0"); + Require(sceSslConnect(connection) == 0, "a connect after the close did not return 0"); + Require(selected && std::strcmp(sceSslGetAlpnSelected(connection), "http/1.1") == 0, "the ALPN protocol was forgotten after the close"); Require(sceSslClose(connection) == 0, "a second close failed"); Require(sceSslDeleteConnection(connection) == 0, "delete the connection"); Require(InUse(context) == loaded, "the connection's pool memory was not returned"); @@ -404,9 +481,10 @@ int main(int argc, char** argv) { Require(++rounds < WaitRounds, "a non-blocking handshake did not finish"); } Require(sceSslRead(polling, buffer.data(), buffer.size(), 0) == ErrorWantPollIn, "a non-blocking read with nothing to read did not ask to poll"); + Require(sceSslRead(polling, buffer.data(), buffer.size(), 1) == ErrorWantPollIn, "a non-blocking partial read with nothing to read did not ask to poll"); Require(sceSslWrite(polling, request, 4, 0) == 4, "write the non-blocking request"); int received; - for (rounds = 0; (received = sceSslRead(polling, buffer.data(), buffer.size(), 0)) == ErrorWantPollIn; Settle()) + for (rounds = 0; (received = sceSslRead(polling, buffer.data(), buffer.size(), 1)) == ErrorWantPollIn; Settle()) Require(++rounds < WaitRounds, "the non-blocking reply did not arrive"); Require(received == 4 && std::memcmp(buffer.data(), "pong", 4) == 0, "read the non-blocking reply"); for (rounds = 0; (received = sceSslRead(polling, buffer.data(), buffer.size(), 0)) == ErrorWantPollIn; Settle()) @@ -419,22 +497,58 @@ int main(int argc, char** argv) { const int dropped = sceSslCreateConnection(context, droppedSocket, "localhost"); Require(sceSslConnect(dropped) == 0, "handshake before the server drops the connection"); Require(sceSslRead(dropped, buffer.data(), buffer.size(), 0) == 0, "a stream that ended without close notify did not end the read"); + Require(sceSslRead(dropped, buffer.data(), buffer.size(), 1) == 0, "a second read after the drop did not return 0"); + Require(sceSslClose(dropped) == 0, "close after the drop"); Require(sceSslDeleteConnection(dropped) == 0 && sockets.Close(droppedSocket) == 0, "release the dropped connection"); const int resetSocket = sockets.Open(server.port); const int reset = sceSslCreateConnection(context, resetSocket, "localhost"); Require(sceSslConnect(reset) == 0, "handshake before the server resets the connection"); Require(sceSslWrite(reset, request, 1, 0) == 1, "write the byte the server resets after"); - Require(sceSslRead(reset, buffer.data(), buffer.size(), 0) == ErrorSocketClosed, "a reset did not report the closed socket"); + Require(sceSslRead(reset, buffer.data(), buffer.size(), 0) == ErrorNetReset, "a reset did not report the libSceNet reset code"); + Require(sceSslRead(reset, buffer.data(), buffer.size(), 1) == ErrorNetReset, "a second read after the reset changed the code"); + Require(sceSslWrite(reset, request, 4, 0) == ErrorNetPipe, "a write after the reset did not report the broken pipe"); + Require(sceSslConnect(reset) == 0, "a connect after the reset did not return 0"); + Require(sceSslClose(reset) == ErrorNetReset && sceSslClose(reset) == ErrorNetReset, "a close after the reset did not report the reset twice"); Require(sceSslDeleteConnection(reset) == 0 && sockets.Close(resetSocket) == 0, "release the reset connection"); + const int alertedSocket = sockets.Open(server.port); + const int alerted = sceSslCreateConnection(context, alertedSocket, "localhost"); + Require(sceSslConnect(alerted) == ErrorFatalAlert, "a fatal alert during the handshake did not report it"); + Require(sceSslConnect(alerted) == 0, "a connect after the fatal alert did not return 0"); + Require(sceSslRead(alerted, buffer.data(), buffer.size(), 0) == 0, "a read after the fatal alert did not return 0"); + Require(sceSslWrite(alerted, request, 4, 0) == ErrorSocketClosed, "a write after the fatal alert did not report the closed socket"); + Require(sceSslClose(alerted) == 0 && sceSslClose(alerted) == 0, "close after the fatal alert"); + Require(sceSslDeleteConnection(alerted) == 0 && sockets.Close(alertedSocket) == 0, "release the alerted connection"); + + const int expiredSocket = sockets.Open(server.port); + const int expired = sceSslCreateConnection(context, expiredSocket, "localhost"); + Require(sceSslConnect(expired) == 0, "an expired server certificate failed with the initial verify options"); + Require(sceSslRead(expired, buffer.data(), buffer.size(), 0) == 0, "close notify did not end the expired stream"); + Require(sceSslDeleteConnection(expired) == 0 && sockets.Close(expiredSocket) == 0, "release the expired connection"); + const int expiredCheckedSocket = sockets.Open(server.port); + const int expiredChecked = sceSslCreateConnection(context, expiredCheckedSocket, "localhost"); + Require(sceSslEnableVerifyOption(expiredChecked, NotAfterCheck) == 0, "enable the expiry check"); + Require(sceSslConnect(expiredChecked) == ErrorNotAfterCheck, "an expired server certificate passed the enabled expiry check"); + Require(sceSslDeleteConnection(expiredChecked) == 0 && sockets.Close(expiredCheckedSocket) == 0, "release the checked expired connection"); + const int futureSocket = sockets.Open(server.port); + const int future = sceSslCreateConnection(context, futureSocket, "localhost"); + Require(sceSslEnableVerifyOption(future, NotAfterCheck) == 0, "enable the expiry check on the future connection"); + Require(sceSslConnect(future) == 0, "a not yet valid server certificate failed without the start check"); + Require(sceSslDeleteConnection(future) == 0 && sockets.Close(futureSocket) == 0, "release the future connection"); + const int futureCheckedSocket = sockets.Open(server.port); + const int futureChecked = sceSslCreateConnection(context, futureCheckedSocket, "localhost"); + Require(sceSslEnableVerifyOption(futureChecked, NotBeforeCheck) == 0, "enable the start check"); + Require(sceSslConnect(futureChecked) == ErrorNotBeforeCheck, "a not yet valid server certificate passed the enabled start check"); + Require(sceSslDeleteConnection(futureChecked) == 0 && sockets.Close(futureCheckedSocket) == 0, "release the checked future connection"); + if (sockets.net) { const int timedSocket = sockets.Open(server.port); const GuestTimeval timeout{0, 200000}; Require(sockets.Set(timedSocket, GuestReceiveTimeout, &timeout, sizeof(timeout)) == 0, "set the receive timeout"); const int timed = sceSslCreateConnection(context, timedSocket, "localhost"); Require(sceSslConnect(timed) == 0, "handshake over the socket with a receive timeout"); - Require(sceSslRead(timed, buffer.data(), buffer.size(), 0) == ErrorReadTimeout, "an expired receive timeout did not report the timeout"); + Require(sceSslRead(timed, buffer.data(), buffer.size(), 0) == ErrorWantPollIn, "an expired receive timeout did not ask to poll"); Require(sceSslWrite(timed, request, 4, 0) == 4, "write the request after a timeout"); Require(sceSslRead(timed, buffer.data(), buffer.size(), 0) == 4 && std::memcmp(buffer.data(), "pong", 4) == 0, "read the reply after a timeout"); Require(sceSslRead(timed, buffer.data(), buffer.size(), 0) == 0, "close notify did not end the timed stream"); diff --git a/core/libs/tests/SslCertificateFixture.hpp b/core/libs/tests/SslCertificateFixture.hpp index bc5637db37..0c86103e10 100644 --- a/core/libs/tests/SslCertificateFixture.hpp +++ b/core/libs/tests/SslCertificateFixture.hpp @@ -91,3 +91,41 @@ g+KCUQ46CTPPM2bFyypaJZ3ZJq2hRANCAASOWLxgY2XcCm5WcZKzafvQHUuJX1Kt bMsNIbMUyke+Sp9+LrTGU52se7eyb3nq5AVuGiBdmoW5QkBMFx00hkyB -----END PRIVATE KEY----- )FIXTURE"; +inline constexpr char ExpiredServerPem[] = R"FIXTURE(-----BEGIN CERTIFICATE----- +MIIBxjCCAWygAwIBAgICEAMwCgYIKoZIzj0EAwIwQjELMAkGA1UEBhMCR0IxFTAT +BgNVBAoMDEFueVBTNSBUZXN0czEcMBoGA1UEAwwTQW55UFM1IGZpeHR1cmUgcm9v +dDAeFw0yNTA5MTQwMDAwMDBaFw0yNjA5MTkwMDAwMDBaMDgxCzAJBgNVBAYTAkdC +MRUwEwYDVQQKDAxBbnlQUzUgVGVzdHMxEjAQBgNVBAMMCWxvY2FsaG9zdDBZMBMG +ByqGSM49AgEGCCqGSM49AwEHA0IABDvmM3W4JGu6KyrQLp/qYYvTX4MBUwWGIP9+ +VcRgPCO7jwtb0Aoaf6tSaG1Reax8J2rvTRRDRibTwZhg/6a7LKSjXDBaMBQGA1Ud +EQQNMAuCCWxvY2FsaG9zdDAMBgNVHRMBAf8EAjAAMBMGA1UdJQQMMAoGCCsGAQUF +BwMBMB8GA1UdIwQYMBaAFFZolbTcGqYVMtT6BD37g9yqaAzcMAoGCCqGSM49BAMC +A0gAMEUCIFOAMPrcpBrmsJrbXaWzyCDe1UweszENyWAW4pZqs/hmAiEA/vehYXPI +exj4IIRU7Zt80wBXEjJNBk8KXmhO/UvPh/A= +-----END CERTIFICATE----- +)FIXTURE"; +inline constexpr char ExpiredServerKey[] = R"FIXTURE(-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg4TeyA0YK3Bq0Rt2l +On7kv/wcghGMcWvA4f5xVsGJUcahRANCAAQ75jN1uCRruisq0C6f6mGL01+DAVMF +hiD/flXEYDwju48LW9AKGn+rUmhtUXmsfCdq700UQ0Ym08GYYP+muyyk +-----END PRIVATE KEY----- +)FIXTURE"; +inline constexpr char FutureServerPem[] = R"FIXTURE(-----BEGIN CERTIFICATE----- +MIIBxzCCAWygAwIBAgICEAQwCgYIKoZIzj0EAwIwQjELMAkGA1UEBhMCR0IxFTAT +BgNVBAoMDEFueVBTNSBUZXN0czEcMBoGA1UEAwwTQW55UFM1IGZpeHR1cmUgcm9v +dDAeFw0zNzExMjMwMDAwMDBaFw0zODEyMjcwMDAwMDBaMDgxCzAJBgNVBAYTAkdC +MRUwEwYDVQQKDAxBbnlQUzUgVGVzdHMxEjAQBgNVBAMMCWxvY2FsaG9zdDBZMBMG +ByqGSM49AgEGCCqGSM49AwEHA0IABP1SqW8j3y1k1uDBPD2cFW3vDPOb09zGYm6g +mUpt2UXlx6WTsiohSzv9emkUSe/YJez3mLoEB4c/ohL0rNhdGTqjXDBaMBQGA1Ud +EQQNMAuCCWxvY2FsaG9zdDAMBgNVHRMBAf8EAjAAMBMGA1UdJQQMMAoGCCsGAQUF +BwMBMB8GA1UdIwQYMBaAFFZolbTcGqYVMtT6BD37g9yqaAzcMAoGCCqGSM49BAMC +A0kAMEYCIQDG0pZG6c9W7tVIA9AoG8YVyQbBZ0krnjw4SlAfy1I3swIhAMIqV5JD +tjTwhuPqzDgkmObSwb6GGD1abVRLCDJJMjsZ +-----END CERTIFICATE----- +)FIXTURE"; +inline constexpr char FutureServerKey[] = R"FIXTURE(-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgecaFHjyMpE80Z1ja +I7gmZUa8yQKqbgS9PZq3PQPD3h+hRANCAAT9UqlvI98tZNbgwTw9nBVt7wzzm9Pc +xmJuoJlKbdlF5celk7IqIUs7/XppFEnv2CXs95i6BAeHP6IS9KzYXRk6 +-----END PRIVATE KEY----- +)FIXTURE"; diff --git a/docs/dev/debt/libSceSsl.md b/docs/dev/debt/libSceSsl.md index 8b1772a8a4..597a2246f7 100644 --- a/docs/dev/debt/libSceSsl.md +++ b/docs/dev/debt/libSceSsl.md @@ -2,9 +2,9 @@ ### Unknown function info -- [sceSslCreateConnection, sceSslDeleteConnection, sceSslSetVerifyCallback, sceSslConnect, sceSslRead, sceSslWrite](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - imported by `ffmpeg.prx`. Signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L195-L252), which also says the socket stays open after `sceSslDeleteConnection` and that `sceSslRead` returns 0 at the end of the stream. An unknown context returns `SSL_ERROR_INVALID_ID` (0x8095F006), as `sceSslGetMemoryPoolStats` does on the console and as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/4865dd92ca6b6bb907748f125883093af038f53b/src/libs/network.cpp#L2688-L2720) and [sharpemu](https://github.com/sharpemu/sharpemu/blob/29021b5a71bbc80f262d887e6ba18b3a72963f49/src/SharpEmu.Libs/Network/SslExports.cs#L41-L54) return for other context calls; for an unknown connection, a connection whose context was terminated and a null host the console's codes are unknown, so these throw. `sceSslConnect` runs a TLS 1.2 client handshake with Mbed TLS 3.6.7 over the host socket that the guest number maps to in the libkernel or libSceNet socket table, and `sceSslRead` and `sceSslWrite` carry application data over it. TLS 1.3 is compiled out: SharpProspero's `SceSslVersion` ends at TLS 1.2, and Mbed TLS 1.3 needs PSA crypto state that is process-wide rather than allocated from a context's pool. Connection state is allocated from the context's pool and returned to it by `sceSslDeleteConnection`; a connection keeps the pool alive after `sceSslTerm`. The server's chain is checked against the CAs loaded with `sceSslLoadCert`, its validity dates and the host name the connection was created with, which is also sent as SNI, and `sceSslConnect` returns the first failure the connection's verify options select (the verify option entry below lists the bits and codes); 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). A client certificate loaded with `sceSslLoadCert` is sent when the server asks for one. The result of the first completed `sceSslConnect` is final for the connection: a failure sends close_notify, later `sceSslConnect` calls return the same code, and `sceSslRead` and `sceSslWrite` throw. With a verify callback set it is called once after the handshake, outside the connection's lock, with the connection id, that code (0 when the chain passed), handles to a copy of the peer chain 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 chain length and the user argument; a negative return fails the connect with `SSL_ERROR_APPLICATION_VERIFICATION_CALLBACK_CHECK` (0x8095F0FD) and any other return accepts the connection whatever the code was, following SharpProspero's "a negative return fails the handshake" ([https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L195-L252](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L195-L252)). Whether the id is the context or the connection, whether the second argument is that code or the bits of the failed checks, the chain element type, when the console runs the callback and whether a non-negative return overrides a failed check are unknown. A `sceSslConnect` from inside the callback, or from another thread while it runs, throws. 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. A blocking libSceNet socket whose `SO_RCVTIMEO` expires returns `SSL_ERROR_READ_TIMEOUT` (0x80951710) from `sceSslConnect` and `sceSslRead`, and the call can be repeated; one whose `SO_SNDTIMEO` expires throws, as no write timeout code is known. libkernel keeps its timeouts above the host socket, so they do not bound SSL reads and writes on a libkernel socket. A reset (`ECONNRESET`, `EPIPE`, `WSAECONNRESET`, `WSAECONNABORTED`, `WSAENETRESET`) returns `SSL_ERROR_SOCKET_CLOSED` (0x8095170D), as does a transport that ends without close_notify during the handshake, and a fatal alert returns `SSL_ERROR_FATAL_ALERT` (0x8095F010); when one of these ends the handshake the session is dropped and the next `sceSslConnect` starts over. Which situation returns which code is inferred: [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/4865dd92ca6b6bb907748f125883093af038f53b/src/libs/errno.h#L512-L546) gives the names and values but not when the console returns them, so `WANT_POLLIN` and `WANT_POLLOUT` rather than `SSL_ERROR_EAGAIN` (0x8095F00F) for a socket that would block, `SOCKET_CLOSED` rather than `SSL_ERROR_EOF` (0x8095177C) for a handshake cut short, `READ_TIMEOUT` for an expired receive timeout, `FATAL_ALERT` for a fatal alert, `INVALID_CERT` (0x8095F00A) for a verification failure no option names, and a repeated `sceSslConnect` returning the first result, 0 included, rather than an error are all unverified on a console. Other handshake and socket failures, a read or write before the handshake, after a failed verification or after `sceSslClose`, a descriptor that is not a socket, a null buffer, write flags other than 0 and read flags other than 0 and `MsgFlagPartialRead` (1) throw, as their console codes are unknown; `MsgFlagPeek` (3) is not implemented. `sceSslRead` returns what one Mbed TLS read delivers, up to `len`, with flags 0 and with `MsgFlagPartialRead` alike; SharpProspero describes `sceSslRead` as reading up to `len` but `MsgFlagPartialRead` as returning before the buffer is full, so whether flags 0 waits to fill the buffer is unknown. It returns 0 after the server's close_notify and also when the transport ends without one, as Mbed TLS reports both as the end of the stream. `sceSslWrite` sends all of `len`, capped at `INT_MAX`, and returns the count. A connection serializes its reads and writes, so a read blocked in one thread holds back a write on the same connection from another. `sceSslDeleteConnection` sends no close_notify. The socket is not checked when the connection is created, `sceSslTerm` leaves the context's connections to `sceSslDeleteConnection`, and `sceSslSetVerifyCallback` accepts any callback, null included, and returns 0. None of this is verified on the console. `SSL_ERROR_BEFORE_INIT` (0x8095F001) is never returned, as the library keeps no state apart from its contexts -- [sceSslClose](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - takes the connection id, as in [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L195-L252). It sends close_notify once if the handshake completed, marks the connection closed so later `sceSslConnect`, `sceSslRead` and `sceSslWrite` throw, and leaves the socket to the title; on a connection that never connected, already failed verification or was already closed it returns 0. Whether the console closes the socket, what it returns for a connection that was never connected, and what happens when a non-blocking socket cannot take the close_notify at once (it is not retried) are 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, extracting OID attribute short names (or numeric strings) and value strings. An unknown context, unknown or expired certificate handle or invalid certificate name handle throws; null output pointers, unsupported hash types or out-of-range entry indices return `SSL_ERROR_INVALID_ARG` (0x8095177a). The error codes, the zero-capacity length query, the NUL-terminated truncation of name entry strings with the full value length in `valueLen`, and the lifetime of name handles (released only by `sceSslFreeSslCertName` or when the context is terminated) have not been measured on a PS5 +- [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 - [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 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 all five checks (0x3D), so a connection whose title never calls these fails closed; what the console starts with is unknown, and the one source, the HTTPS layer of [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/4865dd92ca6b6bb907748f125883093af038f53b/src/libs/network.cpp#L191), starts its libSceHttp connections with `ssl_flags` 0xA7 (`SERVER_VERIFY`, `CLIENT_VERIFY`, `CN_CHECK`, `KNOWN_CA_CHECK`, SNI), evidence that verification is on by default rather than for these bits. A check that is disabled is not reported, whatever the chain. `sceSslSetMinSslVersion` accepts the `SceSslVersion` values SharpProspero lists, 0 (none) to 5 (TLS 1.2), and throws for others; whether the console accepts a TLS 1.3 value is unknown. 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. `sceSslSetAlpn` stores the string up to its terminator and throws for a null list; it is split on commas into the protocols offered in the handshake, as HTTP libraries write ALPN lists, and whether the console expects that or the TLS wire format is unknown. `sceSslGetAlpnSelected` returns the protocol the server selected, 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 throw, as for `sceSslSetVerifyCallback` +- [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 - [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 ae662c873daf9920e98d840f4e85a64dbcbb686a Mon Sep 17 00:00:00 2001 From: ionut27001 <194206737+ionut27001@users.noreply.github.com> Date: Sun, 18 Oct 2026 03:52:36 +0300 Subject: [PATCH 059/269] fix(libc): parse NaN payloads in strtod as measured on a PS5 In a payload process on a PS5 (firmware 12.70), the strtod and wcstod of libSceLibcInternal.sprx keep the payload of a NAN(...) sequence, read as FreeBSD 11's gdtoa hexnan reads it, under the quiet bit and with the sign. strtof, wcstof, strtold and wcstold return the positive default quiet NaN whatever the sign and the sequence, and consume a sequence of letters, digits and underscores up to its closing parenthesis. Here every NaN lost its payload, and strtof and strtold kept the sign. The model reproduces the 64 NaN inputs that p11, p14, p27 and p33 measured, including blanks and other characters inside the parentheses and unterminated sequences, and the test now pins them. It also pins two p71 cases measured through strtod: nan(1 2), and "nan( " followed by NUL and ")". In hexnan, a blank after a payload digit, while a 32-bit word of the payload is still free, starts a blank skip that has no NUL check, so when only blanks follow up to the terminating NUL the console reads past it (p71); that case throws here. A blank right after "nan(" is skipped with a NUL check, and the console stopped at the NUL there. AI-assisted: yes Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libc/src/Strings.cpp | 145 +++++++++++++++++++++++---- core/libs/tests/GuestMath.cpp | 132 ++++++++++++++++++++---- core/libs/tests/GuestWideStrings.cpp | 6 +- docs/dev/debt/libc.md | 2 +- 4 files changed, 243 insertions(+), 42 deletions(-) diff --git a/core/libs/prx/libc/src/Strings.cpp b/core/libs/prx/libc/src/Strings.cpp index 26f4d08bc3..8e1393c7e2 100644 --- a/core/libs/prx/libc/src/Strings.cpp +++ b/core/libs/prx/libc/src/Strings.cpp @@ -7,7 +7,9 @@ #include #include #include +#include #include +#include #include "prx/libc/include/General.hpp" #include "prx/libc/include/SonyCrt.hpp" @@ -37,31 +39,130 @@ std::string AsciiPrefix(const char16_t* text) { return prefix; } -bool IsNanSubject(const char* text) { +struct NanSubject { + const char* tail; + bool negative; +}; + +NanSubject FindNanSubject(const char* text) { while (*text == ' ' || (*text >= '\t' && *text <= '\r')) ++text; - if (*text == '+' || *text == '-') ++text; - return *text == 'n' || *text == 'N'; + bool negative = false; + if (*text == '+' || *text == '-') negative = *text++ == '-'; + if ((text[0] | 0x20) == 'n' && (text[1] | 0x20) == 'a' && (text[2] | 0x20) == 'n') return {text + 3, negative}; + return {nullptr, negative}; } -double QuietNan(double value, const char* text) { - const auto bits = std::bit_cast(value); - if ((bits & 0x7ff0000000000000u) != 0x7ff0000000000000u || !IsNanSubject(text)) return value; - return std::bit_cast((bits & 0x8000000000000000u) | 0x7ff8000000000000u); +unsigned HexNanDigit(unsigned char c) { + if (c >= '0' && c <= '9') return c - '0' + 0x10; + if (c >= 'a' && c <= 'f') return c - 'a' + 0x1a; + if (c >= 'A' && c <= 'F') return c - 'A' + 0x1a; + return 0; } -float QuietNan(float value, const char* text) { - const auto bits = std::bit_cast(value); - if ((bits & 0x7f800000u) != 0x7f800000u || !IsNanSubject(text)) return value; - return std::bit_cast((bits & 0x80000000u) | 0x7fc00000u); +void ShiftNanWords(std::uint32_t* words, int from, int to, int digits) { + const int shift = (8 - digits) * 4; + const int carry = 32 - shift; + int index = from; + do { + words[index] |= words[index + 1] << carry; + words[index + 1] >>= shift; + } while (++index < to); } -long double QuietNan(long double value, const char* text) { - std::uint16_t exponent; - std::memcpy(&exponent, reinterpret_cast(&value) + sizeof(std::uint64_t), sizeof(exponent)); - if ((exponent & 0x7fffu) != 0x7fffu || !IsNanSubject(text)) return value; - const std::uint64_t significand = 0xc000000000000000u; - std::memcpy(&value, &significand, sizeof(significand)); - return value; +std::uint64_t HexNanPayload(const char* open, const char*& end) { + std::uint32_t words[2] = {}; + constexpr int high = 1; + int top = high; + int group = high; + int digits = 0; + int groupStart = 0; + int inWord = 0; + const auto* s = reinterpret_cast(open); + const auto skipBlanksAndPrefix = [&s](bool inside) { + while (s[1] != 0 && s[1] <= ' ') ++s; + if (inside && s[1] == 0) throw std::invalid_argument("strtod: blanks after a NAN payload digit reach the terminating NUL, where the console reads past it"); + if (s[1] == '0' && (s[2] == 'x' || s[2] == 'X') && s[3] > ' ') s += 2; + }; + skipBlanksAndPrefix(false); + for (unsigned char c; (c = *++s) != 0;) { + const unsigned digit = HexNanDigit(c); + if (digit == 0) { + if (c <= ' ') { + if (groupStart < digits) { + if (top < group && inWord < 8) ShiftNanWords(words, top, group, inWord); + if (top <= 0) { + inWord = 8; + continue; + } + groupStart = digits; + words[--top] = 0; + group = top; + inWord = 0; + } + skipBlanksAndPrefix(true); + continue; + } + if (c == ')' && digits != 0) { + end = reinterpret_cast(s + 1); + break; + } + for (; c != 0; c = *++s) { + if (c == ')') { + end = reinterpret_cast(s + 1); + break; + } + } + return 0; + } + ++digits; + if (++inWord > 8) { + if (top <= 0) continue; + inWord = 1; + words[--top] = 0; + } + words[top] = (words[top] << 4) | (digit & 0xf); + } + if (digits == 0) return 0; + if (top < group && inWord < 8) ShiftNanWords(words, top, group, inWord); + if (top > 0) { + int to = 0; + int from = top; + do words[to++] = words[from++]; + while (from <= high); + do words[to++] = 0; + while (to <= high); + } else { + words[high] &= 0xffffffffu >> 12; + } + if (words[0] == 0 && words[1] == 0) words[0] = 1; + return (static_cast(words[1]) << 32) | words[0]; +} + +const char* NanSequenceEnd(const char* tail) { + if (*tail != '(') return tail; + const char* cursor = tail + 1; + while ((*cursor >= '0' && *cursor <= '9') || ((*cursor | 0x20) >= 'a' && (*cursor | 0x20) <= 'z') || *cursor == '_') ++cursor; + return *cursor == ')' ? cursor + 1 : tail; +} + +template +TFloat NanValue(const NanSubject& subject, const char*& end) { + if constexpr (std::is_same_v) { + end = subject.tail; + const std::uint64_t payload = *subject.tail == '(' ? HexNanPayload(subject.tail, end) : 0; + return std::bit_cast((subject.negative ? 0x8000000000000000ull : 0) | 0x7ff8000000000000ull | payload); + } else if constexpr (std::is_same_v) { + end = NanSequenceEnd(subject.tail); + return std::bit_cast(0x7fc00000u); + } else { + end = NanSequenceEnd(subject.tail); + TFloat value{}; + const std::uint64_t significand = 0xc000000000000000u; + const std::uint16_t signExponent = 0x7fff; + std::memcpy(&value, &significand, sizeof(significand)); + std::memcpy(reinterpret_cast(&value) + sizeof(significand), &signExponent, sizeof(signExponent)); + return value; + } } template @@ -75,10 +176,16 @@ auto ParseAsciiPrefix(const char16_t* text, char16_t** end, TParse parse) { template TFloat ParseFloat(const char* text, char** end, TFloat (*parse)(const char*, char**)) { + if (const NanSubject subject = FindNanSubject(text); subject.tail != nullptr) { + const char* stop = subject.tail; + const TFloat value = NanValue(subject, stop); + if (end != nullptr) *end = const_cast(stop); + return value; + } char* parsedEnd = nullptr; const TFloat value = parse(text, &parsedEnd); if (end != nullptr) *end = parsedEnd; - return parsedEnd == text ? TFloat{0} : QuietNan(value, text); + return parsedEnd == text ? TFloat{0} : value; } } diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index 176ac81a25..064db8fdea 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -315,42 +315,136 @@ static bool ExtendedBits(long double value, std::uint64_t mantissa, std::uint16_ } static void CheckNanConversions() { - const struct { const char* text; std::size_t consumed; bool negative; } cases[] = { - {"nan(0x123)", 10, false}, {" -NaN(1)!", 8, true}, {"nan(0x800000)", 13, false}, - {"nan(0x10000000000000)", 21, false}, {"nan(0x8000000000000000)", 23, false}, + struct MeasuredNan { + const char* text; + std::uint64_t bits; + std::size_t used; + std::size_t sequenceUsed; + bool extendedMeasured; + bool wideDoubleMeasured; + bool wideOtherMeasured; + }; + static constexpr MeasuredNan cases[] = { + {"nan", 0x7ff8000000000000ull, 3, 3, true, true, true}, + {"NAN", 0x7ff8000000000000ull, 3, 3, false, false, false}, + {"nan()", 0x7ff8000000000000ull, 5, 5, false, false, false}, + {"nan(1)", 0x7ff8000000000001ull, 6, 6, true, true, true}, + {"nan(0x10)", 0x7ff8000000000010ull, 9, 9, false, false, false}, + {"nan(abc)", 0x7ff8000000000abcull, 8, 8, false, false, false}, + {"nan(", 0x7ff8000000000000ull, 3, 3, true, true, true}, + {"-nan", 0xfff8000000000000ull, 4, 4, true, true, true}, + {"+nan(5)", 0x7ff8000000000005ull, 7, 7, false, false, false}, + {"nan(10)", 0x7ff8000000000010ull, 7, 7, false, false, false}, + {"nan(xyz)", 0x7ff8000000000000ull, 8, 8, false, false, false}, + {"nan(_)", 0x7ff8000000000000ull, 6, 6, false, false, false}, + {"nan(12", 0x7ff8000000000012ull, 3, 3, false, false, false}, + {"-nan(5)", 0xfff8000000000005ull, 7, 7, false, false, false}, + {"nan(0x8000000000000)", 0x7ff8000000000000ull, 20, 20, false, false, false}, + {"nan(0x10000000000000)", 0x7ff8000000000001ull, 21, 21, true, true, false}, + {"nan(0x400000)", 0x7ff8000000400000ull, 13, 13, false, false, false}, + {"NaN(0X1F)", 0x7ff800000000001full, 9, 9, false, false, false}, + {"nan(0x)", 0x7ff8000000000000ull, 7, 7, false, false, false}, + {"nan(123abc)", 0x7ff8000000123abcull, 11, 11, true, true, false}, + {"nan(1x2)", 0x7ff8000000000000ull, 8, 8, true, true, false}, + {"nan(0x1g)", 0x7ff8000000000000ull, 9, 9, true, true, false}, + {"nan(g1)", 0x7ff8000000000000ull, 7, 7, true, true, false}, + {"nan(0x0x1)", 0x7ff8000000000000ull, 10, 10, true, true, false}, + {"nan(fffffffffffff)", 0x7fffffffffffffffull, 18, 18, true, true, false}, + {"nan(1fffffffffffff)", 0x7fffffffffffffffull, 19, 19, true, true, false}, + {"nan(10000000000000)", 0x7ff8000000000001ull, 19, 19, true, true, false}, + {"nan(20000000000000)", 0x7ff8000000000001ull, 19, 19, true, true, false}, + {"nan(0x20000000000000)", 0x7ff8000000000001ull, 21, 21, true, true, false}, + {"nan(ffffffffffffffff)", 0x7fffffffffffffffull, 21, 21, true, true, false}, + {"nan(10000000000000000)", 0x7ff8000000000001ull, 22, 22, true, true, false}, + {"nan(0X)", 0x7ff8000000000000ull, 7, 7, true, true, false}, + {"nan(-1)", 0x7ff8000000000000ull, 7, 3, true, true, false}, + {"nan(+1)", 0x7ff8000000000000ull, 7, 3, true, true, false}, + {"nan( 1)", 0x7ff8000000000001ull, 7, 3, true, true, false}, + {"nan(1 )", 0x7ff8000100000000ull, 7, 3, true, true, false}, + {"nan(0)", 0x7ff8000000000001ull, 6, 6, true, true, false}, + {"nan(00001)", 0x7ff8000000000001ull, 10, 10, true, true, false}, + {"nan(0x0001)", 0x7ff8000000000001ull, 11, 11, true, true, false}, + {"nan(8000000000000)", 0x7ff8000000000000ull, 18, 18, true, true, false}, + {"nan(7ffffffffffff)", 0x7fffffffffffffffull, 18, 18, true, true, false}, + {"nan(123", 0x7ff8000000000123ull, 3, 3, true, true, false}, + {"nan(1)x", 0x7ff8000000000001ull, 6, 6, true, true, false}, + {"-nan(1)", 0xfff8000000000001ull, 7, 7, true, true, false}, + {"nan(abcdefABCDEF)", 0x7ff8abcdefabcdefull, 17, 17, true, true, false}, + {"nan(0xABCDEF)", 0x7ff8000000abcdefull, 13, 13, true, true, false}, + {"nan(9)", 0x7ff8000000000009ull, 6, 6, true, true, false}, + {"nan(1_2)", 0x7ff8000000000000ull, 8, 8, true, true, false}, + {"nan(_1)", 0x7ff8000000000000ull, 7, 7, true, true, false}, + {"nan(1)(2)", 0x7ff8000000000001ull, 6, 6, true, true, false}, + {"+nan", 0x7ff8000000000000ull, 4, 4, true, true, true}, + {"NaN", 0x7ff8000000000000ull, 3, 3, true, true, true}, + {"-NAN(5)", 0xfff8000000000005ull, 7, 7, true, true, true}, + {"nan(5)", 0x7ff8000000000005ull, 6, 6, true, true, true}, + {"-nan(abc)", 0xfff8000000000abcull, 9, 9, true, true, true}, + {" -nan", 0xfff8000000000000ull, 6, 6, true, true, true}, + {"nan(12 x", 0x7ff8000000000000ull, 3, 3, true, true, true}, + {"nan(12x", 0x7ff8000000000000ull, 3, 3, true, true, true}, + {"nan(1", 0x7ff8000000000001ull, 3, 3, true, true, true}, + {"nan(a b)", 0x7ff8000a0000000bull, 8, 3, true, true, true}, + {"nan(ab)", 0x7ff80000000000abull, 7, 7, true, true, true}, + {"nanx", 0x7ff8000000000000ull, 3, 3, true, true, true}, + {"-nan()", 0xfff8000000000000ull, 6, 6, true, true, true}, + {"nan(0x8000000000001)", 0x7ff8000000000001ull, 20, 20, true, true, true}, }; bool correct = true; for (const auto& test : cases) { - const char* expectedEnd = test.text + test.consumed; - const std::uint64_t expectedDouble = test.negative ? 0xfff8000000000000ull : 0x7ff8000000000000ull; char* end = nullptr; const auto value = std::bit_cast(strtod_nid_postfix(test.text, &end)); - if (value != expectedDouble || end != expectedEnd) { + if (value != test.bits || end != test.text + test.used) { std::fprintf(stderr, "strtod(\"%s\") = 0x%016llx\n", test.text, static_cast(value)); correct = false; } - const auto ascii = std::bit_cast(atof_nid_postfix(test.text)); - if (ascii != expectedDouble) { - std::fprintf(stderr, "atof(\"%s\") = 0x%016llx\n", test.text, static_cast(ascii)); + if (std::bit_cast(atof_nid_postfix(test.text)) != test.bits) { + std::fprintf(stderr, "atof(\"%s\")\n", test.text); correct = false; } const auto single = std::bit_cast(strtof_nid_postfix(test.text, &end)); - if (single != (test.negative ? 0xffc00000u : 0x7fc00000u) || end != expectedEnd) { + if (single != 0x7fc00000u || end != test.text + test.sequenceUsed) { std::fprintf(stderr, "strtof(\"%s\") = 0x%08x\n", test.text, static_cast(single)); correct = false; } - const long double extended = strtold_nid_postfix(test.text, &end); - if (!ExtendedBits(extended, 0xc000000000000000ull, test.negative ? 0xffff : 0x7fff) || end != expectedEnd) { - std::uint64_t significand = 0; - std::uint16_t signExponent = 0; - std::memcpy(&significand, &extended, sizeof(significand)); - std::memcpy(&signExponent, reinterpret_cast(&extended) + sizeof(significand), sizeof(signExponent)); - std::fprintf(stderr, "strtold(\"%s\") = 0x%04x'%016llx\n", test.text, static_cast(signExponent), - static_cast(significand)); - correct = false; + if (test.extendedMeasured) { + const long double extended = strtold_nid_postfix(test.text, &end); + if (!ExtendedBits(extended, 0xc000000000000000ull, 0x7fff) || end != test.text + test.sequenceUsed) { + std::fprintf(stderr, "strtold(\"%s\")\n", test.text); + correct = false; + } + } + char16_t wide[64] = {}; + for (std::size_t i = 0; test.text[i] != 0; ++i) wide[i] = static_cast(test.text[i]); + char16_t* wideEnd = nullptr; + if (test.wideDoubleMeasured) { + const auto wideValue = std::bit_cast(wcstod_nid_postfix(wide, &wideEnd)); + if (wideValue != test.bits || wideEnd != wide + test.used) { + std::fprintf(stderr, "wcstod(\"%s\") = 0x%016llx\n", test.text, static_cast(wideValue)); + correct = false; + } + } + if (test.wideOtherMeasured) { + const auto wideSingle = std::bit_cast(wcstof_nid_postfix(wide, &wideEnd)); + const bool singleOk = wideSingle == 0x7fc00000u && wideEnd == wide + test.sequenceUsed; + const long double wideExtended = wcstold_nid_postfix(wide, &wideEnd); + if (!singleOk || !ExtendedBits(wideExtended, 0xc000000000000000ull, 0x7fff) || wideEnd != wide + test.sequenceUsed) { + std::fprintf(stderr, "wcstof/wcstold(\"%s\")\n", test.text); + correct = false; + } } } Require(correct); + for (const char* text : {"nan(1 ", "nan(12 \t"}) { + bool threw = false; + try { strtod_nid_postfix(text, nullptr); } catch (...) { threw = true; } + Require(threw); + } + struct PlainNan { const char* text; std::uint64_t bits; std::size_t used; }; + for (const PlainNan& plain : {PlainNan{"nan(1 2)", 0x7ff8000100000002ull, 8}, PlainNan{"nan( \0)", 0x7ff8000000000000ull, 3}}) { + char* end = nullptr; + Require(std::bit_cast(strtod_nid_postfix(plain.text, &end)) == plain.bits && end == plain.text + plain.used); + } } static void CheckHypotSpecialValue() { diff --git a/core/libs/tests/GuestWideStrings.cpp b/core/libs/tests/GuestWideStrings.cpp index abbba3196d..809d47fe3e 100644 --- a/core/libs/tests/GuestWideStrings.cpp +++ b/core/libs/tests/GuestWideStrings.cpp @@ -322,12 +322,12 @@ int main() { const char16_t* half = u"-1.5é"; require(wcstold_nid_postfix(half, &end) == -1.5L && end == half + 4); const char16_t* wideNan = u" -nan(0x123)é"; - require(std::bit_cast(wcstod_nid_postfix(wideNan, &end)) == 0xfff8000000000000u && end == wideNan + 12); - require(std::bit_cast(wcstof_nid_postfix(wideNan, &end)) == 0xffc00000u && end == wideNan + 12); + require(std::bit_cast(wcstod_nid_postfix(wideNan, &end)) == 0xfff8000000000123u && end == wideNan + 12); + require(std::bit_cast(wcstof_nid_postfix(wideNan, &end)) == 0x7fc00000u && end == wideNan + 12); const long double wideExtended = wcstold_nid_postfix(wideNan, &end); std::uint64_t significand; std::memcpy(&significand, &wideExtended, sizeof(significand)); - require(std::isnan(wideExtended) && std::signbit(wideExtended) && significand == 0xc000000000000000u && end == wideNan + 12); + require(std::isnan(wideExtended) && !std::signbit(wideExtended) && significand == 0xc000000000000000u && end == wideNan + 12); require(wcscoll_nid_postfix(u"a", u"b") < 0); require(wcscoll_nid_postfix(u"￿", u"a") > 0); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 9b31a4e2a0..35e52a3953 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -61,7 +61,7 @@ - [fgetln](../../../core/libs/prx/libc/src/StandardStreams.cpp) (libSceLibcInternal, implemented in libc) throws: in the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x5FE80) the export is a `hlt` instruction, which faults in user mode and ends the process - [Wide-character conversions](../../../core/libs/prx/libc/src/Multibyte.cpp) (libc) - The wide `wcsto*` functions parse the ASCII prefix of the guest string, as the C locale accepts only ASCII white space and digits. [`wcsrtombs`](../../../core/libs/prx/libc/src/Multibyte.cpp) uses the guest's single-byte C locale through `wcrtomb`; other encodings and shift states are not supported - [realpath](../../../core/libs/prx/libc/src/RealPath.cpp) (libc) follows FreeBSD 11's [`realpath.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libc/stdlib/realpath.c) except for symlinks. The console's guest filesystem has none, so a host symlink under the guest root is followed for existence and type but not expanded, and `..` drops the previous component lexically, as `Resolve` does for every guest path. Through a host link `linkdir -> sub/deep`, `linkdir/../x` gives `x` beside the link, not beside its target. -- [strtod, strtof, strtold](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - `NAN(n-char-sequence)` converts to the quiet NaN that `nan` and `nanf` return, negated after a minus sign, and ignores the sequence as they ignore their tag. This is inferred from C17 7.12.11.2p2, which makes `nan("n")` equivalent to `strtod("NAN(n)", NULL)`, and not observed on the console. No console `nanl` was observed, so `strtold` returns the x87 default quiet NaN (exponent `0x7fff`, significand `0xc000000000000000`) +- [strtod, strtof, strtold](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - `strtod` and `wcstod` (and `atof`, which calls `strtod` here) read a `NAN(...)` sequence as FreeBSD 11's gdtoa [`hexnan`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/contrib/gdtoa/hexnan.c) does: hexadecimal digits with an optional `0x`, blanks starting a new 32-bit word, the first 16 digits kept and the payload cut to 52 bits, a zero payload made 1, any other character giving the default NaN but consuming up to the closing parenthesis, and an unterminated sequence keeping its payload while consuming only `nan`. The payload goes under the quiet bit with the sign, as in gdtoa's [`strtod`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/contrib/gdtoa/strtod.c#L316-L328). `strtof`, `wcstof`, `strtold` and `wcstold` return the positive default quiet NaN (`0x7fc00000`; exponent `0x7fff`, significand `0xc000000000000000`) whatever the sign and the sequence, and consume a sequence of letters, digits and underscores up to its closing parenthesis. The console's `libSceLibcInternal.sprx`, called from a payload process on a PS5 (firmware 12.70), gives these results for the 64 NaN inputs tried through `strtod` and `strtof`, 50 of them also through `strtold` and `wcstod`, and 18 through `wcstof` and `wcstold`, and, through `strtod`, for `nan(1 2)` and for `nan( ` followed by NUL and `)`; that a title's own `libc.prx` does the same is assumed. In `hexnan`, a blank after a payload digit, while a 32-bit word of the payload is still free, starts a blank skip that has no NUL check, so when only blanks follow up to the terminating NUL the console's `strtod` reads past it: `nan(1 ` followed by NUL and `)` consumes the `)` after the NUL, so the result depends on the bytes after the string, and here that case throws. A blank right after `nan(` is skipped with a NUL check, and the console stopped at the NUL there - [remquol](../../../core/libs/prx/libc/src/MathExact.cpp) (libc) leaves `*quo` unchanged when the result is NaN; the `libc.prx` bundled with PPSA08427 writes a value that depends on earlier x87 state (`remquol(1, 0)` stored 0 on a first call and 4 on later ones) - [Long double math in MathExact.cpp](../../../core/libs/prx/libc/src/MathExact.cpp) (libc) was compared with the `libc.prx` bundled with PPSA08427 only for canonical 80-bit encodings; unnormals, pseudo-denormals and pseudo-infinities passed to arithmetic functions (not the classifiers) follow the host x87 instead of the console - [nan and nanf](../../../core/libs/prx/libc/src/Math.cpp) (libc) return fixed quiet NaN values (`0x7fc00000` and `0x7ff8000000000000`), ignoring the tag argument as observed in the console's `libc.prx` (both exports load a global constant via `mov rax,[rip+..]; vmovsd xmm0,[rax]; ret`); VLC (PPSA85300) stops on nanf at start-up after strsep. From 185fa968757cdb10a7978e5a1cdb62cadf45d7e1 Mon Sep 17 00:00:00 2001 From: jweidenh <38249793+jweidenh@users.noreply.github.com> Date: Thu, 15 Oct 2026 18:41:41 -0400 Subject: [PATCH 060/269] fix(relinker): handle overlapping relocation tables --- core/relinker/CMakeLists.txt | 3 + .../src/analysis/CodeInstructionCollector.cpp | 22 ++- .../relinker/src/guest/GuestImageReader.cpp | 29 ++- .../src/pipeline/RelinkerPipeline.cpp | 21 +- .../tests/test_overlapping_relocations.py | 184 ++++++++++++++++++ 5 files changed, 236 insertions(+), 23 deletions(-) create mode 100644 core/relinker/relinker/tests/test_overlapping_relocations.py diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 7570e3d0d2..e776d56e99 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -433,6 +433,9 @@ if(BUILD_TESTING) add_test(NAME guest_relative_section_symbols COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_relative_section_symbols.py $) set_tests_properties(guest_relative_section_symbols PROPERTIES TIMEOUT 20) + 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 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/analysis/CodeInstructionCollector.cpp b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp index 3d66046f1d..0dd71dcec0 100644 --- a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp +++ b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp @@ -195,13 +195,21 @@ std::set CodeInstructionCollector::Collect(const std::ve } CodePointers pointers; std::set importSlots; - const auto readRelocations = [&](std::uint64_t addressTag, std::uint64_t sceAddressTag, std::uint64_t sizeTag, std::uint64_t sceSizeTag) { + const auto relocationRange = [&](std::uint64_t addressTag, std::uint64_t sceAddressTag, std::uint64_t sizeTag, std::uint64_t sceSizeTag) -> std::pair { const auto size = value(sizeTag, sceSizeTag); - if (!size) return; + if (!size) return {0, 0}; if (*size % 24 != 0) throw Domain::RelinkerException("Code analysis: invalid relocation table size"); - if (*size == 0) return; - const auto start = table(addressTag, sceAddressTag, *size); - for (std::uint64_t offset = start; offset < start + *size; offset += 24) { + if (*size == 0) return {0, 0}; + return {table(addressTag, sceAddressTag, *size), *size}; + }; + const auto [relaOffset, relaSize] = relocationRange(7, 0x6100002f, 8, 0x61000031); + const auto [pltOffset, pltSize] = relocationRange(23, 0x61000029, 2, 0x6100002d); + if (relaSize != 0 && pltSize != 0 && relaOffset < pltOffset + pltSize && pltOffset < relaOffset + relaSize + && relaOffset % 24 != pltOffset % 24) + throw Domain::RelinkerException("Code analysis: unaligned overlapping relocation tables", pltOffset); + const auto readRelocations = [&](std::uint64_t start, std::uint64_t size, bool plt) { + for (std::uint64_t offset = start; offset < start + size; offset += 24) { + if (!plt && offset >= pltOffset && offset - pltOffset < pltSize) continue; const auto slot = Io::ReadU64(bytes, offset); const auto info = Io::ReadU64(bytes, offset + 8); const auto type = static_cast(info); @@ -228,8 +236,8 @@ std::set CodeInstructionCollector::Collect(const std::ve } } }; - readRelocations(7, 0x6100002f, 8, 0x61000031); - readRelocations(23, 0x61000029, 2, 0x6100002d); + readRelocations(relaOffset, relaSize, false); + readRelocations(pltOffset, pltSize, true); for (const auto& [addressTag, sizeTag] : {std::pair{25u, 27u}, std::pair{26u, 28u}, std::pair{32u, 33u}}) { const auto address = value(addressTag, 0x60000000 + addressTag); const auto size = value(sizeTag, 0x60000000 + sizeTag); diff --git a/core/relinker/relinker/src/guest/GuestImageReader.cpp b/core/relinker/relinker/src/guest/GuestImageReader.cpp index cf296e8b95..3e9a239afa 100644 --- a/core/relinker/relinker/src/guest/GuestImageReader.cpp +++ b/core/relinker/relinker/src/guest/GuestImageReader.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include namespace Relinker { @@ -213,15 +214,26 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector if (name.empty() || name.find_first_of("\\:$\r\n") != std::string::npos) fail("Invalid dependency: " + name); if (dependencies.insert(name).second) image.Dependencies.push_back(name); } - std::map relocationTargets; - const auto copyRelocations = [&](std::uint64_t addressTag, std::uint64_t sceAddressTag, std::uint64_t sizeTag, std::uint64_t sceSizeTag, std::vector& output, bool plt) { - if (!tags.contains(addressTag) && !tags.contains(sceAddressTag) && !tags.contains(sizeTag) && !tags.contains(sceSizeTag)) return; + const auto relocationRange = [&](std::uint64_t addressTag, std::uint64_t sceAddressTag, std::uint64_t sizeTag, std::uint64_t sceSizeTag) -> std::pair { + if (!tags.contains(addressTag) && !tags.contains(sceAddressTag) && !tags.contains(sizeTag) && !tags.contains(sceSizeTag)) return {0, 0}; const auto size = get(sizeTag, sceSizeTag); if (size % 24 != 0) fail("Invalid relocation table size"); - const auto offset = table(addressTag, sceAddressTag, size); - if (plt && size != 0 && get(20, 0x6100002b) != 7) fail("Unsupported PLT relocation format"); + return {table(addressTag, sceAddressTag, size), size}; + }; + if (tags.contains(7) || tags.contains(0x6100002f)) { + if (get(9, 0x61000033) != 24) fail("Invalid RELA entry size"); + } + const auto [relaOffset, relaSize] = relocationRange(7, 0x6100002f, 8, 0x61000031); + const auto [pltOffset, pltSize] = relocationRange(23, 0x61000029, 2, 0x6100002d); + if (pltSize != 0 && get(20, 0x6100002b) != 7) fail("Unsupported PLT relocation format"); + if (relaSize != 0 && pltSize != 0 && relaOffset < pltOffset + pltSize && pltOffset < relaOffset + relaSize + && relaOffset % 24 != pltOffset % 24) + fail("Unaligned overlapping relocation tables"); + std::map relocationTargets; + const auto copyRelocations = [&](std::uint64_t offset, std::uint64_t size, std::vector& output, bool plt) { output.reserve(size); for (std::uint64_t index = 0; index < size; index += 24) { + if (!plt && offset + index >= pltOffset && offset + index - pltOffset < pltSize) continue; const auto info = Io::ReadU64(bytes, offset + index + 8); const auto relocation = static_cast(info); if (relocation == 0) continue; @@ -256,11 +268,8 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector output.insert(output.end(), bytes.begin() + offset + index, bytes.begin() + offset + index + 24); } }; - if (tags.contains(7) || tags.contains(0x6100002f)) { - if (get(9, 0x61000033) != 24) fail("Invalid RELA entry size"); - } - copyRelocations(7, 0x6100002f, 8, 0x61000031, image.Dynamic.RelaData, false); - copyRelocations(23, 0x61000029, 2, 0x6100002d, image.Dynamic.RelaPltData, true); + copyRelocations(relaOffset, relaSize, image.Dynamic.RelaData, false); + copyRelocations(pltOffset, pltSize, image.Dynamic.RelaPltData, true); if (tags.contains(3) || tags.contains(0x61000027)) image.Got = get(3, 0x61000027); if (tags.contains(12)) image.Init = tags.at(12); if (tags.contains(13)) image.Fini = tags.at(13); diff --git a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp index 2bdbf7d841..f9a4309d03 100644 --- a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp +++ b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp @@ -256,6 +256,11 @@ RelinkResult RelinkerPipeline::Relink() { if (dynRelaOffset > raw.size() || dynRelaSize > raw.size() - dynRelaOffset) throw RelinkerException("Relocation table is out of bounds", dynRelaOffset); + if (dynRelaSize != 0 && dynJmpRelSize != 0 + && dynRelaOffset < dynJmpRelOffset + dynJmpRelSize && dynJmpRelOffset < dynRelaOffset + dynRelaSize + && dynRelaOffset % relaEntSize != dynJmpRelOffset % relaEntSize) + throw RelinkerException("Unaligned overlapping relocation tables", dynJmpRelOffset); + auto readCStr = [&](FileByteOffset strOff) -> std::string { if (strOff >= dynStrTabSize) throw RelinkerException("Dynamic string offset is outside DT_STRSZ", strOff); @@ -325,9 +330,11 @@ RelinkResult RelinkerPipeline::Relink() { return definedTlsSymbolValue(symIdx, pos).has_value(); }; - auto extractRela = [&](const FileByteOffset relaOff, const ByteCount relaSize) { + auto extractRela = [&](const FileByteOffset relaOff, const ByteCount relaSize, const bool plt) { for (ByteCount off = 0; relaSize >= relaEntSize && off <= relaSize - relaEntSize; off += relaEntSize) { const FileByteOffset pos = relaEntryPos(relaOff, off); + if (!plt && pos >= dynJmpRelOffset && pos - dynJmpRelOffset < dynJmpRelSize) + continue; std::uint64_t rOffset = 0, rInfo = 0; std::int64_t rAddend = 0; @@ -366,8 +373,8 @@ RelinkResult RelinkerPipeline::Relink() { } }; - extractRela(dynRelaOffset, dynRelaSize); - extractRela(dynJmpRelOffset, dynJmpRelSize); + extractRela(dynRelaOffset, dynRelaSize, false); + extractRela(dynJmpRelOffset, dynJmpRelSize, true); for (const auto& ref : nidRefs) validationPolicy->ValidateRelocationTypeSupported(ref.RelocationTypeValue, ref.RelocationTableOffset); @@ -494,9 +501,11 @@ RelinkResult RelinkerPipeline::Relink() { std::memcpy(buf.data() + pos + 16, &addend, 8); }; - auto extractLocal = [&](const FileByteOffset relaOff, const ByteCount relaSize) { + auto extractLocal = [&](const FileByteOffset relaOff, const ByteCount relaSize, const bool plt) { for (ByteCount off = 0; relaSize >= relaEntSize && off <= relaSize - relaEntSize; off += relaEntSize) { const FileByteOffset pos = relaEntryPos(relaOff, off); + if (!plt && pos >= dynJmpRelOffset && pos - dynJmpRelOffset < dynJmpRelSize) + continue; std::uint64_t rOffset = 0, rInfo = 0; std::int64_t rAddend = 0; std::memcpy(&rOffset, raw.data() + pos, 8); @@ -526,8 +535,8 @@ RelinkResult RelinkerPipeline::Relink() { } }; - extractLocal(dynRelaOffset, dynRelaSize); - extractLocal(dynJmpRelOffset, dynJmpRelSize); + extractLocal(dynRelaOffset, dynRelaSize, false); + extractLocal(dynJmpRelOffset, dynJmpRelSize, true); std::vector entries; entries.reserve(nidRefs.size()); diff --git a/core/relinker/relinker/tests/test_overlapping_relocations.py b/core/relinker/relinker/tests/test_overlapping_relocations.py new file mode 100644 index 0000000000..84633cb6b0 --- /dev/null +++ b/core/relinker/relinker/tests/test_overlapping_relocations.py @@ -0,0 +1,184 @@ +import json +import os +from pathlib import Path +import platform +import signal +import struct +import subprocess +import sys +import tempfile + +from test_dynamic_segment import fixture as executable_fixture +from test_guest_intel_trampolines import main_fixture, pe_sections +from test_guest_none_relocations import change_tag, module +from test_guest_symbol_names import dynamic_tags, file_offset +from test_linux_load_alignment import loads +from test_macos_output import MachO + + +SCE_TAGS = {2: 0x6100002D, 3: 0x61000027, 5: 0x61000035, 6: 0x61000039, + 7: 0x6100002F, 8: 0x61000031, 9: 0x61000033, 10: 0x61000037, + 11: 0x6100003B, 20: 0x6100002B, 23: 0x61000029} +TARGETS = (("linux", []), ("windows", ["--windows"]), ("macos", ["--macos"])) +LAYOUTS = ("disjoint", "alias", "suffix", "partial") + + +def relocation_layout(layout, guest): + address = 0x2400 if guest else 0x740 + local = (0x2500, 8, 0x1000) if guest else (0x308, 8, 0x210) + jump = (0x2500 if layout == "alias" else 0x2508, (1 << 32) | 7, 0) if guest else (0x300, (1 << 32) | 7, 0) + plt = [jump] + if layout == "alias": + return address, [jump], address, plt, [] + if layout == "partial": + plt.append((0x2510 if guest else 0x310, (1 << 32) | 7, 0)) + if layout == "disjoint": + return 0x2300 if guest else 0x700, [local], address, plt, [local] + return address - 24, [local, jump], address, plt, [local] + + +def fixture(layout, sce, guest, misaligned=False, conflicting=False): + rela_address, rela, plt_address, plt, local = relocation_layout(layout, guest) + if conflicting: + rela = [plt[0]] + if guest: + image = module(rela, plt, sce) + for tag, value in ((7, rela_address), (8, len(rela) * 24), + (23, plt_address + int(misaligned)), (2, len(plt) * 24)): + change_tag(image, SCE_TAGS[tag] if sce else tag, + value - 0x2200 if sce and tag in (7, 23) else value) + else: + image = executable_fixture() + strings = b"\0module.prx\0shared\0" + image[0x600:0x600 + len(strings)] = strings + struct.pack_into("> 32 for _, info, _ in actual) + return + pointers = local + plt if guest else local + if target == "windows": + assert data.startswith(b"MZ") + sections = pe_sections(data) + code = next(section for section in sections if section[0] == b".elf0") + writable = next(section for section in sections if section[0] == (b".elf1" if guest else b".elf0")) + header, = struct.unpack_from(" Date: Mon, 19 Oct 2026 02:24:36 -0500 Subject: [PATCH 061/269] feat(libSceVideoOut): answer the HMD2 bus open as the console does sceVideoOutOpen with bus type 0x20, the PS VR2 output PPSA05666 opens as SCE_VIDEO_OUT_BUS_TYPE_HMD2_MAIN after sceHmd2ReprojectionEnableVrMode, threw VIDEO_OUT_ERROR_INVALID_VALUE and aborted the title. The console returns that code to a process that is not a game, with the headset connected and worn or not, for users 255 and 0 and for a null or zeroed open parameter, so the bus is answered that way before the other argument checks and the title carries on with the error as its handle, as it does on the console without a headset. The debt entry records the sequence the title follows on that handle (RegisterBuffers2 with category 16 and 4000x2040 descriptors that point at Hmd2 per-eye textures, ConfigureOutput and IsOutputSupported with 0x2000F, a GPU wait on buffer 0), which a VR backend will have to model, and the test covers the bus and the HMD output modes 0x20001 and 0x2000C to 0x20010, which the console and this implementation answer with VIDEO_OUT_ERROR_UNKNOWN_OUTPUT_MODE on the TV bus. Co-Authored-By: Claude Fable 5.1 --- .../libs/prx/libSceVideoOut/include/VideoOutDriver.hpp | 1 + core/libs/prx/libSceVideoOut/src/Output.cpp | 1 + core/libs/tests/GuestVideoOutOutputMode.cpp | 10 ++++++++++ docs/dev/debt/libSceVideoOut.md | 1 + 4 files changed, 13 insertions(+) diff --git a/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp b/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp index 069a00c174..7c868f3ce2 100644 --- a/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp +++ b/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp @@ -44,6 +44,7 @@ static constexpr int VIDEO_OUT_ERROR_UNKNOWN_OUTPUT_MODE = -2144796642; static constexpr int VIDEO_OUT_BUS_TYPE_MAIN = 0; static constexpr int VIDEO_OUT_BUS_TYPE_OVERLAY = 1; static constexpr int VIDEO_OUT_BUS_TYPE_SUB = 2; +static constexpr int VIDEO_OUT_BUS_TYPE_HMD2_MAIN = 0x20; static constexpr uint32_t VIDEO_OUT_DEFAULT_WIDTH = 1920; static constexpr uint32_t VIDEO_OUT_DEFAULT_HEIGHT = 1080; diff --git a/core/libs/prx/libSceVideoOut/src/Output.cpp b/core/libs/prx/libSceVideoOut/src/Output.cpp index 79af66728a..24f9b6f15c 100644 --- a/core/libs/prx/libSceVideoOut/src/Output.cpp +++ b/core/libs/prx/libSceVideoOut/src/Output.cpp @@ -80,6 +80,7 @@ static void validateOpenParam(const void* param) { extern "C" { int APS5_VABI sceVideoOutOpen(int userId, int busType, int index, const void* param) try { + if (busType == VIDEO_OUT_BUS_TYPE_HMD2_MAIN) return VIDEO_OUT_ERROR_INVALID_VALUE; validateOpenParam(param); if (userId != 255 && userId != 0) { throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_VALUE"); diff --git a/core/libs/tests/GuestVideoOutOutputMode.cpp b/core/libs/tests/GuestVideoOutOutputMode.cpp index 4bed080a11..82ce4bc2fa 100644 --- a/core/libs/tests/GuestVideoOutOutputMode.cpp +++ b/core/libs/tests/GuestVideoOutOutputMode.cpp @@ -18,6 +18,7 @@ int APS5_VABI sceVideoOutConfigureOutput(int handle, std::uint64_t mode, const V static constexpr int SYSTEM_USER = 255; static constexpr int MAIN_BUS = 0; +static constexpr int HMD2_MAIN_BUS = 0x20; static constexpr int INVALID_VALUE = static_cast(0x80290001); static constexpr int INVALID_ADDRESS = static_cast(0x80290002); static constexpr int INVALID_HANDLE = static_cast(0x8029000B); @@ -38,6 +39,11 @@ int main() { for (std::size_t index = 0; index < raw.size(); ++index) Require(raw[index] == (index == 2 ? 0xFF : 0)); Require(sceVideoOutInitializeOutputOptions(nullptr) == INVALID_ADDRESS); + std::array zeroedOpenParam{}; + Require(sceVideoOutOpen(SYSTEM_USER, HMD2_MAIN_BUS, 0, nullptr) == INVALID_VALUE); + Require(sceVideoOutOpen(SYSTEM_USER, HMD2_MAIN_BUS, 0, zeroedOpenParam.data()) == INVALID_VALUE); + Require(sceVideoOutOpen(0, HMD2_MAIN_BUS, 0, nullptr) == INVALID_VALUE); + int handle = 0; try { for (int unopened : {-1, 0, 1, 0x7FFFFFFF}) { @@ -64,6 +70,10 @@ int main() { for (std::uint64_t mode : {0x0ull, 0x2ull, 0x3ull, 0x5ull, 0x6ull, 0x9ull, 0xAull, 0xBull, 0x14ull, 0x20ull, 0x3Full, 0x100ull, 0xD000000Aull, 0x8000000000000001ull, ~0ull}) { Require(sceVideoOutIsOutputSupported(handle, mode, &options, nullptr, 0) == UNKNOWN_OUTPUT_MODE); } + for (std::uint64_t mode : {0x20001ull, 0x2000Cull, 0x2000Dull, 0x2000Eull, 0x2000Full, 0x20010ull}) { + Require(sceVideoOutIsOutputSupported(handle, mode, nullptr, nullptr, 0) == UNKNOWN_OUTPUT_MODE); + Require(sceVideoOutConfigureOutput(handle, mode, nullptr, nullptr, 0) == UNKNOWN_OUTPUT_MODE); + } bool thrown = false; try { sceVideoOutIsOutputSupported(handle, 0x12, &options, nullptr, 0); diff --git a/docs/dev/debt/libSceVideoOut.md b/docs/dev/debt/libSceVideoOut.md index ae73a5efc5..023aa7c671 100644 --- a/docs/dev/debt/libSceVideoOut.md +++ b/docs/dev/debt/libSceVideoOut.md @@ -28,6 +28,7 @@ ### Functional - [sceVideoOutOpen](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - opening a bus that is already open returns `SCE_VIDEO_OUT_ERROR_RESOURCE_BUSY` (`0x80290009`), as [shadPS4's](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/videoout/driver.cpp#L53-L60) PS4 driver does for its main port; the PS5 result is unverified. PPSA99764 opens the main bus twice and carries on, presenting through the first handle +- [sceVideoOutOpen, bus 0x20](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - PPSA05666 opens the PS VR2 output as `sceVideoOutOpen(255, 0x20, 0, NULL)` after `sceHmd2ReprojectionEnableVrMode(0x2000F)`, and its own log calls the bus `SCE_VIDEO_OUT_BUS_TYPE_HMD2_MAIN`. The console answers `0x80290001` from a process that is not a game, with the headset connected and worn or not, for users 255 and 0 and for a null or zeroed open parameter, so that is returned here before the other argument checks; what a game gets with a headset attached is not measured. On the handle the title then calls `sceVideoOutRegisterBuffers2` with set 0, two buffers, a 4000x2040 attribute (tiling 0, pitch 0, pixel format `0x8100000022000000`), category 16 and a null option, where each `VideoOutBuffers.data` points at a 16-byte `{pointer, 2}` whose pointer is the structure it passed to `sceHmd2ReprojectionEnableMirroring`, holding per-eye 2000x2040 texture descriptors rather than frame memory; then `sceVideoOutConfigureOutput` and `sceVideoOutIsOutputSupported` with mode `0x2000F`, and the GPU waits on buffer 0 of that handle. None of that is modelled: the HMD output needs a VR backend. With the error code as its handle the title's next call, `sceVideoOutRegisterBuffers2`, throws here for the unknown handle; the console's answer to it is not measured. The TV bus answers `0x8029001E` to the HMD modes `0x20001` and `0x2000C` to `0x20010` on the console, as here - [Overlay output bus](../../../core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp) (libSceVideoOut) - flips on a port opened with `SCE_VIDEO_OUT_BUS_TYPE_OVERLAY` complete and signal their events like main-bus flips, but their buffers are not shown: the overlay is not composited over the main output. Presenting them as whole frames made the window alternate between the two buses - [sceVideoOutSubmitEopFlip](../../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) reserves a flip and returns an opaque DWORD token; presentation is driven by a matching graphics-queue `RELEASE_MEM` with `INT_SEL` 2 and that token in `INT_CTXID`. This five-argument ABI and packet use are inferred from PPSA21564's bundled AGC driver, whose original packet-emission routine is checked in an isolated host ABI probe. The console token encoding, output width beyond the observed DWORD, error policy and reservation capacity are unverified. Host tokens occupy `0x04000000..0x07ffffff`, never repeat within a process, and exhaustion throws. Closing cancels unsubmitted reservations; only live reservations match; consumed or cancelled context IDs execute as ordinary label writes without targeting a reopened port. Async-queue EOP flips throw. Buffer registration, flip modes, queue capacity and vsync presentation follow the existing host VideoOut model, not measured console behavior. - [Mouse capture and delta scaling](../../../core/libs/prx/libSceVideoOut/src/MouseInput.cpp) (libSceVideoOut) - the console's mouse reports relative motion only, so a host cursor can never coincide with the title's cursor; while a title has a mouse open and the window is focused the host cursor is hidden and captured (SDL relative mode, released on focus loss or while the pad's mouse-to-stick toggle is on). Motion deltas are scaled from the presented image's size in window units to the title's display resolution with sub-pixel carry; the console's deltas are counts, so a title that applies its own sensitivity moves its cursor that much faster than the host pointer would have. The scale takes SDL's relative motion in logical window units, the units of `SDL_GetWindowSize`; under Windows DPI scaling SDL may report it in physical pixels, which is unverified (no Windows run yet), and a drawable larger than the window does not change the scale. From d16e9beed97c085096d9d46c1e87dfea682171d0 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Mon, 19 Oct 2026 03:35:03 -0400 Subject: [PATCH 062/269] fix(ajm): keep batch handles within their creating context --- core/libs/prx/libSceAjm.native/src/Ajm.cpp | 16 ++++++----- core/libs/tests/GuestAjm.cpp | 32 ++++++++++++++++++++++ docs/dev/debt/libSceAjm.native.md | 2 ++ 3 files changed, 43 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceAjm.native/src/Ajm.cpp b/core/libs/prx/libSceAjm.native/src/Ajm.cpp index 29fe39434e..6692aedc7a 100644 --- a/core/libs/prx/libSceAjm.native/src/Ajm.cpp +++ b/core/libs/prx/libSceAjm.native/src/Ajm.cpp @@ -18,7 +18,6 @@ extern "C" { #include #include #include -#include #include #include #include @@ -183,7 +182,7 @@ std::atomic g_nextContext{1}; std::atomic g_nextInstance{1}; std::atomic g_nextBatch{1}; std::mutex& g_batchLock = *new std::mutex(); -std::set& g_batches = *new std::set(); +std::map& g_batches = *new std::map(); enum class JobKind : std::uint32_t { Initialize = 1, @@ -1767,7 +1766,7 @@ int APS5_VABI sceAjmBatchJobGetStatistics(AjmBatchInfo* info, float interval, vo } int APS5_VABI sceAjmBatchStart(uint32_t context, const AjmBatchInfo* info, int priority, AjmBatchError* error, uint32_t* batch) { - (void)context; + if (context == 0 || context >= g_nextContext.load(std::memory_order_relaxed)) return SCE_AJM_ERROR_INVALID_CONTEXT; (void)priority; if (!info || !batch) return SCE_AJM_ERROR_INVALID_PARAMETER; AJM_TRACE("[ajm] batch start: context %u, priority %d, %llu of %llu buffer bytes used\n", context, priority, static_cast(info->offset), static_cast(info->size)); @@ -1797,18 +1796,20 @@ int APS5_VABI sceAjmBatchStart(uint32_t context, const AjmBatchInfo* info, int p const auto id = g_nextBatch.fetch_add(1, std::memory_order_relaxed); { std::lock_guard lock(g_batchLock); - g_batches.insert(id); + g_batches.emplace(id, context); } *batch = id; return 0; } int APS5_VABI sceAjmBatchWait(uint32_t context, uint32_t batch, uint32_t timeout, AjmBatchError* error) { - (void)context; + if (context == 0 || context >= g_nextContext.load(std::memory_order_relaxed)) return SCE_AJM_ERROR_INVALID_CONTEXT; (void)timeout; { std::lock_guard lock(g_batchLock); - if (g_batches.erase(batch) == 0) return SCE_AJM_ERROR_INVALID_BATCH; + const auto found = g_batches.find(batch); + if (found == g_batches.end() || found->second != context) return SCE_AJM_ERROR_INVALID_BATCH; + g_batches.erase(found); } if (error) std::memset(error, 0, sizeof(*error)); return 0; @@ -1817,7 +1818,8 @@ int APS5_VABI sceAjmBatchWait(uint32_t context, uint32_t batch, uint32_t timeout int APS5_VABI sceAjmBatchCancel(uint32_t context, uint32_t batch) { if (context == 0 || context >= g_nextContext.load(std::memory_order_relaxed)) return SCE_AJM_ERROR_INVALID_CONTEXT; std::lock_guard lock(g_batchLock); - return g_batches.contains(batch) ? 0 : SCE_AJM_ERROR_INVALID_BATCH; + const auto found = g_batches.find(batch); + return found != g_batches.end() && found->second == context ? 0 : SCE_AJM_ERROR_INVALID_BATCH; } int APS5_VABI sceAjmBatchErrorDump(const AjmBatchInfo* info, AjmBatchError* error) { diff --git a/core/libs/tests/GuestAjm.cpp b/core/libs/tests/GuestAjm.cpp index 87d872eb74..e9f9573eb3 100644 --- a/core/libs/tests/GuestAjm.cpp +++ b/core/libs/tests/GuestAjm.cpp @@ -833,6 +833,37 @@ void TestBatchCancel(std::uint32_t context) { Require(sceAjmBatchCancel(context, id + 1) == invalidBatch); } +void TestBatchContextOwnership(std::uint32_t context) { + constexpr int invalidContext = static_cast(0x80930002); + constexpr int invalidBatch = static_cast(0x80930004); + std::uint32_t other = 0; + Require(sceAjmInitialize(0, &other) == 0 && other != context); + std::vector storage(256); + AjmBatchInfo info{}; + Require(sceAjmBatchInitialize(storage.data(), storage.size(), &info) == 0); + AjmBatchError error{}; + std::uint32_t first = 0; + std::uint32_t second = 0; + Require(sceAjmBatchStart(context, &info, 0, &error, &first) == 0); + Require(sceAjmBatchStart(other, &info, 0, &error, &second) == 0); + Require(sceAjmBatchCancel(other, first) == invalidBatch); + Require(sceAjmBatchWait(other, first, 0, &error) == invalidBatch); + Require(sceAjmBatchCancel(context, second) == invalidBatch); + Require(sceAjmBatchWait(context, second, 0, &error) == invalidBatch); + Require(sceAjmBatchWait(0, first, 0, &error) == invalidContext); + Require(sceAjmBatchWait(other + 1, first, 0, &error) == invalidContext); + Require(sceAjmBatchCancel(context, first) == 0); + Require(sceAjmBatchWait(context, first, 0, &error) == 0); + Require(sceAjmBatchWait(other, second, 0, &error) == 0); + Require(sceAjmBatchWait(context, first, 0, &error) == invalidBatch); + Require(sceAjmBatchWait(other, second, 0, &error) == invalidBatch); + std::uint32_t untouched = 0xfeedu; + Require(sceAjmBatchStart(0, &info, 0, &error, &untouched) == invalidContext); + Require(sceAjmBatchStart(other + 1, &info, 0, &error, &untouched) == invalidContext); + Require(untouched == 0xfeedu); + Require(sceAjmFinalize(other) == 0); +} + constexpr std::uint64_t CONTROL_RESET = 1ull << 13; constexpr std::uint64_t CONTROL_INITIALIZE = 1ull << 14; constexpr std::uint64_t SIDEBAND_GAPLESS_DECODE = 1ull << 45; @@ -1822,6 +1853,7 @@ int main() { TestBatchWaitRelease(context); TestInstanceHandleLifetime(context); TestBatchCancel(context); + TestBatchContextOwnership(context); TestControlAt9(context); TestMultichannelAt9(context); TestControlMp3(context); diff --git a/docs/dev/debt/libSceAjm.native.md b/docs/dev/debt/libSceAjm.native.md index bcdfde9b1e..35826e4638 100644 --- a/docs/dev/debt/libSceAjm.native.md +++ b/docs/dev/debt/libSceAjm.native.md @@ -18,6 +18,8 @@ ### Functional +- [AJM batch contexts](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native): start, wait and cancel reject context IDs not allocated by initialization; batches belong to their creating context, as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0ebea15c24253b60f514fc37a348dabc8d0bcb90/src/core/libraries/ajm/ajm.cpp). This is not verified on PS5. Finalization still does not invalidate contexts or release their instances and outstanding batches; jobs execute synchronously and cancellation does not interrupt them. + - [sceAjmDecMp3ParseFrame](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) throws when the original file length is requested (`parse_ofl` non-zero) for a layer other than III, for a CRC-protected frame, or for a VBRI header outside MPEG-1 stereo: shadPS4 looks for the tag after the CRC and after the side information, while LAME and FFmpeg place it without the CRC and VBRI always at byte 36. - [ATRAC9 gapless segments](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - a run job stops after the gapless total of output samples counted from the start of the segment, leaving the rest of its input unconsumed, and the next run starts the next segment with the decoder as it is and without a second skip. FMOD in PPSA29141 sets a total shorter than a sound's frames and reads the frames after it on later runs; with the output held at the total its stream reader waited for them forever and the last buffer repeated. The console's handling of the frames after the total is not documented; a reset (`SetGaplessDecode` with the reset flag, `ClearContext`, initialization) starts a new segment. An ATRAC9 run job without `RUN_MULTIPLE_FRAMES` decodes its whole input as one with it: FMOD in PPSA29141 puts two run jobs in one batch with the second's input right after the whole of the first's. Whether the console stops after one frame there is not verified; MP3 and Opus run jobs still stop after one frame - [AJM M4AAC decoder](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native, codec 2) - Ghost of Yotei (PPSA26344) initializes it through `sceAjmBatchJobControl`. The initialize parameters (`u32` config type, 1 ADTS or 2 raw, then `u32` sampling frequency index), instance flags (channels in bits 3-6, `ENABLE_SBR_DECODE` bit 32, `ENABLE_NONDELAY_OUTPUT` bit 33, surround interleave order bits 34-35), `{heaac, reserved}` codec info and decoder error result (`CODEC_ERROR | FATAL`) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/443f022450f6857256e780daa3d5a14029a748f7/src/core/libraries/ajm/ajm_aac.cpp) (PS4, fdk-aac) and are unverified on PS5; decoding uses FFmpeg's AAC decoder. Without `ENABLE_NONDELAY_OUTPUT`, the first two frames after initialization or reset are consumed without output. Raw input takes its channel configuration from the instance (0 means stereo) and is decoded as one access unit per run job. The format reports the last frame's bitrate. Invalid config type or raw sampling frequency index above 11 returns invalid-parameter result 4. The implementation throws for ADTS without a frame header, profiles other than AAC LC, multiple raw data blocks per ADTS frame, unsupported channel configurations, more channels than the instance supports, HE-AAC without `ENABLE_SBR_DECODE`, raw input with `RUN_MULTIPLE_FRAMES`, output room smaller than the previous frame length, and unknown codec flags. From 4bd0f1d450441de88b96d48156b03a2772a5b5c1 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Sun, 18 Oct 2026 13:36:09 -0400 Subject: [PATCH 063/269] fix(agc): preserve infinite depth bounds --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 1 + .../libSceAgcDriver/Graphics/src/State.cpp | 10 +++++-- .../prx/libSceAgcDriver/tests/Graphics.cpp | 28 +++++++++++++++++++ 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index b8674a1f48..c136baa6e1 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1804,6 +1804,7 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() add_test(NAME agc_driver_undithered_alpha_to_coverage_state COMMAND agc_driver_graphics_tests --undithered-alpha-to-coverage-state) + add_test(NAME agc_driver_depth_bounds_state COMMAND agc_driver_graphics_tests --depth-bounds-state) add_test(NAME agc_driver_msaa_dcc_state COMMAND agc_driver_graphics_tests --multisample-dcc-state) add_test(NAME agc_driver_unused_viewport_depth COMMAND agc_driver_graphics_tests --unused-viewport-depth) set_tests_properties(agc_driver_unused_viewport_depth PROPERTIES TIMEOUT 30) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 82a25682d1..af7ce6271c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -53,6 +53,12 @@ float readFloat(const Registers& registers, std::uint32_t offset) { return value; } +float readDepthBound(const Registers& registers, std::uint32_t offset) { + const auto value = std::bit_cast(read(registers, offset)); + if (std::isnan(value)) Require(false, "NaN depth bound at DWORD " + std::to_string(offset)); + return value; +} + std::string zeroMessage(std::uint32_t offset, std::uint32_t value, const char* name) { char detail[64]; std::snprintf(detail, sizeof(detail), " (register 0x%x = 0x%08x)", offset, value); @@ -349,8 +355,8 @@ void decodeDepth(const Registers& cx, std::uint32_t depthControl, bool testValEx result.depthBoundsTest = (depthControl & 8u) != 0; if (result.depthBoundsTest) { Require(zFormat != 0, "depth bounds without a depth plane"); - result.minDepthBounds = readFloat(cx, 0x008); - result.maxDepthBounds = readFloat(cx, 0x009); + result.minDepthBounds = readDepthBound(cx, 0x008); + result.maxDepthBounds = readDepthBound(cx, 0x009); } if (depthClear) { result.depthTest = true; diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 21bdee2218..fbda3b3ffa 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -51,6 +51,7 @@ #include #include #include +#include #include #include #include @@ -2577,6 +2578,28 @@ void DepthBoundsBiasTests() { queue.context[0x009] = bits(0.75f); auto state = AgcDriver::Graphics::DecodeState(queue); Require(state.depthTest && state.depthBoundsTest && state.minDepthBounds == 0.25f && state.maxDepthBounds == 0.75f, "depth bounds decode changed"); + const auto infinity = std::numeric_limits::infinity(); + for (const auto bounds : {std::array{0.0f, infinity}, std::array{-infinity, 1.0f}, std::array{-infinity, infinity}}) { + queue.context[0x008] = bits(bounds[0]); + queue.context[0x009] = bits(bounds[1]); + state = AgcDriver::Graphics::DecodeState(queue); + Require(state.depthBoundsTest && state.minDepthBounds == bounds[0] && state.maxDepthBounds == bounds[1], "infinite depth bounds changed"); + AgcDriver::Graphics::Context context{}; + expectFailure([&] { AgcDriver::Graphics::ValidateDepthBounds(context, state); }, "depth bounds outside [0, 1]"); + context.depthRangeUnrestricted = true; + AgcDriver::Graphics::ValidateDepthBounds(context, state); + } + for (const auto offset : {0x008u, 0x009u}) { + queue.context[0x008] = bits(0.25f); + queue.context[0x009] = bits(0.75f); + queue.context[offset] = bits(std::numeric_limits::quiet_NaN()); + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "NaN depth bound"); + } + queue.context[0x008] = bits(0.25f); + queue.context[0x009] = bits(0.75f); + queue.context[0x00b] = bits(infinity); + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "non-finite register"); + queue.context[0x00b] = bits(0.0f); queue.context[0x1b3] = 2; queue.context[0x1b4] = 2; const auto rejection = AgcDriver::Graphics::DrawRejection(queue, false); @@ -9815,6 +9838,11 @@ int main(int argc, char** argv) { std::cout << "Multisampled DCC state checks passed\n"; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--depth-bounds-state") { + DepthBoundsBiasTests(); + std::cout << "Depth bounds state checks passed\n"; + return 0; + } { const AgcDriver::Graphics::Context context{}; const AgcDriver::Graphics::State state{}; From 8bd880ceb1b358e6e58bb5e9337eff01e34388a3 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Sun, 18 Oct 2026 14:49:51 -0400 Subject: [PATCH 064/269] test(agc): configure depth bounds state runtime environment --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index c136baa6e1..0eef97b9eb 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1808,7 +1808,7 @@ if(BUILD_TESTING) add_test(NAME agc_driver_msaa_dcc_state COMMAND agc_driver_graphics_tests --multisample-dcc-state) add_test(NAME agc_driver_unused_viewport_depth COMMAND agc_driver_graphics_tests --unused-viewport-depth) set_tests_properties(agc_driver_unused_viewport_depth PROPERTIES TIMEOUT 30) - set_tests_properties(agc_driver_undithered_alpha_to_coverage_state PROPERTIES + set_tests_properties(agc_driver_undithered_alpha_to_coverage_state agc_driver_depth_bounds_state PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") add_test(NAME agc_driver_alpha_to_coverage_state COMMAND agc_driver_graphics_tests --alpha-to-coverage-state) set_tests_properties(agc_driver_alpha_to_coverage_state PROPERTIES @@ -1826,7 +1826,7 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_dithered_alpha_to_coverage PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) - set_tests_properties(agc_driver_alpha_to_coverage_state agc_driver_undithered_alpha_to_coverage_state agc_driver_msaa_dcc_state agc_driver_unused_viewport_depth PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_driver_alpha_to_coverage_state agc_driver_undithered_alpha_to_coverage_state agc_driver_depth_bounds_state agc_driver_msaa_dcc_state agc_driver_unused_viewport_depth PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") set_tests_properties(agc_driver_dithered_alpha_to_coverage ${multisampledDccClearTests} PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") From 1e1ce29909c66f0e67078216ad1fc09f8f8a511b Mon Sep 17 00:00:00 2001 From: Josip Parlov Date: Mon, 19 Oct 2026 10:52:04 +0200 Subject: [PATCH 065/269] fix(tools): keep p_align for segments unwrap_self copies outside load segments unwrap_self copied every segment that does not sit inside a PT_LOAD with alignment 1. On real dumps that puts the comment and version segments (p_align 0x10) right after the last load segment, at offsets that break p_offset % p_align == p_vaddr % p_align. Those regions now use the segment's own p_vaddr and p_align, the same as the load segments, which is what the module docstring already promises. Load segment placement and bytes do not change. --- tools/tests/test_unwrap_self.py | 25 +++++++++++++++++++++++++ tools/unwrap_self.py | 3 ++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/tools/tests/test_unwrap_self.py b/tools/tests/test_unwrap_self.py index aff38128e5..de3e398ed2 100644 --- a/tools/tests/test_unwrap_self.py +++ b/tools/tests/test_unwrap_self.py @@ -193,6 +193,31 @@ class UnwrapTest(unittest.TestCase): for header in headers: self.assertLessEqual(header[2] + header[5], len(data)) + def test_segments_copied_outside_the_load_segments_keep_their_alignment(self): + loads = [(PT_LOAD, 1, 0x1234, 0x0, 0x100, PAGE), (PT_LOAD, 4, 0x1500, 0x1000, 0x88, PAGE)] + records = [(0x2804, 0x800, 0x100), (0x102804, 0xA00, 0x88)] + payloads = {0x800: pattern(1, 0x100), 0xA00: pattern(9, 0x88)} + layouts = ( + ([(PT_DYNAMIC, 6, 0x1580, 0x1040, 0x10, 8), (SCE_COMMENT, 4, 0x15A0, 0x0, 0x20, 0x10), + (PT_NOTE, 4, 0x15C0, 0x0, 0x18, 4), (SCE_VERSION, 4, 0x15D8, 0x0, 0x10, 0x10)], + [(0x302804, 0xAA0, 0x20)], {0xAA0: pattern(11, 0x20), 0xAC0: pattern(13, 0x10)}, + [pattern(9, 0x88)[0x40:0x50], pattern(11, 0x20), b"", pattern(13, 0x10)]), + ([(PT_DYNAMIC, 6, 0x1580, 0x1044, 0x10, 8)], [(0x202804, 0xAA0, 0x10)], + {0xAA0: pattern(11, 0x10)}, [pattern(11, 0x10)]), + ) + for segments, extra_records, extra_payloads, blocks in layouts: + with self.subTest(types=[hex(segment[0]) for segment in segments]): + container = build_container(loads + segments, records + extra_records, + {**payloads, **extra_payloads}) + result, output = self.convert(container) + self.assertEqual(result.returncode, 0, result.stderr) + data, headers = read_program_headers(output) + for kind, flags, offset, vaddr, _, size, _, align in headers: + if align > 1: + self.assertEqual(offset % align, vaddr % align, hex(kind)) + self.assertEqual([data[header[2]:header[2] + header[5]] for header in headers], + [pattern(1, 0x100), pattern(9, 0x88)] + blocks) + def test_declared_offsets_are_used_when_the_container_has_no_records(self): segments = [(PT_LOAD, 1, 0x800, 0x0, 0x40, 0x1000), (PT_LOAD, 4, 0x900, 0x1000, 0x40, 0x1000)] payloads = {0x800: pattern(3, 0x40), 0x900: pattern(5, 0x40)} diff --git a/tools/unwrap_self.py b/tools/unwrap_self.py index b03fd9bfe2..3b2c3b48a5 100644 --- a/tools/unwrap_self.py +++ b/tools/unwrap_self.py @@ -260,7 +260,8 @@ def unwrap(path, output, verbose=False): if header["type"] == PT_LOAD or header["size"] == 0: continue if region_for(header["source"], header["size"]) is None: - regions.append({"old": header["source"], "size": header["size"], "vaddr": 0, "align": 1}) + regions.append({"old": header["source"], "size": header["size"], + "vaddr": header["vaddr"], "align": header["align"]}) regions.sort(key=lambda region: region["old"]) program_offset = struct.unpack_from(" Date: Mon, 19 Oct 2026 09:36:27 +0100 Subject: [PATCH 066/269] feat(libs): declare PS5 system service firmware exports --- core/libs/prx/libSceMbus/CMakeLists.txt | 2 +- core/libs/prx/libSceMbus/PendingExports.cpp | 580 ++++ .../prx/libSceSystemService/CMakeLists.txt | 2 +- .../libSceSystemService/PendingExports.cpp | 2390 +++++++++++++++++ docs/dev/debt/libSceMbus.md | 2 + docs/dev/debt/libSceSystemService.md | 2 + 6 files changed, 2976 insertions(+), 2 deletions(-) create mode 100644 core/libs/prx/libSceMbus/PendingExports.cpp create mode 100644 core/libs/prx/libSceSystemService/PendingExports.cpp diff --git a/core/libs/prx/libSceMbus/CMakeLists.txt b/core/libs/prx/libSceMbus/CMakeLists.txt index 7901f908fa..9de325ed3f 100644 --- a/core/libs/prx/libSceMbus/CMakeLists.txt +++ b/core/libs/prx/libSceMbus/CMakeLists.txt @@ -1,4 +1,4 @@ -add_library(libSceMbus SHARED EXCLUDE_FROM_ALL Export.cpp) +add_library(libSceMbus SHARED EXCLUDE_FROM_ALL Export.cpp PendingExports.cpp) target_include_directories(libSceMbus PRIVATE ${LIBS_INCLUDE_DIR}) configure_windows_unwind(libSceMbus) diff --git a/core/libs/prx/libSceMbus/PendingExports.cpp b/core/libs/prx/libSceMbus/PendingExports.cpp new file mode 100644 index 0000000000..c9296ae891 --- /dev/null +++ b/core/libs/prx/libSceMbus/PendingExports.cpp @@ -0,0 +1,580 @@ +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceMbusAcquireControl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusAcquireControlList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusAcquireControlWithState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusAcquireControlWithState2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusAcquireControlWithStateFlag() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusAddHandle() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusAddHandleByDeviceId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusAddHandleByUserId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusAddHandleByVidPid() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusAddHandleWithModuleId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusAttachToApplicationByPid() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusBindDeviceWithUserId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusChangeAppModuleState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusCheckDriverCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusConvertToLocalDeviceId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusConvertToLocalDeviceId2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusConvertToMbusDeviceId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugAcquireControl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugAcquireControlWithState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugAcquireControlWithState2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugAcquireControlWithStateFlag() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugAddProcess() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugCheckProcessResume() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugDisableBgmForShellUi() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugGetApplicationStartupInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugGetControlStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugGetDeviceInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugGetInternalInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugGetPriorityInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugReenableBgmForShellUi() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugReleaseControl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugRemoveCameraAppModuleFocus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugResumeApplication() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugSetApplicationFocusByAppId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugSetCameraAppModuleFocus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugSetControllerFocusByAppId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugSetOtherProcessExcludedAction() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugSetPriority() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugStartApplication() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugStartApplication2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugStartApplicationNull() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugSuspendApplication() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugTerminateApplication() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugTerminateProcess() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDetachFromApplicationByPid() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDisableBgmForShellUi() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDisableChat() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDisconnectDevice() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDumpAppInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDumpDeviceInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDumpHandleStatusInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDumpHandleStatusInfoAll() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDumpModuleStateInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDumpModuleStateInfoAll() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDumpPlayerInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDumpPortHandleInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDumpProcMediaInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDumpProcMediaInfoAll() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDumpUserMediaInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusEnableChat() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusEnterAudioOutAutoAttenuation() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusEventBusStatusChangeSubscribe() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusEventBusStatusChangeUnsubscribe() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusEventCallbackFuncsInit_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusEventCreate_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusEventDestroy() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusEventReceive() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusGetControlStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusGetDeviceDescription() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusGetDeviceInfoByBusId_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusGetDeviceInfoByConditionForDeviceService() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusGetDeviceInfoByCondition_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusGetDeviceInfo_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusGetSimulatedBusUsableStatusByBusType() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusGetSimulatedHandleStatusInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusGetSimulatedHandleStatusInfo2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusGetSparkState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusIsBgmProhibited() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusIsUsingDevice() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusLeaveAudioOutAutoAttenuation() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusNotifyBtDisconnection() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusNotifyDriverEvent() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusReenableBgmForShellUi() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusRegisterDriver() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusReleaseControl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusRemoveCameraAppModuleFocus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusRemoveHandle() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusRemoveProxy() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusResolveByDeviceId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusResolveByHandle() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusResolveByPlayerId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusResolveByUserId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusSetAutoLoginMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusSetCameraAppModuleFocus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusSetDeviceFunctionState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusSetDuckingSetting() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusSetProxy() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusSetRoutingMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusSetScratchData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusSetScratchDataUpdatedEventMask() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusSetSocialScreenAudioMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusSetTestFlag() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusSetThreadParam() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusSetUsbPowerBlackList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusSetUserMuteStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusSetVolumeDown() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusStartAudioOutAuxBroadcast() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusStartAudioOutSharePlay() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusStopAudioOutAuxBroadcast() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusStopAudioOutSharePlay() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusTerm() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusUnregisterDriver() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusUserLogin() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusUserLogout() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceSystemService/CMakeLists.txt b/core/libs/prx/libSceSystemService/CMakeLists.txt index 8c533a1553..645da47bd0 100644 --- a/core/libs/prx/libSceSystemService/CMakeLists.txt +++ b/core/libs/prx/libSceSystemService/CMakeLists.txt @@ -1,4 +1,4 @@ -add_library(libSceSystemService SHARED EXCLUDE_FROM_ALL Export.cpp LoadExec.cpp) +add_library(libSceSystemService SHARED EXCLUDE_FROM_ALL Export.cpp PendingExports.cpp LoadExec.cpp) target_include_directories(libSceSystemService PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(libSceSystemService PRIVATE libSceAgcDriver) configure_windows_unwind(libSceSystemService) diff --git a/core/libs/prx/libSceSystemService/PendingExports.cpp b/core/libs/prx/libSceSystemService/PendingExports.cpp new file mode 100644 index 0000000000..3fe8d5dfc5 --- /dev/null +++ b/core/libs/prx/libSceSystemService/PendingExports.cpp @@ -0,0 +1,2390 @@ +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceAppMessagingClearEventFlag() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppMessagingReceiveMsg() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppMessagingSendMsg() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppMessagingSendMsgToShellCore() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppMessagingSendMsgToShellUI() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppMessagingSetEventFlag() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppMessagingTryGetEventFlag() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppMessagingTryReceiveMsg() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilAcquireCpuBudgetOfExtraAudioDevices() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilAcquireCpuBudgetOfImeForBigApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilAcquireCpuBudgetOfInGameStore() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilActivateCdlg() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilAddLocalProcess() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilBlockAppSuspend() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilBlockingGetEventForDaemon() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilContinueApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilCrashSyscore() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilDeactivateCdlg() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilDeclareReadyForSuspend() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilDisableSuspendNotification() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilEnableSuspendNotification() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilFinishSpecialResume() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilForceKillApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilForceKillLocalProcess() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetAppCategory() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetAppCategoryType() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetAppFocusedAppStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetAppId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetAppIdListOfBigApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetAppIdOfRunningBigApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetAppLaunchedUser() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetAppStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetAppStatusList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetAppStatusListForShellUIReboot() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetAppTitleId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetAppType() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetCdlgExec() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetCdlgType() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetCoredumpState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetDbgExecutablePath() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetEventForDaemon() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetEventForShellUI() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetGpuCrashFullDumpAppStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetLocalProcessStatusList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetParentSocket() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetResultKillApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetResultLaunchAppByTitleId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetVideoFinalizeState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilIsActiveCdlg() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilIsAppLaunched() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilIsAppSuspended() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilIsCdlgActivated() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilIsCpuBudgetOfExtraAudioDevicesAvailable() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilIsHostBoot() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilIsPs2Emu() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilIsShellUiFgAndGameBgCpuMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilIsSpeculativeLaunch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilKickCoredumpOnlyProcMem() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilKillApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilKillAppWithReason() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilKillLocalProcess() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilLaunchApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilLoadExec() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilNotifyCoredumpRequestEnd() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilNotifyCoredumpRequestProgress() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilNotifyVshReady() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilRaiseException() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilRaiseExceptionLocalProcess() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilRegisterCdlgSharedMemoryName() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilRegisterDaemon() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilRegisterShellUI() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilReleaseCpuBudgetOfExtraAudioDevices() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilReleaseCpuBudgetOfImeForBigApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilReleaseCpuBudgetOfInGameStore() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilResumeApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilResumeLocalProcess() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilSetAppFocus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilSetCdlgExec() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilSetControllerFocus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilSetControllerFocusPermission() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilStartKillApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilStartLaunchAppByTitleId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilSuspendApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilSuspendBackgroundApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilSuspendLocalProcess() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilTryBlockAppSuspend() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilUnblockAppSuspend() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilUnregisterCdlgSharedMemoryName() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilUnregisterDaemon() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilUnregisterShellUI() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilAccessibilityZoomLock() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilAccessibilityZoomUnlock() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilAcquireBgmCpuBudget() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilAcquireRemotePlayCpuBudget() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilAcquireSharePlayCpuBudget() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilActivateAbort() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilActivateGetStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilActivateInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilActivateIsActivated() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilActivateRecordActivation() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilActivateStart() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilActivateStartAsync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilActivateTerm() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilChangeRunLevel() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilChangeToStaffModeForIDU() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilCheckerAbort() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilCleanupCrashReport() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilClearAppData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilClearPs4AppCategoryDefaultsListUpdate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilClearPsnAccountInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilCrashReportRequestCancel() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilDeclareBeginOfExternalStorageAppMove() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilDeclareEndOfExternalStorageAppMove() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilDeleteDiscInstalledTitleWorkaroundFile() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilDeleteDownloadedHidConfigFile() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilDeleteDownloadedNetEvConfigFile() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilDeleteDownloadedTitleWorkaroundFile() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilDoFsck() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilDownloadHidConfigFileFromServer() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilDownloadNetEvConfigFileFromServer() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilDownloadTitleWorkaroundFileFromServer() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilEndUmmDictation() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilEnterPowerLockSection() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilExecuteCrashReport() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilExfatFormatExternalHdd() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilExitApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilExitMiniApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilExitMiniAppWithValue() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilFormatExternalHdd() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilFormatHddForRestore() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetAppData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetAppEnableTTS() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetAppEnterButtonAssign() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetAppLaunchTypeInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetAppLaunchedParamInt() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetAppLaunchedParamIntByBudgetType() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetAppLaunchedParamString() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetAppLaunchedParamStringByBudgetType() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetAutoPowerDownRemainingSeconds() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetBasicProductShape() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetCheckerString() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetCheckerStringEx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetCloudClientStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetCrashReportCoreFileSetSize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetCrashReportFilterInfoStart() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetCrashReportInfoForBoot() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetCrashReportInfoForBootStart() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetCrashReportInfoStart() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetCrashReportResult() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetCrashReportStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetCrashReportUploadStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetDeviceIndexBehavior() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetDeviceIndexBehaviorWithTimeout() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetEffectiveTotalSizeOfUserPartition() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetFreeSizeOfAvContentsTmp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetFreeSizeOfUserPartition() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetFsckProgress() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetGameLiveStreamingStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetGnmCompositorOnScreenProfilerFlag() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetGpuLoadEmulationMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetGpuLoadEmulationModeByAppId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetHidConfigFileInfoString() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetHidConfigFileString() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetHidConfigName() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetHidConfigNum() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetIDUMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetImposeMenuFlagForPs2Emu() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetManifestFileStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetNeedSizeOfAppContent() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetNetEvConfigFileInfoString() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetOutOfVrPlayZoneWarning() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetPapcGamePcl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetPbtcUserInfoList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetPlatformPrivacyDefinitionEventData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetProgressOfFormatExternalHdd() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetProgressOfFsck() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetPs4AppCategoryDefaultsListId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetPs4AppCategoryForTitleId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetPsStoreIconLayout() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetPsStoreIconState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetPsnAccountInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetRemotePlayStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetRunLevel() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetSharePlayStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetSocialScreenStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetSplashScreenState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetSupportSiteURL() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetSuspendConfirmationDialogFlag() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetSystemBGState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetSystemBGWaveColor() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetSystemBGWaveState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetTelemetryToken() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetTitleWorkaroundFileInfoString() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetTitleWorkaroundFileString() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetUIStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetUmmStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetUserFocus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetUserIdOfMorpheusUser() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetVMStatsForTelemetry() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetVersionNumberOfCameraCalibrationData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGoBackToKratosCurrentSessionGame() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilHideBlocksFromUser() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIncrementVersionNumberOfCameraCalibrationData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsAccessibilityZoomLocked() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsBgmCpuBudgetAcquired() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsBgmCpuBudgetAvailable() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsBgmPlaying() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsExternalStorageAppMoveInProgress() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsEyeToEyeDistanceAdjusted() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsGameLiveStreamingOnAir() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsIDUState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsImposeScreenOverlaid() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsInSystemSuspendBlackList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsInternalKratosUser() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsKilledOrSuspendedByLogout() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsNeededCrashReport() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsPowerSaveAlertRequested() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsRemotePlayCpuBudgetAcquired() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsScreenSaverOn() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsSharePlayCpuBudgetAcquired() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsShowCrashReport() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsTemperatureDanger() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsTitleWorkaroundEnabled() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsUsbMassStorageMounted() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilLaunchByUri() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilLeavePowerLockSection() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilMakeManifestFile() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilMountAppRight() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilMountDownloadDataForShellUI() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilMountHddForBackup() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilMountHddForRestore() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilNavigateToAnotherApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilNavigateToGoHome() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilNavigateToLaunchedApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilNotificationCancelForIDU() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilNotificationRequestedForIDU() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilNotifyBgmCoreTermination() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilNotifyFsReadError() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilNotifyImeStatusChanged() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilNotifyPsnAccountInfoReceived() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilNotifySystemSuspendTelemetryInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilPfAuthClientConsoleTokenClearCache() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilPostActivityForPsNow() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilPostErrorLog() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilPostLaunchConfirmResult() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilPostPsmEventToShellUI() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilPreNotifyOfGameLiveStreaming() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilPreNotifyOfRemotePlay() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilPreNotifyOfSharePlay() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilReleaseBgmCpuBudget() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilReleaseRemotePlayCpuBudget() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilReleaseSharePlayCpuBudget() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilReportSessionErrorToGaikaiController() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilReportUnexpectedFatalErrorToSystemTelemetry() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilRequestCameraCalibration() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilRequestEjectDevice() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilRequestRebootApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilRequestShutdown() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilResetAutoPowerDownTimer() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilResetBgdcConfig() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetAppData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetBgmProhibition() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetDeviceIndexBehavior() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetGameLiveStreamingOnAirFlag() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetGameLiveStreamingStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetGnmCompositorOnScreenProfilerFlag() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetGpuLoadEmulationMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetGpuLoadEmulationModeByAppId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetIDUMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetImposeStatusFlag() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetPsStoreIconLayout() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetPsStoreIconState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetRemotePlayStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetSharePlayStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetSkipUpdateCheck() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetSocialScreenStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetSplashScreenState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetSystemBGState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetSystemBGWaveColor() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetSystemBGWaveState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetUIStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetUserFocus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilShowCriticalErrorDialog() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilShowErrorDialog() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilShowErrorDialogWithFormatArgs() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilShowErrorDialogWithParam() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilShowPsUnderLockIndicator() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSignalUserInput() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilStartPsNowGame() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilStopPsNowGame() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilTalkKeyEventReceived() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilTestBusTransferSpeed() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilTickHeartBeat() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilTriggerPapcUpdate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilTurnOffScreenSaver() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilUnmountAppRight() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilUnmountDownloadDataForShellUI() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilUnmountHddForBackup() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilUnmountHddForRestore() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreutilGetCrashReportProcessInformation() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemModalDialogClose() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemModalDialogOpen() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceAcquireBgmCpuBudget() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceAcquireFb0() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateHevc() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateHevcAbort() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateHevcGetStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateHevcInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateHevcIsActivated() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateHevcSoft() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateHevcSoftAbort() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateHevcSoftGetStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateHevcSoftInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateHevcSoftIsActivated() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateHevcSoftStart() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateHevcSoftTerm() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateHevcStart() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateHevcTerm() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateMpeg2Abort() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateMpeg2GetStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateMpeg2Init() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateMpeg2IsActivated() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateMpeg2Start() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateMpeg2Term() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceAddLocalProcess() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceAddLocalProcessForJvm() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceAddLocalProcessForPs2Emu() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceAppGetAppInstallStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceChangeAcpClock() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceChangeClock() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceChangeCpuClock() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceChangeCpuPstate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceChangeGpuClock() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceChangeMemPstate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceChangeMemoryClock() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceChangeMemoryClockToBaseMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceChangeMemoryClockToDefault() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceChangeMemoryClockToMultiMediaMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceChangeNumberOfGpuCu() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceChangeSamuClock() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceChangeUvdClock() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceChangeVceClock() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceDeclareReadyForSuspend() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceDisablePartyVoice() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceDisablePersonalEyeToEyeDistanceSetting() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceDisableSuspendConfirmationDialog() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceDisableSuspendNotification() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceDisableVoiceRecognition() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceEnablePersonalEyeToEyeDistanceSetting() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceEnableSuspendConfirmationDialog() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceEnableSuspendNotification() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetAppCategoryType() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetAppFocusedAppStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetAppId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetAppIdListOfBigApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetAppLaunchedUser() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetAppStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetAppTitleId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetAppType() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetDbgExecutablePath() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetEventForDaemon() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetGpuLoadEmulationMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetLocalProcessStatusList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetMainAppTitleId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetPSButtonEvent() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetParentSocket() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetParentSocketForJvm() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetParentSocketForPs2Emu() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetParentSocketForPsmKit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetPlatformPrivacyDefinitionData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetPlatformPrivacyDefinitionVersion() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetPlatformPrivacySetting() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetRecentLotusPushedButton() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetRemoteplayStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetRenderingMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetSubAppTitleId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetSubAppTitleIdForSP() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetTitleWorkaroundInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetUIStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetVersionNumberOfCameraCalibrationData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceInitializeForShellCore() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceInvokeAppLaunchLink() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceIsAppSuspended() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceIsBgmCpuBudgetAvailable() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceIsBgmPlaying() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceIsEyeToEyeDistanceAdjusted() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceIsGameLiveStreamingOnAir() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceIsScreenSaverOn() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceIsShellUiFgAndGameBgCpuMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceKillLocalProcess() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceKillLocalProcessForJvm() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceKillLocalProcessForPs2Emu() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceLaunchEventDetails() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceLaunchStore() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceLaunchTournamentList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceLaunchTournamentsTeamProfile() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceLaunchUdsApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceLaunchWebApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceLoadExecVideoServiceWebApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceNavigateToAnotherApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceNavigateToGoBack() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceNavigateToGoBackWithValue() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceNavigateToGoHome() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceNotifyBgmCoreTermination() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceRaiseExceptionLocalProcess() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceReenablePartyVoice() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceReenableVoiceRecognition() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceRegisterDaemon() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceReleaseBgmCpuBudget() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceReleaseFb0() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceRequestCameraCalibration() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceRequestPowerOff() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceRequestReboot() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceRequestToChangeRenderingMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceResumeLocalProcess() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceSaveVideoToken() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceSetControllerFocusPermission() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceSetGpuLoadEmulationMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceSetOutOfVrPlayAreaFlag() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceSetOutOfVrPlayZoneWarning() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceSetPowerSaveLevel() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceShowClosedCaptionAdvancedSettings() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceShowClosedCaptionSettings() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceShowErrorDialog() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceShowEyeToEyeDistanceSetting() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceShowImposeMenuForPs2Emu() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceSuspendBackgroundApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceSuspendLocalProcess() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceTelemetrySetData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceTelemetrySetData2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceTickVideoPlayback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceTurnOffScreenSaver() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceUsbStorageGetDeviceInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceUsbStorageGetDeviceList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceUsbStorageGetdentsClose() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceUsbStorageGetdentsOpen() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceUsbStorageGetdentsRead() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceUsbStorageInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceUsbStorageIsExist() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceUsbStorageRegisterCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceUsbStorageRegisterCallbackForMapAvailable() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceUsbStorageRegisterCallbackForMapUnavailable() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceUsbStorageRequestMap() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceUsbStorageRequestMapWSB() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceUsbStorageRequestUnmap() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceUsbStorageTerm() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceUsbStorageUnregisterCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceUsbStorageUnregisterCallbackForMapAvailable() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceUsbStorageUnregisterCallbackForMapUnavailable() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrCancelShutdownTimer() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrEnterMediaPlaybackMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrEnterStandby() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrExtendShutdownTimer() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrExtendShutdownTimerForPostAutoUpdateProcess() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrGetCurrentState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrGetTriggerCode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrIsBdDriveReady() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrIsGpuPerformanceNormal() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrIsShellUIShutdownInProgress() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrIsStandbyModeEnabled() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrLeaveMediaPlaybackMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrNotifySystemSuspendResumeProgress() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrReboot() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrRequestToKeepMainOnStandby() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrSendCecOneTouchPlayCommand() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrStartRebootTimer() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrStartShutdownTimer() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrStartStadbyTimer() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrStartVshAutoUpdateTimer() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrTickMusicPlayback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrTickPartyChat() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrTurnOff() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrVshAutoUpdate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrWaitVshAutoUpdateVerifyDone() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrWakeUp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/docs/dev/debt/libSceMbus.md b/docs/dev/debt/libSceMbus.md index e46de1ffb3..8c7cb7dd7e 100644 --- a/docs/dev/debt/libSceMbus.md +++ b/docs/dev/debt/libSceMbus.md @@ -7,3 +7,5 @@ ### 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 + +- [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`. diff --git a/docs/dev/debt/libSceSystemService.md b/docs/dev/debt/libSceSystemService.md index 874c9d09e7..b420ce2c35 100644 --- a/docs/dev/debt/libSceSystemService.md +++ b/docs/dev/debt/libSceSystemService.md @@ -15,6 +15,8 @@ - [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 +- [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`. + ### Functional - [sceSystemServiceLoadExec](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - besides `"exit"`, it starts the title's own `/app0/eboot.bin` or another executable under `/app0` that the relinker converted with `--executable`, which is looked up beside the running one (`/app0/picker/picker.bin` as `picker.picker.bin` with the running executable's extension). The main executable is the one `APS5_EBOOT` names, which the first call sets to its own path. After the guest shutdown the old process ends without running guest exit handlers or host destructors; as during `exit`, a guest thread that calls a library the shutdown has stopped waits for the end instead of unwinding. `"exit"` ends the process after the host shutdown without running the title's `atexit` and `__cxa_atexit` handlers, since the console's shell closes the title instead; a line the title writes just before through its own thread, such as a forwarded standard error line, may not be printed. [VLC for PS5](https://github.com/ZiZc3/VLC/blob/86bb7d474a303f62bb3c2fc12f728db4846085b8/src/platform_ps5.c#L565-L571) (PPSA85300) relaunches itself with no arguments; RetroArch (PPSA99169) starts its frontend picker with `{"", NULL}`, and the picker starts `eboot.bin` with `{"--ps5-mode=retroarch", NULL}` or `/app0/es-de/es-de.bin` ([picker.cpp](https://github.com/mihawk-99/PS5_RetroArch/blob/bead80725cc11e3c5c65bd9424b0a4494ae36d45/frontends/picker/picker.cpp#L444-L446)), so the list holds only arguments. A list with a first entry becomes the whole guest `argv`, with no program name in front ([LoadExec.cpp](../../../core/libs/prx/libSceSystemService/LoadExec.cpp)): a [PS5 RetroArch console transcript](https://github.com/mihawk-99/PS5_RetroArch/blob/bead80725cc11e3c5c65bd9424b0a4494ae36d45/evidence/loadexec-second-image/relaunch-test.jsonl) records a restart with one argument entering with `argc=1`, `argv[0]="--ps5-relaunch=1"`, and a launch from the home screen with `argc=1`, `argv[0]=""` ([relaunch_ps5.cpp](https://github.com/mihawk-99/PS5_RetroArch/blob/bead80725cc11e3c5c65bd9424b0a4494ae36d45/src/relaunch_ps5.cpp#L161-L164)). For such a list Linux replaces the process with `fexecve`, and macOS with `posix_spawn` and `POSIX_SPAWN_SETEXEC`: the PID and the standard streams stay, the other descriptors are closed and the working directory becomes the executable's. Windows starts a suspended process that inherits only the standard streams and a handle of the old process, and its entry stub waits for the old process to end before it loads the title's libraries, so a shell or script that started the title sees it end. A null list or one without a first entry keeps the earlier launch: the new executable gets only its own path as its command line (`CreateProcessW` on Windows, `execv` on Linux and macOS), so its `argv[0]` is the host path, and the old process ends without waiting for it. What the console passes for such a list is not known, nor are its argument limits and error codes. Paths outside `/app0` and unconverted executables throw From cda8e66ccd36281114bd73c18f86a69de15bd9e3 Mon Sep 17 00:00:00 2001 From: Savvanis Spyros Date: Mon, 19 Oct 2026 11:52:22 +0300 Subject: [PATCH 067/269] fix(agc): keep the pipeline library extensions enabled under APS5_NO_GPL APS5_NO_GPL opened a device without VK_EXT_graphics_pipeline_library and the five extensions that go with it. The AMD Windows driver shares its on-disk pipeline cache between devices with and without those extensions, so a compute pipeline compiled on one kind of device and loaded on the other ran with the wrong descriptor layout. The startup f32 flush probe (#6503) runs the same compute pipelines on every device. In a parallel test run, every APS5_NO_GPL test therefore page-faulted at 0xbeefdeadbeef, an address read from the probe's own 0xdeadbeef fill. Each fault reset the GPU, and twice the reset hung the machine. The device now enables the extensions and their features whenever the GPU supports them. APS5_NO_GPL only turns off their use, so every device on a GPU builds the same pipelines. Co-Authored-By: Claude Opus 5.5 --- .../prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp | 10 ++++++---- docs/dev/ENVIRONMENT.md | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 3326b66c8d..ceeedaa26f 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -1475,7 +1475,8 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : VkPhysicalDeviceExtendedDynamicStateFeaturesEXT dynamicStateFeatures{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTENDED_DYNAMIC_STATE_FEATURES_EXT}; VkPhysicalDeviceDynamicRenderingFeaturesKHR renderingFeatures{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DYNAMIC_RENDERING_FEATURES_KHR}; constexpr std::array libraryExtensions{VK_KHR_PIPELINE_LIBRARY_EXTENSION_NAME, VK_EXT_GRAPHICS_PIPELINE_LIBRARY_EXTENSION_NAME, VK_EXT_EXTENDED_DYNAMIC_STATE_EXTENSION_NAME, VK_KHR_DYNAMIC_RENDERING_EXTENSION_NAME, VK_KHR_DEPTH_STENCIL_RESOLVE_EXTENSION_NAME, VK_KHR_CREATE_RENDERPASS_2_EXTENSION_NAME}; - if (std::getenv("APS5_NO_GPL") == nullptr && std::all_of(libraryExtensions.begin(), libraryExtensions.end(), hasExtension)) { + bool libraryDevice = false; + if (std::all_of(libraryExtensions.begin(), libraryExtensions.end(), hasExtension)) { dynamicStateFeatures.pNext = &renderingFeatures; libraryFeatures.pNext = &dynamicStateFeatures; VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &libraryFeatures}; @@ -1483,8 +1484,9 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : VkPhysicalDeviceGraphicsPipelineLibraryPropertiesEXT libraryProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_GRAPHICS_PIPELINE_LIBRARY_PROPERTIES_EXT}; VkPhysicalDeviceProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &libraryProperties}; state->InstanceFunction("vkGetPhysicalDeviceProperties2")(selected, &properties); - state->graphicsPipelineLibrary = libraryFeatures.graphicsPipelineLibrary == VK_TRUE && dynamicStateFeatures.extendedDynamicState == VK_TRUE && renderingFeatures.dynamicRendering == VK_TRUE && libraryProperties.graphicsPipelineLibraryFastLinking == VK_TRUE && properties.properties.limits.maxPushConstantsSize >= Graphics::PipelinePushConstantBytes; - if (state->graphicsPipelineLibrary) deviceExtensions.insert(deviceExtensions.end(), libraryExtensions.begin(), libraryExtensions.end()); + libraryDevice = libraryFeatures.graphicsPipelineLibrary == VK_TRUE && dynamicStateFeatures.extendedDynamicState == VK_TRUE && renderingFeatures.dynamicRendering == VK_TRUE && libraryProperties.graphicsPipelineLibraryFastLinking == VK_TRUE && properties.properties.limits.maxPushConstantsSize >= Graphics::PipelinePushConstantBytes; + if (libraryDevice) deviceExtensions.insert(deviceExtensions.end(), libraryExtensions.begin(), libraryExtensions.end()); + state->graphicsPipelineLibrary = libraryDevice && std::getenv("APS5_NO_GPL") == nullptr; if (state->graphicsPipelineLibrary && 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}; @@ -1708,7 +1710,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : depthClipFeatures.pNext = const_cast(deviceInfo.pNext); deviceInfo.pNext = &depthClipFeatures; } - if (state->graphicsPipelineLibrary) { + if (libraryDevice) { renderingFeatures.pNext = const_cast(deviceInfo.pNext); deviceInfo.pNext = &libraryFeatures; } diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 6c0cd7f815..e78511c652 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -223,7 +223,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_NO_FLUSH_PRETEST` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | lists every pending image on FlushPending instead of testing whether a pending unit lies inside the range | | `APS5_NO_FOREIGN_WRITERS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WriteEvidence.cpp` | | records no draw-written ranges as foreign writers for capture validation | | `APS5_NO_GATE_RETRY` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WriteEvidence.cpp` | | retries no validation whose entry moved, leaving the dispatch or draw unvalidated | -| `APS5_NO_GPL` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | disables graphics pipeline libraries | +| `APS5_NO_GPL` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | draws without graphics pipeline libraries; the device still enables their extensions, so the driver builds its pipelines as for a default device | | `APS5_NO_GPU_DUMP` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | decodes dump frames from the tiled guest buffer on the CPU | | `APS5_NO_GPU_INDIRECT` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/IndirectDispatch.cpp` | | reads indirect dispatch arguments on the CPU instead of letting the GPU count them | | `APS5_NO_GPU_INDIRECT_DRAW` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | reads indirect draw arguments through the flush hook on the CPU instead of from the GPU | From 1dc1301707935c5f1cd527b9072777cd3de2d772 Mon Sep 17 00:00:00 2001 From: akkadbakkad89 Date: Mon, 19 Oct 2026 09:48:52 +0000 Subject: [PATCH 068/269] feat(prx): declare exports for six confirmed system libraries --- core/libs/prx/libSceAppChecker/Export.cpp | 17 +++++++++++-- .../libSceAudioLatencyEstimation/Export.cpp | 22 ++++++++++++++-- core/libs/prx/libSceCompanionUtil/Export.cpp | 25 ++++++++++++++++++- core/libs/prx/libSceDiscId/Export.cpp | 10 +++++++- core/libs/prx/libSceNKWeb/Export.cpp | 12 +++++++-- core/libs/prx/libSceS3da/Export.cpp | 22 ++++++++++++++-- 6 files changed, 98 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libSceAppChecker/Export.cpp b/core/libs/prx/libSceAppChecker/Export.cpp index d81f4a5e32..b50753644e 100644 --- a/core/libs/prx/libSceAppChecker/Export.cpp +++ b/core/libs/prx/libSceAppChecker/Export.cpp @@ -1,10 +1,23 @@ -#include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" extern "C" { -APS5_DUMMY_FUN +int APS5_VABI sceAppCheckerAbort() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppCheckerExecute() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppCheckerExecuteEx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/core/libs/prx/libSceAudioLatencyEstimation/Export.cpp b/core/libs/prx/libSceAudioLatencyEstimation/Export.cpp index d81f4a5e32..71b2edeba4 100644 --- a/core/libs/prx/libSceAudioLatencyEstimation/Export.cpp +++ b/core/libs/prx/libSceAudioLatencyEstimation/Export.cpp @@ -1,10 +1,28 @@ -#include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" extern "C" { -APS5_DUMMY_FUN +int APS5_VABI sceAudioLatencyEstimationAddData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioLatencyEstimationGetDelayTime() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioLatencyEstimationInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioLatencyEstimationRelease() { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/core/libs/prx/libSceCompanionUtil/Export.cpp b/core/libs/prx/libSceCompanionUtil/Export.cpp index fb34206c21..791221033d 100644 --- a/core/libs/prx/libSceCompanionUtil/Export.cpp +++ b/core/libs/prx/libSceCompanionUtil/Export.cpp @@ -5,6 +5,29 @@ extern "C" { -APS5_DUMMY_FUN +int APS5_VABI sceCompanionUtilGetEvent() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompanionUtilGetRemoteOskEvent() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompanionUtilInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompanionUtilOptParamInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompanionUtilTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/core/libs/prx/libSceDiscId/Export.cpp b/core/libs/prx/libSceDiscId/Export.cpp index fb34206c21..055e970b29 100644 --- a/core/libs/prx/libSceDiscId/Export.cpp +++ b/core/libs/prx/libSceDiscId/Export.cpp @@ -5,6 +5,14 @@ extern "C" { -APS5_DUMMY_FUN +int APS5_VABI sceDiscIdGet() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDiscIdGet2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/core/libs/prx/libSceNKWeb/Export.cpp b/core/libs/prx/libSceNKWeb/Export.cpp index d81f4a5e32..81401ab485 100644 --- a/core/libs/prx/libSceNKWeb/Export.cpp +++ b/core/libs/prx/libSceNKWeb/Export.cpp @@ -1,10 +1,18 @@ -#include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" extern "C" { -APS5_DUMMY_FUN +int APS5_VABI sceNKWebInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNKWebTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/core/libs/prx/libSceS3da/Export.cpp b/core/libs/prx/libSceS3da/Export.cpp index d81f4a5e32..34b8330de5 100644 --- a/core/libs/prx/libSceS3da/Export.cpp +++ b/core/libs/prx/libSceS3da/Export.cpp @@ -1,10 +1,28 @@ -#include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" extern "C" { -APS5_DUMMY_FUN +int APS5_VABI sceS3daClose() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceS3daGetInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceS3daOpen() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceS3daSendData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} } From eb4d64a3b05476d8c5d3ab5674d69187d19216e2 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Mon, 19 Oct 2026 10:08:33 +0300 Subject: [PATCH 069/269] fix(libSceVideodec2): frame an access unit whose length prefix looks like a start code --- core/libs/prx/libSceVideodec2/Export.cpp | 36 ++++++++++++++++-------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libSceVideodec2/Export.cpp b/core/libs/prx/libSceVideodec2/Export.cpp index ea5ca36c4e..e5daeed313 100644 --- a/core/libs/prx/libSceVideodec2/Export.cpp +++ b/core/libs/prx/libSceVideodec2/Export.cpp @@ -372,21 +372,35 @@ void OpenCodec(Decoder& decoder) { } } -const std::vector* ToAnnexB(Decoder& decoder, const std::uint8_t* data, std::size_t size) { - if (size >= 4 && data[0] == 0 && data[1] == 0 && (data[2] == 1 || (data[2] == 0 && data[3] == 1))) { - decoder.annexB.assign(data, data + size); - return &decoder.annexB; - } - decoder.annexB.clear(); +bool TilesAsAvcc(const std::uint8_t* data, std::size_t size) { + if (size < 4) return false; for (std::size_t index = 0; index < size;) { - if (size - index < 4) throw std::runtime_error("Videodec2: truncated NAL length"); + if (size - index < 4) return false; const std::size_t length = (static_cast(data[index]) << 24u) | (static_cast(data[index + 1]) << 16u) | (static_cast(data[index + 2]) << 8u) | data[index + 3]; - if (length == 0 || length > size - index - 4) throw std::runtime_error("Videodec2: invalid NAL length"); - static constexpr std::uint8_t startCode[4] = {0, 0, 0, 1}; - decoder.annexB.insert(decoder.annexB.end(), startCode, startCode + 4); - decoder.annexB.insert(decoder.annexB.end(), data + index + 4, data + index + 4 + length); + if (length == 0 || length > size - index - 4) return false; index += 4 + length; } + return true; +} + +bool StartsWithStartCode(const std::uint8_t* data, std::size_t size) { + return size >= 3 && data[0] == 0 && data[1] == 0 && (data[2] == 1 || (size >= 4 && data[2] == 0 && data[3] == 1)); +} + +const std::vector* ToAnnexB(Decoder& decoder, const std::uint8_t* data, std::size_t size) { + if (TilesAsAvcc(data, size)) { + decoder.annexB.clear(); + for (std::size_t index = 0; index < size;) { + const std::size_t length = (static_cast(data[index]) << 24u) | (static_cast(data[index + 1]) << 16u) | (static_cast(data[index + 2]) << 8u) | data[index + 3]; + static constexpr std::uint8_t startCode[4] = {0, 0, 0, 1}; + decoder.annexB.insert(decoder.annexB.end(), startCode, startCode + 4); + decoder.annexB.insert(decoder.annexB.end(), data + index + 4, data + index + 4 + length); + index += 4 + length; + } + return &decoder.annexB; + } + if (!StartsWithStartCode(data, size)) throw std::runtime_error("Videodec2: access unit is neither Annex-B nor length-prefixed"); + decoder.annexB.assign(data, data + size); return &decoder.annexB; } From 43b5f33f80f94ddf3cc5c4363f2f44e6afe98acc Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Mon, 19 Oct 2026 10:08:33 +0300 Subject: [PATCH 070/269] test(libSceVideodec2): decode an AVCC access unit whose length prefix is a start code --- core/libs/tests/GuestVideodec2.cpp | 43 ++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/core/libs/tests/GuestVideodec2.cpp b/core/libs/tests/GuestVideodec2.cpp index f8d3be0ffc..f483c38881 100644 --- a/core/libs/tests/GuestVideodec2.cpp +++ b/core/libs/tests/GuestVideodec2.cpp @@ -576,6 +576,48 @@ void testDecode(bool lengthPrefixed, bool separateParameters = false) { checkThrows([&] { sceVideodec2DeleteDecoder_nid_postfix(handle); }); } +void testLengthPrefixLooksLikeStartCode() { + auto units = accessUnits(true); + std::vector filler; + constexpr std::size_t fillerLength = 0x101; + for (int shift = 24; shift >= 0; shift -= 8) filler.push_back(static_cast(fillerLength >> shift)); + filler.push_back(0x0c); + filler.insert(filler.end(), fillerLength - 1, 0xff); + units.front().insert(units.front().begin(), filler.begin(), filler.end()); + check(units.front()[0] == 0 && units.front()[1] == 0 && units.front()[2] == 1, "the crafted unit does not begin with start-code bytes"); + + const DecoderConfigInfo config{sizeof(DecoderConfigInfo), 0, 1, 100, 0, static_cast(Width), static_cast(Height), 4, 1}; + DecoderMemoryInfo memory{sizeof(DecoderMemoryInfo)}; + check(sceVideodec2QueryDecoderMemoryInfo_nid_postfix(&config, &memory) == 0, "memory query failed"); + std::uint64_t handle = 0; + check(sceVideodec2CreateDecoder_nid_postfix(&config, &memory, &handle) == 0, "decoder creation failed"); + std::vector buffer(memory.maxFrameBufferSize); + std::size_t pictures = 0; + const auto take = [&](const OutputInfo& output) { + if (!output.isValid) return false; + check(pictures < PictureHashes.size(), "more pictures than access units"); + check(!output.isErrorFrame && output.frameWidth == Width && output.frameHeight == CodedHeight, "unexpected picture geometry"); + check(hashNv12(buffer.data(), output.framePitch, output.frameHeight) == PictureHashes[pictures], "picture " + std::to_string(pictures) + " differs from the reference"); + ++pictures; + return true; + }; + for (std::size_t unit = 0; unit < units.size(); ++unit) { + const InputData input{sizeof(InputData), units[unit].data(), units[unit].size(), 1000 + unit, unit, 0xa0 + unit}; + 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); + } + for (;;) { + FrameBuffer frame{sizeof(FrameBuffer), buffer.data(), buffer.size(), false}; + OutputInfo output{sizeof(OutputInfo)}; + check(sceVideodec2Flush_nid_postfix(handle, &frame, &output) == 0, "flush failed"); + if (!take(output)) break; + } + check(pictures == PictureHashes.size(), "missing pictures after flush"); + check(sceVideodec2DeleteDecoder_nid_postfix(handle) == 0, "delete failed"); +} + std::uint64_t createDecoder(std::uint32_t codec, std::uint32_t profile) { const DecoderConfigInfo config{sizeof(DecoderConfigInfo), 1, codec, profile, 0, static_cast(Width), static_cast(Height), 4, 1}; DecoderMemoryInfo memory{sizeof(DecoderMemoryInfo)}; @@ -784,6 +826,7 @@ int main() { testFailures(); testDecode(false); testDecode(true); + testLengthPrefixLooksLikeStartCode(); testDecode(false, true); const auto hevc = hevcUnits(HevcStream.data(), HevcStream.size()); testClip(CodecHevc, 1, hevc, HevcHashes); From 26177b41e3e3a99acb7deb6621ae284794bdc75d Mon Sep 17 00:00:00 2001 From: akkadbakkad89 Date: Mon, 19 Oct 2026 10:11:10 +0000 Subject: [PATCH 071/269] feat(prx): declare exports for seven more confirmed system libraries --- core/libs/prx/libSceCompanionHttpd/Export.cpp | 85 +++++++++- .../libs/prx/libSceCustomMusicCore/Export.cpp | 90 ++++++++++- core/libs/prx/libSceHttpCache/Export.cpp | 145 +++++++++++++++++- core/libs/prx/libSceMoveTracker/Export.cpp | 92 ++++++++++- core/libs/prx/libSceOpusCeltEnc/Export.cpp | 60 +++++++- core/libs/prx/libSceOpusSilkEnc/Export.cpp | 82 +++++++++- core/libs/prx/libSceShellUIUtil/Export.cpp | 62 +++++++- 7 files changed, 606 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libSceCompanionHttpd/Export.cpp b/core/libs/prx/libSceCompanionHttpd/Export.cpp index fb34206c21..48240a3fe1 100644 --- a/core/libs/prx/libSceCompanionHttpd/Export.cpp +++ b/core/libs/prx/libSceCompanionHttpd/Export.cpp @@ -5,6 +5,89 @@ extern "C" { -APS5_DUMMY_FUN +int APS5_VABI sceCompanionHttpdAddHeader() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompanionHttpdGet2ndScreenStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompanionHttpdGetEvent() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompanionHttpdGetUserId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompanionHttpdInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompanionHttpdInitialize2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompanionHttpdOptParamInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompanionHttpdRegisterRequestBodyReceptionCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompanionHttpdRegisterRequestCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompanionHttpdRegisterRequestCallback2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompanionHttpdSetBody() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompanionHttpdSetStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompanionHttpdStart() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompanionHttpdStop() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompanionHttpdTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompanionHttpdUnregisterRequestBodyReceptionCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompanionHttpdUnregisterRequestCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/core/libs/prx/libSceCustomMusicCore/Export.cpp b/core/libs/prx/libSceCustomMusicCore/Export.cpp index fb34206c21..fa0c494e42 100644 --- a/core/libs/prx/libSceCustomMusicCore/Export.cpp +++ b/core/libs/prx/libSceCustomMusicCore/Export.cpp @@ -5,6 +5,94 @@ extern "C" { -APS5_DUMMY_FUN +int APS5_VABI sceCustomMusicCoreBgmClose() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCustomMusicCoreBgmOpen() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCustomMusicCoreBgmOpen2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCustomMusicCoreBgmOutput() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCustomMusicCoreBgmSetAudioVolume() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCustomMusicCoreBgmStart() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCustomMusicCoreBgmStop() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCustomMusicCoreGetBgmAuthorityStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCustomMusicCoreGetHdcpStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCustomMusicCoreGetSystemAudioVolume() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCustomMusicCorePostEvent() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCustomMusicCoreSendEvent() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCustomMusicCoreSendMulticastEvent() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCustomMusicCoreSendSpMessage() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCustomMusicCoreSetSystemAudioVolume() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCustomMusicCoreStartOperation() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCustomMusicCoreStartOperation2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCustomMusicCoreStopOperation() { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/core/libs/prx/libSceHttpCache/Export.cpp b/core/libs/prx/libSceHttpCache/Export.cpp index fb34206c21..3d221f0db2 100644 --- a/core/libs/prx/libSceHttpCache/Export.cpp +++ b/core/libs/prx/libSceHttpCache/Export.cpp @@ -5,6 +5,149 @@ extern "C" { -APS5_DUMMY_FUN +int APS5_VABI sceHttpCacheClear() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheClearAll() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheClearAllUserTags() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheClearCacheInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheCompleteRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheCreateRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheCreateRequestWithTag() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheDebugSetUrl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheDeleteRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheReadData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheRedirectedConnectionEnabled() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheRegisterCacheInfoUrl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheRemoveTag() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheRetrieve() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheRetrieveWithMemoryPool() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheRevalidate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheRevalidateWithMemoryPool() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheSetCacheSharing() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheSetData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheSetQuota() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheSetResponseHeader() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheSystemClearAll() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheSystemInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheSystemOnUserServiceEvent() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheSystemSendStatistics() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheSystemShutdown() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheSystemTerm() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheTerm() { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/core/libs/prx/libSceMoveTracker/Export.cpp b/core/libs/prx/libSceMoveTracker/Export.cpp index d81f4a5e32..6c660b9787 100644 --- a/core/libs/prx/libSceMoveTracker/Export.cpp +++ b/core/libs/prx/libSceMoveTracker/Export.cpp @@ -1,10 +1,98 @@ -#include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" extern "C" { -APS5_DUMMY_FUN +int APS5_VABI sceMoveTrackerCalibrateReset() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveTrackerCalibrateYaw() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveTrackerCameraUpdate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveTrackerCaptureInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveTrackerCaptureTerm() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveTrackerControllersUpdate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveTrackerGetState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveTrackerGetVersion() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveTrackerGetWorkingMemorySize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveTrackerInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveTrackerPlayGetImages() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveTrackerPlayGetStates() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveTrackerPlayStart() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveTrackerPlayStep() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveTrackerRecordSave() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveTrackerRecordStart() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveTrackerRecordStop() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveTrackerTerm() { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/core/libs/prx/libSceOpusCeltEnc/Export.cpp b/core/libs/prx/libSceOpusCeltEnc/Export.cpp index fb34206c21..57600c82c3 100644 --- a/core/libs/prx/libSceOpusCeltEnc/Export.cpp +++ b/core/libs/prx/libSceOpusCeltEnc/Export.cpp @@ -5,6 +5,64 @@ extern "C" { -APS5_DUMMY_FUN +int APS5_VABI sceOpusCeltEncCreate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusCeltEncCreateEx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusCeltEncCtl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusCeltEncDestroy() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusCeltEncEncode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusCeltEncEncodeFloat() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusCeltEncEncodeFloatWithPriority() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusCeltEncEncodeWithPriority() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusCeltEncGetAjmContext() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusCeltEncGetSize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusCeltEncInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusCeltEncTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/core/libs/prx/libSceOpusSilkEnc/Export.cpp b/core/libs/prx/libSceOpusSilkEnc/Export.cpp index d81f4a5e32..2d8195c3d6 100644 --- a/core/libs/prx/libSceOpusSilkEnc/Export.cpp +++ b/core/libs/prx/libSceOpusSilkEnc/Export.cpp @@ -1,10 +1,88 @@ -#include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" extern "C" { -APS5_DUMMY_FUN +int APS5_VABI sceOpusSilkEncCancel() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusSilkEncCreate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusSilkEncCreateEx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusSilkEncCtl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusSilkEncDestroy() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusSilkEncEncode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusSilkEncEncodeCancelForTimeout() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusSilkEncEncodeFloat() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusSilkEncEncodeFloatWithPriority() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusSilkEncEncodeFloatWithPriorityAndTimeout() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusSilkEncEncodeWithPriority() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusSilkEncEncodeWithPriorityAndTimeout() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusSilkEncGetAjmContext() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusSilkEncGetSize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusSilkEncInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusSilkEncTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/core/libs/prx/libSceShellUIUtil/Export.cpp b/core/libs/prx/libSceShellUIUtil/Export.cpp index d81f4a5e32..18a5efad19 100644 --- a/core/libs/prx/libSceShellUIUtil/Export.cpp +++ b/core/libs/prx/libSceShellUIUtil/Export.cpp @@ -1,10 +1,68 @@ -#include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" extern "C" { -APS5_DUMMY_FUN +int APS5_VABI sceShellUIUtilAbortTask() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellUIUtilCreateTaskToken() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellUIUtilGetAppUrl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellUIUtilGetDefaultQueryParameter() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellUIUtilInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellUIUtilLaunchByUri() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellUIUtilLaunchByUriParamInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellUIUtilLaunchTask() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellUIUtilServiceInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellUIUtilServiceSetEventFlag() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellUIUtilServiceTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellUIUtilTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} } From 155e415b8507a7b2de16e1ac03236ec0fe1a52f7 Mon Sep 17 00:00:00 2001 From: fogydes Date: Sat, 17 Oct 2026 11:55:55 +0530 Subject: [PATCH 072/269] feat(libSceContentExport): export missing firmware content export interfaces --- core/libs/prx/libSceContentExport/Export.cpp | 60 ++++++++++++++++++++ core/libs/tests/GuestContentExport.cpp | 26 +++++++++ docs/dev/debt/libSceContentExport.md | 5 ++ 3 files changed, 91 insertions(+) diff --git a/core/libs/prx/libSceContentExport/Export.cpp b/core/libs/prx/libSceContentExport/Export.cpp index f618daeea1..b8a366cc65 100644 --- a/core/libs/prx/libSceContentExport/Export.cpp +++ b/core/libs/prx/libSceContentExport/Export.cpp @@ -55,6 +55,66 @@ int APS5_VABI sceContentExportFromFileWithThumbnail(void) { return 0; } +int APS5_VABI sceContentExportAddToFavorites(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceContentExportDataProvideFunction(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceContentExportFree(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceContentExportFrom(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceContentExportFromFileForSf(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceContentExportFromFileWithContentIdList(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceContentExportFromFileWithTitleIdList(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceContentExportInit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceContentExportMalloc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceContentExportRemoveFromFavorites(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceContentExportRenameContent(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceContentExportValidateContents(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceContentExportStart(void) { std::lock_guard lock(g_sessionMutex); if (!g_initialized.load()) throw std::logic_error(std::string(__func__) + ": not initialized"); diff --git a/core/libs/tests/GuestContentExport.cpp b/core/libs/tests/GuestContentExport.cpp index ef295d6ec9..40c6aa941d 100644 --- a/core/libs/tests/GuestContentExport.cpp +++ b/core/libs/tests/GuestContentExport.cpp @@ -4,6 +4,19 @@ #include #include +extern "C" int APS5_VABI sceContentExportAddToFavorites(void); +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 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 sceContentExportInit2(const ContentExportInitParam2* init_param); extern "C" int APS5_VABI sceContentExportTerm(void); extern "C" int APS5_VABI sceContentExportStart(void); @@ -85,4 +98,17 @@ int main() { Require(sceContentExportTerm() == 0); Require(Throws([] { sceContentExportStart(); })); Require(Throws([&] { sceContentExportFinish(reopened); })); + Require(Throws([] { sceContentExportAddToFavorites(); })); + Require(Throws([] { sceContentExportDataProvideFunction(); })); + Require(Throws([] { sceContentExportFree(); })); + Require(Throws([] { sceContentExportFrom(); })); + Require(Throws([] { sceContentExportFromFileForSf(); })); + Require(Throws([] { sceContentExportFromFileWithContentIdList(); })); + Require(Throws([] { sceContentExportFromFileWithTitleIdList(); })); + Require(Throws([] { sceContentExportInit(); })); + Require(Throws([] { sceContentExportMalloc(); })); + Require(Throws([] { sceContentExportRemoveFromFavorites(); })); + Require(Throws([] { sceContentExportRenameContent(); })); + Require(Throws([] { sceContentExportValidateContents(); })); } + diff --git a/docs/dev/debt/libSceContentExport.md b/docs/dev/debt/libSceContentExport.md index 1c8c0c1285..18355bf87b 100644 --- a/docs/dev/debt/libSceContentExport.md +++ b/docs/dev/debt/libSceContentExport.md @@ -3,3 +3,8 @@ ### 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 + +### 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`. + From f31479bb0c80a6399400c63e32e54ccbb433ae68 Mon Sep 17 00:00:00 2001 From: KernelPanicNT <76663133+KernelPanicNT@users.noreply.github.com> Date: Mon, 19 Oct 2026 10:15:54 +0000 Subject: [PATCH 073/269] fix(libSceGameUpdate): return the module's lifecycle and argument errors Following the PS5 3.20 module as read by prosper: - a second sceGameUpdateInitialize returns 0x80412802, and sceGameUpdateTerminate without initialization returns 0x80412801; - sceGameUpdateCreateRequest returns 0x80412801 before initialization; - sceGameUpdateCheck checks the id, the pointers, the 0x30 sizes and the zero option and reserved words before initialization; - sceGameUpdateAbortRequest and sceGameUpdateDeleteRequest return 0x80412805 for an id that was never created instead of success; - sceGameUpdateGetAddcontLatestVersion checks its pointers before initialization. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceGameUpdate/Export.cpp | 64 ++++++++++++++++++----- core/libs/tests/GuestGameUpdate.cpp | 54 +++++++++++++++++++ docs/dev/debt/libSceGameUpdate.md | 1 + 3 files changed, 106 insertions(+), 13 deletions(-) diff --git a/core/libs/prx/libSceGameUpdate/Export.cpp b/core/libs/prx/libSceGameUpdate/Export.cpp index 7acb4eecc2..37c0901309 100644 --- a/core/libs/prx/libSceGameUpdate/Export.cpp +++ b/core/libs/prx/libSceGameUpdate/Export.cpp @@ -7,45 +7,79 @@ // 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(0x80D50001); static constexpr int SCE_GAME_UPDATE_ERROR_NOT_INITIALIZED = static_cast(0x80412801); +static constexpr int SCE_GAME_UPDATE_ERROR_ALREADY_INITIALIZED = static_cast(0x80412802); static constexpr int SCE_GAME_UPDATE_ERROR_INVALID_ARGUMENT = static_cast(0x80412803); static constexpr int SCE_GAME_UPDATE_ERROR_INVALID_SIZE = static_cast(0x80412804); +static constexpr int SCE_GAME_UPDATE_ERROR_REQUEST_NOT_FOUND = static_cast(0x80412805); + +static_assert(sizeof(GameUpdateCheckParam) == 0x30); +static_assert(sizeof(GameUpdateCheckResult) == 0x30); static std::atomic g_initialized{false}; +static int CheckRequestId(int request_id) { + if (request_id <= 0) { + return SCE_GAME_UPDATE_ERROR_REQUEST_NOT_FOUND; + } + if (!g_initialized.load()) { + return SCE_GAME_UPDATE_ERROR_NOT_INITIALIZED; + } + return SCE_GAME_UPDATE_ERROR_REQUEST_NOT_FOUND; +} + +static bool HasNonZeroReserved(const GameUpdateCheckParam* param) { + for (std::uint32_t word : param->reserved) { + if (word != 0) { + return true; + } + } + return false; +} + extern "C" { int APS5_VABI sceGameUpdateAbortRequest(int request_id) { - (void)request_id; - return 0; + return CheckRequestId(request_id); } int APS5_VABI sceGameUpdateCheck(int request_id, const GameUpdateCheckParam* param, GameUpdateCheckResult* result) { - (void)request_id; - (void)param; - (void)result; - return SCE_GAME_UPDATE_ERROR_OFFLINE; + if (request_id <= 0) { + return SCE_GAME_UPDATE_ERROR_REQUEST_NOT_FOUND; + } + if (param == nullptr || result == nullptr) { + return SCE_GAME_UPDATE_ERROR_INVALID_ARGUMENT; + } + if (param->size != sizeof(GameUpdateCheckParam) || result->size != sizeof(GameUpdateCheckResult)) { + return SCE_GAME_UPDATE_ERROR_INVALID_SIZE; + } + if (param->option != 0 || HasNonZeroReserved(param)) { + return SCE_GAME_UPDATE_ERROR_INVALID_ARGUMENT; + } + return CheckRequestId(request_id); } int APS5_VABI sceGameUpdateCreateRequest(void) { + if (!g_initialized.load()) { + return SCE_GAME_UPDATE_ERROR_NOT_INITIALIZED; + } return SCE_GAME_UPDATE_ERROR_OFFLINE; } int APS5_VABI sceGameUpdateDeleteRequest(int request_id) { - (void)request_id; - return 0; + return CheckRequestId(request_id); } int APS5_VABI sceGameUpdateGetAddcontLatestVersion(uint32_t service_label, const void* entitlement_label, GameUpdateAddcontVersionInfo* info) { (void)service_label; - if (!g_initialized.load()) { - return SCE_GAME_UPDATE_ERROR_NOT_INITIALIZED; - } if (info == nullptr) { return SCE_GAME_UPDATE_ERROR_INVALID_ARGUMENT; } if (entitlement_label == nullptr) { return SCE_GAME_UPDATE_ERROR_INVALID_ARGUMENT; } + if (!g_initialized.load()) { + return SCE_GAME_UPDATE_ERROR_NOT_INITIALIZED; + } if (info->size < sizeof(GameUpdateAddcontVersionInfo)) { return SCE_GAME_UPDATE_ERROR_INVALID_SIZE; } @@ -57,12 +91,16 @@ int APS5_VABI sceGameUpdateGetAddcontLatestVersion(uint32_t service_label, const } int APS5_VABI sceGameUpdateInitialize(void) { - g_initialized.store(true); + if (g_initialized.exchange(true)) { + return SCE_GAME_UPDATE_ERROR_ALREADY_INITIALIZED; + } return 0; } int APS5_VABI sceGameUpdateTerminate(void) { - g_initialized.store(false); + if (!g_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 0394f1283d..13fcac85c2 100644 --- a/core/libs/tests/GuestGameUpdate.cpp +++ b/core/libs/tests/GuestGameUpdate.cpp @@ -3,16 +3,60 @@ #include #include #include +#include #include 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 sceGameUpdateCreateRequest(void); +int APS5_VABI sceGameUpdateCheck(int, const GameUpdateCheckParam*, GameUpdateCheckResult*); +int APS5_VABI sceGameUpdateAbortRequest(int); +int APS5_VABI sceGameUpdateDeleteRequest(int); } static void Require(bool value) { if (!value) std::abort(); } +static void TestRequestArguments(int notInitialized, int invalidArgument, int invalidSize, int notFound) { + GameUpdateCheckParam param{}; + param.size = sizeof(param); + GameUpdateCheckResult result{}; + result.size = sizeof(result); + constexpr int requestId = 0x20000000; + + for (int id : {0, -1}) { + Require(sceGameUpdateCheck(id, nullptr, nullptr) == notFound); + Require(sceGameUpdateAbortRequest(id) == notFound); + Require(sceGameUpdateDeleteRequest(id) == notFound); + } + Require(sceGameUpdateCheck(requestId, nullptr, &result) == invalidArgument); + Require(sceGameUpdateCheck(requestId, ¶m, nullptr) == invalidArgument); + param.size = sizeof(param) - 1; + Require(sceGameUpdateCheck(requestId, ¶m, &result) == invalidSize); + param.size = sizeof(param); + result.size = sizeof(result) + 8; + Require(sceGameUpdateCheck(requestId, ¶m, &result) == invalidSize); + result.size = sizeof(result); + param.option = 1; + Require(sceGameUpdateCheck(requestId, ¶m, &result) == invalidArgument); + param.option = 0; + param.reserved[8] = 1; + Require(sceGameUpdateCheck(requestId, ¶m, &result) == invalidArgument); + param.reserved[8] = 0; + + Require(sceGameUpdateCreateRequest() == notInitialized); + Require(sceGameUpdateCheck(requestId, ¶m, &result) == notInitialized); + Require(sceGameUpdateAbortRequest(requestId) == notInitialized); + Require(sceGameUpdateDeleteRequest(requestId) == notInitialized); + + Require(sceGameUpdateInitialize() == 0); + Require(sceGameUpdateCheck(requestId, ¶m, &result) == notFound); + Require(sceGameUpdateAbortRequest(requestId) == notFound); + Require(sceGameUpdateDeleteRequest(requestId) == notFound); + Require(sceGameUpdateTerminate() == 0); +} + int main() { constexpr int notInitialized = static_cast(0x80412801); constexpr int invalidArgument = static_cast(0x80412803); @@ -23,8 +67,17 @@ int main() { GameUpdateAddcontVersionInfo info{}; info.size = sizeof(info); + constexpr int alreadyInitialized = static_cast(0x80412802); + constexpr int notFound = static_cast(0x80412805); + + Require(sceGameUpdateTerminate() == notInitialized); + TestRequestArguments(notInitialized, invalidArgument, invalidSize, notFound); + 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, nullptr, &info) == invalidArgument); @@ -40,6 +93,7 @@ int main() { for (std::size_t i = sizeof(info.size); i < sizeof(info); ++i) Require(bytes[i] == 0); Require(sceGameUpdateTerminate() == 0); + Require(sceGameUpdateTerminate() == notInitialized); info.size = sizeof(info); Require(sceGameUpdateGetAddcontLatestVersion(0, label, &info) == notInitialized); } diff --git a/docs/dev/debt/libSceGameUpdate.md b/docs/dev/debt/libSceGameUpdate.md index d82804dae6..c67a4a14a3 100644 --- a/docs/dev/debt/libSceGameUpdate.md +++ b/docs/dev/debt/libSceGameUpdate.md @@ -3,3 +3,4 @@ ### Unknown function info - [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 From f9d6f3e93258c180a1787805b1243688c091f7c5 Mon Sep 17 00:00:00 2001 From: SibteProf Date: Mon, 19 Oct 2026 15:35:44 +0500 Subject: [PATCH 074/269] feat(libSceCesCs): implement Unicode scalar conversions --- core/libs/prx/libSceCesCs/CMakeLists.txt | 17 ++ core/libs/prx/libSceCesCs/CesLibrary.cmake | 4 +- core/libs/prx/libSceCesCs/Export.cpp | 24 +- core/libs/prx/libSceCesCs/Unicode.cpp | 204 +++++++++++++++++ core/libs/tests/GuestCesUnicode.cpp | 252 +++++++++++++++++++++ docs/dev/debt/libSceCesCs.md | 2 + 6 files changed, 493 insertions(+), 10 deletions(-) create mode 100644 core/libs/prx/libSceCesCs/Unicode.cpp create mode 100644 core/libs/tests/GuestCesUnicode.cpp diff --git a/core/libs/prx/libSceCesCs/CMakeLists.txt b/core/libs/prx/libSceCesCs/CMakeLists.txt index 674db3453e..2601651d05 100644 --- a/core/libs/prx/libSceCesCs/CMakeLists.txt +++ b/core/libs/prx/libSceCesCs/CMakeLists.txt @@ -15,3 +15,20 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endif() + +if(BUILD_TESTING) + foreach(variant IN ITEMS unicode unicode_module) + add_test_executable(guest_ces_${variant}_tests ${LIBS_INCLUDE_DIR}/tests/GuestCesUnicode.cpp) + target_include_directories(guest_ces_${variant}_tests PRIVATE ${LIBS_INCLUDE_DIR}) + if(variant STREQUAL "unicode_module") + target_link_libraries(guest_ces_${variant}_tests PRIVATE libSceCesCs-module) + else() + target_link_libraries(guest_ces_${variant}_tests PRIVATE libSceCesCs) + endif() + add_test(NAME guest_ces_${variant} COMMAND guest_ces_${variant}_tests) + endforeach() + if(WIN32) + set_tests_properties(guest_ces_unicode guest_ces_unicode_module PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceCesCs/CesLibrary.cmake b/core/libs/prx/libSceCesCs/CesLibrary.cmake index dcad1c7bd6..da7980798a 100644 --- a/core/libs/prx/libSceCesCs/CesLibrary.cmake +++ b/core/libs/prx/libSceCesCs/CesLibrary.cmake @@ -1,5 +1,7 @@ function(add_sce_ces_library target) - add_library(${target} SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Export.cpp) + add_library(${target} SHARED EXCLUDE_FROM_ALL + ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Export.cpp + ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Unicode.cpp) target_include_directories(${target} PRIVATE ${LIBS_INCLUDE_DIR}) configure_windows_unwind(${target}) endfunction() diff --git a/core/libs/prx/libSceCesCs/Export.cpp b/core/libs/prx/libSceCesCs/Export.cpp index ec650f1b6f..f6c91a13dc 100644 --- a/core/libs/prx/libSceCesCs/Export.cpp +++ b/core/libs/prx/libSceCesCs/Export.cpp @@ -405,15 +405,21 @@ int APS5_VABI sceCesUtf8ToSbc(const uint8_t* utf8, uint32_t utf8max, uint32_t* u return 0; } -int APS5_VABI sceCesEucJpToUtf8(void* profile, uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucLength, uint8_t* utf8Buffer, uint32_t utf8Max, uint32_t* utf8Length) { - (void)profile; - (void)eucBuffer; - (void)eucMax; - (void)eucLength; - (void)utf8Buffer; - (void)utf8Max; - (void)utf8Length; - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceCesEucJpToUtf8(const void* profile, const uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucLength, uint8_t* utf8Buffer, uint32_t utf8Max, uint32_t* utf8Length) { + if (!isEucJp(profile)) return CES_ERROR_INVALID_PARAMETER; + if (!eucBuffer) return CES_ERROR_INVALID_SRC_BUFFER; + if (eucMax == 0) return CES_ERROR_SRC_BUFFER_END; + if (!utf8Buffer) return CES_ERROR_INVALID_DST_BUFFER; + uint32_t consumed = 0; + uint32_t code = 0; + if (const int result = decodeMbcs(profile, eucBuffer, eucMax, consumed, code); result != 0) return result; + uint8_t encoded[3]; + const uint32_t produced = encodeUtf8(code, encoded); + if (utf8Max < produced) return CES_ERROR_DST_BUFFER_END; + std::memcpy(utf8Buffer, encoded, produced); + if (eucLength) *eucLength = consumed; + if (utf8Length) *utf8Length = produced; + return 0; } int APS5_VABI sceCesEucKrToUtf8(void* profile, uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucLength, uint8_t* utf8Buffer, uint32_t utf8Max, uint32_t* utf8Length) { diff --git a/core/libs/prx/libSceCesCs/Unicode.cpp b/core/libs/prx/libSceCesCs/Unicode.cpp new file mode 100644 index 0000000000..5a4b10399b --- /dev/null +++ b/core/libs/prx/libSceCesCs/Unicode.cpp @@ -0,0 +1,204 @@ +#include +#include +#include +#include "prx/libc/include/general/VabiMacros.hpp" + +namespace { + +constexpr int INVALID_SRC_BUFFER = static_cast(0x805C0010); +constexpr int SRC_BUFFER_END = static_cast(0x805C0011); +constexpr int INVALID_ENCODE = static_cast(0x805C0014); +constexpr int UNASSIGNED_CODE = static_cast(0x805C0020); +constexpr int INVALID_DST_BUFFER = static_cast(0x805C0030); +constexpr int DST_BUFFER_END = static_cast(0x805C0031); +constexpr bool NATIVE_BIG_ENDIAN = std::endian::native == std::endian::big; + +bool isScalar(uint32_t code) { + return code <= 0x10FFFF && (code < 0xD800 || code > 0xDFFF); +} + +int decodeUtf8(const uint8_t* source, uint32_t available, uint32_t& code, uint32_t& length) { + if (!source) return INVALID_SRC_BUFFER; + if (available == 0) return SRC_BUFFER_END; + const uint8_t lead = source[0]; + if (lead <= 0x7F) { code = lead; length = 1; return 0; } + if (lead >= 0xC2 && lead <= 0xDF) { code = lead & 0x1F; length = 2; } + else if (lead >= 0xE0 && lead <= 0xEF) { code = lead & 0x0F; length = 3; } + else if (lead >= 0xF0 && lead <= 0xF4) { code = lead & 0x07; length = 4; } + else return INVALID_ENCODE; + for (uint32_t i = 1; i < length; ++i) { + if (i >= available) return SRC_BUFFER_END; + const uint8_t next = source[i]; + if (next < 0x80 || next > 0xBF) return INVALID_ENCODE; + if (i == 1 && ((lead == 0xE0 && next < 0xA0) || (lead == 0xED && next > 0x9F) + || (lead == 0xF0 && next < 0x90) || (lead == 0xF4 && next > 0x8F))) return INVALID_ENCODE; + code = code << 6 | (next & 0x3F); + } + return 0; +} + +int encodeUtf8(uint32_t code, uint8_t* target, uint32_t capacity, uint32_t* targetLength) { + if (!isScalar(code)) return INVALID_ENCODE; + if (!target) return INVALID_DST_BUFFER; + uint8_t bytes[4]; + uint32_t length = 0; + if (code < 0x80) { bytes[0] = static_cast(code); length = 1; } + else if (code < 0x800) { + bytes[0] = static_cast(0xC0 | code >> 6); + bytes[1] = static_cast(0x80 | (code & 0x3F)); + length = 2; + } else if (code < 0x10000) { + bytes[0] = static_cast(0xE0 | code >> 12); + bytes[1] = static_cast(0x80 | (code >> 6 & 0x3F)); + bytes[2] = static_cast(0x80 | (code & 0x3F)); + length = 3; + } else { + bytes[0] = static_cast(0xF0 | code >> 18); + bytes[1] = static_cast(0x80 | (code >> 12 & 0x3F)); + bytes[2] = static_cast(0x80 | (code >> 6 & 0x3F)); + bytes[3] = static_cast(0x80 | (code & 0x3F)); + length = 4; + } + if (capacity < length) return DST_BUFFER_END; + std::memcpy(target, bytes, length); + if (targetLength) *targetLength = length; + return 0; +} + +template +uint32_t loadUnit(const Unit* source) { + const auto* bytes = reinterpret_cast(source); + uint32_t code = 0; + for (uint32_t i = 0; i < sizeof(Unit); ++i) code |= uint32_t(bytes[i]) << (8 * (BigEndian ? sizeof(Unit) - 1 - i : i)); + return code; +} + +template +void storeUnit(Unit* target, uint32_t code) { + auto* bytes = reinterpret_cast(target); + for (uint32_t i = 0; i < sizeof(Unit); ++i) bytes[i] = static_cast(code >> (8 * (BigEndian ? sizeof(Unit) - 1 - i : i))); +} + +template +int utf8ToUtf32(const uint8_t* source, uint32_t available, uint32_t* sourceLength, uint32_t* target) { + uint32_t code = 0; + uint32_t length = 0; + if (const int result = decodeUtf8(source, available, code, length); result != 0) return result; + if (!target) return INVALID_DST_BUFFER; + storeUnit(target, code); + if (sourceLength) *sourceLength = length; + return 0; +} + +template +int utf8ToUtf16(const uint8_t* source, uint32_t available, uint32_t* sourceLength, uint16_t* target, uint32_t capacity, uint32_t* targetLength) { + uint32_t code = 0; + uint32_t length = 0; + if (const int result = decodeUtf8(source, available, code, length); result != 0) return result; + if (!target) return INVALID_DST_BUFFER; + const uint32_t units = code < 0x10000 ? 1 : 2; + if (capacity < units) return DST_BUFFER_END; + if (units == 1) storeUnit(target, code); + else { + code -= 0x10000; + storeUnit(target, 0xD800 | code >> 10); + storeUnit(target + 1, 0xDC00 | (code & 0x3FF)); + } + if (sourceLength) *sourceLength = length; + if (targetLength) *targetLength = units; + return 0; +} + +template +int utf16ToUtf8(const uint16_t* source, uint32_t available, uint32_t* sourceLength, uint8_t* target, uint32_t capacity, uint32_t* targetLength) { + if (!source) return INVALID_SRC_BUFFER; + if (available == 0) return SRC_BUFFER_END; + uint32_t code = loadUnit(source); + uint32_t units = 1; + if (code >= 0xDC00 && code <= 0xDFFF) return INVALID_ENCODE; + if (code >= 0xD800 && code <= 0xDBFF) { + if (available < 2) return SRC_BUFFER_END; + const uint32_t low = loadUnit(source + 1); + if (low < 0xDC00 || low > 0xDFFF) return INVALID_ENCODE; + code = 0x10000 + ((code - 0xD800) << 10) + low - 0xDC00; + units = 2; + } + if (const int result = encodeUtf8(code, target, capacity, targetLength); result != 0) return result; + if (sourceLength) *sourceLength = units; + return 0; +} + +template +int utf32ToUtf8(const uint32_t* source, uint8_t* target, uint32_t capacity, uint32_t* targetLength) { + if (!source) return INVALID_SRC_BUFFER; + return encodeUtf8(loadUnit(source), target, capacity, targetLength); +} + +} + +extern "C" { + +int APS5_VABI sceCesUcs2ToUtf8(uint16_t source, uint8_t* target, uint32_t capacity, uint32_t* targetLength) { + return encodeUtf8(source, target, capacity, targetLength); +} + +int APS5_VABI sceCesUtf8ToUcs2(const uint8_t* source, uint32_t available, uint32_t* sourceLength, uint16_t* target) { + uint32_t code = 0; + uint32_t length = 0; + if (const int result = decodeUtf8(source, available, code, length); result != 0) return result; + if (!target) return INVALID_DST_BUFFER; + if (code > 0xFFFF) return UNASSIGNED_CODE; + *target = static_cast(code); + if (sourceLength) *sourceLength = length; + return 0; +} + +int APS5_VABI sceCesUtf8ToUtf32(const uint8_t* source, uint32_t available, uint32_t* sourceLength, uint32_t* target) { + return utf8ToUtf32(source, available, sourceLength, target); +} + +int APS5_VABI sceCesUtf8ToUtf32be(const uint8_t* source, uint32_t available, uint32_t* sourceLength, uint32_t* target) { + return utf8ToUtf32(source, available, sourceLength, target); +} + +int APS5_VABI sceCesUtf8ToUtf32le(const uint8_t* source, uint32_t available, uint32_t* sourceLength, uint32_t* target) { + return utf8ToUtf32(source, available, sourceLength, target); +} + +int APS5_VABI sceCesUtf8ToUtf16(const uint8_t* source, uint32_t available, uint32_t* sourceLength, uint16_t* target, uint32_t capacity, uint32_t* targetLength) { + return utf8ToUtf16(source, available, sourceLength, target, capacity, targetLength); +} + +int APS5_VABI sceCesUtf8ToUtf16be(const uint8_t* source, uint32_t available, uint32_t* sourceLength, uint16_t* target, uint32_t capacity, uint32_t* targetLength) { + return utf8ToUtf16(source, available, sourceLength, target, capacity, targetLength); +} + +int APS5_VABI sceCesUtf8ToUtf16le(const uint8_t* source, uint32_t available, uint32_t* sourceLength, uint16_t* target, uint32_t capacity, uint32_t* targetLength) { + return utf8ToUtf16(source, available, sourceLength, target, capacity, targetLength); +} + +int APS5_VABI sceCesUtf16ToUtf8(const uint16_t* source, uint32_t available, uint32_t* sourceLength, uint8_t* target, uint32_t capacity, uint32_t* targetLength) { + return utf16ToUtf8(source, available, sourceLength, target, capacity, targetLength); +} + +int APS5_VABI sceCesUtf16beToUtf8(const uint16_t* source, uint32_t available, uint32_t* sourceLength, uint8_t* target, uint32_t capacity, uint32_t* targetLength) { + return utf16ToUtf8(source, available, sourceLength, target, capacity, targetLength); +} + +int APS5_VABI sceCesUtf16leToUtf8(const uint16_t* source, uint32_t available, uint32_t* sourceLength, uint8_t* target, uint32_t capacity, uint32_t* targetLength) { + return utf16ToUtf8(source, available, sourceLength, target, capacity, targetLength); +} + +int APS5_VABI sceCesUtf32ToUtf8(uint32_t source, uint8_t* target, uint32_t capacity, uint32_t* targetLength) { + return encodeUtf8(source, target, capacity, targetLength); +} + +int APS5_VABI sceCesUtf32beToUtf8(const uint32_t* source, uint8_t* target, uint32_t capacity, uint32_t* targetLength) { + return utf32ToUtf8(source, target, capacity, targetLength); +} + +int APS5_VABI sceCesUtf32leToUtf8(const uint32_t* source, uint8_t* target, uint32_t capacity, uint32_t* targetLength) { + return utf32ToUtf8(source, target, capacity, targetLength); +} + +} diff --git a/core/libs/tests/GuestCesUnicode.cpp b/core/libs/tests/GuestCesUnicode.cpp new file mode 100644 index 0000000000..1603f09810 --- /dev/null +++ b/core/libs/tests/GuestCesUnicode.cpp @@ -0,0 +1,252 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libSceCesCs/JisX0208.hpp" +#include "prx/libSceCesCs/JisX0212.hpp" +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceCesUcs2ToUtf8(uint16_t, uint8_t*, uint32_t, uint32_t*); +int APS5_VABI sceCesUtf8ToUcs2(const uint8_t*, uint32_t, uint32_t*, uint16_t*); +int APS5_VABI sceCesUtf32ToUtf8(uint32_t, uint8_t*, uint32_t, uint32_t*); +int APS5_VABI sceCesUtf32beToUtf8(const uint32_t*, uint8_t*, uint32_t, uint32_t*); +int APS5_VABI sceCesUtf32leToUtf8(const uint32_t*, uint8_t*, uint32_t, uint32_t*); +int APS5_VABI sceCesUtf8ToUtf32(const uint8_t*, uint32_t, uint32_t*, uint32_t*); +int APS5_VABI sceCesUtf8ToUtf32be(const uint8_t*, uint32_t, uint32_t*, uint32_t*); +int APS5_VABI sceCesUtf8ToUtf32le(const uint8_t*, uint32_t, uint32_t*, uint32_t*); +int APS5_VABI sceCesUtf8ToUtf16(const uint8_t*, uint32_t, uint32_t*, uint16_t*, uint32_t, uint32_t*); +int APS5_VABI sceCesUtf8ToUtf16be(const uint8_t*, uint32_t, uint32_t*, uint16_t*, uint32_t, uint32_t*); +int APS5_VABI sceCesUtf8ToUtf16le(const uint8_t*, uint32_t, uint32_t*, uint16_t*, uint32_t, uint32_t*); +int APS5_VABI sceCesUtf16ToUtf8(const uint16_t*, uint32_t, uint32_t*, uint8_t*, uint32_t, uint32_t*); +int APS5_VABI sceCesUtf16beToUtf8(const uint16_t*, uint32_t, uint32_t*, uint8_t*, uint32_t, uint32_t*); +int APS5_VABI sceCesUtf16leToUtf8(const uint16_t*, uint32_t, uint32_t*, uint8_t*, uint32_t, uint32_t*); +void* APS5_VABI sceCesUcsProfileInitEucJpCp51932(void*); +int APS5_VABI sceCesEucJpToUtf8(const void*, const uint8_t*, uint32_t, uint32_t*, uint8_t*, uint32_t, uint32_t*); +} + +namespace { + +constexpr int INVALID_PARAMETER = static_cast(0x805C0001); +constexpr int INVALID_SRC_BUFFER = static_cast(0x805C0010); +constexpr int SRC_BUFFER_END = static_cast(0x805C0011); +constexpr int INVALID_ENCODE = static_cast(0x805C0014); +constexpr int UNASSIGNED_CODE = static_cast(0x805C0020); +constexpr int INVALID_DST_BUFFER = static_cast(0x805C0030); +constexpr int DST_BUFFER_END = static_cast(0x805C0031); +constexpr uint32_t SENTINEL = 0xA5A5A5A5; + +void Require(bool value) { + if (!value) std::abort(); +} + +void Hash(uint64_t& hash, const void* data, uint32_t size) { + const auto* bytes = static_cast(data); + for (uint32_t i = 0; i < size; ++i) hash = (hash ^ bytes[i]) * 1099511628211ull; +} + +void TestByteOrder() { + const uint8_t source[] = {0xF0, 0x9F, 0x98, 0x80}; + const uint8_t expected16be[] = {0xD8, 0x3D, 0xDE, 0x00}; + const uint8_t expected16le[] = {0x3D, 0xD8, 0x00, 0xDE}; + const uint8_t expected32be[] = {0x00, 0x01, 0xF6, 0x00}; + const uint8_t expected32le[] = {0x00, 0xF6, 0x01, 0x00}; + uint16_t units[2] = {}; + uint32_t scalar = 0; + Require(sceCesUtf8ToUtf16be(source, 4, nullptr, units, 2, nullptr) == 0 && std::memcmp(units, expected16be, 4) == 0); + Require(sceCesUtf8ToUtf16le(source, 4, nullptr, units, 2, nullptr) == 0 && std::memcmp(units, expected16le, 4) == 0); + Require(sceCesUtf8ToUtf32be(source, 4, nullptr, &scalar) == 0 && std::memcmp(&scalar, expected32be, 4) == 0); + Require(sceCesUtf8ToUtf32le(source, 4, nullptr, &scalar) == 0 && std::memcmp(&scalar, expected32le, 4) == 0); + const uint8_t bom[] = {0xEF, 0xBB, 0xBF}; + Require(sceCesUtf8ToUtf32(bom, 3, nullptr, &scalar) == 0 && scalar == 0xFEFF); + Require(sceCesUtf8ToUtf16(bom, 3, nullptr, units, 1, nullptr) == 0 && units[0] == 0xFEFF); +} + +void TestScalars() { + uint64_t utf8Hash = 14695981039346656037ull; + uint64_t utf16Hash = utf8Hash; + for (uint32_t code = 0; code <= 0x10FFFF; ++code) { + if (code >= 0xD800 && code <= 0xDFFF) continue; + std::array bytes; + bytes.fill(0xA5); + uint32_t produced = SENTINEL; + Require(sceCesUtf32ToUtf8(code, bytes.data(), 4, &produced) == 0); + Require(produced >= 1 && produced <= 4 && bytes[produced] == 0xA5); + Hash(utf8Hash, bytes.data(), produced); + uint32_t decoded = SENTINEL; + uint32_t consumed = SENTINEL; + Require(sceCesUtf8ToUtf32(bytes.data(), produced, &consumed, &decoded) == 0); + Require(decoded == code && consumed == produced); + + uint16_t ucs2 = 0xA5A5; + consumed = SENTINEL; + const int ucsResult = sceCesUtf8ToUcs2(bytes.data(), produced, &consumed, &ucs2); + std::array back; + back.fill(0xA5); + uint32_t size = SENTINEL; + if (code <= 0xFFFF) { + Require(ucsResult == 0 && ucs2 == code && consumed == produced); + Require(sceCesUcs2ToUtf8(ucs2, back.data(), 4, &size) == 0); + Require(size == produced && std::memcmp(back.data(), bytes.data(), size) == 0); + } else Require(ucsResult == UNASSIGNED_CODE && ucs2 == 0xA5A5 && consumed == SENTINEL); + + using To16 = int (APS5_VABI *)(const uint8_t*, uint32_t, uint32_t*, uint16_t*, uint32_t, uint32_t*); + using From16 = int (APS5_VABI *)(const uint16_t*, uint32_t, uint32_t*, uint8_t*, uint32_t, uint32_t*); + const To16 to16[] = {sceCesUtf8ToUtf16, sceCesUtf8ToUtf16be, sceCesUtf8ToUtf16le}; + const From16 from16[] = {sceCesUtf16ToUtf8, sceCesUtf16beToUtf8, sceCesUtf16leToUtf8}; + for (uint32_t variant = 0; variant < 3; ++variant) { + uint16_t units[3] = {0xA5A5, 0xA5A5, 0xA5A5}; + uint32_t unitCount = SENTINEL; + consumed = SENTINEL; + Require(to16[variant](bytes.data(), produced, &consumed, units, 2, &unitCount) == 0); + Require(consumed == produced && unitCount == (code <= 0xFFFF ? 1u : 2u) && units[unitCount] == 0xA5A5); + if (variant == 2) Hash(utf16Hash, units, 2 * unitCount); + consumed = size = SENTINEL; + Require(from16[variant](units, unitCount, &consumed, back.data(), 4, &size) == 0); + Require(consumed == unitCount && size == produced && std::memcmp(back.data(), bytes.data(), size) == 0); + } + + using To32 = int (APS5_VABI *)(const uint8_t*, uint32_t, uint32_t*, uint32_t*); + using From32 = int (APS5_VABI *)(const uint32_t*, uint8_t*, uint32_t, uint32_t*); + const To32 to32[] = {sceCesUtf8ToUtf32be, sceCesUtf8ToUtf32le}; + const From32 from32[] = {sceCesUtf32beToUtf8, sceCesUtf32leToUtf8}; + for (uint32_t variant = 0; variant < 2; ++variant) { + uint32_t stored[2] = {SENTINEL, SENTINEL}; + Require(to32[variant](bytes.data(), produced, &consumed, stored) == 0); + Require(consumed == produced && stored[1] == SENTINEL); + const auto* raw = reinterpret_cast(stored); + Require(raw[variant == 0 ? 0 : 3] == 0 && raw[variant == 0 ? 3 : 0] == (code & 0xFF)); + Require(from32[variant](stored, back.data(), 4, &size) == 0); + Require(size == produced && std::memcmp(back.data(), bytes.data(), size) == 0); + } + } + Require(utf8Hash == 0x957CC0987E6013E5ull); + Require(utf16Hash == 0x6A25A430597EC325ull); + std::puts("1112064 Unicode scalars match Python UTF-8 and UTF-16LE reference hashes"); +} + +void TestErrors() { + const uint8_t emoji[] = {0xF0, 0x9F, 0x98, 0x80, 'X'}; + uint32_t consumed = SENTINEL; + uint32_t produced = SENTINEL; + uint32_t scalar = SENTINEL; + uint16_t units[3] = {0xA5A5, 0xA5A5, 0xA5A5}; + uint8_t bytes[5] = {0xA5, 0xA5, 0xA5, 0xA5, 0xA5}; + for (uint32_t cap = 0; cap < 4; ++cap) { + Require(sceCesUtf32ToUtf8(0x1F600, bytes, cap, &produced) == DST_BUFFER_END); + Require(produced == SENTINEL && bytes[0] == 0xA5 && bytes[3] == 0xA5); + Require(sceCesUtf8ToUtf32(emoji, cap, &consumed, &scalar) == SRC_BUFFER_END); + Require(consumed == SENTINEL && scalar == SENTINEL); + } + for (uint32_t cap = 0; cap < 2; ++cap) { + Require(sceCesUtf8ToUtf16(emoji, 4, &consumed, units, cap, &produced) == DST_BUFFER_END); + Require(consumed == SENTINEL && produced == SENTINEL && units[0] == 0xA5A5 && units[1] == 0xA5A5); + } + const uint8_t invalid[][4] = {{0x80}, {0xC0, 0x80}, {0xC1, 0xBF}, {0xC2, 0x7F}, {0xE0, 0x9F}, + {0xED, 0xA0}, {0xF0, 0x8F}, {0xF4, 0x90}, {0xF5}, {0xFF}, {0xE2, 0x82, 0x41}, {0xF0, 0x9F, 0x98, 0x41}}; + const uint32_t lengths[] = {1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 3, 4}; + for (uint32_t i = 0; i < std::size(invalid); ++i) { + Require(sceCesUtf8ToUtf32(invalid[i], lengths[i], &consumed, &scalar) == INVALID_ENCODE); + Require(sceCesUtf8ToUtf16be(invalid[i], lengths[i], &consumed, units, 2, &produced) == INVALID_ENCODE); + Require(consumed == SENTINEL && produced == SENTINEL && scalar == SENTINEL && units[0] == 0xA5A5); + } + for (uint32_t code : {0xD800u, 0xDBFFu, 0xDC00u, 0xDFFFu, 0x110000u, 0xFFFFFFFFu}) { + Require(sceCesUtf32ToUtf8(code, bytes, 4, &produced) == INVALID_ENCODE); + if (code <= 0xFFFF) Require(sceCesUcs2ToUtf8(static_cast(code), bytes, 4, &produced) == INVALID_ENCODE); + Require(produced == SENTINEL && bytes[0] == 0xA5); + } + const uint16_t high[] = {0xD800, 0x0041}; + const uint16_t low[] = {0xDC00}; + Require(sceCesUtf16ToUtf8(high, 1, &consumed, bytes, 4, &produced) == SRC_BUFFER_END); + Require(sceCesUtf16ToUtf8(high, 2, &consumed, bytes, 4, &produced) == INVALID_ENCODE); + Require(sceCesUtf16ToUtf8(low, 1, &consumed, bytes, 4, &produced) == INVALID_ENCODE); + Require(consumed == SENTINEL && produced == SENTINEL && bytes[0] == 0xA5); + Require(sceCesUtf8ToUtf32(nullptr, 4, &consumed, &scalar) == INVALID_SRC_BUFFER); + Require(sceCesUtf8ToUtf32(emoji, 4, &consumed, nullptr) == INVALID_DST_BUFFER); + Require(sceCesUtf8ToUcs2(emoji, 4, &consumed, nullptr) == INVALID_DST_BUFFER); + Require(sceCesUtf8ToUtf16(emoji, 4, &consumed, nullptr, 2, &produced) == INVALID_DST_BUFFER); + Require(sceCesUtf16ToUtf8(nullptr, 1, &consumed, bytes, 4, &produced) == INVALID_SRC_BUFFER); + Require(sceCesUtf16ToUtf8(high, 0, &consumed, bytes, 4, &produced) == SRC_BUFFER_END); + Require(sceCesUtf32beToUtf8(nullptr, bytes, 4, &produced) == INVALID_SRC_BUFFER); + Require(sceCesUtf32ToUtf8(0, nullptr, 4, &produced) == INVALID_DST_BUFFER); + Require(sceCesUtf8ToUtf32(emoji, 5, nullptr, &scalar) == 0 && scalar == 0x1F600); + Require(sceCesUtf8ToUtf16(emoji, 5, nullptr, units, 2, nullptr) == 0 && units[0] == 0xD83D && units[1] == 0xDE00); + Require(sceCesUtf16ToUtf8(units, 2, nullptr, bytes, 4, nullptr) == 0 && std::memcmp(bytes, emoji, 4) == 0); + Require(sceCesUcs2ToUtf8(0, bytes, 1, nullptr) == 0 && bytes[0] == 0); + Require(sceCesUtf8ToUcs2(bytes, 1, nullptr, units) == 0 && units[0] == 0); +} + +void TestEucJp() { + std::array sheet{}; + const void* profile = sceCesUcsProfileInitEucJpCp51932(sheet.data()); + auto check = [&](const uint8_t* source, uint32_t length, uint32_t code) { + uint8_t expected[4] = {}; + uint32_t expectedLength = 0; + Require(sceCesUtf32ToUtf8(code, expected, 4, &expectedLength) == 0); + std::array output; + output.fill(0xA5); + uint32_t consumed = SENTINEL; + uint32_t produced = SENTINEL; + Require(sceCesEucJpToUtf8(profile, source, length, &consumed, output.data(), expectedLength, &produced) == 0); + Require(consumed == length && produced == expectedLength && output[produced] == 0xA5); + Require(std::memcmp(output.data(), expected, produced) == 0); + for (uint32_t cap = 0; cap < expectedLength; ++cap) { + output.fill(0xA5); + consumed = produced = SENTINEL; + Require(sceCesEucJpToUtf8(profile, source, length, &consumed, output.data(), cap, &produced) == DST_BUFFER_END); + Require(consumed == SENTINEL && produced == SENTINEL && output[0] == 0xA5); + } + }; + for (uint32_t code = 0; code < 128; ++code) { + const uint8_t source[] = {static_cast(code)}; + check(source, 1, code); + } + for (uint32_t cell = 0; cell < 63; ++cell) { + const uint8_t source[] = {0x8E, static_cast(0xA1 + cell)}; + check(source, 2, 0xFF61 + cell); + } + uint32_t assigned = 0; + for (uint32_t supplementary = 0; supplementary < 2; ++supplementary) { + for (uint32_t cell = 0; cell < 94 * 94; ++cell) { + const uint32_t code = supplementary ? JisX0212::TO_UCS2[cell] : JisX0208::TO_UCS2[cell]; + const uint8_t source[] = {0x8F, static_cast(0xA1 + cell / 94), static_cast(0xA1 + cell % 94)}; + const uint8_t* start = source + (supplementary ? 0 : 1); + const uint32_t length = supplementary ? 3 : 2; + if (code) { check(start, length, code); ++assigned; } + else { + uint8_t output[4] = {0xA5, 0xA5, 0xA5, 0xA5}; + uint32_t consumed = SENTINEL; + uint32_t produced = SENTINEL; + Require(sceCesEucJpToUtf8(profile, start, length, &consumed, output, 4, &produced) == UNASSIGNED_CODE); + Require(consumed == SENTINEL && produced == SENTINEL && output[0] == 0xA5); + } + } + } + Require(assigned == 13403); + const uint8_t source[] = {0x8F, 0xA2, 0xAF, 'X'}; + uint8_t output[4] = {0xA5, 0xA5, 0xA5, 0xA5}; + uint32_t consumed = SENTINEL; + uint32_t produced = SENTINEL; + Require(sceCesEucJpToUtf8(nullptr, source, 3, &consumed, output, 4, &produced) == INVALID_PARAMETER); + Require(sceCesEucJpToUtf8(profile, nullptr, 3, &consumed, output, 4, &produced) == INVALID_SRC_BUFFER); + Require(sceCesEucJpToUtf8(profile, source, 0, &consumed, output, 4, &produced) == SRC_BUFFER_END); + Require(sceCesEucJpToUtf8(profile, source, 3, &consumed, nullptr, 4, &produced) == INVALID_DST_BUFFER); + for (uint32_t size : {1u, 2u}) Require(sceCesEucJpToUtf8(profile, source, size, &consumed, output, 4, &produced) == SRC_BUFFER_END); + const uint8_t malformed[] = {0x8E, 0x41}; + Require(sceCesEucJpToUtf8(profile, malformed, 2, &consumed, output, 4, &produced) == INVALID_ENCODE); + Require(consumed == SENTINEL && produced == SENTINEL && output[0] == 0xA5); + Require(sceCesEucJpToUtf8(profile, source, 4, &consumed, output, 4, &produced) == 0); + Require(consumed == 3 && produced == 2 && output[0] == 0xCB && output[1] == 0x98 && output[2] == 0xA5); + Require(sceCesEucJpToUtf8(profile, source, 4, nullptr, output, 4, nullptr) == 0); + std::puts("13403 assigned JIS cells plus ASCII, half-width katakana and unassigned cells checked"); +} + +} + +int main() { + TestByteOrder(); + TestErrors(); + TestScalars(); + TestEucJp(); +} diff --git a/docs/dev/debt/libSceCesCs.md b/docs/dev/debt/libSceCesCs.md index 98512c9d7b..cd1e93a34e 100644 --- a/docs/dev/debt/libSceCesCs.md +++ b/docs/dev/debt/libSceCesCs.md @@ -8,4 +8,6 @@ ### Unknown function info +- [Unicode scalar conversions](../../../core/libs/prx/libSceCesCs/Unicode.cpp) (libSceCesCs) - console error precedence, optional length pointers and length units are unverified. Signatures follow the [Orbital type database](https://github.com/AlexAltea/orbital/blob/b321fcdbd700cc02da1cb1ad25412781d5ec73c6/tools/ida/db_types.json); names are in the aerolib table pinned by `tools/nid_names.py`. Encoding follows [Unicode section 3.9](https://www.unicode.org/versions/Unicode17.0.0/core-spec/chapter-3/). Each call converts one scalar, including zero and U+FEFF, without a terminator or byte-order detection; lengths count bytes for UTF-8 and 16-bit units for UTF-16. Native forms use host byte order (supported hosts are little-endian). Errors follow the existing CES converters, not console measurements, and failure leaves outputs unchanged. No console or title comparison has been run +- [sceCesEucJpToUtf8](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - the scalar conversion's errors and optional length pointers are unverified against the console. Its signature follows the Orbital type database linked above; it uses the existing CP51932 profile and JIS tables, with their mapping and profile-layout assumptions listed above, converts one character (including zero), and writes no terminator. Failure leaves the output and length pointers unchanged - [sceCesRefersUcsProfileCp1250](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - returns a profile accepted by `sceCesSbcToUtf8`, `sceCesUtf8ToSbc` and the MBCS context conversions, with the Windows-1250 table of Microsoft's [bestfit1250.txt](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WindowsBestFit/bestfit1250.txt), where the unassigned bytes map to their Latin-1 control characters as the Cp1252 and Cp1251 tables do; the console behavior for those bytes and the whole profile is not verified. PPSA34501 fails to start without this export (`qYu6y1E0Qbg`) From f4f471f415cd52138e60d3b174bc9c1421720518 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Mon, 19 Oct 2026 13:53:35 +0300 Subject: [PATCH 075/269] fix(libSceVideodec2): settle an access unit that reads as both Annex-B and AVCC A unit opening with 00 00 01 xx can also tile as AVCC when its first NAL is 0x1xx bytes long, in either direction. Annex-B requires a zero_byte before the first NAL of an access unit, so a unit opening with a 3-byte start code is taken as AVCC unless the decoder has already seen unambiguous Annex-B units that open that way. A unit opening with 00 00 00 01 that also tiles keeps the framing of the last unambiguous unit, Annex-B when there is none. Media Player Classic PS5 sends Annex-B units with 4-byte start codes and leaves exactly the colliding samples in AVCC, so the decoder's last framing alone would choose wrong there. --- core/libs/prx/libSceVideodec2/Export.cpp | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceVideodec2/Export.cpp b/core/libs/prx/libSceVideodec2/Export.cpp index e5daeed313..6350f757cd 100644 --- a/core/libs/prx/libSceVideodec2/Export.cpp +++ b/core/libs/prx/libSceVideodec2/Export.cpp @@ -304,6 +304,8 @@ struct Decoder { std::deque ready; std::vector annexB; std::vector parameterSets; + bool lengthPrefixed = false; + bool shortStartCodes = false; bool flushing = false; std::array vp9References{}; Vp9PictureInfo vp9Colors{}; @@ -388,7 +390,18 @@ bool StartsWithStartCode(const std::uint8_t* data, std::size_t size) { } const std::vector* ToAnnexB(Decoder& decoder, const std::uint8_t* data, std::size_t size) { - if (TilesAsAvcc(data, size)) { + const bool tiles = TilesAsAvcc(data, size); + const bool opensWithStartCode = StartsWithStartCode(data, size); + if (!tiles && !opensWithStartCode) throw std::runtime_error("Videodec2: access unit is neither Annex-B nor length-prefixed"); + const bool shortStartCode = opensWithStartCode && data[2] == 1; + bool lengthPrefixed = tiles; + if (tiles && opensWithStartCode) { + lengthPrefixed = shortStartCode ? !decoder.shortStartCodes : decoder.lengthPrefixed; + } else { + decoder.lengthPrefixed = tiles; + if (shortStartCode) decoder.shortStartCodes = true; + } + if (lengthPrefixed) { decoder.annexB.clear(); for (std::size_t index = 0; index < size;) { const std::size_t length = (static_cast(data[index]) << 24u) | (static_cast(data[index + 1]) << 16u) | (static_cast(data[index + 2]) << 8u) | data[index + 3]; @@ -399,7 +412,6 @@ const std::vector* ToAnnexB(Decoder& decoder, const std::uint8_t* } return &decoder.annexB; } - if (!StartsWithStartCode(data, size)) throw std::runtime_error("Videodec2: access unit is neither Annex-B nor length-prefixed"); decoder.annexB.assign(data, data + size); return &decoder.annexB; } From a23a24c1e7537dc746c794994aa2c3546c1b9775 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Mon, 19 Oct 2026 13:53:35 +0300 Subject: [PATCH 076/269] test(libSceVideodec2): decode units whose start code and length prefix collide Move the AVCC collision to the second unit, add an AVCC unit opening 00 00 01 23 among Annex-B units, and an Annex-B stream with 3-byte start codes whose SPS and PPS unit, padded with trailing zero bytes, also reads as one length prefix. --- core/libs/tests/GuestVideodec2.cpp | 76 ++++++++++++++++++++++++++---- 1 file changed, 66 insertions(+), 10 deletions(-) diff --git a/core/libs/tests/GuestVideodec2.cpp b/core/libs/tests/GuestVideodec2.cpp index f483c38881..a38a94c9c6 100644 --- a/core/libs/tests/GuestVideodec2.cpp +++ b/core/libs/tests/GuestVideodec2.cpp @@ -576,16 +576,7 @@ void testDecode(bool lengthPrefixed, bool separateParameters = false) { checkThrows([&] { sceVideodec2DeleteDecoder_nid_postfix(handle); }); } -void testLengthPrefixLooksLikeStartCode() { - auto units = accessUnits(true); - std::vector filler; - constexpr std::size_t fillerLength = 0x101; - for (int shift = 24; shift >= 0; shift -= 8) filler.push_back(static_cast(fillerLength >> shift)); - filler.push_back(0x0c); - filler.insert(filler.end(), fillerLength - 1, 0xff); - units.front().insert(units.front().begin(), filler.begin(), filler.end()); - check(units.front()[0] == 0 && units.front()[1] == 0 && units.front()[2] == 1, "the crafted unit does not begin with start-code bytes"); - +void decodeInOrder(const std::vector>& units) { const DecoderConfigInfo config{sizeof(DecoderConfigInfo), 0, 1, 100, 0, static_cast(Width), static_cast(Height), 4, 1}; DecoderMemoryInfo memory{sizeof(DecoderMemoryInfo)}; check(sceVideodec2QueryDecoderMemoryInfo_nid_postfix(&config, &memory) == 0, "memory query failed"); @@ -618,6 +609,70 @@ void testLengthPrefixLooksLikeStartCode() { check(sceVideodec2DeleteDecoder_nid_postfix(handle) == 0, "delete failed"); } +bool readsAsOneLengthPrefix(const std::vector& unit) { + const std::size_t length = (static_cast(unit[0]) << 24u) | (static_cast(unit[1]) << 16u) | (static_cast(unit[2]) << 8u) | unit[3]; + return length + 4 == unit.size(); +} + +std::vector lengthPrefixedFiller(std::size_t length) { + std::vector filler; + for (int shift = 24; shift >= 0; shift -= 8) filler.push_back(static_cast(length >> shift)); + filler.push_back(0x0c); + filler.insert(filler.end(), length - 1, 0xff); + return filler; +} + +void testLengthPrefixLooksLikeStartCode() { + auto units = accessUnits(true); + const auto filler = lengthPrefixedFiller(0x101); + check(!(units[0][0] == 0 && units[0][1] == 0 && (units[0][2] == 1 || units[0][3] == 1)), "the first length-prefixed unit already begins with start-code bytes"); + units[1].insert(units[1].begin(), filler.begin(), filler.end()); + check(units[1][0] == 0 && units[1][1] == 0 && units[1][2] == 1, "the crafted unit does not begin with start-code bytes"); + decodeInOrder(units); + + auto mixed = accessUnits(false); + const auto mixedFiller = lengthPrefixedFiller(0x123); + mixed[1] = accessUnits(true)[1]; + mixed[1].insert(mixed[1].begin(), mixedFiller.begin(), mixedFiller.end()); + check(mixed[0][2] == 0 && mixed[0][3] == 1 && mixed[1][0] == 0 && mixed[1][1] == 0 && mixed[1][2] == 1, "the length-prefixed unit does not follow an Annex-B unit with a four-byte start code"); + decodeInOrder(mixed); +} + +void testStartCodeLooksLikeLengthPrefix() { + auto units = accessUnits(false); + for (auto& unit : units) { + check(unit[0] == 0 && unit[1] == 0 && unit[2] == 0 && unit[3] == 1, "an access unit does not open with a four-byte start code"); + unit.erase(unit.begin()); + } + auto& first = units.front(); + std::size_t sequence = 0; + std::size_t afterPicture = 0; + bool picture = false; + for (std::size_t index = 0; index + 3 < first.size(); ++index) { + if (first[index] != 0 || first[index + 1] != 0 || first[index + 2] != 1) continue; + const auto type = first[index + 3] & 0x1fu; + if (picture) { + afterPicture = first[index - 1] == 0 ? index - 1 : index; + break; + } + if (type == 7 && sequence == 0) sequence = index; + if (type == 8 && sequence != 0) picture = true; + index += 2; + } + check(sequence != 0 && afterPicture > sequence, "the first access unit has no SPS and PPS before another NAL"); + const std::vector delimiter(first.begin(), first.begin() + static_cast(first[sequence - 1] == 0 ? sequence - 1 : sequence)); + check(delimiter.size() >= 4 && delimiter[2] == 1 && !readsAsOneLengthPrefix(delimiter), "the access unit delimiter is not an unambiguous unit with a three-byte start code"); + std::vector parameters(first.begin() + static_cast(sequence), first.begin() + static_cast(afterPicture)); + first.erase(first.begin(), first.begin() + static_cast(afterPicture)); + if (first[2] == 0) first.erase(first.begin()); + const std::size_t padded = 4 + ((std::size_t{1} << 8u) | parameters[3]); + check(parameters.size() < padded, "the parameter sets are too long to pad"); + parameters.resize(padded, 0); + check(readsAsOneLengthPrefix(parameters), "the padded parameter sets do not read as one length-prefixed NAL"); + units.insert(units.begin(), {delimiter, parameters}); + decodeInOrder(units); +} + std::uint64_t createDecoder(std::uint32_t codec, std::uint32_t profile) { const DecoderConfigInfo config{sizeof(DecoderConfigInfo), 1, codec, profile, 0, static_cast(Width), static_cast(Height), 4, 1}; DecoderMemoryInfo memory{sizeof(DecoderMemoryInfo)}; @@ -827,6 +882,7 @@ int main() { testDecode(false); testDecode(true); testLengthPrefixLooksLikeStartCode(); + testStartCodeLooksLikeLengthPrefix(); testDecode(false, true); const auto hevc = hevcUnits(HevcStream.data(), HevcStream.size()); testClip(CodecHevc, 1, hevc, HevcHashes); From 4faf57a98660c34afbc0cc3b614f09ce2a90d8f9 Mon Sep 17 00:00:00 2001 From: Lindamea1 <322051987+Lindamea1@users.noreply.github.com> Date: Mon, 19 Oct 2026 10:59:14 +0100 Subject: [PATCH 077/269] feat(libs): add the Voice and IME exports PPSA24191 needs to load Monster Hunter Wilds imports sceVoicePausePort, sceVoiceResumePort and sceImeSetConsecutiveClauseConversionEnabled, which no library exported, so the loader stopped at the first of them. sceVoicePausePort and sceVoiceResumePort check the library and the port like sceVoiceResetPort. sceImeSetConsecutiveClauseConversionEnabled has no public reference and throws. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceIme/Export.cpp | 6 ++++++ core/libs/prx/libSceVoice/Export.cpp | 14 ++++++++++++++ core/libs/tests/GuestVoice.cpp | 16 ++++++++++++++++ docs/dev/debt/libSceIme.md | 1 + docs/dev/debt/libSceVoice.md | 1 + 5 files changed, 38 insertions(+) diff --git a/core/libs/prx/libSceIme/Export.cpp b/core/libs/prx/libSceIme/Export.cpp index 575463cf73..10b8811f4b 100644 --- a/core/libs/prx/libSceIme/Export.cpp +++ b/core/libs/prx/libSceIme/Export.cpp @@ -415,6 +415,12 @@ int APS5_VABI sceImeSetCaret(const Caret* caret) { return 0; } +int APS5_VABI sceImeSetConsecutiveClauseConversionEnabled(bool enabled) { + (void)enabled; + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceImeSetText(const char16_t* text, uint32_t length) { std::lock_guard lock(g_imeMutex); if (!g_session) return ErrorNotOpened; diff --git a/core/libs/prx/libSceVoice/Export.cpp b/core/libs/prx/libSceVoice/Export.cpp index bcc56dcb98..b0f7906221 100644 --- a/core/libs/prx/libSceVoice/Export.cpp +++ b/core/libs/prx/libSceVoice/Export.cpp @@ -255,6 +255,20 @@ int APS5_VABI sceVoiceResetPort(uint32_t port_id) { return 0; } +int APS5_VABI sceVoicePausePort(uint32_t port_id) { + 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__); + return 0; +} + int APS5_VABI sceVoiceDisableChat(void) { auto& voice = State(); std::lock_guard lock(voice.mutex); diff --git a/core/libs/tests/GuestVoice.cpp b/core/libs/tests/GuestVoice.cpp index 98c2b5f187..3e3bd815fc 100644 --- a/core/libs/tests/GuestVoice.cpp +++ b/core/libs/tests/GuestVoice.cpp @@ -18,6 +18,8 @@ int APS5_VABI sceVoiceGetMuteFlag(std::uint32_t, bool*); int APS5_VABI sceVoiceGetPortInfo(std::uint32_t, VoicePortInfo*); int APS5_VABI sceVoiceReadFromOPort(std::uint32_t, void*, std::uint32_t*); int APS5_VABI sceVoiceResetPort(std::uint32_t); +int APS5_VABI sceVoicePausePort(std::uint32_t); +int APS5_VABI sceVoiceResumePort(std::uint32_t); int APS5_VABI sceVoiceGetResourceInfo(VoiceResourceInfo*); int APS5_VABI sceVoiceEnableChat(void); int APS5_VABI sceVoiceDisableChat(void); @@ -47,6 +49,8 @@ std::uint32_t CreatePort(std::int32_t type, float volume) { int main() { RequireThrows([] { sceVoiceResetPort(0); }); + RequireThrows([] { sceVoicePausePort(0); }); + RequireThrows([] { sceVoiceResumePort(0); }); RequireThrows([] { VoiceResourceInfo info{}; sceVoiceGetResourceInfo(&info); }); RequireThrows([] { sceVoiceEnableChat(); }); RequireThrows([] { sceVoiceDisableChat(); }); @@ -75,6 +79,16 @@ int main() { Require(sceVoiceResetPort(output) == 0); RequireThrows([&] { sceVoiceResetPort(output + 1); }); + 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(output) == 0); + RequireThrows([&] { sceVoicePausePort(output + 1); }); + RequireThrows([&] { sceVoiceResumePort(output + 1); }); + VoicePortInfo after{}; Require(sceVoiceGetPortInfo(input, &after) == 0); Require(after.port_type == before.port_type && after.state == before.state && after.frame_size == before.frame_size); @@ -89,9 +103,11 @@ int main() { Require(sceVoiceDeletePort(input) == 0); RequireThrows([&] { sceVoiceResetPort(input); }); + RequireThrows([&] { sceVoicePausePort(input); }); Require(sceVoiceResetPort(output) == 0); Require(sceVoiceEnd_nid_postfix() == 0); RequireThrows([&] { sceVoiceResetPort(output); }); + RequireThrows([&] { sceVoiceResumePort(output); }); RequireThrows([] { VoiceResourceInfo info{}; sceVoiceGetResourceInfo(&info); }); RequireThrows([] { sceVoiceEnableChat(); }); RequireThrows([] { sceVoiceDisableChat(); }); diff --git a/docs/dev/debt/libSceIme.md b/docs/dev/debt/libSceIme.md index 76178899b3..85b50038ed 100644 --- a/docs/dev/debt/libSceIme.md +++ b/docs/dev/debt/libSceIme.md @@ -6,6 +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 - [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`. A text filter or an external keyboard filter throws, as their contract is unknown. `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 diff --git a/docs/dev/debt/libSceVoice.md b/docs/dev/debt/libSceVoice.md index 5983d70f39..5a1052da95 100644 --- a/docs/dev/debt/libSceVoice.md +++ b/docs/dev/debt/libSceVoice.md @@ -9,3 +9,4 @@ - [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 +- [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 From e8d68f482ce33754846d18e828a52bea39acec4d Mon Sep 17 00:00:00 2001 From: milescoviello Date: Mon, 19 Oct 2026 11:04:22 +0000 Subject: [PATCH 078/269] docs(libScePlayGo): describe the scenario-only chunk list without naming a title Co-Authored-By: Claude Opus 5.5 --- docs/dev/debt/libScePlayGo.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libScePlayGo.md b/docs/dev/debt/libScePlayGo.md index 749b0204ba..6773fb05bb 100644 --- a/docs/dev/debt/libScePlayGo.md +++ b/docs/dev/debt/libScePlayGo.md @@ -2,5 +2,5 @@ ### Unknown function info -- [libScePlayGo](../../../core/libs/prx/libScePlayGo/Export.cpp) - the chunk set comes from `playgo-chunkdefs.xml` (every listed chunk plus chunks 0 through the default chunk), otherwise from `sce_sys/playgo-chunk.dat`, whose `plgx` header is read as holding the chunk count as a 16-bit value at offset 0x0A and ids 0 to count - 1 are taken as valid. The field was identified on four dumps, where exactly that many 0x20-byte entries follow from offset 0x100 (RIDE 4 PPSA01599: 35, Persona 5 Royal PPSA05110: 25, LEGO Horizon Adventures PPSA14632 and Kena PPSA01802: 1); the rest of the format is not decoded and ids are assumed to be contiguous. Every listed chunk is reported as installed (`LOCAL_FAST`). Without either file, a package with `sce_sys/playgo-scenario.json` (PPSA01748 and Ghost of Tsushima PPSA02225, whose dumps list no chunks) accepts every chunk id as installed, and `scePlayGoGetChunkId` lists ids 0 to 999. PPSA02225 builds its set of installed chunks from that list, and with only chunk 0 listed it never reads its English audio, two of its music archives or its opening movie. 1000 is the room that title passes; 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) - 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. From 88f818410218ca4aa117d850354a50f0a1fe4b62 Mon Sep 17 00:00:00 2001 From: Rengundom Date: Mon, 19 Oct 2026 06:16:27 -0500 Subject: [PATCH 079/269] fix(trace): write the benchmark file safely and off the presenter Review of the benchmark report: - The file is written to `.tmp` and moved over the previous one, so a crash or kill during a rewrite keeps the last complete report. - A dedicated writer keeps only the newest pending snapshot, so two reports can no longer be joined into one file when the writer falls behind. - The report is formatted on the writer thread. The presenter hands over the kept intervals, which are shared and never copied, so the interval that closes no longer pays for formatting every earlier one. - Hosts that are not x86 or report no CPU brand string leave the host fields out instead of failing every summary line. - A `contentVersion` that is not a string, or does not fit, is left empty instead of failing the title's metadata. - Tests cover the 240-interval rotation and `intervals_dropped`, the newest-only writer, the pending write on stop and the atomic replace. Co-Authored-By: Claude Opus 5.5 --- .../Execution/include/PerformanceTrace.hpp | 113 +++++++++++++++++- .../Execution/include/TraceFile.hpp | 1 + .../Execution/src/PerformanceTrace.cpp | 38 +++--- .../Execution/src/TraceFile.cpp | 15 +++ .../tests/PerformanceTrace.cpp | 105 +++++++++++++++- .../libkernel/AppMetadata/src/AppMetadata.cpp | 2 +- .../AppMetadata/src/ParamJsonParser.cpp | 2 +- core/libs/tests/ParamJsonParser.cpp | 4 +- docs/dev/BUILD.md | 4 +- 9 files changed, 253 insertions(+), 31 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp index a3aa660629..e683bc7695 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp @@ -10,6 +10,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -19,6 +22,7 @@ #include #include #include +#include #include #include @@ -308,10 +312,20 @@ struct TraceBenchInterval { std::vector frameUs; }; +using TraceBenchIntervals = std::vector>; + class TraceBench { public: static constexpr std::size_t MaximumIntervals = 240; + static void Keep(TraceBenchIntervals& kept, std::shared_ptr interval, std::uint64_t& dropped) { + kept.push_back(std::move(interval)); + if (kept.size() <= MaximumIntervals) return; + const auto excess = kept.size() - MaximumIntervals; + kept.erase(kept.begin(), kept.begin() + static_cast(excess)); + dropped += excess; + } + static bool Parse(const char* bench, bool summaryEvery) { if (bench == nullptr) return false; if (std::string_view(bench) != "1") throw std::invalid_argument("APS5_TRACE_BENCH=" + std::string(bench) + " is not 1"); @@ -320,6 +334,101 @@ public: } }; +struct TraceBenchSnapshot { + TraceSummary summary; + TraceBenchIntervals intervals; + std::uint64_t dropped = 0; +}; + +class TraceBenchWriter { +public: + explicit TraceBenchWriter(std::function writer) : writer(std::move(writer)) { + if (!this->writer) throw std::invalid_argument("invalid benchmark writer"); + worker = std::thread([this] { run(); }); + } + + TraceBenchWriter(const TraceBenchWriter&) = delete; + TraceBenchWriter& operator=(const TraceBenchWriter&) = delete; + + ~TraceBenchWriter() { + Stop(); + } + + void Stop() { + std::lock_guard stopLock(stopMutex); + { + std::lock_guard lock(mutex); + stopping = true; + } + changed.notify_one(); + if (worker.joinable()) worker.join(); + } + + void Write(TraceBenchSnapshot snapshot) { + { + std::lock_guard lock(mutex); + if (failure) std::rethrow_exception(failure); + if (stopping) throw std::runtime_error("benchmark writer is stopping"); + if (pending) ++superseded; + pending = std::move(snapshot); + } + changed.notify_one(); + } + + void Flush() { + std::unique_lock lock(mutex); + idle.wait(lock, [&] { return failure || (!pending && !writing); }); + if (failure) std::rethrow_exception(failure); + } + + std::uint64_t Superseded() { + std::lock_guard lock(mutex); + return superseded; + } + +private: + void run() noexcept { + try { + for (;;) { + TraceBenchSnapshot snapshot; + { + std::unique_lock lock(mutex); + changed.wait(lock, [&] { return stopping || pending.has_value(); }); + if (!pending) return; + snapshot = std::move(*pending); + pending.reset(); + writing = true; + } + writer(snapshot); + { + std::lock_guard lock(mutex); + writing = false; + } + idle.notify_all(); + } + } catch (...) { + { + std::lock_guard lock(mutex); + failure = std::current_exception(); + writing = false; + } + idle.notify_all(); + } + } + + std::function writer; + std::mutex stopMutex; + std::mutex mutex; + std::condition_variable changed; + std::condition_variable idle; + std::optional pending; + std::uint64_t superseded = 0; + bool writing = false; + bool stopping = false; + std::exception_ptr failure; + std::thread worker; +}; + struct TraceGpuRange { const char* name; double beginNs; @@ -549,7 +658,7 @@ public: return frames; } - static std::string Bench(const TraceSummary& summary, std::span intervals, std::uint64_t intervalsDropped) { + static std::string Bench(const TraceSummary& summary, std::span> intervals, std::uint64_t intervalsDropped) { std::string output = "{\"format\":\"aps5-bench/1\",\"commit\":"; AppendString(output, summary.commit); output += ",\"dirty\":"; @@ -570,7 +679,7 @@ public: AppendMachine(output, summary); output += ",\"intervals_dropped\":" + std::to_string(intervalsDropped) + ",\"intervals\":["; for (std::size_t index = 0; index < intervals.size(); ++index) { - const auto& interval = intervals[index]; + const auto& interval = *intervals[index]; if (index != 0) output += ','; output += "\n{\"start_s\":" + number(interval.startS) + ",\"end_s\":" + number(interval.endS); output += ",\"first_present\":" + std::to_string(interval.firstPresent); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/TraceFile.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/TraceFile.hpp index 9174bfbc06..ed7bb2a11d 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/TraceFile.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/TraceFile.hpp @@ -9,6 +9,7 @@ namespace AgcDriver { class TraceFile { public: static void WriteGzip(const std::filesystem::path& path, std::string_view text); + static void ReplaceGzip(const std::filesystem::path& path, std::string_view text); static void AppendLine(const std::filesystem::path& path, std::string_view line); }; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTrace.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTrace.cpp index 8bd62468ca..e9578a2e7c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTrace.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTrace.cpp @@ -56,7 +56,7 @@ struct Session { std::optional gpuClock; std::vector presents; bool bench = false; - std::vector benchIntervals; + TraceBenchIntervals benchIntervals; std::uint64_t benchDropped = 0; }; @@ -75,8 +75,8 @@ std::unique_ptr& summaryWriter() { return output; } -std::unique_ptr& benchWriter() { - static std::unique_ptr output; +std::unique_ptr& benchWriter() { + static std::unique_ptr output; return output; } @@ -108,18 +108,18 @@ std::string operatingSystem() { #endif } -std::string processorName() { +std::optional processorName() { std::array words{}; #if defined(_MSC_VER) && !defined(__clang__) int registers[4]{}; __cpuid(registers, static_cast(0x80000000u)); - if (static_cast(registers[0]) < 0x80000004u) throw std::runtime_error("the CPU reports no brand string"); + if (static_cast(registers[0]) < 0x80000004u) return std::nullopt; for (unsigned leaf = 0; leaf < 3; ++leaf) { __cpuid(registers, static_cast(0x80000002u + leaf)); std::memcpy(words.data() + leaf * 4, registers, sizeof registers); } #elif defined(__x86_64__) || defined(__i386__) - if (__get_cpuid_max(0x80000000u, nullptr) < 0x80000004u) throw std::runtime_error("the CPU reports no brand string"); + if (__get_cpuid_max(0x80000000u, nullptr) < 0x80000004u) return std::nullopt; for (unsigned leaf = 0; leaf < 3; ++leaf) { unsigned eax = 0, ebx = 0, ecx = 0, edx = 0; if (__get_cpuid(0x80000002u + leaf, &eax, &ebx, &ecx, &edx) == 0) throw std::runtime_error("cpuid brand string leaf failed"); @@ -129,12 +129,12 @@ std::string processorName() { words[leaf * 4 + 3] = edx; } #else - throw std::runtime_error("reading the CPU name needs an x86 host"); + return std::nullopt; #endif std::string name(reinterpret_cast(words.data()), sizeof words); name.resize(std::strlen(name.c_str())); const auto first = name.find_first_not_of(' '); - if (first == std::string::npos) throw std::runtime_error("the CPU brand string is empty"); + if (first == std::string::npos) return std::nullopt; return name.substr(first, name.find_last_not_of(' ') - first + 1); } @@ -152,8 +152,12 @@ std::uint64_t memoryMiB() { #endif } -const TraceHost& host() { - static const TraceHost value{processorName(), std::thread::hardware_concurrency(), memoryMiB()}; +const std::optional& host() { + static const auto value = []() -> std::optional { + auto cpu = processorName(); + if (!cpu) return std::nullopt; + return TraceHost{std::move(*cpu), std::thread::hardware_concurrency(), memoryMiB()}; + }(); return value; } @@ -260,26 +264,22 @@ void PerformanceTrace::FlushInterval(Clock::time_point now, std::uint32_t width, summary.interval = std::make_pair(seconds(state.intervalPresents.front() - state.first), seconds(now - state.first)); summaryWriter()->Write(TraceFormat::Summary(summary, snapshots, state.intervalPresents)); if (!state.bench) return; - state.benchIntervals.push_back({summary.interval->first, summary.interval->second, summary.firstPresent, width, height, summary.gpuBusyMs, TraceFormat::FrameMicroseconds(state.intervalPresents)}); - if (state.benchIntervals.size() > TraceBench::MaximumIntervals) { - state.benchIntervals.erase(state.benchIntervals.begin()); - ++state.benchDropped; - } + TraceBench::Keep(state.benchIntervals, std::make_shared(TraceBenchInterval{summary.interval->first, summary.interval->second, summary.firstPresent, width, height, summary.gpuBusyMs, TraceFormat::FrameMicroseconds(state.intervalPresents)}), state.benchDropped); if (!benchWriter()) { const auto name = "aps5-bench-" + summary.commit + "-" + std::to_string(processId()) + ".json.gz"; - benchWriter() = std::make_unique([directory = traceDirectory(), name](std::string_view text) { + benchWriter() = std::make_unique([directory = traceDirectory(), name](const TraceBenchSnapshot& snapshot) { try { std::filesystem::create_directories(directory); - TraceFile::WriteGzip(directory / name, text); + TraceFile::ReplaceGzip(directory / name, TraceFormat::Bench(snapshot.summary, snapshot.intervals, snapshot.dropped)); } catch (const std::exception& error) { std::fprintf(stderr, "[trace] writing %s failed: %s\n", name.c_str(), error.what()); std::fflush(stderr); throw; } - }, std::numeric_limits::max()); + }); ProfilePrint_nid_no_patch("[trace] benchmark frames go to %s, rewritten every interval\n", name.c_str()); } - benchWriter()->Write(TraceFormat::Bench(summary, state.benchIntervals, state.benchDropped)); + benchWriter()->Write(TraceBenchSnapshot{std::move(summary), state.benchIntervals, state.benchDropped}); } void PerformanceTrace::Present(std::uint32_t width, std::uint32_t height, const VulkanDevice* device) { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/TraceFile.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/TraceFile.cpp index 40d0a7e089..38d606fb44 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/TraceFile.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/TraceFile.cpp @@ -6,7 +6,11 @@ #include #include #include +#include #include +#ifdef _WIN32 +#include +#endif namespace AgcDriver { @@ -50,6 +54,17 @@ void TraceFile::WriteGzip(const std::filesystem::path& path, std::string_view te output.close(); } +void TraceFile::ReplaceGzip(const std::filesystem::path& path, std::string_view text) { + auto temporary = path; + temporary += ".tmp"; + WriteGzip(temporary, text); +#ifdef _WIN32 + if (::MoveFileExW(temporary.c_str(), path.c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH) == 0) throw std::system_error(static_cast(::GetLastError()), std::system_category(), "MoveFileExW " + temporary.string()); +#else + std::filesystem::rename(temporary, path); +#endif +} + void TraceFile::AppendLine(const std::filesystem::path& path, std::string_view line) { std::ofstream output; output.exceptions(std::ios::badbit | std::ios::failbit); diff --git a/core/libs/prx/libSceAgcDriver/tests/PerformanceTrace.cpp b/core/libs/prx/libSceAgcDriver/tests/PerformanceTrace.cpp index 37e9bc2e5c..550af95fc4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/PerformanceTrace.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/PerformanceTrace.cpp @@ -1,9 +1,12 @@ #include "prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp" #include "prx/libSceAgcDriver/Execution/include/TraceFile.hpp" #include "zlib.h" +#include +#include #include #include #include +#include namespace { @@ -364,6 +367,9 @@ void SummaryMachineFields() { Check(line.find("\"os\":\"OS 1\",\"cpu\":\"CPU \\\"X\\\"\",\"cpu_threads\":16,\"memory_mib\":32000,\"title_id\":\"PPSA00000\",\"title\":\"Example\",\"content_version\":\"01.002.000\",\"resolution\":\"1920x1080\"") != std::string::npos, "the summary line did not name the machine and title"); summary.title.reset(); Check(TraceFormat::Summary(summary, {}, presents).find("title_id") == std::string::npos, "a summary without param.json named a title"); + summary.host.reset(); + const auto bare = TraceFormat::Summary(summary, {}, presents); + Check(bare.find("\"os\":\"OS 1\",\"resolution\":\"1920x1080\"") != std::string::npos && bare.find("cpu") == std::string::npos, "a summary on a host without a CPU name changed its fields"); } void FrameMicroseconds() { @@ -381,9 +387,9 @@ void BenchFormat() { summary.driver = "Driver 1"; summary.os = "OS 1"; summary.host = AgcDriver::TraceHost{"CPU", 8, 16000}; - const std::vector intervals{ - {0, 30.5, 0, 1920, 1080, 120.25, {16667, 16666}}, - {30.5, 61, 1830, 2560, 1440, 98.5, {}}, + const AgcDriver::TraceBenchIntervals intervals{ + std::make_shared(AgcDriver::TraceBenchInterval{0, 30.5, 0, 1920, 1080, 120.25, {16667, 16666}}), + std::make_shared(AgcDriver::TraceBenchInterval{30.5, 61, 1830, 2560, 1440, 98.5, {}}), }; const std::string expected = "{\"format\":\"aps5-bench/1\",\"commit\":\"abc\",\"dirty\":null,\"env\":{\"APS5_TRACE_BENCH\":\"1\"},\"gpu\":\"GPU\",\"driver\":\"Driver 1\",\"os\":\"OS 1\"," @@ -393,6 +399,95 @@ void BenchFormat() { Check(TraceFormat::Bench(summary, intervals, 3) == expected, "benchmark file changed"); } +void BenchRotation() { + AgcDriver::TraceBenchIntervals kept; + std::uint64_t dropped = 0; + for (std::size_t index = 0; index < AgcDriver::TraceBench::MaximumIntervals; ++index) AgcDriver::TraceBench::Keep(kept, std::make_shared(AgcDriver::TraceBenchInterval{static_cast(index)}), dropped); + Check(kept.size() == AgcDriver::TraceBench::MaximumIntervals && dropped == 0, "intervals were dropped before the file was full"); + for (std::size_t index = 0; index < 5; ++index) AgcDriver::TraceBench::Keep(kept, std::make_shared(AgcDriver::TraceBenchInterval{static_cast(AgcDriver::TraceBench::MaximumIntervals + index)}), dropped); + Check(kept.size() == AgcDriver::TraceBench::MaximumIntervals && dropped == 5, "a full file did not drop one interval per new one"); + Check(kept.front()->startS == 5 && kept.back()->startS == static_cast(AgcDriver::TraceBench::MaximumIntervals + 4), "the file did not keep the newest intervals"); + const auto file = TraceFormat::Bench({}, kept, dropped); + Check(file.find("\"intervals_dropped\":5,") != std::string::npos && file.find("{\"start_s\":5.000,") != std::string::npos && file.find("{\"start_s\":4.000,") == std::string::npos, "the file does not match the kept intervals"); +} + +void BenchWriterKeepsNewest() { + std::mutex mutex; + std::condition_variable changed; + bool entered = false; + bool release = false; + std::vector written; + AgcDriver::TraceBenchWriter writer([&](const AgcDriver::TraceBenchSnapshot& snapshot) { + std::unique_lock lock(mutex); + written.push_back(snapshot.dropped); + entered = true; + changed.notify_all(); + changed.wait(lock, [&] { return release; }); + }); + writer.Write({{}, {}, 1}); + { + std::unique_lock lock(mutex); + changed.wait(lock, [&] { return entered; }); + } + writer.Write({{}, {}, 2}); + writer.Write({{}, {}, 3}); + writer.Write({{}, {}, 4}); + { + std::lock_guard lock(mutex); + release = true; + } + changed.notify_all(); + writer.Flush(); + writer.Stop(); + Check(written == std::vector{1, 4}, "a benchmark snapshot was written after a newer one replaced it"); + Check(writer.Superseded() == 2, "replaced benchmark snapshots were not counted"); +} + +void BenchWriterStopWrites() { + std::vector written; + { + AgcDriver::TraceBenchWriter writer([&](const AgcDriver::TraceBenchSnapshot& snapshot) { written.push_back(snapshot.dropped); }); + writer.Write({{}, {}, 7}); + } + Check(written == std::vector{7}, "stopping the benchmark writer lost its pending snapshot"); +} + +std::string ReadGzip(const std::filesystem::path& path) { + std::ifstream input(path, std::ios::binary); + const std::vector bytes((std::istreambuf_iterator(input)), std::istreambuf_iterator()); + Check(bytes.size() > 18, "the gzip file is truncated"); + std::size_t size = 0; + for (unsigned index = 0; index < 4; ++index) size |= static_cast(bytes[bytes.size() - 4 + index]) << (8 * index); + z_stream stream{}; + Check(inflateInit2(&stream, -MAX_WBITS) == Z_OK, "inflateInit2 failed"); + std::string inflated(size, 0); + stream.next_in = const_cast(bytes.data() + 10); + stream.avail_in = static_cast(bytes.size() - 18); + stream.next_out = reinterpret_cast(inflated.data()); + stream.avail_out = static_cast(inflated.size()); + const int status = inflate(&stream, Z_FINISH); + inflateEnd(&stream); + Check(status == Z_STREAM_END && stream.avail_out == 0, "the gzip file does not inflate to its size"); + return inflated; +} + +void ReplaceGzipFile() { + const auto directory = std::filesystem::temp_directory_path() / ("aps5-bench-test-" + std::to_string(Clock::now().time_since_epoch().count())); + std::filesystem::create_directories(directory); + const auto path = directory / "aps5-bench.json.gz"; + auto temporary = path; + temporary += ".tmp"; + AgcDriver::TraceFile::ReplaceGzip(path, "{\"first\":1}\n"); + AgcDriver::TraceFile::ReplaceGzip(path, "{\"second\":2}\n"); + Check(ReadGzip(path) == "{\"second\":2}\n" && !std::filesystem::exists(temporary), "the benchmark file was not replaced"); + std::filesystem::create_directory(temporary); + bool failed = false; + try { AgcDriver::TraceFile::ReplaceGzip(path, "{\"third\":3}\n"); } catch (const std::exception&) { failed = true; } + const auto kept = ReadGzip(path); + std::filesystem::remove_all(directory); + Check(failed && kept == "{\"second\":2}\n", "a failed rewrite damaged the previous benchmark file"); +} + void BenchSwitch() { Check(!AgcDriver::TraceBench::Parse(nullptr, false), "the benchmark file was enabled without its switch"); Check(AgcDriver::TraceBench::Parse("1", true), "APS5_TRACE_BENCH=1 was not accepted"); @@ -474,8 +569,12 @@ int main() { SummaryMachineFields(); FrameMicroseconds(); BenchFormat(); + BenchRotation(); + BenchWriterKeepsNewest(); + BenchWriterStopWrites(); BenchSwitch(); GzipFile(); + ReplaceGzipFile(); AppendLines(); std::cout << "performance trace tests passed\n"; return 0; diff --git a/core/libs/prx/libkernel/AppMetadata/src/AppMetadata.cpp b/core/libs/prx/libkernel/AppMetadata/src/AppMetadata.cpp index 6598f040e5..3f46122f14 100644 --- a/core/libs/prx/libkernel/AppMetadata/src/AppMetadata.cpp +++ b/core/libs/prx/libkernel/AppMetadata/src/AppMetadata.cpp @@ -48,7 +48,7 @@ void ensureTitleLoaded() { const auto parsed = parseParams(ResolvePath_nid_no_patch(AppMetadataParamJsonGuestPath), ResolvePath_nid_no_patch(AppMetadataParamSfoGuestPath)); copyToFixedBuffer(g_title, sizeof(g_title), parsed.title); copyToFixedBuffer(g_titleId, sizeof(g_titleId), parsed.titleId); - if (!parsed.contentVersion.empty()) copyToFixedBuffer(g_contentVersion, sizeof(g_contentVersion), parsed.contentVersion); + if (!parsed.contentVersion.empty() && parsed.contentVersion.size() < sizeof(g_contentVersion)) copyToFixedBuffer(g_contentVersion, sizeof(g_contentVersion), parsed.contentVersion); g_downloadDataSizeMiB = parsed.downloadDataSizeMiB; g_attribute3 = parsed.attribute3; std::memcpy(g_userDefinedParams, parsed.userDefinedParams, sizeof(g_userDefinedParams)); diff --git a/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp b/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp index 849b5323d2..3150e1419a 100644 --- a/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp +++ b/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp @@ -322,7 +322,7 @@ ParsedParamJson parseParamJson(const std::filesystem::path& paramJsonPath) { ParsedParamJson result; result.title = title; result.titleId = titleId; - if (const JsonValue* contentVersion = findObjectMember(root, "contentVersion")) result.contentVersion = asString(*contentVersion, "contentVersion"); + if (const JsonValue* contentVersion = findObjectMember(root, "contentVersion"); contentVersion != nullptr && contentVersion->type == JsonType::String) result.contentVersion = contentVersion->stringValue; if (const JsonValue* attribute = findObjectMember(root, "attribute3")) { const auto& number = attribute->numberText; if (attribute->type != JsonType::Number || number.find_first_of("-.eE") != std::string::npos) throw std::runtime_error("param.json attribute3 is not an unsigned integer"); diff --git a/core/libs/tests/ParamJsonParser.cpp b/core/libs/tests/ParamJsonParser.cpp index fe68a16d2d..8ec7173a5a 100644 --- a/core/libs/tests/ParamJsonParser.cpp +++ b/core/libs/tests/ParamJsonParser.cpp @@ -84,9 +84,7 @@ static void CheckUserDefinedParams(const std::filesystem::path& path) { static void CheckContentVersion(const std::filesystem::path& path) { Require(ParseText(path, R"({"titleId":"PPSA00000","contentVersion":"01.002.000","localizedParameters":{"en-US":{"titleName":"Example"}}})").contentVersion == "01.002.000", "Incorrect content version"); Require(ParseText(path, R"({"titleId":"PPSA00000","localizedParameters":{"en-US":{"titleName":"Example"}}})").contentVersion.empty(), "A missing content version was not empty"); - bool rejected = false; - try { ParseText(path, R"({"titleId":"PPSA00000","contentVersion":1,"localizedParameters":{"en-US":{"titleName":"Example"}}})"); } catch (const std::exception&) { rejected = true; } - Require(rejected, "Accepted a content version that is not a string"); + Require(ParseText(path, R"({"titleId":"PPSA00000","contentVersion":1,"localizedParameters":{"en-US":{"titleName":"Example"}}})").contentVersion.empty(), "A content version that is not a string was kept"); } static void CheckNesting(const std::filesystem::path& path) { diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index f1f98ec822..bcb7c38d63 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -155,7 +155,7 @@ Set `APS5_TRACE_EVENTS=` to change the queue-worker and presenter buffers Inside the window the queue workers and the presenter record every `PerformanceTimer` scope and stage as a complete event named after its frame timing key: `Driver.WorkerWait`, `Driver.Packet`, `Driver.Packet.draw`, `Graphics.Draw`, `Driver.Present`, `Driver.Present.inflight_wait`, `Vulkan.Retire` and so on. A slice shorter than 20 us is only counted: its time is in the thread's counter ` folded` (milliseconds per present) and in the summary. A thread writes a present's counter sample at its first slice after the next present, so the last present of the window has no sample; its time is still in the summary. By default, each queue worker and presenter allocates a buffer for 262,144 events when it starts; a full buffer stops that thread's recording, sets `trace_dropped` and puts an instant event `trace buffer full` on the thread's track at the start of the first lost slice (slices are appended when their outer scope ends, so that can precede the last recorded slice by up to one outer-scope length), with the number of lost slices in `args.events_lost`. When the window closes, the presenter formats the trace once and a `ProfileOutput` writer thread writes `aps5-trace--.json.gz` (Chrome trace-event JSON, opened by ui.perfetto.dev or chrome://tracing) and appends one line to `aps5-summary.jsonl`, both in `APS5_TRACE_DIR` or the working directory. A window that is still open when the title exits writes nothing. -The summary line has `commit` and `dirty` of the build, `env` (every `APS5_*` variable; a value containing `/` or `\` is written as ``), `gpu`, `driver`, `os`, `cpu` (the CPU brand string), `cpu_threads` and `memory_mib` (physical memory) of the host, `title_id`, `title` and `content_version` from `app0/sce_sys/param.json` (only when the title has one; `content_version` is empty when the file has no `contentVersion`), `resolution` (the video output's), `window`, `presents`, `frame_ms` (p50, p90, p99 and max of the intervals between presents), `gpu_busy_ms_per_present` (the batch timestamps behind `[present]`), `worker_busy_pct` (share of the window each queue worker spends in `Driver.Packet`, `Driver.Suspend`, `Driver.EndSubmission`, `Driver.Rewind` and `Driver.Completion`), `top_phases_ms_per_frame` (the eight largest scopes and stages, without `Driver.WorkerWait`), `trace_dropped`, `trace_dropped_threads` and `trace`. `trace_dropped` is `true` when any thread lost a slice, any thread's totals table (512 scope and stage entries) was full, or the `gpu` track was full. `trace_dropped_threads` is present only then and has one entry per such thread name (threads that share a name, such as the shader prepare threads, share one entry: `events_lost` is summed, `events_full_after_s` is the earliest, `totals_full` is true if any is): `events_full_after_s` (seconds from the first present of the window to the start of the first lost slice; absent for `gpu` and, in every summary line (windowed `APS5_TRACE_FRAMES`/`APS5_TRACE_SECONDS` and `APS5_TRACE_SUMMARY_EVERY`), whenever the first lost slice starts outside the line's span of presents, including a slice whose scope began before the window opened or one lost after the window's last present), `events_lost` (slices not recorded; absent when 0; in `APS5_TRACE_SUMMARY_EVERY` lines it counts the thread buffer's losses since it started, not only those of the line's span) and `totals_full` (`true` when the thread's totals table overflowed, which makes the summary numbers incomplete; a thread with `events_lost` only has complete totals). A trace holds timings, scope names, counts and thread names; no guest memory, paths or strings of the title. +The summary line has `commit` and `dirty` of the build, `env` (every `APS5_*` variable; a value containing `/` or `\` is written as ``), `gpu`, `driver`, `os`, `cpu` (the CPU brand string), `cpu_threads` and `memory_mib` (physical memory) of the host (all three absent on a host that is not x86 or reports no brand string), `title_id`, `title` and `content_version` from `app0/sce_sys/param.json` (only when the title has one; `content_version` is empty when the file has no `contentVersion`, or one that is not a string of at most 15 characters), `resolution` (the video output's), `window`, `presents`, `frame_ms` (p50, p90, p99 and max of the intervals between presents), `gpu_busy_ms_per_present` (the batch timestamps behind `[present]`), `worker_busy_pct` (share of the window each queue worker spends in `Driver.Packet`, `Driver.Suspend`, `Driver.EndSubmission`, `Driver.Rewind` and `Driver.Completion`), `top_phases_ms_per_frame` (the eight largest scopes and stages, without `Driver.WorkerWait`), `trace_dropped`, `trace_dropped_threads` and `trace`. `trace_dropped` is `true` when any thread lost a slice, any thread's totals table (512 scope and stage entries) was full, or the `gpu` track was full. `trace_dropped_threads` is present only then and has one entry per such thread name (threads that share a name, such as the shader prepare threads, share one entry: `events_lost` is summed, `events_full_after_s` is the earliest, `totals_full` is true if any is): `events_full_after_s` (seconds from the first present of the window to the start of the first lost slice; absent for `gpu` and, in every summary line (windowed `APS5_TRACE_FRAMES`/`APS5_TRACE_SECONDS` and `APS5_TRACE_SUMMARY_EVERY`), whenever the first lost slice starts outside the line's span of presents, including a slice whose scope began before the window opened or one lost after the window's last present), `events_lost` (slices not recorded; absent when 0; in `APS5_TRACE_SUMMARY_EVERY` lines it counts the thread buffer's losses since it started, not only those of the line's span) and `totals_full` (`true` when the thread's totals table overflowed, which makes the summary numbers incomplete; a thread with `events_lost` only has complete totals). A trace holds timings, scope names, counts and thread names; no guest memory, paths or strings of the title. The shader preparation threads are traced as well, `shader prepare`, with a buffer for 32,768 events each. A wait for the GPU mutex on a traced thread is a `GpuMutex.wait.` slice (`dispatch`, `draw`, `present`, `flush` and so on, as on the `[lock]` line). @@ -171,7 +171,7 @@ Set `APS5_TRACE_SUMMARY_EVERY=` (a whole number above 0; a malformed va The switch combines with `APS5_TRACE_FRAMES` or `APS5_TRACE_SECONDS`. The intervals run for the whole session and the window records its full trace on top: the event buffers are allocated and filled only while the window is open, the window's own summary line (with `trace` set and no `interval_*` fields) holds only the time inside the window, and the interval lines continue unchanged around it. Without a window switch no buffer is allocated at all. The two switches do not share a clock origin: `interval_*` is relative to the first present, as the window's `APS5_TRACE_SECONDS` start is. -Set `APS5_TRACE_BENCH=1` together with `APS5_TRACE_SUMMARY_EVERY` to also keep every interval's frame times for a benchmark report. At each interval the presenter rewrites `aps5-bench--.json.gz` in `APS5_TRACE_DIR` or the working directory: one JSON object with `format` (`aps5-bench/1`), the identity fields of the summary line (`commit`, `dirty`, `env`, `gpu`, `driver`, `os`, `cpu`, `cpu_threads`, `memory_mib` and, with `param.json`, `title_id`, `title` and `content_version`), `intervals_dropped` and `intervals`. Each interval has `start_s`, `end_s`, `first_present`, `resolution`, `gpu_busy_ms` (the interval's total) and `frame_us`, the whole microseconds between consecutive presents. The file keeps the last 240 intervals; `intervals_dropped` counts older ones it no longer holds. Any other value, or the switch without `APS5_TRACE_SUMMARY_EVERY`, fails the first present. The frame times cost 4 bytes per present in memory, and the file is formatted and compressed on a writer thread. +Set `APS5_TRACE_BENCH=1` together with `APS5_TRACE_SUMMARY_EVERY` to also keep every interval's frame times for a benchmark report. At each interval the presenter hands the kept intervals to a writer thread, which formats and compresses the file and rewrites `aps5-bench--.json.gz` in `APS5_TRACE_DIR` or the working directory: one JSON object with `format` (`aps5-bench/1`), the identity fields of the summary line (`commit`, `dirty`, `env`, `gpu`, `driver`, `os`, `cpu`, `cpu_threads`, `memory_mib` and, with `param.json`, `title_id`, `title` and `content_version`), `intervals_dropped` and `intervals`. Each interval has `start_s`, `end_s`, `first_present`, `resolution`, `gpu_busy_ms` (the interval's total) and `frame_us`, the whole microseconds between consecutive presents. The file keeps the last 240 intervals; `intervals_dropped` counts older ones it no longer holds. Any other value, or the switch without `APS5_TRACE_SUMMARY_EVERY`, fails the first present. The file is written to `.tmp` and then moved over the previous one, so a crash or kill during a rewrite leaves the last complete report. Only the newest report waits for the writer: one handed over while an older one is still pending replaces it. The frame times cost 4 bytes per present in memory. `python tools/trace_digest.py aps5-summary.jsonl` prints the interval lines of such a file as one row per interval (frame p50, p90, p99, max, GPU busy per present, the busiest queue worker and the largest phase) and a text sparkline of the frame p99 and of the GPU busy time; lines of window captures in the file are skipped, and a change of commit, environment, GPU, driver or OS between intervals is reported. `--json` prints the rows as JSON. From 921b8408d4868c65694cbd3f8c78417bf0db07b3 Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Mon, 19 Oct 2026 13:24:47 +0200 Subject: [PATCH 080/269] perf(shader): keep redundant expression operand keys inline --- .../tests/RedundantValueElimination.cpp | 28 ++++++++++++++ .../src/RedundantValueEliminator.cpp | 37 +++++++++++-------- 2 files changed, 50 insertions(+), 15 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/RedundantValueElimination.cpp b/core/libs/prx/libSceAgcDriver/tests/RedundantValueElimination.cpp index 55fb0a5297..a17b7cf87d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/RedundantValueElimination.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/RedundantValueElimination.cpp @@ -256,6 +256,34 @@ int main() { return b.eliminate() == 0u && AllUsed({&plain, &flagged, &otherType, &floatImmediate}); }); + passed &= run("four-operand expressions retain distinct trailing immediates across a large block", [] { + Builder b; + auto& source = b.unknown(); + std::vector reads; + std::vector duplicates; + std::vector different; + for (std::uint32_t index = 0; index < 256u; ++index) { + const auto composite = [&](std::uint32_t last) -> IrValue& { + return b.emit(IrOpcode::CompositeConstructU32x4, IrType::U32x4, {&source, &b.constant(index), &b.constant(7u), &b.constant(last)}); + }; + auto& first = composite(0u); + auto& second = composite(0u); + auto& changed = composite(1u); + auto& firstRead = b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&first, &b.constant(3u)}); + auto& secondRead = b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&second, &b.constant(3u)}); + auto& changedRead = b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&changed, &b.constant(3u)}); + for (auto* read : {&firstRead, &secondRead, &changedRead}) b.keep(*read); + reads.push_back(&firstRead); + duplicates.push_back(&second); + duplicates.push_back(&secondRead); + different.push_back(&changedRead); + } + if (b.eliminate() != 512u || !AllUsed(different)) return false; + for (const auto* duplicate : duplicates) if (duplicate->HasUses()) return false; + for (const auto* read : reads) if (read->UseCount() != 2u) return false; + return true; + }); + passed &= run("float immediates compare by bits: +0 and -0 and two NaN payloads stay apart, one NaN merges", [] { Builder b; auto& x = b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&b.unknown()}); diff --git a/core/shader/recompiler/Optimization/src/RedundantValueEliminator.cpp b/core/shader/recompiler/Optimization/src/RedundantValueEliminator.cpp index a580617abd..6e8c4e269d 100644 --- a/core/shader/recompiler/Optimization/src/RedundantValueEliminator.cpp +++ b/core/shader/recompiler/Optimization/src/RedundantValueEliminator.cpp @@ -1,7 +1,9 @@ #include "Optimization/RedundantValueEliminator.hpp" +#include #include #include #include +#include #include #include #include @@ -134,31 +136,38 @@ struct Operand { bool operator==(const Operand& other) const = default; }; +Operand OperandOf(const IrValue* resolved) { + if (resolved->Parent() == nullptr && resolved->HasImmediate()) return {nullptr, resolved->Type(), resolved->ImmediateU64()}; + return {resolved, resolved->Type(), 0u}; +} + struct ValueKey { IrOpcode opcode; IrType type; std::uint64_t flags; - std::vector operands; + std::array operands{}; + std::size_t operandCount = 0; - bool operator==(const ValueKey& other) const = default; + bool operator==(const ValueKey& other) const { + if (opcode != other.opcode || type != other.type || flags != other.flags || operandCount != other.operandCount) return false; + for (std::size_t index = 0; index < operandCount; ++index) { + if (operands[index] != other.operands[index] && OperandOf(operands[index]) != OperandOf(other.operands[index])) return false; + } + return true; + } }; struct ValueKeyHash { std::size_t operator()(const ValueKey& key) const { std::size_t hash = static_cast(key.opcode) * 0x9e3779b97f4a7c15ull ^ static_cast(key.type) ^ std::hash{}(key.flags) * 31u; - for (const auto& operand : key.operands) { + for (const auto* argument : std::span(key.operands).first(key.operandCount)) { + const auto operand = OperandOf(argument); hash = hash * 1099511628211ull ^ std::hash{}(operand.value) ^ std::hash{}(operand.immediate) * 7u ^ static_cast(operand.type); } return hash; } }; -Operand OperandOf(const IrValue* argument) { - const IrValue* resolved = argument->Resolve(); - if (resolved->Parent() == nullptr && resolved->HasImmediate()) return {nullptr, resolved->Type(), resolved->ImmediateU64()}; - return {resolved, resolved->Type(), 0u}; -} - std::vector ReversePostOrder(IrBlock& entry) { std::vector order; std::unordered_map visited; @@ -216,15 +225,13 @@ RedundantValueEliminationStats RedundantValueEliminator::Eliminate(IrProgram& pr std::unordered_map available; std::vector added; std::unordered_map replacements; - ValueKey key; const auto visit = [&](IrBlock& block) { for (auto* inst : block.Instructions()) { if (!Recomputable(inst->Opcode()) || inst->HasImmediate() || !inst->HasUses()) continue; - key.opcode = inst->Opcode(); - key.type = inst->Type(); - key.flags = inst->Flags(); - key.operands.clear(); - for (std::size_t index = 0; index < inst->ArgumentCount(); ++index) key.operands.push_back(OperandOf(inst->Argument(index))); + ValueKey key{inst->Opcode(), inst->Type(), inst->Flags()}; + key.operandCount = inst->ArgumentCount(); + if (key.operandCount > key.operands.size()) continue; + for (std::size_t index = 0; index < key.operandCount; ++index) key.operands[index] = inst->Argument(index)->Resolve(); const auto [found, inserted] = available.try_emplace(key, inst); if (inserted) { added.push_back(&found->first); From 1427802578e994025c9e90d7ae32800b6284fd23 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Mon, 19 Oct 2026 06:15:43 +0000 Subject: [PATCH 081/269] fix(agc): map the AGC driver memory past ASan's shadow in an ASan build An ANYPS5_ENABLE_ASAN build moves the guest arena up by 0x100000000000, past AddressSanitizer's shadow gap, but the AGC driver memory that holds the FS table (#6160) is still mapped at the fixed 0xfe0000000. That address is in the shadow gap, so the mapping fails with EEXIST and no test that initializes the driver runs under ASan. The offset is now GuestArena::GuestAddressOffset in GuestArena.hpp, and the driver memory and the FS table move by it like the arena. Builds without ASan keep 0xfe0000000. agc_driver_compute_fs_table_calls skips in an ASan build: its shader builds the PS5's fixed table address from literals, as titles' shaders do, and that address can't be mapped there. Co-Authored-By: Claude Opus 5.5 --- .../prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp | 4 ++-- .../libSceAgcDriver/tests/execution/ComputeFsTableCalls.cpp | 5 +++++ core/libs/prx/libc/include/GuestArena.hpp | 6 ++++++ core/libs/prx/libc/src/GuestArena.cpp | 6 +----- 4 files changed, 14 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 23c3b254d3..b7c0caa5cf 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp @@ -52,9 +52,9 @@ void PrintFailure(std::exception_ptr error) { std::fflush(stderr); } -constexpr std::uint64_t DriverMemoryAddress = 0xfe0000000ull; +constexpr std::uint64_t DriverMemoryAddress = 0xfe0000000ull + GuestArena::GuestAddressOffset; constexpr std::size_t DriverMemoryBytes = 2u << 20u; -constexpr std::uint64_t FsTableAddress = 0xfe0040000ull; +constexpr std::uint64_t FsTableAddress = DriverMemoryAddress + 0x40000ull; constexpr std::uint64_t FsTableSlotBytes = 16u; constexpr std::uint64_t FsTableSlots = 6u; constexpr std::uint64_t FsRoutineRecords = 8u; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ComputeFsTableCalls.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ComputeFsTableCalls.cpp index 923b3b7212..a5395d5661 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ComputeFsTableCalls.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ComputeFsTableCalls.cpp @@ -5,6 +5,7 @@ #include "VulkanTestDevice.hpp" #include #include +#include #include #include #include @@ -88,6 +89,10 @@ void CheckTable() { } int main() { +#ifdef ANYPS5_ENABLE_ASAN + std::puts("skipped: the shader calls through the FS table at the PS5's fixed 0xfe0040000, which an ANYPS5_ENABLE_ASAN build cannot map"); + return VulkanTestSkipped; +#endif try { { const auto device = OpenVulkanTestDevice(); diff --git a/core/libs/prx/libc/include/GuestArena.hpp b/core/libs/prx/libc/include/GuestArena.hpp index d5a00acdec..583ad71dbc 100644 --- a/core/libs/prx/libc/include/GuestArena.hpp +++ b/core/libs/prx/libc/include/GuestArena.hpp @@ -8,6 +8,12 @@ // absolute address and breaks on host addresses outside that range. namespace GuestArena { +#ifdef ANYPS5_ENABLE_ASAN +inline constexpr std::uintptr_t GuestAddressOffset = 0x0000100000000000ull; +#else +inline constexpr std::uintptr_t GuestAddressOffset = 0; +#endif + #ifndef _WIN32 using SharedBackingResolver = bool (*)(std::uintptr_t address, std::size_t bytes, int* file, std::uint64_t* offset); using SharedBackingWriter = bool (*)(std::uintptr_t address, const void* source, std::size_t bytes); diff --git a/core/libs/prx/libc/src/GuestArena.cpp b/core/libs/prx/libc/src/GuestArena.cpp index 15d799c07e..d2ce7261a2 100644 --- a/core/libs/prx/libc/src/GuestArena.cpp +++ b/core/libs/prx/libc/src/GuestArena.cpp @@ -44,11 +44,7 @@ void noteAccess(const void* pointer, std::size_t bytes) { if (const auto callback = accessObserver.load(std::memory_order_acquire)) callback(reinterpret_cast(pointer), bytes); } -#ifdef ANYPS5_ENABLE_ASAN -constexpr std::uintptr_t GuestArenaOffset = 0x0000100000000000ull; -#else -constexpr std::uintptr_t GuestArenaOffset = 0; -#endif +constexpr std::uintptr_t GuestArenaOffset = GuestArena::GuestAddressOffset; constexpr std::uintptr_t ArenaStart = 0x0000000200000000ull + GuestArenaOffset; constexpr std::uintptr_t SystemReservedStart = 0x00000007FFFFC000ull + GuestArenaOffset; constexpr std::uintptr_t SystemReservedEnd = 0x0000001000000000ull + GuestArenaOffset; From dca67ae477ebc49d4c25e08ba6979fdf4ac647a7 Mon Sep 17 00:00:00 2001 From: ionut27001 <194206737+ionut27001@users.noreply.github.com> Date: Sun, 18 Oct 2026 02:34:49 +0300 Subject: [PATCH 082/269] fix(libkernel): return kern.rng_pseudo in 64-byte blocks as measured on a PS5 In a payload process on a PS5 (firmware 12.70), sysctlbyname("kern.rng_pseudo") answers a size query with 64 and gives at most 64 bytes per call, up to a 65536-byte buffer. A buffer smaller than 64 bytes gets as many bytes as its length and a null length gets none, both with -1 and ENOMEM; a null buffer with a null length returns 0, and errno is kept when the call succeeds. Here the whole buffer was filled, a size query threw and the length was never changed. The 64 random bytes are now drawn only for a read that passes both a buffer and a length. AI-assisted: yes Co-Authored-By: Claude Opus 5.5 --- .../System/src/SystemConfiguration.cpp | 8 +-- core/libs/tests/GuestSystemConfiguration.cpp | 55 ++++++++++++++----- docs/dev/debt/libkernel.md | 2 +- 3 files changed, 46 insertions(+), 19 deletions(-) diff --git a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp index a9af1155db..33c2ce2d8f 100644 --- a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp +++ b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp @@ -251,11 +251,11 @@ int APS5_VABI sysctlbyname_nid_postfix(const char* name, void* oldValue, std::si *__error_nid_postfix() = 1; return -1; } - if (oldValue == nullptr || oldLength == nullptr) throw std::runtime_error(std::string(__func__) + ": kern.rng_pseudo without an output buffer"); static std::random_device device; - auto* bytes = static_cast(oldValue); - for (std::size_t i = 0; i < *oldLength; ++i) bytes[i] = static_cast(device()); - return 0; + std::uint8_t block[64]{}; + if (oldValue != nullptr && oldLength != nullptr) + for (auto& byte : block) byte = static_cast(device()); + return CopyOut(block, sizeof(block), oldValue, oldLength); } if (std::strcmp(name, "vm.overcommit") == 0) { if (newValue != nullptr) throw std::runtime_error(std::string(__func__) + ": setting vm.overcommit"); diff --git a/core/libs/tests/GuestSystemConfiguration.cpp b/core/libs/tests/GuestSystemConfiguration.cpp index a13c41c46f..635a215ee8 100644 --- a/core/libs/tests/GuestSystemConfiguration.cpp +++ b/core/libs/tests/GuestSystemConfiguration.cpp @@ -289,23 +289,50 @@ static void CheckSystemSwVersion() { } } static void CheckPseudoRandomSysctl() { + unsigned char first[128]; + unsigned char second[128]; + const auto untouchedFrom = [&first](std::size_t from) { + for (std::size_t i = from; i < sizeof(first); ++i) + if (first[i] != 0xaa) return false; + return true; + }; *__error_nid_postfix() = 13; - unsigned char first[64] = {}; - unsigned char second[64] = {}; - std::size_t length = sizeof(first); - Require(sysctlbyname_nid_postfix("kern.rng_pseudo", first, &length, nullptr, 0) == 0 && length == sizeof(first)); - length = sizeof(second); - Require(sysctlbyname_nid_postfix("kern.rng_pseudo", second, &length, nullptr, 0) == 0 && length == sizeof(second)); - Require(std::memcmp(first, second, sizeof(first)) != 0); + std::memset(first, 0xaa, sizeof(first)); + std::memset(second, 0xaa, sizeof(second)); + std::size_t length = 64; + Require(sysctlbyname_nid_postfix("kern.rng_pseudo", first, &length, nullptr, 0) == 0 && length == 64 && untouchedFrom(64) && !untouchedFrom(0)); + length = 64; + Require(sysctlbyname_nid_postfix("kern.rng_pseudo", second, &length, nullptr, 0) == 0 && length == 64); + Require(std::memcmp(first, second, 64) != 0); Require(*__error_nid_postfix() == 13); - Require(sysctlbyname_nid_postfix("kern.rng_pseudo", first, &length, first, sizeof(first)) == -1 && *__error_nid_postfix() == 1); - bool threw = false; - try { - sysctlbyname_nid_postfix("kern.rng_pseudo", nullptr, &length, nullptr, 0); - } catch (const std::runtime_error&) { - threw = true; + static constexpr std::size_t larger[] = {65, 128}; + for (std::size_t asked : larger) { + std::memset(first, 0xaa, sizeof(first)); + length = asked; + Require(sysctlbyname_nid_postfix("kern.rng_pseudo", first, &length, nullptr, 0) == 0 && length == 64 && untouchedFrom(64) && !untouchedFrom(0)); } - Require(threw); + Require(*__error_nid_postfix() == 13); + static constexpr std::size_t smaller[] = {0, 1, 63}; + for (std::size_t asked : smaller) { + std::memset(first, 0xaa, sizeof(first)); + length = asked; + *__error_nid_postfix() = 13; + Require(sysctlbyname_nid_postfix("kern.rng_pseudo", first, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 12); + Require(length == asked && untouchedFrom(asked)); + if (asked == 63) Require(!untouchedFrom(0)); + } + static constexpr std::size_t presets[] = {0, 1, 64, 100, 4096}; + for (std::size_t preset : presets) { + length = preset; + *__error_nid_postfix() = 13; + Require(sysctlbyname_nid_postfix("kern.rng_pseudo", nullptr, &length, nullptr, 0) == 0 && length == 64 && *__error_nid_postfix() == 13); + } + std::memset(first, 0xaa, sizeof(first)); + Require(sysctlbyname_nid_postfix("kern.rng_pseudo", first, nullptr, nullptr, 0) == -1 && *__error_nid_postfix() == 12 && untouchedFrom(0)); + *__error_nid_postfix() = 13; + Require(sysctlbyname_nid_postfix("kern.rng_pseudo", nullptr, nullptr, nullptr, 0) == 0 && *__error_nid_postfix() == 13); + length = 64; + Require(sysctlbyname_nid_postfix("kern.rng_pseudo", first, &length, first, 64) == -1 && *__error_nid_postfix() == 1); } static void CheckOvercommitSysctl() { *__error_nid_postfix() = 13; diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 0e26b387d8..35bb92cda5 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -15,7 +15,7 @@ - [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. - [getlogin_r](../../../core/libs/prx/libkernel/System/src/Login.cpp) (libkernel) returns the host effective account name: `GetUserNameW` as UTF-8 on Windows and `getpwuid_r(geteuid())` on POSIX. A valid account returns a name even without a login session; host `getlogin_r`, `USER` and `LOGNAME` are not consulted. The console's login name and behaviour without a session are unknown. Account lookup or encoding failures, a missing or empty account name, and POSIX account records exceeding the 16 KiB lookup buffer throw; there is no replacement name or retry. A short guest buffer returns `ERANGE` without modifying it, and a null output returns `EFAULT`. - [sceKernelGetSystemSwVersion](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports baseline system software version `0x01000000` ("01.000.000") for ProsperoStore (PPSA99000); the console's firmware version is not modeled. Measured on PS5 firmware 13.42: record values 0, 24, 39, 40, 41, 48 and 64 in the leading 64-bit field are accepted and preserved, the 28-byte string at offset 8 and packed version at 0x24 are filled, and bytes from 0x28 onward remain untouched. This API reports "13.590.001" / `0x13590001` in that environment, while the Prospero variant reports "13.420.000" / `0x13420000`; their values must not be assumed identical. A null pointer still returns `SCE_KERNEL_ERROR_EFAULT`, which was not measured; other firmware versions and record values remain unverified. -- [kern.rng_pseudo](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctlbyname` fills the whole output buffer with bytes from the host's `std::random_device` and leaves the length unchanged; a write returns -1 with `EPERM` and a call without an output buffer throws. The only caller seen is mbedTLS's `mbedtls_hardware_poll` in the catalog's ProsperoLight boilerplate, which asks for 64-byte blocks and fails if fewer come back. The console's generator, its per-call maximum and its answer to a size query are unknown +- [kern.rng_pseudo](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctlbyname` draws a new block of 64 bytes from the host's `std::random_device` for each read (no new value) that passes a buffer and a length, and sizes its answers as a payload process on a PS5 (firmware 12.70) sees it: a size query reports 64; a buffer of 64 bytes or more gets 64 bytes and a length of 64 (tried up to 65536); a smaller buffer gets as many bytes as its length, which is left as given, and a null length gets none, both with -1 and `ENOMEM`; a null buffer with a null length returns 0. `errno` is kept when the call succeeds. A write returns -1 with `EPERM`, which was not tried on the console. The only caller seen is mbedTLS's `mbedtls_hardware_poll` in the catalog's ProsperoLight boilerplate, which asks for 64-byte blocks and fails if fewer come back. The console's generator is unknown, and a title process was not measured. On the console `sysctlnametomib` gives MIB {1, 50} for the name, and `sysctl` with it answers reads of 0, 1, 63, 64, 65 and 128 bytes, a size query, a null length and a null buffer with a null length as the name does; here `sysctlnametomib` for the name and `sysctl` {1, 50} throw - [sysconf](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `_SC_CLK_TCK` returns 128, FreeBSD's `CLK_TCK` as its [`sysconf`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/sysconf.c#L100-L101) returns it ([`time.h`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/include/time.h#L48-L54)); the console's value is not checked, and no `times` reports ticks at that rate. CPython's `posix` module stops its import with "cannot read ticks_per_second" when the value is below 1, as the Python in PS5MC (PPSA99420) does. `_SC_ARG_MAX` returns 262144 (FreeBSD's `NCARGS`), `_SC_OPEN_MAX` returns the `RLIMIT_NOFILE` soft limit via `getrlimit` ([`sysconf.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/sysconf.c#L106-L115)), and `_SC_IOV_MAX` returns 1024 (FreeBSD's `UIO_MAXIOV`). Names other than `_SC_ARG_MAX`, `_SC_CLK_TCK`, `_SC_OPEN_MAX`, `_SC_PAGESIZE`, `_SC_IOV_MAX`, `_SC_NPROCESSORS_CONF`, `_SC_NPROCESSORS_ONLN` and `_SC_PHYS_PAGES` fail with `EINVAL` - [hw.realmem](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctl` {`CTL_HW`, 12} and `sysctlbyname` report the same 8-byte value as `hw.physmem` (the `_SC_PHYS_PAGES` pages of 16 KiB), read-only with `EPERM` for a write and `ENOMEM` for a short buffer, as FreeBSD's [`sysctl_hw_realmem`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_mib.c#L191-L204) does. FreeBSD reports there the memory the firmware describes, which is at least `hw.physmem`; the console's values are not known. Dolphin reads it as the machine's memory size in [`MemPhysical`](https://github.com/dolphin-emu/dolphin/blob/8d2ad1a662472955497e5bb79d7b9ec6999e1f0d/Source/Core/Common/MemoryUtil.cpp#L253-L273) (RetroArch PPSA99169's `dolphin_libretro` core) - [vm.overcommit](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctlbyname` reports the 4-byte value 0, the default FreeBSD gives its [`vm.overcommit`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/swap_pager.c#L158-L161) `int`, with `ENOMEM` for a short buffer; the console's value is not known and a write throws. mimalloc reads it in [`unix_detect_overcommit`](https://github.com/microsoft/mimalloc/blob/31d034d94cdb8e22f7d7ed55967f581a2d6e831d/src/prim/unix/prim.c#L129-L153) and takes 0 as no overcommit, as PS5MC (PPSA99420) does at start-up From c92f09d1d5f650cda01f60daf870bf3ee575c23b Mon Sep 17 00:00:00 2001 From: Savvanis Spyros Date: Mon, 19 Oct 2026 14:53:58 +0300 Subject: [PATCH 083/269] docs(debt): name no title in the SCE code gap entry Co-Authored-By: Claude Opus 5.5 --- docs/dev/debt/relinker.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/relinker.md b/docs/dev/debt/relinker.md index cd67822bf6..9ca49b8361 100644 --- a/docs/dev/debt/relinker.md +++ b/docs/dev/debt/relinker.md @@ -17,7 +17,7 @@ - In Windows output, a guest instruction that addresses memory through GS becomes `UD2` and NOP padding of its own length ([TLS builder](../../../core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp)): the host owns GS, and the guest GS base that FreeBSD's `sysarch(AMD64_SET_GSBASE)` would set is not emulated. A converted module runs until such an instruction is reached, which then faults. Prospero Win (PPSA99995) has six in its Wine `ntdll`. - In executables with a `PT_SCE_DYNLIBDATA` segment, the PS4 layout, the executable segment also holds read-only data after the code (CUSA00900: 37 MiB after its last function). The [relinker pipeline](../../../core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp) therefore scans for syscalls and import call sites only up to the end of the PLT that follows the last function of the unwind table; code after the PLT without an unwind entry is not scanned. `--to-intel` and the `unused-filter=1` and `unused-filter=2` analyses are not limited this way. - The [syscall scan of `sce_module` modules](../../../core/relinker/relinker/src/guest/GuestModuleBuilder.cpp) with a `PT_SCE_DYNLIBDATA` segment stops at the same end, or, when the PLT comes before the code, at the end of the last function of the unwind table; code after it is not scanned. -- In the same executables and modules, the bytes between unwind functions are 16-byte slots of function tails and padding, and a tail can begin inside an instruction (CUSA03312). The [instruction scanner](../../../core/relinker/codegen/src/InstructionScanner.cpp) of the syscall and call-site scans therefore resumes at the next byte when bytes outside every unwind function do not decode, and ends a decoding outside them at the start of the next unwind function. Code without an unwind entry that does not decode from its first byte is skipped by these scans instead of failing the relink; inside an unwind function a decode failure still fails it. +- In the same executables and modules, the bytes between unwind functions are 16-byte slots of function tails and padding, and a tail can begin inside an instruction. The [instruction scanner](../../../core/relinker/codegen/src/InstructionScanner.cpp) of the syscall and call-site scans therefore resumes at the next byte when bytes outside every unwind function do not decode, and ends a decoding outside them at the start of the next unwind function. Code without an unwind entry that does not decode from its first byte is skipped by these scans instead of failing the relink; inside an unwind function a decode failure still fails it. - In executables and guest modules with a `PT_SCE_DYNLIBDATA` segment, the [code analysis](../../../core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp) of the Windows and macOS outputs follows code only from the end of `PT_INTERP`, or of the `/libexec/ld-elf.so.1` string that begins the segment when no `PT_INTERP` covers it, as in OpenOrbis modules, to the end of the PLT after the last function of the unwind table, or to the end of that function when the PLT comes before the code, as in the `libSceFios2.prx` and `libc.prx` that CUSA02636 bundles (PLT at 0xc8); the pipeline's scans of executables above stop at this same end, also when the PLT comes before the code. A guest module with PLT relocations and no PLT before or after its unwind functions, or with a PLT that overlaps one, fails as the executable does. Code after that end is not analyzed, such as the `DT_FINI` routine without an unwind entry that follows the last unwind function in both modules and that the Windows and macOS outputs call when the program exits: a TLS access in it is not rewritten, the ELF entry point, `DT_INIT`, `DT_FINI`, a branch or a pointer into it is ignored, and a function symbol or init/fini array entry pointing into it fails the conversion. - A relocation against a dynamic symbol entry that is all zero, like the null symbol, takes the value 0 plus its addend, as the gABI defines for `STN_UNDEF` ([pipeline](../../../core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp)). The Linux output imports such an entry as an unnamed weak symbol, which the host loader resolves to 0; the Windows and macOS outputs write the value into the slot ([relocation builder](../../../core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp)). What the console's loader does with such an entry is not measured. OliSe Player (PPSA01153) has seven of them, and its code calls through those slots only when they are not 0 ([#4437](https://github.com/boykopovar/AnyPS5/issues/4437)). - In macOS output, a guest instruction with a GS segment prefix, including one with no memory operand (`65 90`, `65 48 89 c8`), which does not touch GS, becomes `UD2` and NOP padding of its own length ([macOS image writer](../../../core/relinker/elfpatcher/src/macos/MacOsImage.cpp)): the host owns GS, which holds the pthread TSD base the guest TLS stubs read, and the guest GS base that FreeBSD's `sysarch(AMD64_SET_GSBASE)` would set is not emulated. A converted module runs until such an instruction is reached, which then faults. Prospero Win (PPSA99995) has six in its Wine `ntdll`. From 0f34160a49a9f44951cd7d5a4f966e8782ae4fb7 Mon Sep 17 00:00:00 2001 From: Nithishvelan M Date: Mon, 19 Oct 2026 17:27:50 +0530 Subject: [PATCH 084/269] feat(libc): export tgammaf float math function PPSA26578 (issue #7624) imports tgammaf (NID B2ZbqV9geCM) from libc, and its bundled libc.prx does not export it, causing the process to exit with 0xC0000139 (GetLastError: 127). Export tgammaf_nid_postfix in core/libs/prx/libc/src/Math.cpp delegating to std::tgamma(float), and add unit test coverage in GuestMath verifying factorials, half-integers, poles, and negative values. --- core/libs/prx/libc/src/Math.cpp | 1 + core/libs/tests/GuestMath.cpp | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index 8d21db8f88..8391b613f2 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -344,6 +344,7 @@ long double APS5_VABI log1pl_nid_postfix(long double x) { return std::log1p(x); long double APS5_VABI log2l_nid_postfix(long double x) { return std::log2(x); } long double APS5_VABI lgammal_nid_postfix(long double x) { return std::lgamma(x); } long double APS5_VABI tgammal_nid_postfix(long double x) { return std::tgamma(x); } +float APS5_VABI tgammaf_nid_postfix(float x) { return std::tgamma(x); } long double APS5_VABI hypotl_nid_postfix(long double x, long double y) { if (std::isinf(x) || std::isinf(y)) return std::numeric_limits::infinity(); return std::hypot(x, y); diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index 176ac81a25..952710ffec 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -152,6 +152,7 @@ long double APS5_VABI log1pl_nid_postfix(long double); long double APS5_VABI log2l_nid_postfix(long double); long double APS5_VABI lgammal_nid_postfix(long double); long double APS5_VABI tgammal_nid_postfix(long double); +float APS5_VABI tgammaf_nid_postfix(float); long double APS5_VABI hypotl_nid_postfix(long double, long double); float APS5_VABI hypot3f_nid_postfix(float, float, float); double APS5_VABI hypot3_nid_postfix(double, double, double); @@ -861,6 +862,9 @@ static void CheckLongDoubleFunctions() { Require(log2l_nid_postfix(8.0L) == 3.0L && log2l_nid_postfix(0x1p-16445L) == -16445.0L); Require(lgammal_nid_postfix(1.0L) == 0.0L && lgammal_nid_postfix(2.0L) == 0.0L && lgammal_nid_postfix(0.0L) == infinity); Require(tgammal_nid_postfix(5.0L) == 24.0L && Near(tgammal_nid_postfix(0.5L), 1.7724538509055160272981675L)); + Require(tgammaf_nid_postfix(5.0f) == 24.0f && std::abs(tgammaf_nid_postfix(0.5f) - 1.77245385f) < 1e-6f); + Require(tgammaf_nid_postfix(1.0f) == 1.0f && tgammaf_nid_postfix(2.0f) == 1.0f && tgammaf_nid_postfix(3.0f) == 2.0f); + Require(std::isinf(tgammaf_nid_postfix(0.0f)) && std::isnan(tgammaf_nid_postfix(-1.0f))); Require(hypotl_nid_postfix(3.0L, -4.0L) == 5.0L && hypotl_nid_postfix(nan, -infinity) == infinity); Require(hypot3f_nid_postfix(3.0f, -4.0f, 12.0f) == 13.0f && hypot3f_nid_postfix(0.0f, 3.0f, -4.0f) == 5.0f); From 7ccf4da4867e966790f6b4029d6f34f46e54b9d8 Mon Sep 17 00:00:00 2001 From: akkadbakkad89 Date: Mon, 19 Oct 2026 12:25:10 +0000 Subject: [PATCH 085/269] fix(prx): align declarations to the PS5 3.20 export inventory and add debt notes Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- core/libs/prx/libSceHttpCache/Export.cpp | 35 ---------------------- core/libs/prx/libSceOpusCeltEnc/Export.cpp | 5 ---- core/libs/prx/libSceOpusSilkEnc/Export.cpp | 10 ------- core/libs/prx/libSceShellUIUtil/Export.cpp | 30 ------------------- docs/dev/debt/libSceCompanionHttpd.md | 5 ++++ docs/dev/debt/libSceCustomMusicCore.md | 5 ++++ docs/dev/debt/libSceHttpCache.md | 5 ++++ docs/dev/debt/libSceMoveTracker.md | 5 ++++ docs/dev/debt/libSceOpusCeltEnc.md | 5 ++++ docs/dev/debt/libSceOpusSilkEnc.md | 5 ++++ docs/dev/debt/libSceShellUIUtil.md | 5 ++++ 11 files changed, 35 insertions(+), 80 deletions(-) create mode 100644 docs/dev/debt/libSceCompanionHttpd.md create mode 100644 docs/dev/debt/libSceCustomMusicCore.md create mode 100644 docs/dev/debt/libSceHttpCache.md create mode 100644 docs/dev/debt/libSceMoveTracker.md create mode 100644 docs/dev/debt/libSceOpusCeltEnc.md create mode 100644 docs/dev/debt/libSceOpusSilkEnc.md create mode 100644 docs/dev/debt/libSceShellUIUtil.md diff --git a/core/libs/prx/libSceHttpCache/Export.cpp b/core/libs/prx/libSceHttpCache/Export.cpp index 3d221f0db2..f76492fc41 100644 --- a/core/libs/prx/libSceHttpCache/Export.cpp +++ b/core/libs/prx/libSceHttpCache/Export.cpp @@ -15,16 +15,6 @@ int APS5_VABI sceHttpCacheClearAll() { return 0; } -int APS5_VABI sceHttpCacheClearAllUserTags() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHttpCacheClearCacheInfo() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceHttpCacheCompleteRequest() { NotImplemented_nid_no_patch(__func__); return 0; @@ -40,11 +30,6 @@ int APS5_VABI sceHttpCacheCreateRequestWithTag() { return 0; } -int APS5_VABI sceHttpCacheDebugSetUrl() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceHttpCacheDeleteRequest() { NotImplemented_nid_no_patch(__func__); return 0; @@ -60,21 +45,6 @@ int APS5_VABI sceHttpCacheReadData() { return 0; } -int APS5_VABI sceHttpCacheRedirectedConnectionEnabled() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHttpCacheRegisterCacheInfoUrl() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHttpCacheRemoveTag() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceHttpCacheRetrieve() { NotImplemented_nid_no_patch(__func__); return 0; @@ -125,11 +95,6 @@ int APS5_VABI sceHttpCacheSystemInit() { return 0; } -int APS5_VABI sceHttpCacheSystemOnUserServiceEvent() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceHttpCacheSystemSendStatistics() { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceOpusCeltEnc/Export.cpp b/core/libs/prx/libSceOpusCeltEnc/Export.cpp index 57600c82c3..df4e7d1854 100644 --- a/core/libs/prx/libSceOpusCeltEnc/Export.cpp +++ b/core/libs/prx/libSceOpusCeltEnc/Export.cpp @@ -45,11 +45,6 @@ int APS5_VABI sceOpusCeltEncEncodeWithPriority() { return 0; } -int APS5_VABI sceOpusCeltEncGetAjmContext() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceOpusCeltEncGetSize() { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceOpusSilkEnc/Export.cpp b/core/libs/prx/libSceOpusSilkEnc/Export.cpp index 2d8195c3d6..201dab2e4b 100644 --- a/core/libs/prx/libSceOpusSilkEnc/Export.cpp +++ b/core/libs/prx/libSceOpusSilkEnc/Export.cpp @@ -35,11 +35,6 @@ int APS5_VABI sceOpusSilkEncEncode() { return 0; } -int APS5_VABI sceOpusSilkEncEncodeCancelForTimeout() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceOpusSilkEncEncodeFloat() { NotImplemented_nid_no_patch(__func__); return 0; @@ -65,11 +60,6 @@ int APS5_VABI sceOpusSilkEncEncodeWithPriorityAndTimeout() { return 0; } -int APS5_VABI sceOpusSilkEncGetAjmContext() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceOpusSilkEncGetSize() { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceShellUIUtil/Export.cpp b/core/libs/prx/libSceShellUIUtil/Export.cpp index 18a5efad19..4ff433cad7 100644 --- a/core/libs/prx/libSceShellUIUtil/Export.cpp +++ b/core/libs/prx/libSceShellUIUtil/Export.cpp @@ -5,11 +5,6 @@ extern "C" { -int APS5_VABI sceShellUIUtilAbortTask() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceShellUIUtilCreateTaskToken() { NotImplemented_nid_no_patch(__func__); return 0; @@ -20,11 +15,6 @@ int APS5_VABI sceShellUIUtilGetAppUrl() { return 0; } -int APS5_VABI sceShellUIUtilGetDefaultQueryParameter() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceShellUIUtilInitialize() { NotImplemented_nid_no_patch(__func__); return 0; @@ -35,31 +25,11 @@ int APS5_VABI sceShellUIUtilLaunchByUri() { return 0; } -int APS5_VABI sceShellUIUtilLaunchByUriParamInit() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceShellUIUtilLaunchTask() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceShellUIUtilServiceInitialize() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceShellUIUtilServiceSetEventFlag() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceShellUIUtilServiceTerminate() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceShellUIUtilTerminate() { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/docs/dev/debt/libSceCompanionHttpd.md b/docs/dev/debt/libSceCompanionHttpd.md new file mode 100644 index 0000000000..2738a6bd6d --- /dev/null +++ b/docs/dev/debt/libSceCompanionHttpd.md @@ -0,0 +1,5 @@ +# libSceCompanionHttpd technical debt + +### Unknown function info + +- [sceCompanionHttpdAddHeader, sceCompanionHttpdGet2ndScreenStatus, sceCompanionHttpdGetEvent, sceCompanionHttpdGetUserId, sceCompanionHttpdInitialize, sceCompanionHttpdInitialize2, sceCompanionHttpdOptParamInitialize, sceCompanionHttpdRegisterRequestBodyReceptionCallback, sceCompanionHttpdRegisterRequestCallback, sceCompanionHttpdRegisterRequestCallback2, sceCompanionHttpdSetBody, sceCompanionHttpdSetStatus, sceCompanionHttpdStart, sceCompanionHttpdStop, sceCompanionHttpdTerminate, sceCompanionHttpdUnregisterRequestBodyReceptionCallback and sceCompanionHttpdUnregisterRequestCallback](../../../core/libs/prx/libSceCompanionHttpd/Export.cpp) (libSceCompanionHttpd) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCompanionHttpd.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceCustomMusicCore.md b/docs/dev/debt/libSceCustomMusicCore.md new file mode 100644 index 0000000000..9ffaf08475 --- /dev/null +++ b/docs/dev/debt/libSceCustomMusicCore.md @@ -0,0 +1,5 @@ +# libSceCustomMusicCore technical debt + +### Unknown function info + +- [sceCustomMusicCoreBgmClose, sceCustomMusicCoreBgmOpen, sceCustomMusicCoreBgmOpen2, sceCustomMusicCoreBgmOutput, sceCustomMusicCoreBgmSetAudioVolume, sceCustomMusicCoreBgmStart, sceCustomMusicCoreBgmStop, sceCustomMusicCoreGetBgmAuthorityStatus, sceCustomMusicCoreGetHdcpStatus, sceCustomMusicCoreGetSystemAudioVolume, sceCustomMusicCorePostEvent, sceCustomMusicCoreSendEvent, sceCustomMusicCoreSendMulticastEvent, sceCustomMusicCoreSendSpMessage, sceCustomMusicCoreSetSystemAudioVolume, sceCustomMusicCoreStartOperation, sceCustomMusicCoreStartOperation2 and sceCustomMusicCoreStopOperation](../../../core/libs/prx/libSceCustomMusicCore/Export.cpp) (libSceCustomMusicCore) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCustomMusicCore.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceHttpCache.md b/docs/dev/debt/libSceHttpCache.md new file mode 100644 index 0000000000..7c93d10351 --- /dev/null +++ b/docs/dev/debt/libSceHttpCache.md @@ -0,0 +1,5 @@ +# libSceHttpCache technical debt + +### 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 diff --git a/docs/dev/debt/libSceMoveTracker.md b/docs/dev/debt/libSceMoveTracker.md new file mode 100644 index 0000000000..156c1dfb0a --- /dev/null +++ b/docs/dev/debt/libSceMoveTracker.md @@ -0,0 +1,5 @@ +# libSceMoveTracker technical debt + +### 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 diff --git a/docs/dev/debt/libSceOpusCeltEnc.md b/docs/dev/debt/libSceOpusCeltEnc.md new file mode 100644 index 0000000000..6f232c9cee --- /dev/null +++ b/docs/dev/debt/libSceOpusCeltEnc.md @@ -0,0 +1,5 @@ +# libSceOpusCeltEnc technical debt + +### 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 diff --git a/docs/dev/debt/libSceOpusSilkEnc.md b/docs/dev/debt/libSceOpusSilkEnc.md new file mode 100644 index 0000000000..3e129c1e1f --- /dev/null +++ b/docs/dev/debt/libSceOpusSilkEnc.md @@ -0,0 +1,5 @@ +# libSceOpusSilkEnc technical debt + +### 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 diff --git a/docs/dev/debt/libSceShellUIUtil.md b/docs/dev/debt/libSceShellUIUtil.md new file mode 100644 index 0000000000..f07f584e14 --- /dev/null +++ b/docs/dev/debt/libSceShellUIUtil.md @@ -0,0 +1,5 @@ +# libSceShellUIUtil technical debt + +### 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 From eac14403cc645c69afccb6bc59273baee09c2498 Mon Sep 17 00:00:00 2001 From: akkadbakkad89 Date: Mon, 19 Oct 2026 12:26:39 +0000 Subject: [PATCH 086/269] docs(libs): add debt notes for the six declared system libraries Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- docs/dev/debt/libSceAppChecker.md | 5 +++++ docs/dev/debt/libSceAudioLatencyEstimation.md | 5 +++++ docs/dev/debt/libSceCompanionUtil.md | 5 +++++ docs/dev/debt/libSceDiscId.md | 5 +++++ docs/dev/debt/libSceNKWeb.md | 5 +++++ docs/dev/debt/libSceS3da.md | 5 +++++ 6 files changed, 30 insertions(+) create mode 100644 docs/dev/debt/libSceAppChecker.md create mode 100644 docs/dev/debt/libSceAudioLatencyEstimation.md create mode 100644 docs/dev/debt/libSceCompanionUtil.md create mode 100644 docs/dev/debt/libSceDiscId.md create mode 100644 docs/dev/debt/libSceNKWeb.md create mode 100644 docs/dev/debt/libSceS3da.md diff --git a/docs/dev/debt/libSceAppChecker.md b/docs/dev/debt/libSceAppChecker.md new file mode 100644 index 0000000000..9fc80d43e0 --- /dev/null +++ b/docs/dev/debt/libSceAppChecker.md @@ -0,0 +1,5 @@ +# libSceAppChecker technical debt + +### Unknown function info + +- [sceAppCheckerAbort, sceAppCheckerExecute and sceAppCheckerExecuteEx](../../../core/libs/prx/libSceAppChecker/Export.cpp) (libSceAppChecker) - names and NIDs are present in the [aerolib symbol database](https://github.com/zecoxao/sce_symbols/blob/master/aerolib.csv) and are attributed to this module by `sceAppChecker` symbol prefix; the library is absent from the PS5 3.20 export inventory, so the pairing is not firmware-verified and no credible prototypes were found. The declarations have no parameters solely to resolve imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceAudioLatencyEstimation.md b/docs/dev/debt/libSceAudioLatencyEstimation.md new file mode 100644 index 0000000000..9e199bf5f3 --- /dev/null +++ b/docs/dev/debt/libSceAudioLatencyEstimation.md @@ -0,0 +1,5 @@ +# libSceAudioLatencyEstimation technical debt + +### Unknown function info + +- [sceAudioLatencyEstimationAddData, sceAudioLatencyEstimationGetDelayTime, sceAudioLatencyEstimationInitialize and sceAudioLatencyEstimationRelease](../../../core/libs/prx/libSceAudioLatencyEstimation/Export.cpp) (libSceAudioLatencyEstimation) - names and NIDs are present in the [aerolib symbol database](https://github.com/zecoxao/sce_symbols/blob/master/aerolib.csv) and are attributed to this module by `sceAudioLatencyEstimation` symbol prefix; the library is absent from the PS5 3.20 export inventory, so the pairing is not firmware-verified and no credible prototypes were found. The declarations have no parameters solely to resolve imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceCompanionUtil.md b/docs/dev/debt/libSceCompanionUtil.md new file mode 100644 index 0000000000..48d3c1cb47 --- /dev/null +++ b/docs/dev/debt/libSceCompanionUtil.md @@ -0,0 +1,5 @@ +# libSceCompanionUtil technical debt + +### Unknown function info + +- [sceCompanionUtilGetEvent, sceCompanionUtilGetRemoteOskEvent, sceCompanionUtilInitialize, sceCompanionUtilOptParamInitialize and sceCompanionUtilTerminate](../../../core/libs/prx/libSceCompanionUtil/Export.cpp) (libSceCompanionUtil) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCompanionUtil.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceDiscId.md b/docs/dev/debt/libSceDiscId.md new file mode 100644 index 0000000000..fb26a7a344 --- /dev/null +++ b/docs/dev/debt/libSceDiscId.md @@ -0,0 +1,5 @@ +# libSceDiscId technical debt + +### 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 diff --git a/docs/dev/debt/libSceNKWeb.md b/docs/dev/debt/libSceNKWeb.md new file mode 100644 index 0000000000..4191fc13a3 --- /dev/null +++ b/docs/dev/debt/libSceNKWeb.md @@ -0,0 +1,5 @@ +# libSceNKWeb technical debt + +### Unknown function info + +- [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 diff --git a/docs/dev/debt/libSceS3da.md b/docs/dev/debt/libSceS3da.md new file mode 100644 index 0000000000..c941424735 --- /dev/null +++ b/docs/dev/debt/libSceS3da.md @@ -0,0 +1,5 @@ +# libSceS3da technical debt + +### 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 From f677163eecc49f0e2370d3550f16e1a11231f634 Mon Sep 17 00:00:00 2001 From: Nithishvelan M Date: Mon, 19 Oct 2026 17:57:40 +0530 Subject: [PATCH 087/269] feat(libSceSysmodule): implement sceSysmoduleUnloadModuleInternalWithArg Add sceSysmoduleUnloadModuleInternalWithArg to match the existing sceSysmoduleLoadModuleInternalWithArg export. Decrement the module load count on valid internal IDs, check module existence in kModuleTable, and set the return pointer when provided. Add unit test coverage in GuestSysmodule for invalid values, unloaded modules, successful argument unload, and unknown IDs. Update technical debt documentation. --- core/libs/prx/libSceSysmodule/Export.cpp | 22 ++++++++++++++++++++++ core/libs/tests/GuestSysmodule.cpp | 17 +++++++++++++++++ docs/dev/debt/libSceSysmodule.md | 2 +- 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceSysmodule/Export.cpp b/core/libs/prx/libSceSysmodule/Export.cpp index 94f6df2150..63d9041f71 100644 --- a/core/libs/prx/libSceSysmodule/Export.cpp +++ b/core/libs/prx/libSceSysmodule/Export.cpp @@ -272,6 +272,28 @@ int APS5_VABI sceSysmoduleUnloadModuleInternal(std::uint32_t id) { return 0; } +int APS5_VABI sceSysmoduleUnloadModuleInternalWithArg(std::uint32_t id, int argc, void* argv, std::uint64_t unk, int* ret) { + (void)argc; + (void)argv; + (void)unk; + if ((id & 0x7fffffffu) == 0) { + return SCE_SYSMODULE_ERROR_INVALID_VALUE; + } + if (!findModuleName(id)) { + throw std::runtime_error(std::string("sceSysmoduleUnloadModuleInternalWithArg: unknown id ") + std::to_string(id)); + } + std::lock_guard lock(gMutex); + auto it = gLoadCount.find(id); + if (it == gLoadCount.end() || it->second < 1) { + return SCE_SYSMODULE_ERROR_UNLOADED; + } + it->second--; + if (ret) { + *ret = 0; + } + return 0; +} + int APS5_VABI sceSysmoduleIsLoadedInternal(std::uint32_t id) { if ((id & 0x7fffffffu) == 0) { return SCE_SYSMODULE_ERROR_INVALID_VALUE; diff --git a/core/libs/tests/GuestSysmodule.cpp b/core/libs/tests/GuestSysmodule.cpp index ddc6fcbdb9..ab80b5c6e7 100644 --- a/core/libs/tests/GuestSysmodule.cpp +++ b/core/libs/tests/GuestSysmodule.cpp @@ -26,6 +26,7 @@ int APS5_VABI sceSysmoduleLoadModuleInternal(std::uint32_t id); int APS5_VABI sceSysmoduleUnloadModuleInternal(std::uint32_t id); int APS5_VABI sceSysmoduleIsLoadedInternal(std::uint32_t id); int APS5_VABI sceSysmoduleLoadModuleInternalWithArg(std::uint32_t id, int argc, void* argv, std::uint64_t unk, int* ret); +int APS5_VABI sceSysmoduleUnloadModuleInternalWithArg(std::uint32_t id, int argc, void* argv, std::uint64_t unk, int* ret); int APS5_VABI sceSysmoduleGetModuleInfoForUnwind(std::uint64_t addr, int flags, ModuleInfoForUnwind* info); int APS5_VABI sceSysmoduleGetModuleHandleInternal(std::uint32_t id, std::int32_t* handle); int APS5_VABI sceKernelDlsym(std::int32_t handle, const char* symbol, void** addr); @@ -138,6 +139,9 @@ void CheckUnknownIds() { int result = 0x5a5a5a5a; Require(ThrowsRuntimeError([internalId, &result] { sceSysmoduleLoadModuleInternalWithArg(internalId, 0, nullptr, 0, &result); })); Require(result == 0x5a5a5a5a); + int unloadResult = 0x5a5a5a5a; + Require(ThrowsRuntimeError([internalId, &unloadResult] { sceSysmoduleUnloadModuleInternalWithArg(internalId, 0, nullptr, 0, &unloadResult); })); + Require(unloadResult == 0x5a5a5a5a); } #ifdef _WIN32 @@ -313,6 +317,19 @@ int main() { Require(sceSysmoduleUnloadModuleInternal(kHdrScopesInternalModuleId) == 0); Require(sceSysmoduleUnloadModuleInternal(kHdrScopesInternalModuleId) == kModuleUnloaded); + int unloadArgResult = -1; + Require(sceSysmoduleUnloadModuleInternalWithArg(0, 0, nullptr, 0, &unloadArgResult) == kInvalidValue); + Require(unloadArgResult == -1); + Require(sceSysmoduleUnloadModuleInternalWithArg(kAgcInternalId, 0, nullptr, 0, &unloadArgResult) == kModuleUnloaded); + Require(unloadArgResult == -1); + int loadArgResult = -1; + Require(sceSysmoduleLoadModuleInternalWithArg(kAgcInternalId, 0, nullptr, 0, &loadArgResult) == 0); + Require(loadArgResult == 0); + Require(sceSysmoduleIsLoadedInternal(kAgcInternalId) == 0); + Require(sceSysmoduleUnloadModuleInternalWithArg(kAgcInternalId, 0, nullptr, 0, &unloadArgResult) == 0); + Require(unloadArgResult == 0); + Require(sceSysmoduleIsLoadedInternal(kAgcInternalId) == kModuleUnloaded); + Require(sceSysmoduleIsLoadedInternal(kAgcModuleId) == kModuleUnloaded); Require(sceSysmoduleUnloadModuleInternal(kAgcModuleId) == kModuleUnloaded); Require(sceSysmoduleLoadModuleInternal(kAgcModuleId) == 0); diff --git a/docs/dev/debt/libSceSysmodule.md b/docs/dev/debt/libSceSysmodule.md index 5f59554ebf..333f53f42d 100644 --- a/docs/dev/debt/libSceSysmodule.md +++ b/docs/dev/debt/libSceSysmodule.md @@ -2,7 +2,7 @@ ### Silent stubs -- [sceSysmoduleIsLoaded, sceSysmoduleLoadModule, sceSysmoduleUnloadModule](../../../core/libs/prx/libSceSysmodule/Export.cpp) (libSceSysmodule) accept a module id missing from the module table: they log it and return 0 without loading anything, so `sceSysmoduleIsLoaded` reports an unknown module as loaded and unloading it succeeds, because titles that load or unload such an id aborted otherwise. The internal variants (`sceSysmoduleLoadModuleInternal`, `sceSysmoduleUnloadModuleInternal`, `sceSysmoduleLoadModuleInternalWithArg`, `sceSysmoduleGetModuleHandleInternal`) throw for an unknown id. What the console returns for an id it does not know is not verified. +- [sceSysmoduleIsLoaded, sceSysmoduleLoadModule, sceSysmoduleUnloadModule](../../../core/libs/prx/libSceSysmodule/Export.cpp) (libSceSysmodule) accept a module id missing from the module table: they log it and return 0 without loading anything, so `sceSysmoduleIsLoaded` reports an unknown module as loaded and unloading it succeeds, because titles that load or unload such an id aborted otherwise. The internal variants (`sceSysmoduleLoadModuleInternal`, `sceSysmoduleUnloadModuleInternal`, `sceSysmoduleLoadModuleInternalWithArg`, `sceSysmoduleUnloadModuleInternalWithArg`, `sceSysmoduleGetModuleHandleInternal`) throw for an unknown id. What the console returns for an id it does not know is not verified. ### Unknown function info From 32b268fe0f6de58e23fd836c016af5dfb5219587 Mon Sep 17 00:00:00 2001 From: ABEL VALENCIANA <260135423+valenciana-nc@users.noreply.github.com> Date: Sun, 18 Oct 2026 20:25:58 +0000 Subject: [PATCH 088/269] fix(savedata): create exclusive temporary files for save replacement --- core/libs/prx/libSceSaveData/SaveDataFile.hpp | 71 +++++---- core/libs/tests/SaveDataFileWriteFailure.cpp | 140 +++++++++++++++--- core/libs/tests/SaveDataMemoryMetadata.cpp | 111 ++++++++------ 3 files changed, 221 insertions(+), 101 deletions(-) diff --git a/core/libs/prx/libSceSaveData/SaveDataFile.hpp b/core/libs/prx/libSceSaveData/SaveDataFile.hpp index 193c1ffbb2..84fd888180 100644 --- a/core/libs/prx/libSceSaveData/SaveDataFile.hpp +++ b/core/libs/prx/libSceSaveData/SaveDataFile.hpp @@ -1,8 +1,11 @@ #pragma once +#include #include +#include #include -#include +#include +#include #include #ifdef _WIN32 @@ -20,6 +23,7 @@ namespace savedata { template inline bool replace_file_with(const std::filesystem::path& path, const void* data, std::size_t size, ReplaceOperation&& replace_operation) { + if (size != 0 && data == nullptr) return false; const auto parent = path.parent_path(); std::error_code error; if (!parent.empty()) { @@ -29,44 +33,39 @@ inline bool replace_file_with(const std::filesystem::path& path, const void* dat } } - auto temporary = path; - temporary += ".tmp"; - const auto remove_temporary = [&temporary]() { - std::error_code ignored; - std::filesystem::remove(temporary, ignored); - }; - - { - std::ofstream file(temporary, std::ios::binary | std::ios::trunc); - if (!file) { - remove_temporary(); - return false; - } - if (size != 0) { - if (data == nullptr) { - file.close(); - remove_temporary(); - return false; - } - file.write(static_cast(data), static_cast(size)); - } - file.flush(); - if (!file) { - file.close(); - remove_temporary(); - return false; - } - file.close(); - if (!file) { - remove_temporary(); - return false; - } + std::filesystem::path temporary; + std::FILE* file = nullptr; + std::random_device random; + for (unsigned attempt = 0; attempt < 128; ++attempt) { + temporary = path; + temporary += ".tmp." + std::to_string(random()) + "." + std::to_string(random()); +#ifdef _WIN32 + file = _wfopen(temporary.c_str(), L"wbx"); +#else + file = std::fopen(temporary.c_str(), "wbx"); +#endif + if (file) break; + if (errno != EEXIST) return false; } + if (!file) return false; + struct Cleanup { + const std::filesystem::path& path; + std::FILE*& file; + bool remove = true; + ~Cleanup() { + if (file) std::fclose(file); + std::error_code ignored; + if (remove) std::filesystem::remove(path, ignored); + } + } cleanup{temporary, file}; + + const bool written = size == 0 || std::fwrite(data, 1, size, file) == size; + const bool closed = std::fclose(file) == 0; + file = nullptr; + if (!written || !closed) return false; const bool replaced = replace_operation(temporary, path); - if (!replaced) { - remove_temporary(); - } + cleanup.remove = !replaced; return replaced; } diff --git a/core/libs/tests/SaveDataFileWriteFailure.cpp b/core/libs/tests/SaveDataFileWriteFailure.cpp index d9046fb753..740221e2fb 100644 --- a/core/libs/tests/SaveDataFileWriteFailure.cpp +++ b/core/libs/tests/SaveDataFileWriteFailure.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -17,46 +18,147 @@ static void Check(bool value, int line) { } #define Require(value) Check((value), __LINE__) +static std::vector ReadFile(const std::filesystem::path& path) { + std::ifstream file(path, std::ios::binary); + Require(file.is_open()); + return {std::istreambuf_iterator(file), std::istreambuf_iterator()}; +} + +static void WriteFile(const std::filesystem::path& path, const std::vector& data) { + std::ofstream file(path, std::ios::binary); + Require(file.is_open()); + file.write(data.data(), static_cast(data.size())); + Require(static_cast(file)); +} + +static bool ReplaceFile(const std::filesystem::path& temporary, const std::filesystem::path& destination) { +#ifdef _WIN32 + return MoveFileExW(temporary.c_str(), destination.c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH) != 0; +#else + std::error_code error; + std::filesystem::rename(temporary, destination, error); + return !error; +#endif +} + int main() { const auto root = std::filesystem::temp_directory_path() / ("anyps5-savedata-write-failure-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); Require(std::filesystem::create_directory(root)); - const auto savePath = root / "memory.dat"; + const auto savePath = root / std::filesystem::path(u8"m\u00e9moire-\u65e5\u672c.dat"); const std::vector oldData{'o', 'l', 'd'}; const std::vector replacement{'n', 'e', 'w'}; - { - std::ofstream file(savePath, std::ios::binary); - Require(file.is_open()); - file.write(oldData.data(), static_cast(oldData.size())); - Require(static_cast(file)); - } + WriteFile(savePath, oldData); int replaceAttempts = 0; + std::filesystem::path failedTemporary; const bool replaced = savedata::replace_file_with( savePath, replacement.data(), replacement.size(), - [&replaceAttempts, &savePath](const std::filesystem::path& temporary, - const std::filesystem::path& destination) { + [&](const std::filesystem::path& temporary, const std::filesystem::path& destination) { ++replaceAttempts; Require(destination == savePath); - auto expectedTemporary = savePath; - expectedTemporary += ".tmp"; - Require(temporary == expectedTemporary); + Require(temporary != destination && temporary.parent_path() == destination.parent_path()); + Require(ReadFile(temporary) == replacement); + failedTemporary = temporary; return false; }); Require(!replaced); Require(replaceAttempts == 1); - std::vector savedData; - { - std::ifstream savedFile(savePath, std::ios::binary); - savedData.assign(std::istreambuf_iterator(savedFile), std::istreambuf_iterator()); - } - Require(savedData == oldData); + Require(ReadFile(savePath) == oldData); + Require(!std::filesystem::exists(failedTemporary)); + auto temporary = savePath; temporary += ".tmp"; - Require(!std::filesystem::exists(temporary)); + WriteFile(temporary, oldData); + Require(savedata::replace_file(savePath, replacement.data(), replacement.size())); + Require(ReadFile(savePath) == replacement); + Require(std::filesystem::exists(temporary)); + Require(ReadFile(temporary) == oldData); + std::filesystem::remove(temporary); + + const auto unrelated = root / "unrelated.dat"; + WriteFile(unrelated, oldData); + std::filesystem::create_hard_link(unrelated, temporary); + Require(savedata::replace_file(savePath, replacement.data(), replacement.size())); + Require(ReadFile(unrelated) == oldData); + Require(std::filesystem::equivalent(unrelated, temporary)); + Require(!std::filesystem::equivalent(unrelated, savePath)); + std::filesystem::remove(temporary); + + std::error_code symlinkError; + std::filesystem::create_symlink(unrelated, temporary, symlinkError); +#ifdef _WIN32 + if (symlinkError == std::errc::permission_denied || + symlinkError == std::error_code(ERROR_PRIVILEGE_NOT_HELD, std::system_category())) { + std::printf("Symlink case skipped: creating Windows symlinks requires developer mode or privilege\n"); + } else +#endif + { + Require(!symlinkError); + Require(savedata::replace_file(savePath, replacement.data(), replacement.size())); + Require(ReadFile(unrelated) == oldData); + Require(std::filesystem::is_symlink(temporary)); + Require(!std::filesystem::is_symlink(savePath)); + Require(ReadFile(savePath) == replacement); + std::filesystem::remove(temporary); + } + + Require(std::filesystem::create_directory(temporary)); + Require(savedata::replace_file(savePath, replacement.data(), replacement.size())); + Require(std::filesystem::is_directory(temporary)); + std::filesystem::remove(temporary); + + std::filesystem::path outerTemporary; + Require(savedata::replace_file_with(savePath, replacement.data(), replacement.size(), + [&](const auto& outer, const auto& destination) { + outerTemporary = outer; + Require(savedata::replace_file_with(savePath, oldData.data(), oldData.size(), + [&](const auto& inner, const auto& innerDestination) { + Require(inner != outer); + Require(ReadFile(outer) == replacement); + Require(ReadFile(inner) == oldData); + return ReplaceFile(inner, innerDestination); + })); + Require(ReadFile(destination) == oldData); + Require(ReadFile(outer) == replacement); + return ReplaceFile(outer, destination); + })); + Require(ReadFile(savePath) == replacement); + Require(!std::filesystem::exists(outerTemporary)); + + Require(savedata::replace_file_with(savePath, replacement.data(), replacement.size(), + [&](const auto& created, const auto& destination) { + Require(ReplaceFile(created, destination)); + outerTemporary = created; + WriteFile(created, oldData); + return true; + })); + Require(ReadFile(outerTemporary) == oldData); + std::filesystem::remove(outerTemporary); + + std::filesystem::path thrownTemporary; + bool threw = false; + try { + savedata::replace_file_with(savePath, oldData.data(), oldData.size(), + [&](const auto& created, const auto&) -> bool { + thrownTemporary = created; + throw std::runtime_error("injected replacement failure"); + }); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw && !thrownTemporary.empty()); + Require(!std::filesystem::exists(thrownTemporary)); + Require(ReadFile(savePath) == replacement); + + Require(!savedata::replace_file(savePath, nullptr, 1)); + Require(ReadFile(savePath) == replacement); + Require(savedata::replace_file(savePath, nullptr, 0)); + Require(ReadFile(savePath).empty()); + Require(std::distance(std::filesystem::directory_iterator(root), std::filesystem::directory_iterator{}) == 2); std::filesystem::remove_all(root); } diff --git a/core/libs/tests/SaveDataMemoryMetadata.cpp b/core/libs/tests/SaveDataMemoryMetadata.cpp index b3ba4f10ef..2f4820edc7 100644 --- a/core/libs/tests/SaveDataMemoryMetadata.cpp +++ b/core/libs/tests/SaveDataMemoryMetadata.cpp @@ -12,7 +12,15 @@ #include #include #include -#ifndef _WIN32 +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#else #include #include #endif @@ -45,6 +53,48 @@ static std::vector Read(const std::filesystem::path& path) { return {std::istreambuf_iterator(file), std::istreambuf_iterator()}; } +static void RequireNoTemporary(const std::filesystem::path& path) { + const auto prefix = path.filename().string() + ".tmp"; + for (const auto& entry : std::filesystem::directory_iterator(path.parent_path())) { + Require(!entry.path().filename().string().starts_with(prefix)); + } +} + +template +static int BlockDestination(const std::filesystem::path& path, Operation&& operation) { + Require(std::filesystem::create_directory(path)); + const int result = operation(); + Require(std::filesystem::is_directory(path)); + Require(std::filesystem::remove(path)); + RequireNoTemporary(path); + return result; +} + +template +static int FailWrite(const std::filesystem::path& path, [[maybe_unused]] std::size_t sizeLimit, + Operation&& operation) { +#ifdef _WIN32 + const auto locked = CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, + nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + Require(locked != INVALID_HANDLE_VALUE); + const int result = operation(); + Require(CloseHandle(locked) != 0); +#else + rlimit previousLimit{}; + Require(getrlimit(RLIMIT_FSIZE, &previousLimit) == 0); + auto writeLimit = previousLimit; + writeLimit.rlim_cur = sizeLimit; + const auto previousHandler = std::signal(SIGXFSZ, SIG_IGN); + Require(previousHandler != SIG_ERR); + Require(setrlimit(RLIMIT_FSIZE, &writeLimit) == 0); + const int result = operation(); + Require(setrlimit(RLIMIT_FSIZE, &previousLimit) == 0); + Require(std::signal(SIGXFSZ, previousHandler) != SIG_ERR); +#endif + RequireNoTemporary(path); + return result; +} + int main() { Require(sceSaveDataCreateTransactionResource(1024) == kSaveDataNotInitialized); Require(sceSaveDataDeleteTransactionResource(1) == kSaveDataNotInitialized); @@ -82,12 +132,9 @@ int main() { const auto original = Read(path); Require(original.size() == sizeof(param)); param.user_param = 42; - const auto temporary = path.string() + ".tmp"; - Require(std::filesystem::create_directory(temporary)); - const int status = sceSaveDataSetSaveDataMemory2(&set); + const int status = FailWrite(path, 16, [&] { return sceSaveDataSetSaveDataMemory2(&set); }); Require(Read(path) == original); Require(status == static_cast(0x809F000Bu)); - Require(!std::filesystem::exists(temporary)); Require(sceSaveDataSetSaveDataMemory2(&set) == 0); const auto* bytes = reinterpret_cast(¶m); Require(Read(path) == std::vector(bytes, bytes + sizeof(param))); @@ -106,26 +153,9 @@ int main() { Require(sceSaveDataSetSaveDataMemory2(&set) == static_cast(0x809F0000u)); Require(Read(memoryPath) == memoryOriginal); data[1].offset = 24; - const auto memoryTemporary = memoryPath.string() + ".tmp"; - Require(std::filesystem::create_directory(memoryTemporary)); - Require(sceSaveDataSetSaveDataMemory2(&set) == static_cast(0x809F000Bu)); - Require(Read(memoryPath) == memoryOriginal); - Require(!std::filesystem::exists(memoryTemporary)); -#ifndef _WIN32 - rlimit previousLimit{}; - Require(getrlimit(RLIMIT_FSIZE, &previousLimit) == 0); - auto writeLimit = previousLimit; - writeLimit.rlim_cur = 16; - const auto previousHandler = std::signal(SIGXFSZ, SIG_IGN); - Require(previousHandler != SIG_ERR); - Require(setrlimit(RLIMIT_FSIZE, &writeLimit) == 0); - const int writeStatus = sceSaveDataSetSaveDataMemory2(&set); - Require(setrlimit(RLIMIT_FSIZE, &previousLimit) == 0); - Require(std::signal(SIGXFSZ, previousHandler) != SIG_ERR); + const int writeStatus = FailWrite(memoryPath, 16, [&] { return sceSaveDataSetSaveDataMemory2(&set); }); Require(writeStatus == static_cast(0x809F000Bu)); Require(Read(memoryPath) == memoryOriginal); - Require(!std::filesystem::exists(memoryTemporary)); -#endif Require(sceSaveDataSetSaveDataMemory2(&set) == 0); auto memoryExpected = memoryOriginal; std::copy(first.begin(), first.end(), memoryExpected.begin() + 4); @@ -156,9 +186,7 @@ int main() { Require(Read(memoryPath) == memoryExpected); const auto setupParamPath = std::filesystem::path("_sd_mem/u42/slot0.param"); const auto setupBinPath = std::filesystem::path("_sd_mem/u42/slot0.bin"); - const auto setupParamTemp = setupParamPath.string() + ".tmp"; Require(std::filesystem::create_directories(setupParamPath.parent_path())); - Require(std::filesystem::create_directory(setupParamTemp)); SaveDataParam setupParam{}; setupParam.user_param = 100; SaveDataMemorySetup2 setup2{}; @@ -169,11 +197,10 @@ int main() { setup2.init_param = &setupParam; SaveDataMemorySetupResult setupResult{}; setupResult.existed_memory_size = 555; - Require(sceSaveDataSetupSaveDataMemory2(&setup2, &setupResult) == static_cast(0x809F000Bu)); + Require(BlockDestination(setupParamPath, [&] { return sceSaveDataSetupSaveDataMemory2(&setup2, &setupResult); }) == static_cast(0x809F000Bu)); Require(setupResult.existed_memory_size == 555); Require(!std::filesystem::exists(setupBinPath)); Require(!std::filesystem::exists(setupParamPath)); - Require(!std::filesystem::exists(setupParamTemp)); Require(sceSaveDataSetupSaveDataMemory2(&setup2, &setupResult) == 0); Require(setupResult.existed_memory_size == 0); Require(Read(setupBinPath) == std::vector(128, 0)); @@ -188,18 +215,15 @@ int main() { seededFile.write(seededBin.data(), static_cast(seededBin.size())); Require(static_cast(seededFile)); } - const auto growthParamTemp = setupParamPath.string() + ".tmp"; - Require(std::filesystem::create_directory(growthParamTemp)); SaveDataParam previousParam = setupParam; setupParam.user_param = 200; setup2.memory_size = 256; setupResult.existed_memory_size = 999; - Require(sceSaveDataSetupSaveDataMemory2(&setup2, &setupResult) == static_cast(0x809F000Bu)); + Require(FailWrite(setupParamPath, 16, [&] { return sceSaveDataSetupSaveDataMemory2(&setup2, &setupResult); }) == static_cast(0x809F000Bu)); Require(setupResult.existed_memory_size == 999); Require(Read(setupBinPath) == seededBin); const auto* previousBytes = reinterpret_cast(&previousParam); Require(Read(setupParamPath) == std::vector(previousBytes, previousBytes + sizeof(previousParam))); - Require(!std::filesystem::exists(growthParamTemp)); Require(sceSaveDataSetupSaveDataMemory2(&setup2, &setupResult) == 0); Require(setupResult.existed_memory_size == 128); std::vector expectedGrownBin = seededBin; @@ -207,28 +231,24 @@ int main() { Require(Read(setupBinPath) == expectedGrownBin); const auto* updatedBytes = reinterpret_cast(&setupParam); Require(Read(setupParamPath) == std::vector(updatedBytes, updatedBytes + sizeof(setupParam))); - const auto growthBinTemp = setupBinPath.string() + ".tmp"; - Require(std::filesystem::create_directory(growthBinTemp)); setupParam.user_param = 300; - setup2.memory_size = 512; + constexpr std::size_t GrownMemorySize = 2 * sizeof(SaveDataParam); + setup2.memory_size = GrownMemorySize; setupResult.existed_memory_size = 888; - Require(sceSaveDataSetupSaveDataMemory2(&setup2, &setupResult) == static_cast(0x809F000Bu)); + Require(FailWrite(setupBinPath, sizeof(SaveDataParam), [&] { return sceSaveDataSetupSaveDataMemory2(&setup2, &setupResult); }) == static_cast(0x809F000Bu)); Require(setupResult.existed_memory_size == 888); Require(Read(setupBinPath) == expectedGrownBin); const auto* param300Bytes = reinterpret_cast(&setupParam); Require(Read(setupParamPath) == std::vector(param300Bytes, param300Bytes + sizeof(setupParam))); - Require(!std::filesystem::exists(growthBinTemp)); Require(sceSaveDataSetupSaveDataMemory2(&setup2, &setupResult) == 0); Require(setupResult.existed_memory_size == 256); - std::vector expectedBlob512 = expectedGrownBin; - expectedBlob512.resize(512, 0); - Require(Read(setupBinPath) == expectedBlob512); + std::vector expectedFinalBlob = expectedGrownBin; + expectedFinalBlob.resize(GrownMemorySize, 0); + Require(Read(setupBinPath) == expectedFinalBlob); Require(Read(setupParamPath) == std::vector(param300Bytes, param300Bytes + sizeof(setupParam))); const auto freshFailBinPath = std::filesystem::path("_sd_mem/u55/slot0.bin"); const auto freshFailParamPath = std::filesystem::path("_sd_mem/u55/slot0.param"); Require(std::filesystem::create_directories(freshFailBinPath.parent_path())); - const auto freshFailBinTemp = freshFailBinPath.string() + ".tmp"; - Require(std::filesystem::create_directory(freshFailBinTemp)); SaveDataParam freshFailParam{}; freshFailParam.user_param = 55; SaveDataMemorySetup2 freshFailSetup{}; @@ -239,12 +259,11 @@ int main() { freshFailSetup.init_param = &freshFailParam; SaveDataMemorySetupResult freshFailResult{}; freshFailResult.existed_memory_size = 777; - Require(sceSaveDataSetupSaveDataMemory2(&freshFailSetup, &freshFailResult) == static_cast(0x809F000Bu)); + Require(BlockDestination(freshFailBinPath, [&] { return sceSaveDataSetupSaveDataMemory2(&freshFailSetup, &freshFailResult); }) == static_cast(0x809F000Bu)); Require(freshFailResult.existed_memory_size == 777); Require(!std::filesystem::exists(freshFailBinPath)); const auto* freshFailBytes = reinterpret_cast(&freshFailParam); Require(Read(freshFailParamPath) == std::vector(freshFailBytes, freshFailBytes + sizeof(freshFailParam))); - Require(!std::filesystem::exists(freshFailBinTemp)); Require(sceSaveDataSetupSaveDataMemory2(&freshFailSetup, &freshFailResult) == 0); Require(freshFailResult.existed_memory_size == 0); Require(Read(freshFailBinPath) == std::vector(64, 0)); @@ -316,13 +335,13 @@ int main() { const auto* orphanBytes = reinterpret_cast(&orphanParam); Require(Read(orphanParamPath) == std::vector(orphanBytes, orphanBytes + sizeof(orphanParam))); Require(sceSaveDataTerminate() == 0); - Require(Read(setupBinPath) == expectedBlob512); + Require(Read(setupBinPath) == expectedFinalBlob); Require(Read(setupParamPath) == std::vector(param300Bytes, param300Bytes + sizeof(setupParam))); Require(sceSaveDataInitialize3(nullptr) == 0); setupResult.existed_memory_size = 111; Require(sceSaveDataSetupSaveDataMemory2(&setup2, &setupResult) == 0); - Require(setupResult.existed_memory_size == 512); - Require(Read(setupBinPath) == expectedBlob512); + Require(setupResult.existed_memory_size == GrownMemorySize); + Require(Read(setupBinPath) == expectedFinalBlob); Require(Read(setupParamPath) == std::vector(param300Bytes, param300Bytes + sizeof(setupParam))); Require(sceSaveDataTerminate() == 0); Require(sceSaveDataCreateTransactionResource(1024) == kSaveDataNotInitialized); From 3eee6be59dfbbe7c8a79939ea0caeea55c6518ac Mon Sep 17 00:00:00 2001 From: valenciana-nc Date: Mon, 19 Oct 2026 06:28:10 -0600 Subject: [PATCH 089/269] test(savedata): update journal and Windows failure cases for exclusive staging --- core/libs/tests/SaveDataFileWriteFailure.cpp | 13 +++++++++++++ core/libs/tests/SaveDataMemoryJournal.cpp | 12 +++++++++--- core/libs/tests/SaveDataMemoryMetadata.cpp | 2 +- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/core/libs/tests/SaveDataFileWriteFailure.cpp b/core/libs/tests/SaveDataFileWriteFailure.cpp index 740221e2fb..933392c62a 100644 --- a/core/libs/tests/SaveDataFileWriteFailure.cpp +++ b/core/libs/tests/SaveDataFileWriteFailure.cpp @@ -89,7 +89,13 @@ int main() { std::filesystem::remove(temporary); std::error_code symlinkError; +#ifdef _WIN32 + if (!CreateSymbolicLinkW(temporary.c_str(), unrelated.c_str(), SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE)) { + symlinkError = std::error_code(GetLastError(), std::system_category()); + } +#else std::filesystem::create_symlink(unrelated, temporary, symlinkError); +#endif #ifdef _WIN32 if (symlinkError == std::errc::permission_denied || symlinkError == std::error_code(ERROR_PRIVILEGE_NOT_HELD, std::system_category())) { @@ -100,8 +106,15 @@ int main() { Require(!symlinkError); Require(savedata::replace_file(savePath, replacement.data(), replacement.size())); Require(ReadFile(unrelated) == oldData); +#ifdef _WIN32 + const auto linkAttributes = GetFileAttributesW(temporary.c_str()); + const auto saveAttributes = GetFileAttributesW(savePath.c_str()); + Require(linkAttributes != INVALID_FILE_ATTRIBUTES && (linkAttributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0); + Require(saveAttributes != INVALID_FILE_ATTRIBUTES && (saveAttributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0); +#else Require(std::filesystem::is_symlink(temporary)); Require(!std::filesystem::is_symlink(savePath)); +#endif Require(ReadFile(savePath) == replacement); std::filesystem::remove(temporary); } diff --git a/core/libs/tests/SaveDataMemoryJournal.cpp b/core/libs/tests/SaveDataMemoryJournal.cpp index 89c0d79a82..e661a1fd1f 100644 --- a/core/libs/tests/SaveDataMemoryJournal.cpp +++ b/core/libs/tests/SaveDataMemoryJournal.cpp @@ -91,10 +91,16 @@ int main() { Require(std::filesystem::create_directory(temporaryPath)); first.fill('q'); - Require(sceSaveDataSetSaveDataMemory2(&set) == static_cast(0x809F000Bu)); - Require(Read(memoryPath) == expected); + Require(sceSaveDataSetSaveDataMemory2(&set) == 0); + std::copy(first.begin(), first.end(), expected.begin() + 8); + Require(Read(memoryPath) == expected && Read(aliasPath) == expected); Require(!std::filesystem::exists(journalPath)); - Require(!std::filesystem::exists(temporaryPath)); + Require(std::filesystem::is_directory(temporaryPath)); + Require(std::filesystem::remove(temporaryPath)); + const auto temporaryPrefix = temporaryPath.filename().string(); + for (const auto& entry : std::filesystem::directory_iterator(memoryPath.parent_path())) { + Require(!entry.path().filename().string().starts_with(temporaryPrefix)); + } Write(journalPath, Journal(16, "jour")); std::array got{}; diff --git a/core/libs/tests/SaveDataMemoryMetadata.cpp b/core/libs/tests/SaveDataMemoryMetadata.cpp index 2f4820edc7..b3acb20559 100644 --- a/core/libs/tests/SaveDataMemoryMetadata.cpp +++ b/core/libs/tests/SaveDataMemoryMetadata.cpp @@ -74,7 +74,7 @@ template static int FailWrite(const std::filesystem::path& path, [[maybe_unused]] std::size_t sizeLimit, Operation&& operation) { #ifdef _WIN32 - const auto locked = CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, + const auto locked = CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); Require(locked != INVALID_HANDLE_VALUE); const int result = operation(); From 1c993d18894a7fadcc30fb6f92b6e413805fe8dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Ortellado?= Date: Fri, 16 Oct 2026 10:08:04 -0300 Subject: [PATCH 090/269] test(agc): bound shader helper test runtime --- core/libs/prx/libSceAgc/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/prx/libSceAgc/CMakeLists.txt b/core/libs/prx/libSceAgc/CMakeLists.txt index 351466befa..a411cacdb2 100644 --- a/core/libs/prx/libSceAgc/CMakeLists.txt +++ b/core/libs/prx/libSceAgc/CMakeLists.txt @@ -428,6 +428,7 @@ configure_windows_unwind(agc_shader_helpers_tests) if(BUILD_TESTING) set_target_properties(agc_shader_helpers_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) add_test(NAME agc_shader_helpers COMMAND agc_shader_helpers_tests) + set_tests_properties(agc_shader_helpers PROPERTIES TIMEOUT 30) if(WIN32) set_tests_properties(agc_shader_helpers PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") From 6c2ff1df855f3d814e1e34fdba16c010818b3584 Mon Sep 17 00:00:00 2001 From: Nithishvelan M Date: Mon, 19 Oct 2026 18:01:06 +0530 Subject: [PATCH 091/269] feat(libSceNpAuth): export sceNpAuthGetAuthorizationCode and sceNpAuthGetIdToken Export sceNpAuthGetAuthorizationCode (NID KxGkOrQJTqY) and sceNpAuthGetIdToken (NID uaB-LoJqHis) in libSceNpAuth, delegating to the corresponding V3 implementations. Add test coverage in GuestNpAuth for both calls covering invalid arguments, signed-out responses on sync requests, deferred async handling, and unknown request IDs, as well as sceNpAuthGetAuthorizedAppCode. Update technical debt documentation. --- core/libs/prx/libSceNpAuth/Export.cpp | 8 ++++++++ core/libs/tests/GuestNpAuth.cpp | 19 +++++++++++++++++-- docs/dev/debt/libSceNpAuth.md | 2 +- 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceNpAuth/Export.cpp b/core/libs/prx/libSceNpAuth/Export.cpp index 86734f9a5a..3de53ed094 100644 --- a/core/libs/prx/libSceNpAuth/Export.cpp +++ b/core/libs/prx/libSceNpAuth/Export.cpp @@ -66,6 +66,14 @@ int APS5_VABI sceNpAuthGetAuthorizedAppCode() { return SCE_NP_ERROR_SIGNED_OUT; } +int APS5_VABI sceNpAuthGetAuthorizationCode(int req_id, const void* param, void* auth_code) { + return sceNpAuthGetAuthorizationCodeV3(req_id, param, auth_code, nullptr); +} + +int APS5_VABI sceNpAuthGetIdToken(int req_id, const void* param, void* id_token) { + return sceNpAuthGetIdTokenV3(req_id, param, id_token); +} + int APS5_VABI sceNpAuthGetAuthorizationCodeV3(int req_id, const void* param, void* auth_code, int* issuer_id) { (void)issuer_id; if (!param || !auth_code) return ORBIS_NP_AUTH_ERROR_INVALID_ARGUMENT; diff --git a/core/libs/tests/GuestNpAuth.cpp b/core/libs/tests/GuestNpAuth.cpp index 92cdf6c335..e9352d3f8c 100644 --- a/core/libs/tests/GuestNpAuth.cpp +++ b/core/libs/tests/GuestNpAuth.cpp @@ -8,7 +8,10 @@ int APS5_VABI sceNpAuthAbortRequest(int req_id); int APS5_VABI sceNpAuthCreateAsyncRequest(const void* param); int APS5_VABI sceNpAuthCreateRequest(void); int APS5_VABI sceNpAuthDeleteRequest(int req_id); +int APS5_VABI sceNpAuthGetAuthorizationCode(int req_id, const void* param, void* auth_code); int APS5_VABI sceNpAuthGetAuthorizationCodeV3(int req_id, const void* param, void* auth_code, int* issuer_id); +int APS5_VABI sceNpAuthGetAuthorizedAppCode(); +int APS5_VABI sceNpAuthGetIdToken(int req_id, const void* param, void* id_token); int APS5_VABI sceNpAuthGetIdTokenV3(int req_id, const void* param, void* id_token); int APS5_VABI sceNpAuthPollAsync(int req_id, int* result); 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); @@ -52,14 +55,24 @@ int main() { int waitUnprocessed = 0; Require(sceNpAuthWaitAsync(asyncReq, &waitUnprocessed) == invalidId); + Require(sceNpAuthGetAuthorizedAppCode() == signedOut); + + Require(sceNpAuthGetIdToken(syncReq, nullptr, token) == invalidArgument); + Require(sceNpAuthGetIdToken(syncReq, param, nullptr) == invalidArgument); + Require(sceNpAuthGetIdToken(syncReq, nullptr, nullptr) == invalidArgument); Require(sceNpAuthGetIdTokenV3(syncReq, nullptr, token) == invalidArgument); Require(sceNpAuthGetIdTokenV3(syncReq, param, nullptr) == invalidArgument); Require(sceNpAuthGetIdTokenV3(syncReq, nullptr, nullptr) == invalidArgument); - Require(sceNpAuthGetIdTokenV3(syncReq, param, token) == signedOut); + Require(sceNpAuthGetIdToken(syncReq, param, token) == signedOut); + Require(sceNpAuthGetIdToken(syncReq, param, token) == invalidArgument); Require(sceNpAuthGetIdTokenV3(syncReq, param, token) == invalidArgument); + + Require(sceNpAuthGetAuthorizationCode(syncReq2, nullptr, authCode) == invalidArgument); + Require(sceNpAuthGetAuthorizationCode(syncReq2, param, nullptr) == invalidArgument); Require(sceNpAuthGetAuthorizationCodeV3(syncReq, nullptr, authCode, &issuerId) == invalidArgument); Require(sceNpAuthGetAuthorizationCodeV3(syncReq, param, nullptr, &issuerId) == invalidArgument); - Require(sceNpAuthGetAuthorizationCodeV3(syncReq2, param, authCode, &issuerId) == signedOut); + Require(sceNpAuthGetAuthorizationCode(syncReq2, param, authCode) == signedOut); + Require(sceNpAuthGetAuthorizationCode(syncReq2, param, authCode) == invalidArgument); Require(sceNpAuthGetAuthorizationCodeV3(syncReq2, param, authCode, &issuerId) == invalidArgument); Require(sceNpAuthGetIdTokenV3(asyncReq, param, token) == 0); @@ -82,7 +95,9 @@ int main() { Require(asyncWait2 == signedOut); constexpr int unknownReq = 999999; + Require(sceNpAuthGetIdToken(unknownReq, param, token) == requestNotFound); Require(sceNpAuthGetIdTokenV3(unknownReq, param, token) == requestNotFound); + Require(sceNpAuthGetAuthorizationCode(unknownReq, param, authCode) == requestNotFound); Require(sceNpAuthGetAuthorizationCodeV3(unknownReq, param, authCode, &issuerId) == requestNotFound); int pollResult = 0; Require(sceNpAuthPollAsync(unknownReq, &pollResult) == requestNotFound); diff --git a/docs/dev/debt/libSceNpAuth.md b/docs/dev/debt/libSceNpAuth.md index 8e344dfda7..1755c39396 100644 --- a/docs/dev/debt/libSceNpAuth.md +++ b/docs/dev/debt/libSceNpAuth.md @@ -3,5 +3,5 @@ ### 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, sceNpAuthGetAuthorizationCodeV3, sceNpAuthGetIdTokenV3, sceNpAuthPollAsync, sceNpAuthWaitAsync](../../../core/libs/prx/libSceNpAuth/Export.cpp) (libSceNpAuth) - request tracking, `ORBIS_NP_AUTH_ERROR_REQUEST_NOT_FOUND` (0x80550306) on unknown request 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 repeated calls on completed requests, and `ORBIS_NP_AUTH_ERROR_ABORTED` (0x80550304) after an abort follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/np/np_auth.cpp) (PS4); async requests return OK and defer `SCE_NP_ERROR_SIGNED_OUT` to `sceNpAuthPollAsync`/`sceNpAuthWaitAsync` while sync requests fail immediately; PlayStation Network is not contacted. The parameter `size` fields are not checked (`ORBIS_NP_AUTH_ERROR_INVALID_SIZE`, structure layouts unknown), the request table has no limit (`ORBIS_NP_AUTH_ERROR_REQUEST_MAX`), and console behavior on PS5 is unverified +- [sceNpAuthAbortRequest, sceNpAuthGetAuthorizationCode, sceNpAuthGetAuthorizationCodeV3, sceNpAuthGetIdToken, 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/main/src/core/libraries/np/np_auth.cpp) (PS4); 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 From 16683be010d714d2b1125f00ef2bedfff181c3e9 Mon Sep 17 00:00:00 2001 From: mazen Date: Mon, 19 Oct 2026 17:39:02 +0400 Subject: [PATCH 092/269] fix(agc): hold a pending producer batch with a timeline semaphore A host event cannot stay unsignaled across vkQueueSubmit, and vkSetEvent is illegal while that wait is still pending. The fixture now waits on a timeline semaphore and signals it only after the leased producer is submitted, so the earlier label stays pending and the indirect dispatch still takes the CPU fallback. --- .../Graphics/include/Recorder.hpp | 3 ++ .../libSceAgcDriver/Graphics/src/Recorder.cpp | 21 +++++++-- .../IndirectArgumentsPendingProducer.cpp | 46 ++++++++++++------- 3 files changed, 50 insertions(+), 20 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp index f4d473d7a5..dc217a1d52 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp @@ -196,6 +196,7 @@ public: std::optional DescribePendingWrite(std::uint64_t address, std::size_t bytes) const; // Ends and submits the open batch without waiting. void Submit(); + void HoldNextSubmit(VkSemaphore semaphore, std::uint64_t value); // Submits the open batch and returns the serial of the newest submitted batch (0 when nothing was // ever submitted); WaitSerial(serial) then covers all recorded work. std::uint64_t SubmitAndEpoch(); @@ -804,6 +805,8 @@ private: Context context; VkSemaphore timeline = VK_NULL_HANDLE; + VkSemaphore submitHold = VK_NULL_HANDLE; + std::uint64_t submitHoldValue = 0; // Identity for a thread that released the mutex around a wait (syncThroughUnlocked): the // recorder may have been torn down meanwhile, so it is looked up by this id, not by pointer. std::uint64_t id = 0; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp index 0cf39a8ad7..4d37bb3317 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp @@ -3395,6 +3395,11 @@ bool Recorder::writtenBackSince(std::uint64_t sequence, std::uint64_t begin, std return false; } +void Recorder::HoldNextSubmit(VkSemaphore semaphore, std::uint64_t value) { + submitHold = semaphore; + submitHoldValue = value; +} + void Recorder::Submit() { PerformanceTimer frameTiming("Recorder.Submit"); // The work count is cleared even when nothing is open: the driver counts a dispatch after its @@ -3444,13 +3449,23 @@ void Recorder::Submit() { const std::uint64_t serial = submissions + 1; CaptureTrace::Log("submit batch=%llu reads=%zu writes=%zu", static_cast(serial), batch->reads.size(), batch->writes.size()); VkTimelineSemaphoreSubmitInfoKHR timelineInfo{VK_STRUCTURE_TYPE_TIMELINE_SEMAPHORE_SUBMIT_INFO_KHR}; - timelineInfo.signalSemaphoreValueCount = 1; - timelineInfo.pSignalSemaphoreValues = &serial; + const VkSemaphore hold = std::exchange(submitHold, VK_NULL_HANDLE); + const std::uint64_t holdValue = submitHoldValue; + VkPipelineStageFlags holdStage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; + if (hold != VK_NULL_HANDLE) { + submission.waitSemaphoreCount = 1; + submission.pWaitSemaphores = &hold; + submission.pWaitDstStageMask = &holdStage; + timelineInfo.waitSemaphoreValueCount = 1; + timelineInfo.pWaitSemaphoreValues = &holdValue; + } if (timeline != VK_NULL_HANDLE) { - submission.pNext = &timelineInfo; + timelineInfo.signalSemaphoreValueCount = 1; + timelineInfo.pSignalSemaphoreValues = &serial; submission.signalSemaphoreCount = 1; submission.pSignalSemaphores = &timeline; } + if (hold != VK_NULL_HANDLE || timeline != VK_NULL_HANDLE) submission.pNext = &timelineInfo; const auto submitStart = std::chrono::steady_clock::now(); const auto result = function(queueSubmit, "vkQueueSubmit")(context.queue, 1, &submission, batch->fence); if (result == VK_ERROR_DEVICE_LOST) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/IndirectArgumentsPendingProducer.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/IndirectArgumentsPendingProducer.cpp index d1cc6ec8e0..306f35cbd0 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/IndirectArgumentsPendingProducer.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/IndirectArgumentsPendingProducer.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -168,12 +169,14 @@ public: Require(library != nullptr, "cannot load Vulkan for the producer submission gate"); resolve = reinterpret_cast(SDL_LoadFunction(library.get(), "vkGetDeviceProcAddr")); Require(resolve != nullptr, "cannot resolve vkGetDeviceProcAddr for the producer submission gate"); - const auto createEvent = function("vkCreateEvent"); - destroyEvent = function("vkDestroyEvent"); - setEvent = function("vkSetEvent"); - waitEvents = function("vkCmdWaitEvents"); - const VkEventCreateInfo info{VK_STRUCTURE_TYPE_EVENT_CREATE_INFO}; - Require(createEvent(device.Device(), &info, nullptr, &event) == VK_SUCCESS, "cannot create the producer submission gate"); + const auto createSemaphore = function("vkCreateSemaphore"); + destroySemaphore = function("vkDestroySemaphore"); + signalSemaphore = reinterpret_cast(resolve(device.Device(), "vkSignalSemaphoreKHR")); + if (signalSemaphore == nullptr) signalSemaphore = function("vkSignalSemaphore"); + VkSemaphoreTypeCreateInfo type{VK_STRUCTURE_TYPE_SEMAPHORE_TYPE_CREATE_INFO}; + type.semaphoreType = VK_SEMAPHORE_TYPE_TIMELINE; + VkSemaphoreCreateInfo info{VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO, &type}; + Require(createSemaphore(device.Device(), &info, nullptr, &semaphore) == VK_SUCCESS, "cannot create the producer submission gate"); } ~ProducerSubmissionGate() { @@ -181,16 +184,17 @@ public: worker.request_stop(); worker.join(); } - setEvent(device.Device(), event); + Release(); try { device.WaitIdle(); } catch (...) { } - destroyEvent(device.Device(), event, nullptr); + destroySemaphore(device.Device(), semaphore, nullptr); } - void Record(AgcDriver::Graphics::Recorder& recorder) { - waitEvents(recorder.Commands(), 1, &event, VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, nullptr, 0, nullptr, 0, nullptr); + void Arm(AgcDriver::Graphics::Recorder& recorder) { + recorder.Commands(); + recorder.HoldNextSubmit(semaphore, 1); } void ReleaseAfterSubmission(AgcDriver::Graphics::Recorder& recorder, std::uint64_t serial) { @@ -198,8 +202,8 @@ public: const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); while (!stop.stop_requested() && recorder.NewestSubmitted() < serial && std::chrono::steady_clock::now() < deadline) std::this_thread::sleep_for(std::chrono::microseconds(100)); producerSubmitted = recorder.NewestSubmitted() >= serial; - releaseResult = setEvent(device.Device(), event); - if (!producerSubmitted && !stop.stop_requested()) std::fprintf(stderr, "pending producer gate: watchdog expired with producer batch %llu unsubmitted (vkSetEvent %d)\n", static_cast(serial), static_cast(releaseResult)); + Release(); + if (!producerSubmitted && !stop.stop_requested()) std::fprintf(stderr, "pending producer gate: watchdog expired with producer batch %llu unsubmitted (vkSignalSemaphore %d)\n", static_cast(serial), static_cast(releaseResult)); }); } @@ -218,14 +222,22 @@ private: return function; } + void Release() { + if (signaled.exchange(true)) return; + VkSemaphoreSignalInfo info{VK_STRUCTURE_TYPE_SEMAPHORE_SIGNAL_INFO}; + info.semaphore = semaphore; + info.value = 1; + releaseResult = signalSemaphore(device.Device(), &info); + } + AgcDriver::VulkanDevice& device; std::unique_ptr library{nullptr, &SDL_UnloadObject}; PFN_vkGetDeviceProcAddr resolve = nullptr; - PFN_vkDestroyEvent destroyEvent = nullptr; - PFN_vkSetEvent setEvent = nullptr; - PFN_vkCmdWaitEvents waitEvents = nullptr; - VkEvent event = VK_NULL_HANDLE; + PFN_vkDestroySemaphore destroySemaphore = nullptr; + PFN_vkSignalSemaphore signalSemaphore = nullptr; + VkSemaphore semaphore = VK_NULL_HANDLE; std::jthread worker; + std::atomic signaled{false}; bool producerSubmitted = false; VkResult releaseResult = VK_NOT_READY; }; @@ -243,7 +255,7 @@ void RunArgumentLabel(Slot& slot) { auto* recorder = AgcDriver::Graphics::Recorder::Active(); Require(recorder != nullptr, "the argument-label test has no active recorder"); ProducerSubmissionGate gate(*device); - gate.Record(*recorder); + gate.Arm(*recorder); const std::uint32_t labelValue = 2; const auto labelAddress = reinterpret_cast(&slot.arguments[1]); Require(device->WriteLabelOnGpu(labelAddress, std::as_bytes(std::span(&labelValue, 1)), 1, Graphics, false) == 0, "the argument label was not recorded on the GPU"); From 21fb50f1d260da513fcbcd9f829823e8264bb578 Mon Sep 17 00:00:00 2001 From: m335671 <110462702+m335671@users.noreply.github.com> Date: Mon, 19 Oct 2026 16:45:15 +0200 Subject: [PATCH 093/269] fix(agc): skip null storage images in a dispatch's write ranges DispatchWriteRanges decoded the T# of every written storage image element before checking its base address, and DecodeTextureResource throws on a null base, so a dispatch that writes a null storage image slot threw instead of contributing no range. On main the APS5_PIPELINE_DISPATCH item path reaches it. Check IsNullTextureResource first and skip the element, as DrawWriteRanges does for draws. The draw write ranges test now also runs DispatchWriteRanges on its written-plus-null image program. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PqKN8pgLXDvWRPSucJTLU7 --- .../Execution/src/Driver/Memory/WrittenBuffers.cpp | 5 +++-- .../prx/libSceAgcDriver/tests/execution/DrawWriteRanges.cpp | 5 +++++ 2 files changed, 8 insertions(+), 2 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 47e17bfdd8..8b0687ac4f 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WrittenBuffers.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WrittenBuffers.cpp @@ -67,8 +67,9 @@ std::vector> DispatchWriteRanges(const S bounded = false; continue; } - const auto resource = Graphics::DecodeTextureResource(std::span(binding.guestDescriptor).subspan(static_cast(element) * 8, 8)); - if (resource.baseAddress == 0) continue; + const auto words = std::span(binding.guestDescriptor).subspan(static_cast(element) * 8, 8); + if (Graphics::IsNullTextureResource(words)) continue; + const auto resource = Graphics::DecodeTextureResource(words); const auto surfaceBytes = Graphics::DescribeSurface(resource).guestBytes; ranges.emplace_back(resource.baseAddress, resource.baseAddress + (surfaceBytes != 0 ? surfaceBytes : UnknownImageBytes)); if (resource.dccAddress != 0) ranges.emplace_back(resource.dccAddress, resource.dccAddress + (surfaceBytes != 0 ? surfaceBytes / 256 + 1 : UnknownImageBytes)); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DrawWriteRanges.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DrawWriteRanges.cpp index d78479f1c3..d45a4cc045 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DrawWriteRanges.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DrawWriteRanges.cpp @@ -82,6 +82,11 @@ int main() { Require(Contains(ranges, written, written + surfaceBytes), "a written image next to a null one is not a write range"); Require(!Touches(ranges, 0, surfaceBytes), "a null storage image is a write range"); } + bool bounded = false; + const auto dispatchRanges = AgcDriver::DriverDetail::DispatchWriteRanges(program, bounded); + Require(bounded, "a dispatch whose written images are a real one and a null one is not bounded"); + Require(Contains(dispatchRanges, written, written + surfaceBytes), "a written image next to a null one is not a dispatch write range"); + Require(!Touches(dispatchRanges, 0, surfaceBytes), "a null storage image is a dispatch write range"); AgcDriver::Graphics::State targets{}; targets.hasColorTarget = true; auto& color = targets.color; From f59fe02b8133af98119fb7ad33f9575bbfb2c3af Mon Sep 17 00:00:00 2001 From: Whitesun123 <41925432+Whitesun123@users.noreply.github.com> Date: Mon, 19 Oct 2026 16:47:20 +0200 Subject: [PATCH 094/269] fix(libc): reduce extreme localtime_s inputs without overflow On Windows, localtime_s moves a time outside the host range into it by subtracting whole 400-year cycles, computed as cycles * CALENDAR_CYCLE_SECONDS. For times within about 7.2e9 seconds of INT64_MIN that product overflows int64, which is undefined behaviour: built with -fsanitize=undefined -fsanitize-trap=all, guest_time_conversion traps there on its INT64_MIN case. Take the remainder of the time by the cycle length instead. It is the same value for every input that did not overflow, so results do not change, and the extreme inputs still fail the year range check. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libc/src/Time.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libc/src/Time.cpp b/core/libs/prx/libc/src/Time.cpp index fca1328c65..f49335af48 100644 --- a/core/libs/prx/libc/src/Time.cpp +++ b/core/libs/prx/libc/src/Time.cpp @@ -587,7 +587,8 @@ GuestTm* APS5_VABI localtime_s_nid_postfix(const int64_t* timer, GuestTm* result const int callerError = errno; #ifdef _WIN32 const std::int64_t cycles = *timer >= 0 && *timer <= HOST_LAST_SECOND ? 0 : calendarCycles(*timer, CALENDAR_CYCLE_SECONDS); - const std::time_t t = static_cast(*timer - cycles * CALENDAR_CYCLE_SECONDS); + const std::int64_t offset = *timer % CALENDAR_CYCLE_SECONDS; + const std::time_t t = static_cast(cycles == 0 ? *timer : offset < 0 ? offset + CALENDAR_CYCLE_SECONDS : offset); if (localtime_s(&host, &t) != 0) { errno = callerError; return nullptr; From a1b6caaf0aa70286ff81a906550877b05d6d1061 Mon Sep 17 00:00:00 2001 From: pushkarreddyy <212133856+pushkarreddyy@users.noreply.github.com> Date: Mon, 19 Oct 2026 20:41:52 +0530 Subject: [PATCH 095/269] feat(libkernel): implement setgid --- core/libs/prx/libkernel/System/src/Process.cpp | 6 ++++++ core/libs/tests/GuestCredentials.cpp | 4 ++++ docs/dev/debt/libkernel.md | 2 +- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/System/src/Process.cpp b/core/libs/prx/libkernel/System/src/Process.cpp index 37239729ee..60ee97dbb6 100644 --- a/core/libs/prx/libkernel/System/src/Process.cpp +++ b/core/libs/prx/libkernel/System/src/Process.cpp @@ -718,6 +718,12 @@ int APS5_VABI setuid_nid_postfix(std::uint32_t uid) { return 0; } +int APS5_VABI setgid_nid_postfix(std::uint32_t gid) { + if (gid != 0) + throw std::runtime_error("setgid: unsupported group id " + std::to_string(gid)); + return 0; +} + int APS5_VABI setreuid_nid_postfix(std::uint32_t ruid, std::uint32_t euid) { constexpr std::uint32_t unchanged = 0xffffffffu; if (ruid != 0 && ruid != unchanged) diff --git a/core/libs/tests/GuestCredentials.cpp b/core/libs/tests/GuestCredentials.cpp index a87d68f39e..887f69c727 100644 --- a/core/libs/tests/GuestCredentials.cpp +++ b/core/libs/tests/GuestCredentials.cpp @@ -8,6 +8,7 @@ extern "C" { int APS5_VABI seteuid_nid_postfix(std::uint32_t); int APS5_VABI setegid_nid_postfix(std::uint32_t); int APS5_VABI setuid_nid_postfix(std::uint32_t); +int APS5_VABI setgid_nid_postfix(std::uint32_t); int APS5_VABI setreuid_nid_postfix(std::uint32_t, std::uint32_t); int APS5_VABI setregid_nid_postfix(std::uint32_t, std::uint32_t); int APS5_VABI setgroups_nid_postfix(int, const std::uint32_t*); @@ -62,6 +63,9 @@ int main() { Require(Throws(setegid_nid_postfix, Unchanged)); Require(setuid_nid_postfix(0) == 0); + Require(setgid_nid_postfix(0) == 0); + Require(Throws(setgid_nid_postfix, 1000)); + Require(Throws(setgid_nid_postfix, Unchanged)); Require(getuid_nid_postfix() == 0 && geteuid_nid_postfix() == 0); Require(getgid_nid_postfix() == 0 && getegid_nid_postfix() == 0 && issetugid_nid_postfix() == 0); Require(setreuid_nid_postfix(Unchanged, Unchanged) == 0); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 6ccdf90aa5..29fc8793ce 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -74,7 +74,7 @@ - [select](../../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) - a descriptor the socket poller reports as unknown that is below `GuestSockets::FirstDescriptor` is polled on the host instead of throwing (on Windows only a pipe, as above); an invalid host descriptor fails with `EBADF`, and mixed host and socket sets are polled in short alternating slices so neither side waits out the full slice alone. A descriptor that stops being a socket, a kqueue or a pipe during the wait fails the call with `EBADF` when it is closed and is selected again with the rest of the timeout when its number was reused, as FreeBSD 11.4's [`kern_select`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/sys_generic.c#L1129-L1140) looks every descriptor up again in each pass. A close that has already removed the descriptor from the guest's kqueue, pipe or file tables but not yet closed its host descriptor is seen by that new look-up as a descriptor of unknown kind, which throws on Windows and is polled through the host on Linux. Wobbly Life (PPSA29413) selects on file descriptors while streaming its bundles - [__progname](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - unknown data export - [environ](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - data export pointing to a fixed empty environment so titles that import it load; it is not the environment libc's `getenv` reads, so it lists none of those variables and `setenv`/`putenv` changes do not appear in it -- [seteuid, setegid, setuid, setreuid, setregid, setgroups, getgroups](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - the guest process is assumed to run as root, with real, effective and saved user and group ids 0. As in FreeBSD's `sys_seteuid`, `sys_setegid`, `sys_setuid`, `sys_setreuid` and `sys_setregid`, id 0 (or -1 for an unchanged `setreuid`/`setregid` id) is then accepted without a privilege check and changes no credential, so 0 is returned; any other id would change an id, which is not modelled, and throws. `setgroups` keeps the supplementary group list as `kern_setgroups` does (`NGROUPS_MAX` 1023, an empty list leaves the effective group as the only entry, a first entry other than 0 would change the effective group and throws) and `getgroups` reports it. The ids and privileges a title has on a console are unverified +- [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 From 179316c45258907f1cc3763c994f476057de202f Mon Sep 17 00:00:00 2001 From: Lindamea1 <322051987+Lindamea1@users.noreply.github.com> Date: Mon, 19 Oct 2026 13:52:56 +0100 Subject: [PATCH 096/269] fix(libkernel): return ENOMEM from sceKernelAvailableDirectMemorySize when the range has no free block A search range with no free direct memory returned EAGAIN (0x80020023). A PS5 returns ENOMEM (0x8002000c) for an occupied range, as the PS5_RetroArch author's console capture records, and PS5_PayloadSDK's ps5_memory_query, which RetroArch uses for its memory status, gives up on any other error. LargestFree now returns the error: ENOMEM when no block is found, and EAGAIN as before for a negative start, an end at or below the start and an alignment that overflows when rounded to a page. guest_memory expects ENOMEM for an occupied range and EAGAIN for those arguments. The unmeasured argument handling is recorded in the libkernel debt file. Co-Authored-By: Claude Opus 5.5 --- .../prx/libkernel/DirectMemory/DirectMemory.hpp | 2 +- core/libs/prx/libkernel/DirectMemory/Export.cpp | 3 ++- .../prx/libkernel/DirectMemory/MemoryPool.cpp | 16 ++++++++-------- core/libs/tests/GuestMemory.cpp | 5 ++++- docs/dev/debt/libkernel.md | 1 + 5 files changed, 16 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp index 2e0f21cf18..480a5841d3 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp @@ -32,7 +32,7 @@ std::vector DirectMemoryViews(int64_t start, size_t len); void UnmapDirectMemoryViews(const std::vector& views); bool DirectMemoryFind(int64_t offset, bool findNext, int64_t* start, int64_t* end, int* memoryType); void DirectMemoryRetype(int64_t start, size_t len, int memoryType); -bool DirectMemoryLargestFree(int64_t searchStart, int64_t searchEnd, size_t alignment, int64_t* startOut, size_t* sizeOut); +int DirectMemoryLargestFree(int64_t searchStart, int64_t searchEnd, size_t alignment, int64_t* startOut, size_t* sizeOut); size_t DirectMemoryFreeRun(uint64_t offset, uint64_t limit); int DoMapDirect(void** addr, size_t len, int prot, int flags, int64_t physStart, size_t alignment); int DoMapAnon(void** addr, size_t len, int prot, int flags, size_t alignment = PS5_PAGE_SIZE, bool exclusive = false, bool allowStackReplacement = false); diff --git a/core/libs/prx/libkernel/DirectMemory/Export.cpp b/core/libs/prx/libkernel/DirectMemory/Export.cpp index d8e6adeb42..ca1789ca0d 100644 --- a/core/libs/prx/libkernel/DirectMemory/Export.cpp +++ b/core/libs/prx/libkernel/DirectMemory/Export.cpp @@ -211,7 +211,8 @@ int APS5_VABI sceKernelAvailableDirectMemorySize(int64_t search_start, int64_t s if (!phys_addr_out || !size_out) return SCE_KERNEL_ERROR_EINVAL; int64_t start = 0; size_t size = 0; - if (!DirectMemoryLargestFree(search_start, search_end, alignment, &start, &size)) { *phys_addr_out = 0; *size_out = 0; return SCE_KERNEL_ERROR_EAGAIN; } + const int result = DirectMemoryLargestFree(search_start, search_end, alignment, &start, &size); + if (result != 0) { *phys_addr_out = 0; *size_out = 0; return result; } *phys_addr_out = start; *size_out = size; return 0; diff --git a/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp b/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp index fc082fb8e9..542ce5310b 100644 --- a/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp +++ b/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp @@ -118,14 +118,14 @@ struct PhysicalMemoryPool { return true; } - bool LargestFree(int64_t searchStart, int64_t searchEnd, size_t alignment, int64_t* startOut, size_t* sizeOut) { - if (!startOut || !sizeOut) return false; - if (searchStart < 0 || searchEnd <= searchStart) return false; + int LargestFree(int64_t searchStart, int64_t searchEnd, size_t alignment, int64_t* startOut, size_t* sizeOut) { + if (!startOut || !sizeOut) return SCE_KERNEL_ERROR_EINVAL; + if (searchStart < 0 || searchEnd <= searchStart) return SCE_KERNEL_ERROR_EAGAIN; uint64_t align; - if (!NormalizeAlignment(alignment, align)) return false; + if (!NormalizeAlignment(alignment, align)) return SCE_KERNEL_ERROR_EAGAIN; uint64_t cur; - if (!RoundUp(static_cast(searchStart), kPage, cur)) return false; + if (!RoundUp(static_cast(searchStart), kPage, cur)) return SCE_KERNEL_ERROR_EAGAIN; const uint64_t limit = std::min(static_cast(searchEnd), kPoolBytes) & ~(kPage - 1); std::lock_guard lock(_mutex); @@ -149,10 +149,10 @@ struct PhysicalMemoryPool { cur = runEnd; } - if (bestSize == 0) return false; + if (bestSize == 0) return SCE_KERNEL_ERROR_ENOMEM; *startOut = static_cast(bestStart); *sizeOut = static_cast(bestSize); - return true; + return 0; } size_t FreeRun(uint64_t offset, uint64_t limit) { @@ -286,7 +286,7 @@ void DirectMemoryRetype(int64_t start, size_t len, int memoryType) { (void)PhysicalMemoryPool::Instance().Retype(static_cast(start), len, memoryType); } -bool DirectMemoryLargestFree(int64_t searchStart, int64_t searchEnd, size_t alignment, int64_t* startOut, size_t* sizeOut) { +int DirectMemoryLargestFree(int64_t searchStart, int64_t searchEnd, size_t alignment, int64_t* startOut, size_t* sizeOut) { return PhysicalMemoryPool::Instance().LargestFree(searchStart, searchEnd, alignment, startOut, sizeOut); } diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index e9a8d30f0e..d5de7d69ca 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -2778,8 +2778,11 @@ static void CheckAvailableDirectMemoryIsLargestFreeBlock() { Require(start == top + 0x200000 && size == 0x400000); Require(sceKernelAvailableDirectMemorySize(top, top + window, 0x1000000, &start, &size) == 0); Require(start == top + 0x1000000 && size == static_cast(window - 0x1000000)); - Require(sceKernelAvailableDirectMemorySize(top, top + 0x200000, 0, &start, &size) == SCE_KERNEL_ERROR_EAGAIN); + Require(sceKernelAvailableDirectMemorySize(top, top + 0x200000, 0, &start, &size) == SCE_KERNEL_ERROR_ENOMEM); Require(start == 0 && size == 0); + Require(sceKernelAvailableDirectMemorySize(top + window, top, 0, &start, &size) == SCE_KERNEL_ERROR_EAGAIN); + Require(sceKernelAvailableDirectMemorySize(-1, top + window, 0, &start, &size) == SCE_KERNEL_ERROR_EAGAIN); + Require(sceKernelAvailableDirectMemorySize(top, top + window, std::numeric_limits::max(), &start, &size) == SCE_KERNEL_ERROR_EAGAIN); Require(sceKernelAvailableDirectMemorySize(top, top + window, 0, nullptr, &size) == SCE_KERNEL_ERROR_EINVAL); Require(sceKernelReleaseDirectMemory(first, 0x200000) == 0); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 6ccdf90aa5..91e290223f 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -49,6 +49,7 @@ - [sceKernelGetLibkernelTextLocation](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the `(void** address, std::size_t* size)` signature and the choice of the main program's first segment follow [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/kernel/ps4_libkernel.pas#L338-L348); whether the console reports that segment, the text of libkernel itself or the calling module's is unverified. The segment is the first one `sceKernelGetModuleInfo` reports for the main program, and 0 is returned. A null argument throws. - [sceKernelReleaseFlexibleMemory](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - unmaps the range like sceKernelMunmap, as fpPS4 and prosper do; whether the console keeps the virtual range reserved after releasing its pages is unverified - [sceKernelAvailableFlexibleMemorySize](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - the segments of the title's executable and of its `.guest.prx` modules count against the configured flexible-memory budget (448 MiB by default) while loaded, for this size and for flexible maps. A title may declare a larger budget through `kernel.flexibleMemorySize` in `param.json`; this does not reduce the 13.5 GiB direct-memory size. Rejection of zero or a value not aligned to 16 KiB has not been measured on a console. This segment-accounting model follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/module.cpp#L165), which maps every segment of a module outside its system module directory as flexible memory on the PS4. [Counted](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) are the segments the title's ELF gives permissions, each rounded to 16 KiB pages; not counted are segments without permissions, the relinker's own blocks (on Linux both are the read/write/execute `PT_LOAD`s, plus the header block at file offset 0; on Windows everything outside the `.elf` sections) and the host system libraries. The Linux image carries no marker for the relinker's blocks, so they are told apart by those flags and that offset only: a title segment that is writable and executable, or starts at file offset 0, is not counted. A module whose segments exceed what is left still loads, and the available size then reads 0; shadPS4 fails the segment map instead ([memory.cpp](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/memory.cpp#L519-L524) returns `EINVAL`, [module.cpp](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/module.cpp#L168-L169) asserts). Which segments the PS5 charges, its rounding, whether system modules use the title's budget and when an unload releases the charge are not measured. PPSA15508 subtracts the reported size and a 10 MiB module allowance from its own 448 MiB budget at start-up and stops when the result exceeds its 146 MiB code and data limit ([#2883](https://github.com/boykopovar/AnyPS5/issues/2883)) +- [sceKernelAvailableDirectMemorySize](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - how the console handles invalid arguments is not measured. A negative `search_start`, a `search_end` at or below `search_start` and an alignment that overflows when rounded up to 16 KiB return `EAGAIN` (`0x80020023`); bounds that are not multiples of 16 KiB are rounded inward, the alignment is raised to a multiple of 16 KiB and need not be a power of two, and a `search_end` past the pool is cut to it. fpPS4's HLE wrapper on its trunk branch refuses all of these with `EINVAL` ([ps4_map_mm.pas](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/kernel/ps4_map_mm.pas#L558-L570)) - [Memory protection bits 0x100 and 0x200](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - taken as audio coprocessor (ACP) read and write, as in the [KytyPS5](https://github.com/KytyPS5/KytyPS5) and [etaHEN](https://github.com/etaHEN/etaHEN) headers; the coprocessor work runs on the host CPU, so they grant host read and read/write access like the GPU bits - [Protecting a host thread's stack](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - on Windows `sceKernelIsStack` reports a host thread's whole stack reservation, which Windows commits on demand from the top down behind a guard page. A protect over that range changes only the pages above the guard; the guard page and the reserved pages below it are left as they are, so the stack still grows and an overflow still raises a stack overflow. A protect that starts at the bottom of the reservation and stops short of the guard page, as HotSpot makes its stack guard zones, is committed and protected. Windows moves the guard page down by committing the pages below it as read/write guard pages, whether they were reserved or committed, so those pages come in read/write as the stack grows to within one guard region of them (three pages on GitHub's runners). A protection other than read/write on them, `PROT_NONE` included, does not hold once the stack is that close, where on the console every page of the reported range takes the protection. Holding it would mean removing the thread's guard page and keeping the stack limit in its TEB by hand. On Linux the reported range of the main thread is mostly unmapped; the unmapped pages from the bottom of a protect up to the top of the stack are mapped read/write in place, then the protect is applied, so the stack never has to grow down to the protected pages, which Linux refuses within its stack guard gap (1 MiB by default) above a readable mapping. The pages below a protect stay unmapped and out of the stack's reach, where on the console the stack goes on below them - [Foreign guest module image protection on Windows](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - `sceKernelMprotect` on Windows allows protection mutations on mapped PE images containing a `.elf0` section (as relinked guest modules produce, such as NVIDIA Real-time Denoisers `libNRD.prx` bundled in Dying Light 2: Stay Human PPSA02262), registering their sections into `GuestAllocations`. Host libraries and system DLLs (such as `libkernel.prx` and `kernel32.dll`) without a `.elf0` section continue to be rejected From 3237083172e4d351b38436b69658c9fd6c0efcb0 Mon Sep 17 00:00:00 2001 From: Lindamea1 <322051987+Lindamea1@users.noreply.github.com> Date: Mon, 19 Oct 2026 13:54:47 +0100 Subject: [PATCH 097/269] fix(libkernel): return ESRCH from scePthreadAttrGet for a thread that has ended scePthreadAttrGet copied the attributes of any non-null handle, so a thread that was joined or had finished still answered 0, and a thread that ended detached was read after its object had been freed. FreeBSD 11.4's _pthread_attr_get_np returns ESRCH for a dead or reclaimed thread, and Monster Hunter Wilds (PPSA24191) reuses a slot of its thread table only when scePthreadAttrGet returns 0x80020003. After the EINVAL checks it now returns ESRCH unless the handle is a live guest thread, and copies the fields while holding the thread registry lock (LockLiveGuestThread), so a detached thread exiting during the call is not freed under the copy. GuestThreadIsLive uses the same lookup. pthread_attr_get_np, which the title does not import, is left as it is; the debt entry records the difference. Co-Authored-By: Claude Opus 5.5 --- .../prx/libkernel/Pthread/include/Pthread.hpp | 1 + core/libs/prx/libkernel/Pthread/src/Attr.cpp | 2 + .../libs/prx/libkernel/Pthread/src/Thread.cpp | 13 +-- core/libs/tests/GuestPthreadAttr.cpp | 84 +++++++++++++++++++ docs/dev/debt/libkernel.md | 1 + 5 files changed, 96 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libkernel/Pthread/include/Pthread.hpp b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp index 5846eb3464..03024d9d7f 100644 --- a/core/libs/prx/libkernel/Pthread/include/Pthread.hpp +++ b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp @@ -169,6 +169,7 @@ bool GuestThreadCpuNanos(int cpuClockThread, std::uint64_t* nanos); void SetLiveGuestThreadsAffinity(KernelCpumask mask); KernelCpumask LiveGuestThreadsAffinity(); bool GuestThreadIsLive(const PthreadPrivate* thread); +std::unique_lock 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); diff --git a/core/libs/prx/libkernel/Pthread/src/Attr.cpp b/core/libs/prx/libkernel/Pthread/src/Attr.cpp index 4102a9e69e..ab3b5a490c 100644 --- a/core/libs/prx/libkernel/Pthread/src/Attr.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Attr.cpp @@ -77,6 +77,8 @@ int APS5_VABI scePthreadAttrGetstack(const PthreadAttr* attr, void** stackaddr, int APS5_VABI scePthreadAttrGet(Pthread thread, PthreadAttr* attr) { if (!thread || !attr || !*attr) return SCE_KERNEL_ERROR_EINVAL; + const auto live = LockLiveGuestThread(thread); + if (!live) return SCE_KERNEL_ERROR_ESRCH; (*attr)->_stacksize = thread->stackSize; (*attr)->stackAddress = thread->stackAddress; (*attr)->_detachstate = thread->_detached ? DETACH_DETACHED : DETACH_JOINABLE; diff --git a/core/libs/prx/libkernel/Pthread/src/Thread.cpp b/core/libs/prx/libkernel/Pthread/src/Thread.cpp index 0afb764b67..bd61a61fc9 100644 --- a/core/libs/prx/libkernel/Pthread/src/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Thread.cpp @@ -439,14 +439,17 @@ KernelCpumask LiveGuestThreadsAffinity() { return mask; } -bool GuestThreadIsLive(const PthreadPrivate* thread) { +std::unique_lock LockLiveGuestThread(const PthreadPrivate* thread) { auto& registry = CpuClocks(); - std::lock_guard lock(registry.lock); + std::unique_lock lock(registry.lock); for (const auto& [id, live] : registry.threads) { - if (live != thread) continue; - return !live->_finished.load(std::memory_order_acquire); + if (live == thread && !live->_finished.load(std::memory_order_acquire)) return lock; } - return false; + return {}; +} + +bool GuestThreadIsLive(const PthreadPrivate* thread) { + return LockLiveGuestThread(thread).owns_lock(); } bool GetLiveGuestThreadAffinity_nid_no_patch(std::int64_t tid, KernelCpumask& mask) { diff --git a/core/libs/tests/GuestPthreadAttr.cpp b/core/libs/tests/GuestPthreadAttr.cpp index 0b28513f9d..5afa252aa8 100644 --- a/core/libs/tests/GuestPthreadAttr.cpp +++ b/core/libs/tests/GuestPthreadAttr.cpp @@ -1,7 +1,9 @@ #include "SceTypes.hpp" +#include #include #include #include +#include extern "C" { int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); @@ -30,6 +32,11 @@ int APS5_VABI pthread_attr_get_np_nid_postfix(Pthread thread, PthreadAttr* attr) int APS5_VABI pthread_attr_setschedparam_nid_postfix(PthreadAttr* attr, const KernelSchedParam* param); int APS5_VABI pthread_attr_setschedpolicy_nid_postfix(PthreadAttr* attr, int policy); int APS5_VABI pthread_attr_getschedpolicy_nid_postfix(const PthreadAttr* attr, int* policy); +int APS5_VABI scePthreadAttrSetdetachstate(PthreadAttr* attr, int state); +int APS5_VABI scePthreadKeyCreate(PthreadKey* key, pthread_key_destructor_func_t destructor); +int APS5_VABI scePthreadKeyDelete(PthreadKey key); +int APS5_VABI scePthreadSetspecific(PthreadKey key, void* value); +Pthread APS5_VABI scePthreadSelf(); } static constexpr int SCE_OK = 0; @@ -47,6 +54,9 @@ static constexpr std::size_t STACK_SIZE = 2u << 20; static constexpr std::size_t MIN_STACK_SIZE = 16384; static constexpr int GUEST_EINVAL = 22; static constexpr int GUEST_ENOTSUP = 45; +static constexpr int SCE_KERNEL_ERROR_ESRCH = static_cast(0x80020003); +static constexpr int DETACH_DETACHED = 1; +static constexpr int DETACHED_THREADS = 32; static void Require(bool value) { if (!value) std::abort(); } @@ -256,12 +266,86 @@ static void CheckThreadScope() { Require(scePthreadJoin(inheritedThread, nullptr) == SCE_OK); } +static int QueryResult(Pthread thread) { + PthreadAttr attr = nullptr; + Require(scePthreadAttrInit(&attr) == SCE_OK); + Require(scePthreadAttrSetstacksize(&attr, STACK_SIZE) == SCE_OK); + const int result = scePthreadAttrGet(thread, &attr); + std::size_t stackSize = 0; + Require(scePthreadAttrGetstacksize(&attr, &stackSize) == SCE_OK); + Require(result == SCE_OK || stackSize == STACK_SIZE); + Require(scePthreadAttrDestroy(&attr) == SCE_OK); + return result; +} + +static bool ReportedEnded(Pthread thread) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + while (QueryResult(thread) == SCE_OK) { + if (std::chrono::steady_clock::now() > deadline) return false; + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + return QueryResult(thread) == SCE_KERNEL_ERROR_ESRCH; +} + +struct ExitQuery { + PthreadKey key = -1; + int result = -1; +}; + +static void APS5_VABI QuerySelfAtExit(void* value) { + auto* query = static_cast(value); + query->result = QueryResult(scePthreadSelf()); +} + +static void* APS5_VABI SetExitQuery(void* arg) { + auto* query = static_cast(arg); + Require(scePthreadSetspecific(query->key, query) == SCE_OK); + return nullptr; +} + +static void* APS5_VABI Finish(void*) { + return nullptr; +} + +static void CheckEndedThreads() { + Require(QueryResult(scePthreadSelf()) == SCE_OK); + + Pthread joined = nullptr; + Require(scePthreadCreate(&joined, nullptr, Finish, nullptr, nullptr) == SCE_OK); + Require(scePthreadJoin(joined, nullptr) == SCE_OK); + Require(QueryResult(joined) == SCE_KERNEL_ERROR_ESRCH); + PthreadAttr unset = nullptr; + Require(scePthreadAttrGet(joined, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrGet(joined, &unset) == SCE_KERNEL_ERROR_EINVAL); + + ExitQuery exitQuery; + Require(scePthreadKeyCreate(&exitQuery.key, reinterpret_cast(QuerySelfAtExit)) == SCE_OK); + Pthread finished = nullptr; + Require(scePthreadCreate(&finished, nullptr, SetExitQuery, &exitQuery, nullptr) == SCE_OK); + Require(ReportedEnded(finished)); + Require(scePthreadJoin(finished, nullptr) == SCE_OK); + Require(exitQuery.result == SCE_OK); + Require(QueryResult(finished) == SCE_KERNEL_ERROR_ESRCH); + Require(scePthreadKeyDelete(exitQuery.key) == SCE_OK); + + PthreadAttr detachedAttr = nullptr; + Require(scePthreadAttrInit(&detachedAttr) == SCE_OK); + Require(scePthreadAttrSetdetachstate(&detachedAttr, DETACH_DETACHED) == SCE_OK); + for (int index = 0; index < DETACHED_THREADS; ++index) { + Pthread detached = nullptr; + Require(scePthreadCreate(&detached, &detachedAttr, Finish, nullptr, nullptr) == SCE_OK); + Require(ReportedEnded(detached)); + } + Require(scePthreadAttrDestroy(&detachedAttr) == SCE_OK); +} + int main() { CheckStackSizeLimit(); CheckSoloSched(); CheckSchedArguments(); CheckSchedulingAttributes(); CheckThreadScope(); + CheckEndedThreads(); PthreadAttr attr = nullptr; Require(scePthreadAttrInit(&attr) == SCE_OK); Require(scePthreadAttrSetinheritsched(&attr, EXPLICIT_SCHED) == SCE_OK); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 6ccdf90aa5..a663cc060f 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -140,6 +140,7 @@ - [getcontext and setcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) and [makecontext](../../../core/libs/prx/libSceLibcInternal/UserContext.cpp) (libSceLibcInternal) use FreeBSD 11's amd64 `ucontext_t` (`mcontext` at 0x10, `mc_len` 800, `uc_link` at 0x330, `uc_stack` at 0x338, 0x370 bytes), the layout homebrew built against FreeBSD's headers passes: PPSA99039's OpenSSL `ASYNC` (`crypto/async/arch/async_posix.c`) captures into a 0x370-byte stack buffer. `getcontext` writes the 0x330 bytes before `uc_link`, as FreeBSD's `UC_COPY_SIZE`. The console's own layout for these functions is not verified; the exception handler's context is a different one (see `sceKernelRaiseException`) - [getcontext and setcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) keep the 512-byte FXSAVE image in `mc_fpstate` (x87, MXCSR, XMM0-15), so the upper halves of YMM0-15 are neither saved nor restored, and `getcontext` leaves `mc_xfpustate` and `mc_xfpustate_len` 0. `getcontext` sets `mc_fpformat` and `mc_ownedfp` to FreeBSD's `_MC_FPFMT_XMM` and `_MC_FPOWNED_FPU`, `mc_cs` and `mc_ss` to the host selectors, and `mc_flags`, `mc_fsbase` and `mc_gsbase` to 0. `setcontext` loads `mc_fpstate` whatever `mc_fpformat` and `mc_ownedfp` say, and restores neither the segment selectors, `mc_fsbase` and `mc_gsbase` nor the extended state, which FreeBSD restores when `mc_flags` asks for them - [setcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) and [longjmp](../../../core/libs/prx/libc/src/JumpBuffer.cpp) (libc) on Windows set the thread's stack bounds in the TEB to the stack they switch to: a `makecontext` stack, or else the committed region around the target stack pointer. A switch to any other guest stack, such as a context built by hand, gets the bounds of that region only +- [scePthreadAttrGet](../../../core/libs/prx/libkernel/Pthread/src/Attr.cpp) (libkernel) - the answer for a thread that has ended is not measured on the console, and [`pthread_attr_get_np`](../../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) still returns 0 for such a thread, reading a detached one's object after it is freed. `scePthreadAttrGet` returns `ESRCH` (`0x80020003`) for a thread that has ended, joined or not and detached or not, as FreeBSD 11.4's [`_pthread_attr_get_np`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_attr.c#L139-L172) does through `_thr_find_thread`; a thread counts as ended once its thread-specific data destructors have run, where FreeBSD marks it `PS_DEAD`. On Windows a host thread adopted by `scePthreadSelf` that ends without `scePthreadExit` keeps answering 0. Monster Hunter Wilds (PPSA24191) reuses a slot of its thread table only when `scePthreadAttrGet` returns `0x80020003` - [scePthreadAttrGet, pthread_attr_get_np and sceKernelIsStack](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) report the stack of a host thread the library did not create, on Windows, from one allocation granule (64 KiB) above the bottom of its reservation when everything above that is committed read-write, as in a relinked executable's main thread and its default-size host threads (0x200000 committed of 0x210000 reserved), so the guest can protect the lowest reported pages, as HotSpot does for its stack guard pages in Java 25 LAN / DNS (PPSA99869). Any other such thread, and every such thread on Linux, gets its whole host stack: on Windows `mprotect` commits its reserved lower pages before protecting them and drops the guard flag of Windows' guard pages, and on Linux it cannot change the unmapped lower pages - [sceKernelInstallExceptionHandler](../../../core/libs/prx/libkernel/System/src/Exception.cpp) records the handler, but a host fault still ends the process: only signals raised with `sceKernelRaiseException` are delivered. Unity's PS5Util module installs a SIGUSR1 handler that reads `uc_mcontext.mc_rsp` (offset 0xf8 of the context) and raises SIGUSR1 on other threads to stop them for garbage collection - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) on Linux sends the host SIGUSR1, which the library takes over, to the target thread, so the handler runs wherever the thread was interrupted, emulator code included. On the console kernel objects are system calls and libthr defers signals while a thread holds its internal locks; here a handler that needs a host lock the interrupted code holds deadlocks, and a thread that the handler keeps waiting keeps its host locks. Host waits resume after the handler. The host calls Linux does not restart end early: sleeps that do not retry return before their time, and the poll of `sceNetEpollWait` and socket calls with a send or receive timeout return `EINTR` to the guest From baa6d4aeee81536bb59ecad24a292eacfc2d9350 Mon Sep 17 00:00:00 2001 From: Hussein Kadhim Date: Wed, 14 Oct 2026 02:36:08 +0300 Subject: [PATCH 098/269] fix(libkernel): fail a malformed ioctl request with ENOTTY before the descriptor is looked at FreeBSD's sys_ioctl rejects a request with no direction bit, an output request with no length, or a void request whose length is not an int before it reads the argument or resolves the descriptor. ioctl threw on such a request for a file, pipe or directory, failed with EBADF for a closed descriptor and with EFAULT for a socket given no argument. (cherry picked from commit 501518f321ac5d0b45ce1090af39c680927edf49) (cherry picked from commit 3eb7edd9cf4a2cb1e7cf39e8e45aabb7c5d70fb4) --- .../libkernel/File/include/DescriptorControl.hpp | 1 + .../prx/libkernel/File/src/DescriptorControl.cpp | 11 +++++++++++ core/libs/prx/libkernel/Socket/src/Udp.cpp | 1 + core/libs/tests/GuestKernelFcntl.cpp | 14 ++++++++++++++ docs/dev/debt/libkernel.md | 2 +- 5 files changed, 28 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/File/include/DescriptorControl.hpp b/core/libs/prx/libkernel/File/include/DescriptorControl.hpp index 81a9018c42..341324638c 100644 --- a/core/libs/prx/libkernel/File/include/DescriptorControl.hpp +++ b/core/libs/prx/libkernel/File/include/DescriptorControl.hpp @@ -9,6 +9,7 @@ enum class FcntlArgument { None, Int, Pointer }; FcntlArgument FcntlArgumentOf(int command); int Fcntl(int fd, int command, std::intptr_t argument); +bool IoctlRequestMalformed(std::uint64_t request); int Ioctl(int fd, std::uint64_t request, void* argument); int DuplicateFrom(int fd, int start); void RememberDescriptor(int fd, int openFlags); diff --git a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp index 82db339af5..86857fca4c 100644 --- a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp +++ b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp @@ -93,6 +93,8 @@ constexpr std::uint32_t KeepOnExecRequest = 0x20006602; constexpr std::uint32_t NonblockingRequest = 0x8004667e; constexpr std::uint32_t ReadableRequest = 0x4004667f; constexpr std::uint32_t RequestIn = 0x80000000u; +constexpr std::uint32_t RequestOut = 0x40000000u; +constexpr std::uint32_t RequestVoid = 0x20000000u; constexpr std::uint32_t TerminalGroup = 't'; struct GuestLock { @@ -508,7 +510,16 @@ int Fcntl(int fd, int command, std::intptr_t argument) { } } +bool IoctlRequestMalformed(std::uint64_t request) { + const auto command = static_cast(request); + const std::uint32_t length = (command >> 16) & 0x1fff; + if ((command & (RequestIn | RequestOut | RequestVoid)) == 0) return true; + if ((command & RequestOut) && length == 0) return true; + return (command & RequestVoid) && length != 0 && length != sizeof(int); +} + int Ioctl(int fd, std::uint64_t request, void* argument) { + if (IoctlRequestMalformed(request)) return Fail(GuestEnotty); const auto command = static_cast(request); const std::uint32_t length = (command >> 16) & 0x1fff; if ((command & RequestIn) && length != 0 && argument == nullptr) return Fail(GuestEfault); diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index b4c71263e0..493d73d433 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -1205,6 +1205,7 @@ int APS5_VABI getpeername_nid_postfix(int descriptor, void* address, std::uint32 } int APS5_VABI ioctl_nid_postfix(int descriptor, std::uint64_t request, void* argument) { if (!GuestSockets::IsOpen(descriptor)) return File::Ioctl(descriptor, request, argument); + if (File::IoctlRequestMalformed(request)) return Fail(25); const auto socket = Lookup(descriptor); if (!socket) return -1; if (!argument) return Fail(14); diff --git a/core/libs/tests/GuestKernelFcntl.cpp b/core/libs/tests/GuestKernelFcntl.cpp index 6d555d64eb..62beb83814 100644 --- a/core/libs/tests/GuestKernelFcntl.cpp +++ b/core/libs/tests/GuestKernelFcntl.cpp @@ -67,6 +67,7 @@ static constexpr std::uint64_t WindowSizeRequest = 0x40087468; static constexpr std::uint64_t AsyncRequest = 0x8004667d; static constexpr std::uint64_t CloseOnExecRequest = 0x20006601; static constexpr std::uint64_t KeepOnExecRequest = 0x20006602; +static constexpr std::uint64_t MalformedRequests[] = {0, 123, 0x5401, 0x40007468, 0x20087468, 0x00087468}; static constexpr int SceEbadf = static_cast(0x80020009u); static constexpr int SceEinval = static_cast(0x80020016u); static constexpr int SceEnotsup = static_cast(0x8002002du); @@ -94,6 +95,14 @@ template static bool ThrowsWith(TCall call, const std::string& return false; } +static void RequireMalformedRequestsFail(int descriptor) { + char buffer[16] = {}; + for (const std::uint64_t request : MalformedRequests) { + RequireFailure(ioctl_nid_postfix(descriptor, request, buffer), GuestEnotty); + RequireFailure(ioctl_nid_postfix(descriptor, request, nullptr), GuestEnotty); + } +} + static void SocketsKeepTheirPath() { const int socket = socket_nid_postfix(2, 2, 0); Require(socket >= 0); @@ -108,6 +117,7 @@ static void SocketsKeepTheirPath() { Require(sceKernelFcntl(socket + 4096, GetFlags) == SceEbadf); int queued = -1; Require(ioctl_nid_postfix(socket, ReadableRequest, &queued) == 0 && queued == 0); + RequireMalformedRequestsFail(socket); Require(close_nid_postfix(socket) == 0); Require(sceKernelFcntl(socket, GetFlags) == SceEbadf); Require(sceKernelFcntl(-1, GetFlags) == SceEbadf); @@ -159,6 +169,7 @@ static void FileFlagsRoundTrip() { Require(ioctl_nid_postfix(file, ReadableRequest, &readable) == 0 && readable == 3); char size[8] = {}; RequireFailure(ioctl_nid_postfix(file, WindowSizeRequest, size), GuestEnotty); + RequireMalformedRequestsFail(file); RequireFailure(ioctl_nid_postfix(file, NonblockingRequest, nullptr), GuestEfault); Require(ThrowsWith([file] { int on = 1; ioctl_nid_postfix(file, AsyncRequest, &on); }, "ioctl: request 0x8004667D on a file is not implemented")); @@ -183,6 +194,8 @@ static void FileFlagsRoundTrip() { RequireFailure(fcntl_nid_postfix(file, GetDescriptorFlags), GuestEbadf); RequireFailure(fcntl_nid_postfix(file, DuplicateCommand, 0), GuestEbadf); RequireFailure(ioctl_nid_postfix(file, ReadableRequest, &readable), GuestEbadf); + RequireMalformedRequestsFail(file); + RequireMalformedRequestsFail(-1); Require(sceKernelFcntl(file, GetFlags) == SceEbadf); const int appending = sceKernelOpen("kernel_fcntl_probe.bin", CreateWriteAppend, 0644); @@ -381,6 +394,7 @@ static void PipeFlagsRoundTrip() { RequireFailure(fcntl_nid_postfix(pipe[1], SetLock, &lock), GuestEbadf); char size[8] = {}; RequireFailure(ioctl_nid_postfix(pipe[0], WindowSizeRequest, size), GuestEnotty); + RequireMalformedRequestsFail(pipe[0]); Require(ThrowsWith([&pipe] { fcntl_nid_postfix(pipe[0], SetFlags, NonBlocking | 0x40); }, "fcntl: O_ASYNC on a pipe is not implemented")); for (const int descriptor : pipe) Require(close_nid_postfix(descriptor) == 0); } diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 6ccdf90aa5..095c6a3cfe 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -196,7 +196,7 @@ - [sceKernelFcntl](../../../core/libs/prx/libkernel/File/src/Open.cpp) (libkernel) runs the POSIX `fcntl` below and returns its guest errno as `SCE_KERNEL_ERROR_*`, the form the other `sceKernel` file exports use. The return values and error codes are unverified on a console - [fcntl](../../../core/libs/prx/libkernel/File/src/DescriptorControl.cpp) (libkernel) follows FreeBSD 11.4's [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L479-L799) and [`kern_dup`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L812) on file, directory, pipe and character device descriptors; a socket keeps the libSceNet path (`F_GETFL`, `F_SETFL` with `O_NONBLOCK`, `F_GETFD` and `F_SETFD`, `EINVAL` for every other command, see the next entry). The close-on-exec flag and the status flags live in a guest table, the status flags shared by `dup`, `dup2` and `F_DUPFD` as in an open file description; a descriptor the table has not seen, such as 0 to 2, starts from the host's flags, since the console's are not known. `F_GETFL` takes the access mode from the host (the granted access of the handle on Windows), so pipe ends report `O_RDONLY` and `O_WRONLY` where FreeBSD's bidirectional pipes report `O_RDWR`. `F_DUPFD` and `F_DUPFD_CLOEXEC` return the lowest free host descriptor at or above the start, so the bound is the host table (`EMFILE` on Windows, `EINVAL` past `RLIMIT_NOFILE` on Linux), not the console's unknown `RLIMIT_NOFILE` and `kern.maxfilesperproc`. `O_NONBLOCK` switches a host pipe (`PIPE_NOWAIT` on Windows, where an empty read and a full write then report `EAGAIN`) and is only recorded on files, directories and the random device, as their reads never block; on a Windows console or other character device it throws. Changing `O_APPEND` on a Windows file, changing `O_FSYNC` on a file, `O_ASYNC` on a pipe or character device (`SIGIO` is not delivered), `F_GETOWN`, `F_SETOWN` and `F_SETLK_REMOTE` throw; `O_DIRECT`, `F_RDAHEAD` and `F_READAHEAD` are recorded as the hints they are. Record locks (`F_GETLK`, `F_SETLK`, `F_SETLKW` and their forms without `l_sysid`) use the host's POSIX locks on Linux and macOS. `F_GETLK` with `F_UNLCK` asks the host with `F_RDLCK`: as in [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) only another owner's overlapping write lock conflicts with either request. On Windows they follow [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L607-L752) and [`lf_advlockasync`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L433-L487) for the one guest process, which owns every lock: `EOVERFLOW` for an `SEEK_CUR` start past `OFF_MAX`, then `EBADF` for `F_RDLCK` without read access or `F_WRLCK` without write access and `EINVAL` for another type, then `EINVAL` and `EOVERFLOW` for the range; as [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) never lets locks of one owner conflict, whichever descriptor or thread asks, `F_SETLK` and `F_SETLKW` then succeed without waiting and `F_GETLK` reports `F_UNLCK`, so no lock list is kept and merging, splitting and the release on close have nothing to change. Nothing is held on the host, since Windows byte-range locks are mandatory and would fail the guest's own reads and writes through other descriptors, so other host processes neither see these locks nor are seen; a lock from the end of a directory throws. The return values and error codes are unverified on a console - [fcntl on a guest socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) answers `F_GETFD`, `F_SETFD`, `F_GETFL` and `F_SETFL` and fails every other command with `EINVAL`; FreeBSD 11.4's `kern_fcntl` also duplicates the descriptor (`F_DUPFD`, `F_DUPFD_CLOEXEC`) and handles `F_GETOWN` and `F_SETOWN` on a socket. `FD_CLOEXEC` is only recorded (there is no `exec`): `dup`, `dup2` and an accepted socket start without it, `SOCK_CLOEXEC` and `F_SETFD` set it. None of this was measured on a console. -- [ioctl, _ioctl](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) handle only `FIONBIO` and `FIONREAD` on a socket, and any other request there fails with `ENOTTY`. On the other descriptors they follow FreeBSD's [`kern_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_generic.c#L758), [`vn_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_vnops.c#L1488) and [`pipe_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_pipe.c#L1346): `FIONBIO` sets `O_NONBLOCK` as `fcntl` does, `FIOCLEX` and `FIONCLEX` set and clear the close-on-exec flag `F_GETFD` reports, and `FIONREAD` reports the size past the offset of a file, the queued bytes of a pipe's read end, 0 on its write end and `ENOTTY` on the random device, while it throws on a directory or another character device. Every termios request (group `t`, `TIOCGWINSZ` included) fails with `ENOTTY`, since those descriptors are taken as not a terminal, as in `tcgetattr`, and any other request throws. Stremio Plus (PPSA74126) imports `_ioctl` for its statically linked `tc*` functions, which send `TIOCGETA`, `TIOCSETA`, `TIOCSPGRP` and the other termios requests +- [ioctl, _ioctl](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) handle only `FIONBIO` and `FIONREAD` on a socket, and any other request there fails with `ENOTTY`. On the other descriptors they follow FreeBSD's [`kern_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_generic.c#L758), [`vn_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_vnops.c#L1488) and [`pipe_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_pipe.c#L1346): `FIONBIO` sets `O_NONBLOCK` as `fcntl` does, `FIOCLEX` and `FIONCLEX` set and clear the close-on-exec flag `F_GETFD` reports, and `FIONREAD` reports the size past the offset of a file, the queued bytes of a pipe's read end, 0 on its write end and `ENOTTY` on the random device, while it throws on a directory or another character device. Every termios request (group `t`, `TIOCGWINSZ` included) fails with `ENOTTY`, since those descriptors are taken as not a terminal, as in `tcgetattr`, and any other request throws. A request whose encoding is malformed fails with `ENOTTY` before the descriptor is looked at, as `sys_ioctl` does for a request without a direction bit, an output request without a length, or a void request with a length other than 4; an input request without a length is left to the rules above, because FreeBSD answers it differently with and without its `COMPAT_43` options and which the PS5 kernel uses is unknown. Stremio Plus (PPSA74126) imports `_ioctl` for its statically linked `tc*` functions, which send `TIOCGETA`, `TIOCSETA`, `TIOCSPGRP` and the other termios requests - [Regular file mappings](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) - on Linux `mmap` maps a regular file's descriptor shared or private like a shared memory object, with the same access checks; SQLite's WAL index (`-shm`) in Dusklight PPSA99305 needs it. FreeBSD 11 `mmap(2)` and shadPS4's `MemoryManager::MapFile` map regular files; the console's behaviour is not measured. Other descriptors (pipes, sockets, directories, devices) still fail with `EOPNOTSUPP`. On Windows a shared mapping is a view of a file section (`CreateFileMappingW` on the descriptor's handle, read-write when the descriptor is, otherwise read-only), so writes through the descriptor and the mapping see each other. This also maps anonymous `shm_open` objects and JIT shared memory descriptors, which are temporary files on Windows. A shared mapping may end inside the 16 KiB page that holds the end of the file, as POSIX [`mmap`](https://pubs.opengroup.org/onlinepubs/9799919799/functions/mmap.html) allows (llama.cpp in PS5LM Plus PPSA99581 maps a whole model file this way): the section keeps the file's size, so the file never grows, the view ends with the 4 KiB host page that holds the end of the file, which Windows fills with zeros past it, and the rest of the guest page is private zero pages. As POSIX requires, the bytes past the end read zero and are never written to the file; bytes written to those private pages are seen only through that mapping, not through other mappings of the file, and a file that grows later does not show its new bytes there. A shared mapping that reaches a whole page past the end of the file fails with `EOPNOTSUPP`, where POSIX maps it and delivers `SIGBUS` on access, as do `PROT_EXEC` and a write-only descriptor. A read-write alias (`GuestArenaMapAlias`) of a mapped page that holds the end of the file throws. `mprotect` above the view's protection (writing through a read-only descriptor, or adding `PROT_EXEC`) fails with `EACCES` before anything changes; FreeBSD allows `PROT_EXEC` on a readable descriptor. Shrinking the file with `ftruncate` fails with `EINVAL` while any shared mapping of it exists, even one that does not cover the removed part, because the section keeps the file's size at `mmap`; FreeBSD truncates. A read-write alias of a view (`GuestArenaMapAlias`, used when the GPU imports guest memory) of a read-only file section fails, as the alias is mapped `PAGE_READWRITE`. Views of the same file made by separate `mmap` calls are separate sections, so write tracking does not treat them as aliases of each other. A private mapping on Windows is a copy of the file read at `mmap`, which POSIX allows (later changes to the file need not show); pages wholly past the end of the file fail with `EOPNOTSUPP` instead of faulting on access, and a write-only descriptor fails with `EOPNOTSUPP`. - [sendfile](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) follows FreeBSD 11's `sendfile` for a connected stream socket: headers, the file from `offset` (to its end when `nbytes` is 0) and trailers, the file offset left unchanged, `EAGAIN` with `*sbytes` set after a partial send on a non-blocking socket. It reads the file through `sceKernelPread` in 64 KiB blocks and sends each with `send`. A socket as the file fails with `EINVAL`, as on FreeBSD, but any other descriptor `sceKernelPread` reads, such as `/dev/urandom`, is sent where FreeBSD requires a regular file or shared memory, and a file descriptor as the socket fails with `EBADF`, as for `send`, where FreeBSD answers `ENOTSOCK`. `flags` are ignored: reads complete before sending, so `SF_NODISKIO` never fails with `EBUSY`, and `SF_SYNC` returns once the data is queued on the socket rather than sent. PS5MC (PPSA99420) imports it - [pthread_kill](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) returns the error number like FreeBSD's `pthread_kill`: `EINVAL` for signals outside 0 to 128 and `ESRCH` for a null thread. Signal 0 reports any other thread handle as existing, without checking that the thread is still alive. A signal to the calling thread goes through `raise`, so it shares `raise`'s limits; a signal to another thread is left on the pending set for whichever thread waits for or unblocks it, since it is not delivered to the chosen thread, and a handle that is not a live guest thread returns `ESRCH` without touching it From 5fafbef246610b5d0c98190157cd38c727a26bb6 Mon Sep 17 00:00:00 2001 From: Lindamea1 <322051987+Lindamea1@users.noreply.github.com> Date: Mon, 19 Oct 2026 13:47:44 +0100 Subject: [PATCH 099/269] test(libkernel): check the Linux TIOCGWINSZ word and the ioctl request boundaries The OpenOrbis net_http sample stops on 0x5413, the Linux TIOCGWINSZ that musl sends to standard output; the carried cases covered 0x5401 but not that word, so the test adds it to the malformed words. It also pins the two boundary words the check takes as well-formed, 0x20046601 (IOC_VOID, length 4) and 0x80006601 (IOC_IN, length 0): on a guest socket with a null argument they fail with EFAULT, not ENOTTY. The debt sentence now states the open gap: an input request with length 0 is not rejected as malformed, while sys_ioctl rejects it in a kernel built without COMPAT_FREEBSD4, COMPAT_FREEBSD5 and COMPAT_43, and which of these the PS5 kernel has is unknown. Co-Authored-By: Claude Opus 5.5 --- core/libs/tests/GuestKernelFcntl.cpp | 6 +++++- docs/dev/debt/libkernel.md | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/core/libs/tests/GuestKernelFcntl.cpp b/core/libs/tests/GuestKernelFcntl.cpp index 62beb83814..524deb371b 100644 --- a/core/libs/tests/GuestKernelFcntl.cpp +++ b/core/libs/tests/GuestKernelFcntl.cpp @@ -67,7 +67,9 @@ static constexpr std::uint64_t WindowSizeRequest = 0x40087468; static constexpr std::uint64_t AsyncRequest = 0x8004667d; static constexpr std::uint64_t CloseOnExecRequest = 0x20006601; static constexpr std::uint64_t KeepOnExecRequest = 0x20006602; -static constexpr std::uint64_t MalformedRequests[] = {0, 123, 0x5401, 0x40007468, 0x20087468, 0x00087468}; +static constexpr std::uint64_t MalformedRequests[] = {0, 123, 0x5401, 0x5413, 0x40007468, 0x20087468, 0x00087468}; +static constexpr std::uint64_t IntegerArgumentRequest = 0x20046601; +static constexpr std::uint64_t EmptyInputRequest = 0x80006601; static constexpr int SceEbadf = static_cast(0x80020009u); static constexpr int SceEinval = static_cast(0x80020016u); static constexpr int SceEnotsup = static_cast(0x8002002du); @@ -118,6 +120,8 @@ static void SocketsKeepTheirPath() { int queued = -1; Require(ioctl_nid_postfix(socket, ReadableRequest, &queued) == 0 && queued == 0); RequireMalformedRequestsFail(socket); + RequireFailure(ioctl_nid_postfix(socket, IntegerArgumentRequest, nullptr), GuestEfault); + RequireFailure(ioctl_nid_postfix(socket, EmptyInputRequest, nullptr), GuestEfault); Require(close_nid_postfix(socket) == 0); Require(sceKernelFcntl(socket, GetFlags) == SceEbadf); Require(sceKernelFcntl(-1, GetFlags) == SceEbadf); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 095c6a3cfe..205de504fe 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -196,7 +196,7 @@ - [sceKernelFcntl](../../../core/libs/prx/libkernel/File/src/Open.cpp) (libkernel) runs the POSIX `fcntl` below and returns its guest errno as `SCE_KERNEL_ERROR_*`, the form the other `sceKernel` file exports use. The return values and error codes are unverified on a console - [fcntl](../../../core/libs/prx/libkernel/File/src/DescriptorControl.cpp) (libkernel) follows FreeBSD 11.4's [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L479-L799) and [`kern_dup`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L812) on file, directory, pipe and character device descriptors; a socket keeps the libSceNet path (`F_GETFL`, `F_SETFL` with `O_NONBLOCK`, `F_GETFD` and `F_SETFD`, `EINVAL` for every other command, see the next entry). The close-on-exec flag and the status flags live in a guest table, the status flags shared by `dup`, `dup2` and `F_DUPFD` as in an open file description; a descriptor the table has not seen, such as 0 to 2, starts from the host's flags, since the console's are not known. `F_GETFL` takes the access mode from the host (the granted access of the handle on Windows), so pipe ends report `O_RDONLY` and `O_WRONLY` where FreeBSD's bidirectional pipes report `O_RDWR`. `F_DUPFD` and `F_DUPFD_CLOEXEC` return the lowest free host descriptor at or above the start, so the bound is the host table (`EMFILE` on Windows, `EINVAL` past `RLIMIT_NOFILE` on Linux), not the console's unknown `RLIMIT_NOFILE` and `kern.maxfilesperproc`. `O_NONBLOCK` switches a host pipe (`PIPE_NOWAIT` on Windows, where an empty read and a full write then report `EAGAIN`) and is only recorded on files, directories and the random device, as their reads never block; on a Windows console or other character device it throws. Changing `O_APPEND` on a Windows file, changing `O_FSYNC` on a file, `O_ASYNC` on a pipe or character device (`SIGIO` is not delivered), `F_GETOWN`, `F_SETOWN` and `F_SETLK_REMOTE` throw; `O_DIRECT`, `F_RDAHEAD` and `F_READAHEAD` are recorded as the hints they are. Record locks (`F_GETLK`, `F_SETLK`, `F_SETLKW` and their forms without `l_sysid`) use the host's POSIX locks on Linux and macOS. `F_GETLK` with `F_UNLCK` asks the host with `F_RDLCK`: as in [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) only another owner's overlapping write lock conflicts with either request. On Windows they follow [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L607-L752) and [`lf_advlockasync`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L433-L487) for the one guest process, which owns every lock: `EOVERFLOW` for an `SEEK_CUR` start past `OFF_MAX`, then `EBADF` for `F_RDLCK` without read access or `F_WRLCK` without write access and `EINVAL` for another type, then `EINVAL` and `EOVERFLOW` for the range; as [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) never lets locks of one owner conflict, whichever descriptor or thread asks, `F_SETLK` and `F_SETLKW` then succeed without waiting and `F_GETLK` reports `F_UNLCK`, so no lock list is kept and merging, splitting and the release on close have nothing to change. Nothing is held on the host, since Windows byte-range locks are mandatory and would fail the guest's own reads and writes through other descriptors, so other host processes neither see these locks nor are seen; a lock from the end of a directory throws. The return values and error codes are unverified on a console - [fcntl on a guest socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) answers `F_GETFD`, `F_SETFD`, `F_GETFL` and `F_SETFL` and fails every other command with `EINVAL`; FreeBSD 11.4's `kern_fcntl` also duplicates the descriptor (`F_DUPFD`, `F_DUPFD_CLOEXEC`) and handles `F_GETOWN` and `F_SETOWN` on a socket. `FD_CLOEXEC` is only recorded (there is no `exec`): `dup`, `dup2` and an accepted socket start without it, `SOCK_CLOEXEC` and `F_SETFD` set it. None of this was measured on a console. -- [ioctl, _ioctl](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) handle only `FIONBIO` and `FIONREAD` on a socket, and any other request there fails with `ENOTTY`. On the other descriptors they follow FreeBSD's [`kern_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_generic.c#L758), [`vn_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_vnops.c#L1488) and [`pipe_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_pipe.c#L1346): `FIONBIO` sets `O_NONBLOCK` as `fcntl` does, `FIOCLEX` and `FIONCLEX` set and clear the close-on-exec flag `F_GETFD` reports, and `FIONREAD` reports the size past the offset of a file, the queued bytes of a pipe's read end, 0 on its write end and `ENOTTY` on the random device, while it throws on a directory or another character device. Every termios request (group `t`, `TIOCGWINSZ` included) fails with `ENOTTY`, since those descriptors are taken as not a terminal, as in `tcgetattr`, and any other request throws. A request whose encoding is malformed fails with `ENOTTY` before the descriptor is looked at, as `sys_ioctl` does for a request without a direction bit, an output request without a length, or a void request with a length other than 4; an input request without a length is left to the rules above, because FreeBSD answers it differently with and without its `COMPAT_43` options and which the PS5 kernel uses is unknown. Stremio Plus (PPSA74126) imports `_ioctl` for its statically linked `tc*` functions, which send `TIOCGETA`, `TIOCSETA`, `TIOCSPGRP` and the other termios requests +- [ioctl, _ioctl](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) handle only `FIONBIO` and `FIONREAD` on a socket, and any other request there fails with `ENOTTY`. On the other descriptors they follow FreeBSD's [`kern_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_generic.c#L758), [`vn_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_vnops.c#L1488) and [`pipe_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_pipe.c#L1346): `FIONBIO` sets `O_NONBLOCK` as `fcntl` does, `FIOCLEX` and `FIONCLEX` set and clear the close-on-exec flag `F_GETFD` reports, and `FIONREAD` reports the size past the offset of a file, the queued bytes of a pipe's read end, 0 on its write end and `ENOTTY` on the random device, while it throws on a directory or another character device. Every termios request (group `t`, `TIOCGWINSZ` included) fails with `ENOTTY`, since those descriptors are taken as not a terminal, as in `tcgetattr`, and any other well-formed request throws. An input request with length 0 is not rejected as malformed: [`sys_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_generic.c#L703-L718) rejects it only in a kernel built without `COMPAT_FREEBSD4`, `COMPAT_FREEBSD5` and `COMPAT_43`, and which of these the PS5 kernel has is unknown. Stremio Plus (PPSA74126) imports `_ioctl` for its statically linked `tc*` functions, which send `TIOCGETA`, `TIOCSETA`, `TIOCSPGRP` and the other termios requests - [Regular file mappings](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) - on Linux `mmap` maps a regular file's descriptor shared or private like a shared memory object, with the same access checks; SQLite's WAL index (`-shm`) in Dusklight PPSA99305 needs it. FreeBSD 11 `mmap(2)` and shadPS4's `MemoryManager::MapFile` map regular files; the console's behaviour is not measured. Other descriptors (pipes, sockets, directories, devices) still fail with `EOPNOTSUPP`. On Windows a shared mapping is a view of a file section (`CreateFileMappingW` on the descriptor's handle, read-write when the descriptor is, otherwise read-only), so writes through the descriptor and the mapping see each other. This also maps anonymous `shm_open` objects and JIT shared memory descriptors, which are temporary files on Windows. A shared mapping may end inside the 16 KiB page that holds the end of the file, as POSIX [`mmap`](https://pubs.opengroup.org/onlinepubs/9799919799/functions/mmap.html) allows (llama.cpp in PS5LM Plus PPSA99581 maps a whole model file this way): the section keeps the file's size, so the file never grows, the view ends with the 4 KiB host page that holds the end of the file, which Windows fills with zeros past it, and the rest of the guest page is private zero pages. As POSIX requires, the bytes past the end read zero and are never written to the file; bytes written to those private pages are seen only through that mapping, not through other mappings of the file, and a file that grows later does not show its new bytes there. A shared mapping that reaches a whole page past the end of the file fails with `EOPNOTSUPP`, where POSIX maps it and delivers `SIGBUS` on access, as do `PROT_EXEC` and a write-only descriptor. A read-write alias (`GuestArenaMapAlias`) of a mapped page that holds the end of the file throws. `mprotect` above the view's protection (writing through a read-only descriptor, or adding `PROT_EXEC`) fails with `EACCES` before anything changes; FreeBSD allows `PROT_EXEC` on a readable descriptor. Shrinking the file with `ftruncate` fails with `EINVAL` while any shared mapping of it exists, even one that does not cover the removed part, because the section keeps the file's size at `mmap`; FreeBSD truncates. A read-write alias of a view (`GuestArenaMapAlias`, used when the GPU imports guest memory) of a read-only file section fails, as the alias is mapped `PAGE_READWRITE`. Views of the same file made by separate `mmap` calls are separate sections, so write tracking does not treat them as aliases of each other. A private mapping on Windows is a copy of the file read at `mmap`, which POSIX allows (later changes to the file need not show); pages wholly past the end of the file fail with `EOPNOTSUPP` instead of faulting on access, and a write-only descriptor fails with `EOPNOTSUPP`. - [sendfile](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) follows FreeBSD 11's `sendfile` for a connected stream socket: headers, the file from `offset` (to its end when `nbytes` is 0) and trailers, the file offset left unchanged, `EAGAIN` with `*sbytes` set after a partial send on a non-blocking socket. It reads the file through `sceKernelPread` in 64 KiB blocks and sends each with `send`. A socket as the file fails with `EINVAL`, as on FreeBSD, but any other descriptor `sceKernelPread` reads, such as `/dev/urandom`, is sent where FreeBSD requires a regular file or shared memory, and a file descriptor as the socket fails with `EBADF`, as for `send`, where FreeBSD answers `ENOTSOCK`. `flags` are ignored: reads complete before sending, so `SF_NODISKIO` never fails with `EBUSY`, and `SF_SYNC` returns once the data is queued on the socket rather than sent. PS5MC (PPSA99420) imports it - [pthread_kill](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) returns the error number like FreeBSD's `pthread_kill`: `EINVAL` for signals outside 0 to 128 and `ESRCH` for a null thread. Signal 0 reports any other thread handle as existing, without checking that the thread is still alive. A signal to the calling thread goes through `raise`, so it shares `raise`'s limits; a signal to another thread is left on the pending set for whichever thread waits for or unblocks it, since it is not delivered to the chosen thread, and a handle that is not a live guest thread returns `ESRCH` without touching it From 6de562f8a33760c686fab7deeb53c0edbcaa4400 Mon Sep 17 00:00:00 2001 From: emirplus Date: Mon, 19 Oct 2026 18:51:14 +0300 Subject: [PATCH 100/269] fix(tools): fetch upstream tracking base before resolving default base --- tools/check_main_sync.py | 14 ++++++++++---- tools/tests/test_check_main_sync.py | 8 ++++++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/tools/check_main_sync.py b/tools/check_main_sync.py index 12d5b78a32..e1f41677d7 100644 --- a/tools/check_main_sync.py +++ b/tools/check_main_sync.py @@ -103,11 +103,17 @@ if __name__ == "__main__": parser.error("--fetch needs a remote-tracking base such as upstream/main") git(args.repo, "fetch", "--quiet", remote, branch) - if args.fetch and args.base: - fetch(args.base) + if args.fetch: + if args.base: + fetch(args.base) + else: + remotes = set(lines(args.repo, "remote")) + for candidate in BASES: + remote, _, branch = candidate.partition("/") + if branch and remote in remotes: + fetch(candidate) + break base = resolve_base(args.repo, args.base) - if args.fetch and not args.base: - fetch(base) data = report(args.repo, base, args.head) except RuntimeError as error: parser.error(str(error)) diff --git a/tools/tests/test_check_main_sync.py b/tools/tests/test_check_main_sync.py index d893e70b0e..a4590a60e5 100644 --- a/tools/tests/test_check_main_sync.py +++ b/tools/tests/test_check_main_sync.py @@ -185,6 +185,14 @@ class CheckMainSyncTests(unittest.TestCase): self.assertEqual(result.returncode, 0, result.stderr) self.assertIn("merge: clean", result.stdout) + def test_default_base_is_fetched_before_it_is_resolved(self): + with tempfile.TemporaryDirectory() as other: + subprocess.run(["git", "clone", "-q", self.directory.name, other], check=True, capture_output=True) + self.repo.git("remote", "add", "upstream", other) + result = subprocess.run([sys.executable, str(TOOL), "--repo", self.directory.name, "--fetch"], capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("merge: clean", result.stdout) + if __name__ == "__main__": unittest.main() From 8836668ef463bdb1176986d24b11ff56e747aaae Mon Sep 17 00:00:00 2001 From: emirplus Date: Mon, 19 Oct 2026 18:52:56 +0300 Subject: [PATCH 101/269] fix(tools): preserve lf line endings on stderr in stubgen --- tools/stubgen.py | 5 +++-- tools/tests/test_stubgen_stdout_newlines.py | 5 +++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/tools/stubgen.py b/tools/stubgen.py index ae9f68955b..a701cb5472 100644 --- a/tools/stubgen.py +++ b/tools/stubgen.py @@ -221,8 +221,9 @@ def main(argv=None): ap.add_argument("--td-out", required=False, default=None, help="output file for TechnicalDebt lines, which go in docs/dev/debt/MODULE.md, default stderr when code uses stdout, otherwise stdout") ap.add_argument("--td-only", action="store_true", help="print only TechnicalDebt lines, no code") args = ap.parse_args(argv) - if hasattr(sys.stdout, "reconfigure"): - sys.stdout.reconfigure(encoding="utf-8", newline="\n") + for stream in (sys.stdout, sys.stderr): + if hasattr(stream, "reconfigure"): + stream.reconfigure(encoding="utf-8", newline="\n") try: if not args.td_only and args.out and args.td_out and same_file(args.out, args.td_out): ap.error("--out and --td-out must name different files") diff --git a/tools/tests/test_stubgen_stdout_newlines.py b/tools/tests/test_stubgen_stdout_newlines.py index 110719d87b..427d45a14c 100644 --- a/tools/tests/test_stubgen_stdout_newlines.py +++ b/tools/tests/test_stubgen_stdout_newlines.py @@ -28,6 +28,11 @@ class StubgenStdoutNewlineTests(unittest.TestCase): self.assertIn(b"\n", result.stdout) self.assertNotIn(b"\r\n", result.stdout) + def test_td_on_stderr_uses_lf(self): + result = self.run_stubgen() + self.assertIn(b"\n", result.stderr) + self.assertNotIn(b"\r\n", result.stderr) + if __name__ == "__main__": unittest.main() From 51e75dec2591d1b884c541f48cdfdd2431bdf0eb Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Mon, 19 Oct 2026 17:31:03 +0200 Subject: [PATCH 102/269] fix(json2): preserve values when type allocation fails --- core/libs/prx/libSceJson2/Export.cpp | 13 ++++++++----- core/libs/tests/JsonCopyFailure.cpp | 18 +++++++++++++++--- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceJson2/Export.cpp b/core/libs/prx/libSceJson2/Export.cpp index 745771a3b4..286b7d8fd0 100644 --- a/core/libs/prx/libSceJson2/Export.cpp +++ b/core/libs/prx/libSceJson2/Export.cpp @@ -196,12 +196,15 @@ void CopyInto(Value& destination, const Value& source) { } void SetType(Node& n, ValueType type) { - Clear(n); - n.type = type; - if (type == TypeString) Construct(n.string, std::string()); - if (type == TypeArray) Construct(n.array); - if (type == TypeObject) Construct(n.object); if (type < TypeNull || type > TypeObject) throw std::invalid_argument("sce::Json::Value::set: invalid value type"); + Node pending{}; + pending.type = type; + if (type == TypeString) Construct(pending.string, std::string()); + if (type == TypeArray) Construct(pending.array); + if (type == TypeObject) Construct(pending.object); + Clear(n); + pending.handlers = n.handlers; + n = pending; } const Value& Defaults(ValueType type) { diff --git a/core/libs/tests/JsonCopyFailure.cpp b/core/libs/tests/JsonCopyFailure.cpp index a3c64622f2..0fa28bad98 100644 --- a/core/libs/tests/JsonCopyFailure.cpp +++ b/core/libs/tests/JsonCopyFailure.cpp @@ -24,6 +24,8 @@ void APS5_VABI _ZN3sce4Json5ValueD1Ev(Value*); Value* APS5_VABI _ZN3sce4Json5ValueaSERKS1_(Value*, const Value*); int APS5_VABI _ZN3sce4Json5Value3setERKNS0_5ArrayE(Value*, const Array*); int APS5_VABI _ZN3sce4Json5Value3setERKNS0_6ObjectE(Value*, const Object*); +int APS5_VABI _ZN3sce4Json5Value3setENS0_9ValueTypeE(Value*, std::int32_t); +std::int32_t APS5_VABI _ZNK3sce4Json5Value7getTypeEv(const Value*); const Array* APS5_VABI _ZNK3sce4Json5Value8getArrayEv(const Value*); const Object* APS5_VABI _ZNK3sce4Json5Value9getObjectEv(const Value*); int APS5_VABI _ZN3sce4Json5Value9serializeERNS0_6StringE(Value*, String*); @@ -92,7 +94,7 @@ static void CheckValue(const Value& value, const char* expected) { enum class Operation { ValueCopy, ArrayCopy, ObjectCopy, ValueAssign, ObjectAssign, ArrayAppend, ValueArray, ValueObject, ValueString, ValueText, ValueTypeString, ValueTypeArray, - ValueTypeObject, SetArray, SetObject, PairCopy + ValueTypeObject, SetArray, SetObject, SetTypeArray, SetTypeObject, SetTypeString, PairCopy }; static bool Run(Operation operation, const Value& source, const char* expected, const Array& array, const Object& object, const String& text) { @@ -101,7 +103,8 @@ static bool Run(Operation operation, const Value& source, const char* expected, Object objectResult{}; Pair pair{}; bool valueReady = false, arrayReady = false, objectReady = false, pairReady = false; - if (operation == Operation::ValueAssign || operation == Operation::SetArray || operation == Operation::SetObject) { + const bool typeSetter = operation == Operation::SetTypeArray || operation == Operation::SetTypeObject || operation == Operation::SetTypeString; + if (operation == Operation::ValueAssign || operation == Operation::SetArray || operation == Operation::SetObject || typeSetter) { _ZN3sce4Json5ValueC1EPKc(&result, "previous"); valueReady = true; } @@ -126,6 +129,9 @@ static bool Run(Operation operation, const Value& source, const char* expected, case Operation::ValueTypeObject: _ZN3sce4Json5ValueC1ENS0_9ValueTypeE(&result, 7); valueReady = true; break; case Operation::SetArray: _ZN3sce4Json5Value3setERKNS0_5ArrayE(&result, &array); break; case Operation::SetObject: _ZN3sce4Json5Value3setERKNS0_6ObjectE(&result, &object); break; + case Operation::SetTypeArray: _ZN3sce4Json5Value3setENS0_9ValueTypeE(&result, 6); break; + case Operation::SetTypeObject: _ZN3sce4Json5Value3setENS0_9ValueTypeE(&result, 7); break; + case Operation::SetTypeString: _ZN3sce4Json5Value3setENS0_9ValueTypeE(&result, 5); break; case Operation::PairCopy: _ZN3sce4Json6Object4PairC1ERKNS0_6StringERKNS0_5ValueE(&pair, &text, &source); pairReady = true; break; } succeeded = true; @@ -134,7 +140,10 @@ static bool Run(Operation operation, const Value& source, const char* expected, allocationBudget = -1; if (valueReady) { if (succeeded) CheckValue(result, expected); - else CheckValue(result, "\"previous\""); + else { + Require(_ZNK3sce4Json5Value7getTypeEv(&result) == 5, "failed setter changed the destination type"); + CheckValue(result, "\"previous\""); + } _ZN3sce4Json5ValueD1Ev(&result); } if (arrayReady) { @@ -183,6 +192,9 @@ int main() { {"empty object value", Operation::ValueTypeObject, &objectValue, "{}"}, {"set array", Operation::SetArray, &arrayValue, arrayJson}, {"set object", Operation::SetObject, &objectValue, objectJson}, + {"set empty array", Operation::SetTypeArray, &objectValue, "[]"}, + {"set empty object", Operation::SetTypeObject, &objectValue, "{}"}, + {"set empty string", Operation::SetTypeString, &objectValue, "\"\""}, {"pair copy", Operation::PairCopy, &objectValue, objectJson}, }; unsigned failures = 0; From 854570694f180ef0c409746ad4a109210be063a0 Mon Sep 17 00:00:00 2001 From: emirplus Date: Mon, 19 Oct 2026 19:18:50 +0300 Subject: [PATCH 103/269] docs(env): list APS5_CPU_FLIP_IN_ORDER in the environment reference --- docs/dev/ENVIRONMENT.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 222ee12c79..7b8dec1cfd 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -102,6 +102,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_COUNT_ALL_COMPLETION_LABELS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | counts a completion label also stored on the GPU as pending from its registration | | `APS5_CPU_COPIES` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | copies misaligned descriptor sub-ranges on the CPU instead of with a GPU buffer copy | | `APS5_CPU_DCC_KEYS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp` | | always stores DCC keys on the CPU | +| `APS5_CPU_FLIP_IN_ORDER` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp` | | orders every CPU flip behind queued work instead of presenting unqueued flips at once | | `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 | From 5965ea6100247e544f3377a7748aacfc0925193c Mon Sep 17 00:00:00 2001 From: emirplus Date: Mon, 19 Oct 2026 19:14:40 +0300 Subject: [PATCH 104/269] fix(tools): read C++ source files with utf-8 encoding in progress scanner --- tools/progress.py | 4 +- tools/tests/test_progress_read_encoding.py | 53 ++++++++++++++++++++++ 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/tools/progress.py b/tools/progress.py index 72113313d8..7cf8d2b549 100644 --- a/tools/progress.py +++ b/tools/progress.py @@ -165,7 +165,7 @@ def scan_library(path): for source in path.rglob("*.cpp"): if "tests" in source.relative_to(path).parts: continue - text = code(source.read_text(errors="ignore")) + text = code(source.read_text(encoding="utf-8", errors="ignore")) calls = stub_calls(text) wrappers = inert_wrappers(text) for match in DEFINITION.finditer(text): @@ -186,7 +186,7 @@ def scan_library(path): if not (done or todo): cmake = path / "CMakeLists.txt" texts = [(CMAKE_SHARED, cmake.read_text(encoding="utf-8", errors="ignore"))] if cmake.is_file() else [] - texts += [(SOURCE_SHARED, source.read_text(errors="ignore")) for source in sorted(path.glob("*.cpp"))] + texts += [(SOURCE_SHARED, source.read_text(encoding="utf-8", errors="ignore")) for source in sorted(path.glob("*.cpp"))] owners = [m[1] for pattern, content in texts for m in pattern.finditer(content)] owners = [o for o in owners if o != path.name and (path.parent / o).is_dir()] if owners: diff --git a/tools/tests/test_progress_read_encoding.py b/tools/tests/test_progress_read_encoding.py index 1c01153b58..40f3d7e02a 100644 --- a/tools/tests/test_progress_read_encoding.py +++ b/tools/tests/test_progress_read_encoding.py @@ -36,6 +36,59 @@ class ProgressReadEncodingTests(unittest.TestCase): self.assertEqual(result.returncode, 0, stderr) self.assertEqual(result.stdout.decode("ascii").strip(), "libSceOther") + def test_utf8_cpp_source_with_ascii_default_codec(self): + source_code = """ +import sys +from pathlib import Path +sys.path.insert(0, sys.argv[1]) +import progress +group = progress.scan_library(Path(sys.argv[2]) / "libSceExample") +print(group.get("done_names")) +""" + with tempfile.TemporaryDirectory(prefix="progress read cpp ") as directory: + root = Path(directory) + library = root / "libSceExample" + library.mkdir() + (library / "Export.cpp").write_bytes( + ("// \u65e5\u672c\u8a9e \U0001f3ae\n" + "int APS5_VABI sceExampleReady() { return 0; }\n").encode("utf-8") + ) + env = {key: value for key, value in os.environ.items() if key != "PYTHONIOENCODING"} + env.update({"PYTHONUTF8": "0", "LC_ALL": "C", "LANG": "C"}) + result = subprocess.run([sys.executable, "-I", "-X", "utf8=0", "-c", source_code, str(TOOLS), str(root)], + env=env, capture_output=True, timeout=20) + stderr = result.stderr.decode("utf-8", errors="replace") + self.assertNotIn("UnicodeDecodeError", stderr) + self.assertEqual(result.returncode, 0, stderr) + self.assertEqual(result.stdout.decode("ascii").strip(), "['sceExampleReady']") + + def test_utf8_shared_cpp_with_ascii_default_codec(self): + source_code = """ +import sys +from pathlib import Path +sys.path.insert(0, sys.argv[1]) +import progress +group = progress.scan_library(Path(sys.argv[2]) / "libSceExample") +print(group.get("shared_sources")) +""" + with tempfile.TemporaryDirectory(prefix="progress shared cpp ") as directory: + root = Path(directory) + (root / "libSceOther").mkdir() + library = root / "libSceExample" + library.mkdir() + (library / "Forward.cpp").write_bytes( + ("// \u2010 \u2192 \u65e5\u672c\u8a9e\n" + '#include "prx/libSceOther/Shared.cpp"\n').encode("utf-8") + ) + env = {key: value for key, value in os.environ.items() if key != "PYTHONIOENCODING"} + env.update({"PYTHONUTF8": "0", "LC_ALL": "C", "LANG": "C"}) + result = subprocess.run([sys.executable, "-I", "-X", "utf8=0", "-c", source_code, str(TOOLS), str(root)], + env=env, capture_output=True, timeout=20) + stderr = result.stderr.decode("utf-8", errors="replace") + self.assertNotIn("UnicodeDecodeError", stderr) + self.assertEqual(result.returncode, 0, stderr) + self.assertEqual(result.stdout.decode("ascii").strip(), "libSceOther") + if __name__ == "__main__": unittest.main() From d2657b975001ae0258e2520506e75b872df2fe89 Mon Sep 17 00:00:00 2001 From: billythemenace2 Date: Mon, 19 Oct 2026 17:22:50 +0100 Subject: [PATCH 105/269] fix(libkernel): classify the raise target's code without loader calls while it is suspended GetModuleHandleExW and GetModuleFileNameW take loader locks the suspended target may hold. While the target is suspended only VirtualQuery and a lock-free cache of image bases are consulted; an image not yet cached is classified after the target is resumed, and the target is sampled again. The statics the checks use are initialized before the first suspend. The host-code wait is recorded in the libkernel debt file, and the test raises 100 times so it fails on every run without the change. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../prx/libkernel/System/src/Exception.cpp | 82 ++++++++++++++++--- core/libs/tests/GuestRaiseInHostCode.cpp | 2 +- docs/dev/debt/libkernel.md | 1 + 3 files changed, 72 insertions(+), 13 deletions(-) diff --git a/core/libs/prx/libkernel/System/src/Exception.cpp b/core/libs/prx/libkernel/System/src/Exception.cpp index be680ca0f0..377c194661 100644 --- a/core/libs/prx/libkernel/System/src/Exception.cpp +++ b/core/libs/prx/libkernel/System/src/Exception.cpp @@ -252,15 +252,59 @@ bool InWinpthread(DWORD64 rip) { return winpthread != nullptr && VirtualQuery(reinterpret_cast(rip), &info, sizeof(info)) == sizeof(info) && info.AllocationBase == winpthread; } -bool InHostModule(DWORD64 rip) { - HMODULE module = nullptr; - if (!GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, reinterpret_cast(rip), &module) || module == nullptr) return false; - if (module == GetModuleHandleW(nullptr)) return false; - wchar_t path[MAX_PATH]{}; - const auto length = GetModuleFileNameW(module, path, MAX_PATH); - const std::wstring_view name(path, length); - constexpr std::wstring_view guestSuffix = L".guest.prx"; - return !(name.size() >= guestSuffix.size() && name.substr(name.size() - guestSuffix.size()) == guestSuffix); +enum class CodeKind : std::uint8_t { Unknown, Guest, Host }; + +struct ImageKinds { + static constexpr std::size_t Capacity = 256; + std::array, Capacity> bases{}; + std::array, Capacity> kinds{}; + std::atomic count{0}; +}; + +ImageKinds& Images() { + static ImageKinds images; + return images; +} + +DWORD64 ImageBase(DWORD64 rip) { + MEMORY_BASIC_INFORMATION info{}; + if (VirtualQuery(reinterpret_cast(rip), &info, sizeof(info)) != sizeof(info) || info.Type != MEM_IMAGE) return 0; + return reinterpret_cast(info.AllocationBase); +} + +CodeKind CachedKind(DWORD64 base) { + if (base == 0) return CodeKind::Guest; + auto& images = Images(); + const auto count = std::min(images.count.load(std::memory_order_acquire), ImageKinds::Capacity); + for (std::size_t index = 0; index < count; ++index) { + if (images.bases[index].load(std::memory_order_acquire) == base) return static_cast(images.kinds[index].load(std::memory_order_acquire)); + } + return CodeKind::Unknown; +} + +DWORD64 ExecutableBase() { + static const auto executable = reinterpret_cast(GetModuleHandleW(nullptr)); + return executable; +} + +CodeKind ClassifyImage(DWORD64 base) { + if (const auto known = CachedKind(base); known != CodeKind::Unknown) return known; + auto kind = CodeKind::Host; + if (base == ExecutableBase()) { + kind = CodeKind::Guest; + } else { + wchar_t path[MAX_PATH]{}; + const auto length = GetModuleFileNameW(reinterpret_cast(base), path, MAX_PATH); + const std::wstring_view name(path, length); + constexpr std::wstring_view guestSuffix = L".guest.prx"; + if (name.size() >= guestSuffix.size() && name.substr(name.size() - guestSuffix.size()) == guestSuffix) kind = CodeKind::Guest; + } + auto& images = Images(); + if (const auto slot = images.count.fetch_add(1, std::memory_order_acq_rel); slot < ImageKinds::Capacity) { + images.kinds[slot].store(static_cast(kind), std::memory_order_release); + images.bases[slot].store(base, std::memory_order_release); + } + return kind; } bool BlockedInWait(DWORD64 rip) { @@ -278,8 +322,11 @@ bool BlockedInWait(DWORD64 rip) { return std::any_of(waits.begin(), waits.end(), [rip](DWORD64 stub) { return rip - stub < 0x20; }); } -bool RunningHostCode(DWORD64 rip) { - return InHostModule(rip) && !BlockedInWait(rip); +CodeKind RunningCode(DWORD64 rip, DWORD64& base) { + base = ImageBase(rip); + auto kind = CachedKind(base); + if (kind == CodeKind::Unknown && Images().count.load(std::memory_order_acquire) >= ImageKinds::Capacity) kind = CodeKind::Host; + return kind == CodeKind::Host && BlockedInWait(rip) ? CodeKind::Guest : kind; } bool RaiseOn(Pthread thread, GuestExceptionHandler handler, int signum) { @@ -293,6 +340,11 @@ bool RaiseOn(Pthread thread, GuestExceptionHandler handler, int signum) { auto queued = std::make_unique(Delivery{handler, signum, {}, {}, 0}); alignas(16) Delivery delivery{handler, signum, {}, {}, SaveVectors ? 1u : 0u}; const auto retryDeadline = std::chrono::steady_clock::now() + RetryLimit; + static_cast(RestoringContext(0)); + static_cast(InWinpthread(0)); + static_cast(BlockedInWait(0)); + static_cast(ExecutableBase()); + static_cast(Images()); for (;;) { if (SuspendThread(native) == static_cast(-1)) { if (Exited(native)) return false; @@ -315,8 +367,14 @@ bool RaiseOn(Pthread thread, GuestExceptionHandler handler, int signum) { return true; } const bool restoring = RestoringContext(delivery.context.Rip) || InWinpthread(delivery.context.Rip); - if (!restoring && (!RunningHostCode(delivery.context.Rip) || std::chrono::steady_clock::now() >= retryDeadline)) break; + DWORD64 image = 0; + const auto running = restoring ? CodeKind::Guest : RunningCode(delivery.context.Rip, image); + if (!restoring && (running == CodeKind::Guest || (running == CodeKind::Host && std::chrono::steady_clock::now() >= retryDeadline))) break; ResumeThread(native); + if (running == CodeKind::Unknown) { + static_cast(ClassifyImage(image)); + continue; + } if (restoring && std::chrono::steady_clock::now() >= retryDeadline) throw std::runtime_error("sceKernelRaiseException: the target thread stayed inside NtContinue or winpthreads for 1 s"); SwitchToThread(); } diff --git a/core/libs/tests/GuestRaiseInHostCode.cpp b/core/libs/tests/GuestRaiseInHostCode.cpp index a3f728545d..955d4605b6 100644 --- a/core/libs/tests/GuestRaiseInHostCode.cpp +++ b/core/libs/tests/GuestRaiseInHostCode.cpp @@ -16,7 +16,7 @@ int APS5_VABI sceKernelRaiseException(Pthread, int); } static constexpr int SigUsr1 = 30; -static constexpr int Raises = 20; +static constexpr int Raises = 100; static void Require(bool value) { if (!value) std::abort(); } static std::FILE* shared = nullptr; diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index b4adfa1c64..f0a295835a 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -138,6 +138,7 @@ - [scePthreadAttrGet, pthread_attr_get_np and sceKernelIsStack](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) report the stack of a host thread the library did not create, on Windows, from one allocation granule (64 KiB) above the bottom of its reservation when everything above that is committed read-write, as in a relinked executable's main thread and its default-size host threads (0x200000 committed of 0x210000 reserved), so the guest can protect the lowest reported pages, as HotSpot does for its stack guard pages in Java 25 LAN / DNS (PPSA99869). Any other such thread, and every such thread on Linux, gets its whole host stack: on Windows `mprotect` commits its reserved lower pages before protecting them and drops the guard flag of Windows' guard pages, and on Linux it cannot change the unmapped lower pages - [sceKernelInstallExceptionHandler](../../../core/libs/prx/libkernel/System/src/Exception.cpp) records the handler, but a host fault still ends the process: only signals raised with `sceKernelRaiseException` are delivered. Unity's PS5Util module installs a SIGUSR1 handler that reads `uc_mcontext.mc_rsp` (offset 0xf8 of the context) and raises SIGUSR1 on other threads to stop them for garbage collection - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) on Linux sends the host SIGUSR1, which the library takes over, to the target thread, so the handler runs wherever the thread was interrupted, emulator code included. On the console kernel objects are system calls and libthr defers signals while a thread holds its internal locks; here a handler that needs a host lock the interrupted code holds deadlocks, and a thread that the handler keeps waiting keeps its host locks. Host waits resume after the handler. The host calls Linux does not restart end early: sleeps that do not retry return before their time, and the poll of `sceNetEpollWait` and socket calls with a send or receive timeout return `EINTR` to the guest +- [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) on Windows suspends the target and redirects it to the handler. A target running host code (any module other than the executable and the `*.guest.prx` modules, so libkernel, libc and the other HLE modules count too) is resumed and sampled again until it is back in guest code, since a handler that waits there would keep the host locks that code holds; a target blocked in an ntdll wait is redirected in place, as are NtContinue and winpthreads after their retry limit. A target that stays in host code without waiting for the whole 1 s retry limit, such as a long HLE or driver call, costs the raising thread up to 1 s per raise and is then redirected in host code anyway, with the same hazard. While the target is suspended only `VirtualQuery` and a lock-free cache of image bases are consulted; an image not yet in the cache is classified with `GetModuleFileNameW` after the target is resumed. An APC queued at the first host-code sample, which an alertable guest wait would run sooner, is not implemented - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) builds the handler's `ucontext` with FreeBSD's amd64 layout (16-byte sigmask, `mcontext` at 0x40, `mc_rsp` at 0xf8); the console's layout is not verified beyond the fields PS5Util reads - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) does not consult the target thread's signal mask - [pthread_sigmask](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) shares the process-wide mask of `sigprocmask` and `_sigprocmask`, which keeps only the first 32 signals; FreeBSD keeps a mask per thread From 13fa14e7dd2ef6aefc747eda1de346676cce31ab Mon Sep 17 00:00:00 2001 From: andreamatt Date: Sun, 18 Oct 2026 22:28:27 +0200 Subject: [PATCH 106/269] fix(progress_history): take history days in UTC daily_commits grouped commits by %cs, the committer date in the committer's own offset. With merges from +13:00 and +02:00 interleaved on main, the newest commit labelled with the later date can be older than the newest commit labelled with the earlier one, so the chart ended on an older tree and showed a drop that did not happen. Co-Authored-By: Claude Opus 5.5 --- docs/dev/PROGRESS.md | 2 +- tools/progress_history.py | 10 +++++----- tools/tests/test_progress_history.py | 15 +++++++++++++-- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/docs/dev/PROGRESS.md b/docs/dev/PROGRESS.md index 7602f54f0f..a1257f85a9 100644 --- a/docs/dev/PROGRESS.md +++ b/docs/dev/PROGRESS.md @@ -14,7 +14,7 @@ A library with no scanned definitions and no shared sources, such as one whose ` ## History -[`tools/progress_history.py`](../../tools/progress_history.py) measures the first-parent history of `main` with the same rules as `progress.py` and writes `history.json`, `history.svg` and `history.html` (the chart with a tooltip per day plus a table, linked from the README and the end of the progress page). The chart plots counts, not percentages, so it shows how much is added over time: one panel for library functions and one for shader instructions, each on its own scale with the implemented count as a solid line and the declared total as a dashed one (for shaders the declared total is the instruction list in `tools/rdna_isa.txt`); the current percentage sits next to each panel's implemented label. It takes one point per day, at the last commit of the day, and skips commits whose tree lacks the scanned paths. The chart compacts as days accumulate: up to 14 days it labels every day, up to 150 days one label per Monday, beyond that one per month; points are drawn up to 45 days, and every day keeps a hover tooltip, while the table and `history.json` keep every day. Old trees are measured with today's counting rules and instruction list, so the series is comparable but not what the page showed on that day. The workflow checks out the full history for it. +[`tools/progress_history.py`](../../tools/progress_history.py) measures the first-parent history of `main` with the same rules as `progress.py` and writes `history.json`, `history.svg` and `history.html` (the chart with a tooltip per day plus a table, linked from the README and the end of the progress page). The chart plots counts, not percentages, so it shows how much is added over time: one panel for library functions and one for shader instructions, each on its own scale with the implemented count as a solid line and the declared total as a dashed one (for shaders the declared total is the instruction list in `tools/rdna_isa.txt`); the current percentage sits next to each panel's implemented label. It takes one point per UTC day, at the last commit of the day, and skips commits whose tree lacks the scanned paths. The chart compacts as days accumulate: up to 14 days it labels every day, up to 150 days one label per Monday, beyond that one per month; points are drawn up to 45 days, and every day keeps a hover tooltip, while the table and `history.json` keep every day. Old trees are measured with today's counting rules and instruction list, so the series is comparable but not what the page showed on that day. The workflow checks out the full history for it. ## Comparisons diff --git a/tools/progress_history.py b/tools/progress_history.py index 5c5efe2f1b..f042791443 100644 --- a/tools/progress_history.py +++ b/tools/progress_history.py @@ -4,7 +4,7 @@ import json import subprocess import tarfile import tempfile -from datetime import date +from datetime import date, datetime, timezone from html import escape from pathlib import Path @@ -22,9 +22,9 @@ def git(*args): def daily_commits(ref): seen = {} - for line in git("log", "--first-parent", "--format=%H %cs", ref).decode().splitlines(): - sha, day = line.split() - seen.setdefault(day, sha) + for line in git("log", "--first-parent", "--format=%H %ct", ref).decode().splitlines(): + sha, timestamp = line.split() + seen.setdefault(datetime.fromtimestamp(int(timestamp), timezone.utc).date().isoformat(), sha) return sorted(seen.items()) @@ -147,7 +147,7 @@ def page(points): f'

Each panel has its own scale: the solid line is what is implemented, the dashed line what is declared. Library ' 'functions are declared in the prx sources; the shader total is the AMD RDNA 1 + RDNA 2 instruction list ' f'(tools/rdna_isa.txt). ' - 'One point per day with commits on main, taken at the last commit of the day. Every point is measured ' + 'One point per UTC day with commits on main, taken at the last commit of the day. Every point is measured ' f'with the current tools/progress.py rules and instruction list, ' 'so the series is comparable across days. Hover a day for the exact numbers. Raw numbers: history.json. ' 'Back to the current progress.

', diff --git a/tools/tests/test_progress_history.py b/tools/tests/test_progress_history.py index b467e9af88..9244d9fc79 100644 --- a/tools/tests/test_progress_history.py +++ b/tools/tests/test_progress_history.py @@ -14,13 +14,13 @@ import progress_history OPCODES = Path("core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaOpcode.hpp") -def commit(root, day, message, body): +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) (root / OPCODES).parent.mkdir(parents=True, exist_ok=True) (root / OPCODES).write_text("enum class RdnaOpcode : int {\n SEndpgm,\n};\n") - env = {**os.environ, "GIT_AUTHOR_DATE": f"{day}T12:00:00Z", "GIT_COMMITTER_DATE": f"{day}T12:00:00Z"} + 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], check=True, env=env) @@ -43,6 +43,17 @@ class ProgressHistoryTests(unittest.TestCase): self.assertIn(" Date: Mon, 19 Oct 2026 19:34:33 +0300 Subject: [PATCH 107/269] fix(tools): avoid invoking ctest when no tests are selected in plan --- tools/select_tests.py | 3 +++ tools/tests/test_select_tests.py | 14 ++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/tools/select_tests.py b/tools/select_tests.py index 50c0a56334..2d1b33ed6e 100644 --- a/tools/select_tests.py +++ b/tools/select_tests.py @@ -197,6 +197,9 @@ def select(graph, changed): def plan(build, graph, picked): _, listed = build.tests() run = [i for i, t in enumerate(listed, 1) if t["name"] in picked or t["name"] not in graph["tests"]] + if not run: + skip = {build.target(t) for t in listed} - {None} + return [], [t for t in build.leaves() if t not in skip], len(listed) _, selected = build.tests("-I", "0,0,0," + ",".join(map(str, run))) names = {t["name"] for t in selected} run = [i for i, t in enumerate(listed, 1) if t["name"] in names] diff --git a/tools/tests/test_select_tests.py b/tools/tests/test_select_tests.py index d739a0c62a..3ff32bc4ee 100644 --- a/tools/tests/test_select_tests.py +++ b/tools/tests/test_select_tests.py @@ -204,5 +204,19 @@ class CaseInsensitivePathTests(unittest.TestCase): self.assertIsNone(build.repo(os.path.join(os.sep, "WORK", "SOURCE", "BUILD", "Generated.h"))) +class PlanTests(unittest.TestCase): + def test_plan_empty_run_avoids_invoking_ctest(self): + build = mock.Mock() + build.tests.return_value = ({}, [{"name": "alpha", "command": ["/build/alpha_tests"]}]) + build.leaves.return_value = ["tests/alpha_tests"] + build.target.side_effect = lambda t: "tests/alpha_tests" + graph = {"tests": {"alpha": {}}} + run, leaves, count = select_tests.plan(build, graph, set()) + self.assertEqual(run, []) + self.assertEqual(leaves, []) + self.assertEqual(count, 1) + build.tests.assert_called_once_with() + + if __name__ == "__main__": unittest.main() From 90587442854c12e80ba0f1ba9d605b2af25676d6 Mon Sep 17 00:00:00 2001 From: emirplus Date: Mon, 19 Oct 2026 19:36:08 +0300 Subject: [PATCH 108/269] fix(tools): recognize tilde code block fences in pr_labels --- tools/pr_labels.py | 2 +- tools/tests/test_pr_labels.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/tools/pr_labels.py b/tools/pr_labels.py index 0ca6f4724b..9002be92ca 100644 --- a/tools/pr_labels.py +++ b/tools/pr_labels.py @@ -13,7 +13,7 @@ import performance_compare UNBLOCKS = re.compile(r'Unblocks:[ \t]*([^\r\n]*(?:\r?\n[ \t]*[-*][ \t]+(?!\[[ xX]\])[^\r\n]*)*)') TITLE_ID = re.compile(r"(?" UNBLOCKS_LABEL = "unblocks title" PERF_LABEL = "perf" diff --git a/tools/tests/test_pr_labels.py b/tools/tests/test_pr_labels.py index 73a0ccd559..7fe1fc3329 100644 --- a/tools/tests/test_pr_labels.py +++ b/tools/tests/test_pr_labels.py @@ -72,6 +72,7 @@ class NumbersTests(unittest.TestCase): (summary("a") + "\n{not json", False), ("{}\n{}", False), ("```json\n" + json.dumps(json.loads(summary("a")), indent=2) + "\n```\n```json\n" + summary("b") + "\n```", True), + ("~~~json\n" + json.dumps(json.loads(summary("a")), indent=2) + "\n~~~\n~~~json\n" + summary("b") + "\n~~~", True), ("\n" + summary("b"), False), (summary("a") + "\r\n" + summary("b") + "\r\n", True), (summary("a") + "\n{" + "[" * 100000 + "\n" + summary("b"), True), From 76c088264679aefc1802511a5f3f674156e3c824 Mon Sep 17 00:00:00 2001 From: Nithishvelan M Date: Mon, 19 Oct 2026 22:06:36 +0530 Subject: [PATCH 109/269] test(libSceSysmodule): use kAgcDriverModuleId in unload internal with arg test --- core/libs/tests/GuestSysmodule.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/core/libs/tests/GuestSysmodule.cpp b/core/libs/tests/GuestSysmodule.cpp index ab80b5c6e7..f98548f2ce 100644 --- a/core/libs/tests/GuestSysmodule.cpp +++ b/core/libs/tests/GuestSysmodule.cpp @@ -320,15 +320,15 @@ int main() { int unloadArgResult = -1; Require(sceSysmoduleUnloadModuleInternalWithArg(0, 0, nullptr, 0, &unloadArgResult) == kInvalidValue); Require(unloadArgResult == -1); - Require(sceSysmoduleUnloadModuleInternalWithArg(kAgcInternalId, 0, nullptr, 0, &unloadArgResult) == kModuleUnloaded); + Require(sceSysmoduleUnloadModuleInternalWithArg(kAgcDriverModuleId, 0, nullptr, 0, &unloadArgResult) == kModuleUnloaded); Require(unloadArgResult == -1); int loadArgResult = -1; - Require(sceSysmoduleLoadModuleInternalWithArg(kAgcInternalId, 0, nullptr, 0, &loadArgResult) == 0); + Require(sceSysmoduleLoadModuleInternalWithArg(kAgcDriverModuleId, 0, nullptr, 0, &loadArgResult) == 0); Require(loadArgResult == 0); - Require(sceSysmoduleIsLoadedInternal(kAgcInternalId) == 0); - Require(sceSysmoduleUnloadModuleInternalWithArg(kAgcInternalId, 0, nullptr, 0, &unloadArgResult) == 0); + Require(sceSysmoduleIsLoadedInternal(kAgcDriverModuleId) == 0); + Require(sceSysmoduleUnloadModuleInternalWithArg(kAgcDriverModuleId, 0, nullptr, 0, &unloadArgResult) == 0); Require(unloadArgResult == 0); - Require(sceSysmoduleIsLoadedInternal(kAgcInternalId) == kModuleUnloaded); + Require(sceSysmoduleIsLoadedInternal(kAgcDriverModuleId) == kModuleUnloaded); Require(sceSysmoduleIsLoadedInternal(kAgcModuleId) == kModuleUnloaded); Require(sceSysmoduleUnloadModuleInternal(kAgcModuleId) == kModuleUnloaded); From 33b17f60b5a607b3f099d8880af6867d02b5bc5c Mon Sep 17 00:00:00 2001 From: Nithishvelan M Date: Mon, 19 Oct 2026 22:09:29 +0530 Subject: [PATCH 110/269] fix(libSceNpAuth): move wrapper definitions below V3 functions --- core/libs/prx/libSceNpAuth/Export.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceNpAuth/Export.cpp b/core/libs/prx/libSceNpAuth/Export.cpp index 3de53ed094..3b3af9014c 100644 --- a/core/libs/prx/libSceNpAuth/Export.cpp +++ b/core/libs/prx/libSceNpAuth/Export.cpp @@ -66,14 +66,6 @@ int APS5_VABI sceNpAuthGetAuthorizedAppCode() { return SCE_NP_ERROR_SIGNED_OUT; } -int APS5_VABI sceNpAuthGetAuthorizationCode(int req_id, const void* param, void* auth_code) { - return sceNpAuthGetAuthorizationCodeV3(req_id, param, auth_code, nullptr); -} - -int APS5_VABI sceNpAuthGetIdToken(int req_id, const void* param, void* id_token) { - return sceNpAuthGetIdTokenV3(req_id, param, id_token); -} - int APS5_VABI sceNpAuthGetAuthorizationCodeV3(int req_id, const void* param, void* auth_code, int* issuer_id) { (void)issuer_id; if (!param || !auth_code) return ORBIS_NP_AUTH_ERROR_INVALID_ARGUMENT; @@ -111,6 +103,14 @@ int APS5_VABI sceNpAuthGetIdTokenV3(int req_id, const void* param, void* id_toke return SCE_NP_ERROR_SIGNED_OUT; } +int APS5_VABI sceNpAuthGetAuthorizationCode(int req_id, const void* param, void* auth_code) { + return sceNpAuthGetAuthorizationCodeV3(req_id, param, auth_code, nullptr); +} + +int APS5_VABI sceNpAuthGetIdToken(int req_id, const void* param, void* id_token) { + return sceNpAuthGetIdTokenV3(req_id, param, id_token); +} + int APS5_VABI sceNpAuthPollAsync(int req_id, int* result) { std::lock_guard lock(g_requestMutex); auto it = g_requests.find(req_id); From c026550122999c38463cdf7c5a5cf5adadb46472 Mon Sep 17 00:00:00 2001 From: Alex Collini Date: Sun, 18 Oct 2026 20:44:01 +0200 Subject: [PATCH 111/269] perf(agc): refuse uncached textures while the sampled cache's least recently used entry is still in use Over budget but within a quarter over it, the sampled texture cache refused no use of a texture it does not hold, so a cyclic scan over a working set larger than the budget rotated the whole cache and hit almost never. The first use of a key in a present is now refused (used for its draw, never cached) when the least recently used entry was used in this or the previous present. A later use of the key is refused only when that entry was used in this present, so a texture used several times per present is still admitted on a later use. The refusal does not apply past the quarter-over ceiling, so a budget that falls still evicts. The test lowers the budget through OverrideCacheBudgetForTest instead of the environment. The debt entry records the unmeasured present window, that a texture used more than once still rotates the cache, and the one A-B-A measurement. Co-Authored-By: Claude Sonnet 5.5 --- .../Graphics/include/ShaderResources.hpp | 1 + .../Graphics/src/ShaderResources.cpp | 16 +++- .../tests/execution/SampledCacheBudget.cpp | 76 ++++++++++++++++++- docs/dev/debt/libSceAgcDriver.md | 1 + 4 files changed, 86 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index 7b4aaa4858..5569cd738a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -42,6 +42,7 @@ struct TextureCacheUse { std::uint64_t storageBytes = 0; }; TextureCacheUse TextureCacheUsage(); +void OverrideCacheBudgetForTest(std::uint64_t bytes); std::uint64_t SampledTextureCacheBudget(const Context& context); std::shared_ptr CachedSampledTexture(const Context& context, std::span words, VkImageViewType multisampleViewType = VK_IMAGE_VIEW_TYPE_2D); bool SampledTexturesShareEntry(std::span first, std::span second); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 56ba82e241..ffaafdeb5b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -274,16 +274,17 @@ bool admitTexture(TextureCache& cache, const TextureKey& key, std::uint64_t inco cache.firstUses.clear(); cache.firstUsesPresent = present; } - const bool firstUse = incoming != 0 && present != 0 && !cache.firstUses.contains(key); + const bool scan = incoming != 0 && present != 0; + const bool firstUse = scan && !cache.firstUses.contains(key); const auto ceiling = budget + budget / 4u; while (!cache.entries.empty() && cache.bytes + incoming > budget) { const auto back = std::prev(cache.entries.end()); - if (firstUse && back->present + 1u >= present) { + const bool over = cache.bytes + incoming > ceiling; + if (scan && !over && (firstUse ? back->present + 1u >= present : back->present == present)) { cache.firstUses.insert(key); ++cache.counts.uncached; return false; } - const bool over = cache.bytes + incoming > ceiling; const bool keep = back->accounted == 0 || (!over && back->texture.use_count() > 1); if (keep && cache.passed < cache.entries.size()) { cache.entries.splice(cache.entries.begin(), cache.entries, back); @@ -306,7 +307,7 @@ void reportCacheCounts(TextureCache& cache, std::uint64_t budget) { if (now - cache.countsReported < std::chrono::seconds(10)) return; cache.countsReported = now; const auto counts = std::exchange(cache.counts, {}); - AgcDriver::ProfilePrint_nid_no_patch("[texture-cache] (10 s): %llu textures cached, %llu used uncached (first use this frame, the least recently used entry used this frame or the last), %llu evicted, %llu held ones passed over, %llu found changed; %llu MiB in %zu entries, budget %llu MiB, frame %llu\n", static_cast(counts.inserted), static_cast(counts.uncached), static_cast(counts.evicted), static_cast(counts.passed), static_cast(counts.stale), static_cast(cache.bytes >> 20u), cache.entries.size(), static_cast(budget >> 20u), static_cast(Recorder::Presents())); + AgcDriver::ProfilePrint_nid_no_patch("[texture-cache] (10 s): %llu textures cached, %llu used uncached (first use this frame with the least recently used entry used this frame or the last, or a later use with it used this frame), %llu evicted, %llu held ones passed over, %llu found changed; %llu MiB in %zu entries, budget %llu MiB, frame %llu\n", static_cast(counts.inserted), static_cast(counts.uncached), static_cast(counts.evicted), static_cast(counts.passed), static_cast(counts.stale), static_cast(cache.bytes >> 20u), cache.entries.size(), static_cast(budget >> 20u), static_cast(Recorder::Presents())); } struct ImageMemoEntry { @@ -551,7 +552,10 @@ std::optional largestDeviceLocalHeap(const VkPhysicalDeviceMemory return heap; } +std::atomic budgetOverride{0}; + std::uint64_t forcedCacheBudget() { + if (const auto overridden = budgetOverride.load(std::memory_order_relaxed); overridden != 0) return overridden; static const std::uint64_t forced = [] { const char* text = std::getenv("APS5_TEXTURE_CACHE_MIB"); return text != nullptr ? std::strtoull(text, nullptr, 10) << 20u : 0u; @@ -1167,6 +1171,10 @@ std::uint64_t StorageTextureCacheBudget(const Context& context) { return storageBudget(context); } +void OverrideCacheBudgetForTest(std::uint64_t bytes) { + budgetOverride.store(bytes, std::memory_order_relaxed); +} + TextureCacheUse TextureCacheUsage() { TextureCacheUse use; { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp index 881867d747..c9dc46e856 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp @@ -563,10 +563,10 @@ void frameTests(const Context& context) { hits += again == made[surface].lock(); } Require(hits == capacity, "the next frame over the same " + std::to_string(frame) + " textures found " + std::to_string(hits) + " of them cached, not the " + std::to_string(capacity) + " the cache kept"); - auto admitted = Sampled(context, block.Surface(frame - 1u)); - Require(Sampled(context, block.Surface(frame - 1u)) == admitted, "a texture used a second time in the frame was not cached"); - Require(TextureCacheUsage().sampledEntries == capacity && TextureCacheUsage().sampledBytes <= Budget, "the texture admitted on its second use left the cache over its budget"); - admitted.reset(); + auto refused = Sampled(context, block.Surface(frame - 1u)); + Require(Sampled(context, block.Surface(frame - 1u)) != refused, "a texture used a second time in the frame was cached in place of one of the frame's textures"); + Require(TextureCacheUsage().sampledEntries == capacity && TextureCacheUsage().sampledBytes <= Budget, "the texture used a second time left the cache over its budget"); + refused.reset(); Recorder::CountPresent(); std::vector> moved(frame); for (std::size_t surface = 0; surface < frame; ++surface) moved[surface] = Sampled(context, block.Surface(frame + surface)); @@ -578,6 +578,71 @@ void frameTests(const Context& context) { Require(TextureCacheUsage().sampledEntries == capacity, "a second frame over other textures left " + std::to_string(TextureCacheUsage().sampledEntries) + " entries cached"); } +void scanTests(const Context& context) { + Block block(context); + ClearCachedTextures(context.device); + Recorder::CountPresent(); + const auto bytes = Sampled(context, block.Surface(0))->AllocationBytes(); + const auto capacity = static_cast(Budget / bytes); + const auto working = 2u * capacity; + Require(capacity >= 2u && working <= Surfaces && TextureCacheUsage().sampledEntries == 1u, "a snapshot of the 256 KiB surface holds " + std::to_string(bytes) + " bytes, which the test's 1 MiB budget does not fit as planned"); + constexpr std::size_t Presents = 6; + constexpr std::size_t Uses = 2; + std::vector> last(working); + std::size_t hits = 0; + std::size_t lookups = 0; + for (std::size_t present = 0; present < Presents; ++present) { + Recorder::CountPresent(); + for (std::size_t surface = 0; surface < working; ++surface) { + for (std::size_t use = 0; use < Uses; ++use) { + const auto texture = Sampled(context, block.Surface(surface)); + if (present != 0u) { + hits += texture == last[surface].lock(); + ++lookups; + } + last[surface] = texture; + } + } + } + Require(TextureCacheUsage().sampledBytes <= Budget, "the sampled texture cache holds more than its budget after a scan"); + const auto wanted = (Presents - 1u) * Uses * capacity / 2u; + Require(hits >= wanted, "a cyclic scan using each of " + std::to_string(working) + " textures twice in a row, twice the " + std::to_string(capacity) + " the budget holds, found " + std::to_string(hits) + " of " + std::to_string(lookups) + " lookups cached, fewer than " + std::to_string(wanted)); +} + +void shrinkTests(const Context& context) { + Block block(context); + ClearCachedTextures(context.device); + Recorder::CountPresent(); + OverrideCacheBudgetForTest(2u * Budget); + const auto bytes = Sampled(context, block.Surface(0))->AllocationBytes(); + const auto capacity = static_cast((2u * Budget) / bytes); + Require(capacity >= 4u && capacity + 1u <= Surfaces, "a snapshot of the 256 KiB surface holds " + std::to_string(bytes) + " bytes, which the test's 2 MiB budget does not fit as planned"); + for (std::size_t surface = 1; surface < capacity; ++surface) Sampled(context, block.Surface(surface)); + Require(TextureCacheUsage().sampledEntries == capacity, "the 2 MiB budget did not keep " + std::to_string(capacity) + " snapshots"); + Recorder::CountPresent(); + OverrideCacheBudgetForTest(Budget); + for (std::size_t surface = 0; surface < capacity; ++surface) Sampled(context, block.Surface(surface)); + Sampled(context, block.Surface(capacity)); + Require(TextureCacheUsage().sampledBytes <= Ceiling, "after the budget fell to 1 MiB, one insert left " + std::to_string(TextureCacheUsage().sampledBytes) + " bytes cached with every entry in use this frame, past a quarter over the budget"); + OverrideCacheBudgetForTest(0); +} + +void secondUseTests(const Context& context) { + Block block(context); + ClearCachedTextures(context.device); + Recorder::CountPresent(); + const auto bytes = Sampled(context, block.Surface(0))->AllocationBytes(); + const auto capacity = static_cast(Budget / bytes); + Require(capacity >= 2u && capacity < Surfaces && TextureCacheUsage().sampledEntries == 1u, "a snapshot of the 256 KiB surface holds " + std::to_string(bytes) + " bytes, which the test's 1 MiB budget does not fit as planned"); + for (std::size_t surface = 1; surface < capacity; ++surface) Sampled(context, block.Surface(surface)); + Recorder::CountPresent(); + auto first = Sampled(context, block.Surface(capacity)); + Require(TextureCacheUsage().sampledEntries == capacity && first.use_count() == 1, "the first use in a frame of a texture the cache does not hold was cached in place of one the last frame used"); + const auto second = Sampled(context, block.Surface(capacity)); + Require(Sampled(context, block.Surface(capacity)) == second, "a texture used a second time in a frame whose least recently used entry is from the last frame was not cached"); + Require(TextureCacheUsage().sampledEntries == capacity && TextureCacheUsage().sampledBytes <= Budget, "the texture admitted on its second use left the cache over its budget"); +} + void earlyMissTests(const Context& context) { Block block(context); ClearCachedTextures(context.device); @@ -774,7 +839,10 @@ int main(int argc, char** argv) { rescanTests(context); viewEvictionTests(context); viewTests(context); + scanTests(context); frameTests(context); + shrinkTests(context); + secondUseTests(context); earlyMissTests(context); collidingKeyTests(context); ClearCachedTextures(context.device); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index b1117bd00d..eaad564f98 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -55,6 +55,7 @@ - [Workgroup count split](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (libSceAgcDriver) - a direct dispatch over `maxComputeWorkGroupCount` runs as `vkCmdDispatch` chunks, each bound to a pipeline variant whose workgroup offset specialization constants add the chunk's base to `WorkgroupId` and `GlobalInvocationId`. A variant is built when its chunk base is first recorded, not when the dispatch is prepared. A split of a shader with device-memory LDS throws: its slot base reads the chunk's `NumWorkgroups`, not the dispatch's. Chunks are ordered by a compute barrier, which the console does not have between workgroups of one dispatch. - [Texture cache budgets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - the storage image cache's budget is what the reported heap budget leaves after the heap's use other than the cached storage images, the unused part of the sampled textures' 2 GiB floor and an eighth of the budget, capped at a quarter of the budget and at `TextureCacheBudget` (a quarter of the heap's size, at least 2 GiB), never below 512 MiB; the sampled cache's budget reserves `TextureCacheBudget` for the storage cache. So the sampled budget falls first while the sampled textures alive stay within their floor, and both fall together above it; each cache trims only at its next insert, a storage image is allocated before its insert reads the budget, and both budgets read the heap at most once a second, so a burst between two reads can still exceed the reported budget. The 2 GiB floor and the eighth are the sampled formula's values reused and the 512 MiB floor and the caps the earlier ones; none was measured beyond one title (PPSA11386) on one 10 GB GPU, and a title whose storage working set exceeds the shrunk budget re-creates storage images at each use. +- [Scan-resistant sampled texture admission](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - while the sampled texture cache is over its budget but within a quarter over it, the first use of a texture it does not hold in a present is refused (used for its draw, then released, never cached) when its least recently used entry was used in the current or previous present, and a later use in the same present is refused when that entry was used in the current present. So a working set larger than the budget keeps a stable subset: a texture used more than once in a present still evicts an entry not yet used in that present, so a working set with interleaved multi-use textures rotates as on main, and the textures that arrive after the cache fills are created again at each use for as long as the least recently used entry is from the current present, including in the transition present after a scene change. Past a quarter over the budget the entries are evicted as before, so after the budget falls the cache can sit at up to 125% of the reduced budget until entries stop being used. Entries unused for two presents are evicted as before. A later use can evict an entry from the previous present and then be refused when the next least recently used entry is from the current present, so that entry is created again in the next present. The "current or previous present" window is an unmeasured threshold: it was chosen, not derived. Measured once, as one A-B-A series on PPSA26344 (Ghost of Yotei, loading phase about 33000 textures per frame against a 5.4 GiB budget) on an RTX 4090, 3600 s each: 932 / 968 / 941 frames, evictions after frame 620 2.72M -> 1.17M, texture creations unchanged (about 5.8M). Nothing is measured on other titles or GPUs, and a title whose per-present working set only slightly exceeds the budget now re-creates the excess textures at every use. - [Polygon primitive type](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - `VGT_PRIMITIVE_TYPE` 21 (`PrimitiveType::Polygon`) decodes to `VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN` and is treated as inactive for vertex point size, like triangle fans and strips. PPSA01317 (Gran Turismo 7) submits polygon draws; whether the console performs polygon-mode decomposition or custom vertex routing is unverified. - [Native multisampled DCC fast clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) import uniform 0000/0001/1110/1111 keys into a single-layer, level-zero SW_64KB_R_X color image with 2, 4 or 8 fragments. Color and DCC/CMASK ranges must stay write-watched and unchanged; expanded or mixed initial DCC, register CMASK clears, compressed payloads, color transforms, lossy modes, metadata changes, guest publication and writes over a live owner throw. Driver-mediated reads of the color and metadata stay guarded. The metadata extent follows [AMD Addrlib](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp#L347-L454) with the [PS5 Mesa model](https://github.com/mihawk-99/PS5_Mesa/blob/0b2d6d1a61d9bbf89cf8beb88a696144f67c61f8/src/amd/vulkan/winsys/ps5/radv_ps5_winsys.c): GFX10.1, 16 pipes, 256-byte interleave, two maximum compressed fragments. Console geometry, concurrent guest mutation and transitions outside this subset are unverified. From 8706cd8a7dc7e7bfc57735710942df1e051d0403 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Mon, 19 Oct 2026 18:44:04 +0200 Subject: [PATCH 112/269] docs(agc): keep the title name out of the scan-resistant admission debt entry --- 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 eaad564f98..c99aacc332 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -55,7 +55,7 @@ - [Workgroup count split](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (libSceAgcDriver) - a direct dispatch over `maxComputeWorkGroupCount` runs as `vkCmdDispatch` chunks, each bound to a pipeline variant whose workgroup offset specialization constants add the chunk's base to `WorkgroupId` and `GlobalInvocationId`. A variant is built when its chunk base is first recorded, not when the dispatch is prepared. A split of a shader with device-memory LDS throws: its slot base reads the chunk's `NumWorkgroups`, not the dispatch's. Chunks are ordered by a compute barrier, which the console does not have between workgroups of one dispatch. - [Texture cache budgets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - the storage image cache's budget is what the reported heap budget leaves after the heap's use other than the cached storage images, the unused part of the sampled textures' 2 GiB floor and an eighth of the budget, capped at a quarter of the budget and at `TextureCacheBudget` (a quarter of the heap's size, at least 2 GiB), never below 512 MiB; the sampled cache's budget reserves `TextureCacheBudget` for the storage cache. So the sampled budget falls first while the sampled textures alive stay within their floor, and both fall together above it; each cache trims only at its next insert, a storage image is allocated before its insert reads the budget, and both budgets read the heap at most once a second, so a burst between two reads can still exceed the reported budget. The 2 GiB floor and the eighth are the sampled formula's values reused and the 512 MiB floor and the caps the earlier ones; none was measured beyond one title (PPSA11386) on one 10 GB GPU, and a title whose storage working set exceeds the shrunk budget re-creates storage images at each use. -- [Scan-resistant sampled texture admission](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - while the sampled texture cache is over its budget but within a quarter over it, the first use of a texture it does not hold in a present is refused (used for its draw, then released, never cached) when its least recently used entry was used in the current or previous present, and a later use in the same present is refused when that entry was used in the current present. So a working set larger than the budget keeps a stable subset: a texture used more than once in a present still evicts an entry not yet used in that present, so a working set with interleaved multi-use textures rotates as on main, and the textures that arrive after the cache fills are created again at each use for as long as the least recently used entry is from the current present, including in the transition present after a scene change. Past a quarter over the budget the entries are evicted as before, so after the budget falls the cache can sit at up to 125% of the reduced budget until entries stop being used. Entries unused for two presents are evicted as before. A later use can evict an entry from the previous present and then be refused when the next least recently used entry is from the current present, so that entry is created again in the next present. The "current or previous present" window is an unmeasured threshold: it was chosen, not derived. Measured once, as one A-B-A series on PPSA26344 (Ghost of Yotei, loading phase about 33000 textures per frame against a 5.4 GiB budget) on an RTX 4090, 3600 s each: 932 / 968 / 941 frames, evictions after frame 620 2.72M -> 1.17M, texture creations unchanged (about 5.8M). Nothing is measured on other titles or GPUs, and a title whose per-present working set only slightly exceeds the budget now re-creates the excess textures at every use. +- [Scan-resistant sampled texture admission](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - while the sampled texture cache is over its budget but within a quarter over it, the first use of a texture it does not hold in a present is refused (used for its draw, then released, never cached) when its least recently used entry was used in the current or previous present, and a later use in the same present is refused when that entry was used in the current present. So a working set larger than the budget keeps a stable subset: a texture used more than once in a present still evicts an entry not yet used in that present, so a working set with interleaved multi-use textures rotates as on main, and the textures that arrive after the cache fills are created again at each use for as long as the least recently used entry is from the current present, including in the transition present after a scene change. Past a quarter over the budget the entries are evicted as before, so after the budget falls the cache can sit at up to 125% of the reduced budget until entries stop being used. Entries unused for two presents are evicted as before. A later use can evict an entry from the previous present and then be refused when the next least recently used entry is from the current present, so that entry is created again in the next present. The "current or previous present" window is an unmeasured threshold: it was chosen, not derived. Measured once, as one A-B-A series on one title (loading phase about 33000 textures per frame against a 5.4 GiB budget) on one RTX 4090, 3600 s each: 932 / 968 / 941 frames, evictions after frame 620 2.72M -> 1.17M, texture creations unchanged (about 5.8M). Nothing is measured on other titles or GPUs, and a title whose per-present working set only slightly exceeds the budget now re-creates the excess textures at every use. - [Polygon primitive type](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - `VGT_PRIMITIVE_TYPE` 21 (`PrimitiveType::Polygon`) decodes to `VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN` and is treated as inactive for vertex point size, like triangle fans and strips. PPSA01317 (Gran Turismo 7) submits polygon draws; whether the console performs polygon-mode decomposition or custom vertex routing is unverified. - [Native multisampled DCC fast clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) import uniform 0000/0001/1110/1111 keys into a single-layer, level-zero SW_64KB_R_X color image with 2, 4 or 8 fragments. Color and DCC/CMASK ranges must stay write-watched and unchanged; expanded or mixed initial DCC, register CMASK clears, compressed payloads, color transforms, lossy modes, metadata changes, guest publication and writes over a live owner throw. Driver-mediated reads of the color and metadata stay guarded. The metadata extent follows [AMD Addrlib](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp#L347-L454) with the [PS5 Mesa model](https://github.com/mihawk-99/PS5_Mesa/blob/0b2d6d1a61d9bbf89cf8beb88a696144f67c61f8/src/amd/vulkan/winsys/ps5/radv_ps5_winsys.c): GFX10.1, 16 pipes, 256-byte interleave, two maximum compressed fragments. Console geometry, concurrent guest mutation and transitions outside this subset are unverified. From e229e3bd47615d83cb9241c463421371d99104a5 Mon Sep 17 00:00:00 2001 From: emirplus Date: Mon, 19 Oct 2026 19:52:03 +0300 Subject: [PATCH 113/269] fix(libc): validate time ranges and null-terminate in asctime_s and ctime_s --- core/libs/prx/libc/include/SonyCrt.hpp | 28 +++++++++++++++++++++----- core/libs/tests/GuestCrt.cpp | 17 +++++++++++++--- 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libc/include/SonyCrt.hpp b/core/libs/prx/libc/include/SonyCrt.hpp index 2790a06d04..cf1879e2c0 100644 --- a/core/libs/prx/libc/include/SonyCrt.hpp +++ b/core/libs/prx/libc/include/SonyCrt.hpp @@ -88,21 +88,39 @@ inline int SonyFormatAsctime(char* buffer, std::size_t size, const GuestTm& time static const char* const days[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; static const char* const months[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; if (!buffer || size < 26) return EINVAL; - if (time.tm_wday < 0 || time.tm_wday > 6 || time.tm_mon < 0 || time.tm_mon > 11) return EINVAL; + const int year = 1900 + time.tm_year; + if (time.tm_wday < 0 || time.tm_wday > 6 || time.tm_mon < 0 || time.tm_mon > 11 || + time.tm_mday < 1 || time.tm_mday > 31 || time.tm_hour < 0 || time.tm_hour > 23 || + time.tm_min < 0 || time.tm_min > 59 || time.tm_sec < 0 || time.tm_sec > 60 || + year < 0 || year > 9999) { + buffer[0] = '\0'; + return EINVAL; + } std::snprintf(buffer, size, "%.3s %.3s %2d %.2d:%.2d:%.2d %4d\n", days[time.tm_wday], months[time.tm_mon], - time.tm_mday, time.tm_hour, time.tm_min, time.tm_sec, 1900 + time.tm_year); + time.tm_mday, time.tm_hour, time.tm_min, time.tm_sec, year); return 0; } inline int SonyCtimeS(char* buffer, std::size_t size, const std::int64_t* time) { - if (!buffer || size < 26 || !time) return EINVAL; + if (!buffer || size < 26) return EINVAL; + if (!time) { + buffer[0] = '\0'; + return EINVAL; + } GuestTm parts{}; - if (!localtime_s_nid_postfix(time, &parts)) return EINVAL; + if (!localtime_s_nid_postfix(time, &parts)) { + buffer[0] = '\0'; + return EINVAL; + } return SonyFormatAsctime(buffer, size, parts); } inline int SonyAsctimeS(char* buffer, std::size_t size, const GuestTm* time) { - if (!buffer || size < 26 || !time) return EINVAL; + if (!buffer || size < 26) return EINVAL; + if (!time) { + buffer[0] = '\0'; + return EINVAL; + } return SonyFormatAsctime(buffer, size, *time); } diff --git a/core/libs/tests/GuestCrt.cpp b/core/libs/tests/GuestCrt.cpp index df087488e3..d2fe45d744 100644 --- a/core/libs/tests/GuestCrt.cpp +++ b/core/libs/tests/GuestCrt.cpp @@ -189,10 +189,20 @@ int main() { Require(asctime_s_nid_postfix(text, sizeof(text), &known) == 0); Require(std::strcmp(text, "Thu Jan 1 00:00:00 1970\n") == 0); known.tm_wday = 7; - Require(sceAmprAsctimeS(text, sizeof(text), &known) == EINVAL); + text[0] = 'x'; + Require(sceAmprAsctimeS(text, sizeof(text), &known) == EINVAL && text[0] == '\0'); known.tm_wday = 4; + known.tm_year = 9000; + text[0] = 'x'; + Require(sceAmprAsctimeS(text, sizeof(text), &known) == EINVAL && text[0] == '\0'); + known.tm_year = 70; + known.tm_mday = 32; + text[0] = 'x'; + Require(sceAmprAsctimeS(text, sizeof(text), &known) == EINVAL && text[0] == '\0'); + known.tm_mday = 1; Require(sceAmprAsctimeS(nullptr, sizeof(text), &known) == EINVAL); - Require(sceAmprAsctimeS(text, sizeof(text), nullptr) == EINVAL); + text[0] = 'x'; + Require(sceAmprAsctimeS(text, sizeof(text), nullptr) == EINVAL && text[0] == '\0'); Require(sceAmprAsctimeS(text, 25, &known) == EINVAL); const std::int64_t epoch = 0; Require(sceAmprCtimeS(text, sizeof(text), &epoch) == 0); @@ -201,7 +211,8 @@ int main() { Require(ctime_s_nid_postfix(libcText, sizeof(libcText), &epoch) == 0); Require(std::strcmp(libcText, text) == 0); Require(sceAmprCtimeS(text, 25, &epoch) == EINVAL); - Require(sceAmprCtimeS(text, sizeof(text), nullptr) == EINVAL); + text[0] = 'x'; + Require(sceAmprCtimeS(text, sizeof(text), nullptr) == EINVAL && text[0] == '\0'); char line[] = "a,b,,c"; char* context = nullptr; From 3440a1756fb7b7237775cda5a64c3628b996e086 Mon Sep 17 00:00:00 2001 From: davidamsellem Date: Mon, 19 Oct 2026 18:54:37 +0200 Subject: [PATCH 114/269] test(agc): skip vouched lookups where host imports report GPU reads as writes On RADV, importing host memory and reading it from the GPU marks the pages as written in the write watch. The test forces APS5_WRITE_WATCH_IMPORTS=watch, so the vouch refuses the table and supplyWords is never called. Probe host-import write protection on Linux, as the Windows path already does, and skip when a GPU read reports writes or the probe fails. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../tests/execution/ImageTableCacheBounds.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTableCacheBounds.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTableCacheBounds.cpp index 3e16e1068a..765947dc99 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTableCacheBounds.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTableCacheBounds.cpp @@ -358,6 +358,15 @@ bool WindowsImportsStayWatchedThroughReads(const AgcDriver::Graphics::Context& c } return false; } +#else +bool ImportsStayWatchedThroughReads(const AgcDriver::Graphics::Context& context) { + if (context.hostImportAlignment == 0) return true; + const auto probe = AgcDriver::Graphics::ProbeImportWriteProtection(context); + if (probe.failure == nullptr && probe.writtenAfterSubmit == 0) return true; + if (probe.failure != nullptr) std::printf("skipped, the device reports no host import probe result: it failed at %s (%d)\n", probe.failure, static_cast(probe.result)); + else std::printf("skipped, the device reports %u of %u host-imported pages written after a GPU read, so a watched imported table cannot be vouched for\n", probe.writtenAfterSubmit, probe.pages); + return false; +} #endif } @@ -371,6 +380,8 @@ int main(int argc, char** argv) { #ifdef _WIN32 if (argc == 2 && std::string(argv[1]) == "--vouched-lookups" && !WindowsImportsStayWatchedThroughReads(device->graphicsContext())) return VulkanTestSkipped; +#else + if (argc == 2 && std::string(argv[1]) == "--vouched-lookups" && !ImportsStayWatchedThroughReads(device->graphicsContext())) return VulkanTestSkipped; #endif } GuestBlock block; From 48777166735ea6ce3cc347d4402083c48096e2d7 Mon Sep 17 00:00:00 2001 From: Federico Marras Date: Mon, 19 Oct 2026 13:18:54 +0200 Subject: [PATCH 115/269] test(libSceNet): accept every unusable IPv6 loopback error in guest_sce_net guest_sce_net skips its IPv6 checks when sending to ::1 fails and HostIpv6LoopbackUnavailable confirms the host cannot reach it. The host probe accepts EADDRNOTAVAIL, ENETUNREACH and EHOSTUNREACH, but the guest check accepted only ENETUNREACH (51). On a Linux host without ::1 the send can fail with EADDRNOTAVAIL instead, which the guest sees as 49, and the test aborted in CheckUnspecifiedIpv6 instead of skipping. Accept the guest numbers of the same three errors (49, 51 and 65). --- core/libs/tests/GuestSceNet.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index 76d5b63109..ee08772e35 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -1014,7 +1014,7 @@ static void CheckUnspecifiedIpv6() { const char payload[] = "IPv6 wildcard receive"; const auto sent = sceNetSendto(sender, payload, sizeof(payload), 0, address.data(), address.size()); - if (sent < 0 && *sceNetErrnoLoc() == 51 && HostIpv6LoopbackUnavailable()) { + if (sent < 0 && (*sceNetErrnoLoc() == 49 || *sceNetErrnoLoc() == 51 || *sceNetErrnoLoc() == 65) && HostIpv6LoopbackUnavailable()) { Require(sceNetSocketClose(sender) == 0); Require(sceNetSocketClose(receiver) == 0); ipv6Unavailable = true; From 323deb832a0dfebf7cb86d577f35ae4517802077 Mon Sep 17 00:00:00 2001 From: Federico Marras Date: Mon, 19 Oct 2026 16:06:01 +0200 Subject: [PATCH 116/269] feat(audioin): implement sceAudioInGetGain and sceAudioInGetRerouteCount Write the unattenuated linear gain 1.0f into *gain before checking the port handle in sceAudioInGetGain, and return 0 for open ports or SCE_AUDIO_IN_ERROR_INVALID_HANDLE (0x80260101) for closed or out-of-range handles in both sceAudioInGetGain and sceAudioInGetRerouteCount. --- core/libs/prx/libSceAudioIn/Export.cpp | 14 +++++------ core/libs/tests/GuestAudioIn.cpp | 35 +++++++++++++++++++++++--- docs/dev/debt/libSceAudioIn.md | 1 + 3 files changed, 39 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libSceAudioIn/Export.cpp b/core/libs/prx/libSceAudioIn/Export.cpp index c21557a76f..a5a107c931 100644 --- a/core/libs/prx/libSceAudioIn/Export.cpp +++ b/core/libs/prx/libSceAudioIn/Export.cpp @@ -9,6 +9,7 @@ #include #include #include +#include namespace { @@ -196,16 +197,15 @@ int32_t APS5_VABI sceAudioInIsPadEmbeddedMic(int32_t handle) { } int32_t APS5_VABI sceAudioInGetGain(int32_t handle, float* gain) { - (void)handle; - (void)gain; - NotImplemented_nid_no_patch(__func__); - return 0; + if (!gain) throw std::runtime_error("sceAudioInGetGain: null gain"); + *gain = 1.0f; + std::lock_guard lock(g_mutex); + return find(handle) ? 0 : AUDIO_IN_ERROR_INVALID_HANDLE; } int32_t APS5_VABI sceAudioInGetRerouteCount(int32_t handle) { - (void)handle; - NotImplemented_nid_no_patch(__func__); - return 0; + 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 7dc3e6ff92..124bc9a880 100644 --- a/core/libs/tests/GuestAudioIn.cpp +++ b/core/libs/tests/GuestAudioIn.cpp @@ -17,6 +17,8 @@ int APS5_VABI sceAudioInOpen(int, std::uint32_t, std::uint32_t, std::uint32_t, s int APS5_VABI sceAudioInHqOpen(int, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t); int APS5_VABI sceAudioInAsyncOpen(int, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t); int32_t APS5_VABI sceAudioInIsPadEmbeddedMic(int32_t); +int32_t APS5_VABI sceAudioInGetGain(int32_t, float*); +int32_t APS5_VABI sceAudioInGetRerouteCount(int32_t); int APS5_VABI sceAudioInInput(int, void*); int APS5_VABI sceAudioInGetSilentState(int); int APS5_VABI sceAudioInClose(int); @@ -34,6 +36,16 @@ static bool ReportsNotImplemented(const char* name, TCall call) { return false; } +template +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()); @@ -47,6 +59,7 @@ namespace { constexpr int user = 0x10000000; void TestValidation() { + constexpr int invalidHandle = static_cast(0x80260101); constexpr int invalidSize = static_cast(0x80260102); constexpr int invalidFreq = static_cast(0x80260103); constexpr int invalidType = static_cast(0x80260104); @@ -70,14 +83,28 @@ void TestValidation() { Require(sceAudioInAsyncOpen(user, 0, 0, 256, 48000, 0) == invalidParam); 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); })); + } int port = sceAudioInAsyncOpen(user, 0, 0, 384, 48000, 2); - Require(port > 0 && sceAudioInClose(port) == 0); + float gain = 0.0f; + Require(port > 0 && sceAudioInGetGain(port, &gain) == 0 && gain == 1.0f && sceAudioInGetRerouteCount(port) == 0); + Require(ThrowsRuntimeError([port] { sceAudioInGetGain(port, nullptr); })); + 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); - Require(port > 0 && sceAudioInClose(port) == 0); + 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); - Require(port > 0 && sceAudioInClose(port) == 0); + 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); - Require(port > 0 && sceAudioInClose(port) == 0); + gain = 0.5f; + Require(port > 0 && sceAudioInGetGain(port, &gain) == 0 && gain == 1.0f && sceAudioInGetRerouteCount(port) == 0 && sceAudioInClose(port) == 0); Require(ReportsNotImplemented("sceAudioInIsPadEmbeddedMic", [] { sceAudioInIsPadEmbeddedMic(1); })); } diff --git a/docs/dev/debt/libSceAudioIn.md b/docs/dev/debt/libSceAudioIn.md index 99e0629243..63b2d8b02c 100644 --- a/docs/dev/debt/libSceAudioIn.md +++ b/docs/dev/debt/libSceAudioIn.md @@ -11,3 +11,4 @@ ### 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. From 87bd71d64c4a95124761e1e740e5596658d7c3ac Mon Sep 17 00:00:00 2001 From: Federico Marras Date: Mon, 19 Oct 2026 17:10:43 +0200 Subject: [PATCH 117/269] feat(libSceNet): reverse-resolve IPv6 addresses in sceNetResolverStartAton6 --- core/libs/prx/libSceNet/Export.cpp | 41 +++++++++++++++---------- core/libs/tests/GuestSceNet.cpp | 49 ++++++++++++++++++++++++++++++ docs/dev/debt/libSceNet.md | 2 +- 3 files changed, 75 insertions(+), 17 deletions(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 945f71079c..1561be8c26 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -1717,23 +1717,15 @@ int APS5_VABI sceNetResolverStartNtoaMultipleRecordsEx(int rid, const char* host return lookup_records(rid, hostname, info, flags, __func__); } -int APS5_VABI sceNetResolverStartAton(int rid, const void* addr, char* hostname, int len, int timeout, int retry, int flags) { - (void)timeout; - (void)retry; - (void)flags; - if (!addr || !hostname || len <= 0) return fail(NET_EINVAL); +int reverse_lookup(int rid, const sockaddr* address, NativeLength addressLength, char* hostname, int len, const char* func) { if (!initialize_sockets()) return fail(5); std::uint64_t generation = 0; if (const int error = begin_lookup(rid, &Resolver::abort_aton, generation)) return error; - sockaddr_in address{}; - address.sin_family = AF_INET; - std::memcpy(&address.sin_addr, addr, sizeof(address.sin_addr)); std::vector name(static_cast(len)); - const int result = ::getnameinfo(reinterpret_cast(&address), sizeof(address), name.data(), - static_cast(len), nullptr, 0, NI_NAMEREQD); + const int result = ::getnameinfo(address, addressLength, name.data(), static_cast(len), nullptr, 0, NI_NAMEREQD); if (result != 0) { set_errno(result == EAI_AGAIN ? NET_ETIMEDOUT : NET_ENOENT); - log_soft(__func__, "host reverse DNS lookup failed"); + log_soft(func, "host reverse DNS lookup failed"); return end_lookup(rid, generation, NET_ERROR_RESOLVER_ENODNS); } if (const int error = end_lookup(rid, generation, 0)) return error; @@ -1741,6 +1733,28 @@ int APS5_VABI sceNetResolverStartAton(int rid, const void* addr, char* hostname, return 0; } +int APS5_VABI sceNetResolverStartAton(int rid, const void* addr, char* hostname, int len, int timeout, int retry, int flags) { + (void)timeout; + (void)retry; + (void)flags; + if (!addr || !hostname || len <= 0) return fail(NET_EINVAL); + sockaddr_in address{}; + address.sin_family = AF_INET; + std::memcpy(&address.sin_addr, addr, sizeof(address.sin_addr)); + return reverse_lookup(rid, reinterpret_cast(&address), sizeof(address), hostname, len, __func__); +} + +int APS5_VABI sceNetResolverStartAton6(int rid, const void* addr, char* hostname, int len, int timeout, int retry, int flags) { + (void)timeout; + (void)retry; + if (flags != 0) throw std::runtime_error("sceNetResolverStartAton6: flags " + std::to_string(flags) + " are not supported"); + if (!addr || !hostname || len <= 0) return fail(NET_EINVAL); + sockaddr_in6 address{}; + address.sin6_family = AF_INET6; + std::memcpy(&address.sin6_addr, addr, sizeof(address.sin6_addr)); + return reverse_lookup(rid, reinterpret_cast(&address), sizeof(address), hostname, len, __func__); +} + int APS5_VABI sceNetResolverGetError(int rid, int* status) { if (!status) return fail(NET_EINVAL); std::lock_guard lk(g_mutex); @@ -1818,11 +1832,6 @@ int APS5_VABI sceNetShowRouteWithMemory(void) { return 0; } -int APS5_VABI sceNetResolverStartAton6(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNetResolverConnectAbort(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index 76d5b63109..91044dbdc2 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -5,6 +5,7 @@ #include #else #include +#include #include #include #include @@ -64,6 +65,7 @@ int APS5_VABI sceNetResolverStartNtoa6(int, const char*, void*, int, int, int); int APS5_VABI sceNetResolverStartNtoaMultipleRecordsEx(int, const char*, void*, int, int, int); int APS5_VABI sceNetResolverStartNtoaMultipleRecords(int, const char*, void*, int, int, int); int APS5_VABI sceNetResolverStartAton(int, const void*, char*, int, int, int, int); +int APS5_VABI sceNetResolverStartAton6(int, const void*, char*, int, int, int, int); int APS5_VABI sceNetResolverAbort(int, int); int APS5_VABI sceNetResolverDestroy(int); int APS5_VABI sceNetResolverGetError(int, int*); @@ -1524,6 +1526,53 @@ int main() { } Require(sceNetResolverStartNtoa(resolver, "localhost", ipv4.data(), 5000000, 1, 0) == 0); + std::array loopback6{}; + loopback6[15] = 1; + std::memset(reverse, 0x5A, sizeof(reverse)); + const int reverse6_result = sceNetResolverStartAton6(resolver, loopback6.data(), reverse, sizeof(reverse), 5000000, 1, 0); + Require(reverse6_result == 0 || reverse6_result == static_cast(0x804101E1)); + Require(sceNetResolverGetError(resolver, &resolver_error) == 0 && resolver_error == reverse6_result); + char host_reverse6[256]{}; + sockaddr_in6 host_loopback6{}; + host_loopback6.sin6_family = AF_INET6; + std::memcpy(&host_loopback6.sin6_addr, loopback6.data(), loopback6.size()); + const bool host_named6 = ::getnameinfo(reinterpret_cast(&host_loopback6), sizeof(host_loopback6), + host_reverse6, sizeof(host_reverse6), nullptr, 0, NI_NAMEREQD) == 0; + Require(host_named6 == (reverse6_result == 0)); + if (reverse6_result == 0) { + Require(std::strcmp(reverse, host_reverse6) == 0); + const auto name_length = static_cast(std::strlen(host_reverse6)); + char exact[256]; + std::memset(exact, 0x5A, sizeof(exact)); + Require(sceNetResolverStartAton6(resolver, loopback6.data(), exact, name_length + 1, 5000000, 1, 0) == 0 && + std::strcmp(exact, host_reverse6) == 0 && static_cast(exact[name_length + 1]) == 0x5A); + std::memset(exact, 0x5A, sizeof(exact)); + Require(sceNetResolverStartAton6(resolver, loopback6.data(), exact, name_length, 5000000, 1, 0) == + static_cast(0x804101E1) && static_cast(exact[0]) == 0x5A); + } else { + Require(static_cast(reverse[0]) == 0x5A); + } + std::memset(reverse, 0x5A, sizeof(reverse)); + Require(sceNetResolverAbort(resolver, 2) == 0); + Require(sceNetResolverStartAton6(resolver, loopback6.data(), reverse, sizeof(reverse), 5000000, 1, 0) == + static_cast(0x80410104) && *sceNetErrnoLoc() == 4 && static_cast(reverse[0]) == 0x5A); + Require(sceNetResolverGetError(resolver, &resolver_error) == 0 && resolver_error == static_cast(0x80410104)); + Require(sceNetResolverAbort(resolver, 1) == 0); + Require(sceNetResolverStartAton6(resolver, loopback6.data(), reverse, sizeof(reverse), 5000000, 1, 0) == reverse6_result); + Require(sceNetResolverStartNtoa(resolver, "localhost", ipv4.data(), 5000000, 1, 0) == static_cast(0x80410104)); + Require(sceNetResolverStartAton6(resolver, nullptr, reverse, sizeof(reverse), 5000000, 1, 0) == static_cast(0x80410116) && + *sceNetErrnoLoc() == 22); + Require(sceNetResolverStartAton6(resolver, loopback6.data(), nullptr, sizeof(reverse), 5000000, 1, 0) == static_cast(0x80410116)); + Require(sceNetResolverStartAton6(resolver, loopback6.data(), reverse, 0, 5000000, 1, 0) == static_cast(0x80410116)); + Require(sceNetResolverStartAton6(resolver + 1000, loopback6.data(), reverse, sizeof(reverse), 5000000, 1, 0) == + static_cast(0x80410109) && *sceNetErrnoLoc() == 9); + for (const int flags : {0x1, 0x10000, -1}) { + bool threw = false; + try { sceNetResolverStartAton6(resolver, loopback6.data(), reverse, sizeof(reverse), 5000000, 1, flags); } + catch (const std::runtime_error&) { threw = true; } + Require(threw); + } + const int other = sceNetResolverCreate("guest-sce-net-other", 0, 0); Require(other >= 0 && other != resolver); Require(sceNetResolverAbort(other, 3) == 0); diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index a0947b7725..63319896ba 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -16,7 +16,6 @@ - [sceNetShowPolicyWithMemory](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetShowRoute](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetShowRouteWithMemory](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetResolverStartAton6](../../../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 @@ -180,6 +179,7 @@ ### Functional +- [sceNetResolverStartAton6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) reverse-resolves a 16-byte IPv6 address through the host resolver (`getnameinfo` with `NI_NAMEREQD`) into the caller's buffer of `len` bytes, as `sceNetResolverStartAton` does for IPv4. The `(rid, addr, hostname, len, timeout, retry, flags)` signature comes from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs#L992-L997). An address with no name, or a name longer than `len - 1` bytes, fails with `RESOLVER_ENODNS` (`0x804101E1`) and leaves the buffer untouched; whether the console truncates instead is not verified. Abort flag 2 (ATON preservation) fails the next call with `EINTR` as it does `sceNetResolverStartAton`. Timeout and retry are ignored, and any nonzero flags throw because their meaning is unknown - [sceNetAccept](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) inherits the listener's non-blocking mode through the existing `SO_NBIO` setter, as the libkernel socket path does and FreeBSD's [`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L571) copies the socket state. The mode is sampled when accept starts; changes between connection creation and accept are not modelled. Not measured on a console. - [sceNetConnect to AF_UNSPEC](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) dissolves a datagram association as FreeBSD 11.4's `udp_disconnect()` does, which resets the local address to the wildcard address and keeps the port. On Linux a socket bound to an explicit port keeps its local address as well (`127.0.0.1:` stays `127.0.0.1:`), because a bound socket cannot be bound again; one bound to port 0 is bound again to the wildcard address with the port it had From 792fb27631ccf27dc33c6e49b7b75cd2cbcf4e71 Mon Sep 17 00:00:00 2001 From: Federico Marras Date: Mon, 19 Oct 2026 13:28:55 +0200 Subject: [PATCH 118/269] feat(libc): list each interface's AF_LINK entry in getifaddrs on Linux getifaddrs listed only the host's IPv4 and IPv6 addresses, so the per-interface AF_LINK entries FreeBSD's getifaddrs puts first were missing, and with them the interface index, hardware address, MTU and counters and every interface without an address. On Linux each interface now starts with an AF_LINK entry, followed by its addresses, as FreeBSD 11.4's getifaddrs orders them. Its address is a sockaddr_dl with the host index, type, name and hardware address (none for a loopback), sized as FreeBSD's if_attach sizes it. Its ifa_data is a 152-byte FreeBSD if_data filled as if_data_copy fills it: type, address and header length, link state, MTU, speed and the host's packet, byte, error, drop and multicast counters from sysfs, or the host list's 32-bit counters when sysfs cannot be read. Fields with no host equivalent are 0 and listed in the debt entry. Windows is unchanged. --- core/libs/prx/libc/src/Interfaces.cpp | 140 +++++++++++++++++++++++++- core/libs/tests/GuestInterfaces.cpp | 132 +++++++++++++++++++++++- docs/dev/debt/libc.md | 2 +- 3 files changed, 265 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libc/src/Interfaces.cpp b/core/libs/prx/libc/src/Interfaces.cpp index f61e17fbd4..8e39cfa29b 100644 --- a/core/libs/prx/libc/src/Interfaces.cpp +++ b/core/libs/prx/libc/src/Interfaces.cpp @@ -5,9 +5,17 @@ #include #else #include +#include #include +#include #include +#include +#include +#include +#include +#include #include +#include #endif #include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libc/include/general/VabiMacros.hpp" @@ -15,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -52,6 +61,7 @@ struct Entry { std::vector address; std::vector netmask; std::vector destination; + std::vector data; }; std::vector GuestIpv4(const std::uint8_t* bytes) { @@ -150,6 +160,100 @@ int HostEntries(std::vector& entries) { return 0; } #else +constexpr std::uint8_t guestLinkFamily = 18; +constexpr std::size_t guestLinkNameArea = 8 + 16; +constexpr std::size_t guestLinkMinimum = 54; +constexpr std::uint8_t guestTypeOther = 0x1; +constexpr std::uint8_t guestTypeEthernet = 0x6; +constexpr std::uint8_t guestTypePpp = 0x17; +constexpr std::uint8_t guestTypeLoopback = 0x18; +constexpr std::uint8_t guestLinkStateDown = 1; +constexpr std::uint8_t guestLinkStateUp = 2; +constexpr std::size_t guestInterfaceDataSize = 152; +constexpr unsigned int hostLowerUp = 0x10000; + +struct HostInterface { + std::string name; + bool linked = false; + Entry link; + std::vector addresses; +}; + +template +void Store(std::vector& bytes, std::size_t offset, TValue value) { + std::memcpy(bytes.data() + offset, &value, sizeof(value)); +} + +std::vector GuestLinkAddress(const std::string& name, std::uint16_t index, std::uint8_t type, const std::uint8_t* hardware, std::size_t hardwareLength) { + std::size_t size = std::max(guestLinkMinimum, guestLinkNameArea + hardwareLength); + size = Aligned(size); + if (size > 0xFF) throw std::runtime_error("getifaddrs: link-level address of " + name + " does not fit sockaddr_dl"); + std::vector guest(size, 0); + guest[0] = static_cast(size); + guest[1] = guestLinkFamily; + Store(guest, 2, index); + guest[4] = type; + guest[5] = static_cast(name.size()); + guest[6] = static_cast(hardwareLength); + std::memcpy(guest.data() + 8, name.data(), name.size()); + if (hardwareLength != 0) std::memcpy(guest.data() + 8 + name.size(), hardware, hardwareLength); + return guest; +} + +std::optional ReadSysfsCounter(const std::string& name, const char* file) { + std::ifstream input("/sys/class/net/" + name + "/" + file); + long long value = 0; + if (!(input >> value)) return std::nullopt; + return static_cast(value); +} + +std::uint32_t HostMtu(const std::string& name) { + const int probe = ::socket(AF_INET, SOCK_DGRAM, 0); + if (probe < 0) throw std::runtime_error(std::string("getifaddrs: no socket to read the MTU of ") + name + ": " + std::strerror(errno)); + ifreq request{}; + std::memcpy(request.ifr_name, name.c_str(), name.size() + 1); + const int result = ::ioctl(probe, SIOCGIFMTU, &request); + const int error = errno; + ::close(probe); + if (result != 0) throw std::runtime_error(std::string("getifaddrs: SIOCGIFMTU failed on ") + name + ": " + std::strerror(error)); + return static_cast(request.ifr_mtu); +} + +std::vector GuestInterfaceData(const std::string& name, unsigned int hostFlags, const void* hostStats, std::uint8_t type, std::uint8_t hardwareLength) { + std::vector data(guestInterfaceDataSize, 0); + data[0] = type; + data[2] = hardwareLength; + data[3] = type == guestTypeEthernet ? 14 : 0; + if (type == guestTypeLoopback) data[4] = (hostFlags & IFF_UP) ? guestLinkStateUp : guestLinkStateDown; + else data[4] = (hostFlags & hostLowerUp) ? guestLinkStateUp : guestLinkStateDown; + Store(data, 6, static_cast(guestInterfaceDataSize)); + Store(data, 8, HostMtu(name)); + if (const auto speed = ReadSysfsCounter(name, "speed"); speed && static_cast(*speed) > 0) + Store(data, 16, *speed * 1000000u); + constexpr std::pair counters[] = { + {"statistics/rx_packets", 24}, {"statistics/rx_errors", 32}, {"statistics/tx_packets", 40}, {"statistics/tx_errors", 48}, + {"statistics/collisions", 56}, {"statistics/rx_bytes", 64}, {"statistics/tx_bytes", 72}, {"statistics/multicast", 80}, + {"statistics/rx_dropped", 96}, {"statistics/tx_dropped", 104}, + }; + bool complete = true; + for (const auto& [file, offset] : counters) { + const auto value = ReadSysfsCounter(name, file); + if (!value) { complete = false; break; } + Store(data, offset, *value); + } + if (!complete) { + if (!hostStats) throw std::runtime_error("getifaddrs: no counters for " + name + " in sysfs or the host list"); + rtnl_link_stats stats{}; + std::memcpy(&stats, hostStats, sizeof(stats)); + const std::pair fallback[] = { + {stats.rx_packets, 24}, {stats.rx_errors, 32}, {stats.tx_packets, 40}, {stats.tx_errors, 48}, {stats.collisions, 56}, + {stats.rx_bytes, 64}, {stats.tx_bytes, 72}, {stats.multicast, 80}, {stats.rx_dropped, 96}, {stats.tx_dropped, 104}, + }; + for (const auto& [value, offset] : fallback) Store(data, offset, static_cast(value)); + } + return data; +} + std::uint32_t InterfaceFlags(unsigned int host) { constexpr std::pair flags[] = { {IFF_UP, guestUp}, {IFF_BROADCAST, guestBroadcast}, {IFF_LOOPBACK, guestLoopback}, @@ -174,9 +278,31 @@ int HostEntries(std::vector& entries) { default: throw std::runtime_error(std::string("getifaddrs: host enumeration failed: ") + std::strerror(hostError)); } } + std::vector interfaces; + const auto interfaceFor = [&](const std::string& name) -> HostInterface& { + const auto found = std::find_if(interfaces.begin(), interfaces.end(), [&](const HostInterface& candidate) { return candidate.name == name; }); + if (found != interfaces.end()) return *found; + interfaces.push_back({name, false, {}, {}}); + return interfaces.back(); + }; try { for (auto* item = host; item; item = item->ifa_next) { - if (!item->ifa_addr || (item->ifa_addr->sa_family != AF_INET && item->ifa_addr->sa_family != AF_INET6)) continue; + if (!item->ifa_addr) continue; + if (item->ifa_addr->sa_family == AF_PACKET) { + const auto& link = *reinterpret_cast(item->ifa_addr); + HostInterface& owner = interfaceFor(CheckedName(item->ifa_name)); + if (owner.linked) throw std::runtime_error("getifaddrs: two link-level entries for " + owner.name); + if (link.sll_ifindex <= 0 || link.sll_ifindex > 0xFFFF) throw std::runtime_error("getifaddrs: interface index of " + owner.name + " does not fit sockaddr_dl"); + const std::uint8_t type = link.sll_hatype == ARPHRD_ETHER ? guestTypeEthernet : link.sll_hatype == ARPHRD_LOOPBACK ? guestTypeLoopback + : link.sll_hatype == ARPHRD_PPP ? guestTypePpp : guestTypeOther; + const std::uint8_t hardwareLength = type == guestTypeLoopback ? 0 : std::min(link.sll_halen, sizeof(link.sll_addr)); + owner.link = Entry{owner.name, InterfaceFlags(item->ifa_flags), + GuestLinkAddress(owner.name, static_cast(link.sll_ifindex), type, link.sll_addr, hardwareLength), {}, {}, + GuestInterfaceData(owner.name, item->ifa_flags, item->ifa_data, type, hardwareLength)}; + owner.linked = true; + continue; + } + if (item->ifa_addr->sa_family != AF_INET && item->ifa_addr->sa_family != AF_INET6) continue; if (!item->ifa_netmask) throw std::runtime_error(std::string("getifaddrs: host address without a netmask on ") + item->ifa_name); Entry entry{CheckedName(item->ifa_name), InterfaceFlags(item->ifa_flags), GuestAddress(item->ifa_addr), GuestAddress(item->ifa_netmask), {}}; const int family = item->ifa_addr->sa_family; @@ -187,13 +313,17 @@ int HostEntries(std::vector& entries) { entry.destination = item->ifa_broadaddr && item->ifa_broadaddr->sa_family == AF_INET ? GuestAddress(item->ifa_broadaddr) : BroadcastAddress(entry.address, entry.netmask); } - entries.push_back(std::move(entry)); + interfaceFor(entry.name).addresses.push_back(std::move(entry)); } } catch (...) { ::freeifaddrs(host); throw; } ::freeifaddrs(host); + for (auto& owner : interfaces) { + if (owner.linked) entries.push_back(std::move(owner.link)); + for (auto& address : owner.addresses) entries.push_back(std::move(address)); + } return 0; } #endif @@ -212,7 +342,7 @@ int APS5_VABI getifaddrs_nid_postfix(GuestInterfaceAddress** list) { } std::size_t size = entries.size() * sizeof(GuestInterfaceAddress); for (const auto& entry : entries) - size += Aligned(entry.name.size() + 1) + Aligned(entry.address.size()) + Aligned(entry.netmask.size()) + Aligned(entry.destination.size()); + size += Aligned(entry.name.size() + 1) + Aligned(entry.address.size()) + Aligned(entry.netmask.size()) + Aligned(entry.destination.size()) + Aligned(entry.data.size()); auto* block = static_cast(ApplicationHeapAllocate_nid_no_patch(size)); if (!block) { *__error_nid_postfix() = 12; @@ -233,9 +363,9 @@ int APS5_VABI getifaddrs_nid_postfix(GuestInterfaceAddress** list) { item.name = static_cast(place(entry.name.c_str(), entry.name.size() + 1)); item.flags = entry.flags; item.address = place(entry.address.data(), entry.address.size()); - item.netmask = place(entry.netmask.data(), entry.netmask.size()); + item.netmask = entry.netmask.empty() ? nullptr : place(entry.netmask.data(), entry.netmask.size()); item.destination = entry.destination.empty() ? nullptr : place(entry.destination.data(), entry.destination.size()); - item.data = nullptr; + item.data = entry.data.empty() ? nullptr : place(entry.data.data(), entry.data.size()); } *list = items; return 0; diff --git a/core/libs/tests/GuestInterfaces.cpp b/core/libs/tests/GuestInterfaces.cpp index 4a7202cf4e..5b69f12016 100644 --- a/core/libs/tests/GuestInterfaces.cpp +++ b/core/libs/tests/GuestInterfaces.cpp @@ -4,8 +4,17 @@ #include #include #else +#include #include +#include +#include #include +#include +#include +#include +#include +#include +#include #endif #include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libc/include/general/VabiMacros.hpp" @@ -46,6 +55,12 @@ struct HostInterfaceAddress { std::uint8_t address[16]; std::uint8_t netmask[16]; std::uint32_t scope; + char name[16]; + unsigned int index; + std::uint8_t type; + std::uint8_t hardwareLength; + std::uint8_t hardware[8]; + bool loopbackUp; }; static HostInterfaceAddress HostAddress(const sockaddr* address) { @@ -61,6 +76,43 @@ static HostInterfaceAddress HostAddress(const sockaddr* address) { return entry; } +#ifndef _WIN32 +static std::uint64_t SysfsCounter(const char* name, const char* file) { + std::ifstream input(std::string("/sys/class/net/") + name + "/statistics/" + file); + unsigned long long value = 0; + Require(static_cast(input >> value)); + return value; +} + +static std::uint32_t HostMtu(const char* name) { + const int probe = ::socket(AF_INET, SOCK_DGRAM, 0); + Require(probe >= 0); + ifreq request{}; + std::strncpy(request.ifr_name, name, sizeof(request.ifr_name) - 1); + Require(::ioctl(probe, SIOCGIFMTU, &request) == 0); + ::close(probe); + return static_cast(request.ifr_mtu); +} + +static void SendLoopbackDatagram() { + const int receiver = ::socket(AF_INET, SOCK_DGRAM, 0); + const int sender = ::socket(AF_INET, SOCK_DGRAM, 0); + Require(receiver >= 0 && sender >= 0); + sockaddr_in address{}; + address.sin_family = AF_INET; + address.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + Require(::bind(receiver, reinterpret_cast(&address), sizeof(address)) == 0); + socklen_t length = sizeof(address); + Require(::getsockname(receiver, reinterpret_cast(&address), &length) == 0); + const char payload = 'x'; + Require(::sendto(sender, &payload, 1, 0, reinterpret_cast(&address), sizeof(address)) == 1); + char received = 0; + Require(::recv(receiver, &received, 1, 0) == 1); + ::close(sender); + ::close(receiver); +} +#endif + static std::vector HostSnapshot() { std::vector entries; #ifdef _WIN32 @@ -88,22 +140,47 @@ static std::vector HostSnapshot() { #else ifaddrs* host = nullptr; Require(::getifaddrs(&host) == 0); + std::vector names; + std::vector> groups; + const auto group = [&](const char* name) -> std::vector& { + for (std::size_t index = 0; index < names.size(); ++index) + if (names[index] == name) return groups[index]; + names.emplace_back(name); + return groups.emplace_back(); + }; try { for (auto* item = host; item; item = item->ifa_next) { - if (!item->ifa_addr || (item->ifa_addr->sa_family != AF_INET && item->ifa_addr->sa_family != AF_INET6)) continue; + if (!item->ifa_addr) continue; + if (item->ifa_addr->sa_family == AF_PACKET) { + const auto& link = *reinterpret_cast(item->ifa_addr); + HostInterfaceAddress entry{}; + entry.family = AF_PACKET; + std::strncpy(entry.name, item->ifa_name, sizeof(entry.name) - 1); + entry.index = static_cast(link.sll_ifindex); + entry.type = link.sll_hatype == ARPHRD_ETHER ? 0x6 : link.sll_hatype == ARPHRD_LOOPBACK ? 0x18 : link.sll_hatype == ARPHRD_PPP ? 0x17 : 0x1; + entry.hardwareLength = entry.type == 0x18 ? 0 : link.sll_halen; + std::memcpy(entry.hardware, link.sll_addr, entry.hardwareLength); + entry.loopbackUp = (item->ifa_flags & IFF_UP) != 0; + auto& members = group(item->ifa_name); + members.insert(members.begin(), entry); + continue; + } + if (item->ifa_addr->sa_family != AF_INET && item->ifa_addr->sa_family != AF_INET6) continue; Require(item->ifa_netmask != nullptr); HostInterfaceAddress entry = HostAddress(item->ifa_addr); + std::strncpy(entry.name, item->ifa_name, sizeof(entry.name) - 1); if (entry.family == AF_INET) std::memcpy(entry.netmask, &reinterpret_cast(item->ifa_netmask)->sin_addr, 4); else std::memcpy(entry.netmask, &reinterpret_cast(item->ifa_netmask)->sin6_addr, 16); - entries.push_back(entry); + group(item->ifa_name).push_back(entry); } } catch (...) { ::freeifaddrs(host); throw; } ::freeifaddrs(host); + for (const auto& members : groups) entries.insert(entries.end(), members.begin(), members.end()); #endif return entries; } @@ -115,6 +192,10 @@ int main() { const std::uint8_t loopbackAddress[] = {127, 0, 0, 1}; const std::uint8_t loopbackMask[] = {255, 0, 0, 0}; bool matched = false; +#ifndef _WIN32 + bool linkChecked = false; + SendLoopbackDatagram(); +#endif for (unsigned attempt = 0; attempt < 3 && !matched; ++attempt) { const std::vector host = HostSnapshot(); GuestInterfaceAddress* list = nullptr; @@ -128,8 +209,47 @@ int main() { const std::size_t nameLength = std::strlen(item->name); Require(nameLength >= 1 && nameLength <= 15); Require((item->flags & ~knownFlags) == 0); - Require(item->data == nullptr); const auto* address = static_cast(item->address); + Require(address != nullptr && reinterpret_cast(address) % 8 == 0); +#ifndef _WIN32 + if (address[1] == 18) { + Require(item->netmask == nullptr && item->destination == nullptr); + const auto* data = static_cast(item->data); + Require(data != nullptr && reinterpret_cast(data) % 8 == 0); + std::uint16_t index = 0; + std::uint16_t dataLength = 0; + std::uint32_t mtu = 0; + std::uint64_t counters[11] = {}; + std::memcpy(&index, address + 2, 2); + std::memcpy(&dataLength, data + 6, 2); + std::memcpy(&mtu, data + 8, 4); + std::memcpy(counters, data + 24, sizeof(counters)); + Require(address[0] >= 54 && address[0] % 8 == 0 && address[0] >= 24 + address[6]); + Require(address[5] == nameLength && std::memcmp(address + 8, item->name, nameLength) == 0 && address[7] == 0); + Require(dataLength == 152 && data[0] == address[4] && data[2] == address[6] && data[1] == 0 && data[5] == 0); + Require(data[4] == 1 || data[4] == 2); + Require(index == if_nametoindex(item->name)); + Require(mtu == HostMtu(item->name)); + Require(counters[0] <= SysfsCounter(item->name, "rx_packets") && counters[2] <= SysfsCounter(item->name, "tx_packets")); + Require(counters[5] <= SysfsCounter(item->name, "rx_bytes") && counters[6] <= SysfsCounter(item->name, "tx_bytes")); + if (entries <= host.size()) { + const HostInterfaceAddress& expected = host[entries - 1]; + equal = equal && expected.family == AF_PACKET && std::strcmp(expected.name, item->name) == 0 && expected.index == index && + expected.type == address[4] && expected.hardwareLength == address[6] && + std::memcmp(expected.hardware, address + 8 + nameLength, expected.hardwareLength) == 0; + if (expected.type == 0x18) { + Require(data[3] == 0 && data[4] == (expected.loopbackUp ? 2 : 1)); + if (expected.loopbackUp) Require(counters[0] > 0 && counters[2] > 0 && counters[5] > 0 && counters[6] > 0); + linkChecked = linkChecked || expected.loopbackUp; + } + if (expected.type == 0x6) Require(data[3] == 14 && address[6] == 6); + } else { + equal = false; + } + continue; + } +#endif + Require(item->data == nullptr); const auto* netmask = static_cast(item->netmask); Require(address != nullptr && netmask != nullptr); Require(reinterpret_cast(address) % 8 == 0); @@ -151,6 +271,9 @@ int main() { if (item->flags & 0x10) Require(item->destination != nullptr); if (entries <= host.size()) { const HostInterfaceAddress& expected = host[entries - 1]; +#ifndef _WIN32 + equal = equal && std::strcmp(expected.name, item->name) == 0; +#endif const bool ipv4 = address[1] == 2; const std::size_t offset = ipv4 ? 4 : 8; const std::size_t length = ipv4 ? 4 : 16; @@ -172,6 +295,9 @@ int main() { freeifaddrs_nid_postfix(list); } Require(matched); +#ifndef _WIN32 + Require(linkChecked); +#endif GuestInterfaceAddress* released = nullptr; Require(getifaddrs_nid_postfix(&released) == 0 && released != nullptr); free_nid_postfix(released); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index cde40c7ff4..7cdfe88e19 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -89,7 +89,7 @@ - The [`_Unwind_Context`](../../../core/libs/prx/libc/include/exceptions/Unwind.hpp) that `_Unwind_Backtrace` passes to its callback starts with a table laid out like LLVM libunwind's `AbstractUnwindCursor` virtual table, because homebrew built with the ps5-payload-dev SDK links libunwind statically and reads the frames with its own `_Unwind_GetIP` (`__unw_get_reg`), which calls through that table. Only register reads and writes (`validReg`, `getReg`, `setReg`, with `UNW_REG_IP` and `UNW_REG_SP`), `validFloatReg` (always false) and `isSignalFrame` are answered; the other slots throw. The layout follows libunwind's `UnwindCursor.hpp`, not the console's own `libSceLibcInternal`, which is not checked - The Windows libc [exception unwinder](../../../core/libs/prx/libc/src/exception/Unwind.cpp) steps through frames of modules without the project's `.ehmeta` and `.ehfram` tables (system DLLs, and WinLibs GCC 15.2's `libstdc++-6.dll` and `libwinpthread-1.dll`, whose `.eh_frame` sections are empty) with their PE unwind data through `RtlVirtualUnwind`, but does not run Windows exception or termination handlers: it stops at a frame whose unwind data has one, such as a C++ cleanup through `__gxx_personality_seh0` in `libstdc++-6.dll` or a system DLL function like `qsort` in `ucrtbase.dll` (Windows 10 22H2), and the exception is then reported as unhandled - The libc [SSE4a trap emulation](../../../core/libs/prx/libc/src/specifics/windows/Sse4aEmulation.hpp) on Windows is superseded by `--to-intel` and remains only until the relinked title has been verified without it. -- [getifaddrs](../../../core/libs/prx/libc/src/Interfaces.cpp) (libc) lists the host's IPv4 and IPv6 addresses in FreeBSD's `ifaddrs` and sockaddr layouts. FreeBSD's per-interface AF_LINK entries (MAC, index, MTU) and their `if_data` are not produced, so interfaces without an address are omitted. Names are the host's (Linux `lo`, `eth0`; Windows NDIS names such as `ethernet_32769`, or `if` when one does not fit 15 bytes), not the console's. A Linux point-to-point address without a peer loses `IFF_POINTOPOINT`, and an IPv4 broadcast address the host omits is computed from the netmask. On Windows the flags are only `IFF_UP`, `IFF_RUNNING`, `IFF_LOOPBACK`, `IFF_BROADCAST` and `IFF_MULTICAST`, derived from the adapter: PPP and tunnel adapters get neither `IFF_BROADCAST` nor `IFF_POINTOPOINT` because their peer address is not reported, and an address whose prefix length the host reports as unspecified gets a host mask. +- [getifaddrs](../../../core/libs/prx/libc/src/Interfaces.cpp) (libc) lists host interfaces and IPv4/IPv6 addresses in FreeBSD's layouts. Windows omits per-interface AF_LINK entries and interfaces without an address; its flags are only `IFF_UP`, `IFF_RUNNING`, `IFF_LOOPBACK`, `IFF_BROADCAST` and `IFF_MULTICAST` (PPP/tunnel adapters omit broadcast and point-to-point, and an unspecified prefix length gets a host mask). On Linux, `AF_LINK` `if_data` maps non-Ethernet/loopback/PPP types to `IFT_OTHER` (1) and leaves `ifi_omcasts`, `ifi_noproto`, `ifi_hwassist`, `ifi_metric`, `ifi_epoch`, `ifi_lastchange`, unknown baud rates and address-entry `ifa_data` as 0/null; a point-to-point address without a peer loses `IFF_POINTOPOINT`, and an omitted IPv4 broadcast is derived from the netmask. Interface names are the host's (`if` on Windows above 15 bytes). - [gethostname](../../../core/libs/prx/libc/src/Hostname.cpp) (libc) reports the fixed guest name `AnyPS5` without querying the host machine name. Buffer copying, null queries and `ENAMETOOLONG` follow [FreeBSD gethostname](https://github.com/freebsd/freebsd-src/blob/main/lib/libc/gen/gethostname.c); the console's configurable hostname and its integration with `sysctl` and name resolution are unverified and not implemented. - [wcsrtombs_s](../../../core/libs/prx/libc/src/Multibyte.cpp) (libc) follows the console's `libc.prx`, but a runtime-constraint violation only returns its error: the default handler there writes the message and a newline to `stderr` before returning, which is not done here. Conversion uses the C locale of `wcrtomb` (`MB_CUR_MAX` 1), so the path where a multibyte character does not fit the remaining space is not reached - [c16rtomb, c32rtomb, mbrtoc16 and mbrtoc32](../../../core/libs/prx/libc/src/Multibyte.cpp) (libSceLibcInternal, implemented in libc) follow the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x7D480 to 0x7D920), which converts to and from UTF-8 whatever the locale, with the original sequences of up to six bytes and no check for surrogates or for values above 0x10FFFF when encoding. The conversion state keeps the value in its first eight bytes and the count of bytes still expected (or 0x10 for a pending low surrogate, 1 for a stored high surrogate) as 16 bits at offset 0xA; a null state, which the console dereferences, throws. As on the console, `mbrtoc16` does not store the complete code point when it returns a high surrogate, so the low surrogate of the next call is built from the value the state last held, which is the partial value of an earlier incomplete call or whatever the caller initialised it to, `c16rtomb` returns -1 without setting `errno` when a high surrogate is not followed by a low one, and `mbrtoc16` and `mbrtoc32` report `EILSEQ` as 86. Measured on a PS5 (firmware 13.60) from a homebrew payload, the system `libSceLibcInternal`'s default locale also converts UTF-8 in `wcrtomb`, `wctomb`, `mbrtowc` and `mblen` (`wctomb` of U+263A gives `E2 98 BA`, `mblen` of a lone 0xE9 gives -1); this libc keeps its single-byte C locale, so [wctomb and mblen](../../../core/libs/prx/libc/src/Multibyte.cpp), which call `wcrtomb` and `mbtowc` here as they call the same internal routines on the console, share that difference. Titles bundle their own `libc.prx`, whose locale was not measured From d65c969df85150cc4579f8f09926c471c5e69c9f Mon Sep 17 00:00:00 2001 From: Federico Marras Date: Mon, 19 Oct 2026 13:16:52 +0200 Subject: [PATCH 119/269] feat(libSceJpegDec): decode with a down scale of 2, 4 or 8 sceJpegDecParseHeader and sceJpegDecDecode threw for every down scale other than 1. SharpProspero's JpegDec.cs documents the factors 1, 2, 4 and 8 and output dimensions that account for the down scale. Decoder::Jpeg gains DecodeScaled, which decodes through libjpeg-turbo's scaled inverse DCT, and ScaledDimension, its rounded-up output size. sceJpegDecParseHeader reports the scaled output size; sceJpegDecDecode checks the pitch and image buffer against it, writes the scaled image and packs the output size into its result. A down scale of 1 still decodes as before. Decode modes other than 0, other down scales and a down-scaled CMYK or YCCK image still throw. --- core/Decoder/Jpeg/include/Decoder/Jpeg.hpp | 4 ++ core/Decoder/Jpeg/src/Jpeg.cpp | 43 ++++++++++++- core/Decoder/Jpeg/tests/Jpeg.cpp | 68 ++++++++++++++++++++ core/libs/prx/libSceJpegDec/Export.cpp | 37 ++++++----- core/libs/tests/GuestJpegDec.cpp | 73 ++++++++++++++++++++-- docs/dev/debt/libSceJpegDec.md | 2 +- 6 files changed, 203 insertions(+), 24 deletions(-) diff --git a/core/Decoder/Jpeg/include/Decoder/Jpeg.hpp b/core/Decoder/Jpeg/include/Decoder/Jpeg.hpp index 13c1d8accc..ed59467ac3 100644 --- a/core/Decoder/Jpeg/include/Decoder/Jpeg.hpp +++ b/core/Decoder/Jpeg/include/Decoder/Jpeg.hpp @@ -35,6 +35,10 @@ std::optional
ParseHeader(std::span jpeg); std::optional Decode(std::span jpeg); +std::uint32_t ScaledDimension(std::uint32_t dimension, std::uint32_t denominator); + +std::optional DecodeScaled(std::span jpeg, std::uint32_t denominator); + } // namespace Decoder::Jpeg #endif // DECODER_JPEG_HPP diff --git a/core/Decoder/Jpeg/src/Jpeg.cpp b/core/Decoder/Jpeg/src/Jpeg.cpp index d446841cc4..8e260437e2 100644 --- a/core/Decoder/Jpeg/src/Jpeg.cpp +++ b/core/Decoder/Jpeg/src/Jpeg.cpp @@ -19,8 +19,8 @@ constexpr std::uint32_t MAX_DIMENSION = 0xFFFF; class Handle { public: - Handle() : handle(tj3Init(TJINIT_COMPRESS)) { - if (!handle) throw std::runtime_error("Jpeg::Encode: encoder initialization failed"); + explicit Handle(int type) : handle(tj3Init(type)) { + if (!handle) throw std::runtime_error(type == TJINIT_COMPRESS ? "Jpeg::Encode: encoder initialization failed" : "Jpeg::DecodeScaled: decoder initialization failed"); } ~Handle() { tj3Destroy(handle); } Handle(const Handle&) = delete; @@ -63,7 +63,7 @@ std::vector Encode(std::span pixels, std::uint throw std::invalid_argument("Jpeg::Encode: invalid restart interval"); } const int subsampling = toSubsampling(sampling); - Handle handle; + Handle handle(TJINIT_COMPRESS); if (tj3Set(handle, TJPARAM_QUALITY, quality) < 0 || tj3Set(handle, TJPARAM_SUBSAMP, channels == 1 ? TJSAMP_GRAY : subsampling) < 0) { throw std::runtime_error("Jpeg::Encode: encoder configuration failed"); } @@ -80,6 +80,43 @@ std::vector Encode(std::span pixels, std::uint return {buffer.data, buffer.data + size}; } +std::uint32_t ScaledDimension(std::uint32_t dimension, std::uint32_t denominator) { + if (denominator != 1 && denominator != 2 && denominator != 4 && denominator != 8) { + throw std::invalid_argument("Jpeg::ScaledDimension: the scale must be 1/1, 1/2, 1/4 or 1/8"); + } + const tjscalingfactor factor{1, static_cast(denominator)}; + return static_cast(TJSCALED(static_cast(dimension), factor)); +} + +std::optional DecodeScaled(std::span jpeg, std::uint32_t denominator) { + if (denominator != 1 && denominator != 2 && denominator != 4 && denominator != 8) { + throw std::invalid_argument("Jpeg::DecodeScaled: the scale must be 1/1, 1/2, 1/4 or 1/8"); + } + if (jpeg.empty() || jpeg.size() > INT_MAX) return std::nullopt; + + Handle handle(TJINIT_DECOMPRESS); + if (tj3Set(handle, TJPARAM_STOPONWARNING, 1) < 0) throw std::runtime_error("Jpeg::DecodeScaled: decoder configuration failed"); + if (tj3DecompressHeader(handle, jpeg.data(), jpeg.size()) < 0) return std::nullopt; + const int colorspace = tj3Get(handle, TJPARAM_COLORSPACE); + if (colorspace == TJCS_CMYK || colorspace == TJCS_YCCK) { + throw std::runtime_error("Jpeg::DecodeScaled: scaled decoding of a CMYK or YCCK image is not implemented"); + } + const int width = tj3Get(handle, TJPARAM_JPEGWIDTH); + const int height = tj3Get(handle, TJPARAM_JPEGHEIGHT); + if (width <= 0 || height <= 0) return std::nullopt; + if (tj3SetScalingFactor(handle, tjscalingfactor{1, static_cast(denominator)}) < 0) { + throw std::runtime_error("Jpeg::DecodeScaled: the decoder refused the scaling factor"); + } + + const std::uint32_t channels = colorspace == TJCS_GRAY ? 1 : 3; + Image image{ScaledDimension(static_cast(width), denominator), ScaledDimension(static_cast(height), denominator), channels, {}}; + image.pixels.resize(static_cast(image.width) * image.height * channels); + if (tj3Decompress8(handle, jpeg.data(), jpeg.size(), image.pixels.data(), 0, channels == 1 ? TJPF_GRAY : TJPF_RGB) < 0) { + return std::nullopt; + } + return image; +} + std::optional
ParseHeader(std::span jpeg) { if (jpeg.empty() || jpeg.size() > INT_MAX) return std::nullopt; diff --git a/core/Decoder/Jpeg/tests/Jpeg.cpp b/core/Decoder/Jpeg/tests/Jpeg.cpp index f5f9c309ab..e19be53028 100644 --- a/core/Decoder/Jpeg/tests/Jpeg.cpp +++ b/core/Decoder/Jpeg/tests/Jpeg.cpp @@ -1,5 +1,7 @@ #include "Decoder/Jpeg.hpp" +#include +#include #include #include #include @@ -108,5 +110,71 @@ int main() { const std::vector garbage{1, 2, 3, 4, 5, 6, 7, 8}; Require(!Decoder::Jpeg::Decode(garbage).has_value()); + Require(Decoder::Jpeg::ScaledDimension(7, 1) == 7 && Decoder::Jpeg::ScaledDimension(7, 2) == 4); + Require(Decoder::Jpeg::ScaledDimension(7, 4) == 2 && Decoder::Jpeg::ScaledDimension(7, 8) == 1); + Require(Decoder::Jpeg::ScaledDimension(32, 8) == 4 && Decoder::Jpeg::ScaledDimension(33, 8) == 5 && Decoder::Jpeg::ScaledDimension(1, 8) == 1); + Require(Decoder::Jpeg::ScaledDimension(0xFFFF, 2) == 0x8000); + for (const std::uint32_t denominator : {0u, 3u, 16u}) { + Require(ThrowsInvalidArgument([&] { Decoder::Jpeg::ScaledDimension(7, denominator); })); + Require(ThrowsInvalidArgument([&] { Decoder::Jpeg::DecodeScaled(rgbJpeg, denominator); })); + } + + const auto boxError = [](const std::vector& source, std::uint32_t sourceWidth, std::uint32_t sourceHeight, + std::uint32_t channels, const Decoder::Jpeg::Image& scaled, std::uint32_t denominator) { + long error = 0; + for (std::uint32_t y = 0; y < scaled.height; ++y) { + for (std::uint32_t x = 0; x < scaled.width; ++x) { + for (std::uint32_t c = 0; c < channels; ++c) { + long sum = 0; + long count = 0; + for (std::uint32_t sy = y * denominator; sy < std::min(sourceHeight, (y + 1) * denominator); ++sy) { + for (std::uint32_t sx = x * denominator; sx < std::min(sourceWidth, (x + 1) * denominator); ++sx) { + sum += source[(static_cast(sy) * sourceWidth + sx) * channels + c]; + ++count; + } + } + const auto average = static_cast((sum + count / 2) / count); + error += Difference(average, scaled.pixels[(static_cast(y) * scaled.width + x) * channels + c]); + } + } + } + return error / static_cast(static_cast(scaled.width) * scaled.height * channels); + }; + + for (const std::uint32_t denominator : {1u, 2u, 4u, 8u}) { + const auto scaled = Decoder::Jpeg::DecodeScaled(rgbJpeg, denominator); + Require(scaled.has_value() && scaled->channels == 3); + Require(scaled->width == width / denominator && scaled->height == height / denominator); + Require(scaled->pixels.size() == static_cast(scaled->width) * scaled->height * 3); + Require(boxError(rgb, width, height, 3, *scaled, denominator) < 4); + + const auto grayScaled = Decoder::Jpeg::DecodeScaled(grayJpeg, denominator); + Require(grayScaled.has_value() && grayScaled->channels == 1); + Require(grayScaled->width == width / denominator && grayScaled->height == height / denominator); + Require(boxError(gray, width, height, 1, *grayScaled, denominator) < 4); + } + + std::vector odd(31 * 23 * 3); + for (std::uint32_t y = 0; y < 23; ++y) { + for (std::uint32_t x = 0; x < 31; ++x) { + std::uint8_t* pixel = &odd[(y * 31 + x) * 3]; + pixel[0] = static_cast(x * 8); + pixel[1] = static_cast(y * 11); + pixel[2] = static_cast(200 - x * 2); + } + } + const auto oddJpeg = Decoder::Jpeg::Encode(odd, 31, 23, 3, 95, Decoder::Jpeg::Sampling::Yuv420); + for (const std::uint32_t denominator : {2u, 4u, 8u}) { + const auto scaled = Decoder::Jpeg::DecodeScaled(oddJpeg, denominator); + Require(scaled.has_value() && scaled->channels == 3); + Require(scaled->width == Decoder::Jpeg::ScaledDimension(31, denominator) && scaled->height == Decoder::Jpeg::ScaledDimension(23, denominator)); + Require(boxError(odd, 31, 23, 3, *scaled, denominator) < 8); + } + + Require(!Decoder::Jpeg::DecodeScaled({}, 2).has_value()); + Require(!Decoder::Jpeg::DecodeScaled(garbage, 2).has_value()); + const std::vector truncated(rgbJpeg.begin(), rgbJpeg.begin() + static_cast(rgbJpeg.size() / 2)); + Require(!Decoder::Jpeg::DecodeScaled(truncated, 2).has_value()); + return 0; } diff --git a/core/libs/prx/libSceJpegDec/Export.cpp b/core/libs/prx/libSceJpegDec/Export.cpp index 0d92c485bb..331a8ccf46 100644 --- a/core/libs/prx/libSceJpegDec/Export.cpp +++ b/core/libs/prx/libSceJpegDec/Export.cpp @@ -32,6 +32,9 @@ constexpr std::uintptr_t HANDLE_ALIGNMENT = 8; constexpr std::uint16_t DECODE_MODE_NORMAL = 0; constexpr std::uint16_t DOWN_SCALE_NONE = 1; +constexpr std::uint16_t DOWN_SCALE_HALF = 2; +constexpr std::uint16_t DOWN_SCALE_QUARTER = 4; +constexpr std::uint16_t DOWN_SCALE_EIGHTH = 8; constexpr std::uint16_t PIXEL_FORMAT_R8G8B8A8 = 0; constexpr std::uint16_t PIXEL_FORMAT_B8G8R8A8 = 1; @@ -55,9 +58,11 @@ std::int32_t validateCreateParam(const JpegDecCreateParam* param) { return 0; } -void requireNormalDecode(const char* function, std::uint16_t decodeMode, std::uint16_t downScale) { +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) throw std::runtime_error(std::string(function) + ": down scale " + std::to_string(downScale) + " 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) { @@ -67,15 +72,15 @@ Context* toContext(void* handle) { return context->self == context ? context : nullptr; } -void fillImageInfo(const Decoder::Jpeg::Header& header, JpegDecImageInfo* info) { +void fillImageInfo(const Decoder::Jpeg::Header& header, std::uint32_t downScale, JpegDecImageInfo* info) { *info = {}; info->image_width = header.width; info->image_height = header.height; info->color_space = COLOR_SPACE_RGB; info->component_number = static_cast(header.channels); info->suitable_csc_attribute = ATTRIBUTE_CSC_ORDINARY; - info->output_image_width = header.width; - info->output_image_height = header.height; + info->output_image_width = Decoder::Jpeg::ScaledDimension(header.width, downScale); + info->output_image_height = Decoder::Jpeg::ScaledDimension(header.height, downScale); } std::span toBytes(const void* data, std::uint32_t size) { @@ -108,19 +113,21 @@ int32_t APS5_VABI sceJpegDecDecode(void* handle, const JpegDecDecodeParam* param if (!param->jpeg_mem_addr || !param->image_mem_addr) 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; - requireNormalDecode(__func__, param->decode_mode, param->down_scale); + const std::uint32_t downScale = requireDownScale(__func__, param->decode_mode, param->down_scale); const std::span jpeg = toBytes(param->jpeg_mem_addr, param->jpeg_mem_size); const std::optional header = Decoder::Jpeg::ParseHeader(jpeg); if (!header) return SCE_JPEG_DEC_ERROR_INVALID_DATA; + const std::uint32_t outputWidth = Decoder::Jpeg::ScaledDimension(header->width, downScale); + const std::uint32_t outputHeight = Decoder::Jpeg::ScaledDimension(header->height, downScale); - const std::uint64_t rowSize = static_cast(header->width) * BYTES_PER_PIXEL; + const std::uint64_t rowSize = static_cast(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 ((header->height - 1) * pitch + rowSize > param->image_mem_size) return SCE_JPEG_DEC_ERROR_INVALID_SIZE; + if ((outputHeight - 1) * pitch + rowSize > param->image_mem_size) return SCE_JPEG_DEC_ERROR_INVALID_SIZE; - const std::optional image = Decoder::Jpeg::Decode(jpeg); - if (!image || image->width != header->width || image->height != header->height) return SCE_JPEG_DEC_ERROR_DECODE_ERROR; + const std::optional 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; const bool swapRedBlue = param->pixel_format == PIXEL_FORMAT_B8G8R8A8; const auto alpha = static_cast(param->alpha_value); @@ -144,9 +151,9 @@ int32_t APS5_VABI sceJpegDecDecode(void* handle, const JpegDecDecodeParam* param } } - if (image_info) fillImageInfo(*header, image_info); - if (header->width > MAX_PACKED_DIMENSION || header->height > MAX_PACKED_DIMENSION) return 0; - return static_cast(header->width << 16 | header->height); + if (image_info) fillImageInfo(*header, downScale, image_info); + if (outputWidth > MAX_PACKED_DIMENSION || outputHeight > MAX_PACKED_DIMENSION) return 0; + return static_cast(outputWidth << 16 | outputHeight); } int32_t APS5_VABI sceJpegDecDelete(void* handle) { @@ -160,10 +167,10 @@ int32_t APS5_VABI sceJpegDecParseHeader(const JpegDecParseParam* param, JpegDecI if (!param) return SCE_JPEG_DEC_ERROR_INVALID_PARAM; if (!param->jpeg_mem_addr || !image_info) return SCE_JPEG_DEC_ERROR_INVALID_ADDR; if (param->jpeg_mem_size == 0) return SCE_JPEG_DEC_ERROR_INVALID_SIZE; - requireNormalDecode(__func__, param->decode_mode, param->down_scale); + const std::uint32_t downScale = requireDownScale(__func__, param->decode_mode, param->down_scale); const std::optional header = Decoder::Jpeg::ParseHeader(toBytes(param->jpeg_mem_addr, param->jpeg_mem_size)); if (!header) return SCE_JPEG_DEC_ERROR_INVALID_DATA; - fillImageInfo(*header, image_info); + fillImageInfo(*header, downScale, image_info); return 0; } diff --git a/core/libs/tests/GuestJpegDec.cpp b/core/libs/tests/GuestJpegDec.cpp index 0d9e9dcf43..8dfa4cb137 100644 --- a/core/libs/tests/GuestJpegDec.cpp +++ b/core/libs/tests/GuestJpegDec.cpp @@ -1,6 +1,7 @@ #include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include "Decoder/Jpeg.hpp" +#include #include #include #include @@ -80,8 +81,19 @@ int main() { Require(sceJpegDecParseHeader(&parseParam, nullptr) == invalidAddr); const JpegDecParseParam emptyParam{nullptr, 0, 0, 1}; Require(sceJpegDecParseHeader(&emptyParam, &info) == invalidAddr); - const JpegDecParseParam downScaledParse{jpeg.data(), static_cast(jpeg.size()), 0, 2}; - Require(ThrowsRuntimeError([&] { sceJpegDecParseHeader(&downScaledParse, &info); })); + 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(jpeg.size()), 0, downScale}; + Require(ThrowsRuntimeError([&] { sceJpegDecParseHeader(&unknownScaleParse, &info); })); + } + const JpegDecParseParam otherModeParse{jpeg.data(), static_cast(jpeg.size()), 1, 1}; + Require(ThrowsRuntimeError([&] { sceJpegDecParseHeader(&otherModeParse, &info); })); + for (const auto [downScale, outputWidth, outputHeight] : {std::array{2, 4, 3}, std::array{4, 2, 2}, std::array{8, 1, 1}}) { + const JpegDecParseParam downScaledParse{jpeg.data(), static_cast(jpeg.size()), 0, static_cast(downScale)}; + info = {}; + Require(sceJpegDecParseHeader(&downScaledParse, &info) == 0); + Require(info.image_width == 7 && info.image_height == 5 && info.component_number == 3); + Require(info.output_image_width == outputWidth && info.output_image_height == outputHeight); + } const std::vector grayJpeg = TestJpeg(3, 2, 1); const JpegDecParseParam grayParse{grayJpeg.data(), static_cast(grayJpeg.size()), 0, 1}; Require(sceJpegDecParseHeader(&grayParse, &info) == 0); @@ -125,9 +137,9 @@ int main() { JpegDecDecodeParam badFormat = decodeParam; badFormat.pixel_format = 2; Require(sceJpegDecDecode(handle, &badFormat, &info) == invalidParam); - JpegDecDecodeParam downScaled = decodeParam; - downScaled.down_scale = 2; - Require(ThrowsRuntimeError([&] { sceJpegDecDecode(handle, &downScaled, &info); })); + JpegDecDecodeParam unknownScale = decodeParam; + unknownScale.down_scale = 3; + Require(ThrowsRuntimeError([&] { sceJpegDecDecode(handle, &unknownScale, &info); })); JpegDecDecodeParam otherMode = decodeParam; otherMode.decode_mode = 1; Require(ThrowsRuntimeError([&] { sceJpegDecDecode(handle, &otherMode, &info); })); @@ -157,12 +169,63 @@ int main() { for (std::size_t x = 7 * 4; x < pitch; ++x) Require(bgra[y * pitch + x] == 0xEE); } + const std::vector large = TestJpeg(37, 21, 3); + const std::vector largeGray = TestJpeg(19, 11, 1); + for (const auto* source : {&large, &largeGray}) { + const std::optional sourceHeader = Decoder::Jpeg::ParseHeader(*source); + Require(sourceHeader.has_value()); + for (const std::uint16_t downScale : {std::uint16_t{2}, std::uint16_t{4}, std::uint16_t{8}}) { + const std::uint32_t width = (sourceHeader->width + downScale - 1) / downScale; + const std::uint32_t height = (sourceHeader->height + downScale - 1) / downScale; + const std::optional scaled = Decoder::Jpeg::DecodeScaled(*source, downScale); + Require(scaled && scaled->width == width && scaled->height == height); + + std::vector exact(static_cast(width) * height * 4, 0xCC); + JpegDecDecodeParam scaledParam = DecodeParam(*source, exact, 0); + scaledParam.down_scale = downScale; + info = {}; + Require(sceJpegDecDecode(handle, &scaledParam, &info) == static_cast(width << 16 | height)); + Require(info.image_width == sourceHeader->width && info.image_height == sourceHeader->height); + Require(info.output_image_width == width && info.output_image_height == height); + for (std::size_t pixel = 0; pixel < static_cast(width) * height; ++pixel) { + for (std::size_t c = 0; c < 3; ++c) Require(exact[pixel * 4 + c] == scaled->pixels[pixel * scaled->channels + (scaled->channels == 1 ? 0 : c)]); + Require(exact[pixel * 4 + 3] == 0xFF); + } + + const std::uint32_t paddedPitch = width * 4 + 4; + std::vector padded((height - 1) * paddedPitch + width * 4, 0xEE); + JpegDecDecodeParam paddedParam = DecodeParam(*source, padded, paddedPitch); + paddedParam.down_scale = downScale; + paddedParam.pixel_format = 1; + Require(sceJpegDecDecode(handle, &paddedParam, nullptr) == static_cast(width << 16 | height)); + for (std::size_t y = 0; y < height; ++y) { + for (std::size_t x = 0; x < width; ++x) { + const std::uint8_t* pixel = &padded[y * paddedPitch + x * 4]; + const std::uint8_t* rgba = &exact[(y * width + x) * 4]; + Require(pixel[0] == rgba[2] && pixel[1] == rgba[1] && pixel[2] == rgba[0] && pixel[3] == 0xFF); + } + if (y + 1 < height) for (std::size_t x = width * 4; x < paddedPitch; ++x) Require(padded[y * paddedPitch + x] == 0xEE); + } + + std::vector small(exact.size() - 1, 0); + JpegDecDecodeParam smallParam = DecodeParam(*source, small, 0); + smallParam.down_scale = downScale; + Require(sceJpegDecDecode(handle, &smallParam, &info) == invalidSize); + JpegDecDecodeParam narrowParam = DecodeParam(*source, exact, width * 4 - 1); + narrowParam.down_scale = downScale; + Require(sceJpegDecDecode(handle, &narrowParam, &info) == invalidParam); + } + } + const std::vector garbage{0x00, 0x01, 0x02, 0x03}; const JpegDecParseParam garbageParam{garbage.data(), static_cast(garbage.size()), 0, 1}; Require(sceJpegDecParseHeader(&garbageParam, &info) == invalidData); std::vector garbageOut(64, 0); const JpegDecDecodeParam garbageDecode = DecodeParam(garbage, garbageOut, 4 * 4); Require(sceJpegDecDecode(handle, &garbageDecode, &info) == invalidData); + JpegDecDecodeParam garbageScaled = garbageDecode; + garbageScaled.down_scale = 2; + Require(sceJpegDecDecode(handle, &garbageScaled, &info) == invalidData); Require(sceJpegDecDelete(handle) == 0); return 0; diff --git a/docs/dev/debt/libSceJpegDec.md b/docs/dev/debt/libSceJpegDec.md index 342b2df171..ee20ae229b 100644 --- a/docs/dev/debt/libSceJpegDec.md +++ b/docs/dev/debt/libSceJpegDec.md @@ -6,4 +6,4 @@ ### Functional -- [libSceJpegDec](../../../core/libs/prx/libSceJpegDec/Export.cpp) (libSceJpegDec) - decode modes other than 0 and down scales other than 1 throw +- [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 From 65da8ac1065e48dcc103d3f05899fcd76b57eeb5 Mon Sep 17 00:00:00 2001 From: emirplus Date: Mon, 19 Oct 2026 20:07:43 +0300 Subject: [PATCH 120/269] fix(tools): recognize _wgetenv wide environment readers in env_reference --- tools/env_reference.py | 7 ++++--- tools/tests/test_env_reference.py | 10 +++++++++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/tools/env_reference.py b/tools/env_reference.py index 55aa42af6b..5e6c23f713 100644 --- a/tools/env_reference.py +++ b/tools/env_reference.py @@ -12,9 +12,9 @@ SKIP = {"build", "build-linux", "build-relinker", "3rdparty", ".git"} INTEGER_READERS = {"kibSetting", "MaskFromEnvironment", "EnvInt", "configured", "environmentCount"} STRING_READERS = {"GetEnvironmentVariableA", "GetEnvironmentVariableW", "addWideString"} -NULL_TEST = re.compile(r"[!=]=\s*(nullptr|NULL|0)\b|!\s*(?:std::)?getenv\(") -INTEGER_TEST = re.compile(r"\b(atoi|strtol|strtoul|strtoll|strtoull|stoi|stol|stoul|from_chars|atoi64)\s*\(") -GREP = (r'(getenv|IsEnvironmentSet|kibSetting|MaskFromEnvironment|EnvInt|GetEnvironmentVariable[AW]' +NULL_TEST = re.compile(r"[!=]=\s*(nullptr|NULL|0)\b|!\s*(?:std::)?_?w?getenv\(") +INTEGER_TEST = re.compile(r"\b(atoi|strtol|strtoul|strtoll|strtoull|stoi|stol|stoul|from_chars|atoi64|_wtoi|_wtol|_wtoi64)\s*\(") +GREP = (r'(getenv|_wgetenv|wgetenv|IsEnvironmentSet|kibSetting|MaskFromEnvironment|EnvInt|GetEnvironmentVariable[AW]' r'|configured|PipelineSwitch|environmentCount|addWideString)' r'\([[:space:]]*(u|L)?"(APS5|ANYPS5)_[A-Za-z0-9_]+"') @@ -28,6 +28,7 @@ def forms(prefixes=PREFIXES): name = _name(prefixes) return ( ("getenv", re.compile(r'(?:std::)?getenv\(\s*"(?P' + name + r')"\s*\)')), + ("_wgetenv", re.compile(r'(?:std::)?_?wgetenv\(\s*L"(?P' + name + r')"\s*\)')), ("IsEnvironmentSet", re.compile(r'IsEnvironmentSet\(\s*"(?P' + name + r')"\s*\)')), ("MaskFromEnvironment", re.compile(r'MaskFromEnvironment\(\s*"(?P' + name + r')"\s*\)')), ("kibSetting", re.compile(r'kibSetting\(\s*"(?P' + name + r')"\s*,\s*(?P[0-9]+)')), diff --git a/tools/tests/test_env_reference.py b/tools/tests/test_env_reference.py index ae390c5e9d..a101a5fe2d 100644 --- a/tools/tests/test_env_reference.py +++ b/tools/tests/test_env_reference.py @@ -25,6 +25,10 @@ static std::size_t m = environmentCount("APS5_PM4_RING", 16, MaximumCapacity, "A static int n = GetEnvironmentVariableW(L"ANYPS5_LOADEXEC_ARGV_SKIP", skip, 2); static const auto* o = addWideString(u"ANYPS5_LOADEXEC_PARENT"); static unsigned long p = reconfigured("APS5_NOT_A_READ", 1); +static bool q = _wgetenv(L"APS5_WIDE_PRESENT") != nullptr; +static int r = _wtoi(_wgetenv(L"APS5_WIDE_INT")); +static const wchar_t* s = _wgetenv(L"ANYPS5_WIDE_CONFIG"); +static const char* t = std::getenv("NOT_AN_ENV"); """ @@ -46,7 +50,8 @@ class EnvReferenceTests(unittest.TestCase): "APS5_WORKER_AFFINITY_MASK", "ANYPS5_GPU", "APS5_THREAD_NICE", "APS5_CRASH_INDIRECT_MEMORY", "APS5_PIPELINE_JOURNAL_MAX_MIB", "APS5_PIPELINE_STORE_ENTRIES", "APS5_PIPELINE_JOURNAL_THREADS", "APS5_PM4_RING", - "ANYPS5_LOADEXEC_ARGV_SKIP", "ANYPS5_LOADEXEC_PARENT"}) + "ANYPS5_LOADEXEC_ARGV_SKIP", "ANYPS5_LOADEXEC_PARENT", + "ANYPS5_WIDE_CONFIG", "APS5_WIDE_PRESENT", "APS5_WIDE_INT"}) self.assertEqual(found["APS5_PROFILE_DRAW"][0][1], 1) self.assertEqual(found["APS5_THREAD_NICE"][0][2:], ("integer", "-10")) self.assertEqual(found["APS5_HOST_IMPORT_MIB"][0][2:], ("integer", "6144")) @@ -56,6 +61,9 @@ class EnvReferenceTests(unittest.TestCase): self.assertEqual(found["APS5_PM4_RING"][0][2:], ("integer", "16")) self.assertEqual(found["ANYPS5_LOADEXEC_ARGV_SKIP"][0][2:], ("string", "")) self.assertEqual(found["ANYPS5_LOADEXEC_PARENT"][0][2:], ("string", "")) + self.assertEqual(found["ANYPS5_WIDE_CONFIG"][0][2:], ("string", "")) + self.assertEqual(found["APS5_WIDE_PRESENT"][0][2:], ("presence", "")) + self.assertEqual(found["APS5_WIDE_INT"][0][2:], ("integer", "")) def test_the_default_prefixes_cover_both(self): root = self.fixture() From 2fa97c7db0025c944ed068e3501cd5a1ebeda297 Mon Sep 17 00:00:00 2001 From: Akul Tyagi <69496404+AkulTyagi11@users.noreply.github.com> Date: Mon, 19 Oct 2026 22:38:38 +0530 Subject: [PATCH 121/269] test(libc): cover integer builtins edge cases Add edge case test rows in guest_integer_builtins for zero shifts, cross-word shift boundaries, zero bitcounts, comparison symmetry, arithmetic identities, division boundary and wrapping cases. --- core/libs/tests/GuestIntegerBuiltins.cpp | 72 ++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/core/libs/tests/GuestIntegerBuiltins.cpp b/core/libs/tests/GuestIntegerBuiltins.cpp index a347bc0c22..64f5cbb0e1 100644 --- a/core/libs/tests/GuestIntegerBuiltins.cpp +++ b/core/libs/tests/GuestIntegerBuiltins.cpp @@ -95,6 +95,23 @@ static void CheckShifts() { Require(__ashrti3_nid_postfix(Int128Min, 127) == -1); Require(__lshrti3_nid_postfix(Int128Min, 127) == 1); Require(static_cast(__lshrti3_nid_postfix(static_cast(Make(0x8000000000000001ull, 0)), 64)) == Make(0, 0x8000000000000001ull)); + Require(__ashldi3_nid_postfix(12345, 0) == 12345); + Require(__ashrdi3_nid_postfix(-12345, 0) == -12345); + Require(__lshrdi3_nid_postfix(INT64_MIN, 0) == INT64_MIN); + Require(__ashlti3_nid_postfix(Int128Max, 0) == Int128Max); + Require(__ashrti3_nid_postfix(Int128Min, 0) == Int128Min); + Require(__lshrti3_nid_postfix(Int128Max, 0) == Int128Max); + Require(__ashldi3_nid_postfix(0, 10) == 0); + Require(__ashrdi3_nid_postfix(0, 10) == 0); + Require(__lshrdi3_nid_postfix(0, 10) == 0); + Require(__ashlti3_nid_postfix(0, 70) == 0); + Require(__ashrti3_nid_postfix(0, 70) == 0); + Require(__lshrti3_nid_postfix(0, 70) == 0); + Require(static_cast(__ashlti3_nid_postfix(1, 64)) == Make(1, 0)); + Require(static_cast(__ashlti3_nid_postfix(1, 127)) == Make(1ull << 63, 0)); + Require(__lshrti3_nid_postfix(static_cast(Make(1, 0)), 64) == 1); + Require(__lshrti3_nid_postfix(Int128Min, 127) == 1); + Require(__ashrti3_nid_postfix(Int128Min, 64) == static_cast(Make(~0ull, 1ull << 63))); } static void CheckBitCounts() { @@ -110,6 +127,22 @@ static void CheckBitCounts() { Require(__parityti2_nid_postfix(Make(1, 1)) == 0 && __parityti2_nid_postfix(Make(1, 0)) == 1); Require(__popcountsi2_nid_postfix(0xffffffffu) == 32 && __popcountdi2_nid_postfix(~0ull) == 64); Require(__popcountti2_nid_postfix(Make(~0ull, ~0ull)) == 128); + Require(__clzsi2_nid_postfix(0) == 32); + Require(__ctzsi2_nid_postfix(0) == 32); + Require(__popcountsi2_nid_postfix(0) == 0); + Require(__popcountdi2_nid_postfix(0) == 0); + Require(__popcountti2_nid_postfix(0) == 0); + Require(__paritysi2_nid_postfix(0) == 0); + Require(__paritydi2_nid_postfix(0) == 0); + Require(__parityti2_nid_postfix(0) == 0); + Require(__clzsi2_nid_postfix(0x80000000u) == 0); + Require(__clzdi2_nid_postfix(1ull << 63) == 0); + Require(__clzti2_nid_postfix(Make(1ull << 63, 0)) == 0); + Require(__ctzsi2_nid_postfix(1) == 0); + Require(__ctzdi2_nid_postfix(1) == 0); + Require(__ctzti2_nid_postfix(1) == 0); + Require(__ffsdi2_nid_postfix(1) == 1 && __ffsdi2_nid_postfix(1ull << 63) == 64); + Require(__ffsti2_nid_postfix(1) == 1 && __ffsti2_nid_postfix(Make(1ull << 63, 0)) == 128); } static void CheckComparisons() { @@ -117,6 +150,13 @@ static void CheckComparisons() { Require(__cmpti2_nid_postfix(Int128Min, 0) == 0 && __cmpti2_nid_postfix(Int128Max, 0) == 2); Require(__ucmpdi2_nid_postfix(~0ull, 0) == 2 && __ucmpdi2_nid_postfix(0, 1) == 0); Require(__ucmpti2_nid_postfix(Make(1, 0), Make(0, ~0ull)) == 2 && __ucmpti2_nid_postfix(3, 3) == 1); + Require(__cmpdi2_nid_postfix(INT64_MIN, INT64_MIN) == 1 && __cmpdi2_nid_postfix(INT64_MAX, INT64_MAX) == 1); + Require(__cmpdi2_nid_postfix(INT64_MIN, INT64_MAX) == 0 && __cmpdi2_nid_postfix(INT64_MAX, INT64_MIN) == 2); + Require(__cmpti2_nid_postfix(Int128Min, Int128Min) == 1 && __cmpti2_nid_postfix(Int128Max, Int128Max) == 1); + Require(__cmpti2_nid_postfix(Int128Min, Int128Max) == 0 && __cmpti2_nid_postfix(-1, 1) == 0 && __cmpti2_nid_postfix(1, -1) == 2); + Require(__ucmpdi2_nid_postfix(~0ull, ~0ull) == 1 && __ucmpdi2_nid_postfix(0, 0) == 1); + Require(__ucmpti2_nid_postfix(0, 0) == 1 && __ucmpti2_nid_postfix(Make(~0ull, ~0ull), Make(~0ull, ~0ull)) == 1); + Require(__ucmpti2_nid_postfix(0, Make(~0ull, ~0ull)) == 0 && __ucmpti2_nid_postfix(Make(0, ~0ull), Make(1, 0)) == 0); } static void CheckArithmetic() { @@ -126,6 +166,17 @@ static void CheckArithmetic() { Require(static_cast(__multi3_nid_postfix(static_cast(Make(0, ~0ull)), static_cast(Make(0, ~0ull)))) == Make(0xfffffffffffffffeull, 1)); Require(__multi3_nid_postfix(-3, static_cast(Make(1, 0))) == -static_cast(Make(3, 0))); + Require(__negdi2_nid_postfix(0) == 0 && __negdi2_nid_postfix(1) == -1 && __negdi2_nid_postfix(-1) == 1); + Require(__negti2_nid_postfix(0) == 0 && __negti2_nid_postfix(1) == -1 && __negti2_nid_postfix(-1) == 1); + Require(__negti2_nid_postfix(Int128Max) == -Int128Max); + Require(__muldi3_nid_postfix(0, 100) == 0 && __muldi3_nid_postfix(100, 0) == 0); + Require(__muldi3_nid_postfix(1, INT64_MIN) == INT64_MIN && __muldi3_nid_postfix(1, INT64_MAX) == INT64_MAX); + Require(__muldi3_nid_postfix(-1, INT64_MAX) == -INT64_MAX && __muldi3_nid_postfix(-1, INT64_MIN) == INT64_MIN); + Require(__muldi3_nid_postfix(INT64_MAX, 2) == -2); + Require(__multi3_nid_postfix(0, Int128Max) == 0 && __multi3_nid_postfix(Int128Min, 0) == 0); + Require(__multi3_nid_postfix(1, Int128Min) == Int128Min && __multi3_nid_postfix(1, Int128Max) == Int128Max); + Require(__multi3_nid_postfix(-1, Int128Max) == -Int128Max && __multi3_nid_postfix(-1, Int128Min) == Int128Min); + Require(__multi3_nid_postfix(Int128Max, 2) == -2 && __multi3_nid_postfix(Int128Min, 2) == 0); } static void CheckDivision() { @@ -151,6 +202,18 @@ static void CheckDivision() { RequireThrows([] { __udivdi3_nid_postfix(1, 0); }); RequireThrows([] { __umoddi3_nid_postfix(1, 0); }); RequireThrows([] { __udivmodti4_nid_postfix(1, 0, nullptr); }); + Require(__divsi3_nid_postfix(INT32_MIN, -1) == INT32_MIN && __modsi3_nid_postfix(INT32_MIN, -1) == 0); + Require(__divsi3_nid_postfix(0, 5) == 0 && __modsi3_nid_postfix(0, 5) == 0); + Require(__divdi3_nid_postfix(0, 5) == 0 && __moddi3_nid_postfix(0, 5) == 0); + Require(__divmodsi4_nid_postfix(INT32_MIN, -1, &remainder32) == INT32_MIN && remainder32 == 0); + Require(__divmoddi4_nid_postfix(INT64_MIN, -1, &remainder64) == INT64_MIN && remainder64 == 0); + Require(__udivsi3_nid_postfix(0, 5) == 0 && __umodsi3_nid_postfix(0, 5) == 0); + Require(__udivdi3_nid_postfix(0, 5) == 0 && __umoddi3_nid_postfix(0, 5) == 0); + Require(__udivmodsi4_nid_postfix(0, 5, &uremainder32) == 0 && uremainder32 == 0); + Require(__udivmoddi4_nid_postfix(0, 5, &uremainder64) == 0 && uremainder64 == 0); + Require(__udivmodti4_nid_postfix(0, 5, &remainder128) == 0 && remainder128 == 0); + Require(__udivmodti4_nid_postfix(Make(~0ull, ~0ull), Make(~0ull, ~0ull), &remainder128) == 1 && remainder128 == 0); + Require(__udivmodti4_nid_postfix(Make(0, 10), Make(1, 0), &remainder128) == 0 && remainder128 == 10); } static void CheckOverflow() { @@ -181,6 +244,15 @@ static void CheckOverflow() { RequireThrows([] { __negvsi2_nid_postfix(INT32_MIN); }); RequireThrows([] { __absvdi2_nid_postfix(INT64_MIN); }); RequireThrows([] { __absvti2_nid_postfix(Int128Min); }); + Require(__mulosi4_nid_postfix(INT32_MIN, 1, &overflow) == INT32_MIN && overflow == 0); + Require(__mulosi4_nid_postfix(INT32_MIN, -1, &overflow) == INT32_MIN && overflow == 1); + Require(__mulosi4_nid_postfix(0, INT32_MIN, &overflow) == 0 && overflow == 0); + Require(__mulodi4_nid_postfix(0, INT64_MIN, &overflow) == 0 && overflow == 0); + Require(__muloti4_nid_postfix(Int128Min, 1, &overflow) == Int128Min && overflow == 0); + Require(__muloti4_nid_postfix(Int128Min, -1, &overflow) == Int128Min && overflow == 1); + Require(__muloti4_nid_postfix(0, Int128Min, &overflow) == 0 && overflow == 0); + Require(__negvsi2_nid_postfix(0) == 0 && __negvdi2_nid_postfix(0) == 0 && __negvti2_nid_postfix(0) == 0); + Require(__absvsi2_nid_postfix(0) == 0 && __absvdi2_nid_postfix(0) == 0 && __absvti2_nid_postfix(0) == 0); } int main() { From 28d2d70150bd2fe8ed087b1ff768352e95edc758 Mon Sep 17 00:00:00 2001 From: KernelPanicNT <76663133+KernelPanicNT@users.noreply.github.com> Date: Mon, 19 Oct 2026 17:10:05 +0000 Subject: [PATCH 122/269] fix(libSceVoice): return the module's initialization errors A second sceVoiceInit returns 0x804E0802, and sceVoiceStart, sceVoiceCreatePort and sceVoiceGetPortInfo return 0x804E0801 before checking any argument while the library is not initialized, as the shipped module does. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceVoice/Export.cpp | 17 ++++++++++------- core/libs/tests/GuestVoice.cpp | 28 ++++++++++++++++++++++++++++ docs/dev/debt/libSceVoice.md | 1 + 3 files changed, 39 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceVoice/Export.cpp b/core/libs/prx/libSceVoice/Export.cpp index bcc56dcb98..a68d4c818f 100644 --- a/core/libs/prx/libSceVoice/Export.cpp +++ b/core/libs/prx/libSceVoice/Export.cpp @@ -21,6 +21,8 @@ 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(0x804E0801u); +constexpr int VoiceErrorAlreadyInitialized = static_cast(0x804E0802u); struct Port { std::int32_t type; @@ -66,7 +68,7 @@ int APS5_VABI sceVoiceInit(VoiceInitParam* param, int32_t version) { if (param == nullptr) APS5_INVALID_ARG_EX; auto& voice = State(); std::lock_guard lock(voice.mutex); - if (voice.initialized) Fail(__func__, "already initialized"); + if (voice.initialized) return VoiceErrorAlreadyInitialized; voice.initialized = true; return 0; } @@ -77,10 +79,10 @@ int APS5_VABI sceVoiceSetThreadsParams(void* params) { } int APS5_VABI sceVoiceStart(const VoiceStartParam* param) { - if (param == nullptr) APS5_INVALID_ARG_EX; auto& voice = State(); std::lock_guard lock(voice.mutex); - if (!voice.initialized) Fail(__func__, "library not initialized"); + if (!voice.initialized) return VoiceErrorNotInitialized; + if (param == nullptr) APS5_INVALID_ARG_EX; voice.started = true; return 0; } @@ -106,11 +108,11 @@ int APS5_VABI sceVoiceEnd_nid_postfix(void) { } int APS5_VABI sceVoiceCreatePort(uint32_t* port_id, const VoicePortParam* param) { - 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)); auto& voice = State(); std::lock_guard lock(voice.mutex); - if (!voice.initialized) Fail(__func__, "library not initialized"); + 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(param->voice.bitrate) : 0u}); @@ -171,9 +173,10 @@ int APS5_VABI sceVoiceGetVolume(uint32_t port_id, float* volume) { } int APS5_VABI sceVoiceGetPortInfo(uint32_t port_id, VoicePortInfo* info) { - if (info == nullptr) APS5_INVALID_ARG_EX; auto& voice = State(); std::lock_guard lock(voice.mutex); + 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; diff --git a/core/libs/tests/GuestVoice.cpp b/core/libs/tests/GuestVoice.cpp index 98c2b5f187..8a8b4431d5 100644 --- a/core/libs/tests/GuestVoice.cpp +++ b/core/libs/tests/GuestVoice.cpp @@ -2,6 +2,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include #include +#include #include extern "C" { @@ -25,6 +26,9 @@ int APS5_VABI sceVoiceDisableChat(void); namespace { +constexpr int NotInitialized = static_cast(0x804E0801u); +constexpr int AlreadyInitialized = static_cast(0x804E0802u); + void Require(bool value) { if (!value) std::abort(); } template @@ -33,6 +37,27 @@ void RequireThrows(TFunction function) { std::abort(); } +void TestNotInitialized() { + Require(sceVoiceStart(nullptr) == NotInitialized); + VoiceStartParam start{}; + Require(sceVoiceStart(&start) == NotInitialized); + Require(sceVoiceCreatePort(nullptr, nullptr) == NotInitialized); + VoicePortParam badType{}; + badType.port_type = 99; + std::uint32_t id = 0xA5A5A5A5u; + Require(sceVoiceCreatePort(&id, &badType) == NotInitialized); + VoicePortParam param{}; + param.port_type = 3; + Require(sceVoiceCreatePort(&id, ¶m) == NotInitialized); + Require(id == 0xA5A5A5A5u); + Require(sceVoiceGetPortInfo(0, nullptr) == NotInitialized); + VoicePortInfo info{}; + std::memset(&info, 0xA5, sizeof(info)); + VoicePortInfo sentinel = info; + Require(sceVoiceGetPortInfo(0, &info) == NotInitialized); + Require(std::memcmp(&info, &sentinel, sizeof(info)) == 0); +} + std::uint32_t CreatePort(std::int32_t type, float volume) { VoicePortParam param{}; param.port_type = type; @@ -46,6 +71,7 @@ std::uint32_t CreatePort(std::int32_t type, float volume) { } // namespace int main() { + TestNotInitialized(); RequireThrows([] { sceVoiceResetPort(0); }); RequireThrows([] { VoiceResourceInfo info{}; sceVoiceGetResourceInfo(&info); }); RequireThrows([] { sceVoiceEnableChat(); }); @@ -53,6 +79,7 @@ int main() { VoiceInitParam init{}; VoiceStartParam start{}; Require(sceVoiceInit(&init, 0) == 0); + Require(sceVoiceInit(&init, 0) == AlreadyInitialized); Require(sceVoiceStart(&start) == 0); RequireThrows([] { sceVoiceGetResourceInfo(nullptr); }); VoiceResourceInfo resourceInfo{}; @@ -91,6 +118,7 @@ int main() { RequireThrows([&] { sceVoiceResetPort(input); }); Require(sceVoiceResetPort(output) == 0); Require(sceVoiceEnd_nid_postfix() == 0); + TestNotInitialized(); RequireThrows([&] { sceVoiceResetPort(output); }); RequireThrows([] { VoiceResourceInfo info{}; sceVoiceGetResourceInfo(&info); }); RequireThrows([] { sceVoiceEnableChat(); }); diff --git a/docs/dev/debt/libSceVoice.md b/docs/dev/debt/libSceVoice.md index 5983d70f39..44d488840b 100644 --- a/docs/dev/debt/libSceVoice.md +++ b/docs/dev/debt/libSceVoice.md @@ -9,3 +9,4 @@ - [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 From 11540b9850935358b4a9e9ff763daa404c9436bf Mon Sep 17 00:00:00 2001 From: emirplus Date: Mon, 19 Oct 2026 20:11:28 +0300 Subject: [PATCH 123/269] fix(tools): decode CMake cache with utf-8-sig in configure_seed --- tools/configure_seed.py | 2 +- tools/tests/test_configure_seed.py | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/tools/configure_seed.py b/tools/configure_seed.py index 071d48e1a1..6c07178a56 100644 --- a/tools/configure_seed.py +++ b/tools/configure_seed.py @@ -9,7 +9,7 @@ ENTRY = re.compile(r"^([^#/:][^:]*):([A-Z]+)=(.*)$") def checks(cache): found, doc = {}, "" - for line in Path(cache).read_text(encoding="utf-8").splitlines(): + for line in Path(cache).read_text(encoding="utf-8-sig").splitlines(): if line.startswith("//"): doc = line continue diff --git a/tools/tests/test_configure_seed.py b/tools/tests/test_configure_seed.py index e1a1413fca..38d7109c37 100644 --- a/tools/tests/test_configure_seed.py +++ b/tools/tests/test_configure_seed.py @@ -90,5 +90,14 @@ class ConfigureSeedTests(unittest.TestCase): self.assertEqual(self.seed(cache, "empty.cmake").read_text(encoding="utf-8"), "") +class ChecksParsingTests(unittest.TestCase): + def test_checks_reads_cache_with_utf8_bom(self): + with tempfile.TemporaryDirectory() as directory: + cache = Path(directory) / "CMakeCache.txt" + cache.write_bytes(b"\xef\xbb\xbf//Test FIRST_CHECK\nFIRST_CHECK:INTERNAL=1\n") + found = configure_seed.checks(cache) + self.assertEqual(found, {"FIRST_CHECK": ("1", "Test FIRST_CHECK")}) + + if __name__ == "__main__": unittest.main() From dcd9cf8053703c3cc9269f761003326cf619547a Mon Sep 17 00:00:00 2001 From: KernelPanicNT <76663133+KernelPanicNT@users.noreply.github.com> Date: Mon, 19 Oct 2026 17:11:29 +0000 Subject: [PATCH 124/269] fix(libSceLoginDialog): return the module's error codes and check the open param Replace the 0x80B8xxxx common-dialog codes with the module's 0x8134xxxx codes, validate the sceLoginDialogOpen param, and write the module's result words from sceLoginDialogGetResult. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceLoginDialog/Export.cpp | 39 ++++++++++++----- core/libs/tests/GuestLoginDialog.cpp | 49 ++++++++++++++++------ docs/dev/debt/libSceLoginDialog.md | 4 ++ docs/dev/debt/libSceSigninDialog.md | 2 +- 4 files changed, 69 insertions(+), 25 deletions(-) diff --git a/core/libs/prx/libSceLoginDialog/Export.cpp b/core/libs/prx/libSceLoginDialog/Export.cpp index a986655ab2..dc34468961 100644 --- a/core/libs/prx/libSceLoginDialog/Export.cpp +++ b/core/libs/prx/libSceLoginDialog/Export.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -11,12 +12,29 @@ constexpr int COMMON_DIALOG_STATUS_NONE = 0; constexpr int COMMON_DIALOG_STATUS_INITIALIZED = 1; constexpr int COMMON_DIALOG_STATUS_RUNNING = 2; constexpr int COMMON_DIALOG_STATUS_FINISHED = 3; -constexpr int COMMON_DIALOG_ERROR_NOT_INITIALIZED = static_cast(0x80B80003u); -constexpr int COMMON_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast(0x80B80004u); -constexpr int COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast(0x80B80005u); -constexpr int COMMON_DIALOG_ERROR_BUSY = static_cast(0x80B80007u); -constexpr int COMMON_DIALOG_ERROR_ARG_NULL = static_cast(0x80B8000Du); +constexpr int COMMON_DIALOG_ERROR_NOT_INITIALIZED = static_cast(0x81340001u); +constexpr int COMMON_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast(0x81340002u); +constexpr int COMMON_DIALOG_ERROR_PARAM_INVALID = static_cast(0x81340003u); +constexpr int COMMON_DIALOG_ERROR_BUSY = static_cast(0x81340005u); +constexpr int COMMON_DIALOG_ERROR_NO_SERVICE = static_cast(0x81340007u); 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; +} std::atomic g_status{COMMON_DIALOG_STATUS_NONE}; @@ -31,10 +49,10 @@ int APS5_VABI sceLoginDialogInitialize(void) { } int APS5_VABI sceLoginDialogOpen(const void* param) { + if (!ValidOpenParam(param)) return COMMON_DIALOG_ERROR_PARAM_INVALID; 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; } @@ -48,13 +66,12 @@ int APS5_VABI sceLoginDialogGetStatus(void) { } int APS5_VABI sceLoginDialogGetResult(void* result) { + if (result == nullptr) return COMMON_DIALOG_ERROR_PARAM_INVALID; 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; + 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; } int APS5_VABI sceLoginDialogClose(void) { diff --git a/core/libs/tests/GuestLoginDialog.cpp b/core/libs/tests/GuestLoginDialog.cpp index cd631caa4e..4d260b1730 100644 --- a/core/libs/tests/GuestLoginDialog.cpp +++ b/core/libs/tests/GuestLoginDialog.cpp @@ -1,6 +1,9 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include +#include +#include extern "C" { int APS5_VABI sceLoginDialogInitialize(void); @@ -19,22 +22,37 @@ void Require(bool value) { if (!value) std::abort(); } constexpr int kStatusNone = 0; constexpr int kStatusInitialized = 1; constexpr int kStatusFinished = 3; -constexpr int kErrNotInitialized = static_cast(0x80B80003); -constexpr int kErrAlreadyInitialized = static_cast(0x80B80004); -constexpr int kErrNotFinished = static_cast(0x80B80005); -constexpr int kErrArgNull = static_cast(0x80B8000D); +constexpr int kErrNotInitialized = static_cast(0x81340001); +constexpr int kErrAlreadyInitialized = static_cast(0x81340002); +constexpr int kErrParamInvalid = static_cast(0x81340003); +constexpr int kErrBusy = static_cast(0x81340005); +constexpr int kErrNoService = static_cast(0x81340007); 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() { - std::int64_t param = 0; - std::int32_t result[2] = {-1, -1}; + const std::uint32_t param[16] = {0x40}; + std::int32_t result[4] = {-1, -1, -1, -1}; Require(sceLoginDialogTerminate() == kErrNotInitialized); - Require(sceLoginDialogOpen(¶m) == kErrNotInitialized); + Require(sceLoginDialogOpen(param) == kErrNotInitialized); Require(sceLoginDialogClose() == kErrNotInitialized); Require(sceLoginDialogGetResult(result) == kErrNotInitialized); + Require(result[0] == -1 && result[1] == -1); Require(sceLoginDialogGetStatus() == kStatusNone); Require(sceLoginDialogUpdateStatus() == kStatusNone); @@ -43,17 +61,22 @@ int main() { Require(sceLoginDialogGetStatus() == kStatusInitialized); Require(sceLoginDialogUpdateStatus() == kStatusInitialized); - Require(sceLoginDialogOpen(nullptr) == kErrArgNull); - Require(sceLoginDialogGetResult(result) == kErrNotFinished); + 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(¶m) == 0); + Require(sceLoginDialogOpen(param) == 0); Require(sceLoginDialogGetStatus() == kStatusFinished); Require(sceLoginDialogUpdateStatus() == kStatusFinished); - Require(sceLoginDialogGetResult(nullptr) == kErrArgNull); + Require(sceLoginDialogGetResult(nullptr) == kErrParamInvalid); Require(sceLoginDialogGetResult(result) == 0); - Require(result[0] == kResultUserCanceled); - Require(result[1] == -1); + RequireResult(result); Require(sceLoginDialogClose() == 0); Require(sceLoginDialogTerminate() == 0); diff --git a/docs/dev/debt/libSceLoginDialog.md b/docs/dev/debt/libSceLoginDialog.md index 5e631eb2be..86b8a12e12 100644 --- a/docs/dev/debt/libSceLoginDialog.md +++ b/docs/dev/debt/libSceLoginDialog.md @@ -3,3 +3,7 @@ ### Silent stubs - [libSceLoginDialog](../../../core/libs/prx/libSceLoginDialog/Export.cpp) - the login dialog runs the common-dialog state machine but shows nothing and reports the user as having cancelled + +### 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 diff --git a/docs/dev/debt/libSceSigninDialog.md b/docs/dev/debt/libSceSigninDialog.md index 9c05ac5649..5353db25a6 100644 --- a/docs/dev/debt/libSceSigninDialog.md +++ b/docs/dev/debt/libSceSigninDialog.md @@ -2,4 +2,4 @@ ### Unknown function info -- [sceSigninDialogClose, sceSigninDialogGetResult](../../../core/libs/prx/libSceSigninDialog/Export.cpp) (libSceSigninDialog) - SceSigninDialogResult layout (int32 result and 3 reserved int32 words, zeroed) follows [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/dialog.cpp) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/SigninDialog/SigninDialog.cs); error codes follow libSceLoginDialog's 0x80B8xxxx common-dialog codes rather than KytyPS5's 0x8135xxxx, and offline calls finish as user cancelled without opening a dialog. Neither choice is verified on hardware +- [sceSigninDialogClose, sceSigninDialogGetResult](../../../core/libs/prx/libSceSigninDialog/Export.cpp) (libSceSigninDialog) - SceSigninDialogResult layout (int32 result and 3 reserved int32 words, zeroed) follows [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/dialog.cpp) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/SigninDialog/SigninDialog.cs); error codes are the 0x80B8xxxx common-dialog codes rather than KytyPS5's 0x8135xxxx, and offline calls finish as user cancelled without opening a dialog. Neither choice is verified on hardware From 4c8daaf25aedf056f05479d16e110c7333ae8b0b Mon Sep 17 00:00:00 2001 From: maximaxi01 Date: Sun, 18 Oct 2026 21:54:33 -0400 Subject: [PATCH 125/269] fix(tools): ignore CMake literals when checking test registration --- tools/check_test_registration.py | 3 ++- tools/tests/test_test_registration.py | 37 +++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/tools/check_test_registration.py b/tools/check_test_registration.py index da156692b5..3a8baf0193 100644 --- a/tools/check_test_registration.py +++ b/tools/check_test_registration.py @@ -53,8 +53,9 @@ def built(build): def commands(content): content = LEXICAL.sub(lambda match: re.sub(r"[^\n]", " ", match.group()) if match.group("comment") is not None else match.group(), content) + code = LEXICAL.sub(lambda match: re.sub(r"[^\n]", "_", match.group()), content) parsed_until = 0 - for command in COMMAND.finditer(content): + for command in COMMAND.finditer(code): if command.start() < parsed_until: continue position, arguments = command.end(), [] diff --git a/tools/tests/test_test_registration.py b/tools/tests/test_test_registration.py index b53754fcb7..9437bd0b11 100644 --- a/tools/tests/test_test_registration.py +++ b/tools/tests/test_test_registration.py @@ -12,6 +12,43 @@ import check_test_registration class RegistrationTests(unittest.TestCase): + def test_literal_prefixes_cannot_expose_commands_inside_arguments(self): + prefixes = (r"\#", r"\(", '"prefix" ', '"" ', '"line\nprefix" ', "[[prefix]] ", + "[=[prefix]=] ", "[===[line\nprefix]===] ") + for prefix in prefixes: + with self.subTest(prefix=prefix): + content = (f"set(example\n{prefix}add_test(ghost /build/tests/missing_tests)\n)\n" + 'add_test("real" "/build/tests/real_tests")\n') + self.assertEqual(self.registered(content), {"real_tests"}) + + def test_commands_inside_literals_do_not_register_tests(self): + for opening, closing in (('"', '"'), ("[[", "]]"), ("[=[", "]=]"), ("[===[", "]===]")): + with self.subTest(opening=opening): + content = (f"set(example {opening}\nadd_test(ghost /build/tests/missing_tests)\n{closing})\n" + 'add_test("real" "/build/tests/real_tests")\n') + self.assertEqual(self.registered(content), {"real_tests"}) + + def test_properties_inside_literals_do_not_disable_a_real_test(self): + for opening, closing in (('"', '"'), ("[[", "]]"), ("[=[", "]=]"), ("[===[", "]===]")): + with self.subTest(opening=opening): + content = ('add_test("real" "/build/tests/real_tests")\n' + f"set(example {opening}\nset_tests_properties(real PROPERTIES DISABLED TRUE)\n{closing})\n") + self.assertEqual(self.registered(content), {"real_tests"}) + + def test_subdirectories_inside_literals_are_not_followed(self): + with tempfile.TemporaryDirectory() as directory: + build = Path(directory) + ignored = build / "ignored" + ignored.mkdir() + (ignored / "CTestTestfile.cmake").write_text( + 'add_test("ghost" "/build/tests/missing_tests")\n', encoding="utf-8") + for opening, closing in (('"', '"'), ("[[", "]]"), ("[=[", "]=]"), ("[===[", "]===]")): + with self.subTest(opening=opening): + content = (f"set(example {opening}\nsubdirs(ignored)\n{closing})\n" + 'add_test("real" "/build/tests/real_tests")\n') + (build / "CTestTestfile.cmake").write_text(content, encoding="utf-8") + self.assertEqual(check_test_registration.registered(build), {"real_tests"}) + def test_disabled_property_is_local_to_each_configuration(self): for configurations in (("OFF", "TRUE"), ("TRUE", "OFF"), ("TRUE", "TRUE")): with self.subTest(configurations=configurations): From 53617c5a6a547bafd4be84dd0d98c1e34d208aa8 Mon Sep 17 00:00:00 2001 From: pushkarreddyy <212133856+pushkarreddyy@users.noreply.github.com> Date: Sun, 18 Oct 2026 20:00:28 +0530 Subject: [PATCH 126/269] feat(libSceAcm): validate context and batch parameters in batch wait and lifecycle exports --- core/libs/prx/libSceAcm/Export.cpp | 64 ++++++++++++++++++++++-------- core/libs/tests/GuestAcm.cpp | 49 +++++++++++++++++------ docs/dev/debt/libSceAcm.md | 2 +- 3 files changed, 85 insertions(+), 30 deletions(-) diff --git a/core/libs/prx/libSceAcm/Export.cpp b/core/libs/prx/libSceAcm/Export.cpp index 97dfc19d91..1cdd2fc5dd 100644 --- a/core/libs/prx/libSceAcm/Export.cpp +++ b/core/libs/prx/libSceAcm/Export.cpp @@ -1,54 +1,84 @@ #include #include +#include +#include +#include +#include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" -#include -// Audio co-processor batches complete immediately without producing output; audio is silent. -static constexpr int SCE_ACM_ERROR_INVALID_PARAMETER = static_cast(0x80E30002); -static std::atomic g_nextId{1}; +namespace { + +constexpr int SCE_ACM_ERROR_BAD_CONTEXT = static_cast(0x81940002); +constexpr int SCE_ACM_ERROR_BAD_POINTER = static_cast(0x81940006); + +std::mutex g_mutex; +std::unordered_set g_contexts; +std::atomic g_nextId{1}; + +} extern "C" { int APS5_VABI sceAcmBatchStartBuffer(AcmContextId context, const void* batch_commands, size_t batch_size, AcmBatchError* batch_error, AcmBatchId* batch) { - (void)context; (void)batch_commands; (void)batch_size; - (void)batch_error; - if (!batch) return SCE_ACM_ERROR_INVALID_PARAMETER; - *batch = g_nextId.fetch_add(1, std::memory_order_relaxed); + if (!batch) return SCE_ACM_ERROR_BAD_POINTER; + if (context == 0) return SCE_ACM_ERROR_BAD_CONTEXT; + std::lock_guard lock(g_mutex); + if (!g_contexts.contains(context)) return SCE_ACM_ERROR_BAD_CONTEXT; + AcmBatchId id = g_nextId.fetch_add(1, std::memory_order_relaxed); + while (id == 0) id = g_nextId.fetch_add(1, std::memory_order_relaxed); + if (batch_error) std::memset(batch_error, 0, sizeof(*batch_error)); + *batch = id; return 0; } int APS5_VABI sceAcmBatchStartBuffers(AcmContextId context, uint32_t batch_info_count, const AcmBatchInfo* const batch_info[], AcmBatchError* batch_error, AcmBatchId* batch) { - (void)context; (void)batch_info_count; (void)batch_info; - (void)batch_error; - if (!batch) return SCE_ACM_ERROR_INVALID_PARAMETER; - *batch = g_nextId.fetch_add(1, std::memory_order_relaxed); + if (!batch) return SCE_ACM_ERROR_BAD_POINTER; + if (context == 0) return SCE_ACM_ERROR_BAD_CONTEXT; + std::lock_guard lock(g_mutex); + if (!g_contexts.contains(context)) return SCE_ACM_ERROR_BAD_CONTEXT; + AcmBatchId id = g_nextId.fetch_add(1, std::memory_order_relaxed); + while (id == 0) id = g_nextId.fetch_add(1, std::memory_order_relaxed); + if (batch_error) std::memset(batch_error, 0, sizeof(*batch_error)); + *batch = id; return 0; } int APS5_VABI sceAcmBatchWait(AcmContextId context, AcmBatchId batch, uint32_t timeout) { - (void)context; (void)batch; (void)timeout; + if (context == 0) return SCE_ACM_ERROR_BAD_CONTEXT; + std::lock_guard lock(g_mutex); + if (!g_contexts.contains(context)) return SCE_ACM_ERROR_BAD_CONTEXT; return 0; } int APS5_VABI sceAcmContextCreate(AcmContextId* context) { - if (!context) return SCE_ACM_ERROR_INVALID_PARAMETER; - *context = g_nextId.fetch_add(1, std::memory_order_relaxed); + if (!context) return SCE_ACM_ERROR_BAD_POINTER; + AcmContextId id = g_nextId.fetch_add(1, std::memory_order_relaxed); + while (id == 0) id = g_nextId.fetch_add(1, std::memory_order_relaxed); + { + std::lock_guard lock(g_mutex); + g_contexts.insert(id); + } + *context = id; return 0; } int APS5_VABI sceAcmContextDestroy(AcmContextId context) { - (void)context; + if (context == 0) return SCE_ACM_ERROR_BAD_CONTEXT; + std::lock_guard lock(g_mutex); + auto it = g_contexts.find(context); + if (it == g_contexts.end()) return SCE_ACM_ERROR_BAD_CONTEXT; + g_contexts.erase(it); return 0; } -// Builds a convolution-reverb command into a batch; batches execute as no-ops, so nothing is encoded. int APS5_VABI sceAcm_ConvReverb_SharedInput(void) { return 0; } diff --git a/core/libs/tests/GuestAcm.cpp b/core/libs/tests/GuestAcm.cpp index e360f84445..c6691f898d 100644 --- a/core/libs/tests/GuestAcm.cpp +++ b/core/libs/tests/GuestAcm.cpp @@ -18,24 +18,49 @@ static void Require(bool value) { if (!value) std::abort(); } -int main() { - std::uint32_t context = 0; - Require(sceAcmContextCreate(nullptr) != 0); - Require(sceAcmContextCreate(&context) == 0 && context != 0); +constexpr int BadContext = static_cast(0x81940002); +constexpr int BadPointer = static_cast(0x81940006); - std::uint32_t batch = 0; - Require(sceAcmBatchStartBuffer(context, nullptr, 0, nullptr, nullptr) != 0); - Require(sceAcmBatchStartBuffer(context, nullptr, 0, nullptr, &batch) == 0 && batch != 0); - Require(sceAcmBatchWait(context, batch, 0) == 0); +int main() { + Require(sceAcmContextCreate(nullptr) == BadPointer); + Require(sceAcmContextDestroy(0) == BadContext); + Require(sceAcmContextDestroy(999999) == BadContext); + + std::uint32_t context1 = 0; + Require(sceAcmContextCreate(&context1) == 0 && context1 != 0); + + std::uint32_t context2 = 0; + Require(sceAcmContextCreate(&context2) == 0 && context2 != 0 && context2 != context1); + + std::uint32_t batch1 = 0; + Require(sceAcmBatchStartBuffer(0, nullptr, 0, nullptr, &batch1) == BadContext); + Require(sceAcmBatchStartBuffer(999999, nullptr, 0, nullptr, &batch1) == BadContext); + Require(sceAcmBatchStartBuffer(context1, nullptr, 0, nullptr, nullptr) == BadPointer); + Require(sceAcmBatchStartBuffer(context1, nullptr, 0, nullptr, &batch1) == 0 && batch1 != 0); + + Require(sceAcmBatchWait(0, batch1, 0) == BadContext); + Require(sceAcmBatchWait(999999, batch1, 0) == BadContext); + Require(sceAcmBatchWait(context1, batch1, 0) == 0); + Require(sceAcmBatchWait(context1, batch1, 0) == 0); std::uint32_t batch2 = 0; - Require(sceAcmBatchStartBuffers(context, 0, nullptr, nullptr, nullptr) != 0); - Require(sceAcmBatchStartBuffers(context, 0, nullptr, nullptr, &batch2) == 0 && batch2 != 0); - Require(sceAcmBatchWait(context, batch2, 0) == 0); + Require(sceAcmBatchStartBuffers(0, 0, nullptr, nullptr, &batch2) == BadContext); + Require(sceAcmBatchStartBuffers(999999, 0, nullptr, nullptr, &batch2) == BadContext); + Require(sceAcmBatchStartBuffers(context2, 0, nullptr, nullptr, nullptr) == BadPointer); + Require(sceAcmBatchStartBuffers(context2, 0, nullptr, nullptr, &batch2) == 0 && batch2 != 0); + Require(sceAcmBatchWait(context2, batch2, 0) == 0); Require(sceAcm_ConvReverb_SharedInput() == 0); Require(sceAcm_FFT() == 0); Require(sceAcm_Panner() == 0); - Require(sceAcmContextDestroy(context) == 0); + Require(sceAcmContextDestroy(context1) == 0); + Require(sceAcmContextDestroy(context1) == BadContext); + Require(sceAcmBatchStartBuffer(context1, nullptr, 0, nullptr, &batch1) == BadContext); + Require(sceAcmBatchWait(context1, batch1, 0) == BadContext); + + Require(sceAcmContextDestroy(context2) == 0); + Require(sceAcmContextDestroy(context2) == BadContext); + + return 0; } diff --git a/docs/dev/debt/libSceAcm.md b/docs/dev/debt/libSceAcm.md index c5c9b90f30..9b0f7a5099 100644 --- a/docs/dev/debt/libSceAcm.md +++ b/docs/dev/debt/libSceAcm.md @@ -6,5 +6,5 @@ ### 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. - 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). - From 25e700faba5889353d81aed72132f74f864e903d Mon Sep 17 00:00:00 2001 From: ANONYMOUSZED-beep <72853699+ANONYMOUSZED-beep@users.noreply.github.com> Date: Mon, 19 Oct 2026 06:58:14 +0000 Subject: [PATCH 127/269] perf(agc): cache the main image's page states on Linux --- .../Execution/src/GuestMemory.cpp | 27 +++++++++++++++++++ .../tests/GuestAllocations.cpp | 10 +++++++ 2 files changed, 37 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index 6ea26ec9ab..bfcafce337 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -35,6 +35,7 @@ #include #include #include +#include #include #include #include @@ -494,6 +495,26 @@ void ForgetInaccessiblePages(std::uintptr_t address, std::size_t bytes) { Pages().forgetInaccessible(address, bytes); } +#ifndef _WIN32 +int mainImageExtent(dl_phdr_info* info, std::size_t, void* data) { + constexpr auto mask = static_cast(PageBytes - 1); + auto& span = *static_cast(data); + std::uintptr_t first = std::numeric_limits::max(); + std::uintptr_t last = 0; + for (int index = 0; index < info->dlpi_phnum; ++index) { + const auto& header = info->dlpi_phdr[index]; + if (header.p_type != PT_LOAD || header.p_memsz == 0) continue; + first = std::min(first, static_cast(info->dlpi_addr + header.p_vaddr) & ~mask); + last = std::max(last, static_cast(info->dlpi_addr + header.p_vaddr + header.p_memsz + mask) & ~mask); + } + if (first < last) { + span.base = first; + span.size = static_cast(last - first); + } + return 1; +} +#endif + void PageStates::initialize() { if (ready.load(std::memory_order_acquire)) return; std::call_once(once, [&] { @@ -516,6 +537,12 @@ void PageStates::initialize() { image.size = cursor - start; imageCached = image.allocate(); } +#else + static const bool noImageCache = std::getenv("APS5_NO_IMAGE_PAGE_CACHE") != nullptr; + if (!noImageCache) { + dl_iterate_phdr(mainImageExtent, &image); + imageCached = image.allocate(); + } #endif if (arenaCached || imageCached) { GuestAllocations::GuestAllocationsSetInvalidator_nid_postfix(&ForgetPages); diff --git a/core/libs/prx/libSceAgcDriver/tests/GuestAllocations.cpp b/core/libs/prx/libSceAgcDriver/tests/GuestAllocations.cpp index 73c243f198..89d83afd29 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GuestAllocations.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/GuestAllocations.cpp @@ -258,6 +258,16 @@ void RunUnmappedGapTests() { Require(!GuestMemory::Accessible(reinterpret_cast(std::uintptr_t{0x18}), 4), "a near-null address counts as accessible"); munmap(block, page); munmap(block + 2 * page, page); + alignas(4096) static std::array imagePage{}; + const auto imageAddress = reinterpret_cast(imagePage.data()); + Require(GuestMemory::Accessible(imagePage.data(), page, true), "a main image data page is not writable"); + Require(mprotect(imagePage.data(), page, PROT_READ) == 0, "cannot protect the main image test page behind the registry"); + Require(GuestMemory::Accessible(imagePage.data(), page, true), "a checked main image page was queried again instead of read from the page table"); + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(imageAddress, page); + Require(!GuestMemory::Accessible(imagePage.data(), page, true) && GuestMemory::Accessible(imagePage.data(), page), "an invalidated main image page kept its old access"); + Require(mprotect(imagePage.data(), page, PROT_READ | PROT_WRITE) == 0, "cannot restore the main image test page"); + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(imageAddress, page); + Require(GuestMemory::Accessible(imagePage.data(), page, true), "a restored main image page is not writable"); if (!GuestArena::GuestArenaAvailable_nid_postfix()) return; auto* guest = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(2 * page, page)); Require(mmap(guest, 2 * page, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0) == guest, "cannot map the arena page test pages"); From 2ae47ba41f971f2084311fc34ed6397aa411a45b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Mon, 19 Oct 2026 19:26:53 +0200 Subject: [PATCH 128/269] fix(agc): guard the Linux image span with __linux__ so macOS keeps the uncached path --- core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index bfcafce337..60a6cb71d7 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -495,7 +495,7 @@ void ForgetInaccessiblePages(std::uintptr_t address, std::size_t bytes) { Pages().forgetInaccessible(address, bytes); } -#ifndef _WIN32 +#ifdef __linux__ int mainImageExtent(dl_phdr_info* info, std::size_t, void* data) { constexpr auto mask = static_cast(PageBytes - 1); auto& span = *static_cast(data); @@ -537,7 +537,7 @@ void PageStates::initialize() { image.size = cursor - start; imageCached = image.allocate(); } -#else +#elif defined(__linux__) static const bool noImageCache = std::getenv("APS5_NO_IMAGE_PAGE_CACHE") != nullptr; if (!noImageCache) { dl_iterate_phdr(mainImageExtent, &image); From 173c827c6511a4a10171731aeea176dd4067c016 Mon Sep 17 00:00:00 2001 From: al3w0f205 Date: Fri, 16 Oct 2026 08:44:21 -0500 Subject: [PATCH 129/269] feat(libSceBluetoothHid): export missing firmware functions from database --- core/libs/prx/libSceBluetoothHid/Export.cpp | 45 +++++++++++++++++++++ core/libs/tests/GuestBluetoothHid.cpp | 40 ++++++++++++++++++ docs/dev/debt/libSceBluetoothHid.md | 1 + 3 files changed, 86 insertions(+) diff --git a/core/libs/prx/libSceBluetoothHid/Export.cpp b/core/libs/prx/libSceBluetoothHid/Export.cpp index 46e728e4fd..98a466cddf 100644 --- a/core/libs/prx/libSceBluetoothHid/Export.cpp +++ b/core/libs/prx/libSceBluetoothHid/Export.cpp @@ -49,4 +49,49 @@ int APS5_VABI sceBluetoothHidSetOutputReport() { return 0; } +int APS5_VABI sceBluetoothHidDebugGetVersion() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBluetoothHidDisconnectDevice() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBluetoothHidGetDeviceInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBluetoothHidGetFeatureReport() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBluetoothHidGetReportDescriptor() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBluetoothHidInterruptOutput() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBluetoothHidParamInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBluetoothHidSetFeatureReport() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBluetoothHidThreadParamInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/tests/GuestBluetoothHid.cpp b/core/libs/tests/GuestBluetoothHid.cpp index f85b1d9ae1..67b25d22c6 100644 --- a/core/libs/tests/GuestBluetoothHid.cpp +++ b/core/libs/tests/GuestBluetoothHid.cpp @@ -12,6 +12,11 @@ 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 sceBluetoothHidDebugGetVersion(); +int APS5_VABI sceBluetoothHidGetDeviceInfo(); +int APS5_VABI sceBluetoothHidDisconnectDevice(); +int APS5_VABI sceBluetoothHidGetFeatureReport(); +int APS5_VABI sceBluetoothHidSetFeatureReport(); } namespace { @@ -51,6 +56,41 @@ int main() { threw = true; } Require(threw, "an input report without a device did not throw"); + threw = false; + try { + sceBluetoothHidDebugGetVersion(); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw, "debug get version did not throw"); + threw = false; + try { + sceBluetoothHidGetDeviceInfo(); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw, "get device info did not throw"); + threw = false; + try { + sceBluetoothHidDisconnectDevice(); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw, "disconnect device did not throw"); + threw = false; + try { + sceBluetoothHidGetFeatureReport(); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw, "get feature report did not throw"); + threw = false; + try { + sceBluetoothHidSetFeatureReport(); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw, "set feature report did not throw"); Require(sceBluetoothHidUnregisterDevice(0x44f, 0x1000) == 0, "device unregistration failed"); Require(sceBluetoothHidUnregisterCallback() == 0, "callback unregistration failed"); std::puts("BluetoothHid tests passed"); diff --git a/docs/dev/debt/libSceBluetoothHid.md b/docs/dev/debt/libSceBluetoothHid.md index d5e47f67fa..b8ec7bf2ab 100644 --- a/docs/dev/debt/libSceBluetoothHid.md +++ b/docs/dev/debt/libSceBluetoothHid.md @@ -7,3 +7,4 @@ ### 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 From 4436a42ad8c68e33f580a58efd0f26c29bac64fd Mon Sep 17 00:00:00 2001 From: emirplus Date: Mon, 19 Oct 2026 20:27:35 +0300 Subject: [PATCH 130/269] fix(tools): create parent directory for body output in prepare_release_notes --- tools/prepare_release_notes.py | 1 + tools/tests/test_package_release.py | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/tools/prepare_release_notes.py b/tools/prepare_release_notes.py index 253ac3679d..0163999059 100644 --- a/tools/prepare_release_notes.py +++ b/tools/prepare_release_notes.py @@ -34,6 +34,7 @@ def prepare_notes(source, body, full): raise IsADirectoryError(str(destination)) if os.name == "nt" and destination.exists() and destination.stat().st_file_attributes & stat.FILE_ATTRIBUTE_READONLY: raise PermissionError(f"Release note destination is read-only: {destination}") + body.parent.mkdir(parents=True, exist_ok=True) full.parent.mkdir(parents=True, exist_ok=True) full.write_bytes(notes.encode("utf-8")) body.write_bytes(release_body(notes).encode("utf-8")) diff --git a/tools/tests/test_package_release.py b/tools/tests/test_package_release.py index b3487ae20c..e03d526eb4 100644 --- a/tools/tests/test_package_release.py +++ b/tools/tests/test_package_release.py @@ -556,9 +556,10 @@ class ReleaseNotesEdgeCaseTests(unittest.TestCase): directory = Path(directory) source_path = directory / "source.json" source_path.write_text(source, encoding="utf-8") - body_path = directory / "nested" / "body.md" - full_path = directory / "nested" / "full.md" + body_path = directory / "nested_body" / "body.md" + full_path = directory / "nested_full" / "full.md" prepare_release_notes.prepare_notes(source_path, body_path, full_path) + self.assertEqual(body_path.read_text(encoding="utf-8"), notes) self.assertEqual(full_path.read_text(encoding="utf-8"), notes) def test_body_over_limit_without_whats_changed_is_returned_unchanged(self): From 84546771871f9cc9b9a2e510951c5ad408d4338f Mon Sep 17 00:00:00 2001 From: primesoul-web Date: Mon, 19 Oct 2026 17:29:10 +0000 Subject: [PATCH 131/269] feat(libs): the sceImeOpen text and external-keyboard filters The text filter follows KytyPS5's contract; the external keyboard filter is stored on the session and is not called for the host input path, which delivers no key data. --- core/libs/prx/libSceIme/Export.cpp | 75 +++++++++++-- core/libs/tests/GuestImeOpen.cpp | 165 +++++++++++++++++++++++++++-- docs/dev/debt/libSceIme.md | 3 +- 3 files changed, 225 insertions(+), 18 deletions(-) diff --git a/core/libs/prx/libSceIme/Export.cpp b/core/libs/prx/libSceIme/Export.cpp index 575463cf73..49e108d0da 100644 --- a/core/libs/prx/libSceIme/Export.cpp +++ b/core/libs/prx/libSceIme/Export.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "prx/libSceIme/include/ImeInput.hpp" @@ -59,6 +60,7 @@ constexpr uint32_t CaretMoveRight = 2; struct Session { Param param{}; + ImeExtKeyboardFilter extKeyboardFilter = nullptr; std::u16string text; std::uint32_t caret = 0; std::deque events; @@ -157,6 +159,64 @@ void QueueCaretSteps(Session& session, uint32_t direction, std::size_t steps) { } } +struct EditDelta { + std::uint32_t index = 0; + std::int32_t length = 0; +}; + +EditDelta ComputeEditDelta(const std::u16string& before, const std::u16string& after) { + std::size_t prefix = 0; + while (prefix < before.size() && prefix < after.size() && before[prefix] == after[prefix]) ++prefix; + std::size_t beforeTail = before.size(); + std::size_t afterTail = after.size(); + while (beforeTail > prefix && afterTail > prefix && before[beforeTail - 1] == after[afterTail - 1]) { + --beforeTail; + --afterTail; + } + return {static_cast(prefix), + static_cast(afterTail - prefix) - static_cast(beforeTail - prefix)}; +} + +void NormalizeCaret(Session& session) { + if (session.caret > session.text.size()) session.caret = static_cast(session.text.size()); + if (session.caret > 0 && session.caret < session.text.size() && + IsHighSurrogate(session.text[session.caret - 1]) && IsLowSurrogate(session.text[session.caret])) { + ++session.caret; + } +} + +bool RunTextFilter(const Session& session, std::u16string* filtered) { + std::vector buffer(session.param.max_text_length + 1, u'\0'); + std::uint32_t length = session.param.max_text_length; + if (session.param.filter(buffer.data(), &length, session.text.c_str(), + static_cast(session.text.size())) != 0) { + return false; + } + if (length > session.param.max_text_length || + !ValidText(buffer.data(), length, (session.param.option & OptionMultiline) != 0)) { + return false; + } + filtered->assign(buffer.data(), buffer.data() + length); + return true; +} + +void CommitEdit(Session& session, const std::u16string& oldText, std::uint32_t plainIndex, std::int32_t plainDelta) { + if (session.param.filter) { + std::u16string filtered; + if (RunTextFilter(session, &filtered)) { + session.text = std::move(filtered); + NormalizeCaret(session); + } + if (session.text == oldText) return; + SyncBuffers(session); + const EditDelta edit = ComputeEditDelta(oldText, session.text); + QueueText(session, EventUpdateText, edit.index, edit.length); + return; + } + SyncBuffers(session); + QueueText(session, EventUpdateText, plainIndex, plainDelta); +} + void Insert(Session& session, std::u16string units) { const bool multiline = (session.param.option & OptionMultiline) != 0; std::u16string accepted; @@ -169,10 +229,10 @@ void Insert(Session& session, std::u16string units) { if (count > 0 && count < accepted.size() && IsHighSurrogate(accepted[count - 1])) --count; if (count == 0) return; const uint32_t index = session.caret; + const std::u16string oldText = session.text; session.text.insert(index, accepted, 0, count); session.caret += static_cast(count); - SyncBuffers(session); - QueueText(session, EventUpdateText, index, static_cast(count)); + CommitEdit(session, oldText, index, static_cast(count)); } std::size_t UnitsBefore(const Session& session) { @@ -198,18 +258,18 @@ void Apply(Session& session, const ImeHostInput& input) { case ImeHostInputKind::Backspace: { const std::size_t units = UnitsBefore(session); if (units == 0) return; + const std::u16string oldText = session.text; session.caret -= static_cast(units); session.text.erase(session.caret, units); - SyncBuffers(session); - QueueText(session, EventUpdateText, session.caret, -static_cast(units)); + CommitEdit(session, oldText, session.caret, -static_cast(units)); return; } case ImeHostInputKind::Delete: { const std::size_t units = UnitsAfter(session); if (units == 0) return; + const std::u16string oldText = session.text; session.text.erase(session.caret, units); - SyncBuffers(session); - QueueText(session, EventUpdateText, session.caret, -static_cast(units)); + CommitEdit(session, oldText, session.caret, -static_cast(units)); return; } case ImeHostInputKind::Left: { @@ -317,10 +377,9 @@ int APS5_VABI sceImeOpen_nid_postfix(const Param* param, const ExtendedParam* ex std::lock_guard lock(g_imeMutex); if (g_session) return ErrorBusy; if (const int error = ValidateOpen(param, extended)) return error; - if (param->filter) NotImplemented_nid_no_patch("sceImeOpen: text filter"); - if (extended && extended->ext_keyboard_filter) NotImplemented_nid_no_patch("sceImeOpen: external keyboard filter"); Session session; session.param = *param; + session.extKeyboardFilter = extended ? extended->ext_keyboard_filter : nullptr; session.text.assign(param->input_text_buffer, TextLength(param->input_text_buffer, param->max_text_length)); session.caret = static_cast(session.text.size()); SyncBuffers(session); diff --git a/core/libs/tests/GuestImeOpen.cpp b/core/libs/tests/GuestImeOpen.cpp index 15029e736b..a1ea6fdc87 100644 --- a/core/libs/tests/GuestImeOpen.cpp +++ b/core/libs/tests/GuestImeOpen.cpp @@ -73,6 +73,58 @@ void APS5_VABI Other(void*, const ImeEvent*) { std::abort(); } +int32_t UpperFilter(char16_t* outText, uint32_t* outTextLength, const char16_t* sourceText, uint32_t sourceTextLength) { + for (uint32_t i = 0; i < sourceTextLength; ++i) { + const char16_t unit = sourceText[i]; + outText[i] = unit >= u'a' && unit <= u'z' ? static_cast(unit - u'a' + u'A') : unit; + } + *outTextLength = sourceTextLength; + return 0; +} + +int32_t FailingFilter(char16_t*, uint32_t*, const char16_t*, uint32_t) { + return 1; +} + +int32_t StripXFilter(char16_t* outText, uint32_t* outTextLength, const char16_t* sourceText, uint32_t sourceTextLength) { + uint32_t length = 0; + for (uint32_t i = 0; i < sourceTextLength; ++i) { + if (sourceText[i] != u'x') outText[length++] = sourceText[i]; + } + *outTextLength = length; + return 0; +} + +int32_t InvalidOutputFilter(char16_t* outText, uint32_t* outTextLength, const char16_t*, uint32_t) { + outText[0] = 0xdc00; + *outTextLength = 1; + return 0; +} + +int32_t TooLongFilter(char16_t*, uint32_t* outTextLength, const char16_t*, uint32_t) { + *outTextLength = 9; + return 0; +} + +int32_t ShortenFilter(char16_t* outText, uint32_t* outTextLength, const char16_t*, uint32_t) { + outText[0] = u'a'; + *outTextLength = 1; + return 0; +} + +int32_t PairAtCaretFilter(char16_t* outText, uint32_t* outTextLength, const char16_t*, uint32_t) { + outText[0] = u'a'; + outText[1] = u'b'; + outText[2] = 0xd83d; + outText[3] = 0xde00; + *outTextLength = 4; + return 0; +} + +int ExtNoop(const ImeKeycode*, std::uint16_t*, std::uint32_t*, void*) { + return 0; +} + void Require(bool value, const char* message) { if (value) return; std::fprintf(stderr, "%s\n", message); @@ -174,15 +226,17 @@ void CheckValidation() { Require(sceImeClose_nid_postfix() == 0, "close after over-2K open"); param = MakeParam(buffers, 16); - param.filter = reinterpret_cast(&Record); - bool threw = false; - try { - sceImeOpen_nid_postfix(¶m, nullptr); - } catch (const std::exception&) { - threw = true; - } - Require(threw, "text filter must throw"); - Require(!ImeCapturesKeyboard_nid_postfix(), "failed open left a session"); + param.filter = &UpperFilter; + Require(sceImeOpen_nid_postfix(¶m, nullptr) == 0, "open with a text filter"); + Require(ImeCapturesKeyboard_nid_postfix(), "text filter session not open"); + Require(sceImeClose_nid_postfix() == 0, "close the text filter session"); + + param = MakeParam(buffers, 16); + ExtendedParam extFilters{}; + extFilters.ext_keyboard_filter = &ExtNoop; + Require(sceImeOpen_nid_postfix(¶m, &extFilters) == 0, "open with an external keyboard filter"); + Require(ImeCapturesKeyboard_nid_postfix(), "external keyboard filter session not open"); + Require(sceImeClose_nid_postfix() == 0, "close the external keyboard filter session"); } void CheckEditing() { @@ -304,6 +358,98 @@ void CheckMultiline() { Require(sceImeClose_nid_postfix() == 0, "multiline close"); } +void CheckTextFilter() { + Buffers buffers; + std::memcpy(buffers.text, u"ab", sizeof(u"ab")); + Param param = MakeParam(buffers, 8); + param.filter = &UpperFilter; + ExtendedParam extFilters{}; + extFilters.ext_keyboard_filter = &ExtNoop; + Require(sceImeOpen_nid_postfix(¶m, &extFilters) == 0, "open with both filters"); + Drain(); + + Publish(ImeHostInputKind::Text, "c"); + RequireText(Drain().at(0), EventUpdateText, u"ABC", 3, 0, 1); + Require(std::u16string(buffers.text) == u"ABC", "filtered input buffer"); + Require(std::u16string(reinterpret_cast(buffers.work)) == u"ABC", "filtered work copy"); + + Publish(ImeHostInputKind::Backspace); + RequireText(Drain().at(0), EventUpdateText, u"AB", 2, 2, -1); + + Publish(ImeHostInputKind::Left); + RequireCaretSteps(Drain(), 1, 1); + Publish(ImeHostInputKind::Delete); + RequireText(Drain().at(0), EventUpdateText, u"A", 1, 1, -1); + + Require(sceImeSetText(u"xy", 2) == 0, "set text with a filter"); + Require(std::u16string(buffers.text) == u"xy", "set text is not filtered"); + Require(Drain().empty(), "set text with a filter queued an event"); + Require(sceImeClose_nid_postfix() == 0, "close the filtered session"); + + buffers = Buffers{}; + param = MakeParam(buffers, 8); + param.filter = &StripXFilter; + Require(sceImeOpen_nid_postfix(¶m, nullptr) == 0, "open with a stripping filter"); + Drain(); + Publish(ImeHostInputKind::Text, "x"); + Require(Drain().empty(), "a stripped edit sends no event"); + Require(std::u16string(buffers.text).empty(), "a stripped edit leaves the buffers"); + Publish(ImeHostInputKind::Text, "b"); + RequireText(Drain().at(0), EventUpdateText, u"b", 1, 0, 1); + Publish(ImeHostInputKind::Text, "x"); + Require(Drain().empty(), "a stripped edit sends no event again"); + Require(std::u16string(buffers.text) == u"b", "buffers after a stripped edit"); + Require(sceImeClose_nid_postfix() == 0, "close the stripping session"); + + param = MakeParam(buffers, 8); + std::memcpy(buffers.text, u"ab", sizeof(u"ab")); + param.filter = &FailingFilter; + Require(sceImeOpen_nid_postfix(¶m, nullptr) == 0, "open with a failing filter"); + Drain(); + Publish(ImeHostInputKind::Text, "c"); + RequireText(Drain().at(0), EventUpdateText, u"abc", 3, 2, 1); + Require(std::u16string(buffers.text) == u"abc", "a failed filter keeps the edit"); + Require(sceImeClose_nid_postfix() == 0, "close the failing session"); + + param = MakeParam(buffers, 8); + std::memcpy(buffers.text, u"ab", sizeof(u"ab")); + param.filter = &InvalidOutputFilter; + Require(sceImeOpen_nid_postfix(¶m, nullptr) == 0, "open with an invalid-output filter"); + Drain(); + Publish(ImeHostInputKind::Text, "c"); + RequireText(Drain().at(0), EventUpdateText, u"abc", 3, 2, 1); + Require(std::u16string(buffers.text) == u"abc", "an invalid filter output keeps the edit"); + Require(sceImeClose_nid_postfix() == 0, "close the invalid-output session"); + + param = MakeParam(buffers, 8); + std::memcpy(buffers.text, u"ab", sizeof(u"ab")); + param.filter = &TooLongFilter; + Require(sceImeOpen_nid_postfix(¶m, nullptr) == 0, "open with a too-long filter"); + Drain(); + Publish(ImeHostInputKind::Text, "c"); + RequireText(Drain().at(0), EventUpdateText, u"abc", 3, 2, 1); + Require(std::u16string(buffers.text) == u"abc", "an overlong filter output keeps the edit"); + Require(sceImeClose_nid_postfix() == 0, "close the too-long session"); + + param = MakeParam(buffers, 8); + std::memcpy(buffers.text, u"ab", sizeof(u"ab")); + param.filter = &ShortenFilter; + Require(sceImeOpen_nid_postfix(¶m, nullptr) == 0, "open with a shortening filter"); + Drain(); + Publish(ImeHostInputKind::Text, "c"); + RequireText(Drain().at(0), EventUpdateText, u"a", 1, 1, -1); + Require(sceImeClose_nid_postfix() == 0, "close the shortening session"); + + param = MakeParam(buffers, 8); + std::memcpy(buffers.text, u"ab", sizeof(u"ab")); + param.filter = &PairAtCaretFilter; + Require(sceImeOpen_nid_postfix(¶m, nullptr) == 0, "open with a pair filter"); + Drain(); + Publish(ImeHostInputKind::Text, "c"); + RequireText(Drain().at(0), EventUpdateText, u"ab\U0001F600", 4, 2, 2); + Require(sceImeClose_nid_postfix() == 0, "close the pair session"); +} + void CheckHostEvents() { Buffers buffers; Param param = MakeParam(buffers, 16); @@ -346,5 +492,6 @@ int main() { CheckValidation(); CheckEditing(); CheckMultiline(); + CheckTextFilter(); CheckHostEvents(); } diff --git a/docs/dev/debt/libSceIme.md b/docs/dev/debt/libSceIme.md index 76178899b3..0bbee7c5b0 100644 --- a/docs/dev/debt/libSceIme.md +++ b/docs/dev/debt/libSceIme.md @@ -7,10 +7,11 @@ ### Unknown function info - [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 -- [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`. A text filter or an external keyboard filter throws, as their contract is unknown. `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 +- [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 ### Functional - [sceImeKeyboardOpen, sceImeKeyboardClose](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - opening a keyboard that is already open for the same user returns `0x80bc0001` (busy) and closing one that is not open returns `0x80bc0002`, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/ime.cpp) does; it tracks one keyboard for all users, while the open state is kept per user id here, and the PS5 result is unverified - [sceImeKeyboardGetInfo](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) returns `0x80bc0023` (no resource id) for every id while a keyboard is open, since the IME keyboard never reports a connected device. [sceImeKeyboardSetMode](../../../core/libs/prx/libSceIme/Export.cpp) validates the mode bits but the mode has no effect, as no IME keyboard events are delivered. - [sceImeKeyboardGetResourceId](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) reports that no USB keyboard is connected (`0x80bc0004`, as [shadPS4](https://github.com/shadps4-emu/shadPS4) does) because IME keyboard events are not delivered to the handler, although [libSceKeyboard](../../../core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp) exposes the host keyboard +- [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/main/src/libs/ime.cpp), not confirmed on a PS5. 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. 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 From 64fa4c277d48ae659f8731c6e35f7dbf23b34395 Mon Sep 17 00:00:00 2001 From: Nithishvelan M Date: Mon, 19 Oct 2026 23:05:15 +0530 Subject: [PATCH 132/269] fix(libc): compute tgammaf in double to match FreeBSD msun and avoid host float rounding --- core/libs/prx/libc/src/Math.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index 8391b613f2..008700f589 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -344,7 +344,7 @@ long double APS5_VABI log1pl_nid_postfix(long double x) { return std::log1p(x); long double APS5_VABI log2l_nid_postfix(long double x) { return std::log2(x); } long double APS5_VABI lgammal_nid_postfix(long double x) { return std::lgamma(x); } long double APS5_VABI tgammal_nid_postfix(long double x) { return std::tgamma(x); } -float APS5_VABI tgammaf_nid_postfix(float x) { return std::tgamma(x); } +float APS5_VABI tgammaf_nid_postfix(float x) { return static_cast(std::tgamma(static_cast(x))); } long double APS5_VABI hypotl_nid_postfix(long double x, long double y) { if (std::isinf(x) || std::isinf(y)) return std::numeric_limits::infinity(); return std::hypot(x, y); From b51adcaf12436e8cb848e652939e3b9631726b03 Mon Sep 17 00:00:00 2001 From: emirplus Date: Mon, 19 Oct 2026 20:36:08 +0300 Subject: [PATCH 133/269] fix(tools): decode CMake cache with utf-8-sig in package_release --- tools/package_release.py | 4 ++-- tools/tests/test_package_release.py | 11 +++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/tools/package_release.py b/tools/package_release.py index 25474b757e..cc67ede80b 100644 --- a/tools/package_release.py +++ b/tools/package_release.py @@ -12,7 +12,7 @@ ROOT = Path(__file__).resolve().parent.parent def windows_runtime(build): - for line in (build / "CMakeCache.txt").read_text(encoding="utf-8").splitlines(): + for line in (build / "CMakeCache.txt").read_text(encoding="utf-8-sig").splitlines(): if line.startswith("CMAKE_CXX_COMPILER:"): compiler = line.partition("=")[2] if compiler and not compiler.endswith("-NOTFOUND"): @@ -24,7 +24,7 @@ def tracy_runtime(platform, build): cache = build / "CMakeCache.txt" if not cache.is_file(): return [] - lines = cache.read_text(encoding="utf-8").splitlines() + lines = cache.read_text(encoding="utf-8-sig").splitlines() enabled = next((line.partition("=")[2].upper() for line in lines if line.startswith("APS5_ENABLE_TRACY:")), "OFF") if enabled in ("", "0", "OFF", "NO", "FALSE", "N", "IGNORE", "NOTFOUND") or enabled.endswith("-NOTFOUND"): return [] diff --git a/tools/tests/test_package_release.py b/tools/tests/test_package_release.py index b3487ae20c..1c8adb476b 100644 --- a/tools/tests/test_package_release.py +++ b/tools/tests/test_package_release.py @@ -82,6 +82,17 @@ class ReleasePackageTests(unittest.TestCase): self.assertEqual(list(self.output.iterdir()), [previous]) self.assertEqual(previous.read_bytes(), b"existing output") + def test_windows_runtime_decodes_cmake_cache_with_utf8_bom(self): + compiler = self.root / "custom toolchain" / "bin" / "g++.exe" + (self.build / "CMakeCache.txt").write_bytes(b"\xef\xbb\xbfCMAKE_CXX_COMPILER:STRING=" + compiler.as_posix().encode("utf-8") + b"\n") + self.assertEqual(package_release.windows_runtime(self.build), compiler.parent) + + def test_tracy_runtime_decodes_cmake_cache_with_utf8_bom(self): + client = self.build / "core/libs/libs/libTracyClient.dll" + client.write_bytes(b"client") + (self.build / "CMakeCache.txt").write_bytes(b"\xef\xbb\xbfAPS5_ENABLE_TRACY:BOOL=ON\n") + self.assertEqual(package_release.tracy_runtime("windows", self.build), [client]) + def test_tracy_packages_include_only_the_target_platform_client(self): directory = self.build / "core/libs/libs" for platform, name in (("linux", "libTracyClient.so.0.14.1"), ("windows", "libTracyClient.dll")): From 1537dff78b9d10b767b06dcb481d3cb7a4bf76a4 Mon Sep 17 00:00:00 2001 From: Federico Marras Date: Mon, 19 Oct 2026 19:41:21 +0200 Subject: [PATCH 134/269] feat(libSceNet): accept sceNetClearDnsCache with no library DNS cache to drop --- core/libs/prx/libSceNet/Export.cpp | 4 ++-- core/libs/tests/GuestSceNet.cpp | 10 ++++++++++ docs/dev/debt/libSceNet.md | 2 +- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 945f71079c..d462a5cfd7 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -1773,8 +1773,8 @@ int APS5_VABI sceNetShowIfconfig() { 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(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceNetClearDnsCache(int flags) { + if (flags != 0) throw std::runtime_error("sceNetClearDnsCache: flags " + std::to_string(flags) + " are not supported"); return 0; } diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index 76d5b63109..6d0e9e2b1a 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -67,6 +67,7 @@ int APS5_VABI sceNetResolverStartAton(int, const void*, char*, int, int, int, in int APS5_VABI sceNetResolverAbort(int, int); int APS5_VABI sceNetResolverDestroy(int); int APS5_VABI sceNetResolverGetError(int, int*); +int APS5_VABI sceNetClearDnsCache(int); int APS5_VABI sceNetCtlGetState(int*); int APS5_VABI sceNetCtlGetIfStat(NetCtlIfStat*); int APS5_VABI sceNetCtlRegisterCallback(NetCtlCallback, void*, int*); @@ -1532,6 +1533,15 @@ int main() { Require(sceNetResolverAbort(other, 0) == static_cast(0x80410109) && *sceNetErrnoLoc() == 9); Require(sceNetResolverAbort(-1, 1) == static_cast(0x80410109) && *sceNetErrnoLoc() == 9); Require(sceNetResolverGetError(resolver, nullptr) == static_cast(0x80410116) && *sceNetErrnoLoc() == 22); + Require(sceNetClearDnsCache(0) == 0); + 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); + } Require(sceNetResolverDestroy(resolver) == 0); resolver_error = -1; Require(sceNetResolverGetError(resolver, &resolver_error) == static_cast(0x80410109) && diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index a0947b7725..8e421d1763 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -7,7 +7,7 @@ ### Unknown function info - [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 +- [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 - [sceNetGetInterfaceStats](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetSetDnsInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature From 81cd06b8bbd1f574ed8c45f7eec9f13ee5aa6494 Mon Sep 17 00:00:00 2001 From: Atharv Jairath <54663702+atharvjairath@users.noreply.github.com> Date: Mon, 19 Oct 2026 22:55:20 +0530 Subject: [PATCH 135/269] fix(http): make URI parsing independent of host locale --- core/libs/prx/libSceHttp/src/Uri.cpp | 23 +++++++++++++----- core/libs/tests/GuestHttp.cpp | 36 +++++++++++++++++++++++++--- docs/dev/debt/libSceHttp.md | 2 ++ 3 files changed, 52 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceHttp/src/Uri.cpp b/core/libs/prx/libSceHttp/src/Uri.cpp index 2268da4681..ac9424a75a 100644 --- a/core/libs/prx/libSceHttp/src/Uri.cpp +++ b/core/libs/prx/libSceHttp/src/Uri.cpp @@ -1,5 +1,4 @@ #include -#include #include #include #include @@ -34,10 +33,22 @@ struct UriParts { uint16_t port = 0; }; +bool isAsciiAlpha(char c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); +} + +bool isAsciiDigit(char c) { + return c >= '0' && c <= '9'; +} + +char toAsciiLower(char c) { + return c >= 'A' && c <= 'Z' ? static_cast(c + ('a' - 'A')) : c; +} + bool equalsIgnoreCase(std::string_view left, std::string_view right) { if (left.size() != right.size()) return false; for (size_t i = 0; i < left.size(); ++i) { - if (std::tolower(static_cast(left[i])) != std::tolower(static_cast(right[i]))) return false; + if (toAsciiLower(left[i]) != toAsciiLower(right[i])) return false; } return true; } @@ -49,7 +60,7 @@ uint16_t defaultPort(std::string_view scheme) { } bool isHostCharacter(char c) { - return std::isalnum(static_cast(c)) || c == '-' || c == '.' || c == '_'; + return isAsciiAlpha(c) || isAsciiDigit(c) || c == '-' || c == '.' || c == '_'; } std::string removeDotSegments(std::string_view input) { @@ -76,9 +87,9 @@ std::string removeDotSegments(std::string_view input) { size_t schemeLength(std::string_view uri) { size_t schemeEnd = 0; - if (!uri.empty() && std::isalpha(static_cast(uri[0]))) { + if (!uri.empty() && isAsciiAlpha(uri[0])) { schemeEnd = 1; - while (schemeEnd < uri.size() && (std::isalnum(static_cast(uri[schemeEnd])) || uri[schemeEnd] == '+' + while (schemeEnd < uri.size() && (isAsciiAlpha(uri[schemeEnd]) || isAsciiDigit(uri[schemeEnd]) || uri[schemeEnd] == '+' || uri[schemeEnd] == '-' || uri[schemeEnd] == '.')) ++schemeEnd; } return schemeEnd > 0 && schemeEnd < uri.size() && uri[schemeEnd] == ':' ? schemeEnd : 0; @@ -133,7 +144,7 @@ int parseUri(std::string_view uri, UriParts& parts) { if (port.size() > 5) return ERROR_INVALID_URL; uint32_t portValue = 0; for (const char c : port) { - if (!std::isdigit(static_cast(c))) return ERROR_INVALID_URL; + if (!isAsciiDigit(c)) return ERROR_INVALID_URL; portValue = portValue * 10 + static_cast(c - '0'); } if (portValue > 0xFFFF) return ERROR_INVALID_URL; diff --git a/core/libs/tests/GuestHttp.cpp b/core/libs/tests/GuestHttp.cpp index bd3a9b01c9..cb35456d8c 100644 --- a/core/libs/tests/GuestHttp.cpp +++ b/core/libs/tests/GuestHttp.cpp @@ -50,6 +50,27 @@ static void Require(bool value) { if (!value) std::abort(); } static bool Equal(const char* left, const char* right) { return std::strcmp(left, right) == 0; } +static void CheckUriParseCharacters() { + char pool[256]; + SceHttpUriElement element{}; + Require(sceHttpUriParse(&element, "HtTpS://AZaz09-._/path", pool, nullptr, sizeof(pool)) == 0); + Require(Equal(element.scheme, "HtTpS") && Equal(element.hostname, "AZaz09-._") && element.port == 443); + Require(sceHttpUriParse(&element, "aZ09+.-://host/", pool, nullptr, sizeof(pool)) == 0); + Require(Equal(element.scheme, "aZ09+.-") && element.port == 0); + for (unsigned value = 128; value <= 255; ++value) { + const std::string byte(1, static_cast(value)); + const std::string urls[] = { + "http://" + byte + "/", "http://[" + byte + "]/", byte + "://host/", + "a" + byte + "://host/", "http://host:" + byte + "/" + }; + for (const auto& url : urls) { + std::size_t required = 0; + Require(sceHttpUriParse(nullptr, url.c_str(), nullptr, &required, 0) == static_cast(0x80433060)); + Require(sceHttpUriParse(&element, url.c_str(), pool, nullptr, sizeof(pool)) == static_cast(0x80433060)); + } + } +} + static void CheckUriEscapeBytes() { constexpr char unreserved[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"; constexpr char hex[] = "0123456789ABCDEF"; @@ -163,13 +184,22 @@ int main() { Require(sceHttpUriEscape(escaped, nullptr, sizeof(escaped), "a b/~\xC3\xA9") == 0); Require(Equal(escaped, "a%20b%2F~%C3%A9")); Require(sceHttpUriEscape(escaped, nullptr, sizeof(escaped), nullptr) == invalidValue); - CheckUriEscapeBytes(); -#ifdef _WIN32 const std::string previousCtype = std::setlocale(LC_CTYPE, nullptr); + Require(std::setlocale(LC_CTYPE, "C") != nullptr); + CheckUriEscapeBytes(); + CheckUriParseCharacters(); +#ifdef _WIN32 Require(std::setlocale(LC_CTYPE, ".1252") != nullptr); CheckUriEscapeBytes(); - Require(std::setlocale(LC_CTYPE, previousCtype.c_str()) != nullptr); + CheckUriParseCharacters(); +#else + for (const char* locale : {"en_US.ISO8859-1", "en_US.iso88591", "en_US.UTF-8"}) { + if (!std::setlocale(LC_CTYPE, locale)) continue; + CheckUriEscapeBytes(); + CheckUriParseCharacters(); + } #endif + Require(std::setlocale(LC_CTYPE, previousCtype.c_str()) != nullptr); constexpr const char* unescapeCases[][2] = { {"", ""}, {"plain+text", "plain+text"}, {"a%20b%2F~%c3%a9", "a b/~\xC3\xA9"}, diff --git a/docs/dev/debt/libSceHttp.md b/docs/dev/debt/libSceHttp.md index cfcbd069b6..03d5b2b29e 100644 --- a/docs/dev/debt/libSceHttp.md +++ b/docs/dev/debt/libSceHttp.md @@ -22,6 +22,8 @@ ### Functional +- [sceHttpUriParse, sceHttpUriBuild, sceHttpUriMerge](../../../core/libs/prx/libSceHttp/src/Uri.cpp) - console behavior for non-ASCII scheme and hostname bytes remains unmeasured. ASCII character classification and scheme case folding follow [RFC 3986 sections 3.1 and 3.2](https://www.rfc-editor.org/rfc/rfc3986#section-3.1), independently of the host locale; parsing retains the existing C-locale rejection of those bytes. + - [sceHttpUriEscape](../../../core/libs/prx/libSceHttp/src/Uri.cpp) escapes every byte except the ASCII unreserved set in [RFC 3986 section 2.3](https://www.rfc-editor.org/rfc/rfc3986#section-2.3), independently of the host locale. Console behaviour for non-ASCII input remains unmeasured. - [sceHttpAuthCacheFlush, sceHttpCookieFlush](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - the context-id signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Net/Http.cs), and the NIDs `zzB0StvRab4` and `seCvUt91WHY` match [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp). The offline backend never stores authentication credentials or cookies, so flushing a live context succeeds with empty caches. Calls return `BEFORE_INIT` (0x80431001) when no context is alive and `INVALID_ID` (0x80431100) for an unknown, deleted or non-context handle, following the existing context lifecycle checks. These error codes and their precedence have not been measured on PS5 hardware; shadPS4 only stubs these calls. Network-backed caches remain unimplemented. From 196e2e2ada5c013b37731f5864e98af94ceb07fb Mon Sep 17 00:00:00 2001 From: Yuri Olive Date: Mon, 19 Oct 2026 14:55:22 -0300 Subject: [PATCH 136/269] feat(libSceNetCtl): export missing firmware interfaces --- core/libs/prx/libSceNetCtl/Export.cpp | 355 ++++++++++++++++++++++++++ docs/dev/debt/libSceNetCtl.md | 71 ++++++ 2 files changed, 426 insertions(+) diff --git a/core/libs/prx/libSceNetCtl/Export.cpp b/core/libs/prx/libSceNetCtl/Export.cpp index f29a351575..14540791d2 100644 --- a/core/libs/prx/libSceNetCtl/Export.cpp +++ b/core/libs/prx/libSceNetCtl/Export.cpp @@ -506,4 +506,359 @@ int APS5_VABI sceNetCtlUnregisterCallbackV6(int cid) { return UnregisterCallback(g_callbacksV6, cid); } +int APS5_VABI sceNetCtlRegisterCallbackForNpToolkit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlRegisterCallbackIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlRegisterCallbackForLibIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlUnregisterCallbackForNpToolkit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlUnregisterCallbackIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlUnregisterCallbackForLibIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlCheckCallbackForNpToolkit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlCheckCallbackForLibIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlClearEventIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlClearEventForNpToolkit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlClearEventForLibIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetResultIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetStateIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetState2IpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetInfoIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetEtherLinkMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetWifiType(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlConnectIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlConnectWithRetryIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlConnectConfIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlDisconnectIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlScanIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetScanInfoBssidIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetScanInfoByBssidIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetScanInfoForSsidScanIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetScanInfoBssidForSsidListScanIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetScanInfoForSsidListScanIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlSetErrorNotificationEnabledIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlRegisterCallbackV6IpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlUnregisterCallbackV6IpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetResultV6(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetResultV6IpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetStateV6IpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetInfoV6IpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetNatInfoIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlSetStunWithPaddingFlagIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlUnsetStunWithPaddingFlagIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlIsBandwidthManagementEnabledIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlDisableBandwidthManagementIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlEnableBandwidthManagementIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetBandwidthInfoIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetNetEvConfigInfoIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetConnectionElapsedTimeIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlApRpCheckCallback(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlApRpClearEvent(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlApRpRegisterCallback(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlApRpUnregisterCallback(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlApRpGetResult(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlApRpGetState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlApRpGetInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlApRpStart(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlApCpStart(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlApAppStartWithRetryPid(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlApAppStartWithRetry(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlApRpStartWithRetry(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlApRpStartConf(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlApRpStop(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlApCpStop(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlApAppInitWpaKey(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlApAppInitWpaKeyForQa(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlApRestart(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetBweCheckCallbackIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetBweClearEventIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetBweRegisterCallbackIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetBweUnregisterCallbackIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetBweGetInfoIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetBweStartInternetConnectionTestIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetBweFinishInternetConnectionTestIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetBweStartInternetConnectionTestBandwidthTestIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetBweSetInternetConnectionTestResultIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetWifiInfoIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/docs/dev/debt/libSceNetCtl.md b/docs/dev/debt/libSceNetCtl.md index cd450e632c..6f0c1ed20f 100644 --- a/docs/dev/debt/libSceNetCtl.md +++ b/docs/dev/debt/libSceNetCtl.md @@ -7,6 +7,77 @@ ### Unknown function info - [sceNetCtlGetInfo](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - answers from the host interface that holds an IPv4 address, preferring the one that carries the default route: its address, netmask, gateway, MTU and first two IPv4 DNS servers. The device is reported as wired, the link as connected, the IP configuration as DHCP and the HTTP proxy as off, whatever the host uses, and the Ethernet address is the one `sceNetGetMacAddress` reports. A gateway or DNS server the interface does not have is reported as `0.0.0.0`; what a console reports then is unknown. The DHCP host name (12), the PPPoE user name (13), 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 +- [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 +- [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 +- [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 +- [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) - unknown signature +- [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 +- [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 +- [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 +- [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 +- [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 +- [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 +- [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 +- [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 ### Functional From 1a34b1cd3c887ac11c88b6c32abfb53a85e45857 Mon Sep 17 00:00:00 2001 From: Yuri Olive Date: Mon, 19 Oct 2026 14:55:31 -0300 Subject: [PATCH 137/269] feat(libSceIme): export missing firmware interfaces --- core/libs/prx/libSceIme/Export.cpp | 215 +++++++++++++++++++++++++++++ docs/dev/debt/libSceIme.md | 43 ++++++ 2 files changed, 258 insertions(+) diff --git a/core/libs/prx/libSceIme/Export.cpp b/core/libs/prx/libSceIme/Export.cpp index 575463cf73..f7e3623b07 100644 --- a/core/libs/prx/libSceIme/Export.cpp +++ b/core/libs/prx/libSceIme/Export.cpp @@ -452,4 +452,219 @@ int APS5_VABI sceImeUpdate(EventHandler handler) { } } +int APS5_VABI sceImeVshOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeKeyboardUpdate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshUpdate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshUpdateContext(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshClearPreedit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshNotifySetText(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshSetCaretGeometry(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshConfirmPreedit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshSetCaretIndexInPreedit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshSetPreeditGeometry(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshSetSelectGeometry(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshClose(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshGetPanelPositionAndForm(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeSetCandidateIndex(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeConfirmCandidate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeDisableController(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeSetVoiceDictationEnabled(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeSetEditEventWithRemotePlayEnabled(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeForTestFunction(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshInformConfirmdString2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshSetSelectionText(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeCheckUpdateTextInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeCheckRemoteEventParam(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeKeyboardOpenInternal(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshSetParam(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshUpdateContext2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshSendTextInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshInformConfirmdString(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshNotifyCurrentTextInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshNotifyCurrentTextInfoForReconvertText(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshNotifyDeleteText(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshNotifyUpdateCaretText(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshCheckAutoReConvertingPreeditText(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshSetPanelPosition(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshNotifyPreeditMetaInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeConfigSet(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeConfigGet(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeDicGetWords(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeDicAddWord(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeDicDeleteWord(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeDicReplaceWord(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeDicDeleteLearnDics(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeDicDeleteUserDics(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/docs/dev/debt/libSceIme.md b/docs/dev/debt/libSceIme.md index 76178899b3..df3f8cd2d4 100644 --- a/docs/dev/debt/libSceIme.md +++ b/docs/dev/debt/libSceIme.md @@ -8,6 +8,49 @@ - [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 - [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`. A text filter or an external keyboard filter throws, as their contract is unknown. `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 +- [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 +- [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 +- [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 +- [sceImeDicDeleteLearnDics](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature +- [sceImeDicDeleteUserDics](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - unknown signature ### Functional From 7d9d41c3dbef3e47cee0e0f66c41e6ef8b156c0c Mon Sep 17 00:00:00 2001 From: Yuri Olive Date: Mon, 19 Oct 2026 14:55:42 -0300 Subject: [PATCH 138/269] feat(libSceHttp): export missing firmware interfaces --- core/libs/prx/libSceHttp/Export.cpp | 246 ++++++++++++++++++++++++++++ docs/dev/debt/libSceHttp.md | 49 ++++++ 2 files changed, 295 insertions(+) diff --git a/core/libs/prx/libSceHttp/Export.cpp b/core/libs/prx/libSceHttp/Export.cpp index a1de4ee746..04e5b51e72 100644 --- a/core/libs/prx/libSceHttp/Export.cpp +++ b/core/libs/prx/libSceHttp/Export.cpp @@ -609,4 +609,250 @@ int APS5_VABI sceHttpsFreeCaList(int http_ctx_id, HttpsCaList* ca_list) { ca_list->certsNum = 0; return 0; } + +int APS5_VABI sceHttpAuthCacheImport(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpAuthCacheExport(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpDbgEnableProfile(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpDbgSetPrintf(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpDbgShowStat(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpGetMemoryPoolStats(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpDbgShowConnectionStat(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpDbgShowRequestStat(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpDbgShowMemoryPoolStat(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpDbgGetConnectionStat(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpDbgGetRequestStat(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpAbortRequestForce(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpAddRequestHeaderRaw(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpRemoveRequestHeader(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpGetAuthEnabled(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpGetAutoRedirect(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpTrySetNonblock(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpGetNonblock(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpTryGetNonblock(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpSetEpollId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpGetEpollId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpGetEpoll(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpGetCookie(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpGetCookieEnabled(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpAddCookie(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpSetCookieSendCallback(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCookieExport(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCookieImport(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpSetCookieTotalMaxSize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpSetCookieMaxSize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpSetCookieMaxNum(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpSetCookieMaxNumPerDomain(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpGetCookieStats(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpsEnableOptionPrivate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpsDisableOptionPrivate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpSetPriorityOption(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpSetPolicyOption(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpRequestGetAllHeaders(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpSetAcceptEncodingGZIPEnabled(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpGetAcceptEncodingGZIPEnabled(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpSetDefaultAcceptEncodingGZIPEnabled(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpSetProxy(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpSetSocketCreationCallback(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpSetHttp09Enabled(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpGetRegisteredCtxIds(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpGetConnectionStat(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpAddQuery(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpSetDelayBuildRequestEnabled(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheRedirectedConnectionEnabled(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/docs/dev/debt/libSceHttp.md b/docs/dev/debt/libSceHttp.md index cfcbd069b6..eb563b8ed6 100644 --- a/docs/dev/debt/libSceHttp.md +++ b/docs/dev/debt/libSceHttp.md @@ -19,6 +19,55 @@ - [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 +- [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 +- [sceHttpDbgGetConnectionStat](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpDbgGetRequestStat](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpAbortRequestForce](../../../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 +- [sceHttpGetAutoRedirect](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpTrySetNonblock](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpGetNonblock](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpTryGetNonblock](../../../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 +- [sceHttpGetEpoll](../../../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 +- [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 +- [sceHttpsEnableOptionPrivate](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpsDisableOptionPrivate](../../../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 +- [sceHttpSetAcceptEncodingGZIPEnabled](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpGetAcceptEncodingGZIPEnabled](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpSetDefaultAcceptEncodingGZIPEnabled](../../../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 +- [sceHttpCacheRedirectedConnectionEnabled](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature ### Functional From df6b81f83b7b55264cf7fc1b17c717f3f278f606 Mon Sep 17 00:00:00 2001 From: Yuri Olive Date: Mon, 19 Oct 2026 14:56:52 -0300 Subject: [PATCH 139/269] feat(libSceAudioOut): export missing firmware audio output interfaces --- core/libs/prx/libSceAudioOut/CMakeLists.txt | 1 + .../prx/libSceAudioOut/src/Unimplemented.cpp | 548 ++++++++++++++++++ docs/dev/debt/libSceAudioOut.md | 108 ++++ 3 files changed, 657 insertions(+) create mode 100644 core/libs/prx/libSceAudioOut/src/Unimplemented.cpp diff --git a/core/libs/prx/libSceAudioOut/CMakeLists.txt b/core/libs/prx/libSceAudioOut/CMakeLists.txt index 59f2383f50..665bb433be 100644 --- a/core/libs/prx/libSceAudioOut/CMakeLists.txt +++ b/core/libs/prx/libSceAudioOut/CMakeLists.txt @@ -9,6 +9,7 @@ add_library(libSceAudioOut SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/src/AudioOut2User.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/AudioOut2Mastering.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/AudioOut2System.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/Unimplemented.cpp ) target_include_directories(libSceAudioOut PRIVATE diff --git a/core/libs/prx/libSceAudioOut/src/Unimplemented.cpp b/core/libs/prx/libSceAudioOut/src/Unimplemented.cpp new file mode 100644 index 0000000000..99e026d61e --- /dev/null +++ b/core/libs/prx/libSceAudioOut/src/Unimplemented.cpp @@ -0,0 +1,548 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceAudioOut2UserCreateEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2LoPortDestroy(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2LoContextDestroy(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutOpenEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2LoPortSetAttributes(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2LoContextAdvance(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2LoContextPush(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2LoContextGetQueueLevel(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2LoPortSetAttributesEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2LoPortGetAttributes(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSetSystemDebugState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutGetSystemState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2LoPortGetState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutGetHandleStatusInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutGetSimulatedHandleStatusInfo2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutChangeAppModuleState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutGetSimulatedHandleStatusInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutGetSimulatedBusUsableStatusByBusType(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutAttachToApplicationByPid(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutDetachFromApplicationByPid(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutPtOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutPtClose(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutPtGetLastOutputTime(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutExPtOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutExPtClose(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutExPtGetLastOutputTime(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutExGetLastOutputTime(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutGetInfoOpenNum(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutGetInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutMasteringInit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutMasteringSetParam(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutMasteringGetState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutMasteringTerm(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2LoPortCreateEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSetPadSpkVolume(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSetPadJackVolume(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSetUsbVolume(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSetCaesarVolume(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSetHeadphoneOutMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutGetFocusEnablePid(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSetMainOutput(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSetVolumeDown(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSetChatBalance(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSetRecMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSetAllMute(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSetSparkEQ(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutArbitrationInitialize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2ArbitrationInitialize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutEnable3DAudioByArbitration(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutAssignHRTF(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutEnable3DAudioForUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutEnableMonoHpOutput(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutEnableTVVirtual(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutEnableTVCorrection(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutEnableVrControllerLowPowerMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutEnableCinematicVrRotation(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutCustomHrtfIsExist(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutExConfigureOutputMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutExSystemInfoIsSupportedAudioOutExMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutExGetSystemInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutExGetOutputInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSysConfigureOutputMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSysGetMonitorInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSysHdmiMonitorInfoIsSupportedAudioOutMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSysGetSystemInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSysGetOutputInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2LoContextQueryMemory(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2LoContextQueryMemoryEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2LoContextGetAttributes(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2LoContextCreate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2LoContextCreateEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2LoContextSetAttributes(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2MbusInit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2MbusSetPortConnections(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2MbusSetConnections(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2MbusSetPortStatuses(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2MbusSetHeadphoneOutMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2MbusUserLoginFor3DAudio(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2MbusSetDeviceConnections(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2MbusSetMorpheusParam(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2MbusSetCaesarParam(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2MbusSetSparkParam(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2AllAudioMainToHeadphoneForDiag(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2LoPortCreate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2LoPortGetStateEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2ReportRegisterHandler(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2ReportUnregisterHandler(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2GetTvCorrectionInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2SetTvCorrectionInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2GetIpcHandle(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2GetSystemStateEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2GetSystemDebugState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2MasteringInitEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2SetSpeakerInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2SetDebugHeadphoneMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2DebugStateCtrl(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2LoInit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2LoTerminate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2ContextBedWrite(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2PortCreateEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2PortSetAttributesEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2InitializeEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2Terminate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2PortRegister(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2PortUnregister(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2GetAmbisonicsEncodeCoefficients(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2GetHrtfIdForCronos(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2SetHrtfIdForCronos(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/docs/dev/debt/libSceAudioOut.md b/docs/dev/debt/libSceAudioOut.md index 43372d1a15..ec058e7f31 100644 --- a/docs/dev/debt/libSceAudioOut.md +++ b/docs/dev/debt/libSceAudioOut.md @@ -24,6 +24,114 @@ - [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 - [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 +- [sceAudioOut2LoPortDestroy](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2LoContextDestroy](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutOpenEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2LoPortSetAttributes](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2LoContextAdvance](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2LoContextPush](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2LoContextGetQueueLevel](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2LoPortSetAttributesEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2LoPortGetAttributes](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutSetSystemDebugState](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutGetSystemState](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2LoPortGetState](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutGetHandleStatusInfo](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutGetSimulatedHandleStatusInfo2](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutChangeAppModuleState](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutGetSimulatedHandleStatusInfo](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutGetSimulatedBusUsableStatusByBusType](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutAttachToApplicationByPid](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutDetachFromApplicationByPid](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutPtOpen](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutPtClose](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutPtGetLastOutputTime](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutExPtOpen](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutExPtClose](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutExPtGetLastOutputTime](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutExGetLastOutputTime](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutGetInfoOpenNum](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutGetInfo](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutMasteringInit](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutMasteringSetParam](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutMasteringGetState](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutMasteringTerm](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2LoPortCreateEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutSetPadSpkVolume](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutSetPadJackVolume](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutSetUsbVolume](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutSetCaesarVolume](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutSetHeadphoneOutMode](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutGetFocusEnablePid](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutSetMainOutput](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutSetVolumeDown](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutSetChatBalance](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutSetRecMode](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutSetAllMute](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutSetSparkEQ](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutArbitrationInitialize](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2ArbitrationInitialize](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutEnable3DAudioByArbitration](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutAssignHRTF](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutEnable3DAudioForUser](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutEnableMonoHpOutput](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutEnableTVVirtual](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutEnableTVCorrection](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutEnableVrControllerLowPowerMode](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutEnableCinematicVrRotation](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutCustomHrtfIsExist](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutExConfigureOutputMode](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutExSystemInfoIsSupportedAudioOutExMode](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutExGetSystemInfo](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutExGetOutputInfo](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutSysConfigureOutputMode](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutSysGetMonitorInfo](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutSysHdmiMonitorInfoIsSupportedAudioOutMode](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutSysGetSystemInfo](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutSysGetOutputInfo](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2LoContextQueryMemory](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2LoContextQueryMemoryEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2LoContextGetAttributes](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2LoContextCreate](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2LoContextCreateEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2LoContextSetAttributes](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2MbusInit](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2MbusSetPortConnections](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2MbusSetConnections](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2MbusSetPortStatuses](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2MbusSetHeadphoneOutMode](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2MbusUserLoginFor3DAudio](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2MbusSetDeviceConnections](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2MbusSetMorpheusParam](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2MbusSetCaesarParam](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2MbusSetSparkParam](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2AllAudioMainToHeadphoneForDiag](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2LoPortCreate](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2LoPortGetStateEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2ReportRegisterHandler](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2ReportUnregisterHandler](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2GetTvCorrectionInfo](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2SetTvCorrectionInfo](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2GetIpcHandle](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2GetSystemStateEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2GetSystemDebugState](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2MasteringInitEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2SetSpeakerInfo](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2SetDebugHeadphoneMode](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2DebugStateCtrl](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2LoInit](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2LoTerminate](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2ContextBedWrite](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2PortCreateEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2PortSetAttributesEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2InitializeEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2Terminate](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2PortRegister](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2PortUnregister](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2GetAmbisonicsEncodeCoefficients](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2GetHrtfIdForCronos](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2SetHrtfIdForCronos](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature ### Functional From ac008fa8ffd931e69535f30c55b48c9284d8efa3 Mon Sep 17 00:00:00 2001 From: emirplus Date: Mon, 19 Oct 2026 20:58:03 +0300 Subject: [PATCH 140/269] fix(tools): decode summary and stack inputs with utf-8-sig in trace_digest --- tools/tests/test_trace_digest.py | 23 +++++++++++++++++++++++ tools/trace_digest.py | 11 ++++++----- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/tools/tests/test_trace_digest.py b/tools/tests/test_trace_digest.py index 8b0ca800fd..6a8e60a567 100644 --- a/tools/tests/test_trace_digest.py +++ b/tools/tests/test_trace_digest.py @@ -628,5 +628,28 @@ class IntervalDigestTests(unittest.TestCase): cells = [cell.strip() for cell in rows[2].split("|")[1:-1]] self.assertEqual(cells, ["1", "0-30", "1800", "16.6", "18.0", "20.0", "25.0", "5.0", "50 %", escaped + " 7.5"]) + def test_bom_encoded_stacks_load(self): + with tempfile.TemporaryDirectory() as directory: + stacks_file = Path(directory) / "stacks.txt" + stacks_file.write_bytes(b"\xef\xbb\xbf# samples=1 depth=1 truncated=0\nmodule.prx+0x10 1\n") + loaded = trace_digest.load_stacks(stacks_file) + self.assertEqual(loaded, [(["module.prx+0x10"], 1)]) + + def test_bom_encoded_intervals_load(self): + with tempfile.TemporaryDirectory() as directory: + intervals_file = Path(directory) / "intervals.jsonl" + intervals_file.write_bytes(b"\xef\xbb\xbf" + json.dumps({"interval_start_s": 0.0, "interval_end_s": 10.0, "presents": 600}).encode("utf-8") + b"\n") + loaded = trace_digest.load_intervals(intervals_file) + self.assertEqual(len(loaded), 1) + self.assertEqual(loaded[0]["interval_start_s"], 0.0) + + def test_bom_encoded_summary_loads(self): + with tempfile.TemporaryDirectory() as directory: + summary_file = Path(directory) / "aps5-summary.jsonl" + summary_file.write_bytes(b"\xef\xbb\xbf" + json.dumps(SUMMARY).encode("utf-8") + b"\n") + loaded = trace_digest.load_summary(str(summary_file), SUMMARY["trace"]) + self.assertEqual(loaded["commit"], SUMMARY["commit"]) + + if __name__ == "__main__": unittest.main() diff --git a/tools/trace_digest.py b/tools/trace_digest.py index a2dae0800f..40928b876e 100644 --- a/tools/trace_digest.py +++ b/tools/trace_digest.py @@ -80,7 +80,7 @@ def load_summary(path, trace_path): candidate = Path(path) if path else Path(trace_path).with_name("aps5-summary.jsonl") if not candidate.exists(): return None - lines = [json.loads(line) for line in candidate.read_text(encoding="utf-8").splitlines() if line.strip()] + lines = [json.loads(line) for line in candidate.read_text(encoding="utf-8-sig").splitlines() if line.strip()] name = Path(trace_path).name alternate = name[:-3] if name.endswith(".gz") else name + ".gz" for filename in (name, alternate): @@ -92,7 +92,7 @@ def load_summary(path, trace_path): def load_stacks(path): stacks = [] - for number, line in enumerate(Path(path).read_text(encoding="utf-8").splitlines(), 1): + for number, line in enumerate(Path(path).read_text(encoding="utf-8-sig").splitlines(), 1): if line.startswith("#") or not line.strip(): continue frames, _, count = line.rpartition(" ") @@ -264,7 +264,7 @@ def markdown(result): def load_intervals(path): - lines = [json.loads(line) for line in Path(path).read_text(encoding="utf-8").splitlines() if line.strip()] + lines = [json.loads(line) for line in Path(path).read_text(encoding="utf-8-sig").splitlines() if line.strip()] return [line for line in lines if "interval_start_s" in line] @@ -349,8 +349,9 @@ def main(arguments=None): if __name__ == "__main__": - if hasattr(sys.stdout, "reconfigure"): - sys.stdout.reconfigure(encoding="utf-8", errors="replace") + for stream in (sys.stdout, sys.stderr): + if hasattr(stream, "reconfigure"): + stream.reconfigure(encoding="utf-8", errors="replace") try: sys.exit(main()) except (EOFError, OSError, ValueError, zlib.error) as error: From 6ce3940a7fb30e40bfe7a94a70fdc165806ac093 Mon Sep 17 00:00:00 2001 From: Yuri Olive Date: Mon, 19 Oct 2026 14:59:28 -0300 Subject: [PATCH 141/269] feat(libSceHmd): export missing firmware HMD interfaces --- core/libs/prx/libSceHmd/Export.cpp | 796 +++++++++++++++++++++++++++++ docs/dev/debt/libSceHmd.md | 162 ++++++ 2 files changed, 958 insertions(+) diff --git a/core/libs/prx/libSceHmd/Export.cpp b/core/libs/prx/libSceHmd/Export.cpp index 36c2ca3c06..0112025f52 100644 --- a/core/libs/prx/libSceHmd/Export.cpp +++ b/core/libs/prx/libSceHmd/Export.cpp @@ -1,4 +1,5 @@ #include "Hmd.hpp" +#include "prx/libc/include/General.hpp" #include @@ -68,4 +69,799 @@ std::int32_t APS5_VABI sceHmdInternalMmapIsConnect() { return 0; } +int APS5_VABI sceHmdInternalGetPUVersion(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalEventInitialize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetVirtualDisplaySize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSocialScreenSetOutput(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetVirtualDisplayDepth(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetEyeStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalAnotherScreenOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionUnsetCallback(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetCrashReportCommand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetDebugTextMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdDistortionGetWorkMemoryAlign(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalRegisterEventCallback(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdDistortionGetWorkMemorySize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionStartWideNear(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetPUSerialNumber(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetDebugSocialScreenMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetM2LedBrightness(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetIPD(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalMirroringModeSetAspect(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalDfuGetStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalAnotherScreenSendVideo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetRequiredPUPVersion(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionSetCallback(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalDfuCheckAfterPvt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalResetInertialSensor(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionSetUserEventStart(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdDistortionSetOutputMinColor(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetVirtualDisplayDepth(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdGetDistortionWorkMemoryAlign(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionStartMultilayer(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalPushVr2dData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionClearUserEventEnd(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetIPD(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetVRMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalCrashReportReadDataSize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetVr2dData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalAnotherScreenSendAudio(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetPuPositionParam(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionInitializeCapture(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdGet2DEyeOffset(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetDebugSocialScreenMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetHmuPowerStatusForDebug(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdGetDistortionWorkMemorySize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionSetDisplayBuffers(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalAnotherScreenSetFadeAndSwitch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalCreateSharedMemory(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalMirroringModeSetAspectDebug(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetPuBuildNumber(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdDistortionGetCompoundEyeCorrectionCommand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetS3dPassMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionStopCapture(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalIsCommonDlgMiniAppVr2d(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalMmapGetSensorCalibrationData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdDistortionGetCorrectionCommand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionQueryOnionBuffAlign(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetDebugMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalIsMiniAppVr2d(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalIsCommonDlgVr2d(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionSetOutputMinColor(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetPortConnection(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionDebugGetLastInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetM2LedOn(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdGetFieldOfView(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionAddDisplayBuffer(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetIpdSettingEnableForSystemService(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetLedOn(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternal3dAudioSendData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionInitialize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetDeviceInformationByHandle(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionStopLiveCapture(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalAnotherScreenClose(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalCrashReportReadData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetDeviceInformation(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionStartCapture(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetDefaultLedData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalDfuClose(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionQueryGarlicBuffAlign(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetHmuPowerControl(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalBindDeviceWithUserId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetHmuSerialNumber(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdDistortionGetWideNearCorrectionCommand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalCheckS3dPassModeAvailable(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalAnotherScreenGetAudioStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSeparateModeGetAudioStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionStartLiveCapture(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalIsGameVr2d(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdGetAssyError(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionFinalize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSocialScreenSetFadeAndSwitch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalDfuStart(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetVirtualDisplayHeight(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdDistortionInitialize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalDfuSetMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetForcedCrash(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalDfuCheckPartialUpdateAvailable(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSeparateModeClose(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetDebugTextMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalCrashReportClose(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetHmuPowerControlForDebug(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetVirtualDisplayHeight(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionStart(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalResetLedForVsh(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalDfuOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetDebugMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalCrashReportCancel(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSeparateModeGetVideoStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalCheckDeviceModelMk3(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdSetVr2dEnable(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetSidetone(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSeparateModeOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalMmapGetModeId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdDistortionGet2dVrCommand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalDfuSendSize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdGetWideNearDistortionCorrectionCommand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionStartMultilayer2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdGetDistortionCorrectionCommand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetDeviceConnection(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetVirtualDisplaySize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionUnsetDisplayBuffers(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetHmuOpticalParam(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSeparateModeSendVideo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalResetLedForVrTracker(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionQueryOnionBuffSize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionStartWithOverlay(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionSetUserEventEnd(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalCrashReportOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetDemoMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionStartWideNearWithOverlay(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdGetDistortionParams(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalDfuSend(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionClearUserEventStart(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetUserType(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetDemoMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalMapSharedMemory(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetCrashDumpInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetTv4kCapability(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalDfuReset(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionStart2dVr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalMmapGetCount(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetDefaultLedData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdGetInertialSensorData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternal3dAudioClose(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetBrightness(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSocialScreenGetFadeState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSeparateModeSendAudio(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetStatusReport(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetBrightness(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionFinalizeCapture(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetPortStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionStop(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalAnotherScreenGetVideoStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternal3dAudioOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalAnotherScreenGetFadeState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetDebugGpo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetIpdSettingEnableForSystemService(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionDebugGetLastInfoMultilayer(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetPuRevision(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionQueryGarlicBuffSize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdGet2dVrCommand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/docs/dev/debt/libSceHmd.md b/docs/dev/debt/libSceHmd.md index 1206760ae4..54a6bf362d 100644 --- a/docs/dev/debt/libSceHmd.md +++ b/docs/dev/debt/libSceHmd.md @@ -3,3 +3,165 @@ ### Silent stubs - [libSceHmd](../../../core/libs/prx/libSceHmd/Export.cpp) implements only the disconnected-headset path: initialization succeeds, device queries report `NotDetected`, and opening a device returns `DeviceDisconnected`. Headset support, tracking, rendering and additional HMD exports are not implemented; SDK-level ABI compatibility and in-game behaviour remain unverified. + +### Unknown function info + +- [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 +- [sceHmdDistortionGetWorkMemoryAlign](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalRegisterEventCallback](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdDistortionGetWorkMemorySize](../../../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 +- [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 +- [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 +- [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 +- [sceHmdInternalGetPuPositionParam](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionInitializeCapture](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdGet2DEyeOffset](../../../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 +- [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 +- [sceHmdReprojectionQueryOnionBuffAlign](../../../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 +- [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 +- [sceHmdGetFieldOfView](../../../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 +- [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 +- [sceHmdInternalGetDefaultLedData](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdInternalDfuClose](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionQueryGarlicBuffAlign](../../../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 +- [sceHmdGetAssyError](../../../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 +- [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 +- [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 +- [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 +- [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 +- [sceHmdInternalResetLedForVrTracker](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdReprojectionQueryOnionBuffSize](../../../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 +- [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 +- [sceHmdGetInertialSensorData](../../../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 +- [sceHmdReprojectionQueryGarlicBuffSize](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature +- [sceHmdGet2dVrCommand](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - unknown signature From 26a7d9c8ec7d02cd50c5b0276cc8f9f21cbe86c5 Mon Sep 17 00:00:00 2001 From: orezraey Date: Mon, 19 Oct 2026 15:04:55 -0300 Subject: [PATCH 142/269] fix(tools): decode check_compile's Ninja commands in the build-file encoding Co-Authored-By: Claude Opus 5.5 --- tools/check_compile.py | 16 +++++++++++++--- tools/tests/test_check_compile.py | 29 ++++++++++++++++++++++++++--- 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/tools/check_compile.py b/tools/check_compile.py index 47076b046b..dd5c7e9d65 100644 --- a/tools/check_compile.py +++ b/tools/check_compile.py @@ -13,11 +13,21 @@ DROP = ("-c", "-MD", "-MMD", "-MP") DROP_WITH_VALUE = ("-o", "-MT", "-MF", "-MJ") -def capture(command, cwd=None): - return subprocess.run(command, capture_output=True, text=True, encoding="utf-8", +def capture(command, cwd=None, encoding="utf-8"): + return subprocess.run(command, capture_output=True, text=True, encoding=encoding, errors="replace", cwd=cwd) +def ninja_encoding(ninja): + if sys.platform != "win32": + return "utf-8" + declared = capture([ninja, "-t", "wincodepage"]).stdout.strip() + encodings = {"Build file encoding: UTF-8": "utf-8", "Build file encoding: ANSI": "mbcs"} + if declared not in encodings: + raise ValueError(f"unsupported Ninja build-file encoding: {declared!r}") + return encodings[declared] + + def repository(): result = capture(["git", "rev-parse", "--show-toplevel"]) if result.returncode != 0: @@ -34,7 +44,7 @@ def entries(build): ninja = shutil.which("ninja") if ninja is None: raise ValueError(f"no {manifest} and ninja is not on PATH") - result = capture([ninja, "-C", str(build), "-t", "compdb", "-x"]) + result = capture([ninja, "-C", str(build), "-t", "compdb", "-x"], encoding=ninja_encoding(ninja)) if result.returncode != 0: raise ValueError(f"ninja -t compdb: {result.stderr.strip()}") return json.loads(result.stdout) diff --git a/tools/tests/test_check_compile.py b/tools/tests/test_check_compile.py index 3b83764f0a..f7ca040770 100644 --- a/tools/tests/test_check_compile.py +++ b/tools/tests/test_check_compile.py @@ -32,6 +32,26 @@ class CheckCompileTests(unittest.TestCase): self.assertFalse((self.build / "unit.o.d").exists()) self.assertFalse((self.build / "unit.o.rsp").exists()) + def test_ninja_commands_in_a_non_ascii_build_directory_are_found(self): + compiler = shutil.which("clang") or shutil.which("gcc") + if compiler is None or shutil.which("ninja") is None: + self.skipTest("needs Ninja and a C compiler") + name = "ação" + if os.name == "nt": + try: + name.encode("mbcs", "strict") + except UnicodeEncodeError: + self.skipTest("the ANSI code page cannot represent the build directory name") + build = self.root / name + build.mkdir() + self.source("unit.c") + command = subprocess.list2cmdline([compiler]) if os.name == "nt" else '"' + compiler + '"' + (build / "build.ninja").write_text( + "rule cc\n command = " + command + " -c $in -o $out\nbuild unit.o: cc ../unit.c\n", encoding="utf-8") + result = self.tool("--build", name, "unit.c") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn("1 translation units compile", result.stdout) + def test_syntax_check_preserves_existing_clang_compilation_fragment(self): compiler = shutil.which("clang") if compiler is None: @@ -327,14 +347,17 @@ class CheckCompileTests(unittest.TestCase): bin = self.root / "bin" bin.mkdir() stub = bin / "ninja_stub.py" - stub.write_text("import json\nprint(json.dumps(%r))\n" % [entry], encoding="utf-8") + stub.write_text("import json\nimport sys\n" + "if sys.argv[1:] == ['-t', 'wincodepage']:\n print('Build file encoding: UTF-8')\n" + "else:\n print(json.dumps(%r))\n" % [entry], encoding="utf-8") if os.name == "nt": - (bin / "ninja.cmd").write_text(f'@"{sys.executable}" "{stub}"\n', encoding="utf-8") + (bin / "ninja.cmd").write_text('@"%NINJA_STUB_PYTHON%" "%~dp0ninja_stub.py" %*\n', encoding="ascii") else: ninja = bin / "ninja" ninja.write_text(f'#!/bin/sh\nexec "{sys.executable}" "{stub}"\n', encoding="utf-8") ninja.chmod(0o755) - result = self.tool("unit.cpp", env={"PATH": f"{bin}{os.pathsep}{os.environ['PATH']}"}) + result = self.tool("unit.cpp", env={"PATH": f"{bin}{os.pathsep}{os.environ['PATH']}", + "NINJA_STUB_PYTHON": sys.executable}) self.assertEqual(result.returncode, 0) self.assertIn("1 translation units compile", result.stdout) From 6c039bd3e238190d4896948e72eb94d4a7c4fad9 Mon Sep 17 00:00:00 2001 From: orezraey Date: Mon, 19 Oct 2026 15:09:27 -0300 Subject: [PATCH 143/269] fix(tools): report a failed Ninja wincodepage query in check_compile Co-Authored-By: Claude Opus 5.5 --- tools/check_compile.py | 5 ++++- tools/tests/test_check_compile.py | 24 +++++++++++++++++++++--- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/tools/check_compile.py b/tools/check_compile.py index dd5c7e9d65..6edb578f2f 100644 --- a/tools/check_compile.py +++ b/tools/check_compile.py @@ -21,7 +21,10 @@ def capture(command, cwd=None, encoding="utf-8"): def ninja_encoding(ninja): if sys.platform != "win32": return "utf-8" - declared = capture([ninja, "-t", "wincodepage"]).stdout.strip() + result = capture([ninja, "-t", "wincodepage"]) + if result.returncode != 0: + raise ValueError(f"ninja -t wincodepage: {result.stderr.strip() or result.stdout.strip()}") + declared = result.stdout.strip() encodings = {"Build file encoding: UTF-8": "utf-8", "Build file encoding: ANSI": "mbcs"} if declared not in encodings: raise ValueError(f"unsupported Ninja build-file encoding: {declared!r}") diff --git a/tools/tests/test_check_compile.py b/tools/tests/test_check_compile.py index f7ca040770..9e09dbecda 100644 --- a/tools/tests/test_check_compile.py +++ b/tools/tests/test_check_compile.py @@ -6,6 +6,7 @@ import sys import tempfile import unittest from pathlib import Path +from unittest.mock import patch sys.path.insert(0, str(Path(__file__).resolve().parents[1])) import check_compile @@ -37,12 +38,12 @@ class CheckCompileTests(unittest.TestCase): if compiler is None or shutil.which("ninja") is None: self.skipTest("needs Ninja and a C compiler") name = "ação" + build = self.root / name if os.name == "nt": try: - name.encode("mbcs", "strict") + str(build).encode("mbcs", "strict") except UnicodeEncodeError: - self.skipTest("the ANSI code page cannot represent the build directory name") - build = self.root / name + self.skipTest("the ANSI code page cannot represent the build directory") build.mkdir() self.source("unit.c") command = subprocess.list2cmdline([compiler]) if os.name == "nt" else '"' + compiler + '"' @@ -52,6 +53,23 @@ class CheckCompileTests(unittest.TestCase): self.assertEqual(result.returncode, 0, result.stdout + result.stderr) self.assertIn("1 translation units compile", result.stdout) + def test_windows_ninja_output_is_decoded_in_its_declared_build_file_encoding(self): + cases = (("Build file encoding: UTF-8\n", 0, "utf-8"), ("Build file encoding: ANSI\n", 0, "mbcs"), + ("Build file encoding: OEM\n", 0, None), ("", 1, None)) + for declared, status, expected in cases: + with self.subTest(declared=declared, status=status): + result = subprocess.CompletedProcess([], status, declared, "ninja: error: unknown tool" if status else "") + with patch.object(check_compile.sys, "platform", "win32"), \ + patch.object(check_compile, "capture", return_value=result): + if expected is None: + with self.assertRaises(ValueError): + check_compile.ninja_encoding("ninja") + else: + self.assertEqual(check_compile.ninja_encoding("ninja"), expected) + with patch.object(check_compile.sys, "platform", "linux"), \ + patch.object(check_compile, "capture", side_effect=AssertionError("queried Ninja")): + self.assertEqual(check_compile.ninja_encoding("ninja"), "utf-8") + def test_syntax_check_preserves_existing_clang_compilation_fragment(self): compiler = shutil.which("clang") if compiler is None: From f32cab0b1bd848a6c018925d5642bb4cfa92a6d4 Mon Sep 17 00:00:00 2001 From: emirplus Date: Mon, 19 Oct 2026 21:13:11 +0300 Subject: [PATCH 144/269] fix(tools): decode debt files with utf-8-sig in debt_split --- tools/debt_split.py | 13 ++++++++----- tools/tests/test_debt_split.py | 12 ++++++++++++ 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/tools/debt_split.py b/tools/debt_split.py index d6b0ef0793..728521a76a 100644 --- a/tools/debt_split.py +++ b/tools/debt_split.py @@ -71,7 +71,7 @@ def read_debt(root, name): sections = {} if path.exists(): section = None - for kind, item_section, text in parse(path.read_text(encoding="utf-8")): + for kind, item_section, text in parse(path.read_text(encoding="utf-8-sig")): if kind == "heading" and item_section and text.startswith("### "): section = item_section sections.setdefault(section, []) @@ -129,7 +129,7 @@ def index_note(): def command_split(root): - text = (root / INDEX).read_text(encoding="utf-8") + text = (root / INDEX).read_text(encoding="utf-8-sig") index, files, order = split(text) if not files: sys.exit(f"{INDEX} has no entries to move") @@ -145,7 +145,7 @@ def command_split(root): def git(root, *args, check=True): - return subprocess.run(["git", *args], cwd=root, capture_output=True, text=True, encoding="utf-8", check=check).stdout + return subprocess.run(["git", *args], cwd=root, capture_output=True, text=True, encoding="utf-8", errors="replace", check=check).stdout def apply_change(root, base, new, index_text): @@ -228,11 +228,14 @@ def command_resolve(root): print("\n".join(touched)) -def main(): +def main(argv=None): + for stream in (sys.stdout, sys.stderr): + if hasattr(stream, "reconfigure"): + stream.reconfigure(encoding="utf-8") parser = argparse.ArgumentParser(description=f"Split {INDEX} into one file per library or component, and move entries of a change made before the split") parser.add_argument("command", choices=("split", "resolve"), help=f"split: move the entries of {INDEX} into {DEBT_DIR}/; resolve: during a rebase stopped on {INDEX}, apply the stopped commit's entry changes to {DEBT_DIR}/ and stage them") - args = parser.parse_args() + args = parser.parse_args(argv) root = Path(git(Path.cwd(), "rev-parse", "--show-toplevel").strip()) command_split(root) if args.command == "split" else command_resolve(root) diff --git a/tools/tests/test_debt_split.py b/tools/tests/test_debt_split.py index 3596e47a76..d2d25e96be 100644 --- a/tools/tests/test_debt_split.py +++ b/tools/tests/test_debt_split.py @@ -5,6 +5,9 @@ import tempfile import unittest from pathlib import Path +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import debt_split + TOOL = Path(__file__).resolve().parents[1] / "debt_split.py" INDEX = "docs/dev/TechnicalDebt.md" @@ -222,6 +225,15 @@ class DebtSplitTests(unittest.TestCase): self.assertIn(paragraph, self.read(INDEX)) self.assertNotIn(paragraph, self.read("docs/dev/debt/libSceA.md")) + def test_read_debt_and_split_accept_utf8_bom(self): + self.write("docs/dev/debt/libSceA.md", "\ufeff### Silent stubs\n\n- entry 1\n") + sections = debt_split.read_debt(self.root, "libSceA") + self.assertEqual(sections, {"Silent stubs": ["- entry 1"]}) + self.write(INDEX, "\ufeff" + ORIGINAL) + self.split() + self.assertTrue((self.root / "docs/dev/debt/libSceA.md").is_file()) + self.assertIn("libSceA technical debt", self.read("docs/dev/debt/libSceA.md")) + if __name__ == "__main__": unittest.main() From ed021c48c5d2a0c3e4a4722ba62398d1d68e5228 Mon Sep 17 00:00:00 2001 From: thuanquang Date: Tue, 20 Oct 2026 01:15:59 +0700 Subject: [PATCH 145/269] test(libkernel): check the file lock process case with four child processes Co-Authored-By: Claude --- core/libs/tests/GuestFileLock.cpp | 63 ++++++++++++++++++------------- 1 file changed, 36 insertions(+), 27 deletions(-) diff --git a/core/libs/tests/GuestFileLock.cpp b/core/libs/tests/GuestFileLock.cpp index 0a49b8d102..3fb7ac7b71 100644 --- a/core/libs/tests/GuestFileLock.cpp +++ b/core/libs/tests/GuestFileLock.cpp @@ -35,10 +35,10 @@ static void Require(bool condition) { if (!condition) std::abort(); } -static int Duplicate(int source, int kind) { +static int Duplicate(int source, int kind, int target = 20) { if (kind == 0) return dup_nid_postfix(source); - if (kind == 1) return dup2_nid_postfix(source, 20); - return fcntl_nid_postfix(source, 0, 20); + if (kind == 1) return dup2_nid_postfix(source, target); + return fcntl_nid_postfix(source, 0, target); } static bool Acquires(int descriptor, int mode) { @@ -64,8 +64,8 @@ struct Fixture { descriptors.insert(descriptor); return descriptor; } - int Copy(int kind, int from = -1) { - const int descriptor = Duplicate(from < 0 ? source : from, kind); + int Copy(int kind, int from = -1, int target = 20) { + const int descriptor = Duplicate(from < 0 ? source : from, kind, target); Require(descriptor >= 0); descriptors.insert(descriptor); return descriptor; @@ -258,10 +258,11 @@ static void Streams() { Require(fclose_nid_postfix(stream) == 0); } -static int ChildTry(const std::string& path, int mode) { +static int ChildTry(const Fixture (&fixtures)[6], int mode) { wchar_t executable[32768]; Require(::GetModuleFileNameW(nullptr, executable, 32768) != 0); - std::wstring command = L"\"" + std::wstring(executable) + L"\" child \"" + std::filesystem::path(path).wstring() + L"\" " + std::to_wstring(mode | 4); + std::wstring command = L"\"" + std::wstring(executable) + L"\" child " + std::to_wstring(mode | 4); + for (const Fixture& fixture : fixtures) command += L" \"" + std::filesystem::path(fixture.path).wstring() + L"\""; STARTUPINFOW startup{}; startup.cb = sizeof(startup); PROCESS_INFORMATION process{}; @@ -278,30 +279,38 @@ static int ChildTry(const std::string& path, int mode) { } static void Processes() { - for (int kind = 0; kind != 3; ++kind) { - for (int mode : {1, 2}) { - Fixture fixture; - Require(flock_nid_postfix(fixture.source, mode | 4) == 0); - const int copy = fixture.Copy(kind); - Require(ChildTry(fixture.path, 2) == 35); - Require(ChildTry(fixture.path, 1) == (mode == 1 ? 0 : 35)); - fixture.Close(fixture.source); - Require(ChildTry(fixture.path, 2) == 35); - Require(flock_nid_postfix(copy, 8) == 0); - Require(ChildTry(fixture.path, 2) == 0); - } + Fixture fixtures[6]; + int copies[6]; + int shared = 0; + for (int index = 0; index != 6; ++index) { + const int mode = index % 2 == 0 ? 1 : 2; + if (mode == 1) shared |= 1 << index; + Require(flock_nid_postfix(fixtures[index].source, mode | 4) == 0); + copies[index] = fixtures[index].Copy(index / 2, -1, 100 + index); } + Require(ChildTry(fixtures, 2) == 0x3f); + Require(ChildTry(fixtures, 1) == (0x3f & ~shared)); + for (Fixture& fixture : fixtures) fixture.Close(fixture.source); + Require(ChildTry(fixtures, 2) == 0x3f); + for (const int copy : copies) Require(flock_nid_postfix(copy, 8) == 0); + Require(ChildTry(fixtures, 2) == 0); } int main(int argc, char** argv) { - if (argc == 4 && std::string(argv[1]) == "child") { - const int descriptor = open_nid_postfix(argv[2], 2, 0); - Require(descriptor >= 0); - const int result = flock_nid_postfix(descriptor, std::atoi(argv[3])); - const int error = result == 0 ? 0 : *__error_nid_postfix(); - if (result == 0) Require(flock_nid_postfix(descriptor, 8) == 0); - Require(close_nid_postfix(descriptor) == 0); - return error; + if (argc >= 4 && std::string(argv[1]) == "child") { + int busy = 0; + for (int index = 3; index < argc; ++index) { + const int descriptor = open_nid_postfix(argv[index], 2, 0); + Require(descriptor >= 0); + if (flock_nid_postfix(descriptor, std::atoi(argv[2])) == 0) { + Require(flock_nid_postfix(descriptor, 8) == 0); + } else { + Require(*__error_nid_postfix() == 35); + busy |= 1 << (index - 3); + } + Require(close_nid_postfix(descriptor) == 0); + } + return busy; } Require(argc == 2); const std::string mode = argv[1]; From 3eee2b281faa95b4e409d7c13e819251e74f76b2 Mon Sep 17 00:00:00 2001 From: Yuri Olive Date: Mon, 19 Oct 2026 15:17:06 -0300 Subject: [PATCH 146/269] fix(libSceAudioOut): leave sceAudioOutGetSystemState to #5965 --- core/libs/prx/libSceAudioOut/src/Unimplemented.cpp | 5 ----- docs/dev/debt/libSceAudioOut.md | 1 - 2 files changed, 6 deletions(-) diff --git a/core/libs/prx/libSceAudioOut/src/Unimplemented.cpp b/core/libs/prx/libSceAudioOut/src/Unimplemented.cpp index 99e026d61e..78fd125308 100644 --- a/core/libs/prx/libSceAudioOut/src/Unimplemented.cpp +++ b/core/libs/prx/libSceAudioOut/src/Unimplemented.cpp @@ -60,11 +60,6 @@ int APS5_VABI sceAudioOutSetSystemDebugState(void) { return 0; } -int APS5_VABI sceAudioOutGetSystemState(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceAudioOut2LoPortGetState(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/docs/dev/debt/libSceAudioOut.md b/docs/dev/debt/libSceAudioOut.md index ec058e7f31..4ce0e79bfb 100644 --- a/docs/dev/debt/libSceAudioOut.md +++ b/docs/dev/debt/libSceAudioOut.md @@ -35,7 +35,6 @@ - [sceAudioOut2LoPortSetAttributesEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2LoPortGetAttributes](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOutSetSystemDebugState](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature -- [sceAudioOutGetSystemState](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2LoPortGetState](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOutGetHandleStatusInfo](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOutGetSimulatedHandleStatusInfo2](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature From 4ea1c112bb9789928871570ffd1c5ae64be67f98 Mon Sep 17 00:00:00 2001 From: pushkarreddyy <212133856+pushkarreddyy@users.noreply.github.com> Date: Mon, 19 Oct 2026 23:48:09 +0530 Subject: [PATCH 147/269] feat(libkernel): implement pthread_mutexattr_getprotocol --- core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp | 7 +++++++ core/libs/tests/GuestPthreadMutexattr.cpp | 11 +++++++++++ docs/dev/debt/libkernel.md | 2 +- 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp b/core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp index bf93cf4691..3a5d38dc3f 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp @@ -111,6 +111,13 @@ int APS5_VABI pthread_mutexattr_setprotocol_nid_postfix(PthreadMutexattr* attr, return 0; } +int APS5_VABI pthread_mutexattr_getprotocol_nid_postfix(const PthreadMutexattr* attr, int* protocol) { + if (!attr || !*attr) return PosixThread::GUEST_EINVAL; + if (!protocol) throw std::runtime_error("pthread_mutexattr_getprotocol: null protocol"); + *protocol = (*attr)->protocol; + return 0; +} + int APS5_VABI pthread_mutexattr_setprioceiling_nid_postfix(PthreadMutexattr* attr, int prioceiling) { if (!attr || !*attr) return PosixThread::GUEST_EINVAL; if ((*attr)->protocol != POSIX_PRIO_PROTECT) return POSIX_EINVAL; diff --git a/core/libs/tests/GuestPthreadMutexattr.cpp b/core/libs/tests/GuestPthreadMutexattr.cpp index 7e704677e5..48dd291a5b 100644 --- a/core/libs/tests/GuestPthreadMutexattr.cpp +++ b/core/libs/tests/GuestPthreadMutexattr.cpp @@ -10,6 +10,7 @@ int APS5_VABI pthread_mutexattr_destroy_nid_postfix(PthreadMutexattr* attr); int APS5_VABI pthread_mutexattr_getpshared_nid_postfix(const PthreadMutexattr* attr, int* pshared); int APS5_VABI pthread_mutexattr_setpshared_nid_postfix(PthreadMutexattr* attr, int pshared); int APS5_VABI pthread_mutexattr_setprotocol_nid_postfix(PthreadMutexattr* attr, int protocol); +int APS5_VABI pthread_mutexattr_getprotocol_nid_postfix(const PthreadMutexattr* attr, int* protocol); int APS5_VABI pthread_mutexattr_setprioceiling_nid_postfix(PthreadMutexattr* attr, int prioceiling); int APS5_VABI pthread_mutexattr_getprioceiling_nid_postfix(const PthreadMutexattr* attr, int* prioceiling); int APS5_VABI pthread_mutexattr_settype_nid_postfix(PthreadMutexattr* attr, int type); @@ -114,8 +115,11 @@ static void CheckProcessSharing() { static void CheckInvalidPriorityAttribute(PthreadMutexattr* attr) { int ceiling = -1; + int protocol = -1; Require(pthread_mutexattr_setprotocol_nid_postfix(attr, PRIO_PROTECT) == GUEST_EINVAL); Require(pthread_mutexattr_setprotocol_nid_postfix(attr, -1) == GUEST_EINVAL); + Require(pthread_mutexattr_getprotocol_nid_postfix(attr, &protocol) == GUEST_EINVAL && protocol == -1); + Require(pthread_mutexattr_getprotocol_nid_postfix(attr, nullptr) == GUEST_EINVAL); Require(pthread_mutexattr_setprioceiling_nid_postfix(attr, 300) == GUEST_EINVAL); Require(pthread_mutexattr_getprioceiling_nid_postfix(attr, &ceiling) == GUEST_EINVAL && ceiling == -1); Require(pthread_mutexattr_getprioceiling_nid_postfix(attr, nullptr) == GUEST_EINVAL); @@ -130,11 +134,15 @@ int main() { CheckInvalidPriorityAttribute(&attr); Require(pthread_mutexattr_init_nid_postfix(&attr) == 0); int ceiling = -1; + int protocol = -1; + Require(pthread_mutexattr_getprotocol_nid_postfix(&attr, &protocol) == 0 && protocol == PRIO_NONE); Require(pthread_mutexattr_getprioceiling_nid_postfix(&attr, &ceiling) == GUEST_EINVAL && ceiling == -1); Require(pthread_mutexattr_setprioceiling_nid_postfix(&attr, 300) == GUEST_EINVAL); Require(pthread_mutexattr_setprotocol_nid_postfix(&attr, PRIO_INHERIT) == 0); + Require(pthread_mutexattr_getprotocol_nid_postfix(&attr, &protocol) == 0 && protocol == PRIO_INHERIT); Require(pthread_mutexattr_setprioceiling_nid_postfix(&attr, 300) == GUEST_EINVAL); Require(pthread_mutexattr_setprotocol_nid_postfix(&attr, PRIO_PROTECT) == 0); + Require(pthread_mutexattr_getprotocol_nid_postfix(&attr, &protocol) == 0 && protocol == PRIO_PROTECT); Require(pthread_mutexattr_getprioceiling_nid_postfix(&attr, &ceiling) == 0 && ceiling == sched_get_priority_max_nid_postfix(SCHED_RR)); Require(pthread_mutexattr_setprioceiling_nid_postfix(&attr, 300) == 0); Require(pthread_mutexattr_getprioceiling_nid_postfix(&attr, &ceiling) == 0 && ceiling == 300); @@ -144,9 +152,12 @@ int main() { Require(pthread_mutex_unlock_nid_postfix(&mutex) == 0); Require(pthread_mutex_destroy_nid_postfix(&mutex) == 0); Require(pthread_mutexattr_setprotocol_nid_postfix(&attr, PRIO_PROTECT) == 0); + Require(pthread_mutexattr_getprotocol_nid_postfix(&attr, &protocol) == 0 && protocol == PRIO_PROTECT); Require(pthread_mutexattr_getprioceiling_nid_postfix(&attr, &ceiling) == 0 && ceiling == sched_get_priority_max_nid_postfix(SCHED_RR)); Require(pthread_mutexattr_setprotocol_nid_postfix(&attr, PRIO_NONE) == 0); + Require(pthread_mutexattr_getprotocol_nid_postfix(&attr, &protocol) == 0 && protocol == PRIO_NONE); Require(pthread_mutexattr_getprioceiling_nid_postfix(&attr, &ceiling) == GUEST_EINVAL); + Require(Throws([&] { pthread_mutexattr_getprotocol_nid_postfix(&attr, nullptr); })); Require(Throws([&] { pthread_mutexattr_getprioceiling_nid_postfix(&attr, nullptr); })); Require(pthread_mutexattr_destroy_nid_postfix(&attr) == 0); CheckInvalidPriorityAttribute(&attr); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 41b7930c25..19417ae0d5 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -4,7 +4,7 @@ - [sceKernelGetSocPowerConsumption](../../../core/libs/prx/libkernel/System/src/ThermalSensors.cpp) (libkernel) - returns `EINVAL` and writes nothing, as `sceKernelGetSocSensorTemperature` does when the host has no sensor: the host's package power is not read (Linux RAPL `energy_uj` is readable by root only, and Windows has no general source). The prototype is not documented. PS5LM Plus (PPSA99581) passes a zeroed 128-byte buffer and, on success, reads its first 32-bit word as milliwatts, accepting 1000 to 350000; on failure it shows 0 W - [scePthreadMutexattrSetprotocol](../../../core/libs/prx/libkernel/Pthread/src/Mutex.cpp) (libkernel) - accepts 0 to 2 as a PS5 (firmware 13.60) does and returns `EINVAL` otherwise, but the protocol is not stored or applied: priority inheritance and protection are not modelled. On the console a null or uninitialized mutex attribute returns `EINVAL` from the SCE attribute calls, `scePthreadMutexInit` takes an uninitialized attribute as the default one, and a destroyed mutex returns `EINVAL` from `scePthreadMutexDestroy`; a null mutex pointer crashed the payload and still throws -- [pthread_mutexattr_setprotocol, pthread_mutexattr_setprioceiling, pthread_mutexattr_getprioceiling](../../../core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp) (libkernel) - the protocol and the priority ceiling are stored and reported as in [FreeBSD 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 +- [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)) From 89b1b53176628cef6d1e5cb9e065320b43e93445 Mon Sep 17 00:00:00 2001 From: Federico Marras Date: Mon, 19 Oct 2026 20:18:50 +0200 Subject: [PATCH 148/269] feat(libc): implement bounded decimal parsing with strtonum --- core/libs/prx/libc/src/StringExtensions.cpp | 21 ++++++++++ core/libs/tests/GuestStrings.cpp | 46 +++++++++++++++++++++ docs/dev/debt/libc.md | 2 + 3 files changed, 69 insertions(+) diff --git a/core/libs/prx/libc/src/StringExtensions.cpp b/core/libs/prx/libc/src/StringExtensions.cpp index f1f4eb975d..a6cb27bb7c 100644 --- a/core/libs/prx/libc/src/StringExtensions.cpp +++ b/core/libs/prx/libc/src/StringExtensions.cpp @@ -3,10 +3,31 @@ #include #include #include +#include +#include #include extern "C" { +long long APS5_VABI strtonum_nid_postfix(const char* text, long long minimum, long long maximum, const char** error) { + const int savedError = errno; + const auto fail = [error](const char* message, int number) -> long long { + if (error) *error = message; + errno = number; + return 0; + }; + if (minimum > maximum) return fail("invalid", 22); + errno = 0; + char* end = nullptr; + const long long value = std::strtoll(text, &end, 10); + if (errno == EINVAL || end == text || *end != '\0') return fail("invalid", 22); + if ((value == LLONG_MIN && errno == ERANGE) || value < minimum) return fail("too small", 34); + if ((value == LLONG_MAX && errno == ERANGE) || value > maximum) return fail("too large", 34); + if (error) *error = nullptr; + errno = savedError; + return value; +} + char* APS5_VABI basename_nid_postfix(const char* path) { thread_local char buffer[1024]; std::string_view name = path && *path ? path : "."; diff --git a/core/libs/tests/GuestStrings.cpp b/core/libs/tests/GuestStrings.cpp index e843751cd4..84836f5421 100644 --- a/core/libs/tests/GuestStrings.cpp +++ b/core/libs/tests/GuestStrings.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -13,6 +14,7 @@ char* APS5_VABI basename_nid_postfix(const char*); char* APS5_VABI __inet_ntoa_nid_postfix(unsigned int); char* APS5_VABI dirname_nid_postfix(const char*); int* APS5_VABI __error_nid_postfix(); +long long APS5_VABI strtonum_nid_postfix(const char*, long long, long long, const char**); int APS5_VABI strcasecmp_nid_postfix(const char*, const char*); int APS5_VABI strncasecmp_nid_postfix(const char*, const char*, std::size_t); std::size_t APS5_VABI strnlen_nid_postfix(const char*, std::size_t); @@ -262,7 +264,51 @@ static void CheckBcopy() { bcopy_nid_postfix(nullptr, nullptr, 0); } +static void CheckStrtonum() { + struct Valid { + const char* text; + long long value; + }; + for (const auto& test : {Valid{"0", 0}, Valid{"-0", 0}, Valid{" +42", 42}, Valid{"010", 10}, + Valid{"9223372036854775807", LLONG_MAX}, Valid{"-9223372036854775808", LLONG_MIN}}) { + const char* error = "unchanged"; + *__error_nid_postfix() = 13; + Require(strtonum_nid_postfix(test.text, LLONG_MIN, LLONG_MAX, &error) == test.value); + Require(error == nullptr && *__error_nid_postfix() == 13); + } + for (const auto& test : {Valid{"-5", -5}, Valid{"5", 5}, Valid{"0", 0}}) { + const char* error = "unchanged"; + Require(strtonum_nid_postfix(test.text, -5, 5, &error) == test.value && error == nullptr); + Require(strtonum_nid_postfix(test.text, test.value, test.value, &error) == test.value && error == nullptr); + } + const auto fails = [](const char* text, long long minimum, long long maximum, const char* expected, int number) { + const char* error = nullptr; + *__error_nid_postfix() = 13; + Require(strtonum_nid_postfix(text, minimum, maximum, &error) == 0); + Require(error != nullptr && std::strcmp(error, expected) == 0 && *__error_nid_postfix() == number); + *__error_nid_postfix() = 13; + Require(strtonum_nid_postfix(text, minimum, maximum, nullptr) == 0 && *__error_nid_postfix() == number); + }; + for (const char* text : {"", " ", "+", "-", "--1", "0x10", "0b10", "1x", "42 ", "1\n", + "9223372036854775808x", "-9223372036854775809x"}) + fails(text, LLONG_MIN, LLONG_MAX, "invalid", 22); + fails("0", 1, -1, "invalid", 22); + fails(nullptr, 1, -1, "invalid", 22); + fails("9223372036854775808", 1, -1, "invalid", 22); + fails("-6", -5, 5, "too small", 34); + fails("6", -5, 5, "too large", 34); + fails("0", 1, 5, "too small", 34); + fails("0", -5, -1, "too large", 34); + fails("-9223372036854775809", LLONG_MIN, LLONG_MAX, "too small", 34); + fails("9223372036854775808", LLONG_MIN, LLONG_MAX, "too large", 34); + fails("-9999999999999999999999999999999999", LLONG_MIN, LLONG_MAX, "too small", 34); + fails("9999999999999999999999999999999999", LLONG_MIN, LLONG_MAX, "too large", 34); + *__error_nid_postfix() = 34; + Require(strtonum_nid_postfix("42", 0, 100, nullptr) == 42 && *__error_nid_postfix() == 34); +} + int main() { + CheckStrtonum(); CheckBcopy(); if (!CheckDirnameLengths()) return 1; CheckBoundsCheckedFunctions(); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 17eb3d7260..b3d52c2407 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -2,6 +2,8 @@ ### Unknown function info +- [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. Implemented only on Linux; other platforms throw - [ApplicationGuestHeapApi](../../../core/libs/prx/libc/src/ApplicationProcessParameters.cpp) (libc) - on Linux, HLE application allocation uses a loaded guest SDK's ten allocator exports when they belong to one native module. Its CRT owns application allocator initialization and finalization; HLE retains the module until process exit. Partial or mixed providers throw. This avoids reinitializing an allocator already used by SDK startup; console behavior and other host platforms remain unverified. - [fdopen](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) validates descriptor access modes as FreeBSD 11.4 [fdopen.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/fdopen.c) does: a requested mode incompatible with a descriptor not opened for both reading and writing returns null with `EINVAL`, leaving the descriptor open. Windows reads the handle's granted access through the same host helper as `fcntl(F_GETFL)`; a closed descriptor returns null with `EBADF`. Console behavior is unverified. From 3cf8eec26a5d3dc74eada464c7c76065850a5db6 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Mon, 19 Oct 2026 19:01:11 +0200 Subject: [PATCH 149/269] docs(agc): record the PM4 type-0 packets a submission is rejected for Co-Authored-By: Claude Sonnet 5.5 --- docs/dev/debt/libSceAgcDriver.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index eeb7c92bac..e667f5c5a1 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -105,6 +105,7 @@ - [PM4 submission rings](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/SubmissionRing.cpp) (libSceAgcDriver) keep only the first 512 dwords of each submission, so the packets past them are not printed. The [failure report](../../../core/libs/prx/libc/src/FailureReport.cpp) dump does not allocate from the C runtime heap: packet names come from static strings, each line is formatted with integer and string conversions into a 512-byte stack buffer and written with one `WriteFile`. It still runs on whatever state the crash left. It waits about 100 yields for each ring's lock and otherwise prints that queue as busy, so a crash while recording a submission loses that queue's ring. It reads the rings without checking them, so a crash that corrupted them can fault inside the dump; such a fault during the report of an unhandled exception stops in that report's `Sleep(INFINITE)` and the process hangs instead of ending. The reports run at most once per process. Linux has no crash report: there the rings are printed only by `APS5_STALL_WATCHDOG_SECS`. - [WAIT_REG_MEM](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp) reports a diagnostic after `APS5_GPU_WAIT_TIMEOUT_MS` without treating the timeout as completion. An unsatisfied wait keeps polling and checking driver failures; a missing producer can therefore leave its queue waiting indefinitely. Existing recorded-label shortcuts remain in use. - A [WAIT_REG_MEM](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp) (libSceAgcDriver) on a queue 0x20 label can time out while the awaited word holds the address of another slot of the same label area instead of 0 or 1, with no store to that label in the PM4 ring window (`APS5_PM4_RING`). What writes the address (guest CPU or the emulator) and whether the console would have satisfied the wait before the slot was reused are unverified; the wait keeps polling and its queue stays blocked. Seen in several titles on a Windows NVIDIA GPU; not measured on hardware. +- [PM4 type-0 packets](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/CommandValidation.cpp) (libSceAgcDriver) - a dword whose packet type is 0, a zero header word included, is rejected with `unsupported PM4 packet type`, so the submission throws. Whether the console's command processor skips or executes such dwords, and what a run of them in front of a REWIND packet means, is unverified; shadPS4 (`liverpool.cpp`) and KytyPS5 (`pm4.cpp`) also treat type 0 as unsupported. A title that submits a zero run in a command buffer stops there. - [COND_EXEC](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp) reads its condition after the earlier packets of its queue complete. Without PFP_SYNC_ME the console's graphics PFP reads it ahead of the ME, so a title that races the read sees a later value here. - A [flip inside a COND_EXEC range](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/CommandValidation.cpp) throws: the flip is reserved with the video output when the buffer is submitted, and how the console withdraws a skipped one is unknown. - A [COND_EXEC range](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) must hold whole packets and end inside its command buffer. A chained INDIRECT_BUFFER or a REWIND inside a range throws, as what the CP does with the skipped dwords then is unknown. From 4e3e810a79f04d57bfd866c3e41f8839da4e8280 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Mon, 19 Oct 2026 19:23:21 +0200 Subject: [PATCH 150/269] docs(agc): describe the zero-header run in the PM4 type-0 debt entry Co-Authored-By: Claude Sonnet 5.5 --- docs/dev/debt/libSceAgcDriver.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index e667f5c5a1..3074da53c9 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -105,7 +105,7 @@ - [PM4 submission rings](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/SubmissionRing.cpp) (libSceAgcDriver) keep only the first 512 dwords of each submission, so the packets past them are not printed. The [failure report](../../../core/libs/prx/libc/src/FailureReport.cpp) dump does not allocate from the C runtime heap: packet names come from static strings, each line is formatted with integer and string conversions into a 512-byte stack buffer and written with one `WriteFile`. It still runs on whatever state the crash left. It waits about 100 yields for each ring's lock and otherwise prints that queue as busy, so a crash while recording a submission loses that queue's ring. It reads the rings without checking them, so a crash that corrupted them can fault inside the dump; such a fault during the report of an unhandled exception stops in that report's `Sleep(INFINITE)` and the process hangs instead of ending. The reports run at most once per process. Linux has no crash report: there the rings are printed only by `APS5_STALL_WATCHDOG_SECS`. - [WAIT_REG_MEM](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp) reports a diagnostic after `APS5_GPU_WAIT_TIMEOUT_MS` without treating the timeout as completion. An unsatisfied wait keeps polling and checking driver failures; a missing producer can therefore leave its queue waiting indefinitely. Existing recorded-label shortcuts remain in use. - A [WAIT_REG_MEM](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp) (libSceAgcDriver) on a queue 0x20 label can time out while the awaited word holds the address of another slot of the same label area instead of 0 or 1, with no store to that label in the PM4 ring window (`APS5_PM4_RING`). What writes the address (guest CPU or the emulator) and whether the console would have satisfied the wait before the slot was reused are unverified; the wait keeps polling and its queue stays blocked. Seen in several titles on a Windows NVIDIA GPU; not measured on hardware. -- [PM4 type-0 packets](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/CommandValidation.cpp) (libSceAgcDriver) - a dword whose packet type is 0, a zero header word included, is rejected with `unsupported PM4 packet type`, so the submission throws. Whether the console's command processor skips or executes such dwords, and what a run of them in front of a REWIND packet means, is unverified; shadPS4 (`liverpool.cpp`) and KytyPS5 (`pm4.cpp`) also treat type 0 as unsupported. A title that submits a zero run in a command buffer stops there. +- [PM4 type-0 packets](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/CommandValidation.cpp) (libSceAgcDriver) - a dword whose packet type is 0, a zero header word included, is rejected with `unsupported PM4 packet type`, so the submission throws. Whether the console's command processor skips or executes such dwords, and what a run of them in front of a REWIND packet means, is unverified; shadPS4 (`liverpool.cpp`) and KytyPS5 (`pm4.cpp`) also treat type 0 as unsupported. One such submission begins with a seven-dword NOP and an eight-dword ACQUIRE_MEM, then 169 dwords of zero headers (dwords 15 to 183, zero in one run and stale words in another), and its packets start at dword 184, as sections of NUM_INSTANCES, SET_BASE, REWIND, NOP and ACQUIRE_MEM separated by zero gaps of 26 to 455 dwords (odd lengths included); letting a zero header through only moved the stop to a stale word. - [COND_EXEC](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp) reads its condition after the earlier packets of its queue complete. Without PFP_SYNC_ME the console's graphics PFP reads it ahead of the ME, so a title that races the read sees a later value here. - A [flip inside a COND_EXEC range](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/CommandValidation.cpp) throws: the flip is reserved with the video output when the buffer is submitted, and how the console withdraws a skipped one is unknown. - A [COND_EXEC range](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) must hold whole packets and end inside its command buffer. A chained INDIRECT_BUFFER or a REWIND inside a range throws, as what the CP does with the skipped dwords then is unknown. From 14b52daf53aadcd3443bc88295a292ac0a9c7a71 Mon Sep 17 00:00:00 2001 From: erickerxhalli <313650569+erickerxhalli@users.noreply.github.com> Date: Mon, 19 Oct 2026 18:23:44 +0000 Subject: [PATCH 151/269] feat(libSceMove): report no connected PS Move controller Add sceMoveInit, sceMoveTerm, sceMoveOpen, sceMoveGetDeviceInfo and sceMoveSetVibration, and replace the throwing stubs of the other six exports. Behaviour and error codes follow shadPS4; signatures follow Orbital. A guest test covers the init state, argument checks and the no-controller results. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Arbzg9c2DA4DEFKVSvRgYv --- core/libs/CMakeLists.txt | 10 +++ core/libs/prx/libSceMove/Export.cpp | 92 +++++++++++++++++++---- core/libs/tests/GuestMove.cpp | 110 ++++++++++++++++++++++++++++ docs/dev/debt/libSceMove.md | 5 ++ 4 files changed, 203 insertions(+), 14 deletions(-) create mode 100644 core/libs/tests/GuestMove.cpp create mode 100644 docs/dev/debt/libSceMove.md diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 74a5ac5009..3823e0eef0 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -398,6 +398,16 @@ if(BUILD_TESTING) "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) + configure_windows_unwind(guest_move_tests) + add_test(NAME guest_move COMMAND guest_move_tests) + if(WIN32) + set_tests_properties(guest_move PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_netctl_ap_dialog_tests tests/GuestNetCtlApDialog.cpp) target_include_directories(guest_netctl_ap_dialog_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_netctl_ap_dialog_tests PRIVATE libSceNetCtlApDialog libc) diff --git a/core/libs/prx/libSceMove/Export.cpp b/core/libs/prx/libSceMove/Export.cpp index 6c2199ff94..f86a61a317 100644 --- a/core/libs/prx/libSceMove/Export.cpp +++ b/core/libs/prx/libSceMove/Export.cpp @@ -1,33 +1,97 @@ +#include #include #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { + +constexpr int MOVE_OK = 0; +constexpr int MOVE_NO_CONTROLLER_CONNECTED = 1; +constexpr int MOVE_ERROR_NOT_INIT = static_cast(0x80EE0001); +constexpr int MOVE_ERROR_ALREADY_INIT = static_cast(0x80EE0002); +constexpr int MOVE_ERROR_INVALID_ARG = static_cast(0x80EE0003); +constexpr int32_t MOVE_FIRST_HANDLE = 0x30B0100; +constexpr int32_t MOVE_HANDLE_STEP = 0x100; + +std::atomic initialized{false}; +std::atomic nextHandle{MOVE_FIRST_HANDLE}; + +} + extern "C" { +int APS5_VABI sceMoveInit(void) { + bool expected = false; + return initialized.compare_exchange_strong(expected, true) ? MOVE_OK : MOVE_ERROR_ALREADY_INIT; +} + +int APS5_VABI sceMoveTerm(void) { + return initialized.exchange(false) ? MOVE_OK : MOVE_ERROR_NOT_INIT; +} + +int APS5_VABI sceMoveOpen(int32_t userId, int32_t type, int32_t index) { + (void)userId; + (void)type; + (void)index; + if (!initialized) return MOVE_ERROR_NOT_INIT; + return nextHandle.fetch_add(MOVE_HANDLE_STEP); +} + int APS5_VABI sceMoveClose(int32_t handle) { - NotImplemented_nid_no_patch(__func__); - return 0; + (void)handle; + if (!initialized) return MOVE_ERROR_NOT_INIT; + return MOVE_OK; } + +int APS5_VABI sceMoveGetDeviceInfo(int32_t handle, void* info) { + (void)handle; + if (!initialized) return MOVE_ERROR_NOT_INIT; + if (!info) return MOVE_ERROR_INVALID_ARG; + return MOVE_NO_CONTROLLER_CONNECTED; +} + int APS5_VABI sceMoveGetExtensionPortInfo(int32_t handle, void* data) { - NotImplemented_nid_no_patch(__func__); - return 0; + (void)handle; + if (!initialized) return MOVE_ERROR_NOT_INIT; + if (!data) return MOVE_ERROR_INVALID_ARG; + return MOVE_NO_CONTROLLER_CONNECTED; } + int APS5_VABI sceMoveReadStateLatest(int32_t handle, void* data) { - NotImplemented_nid_no_patch(__func__); - return 0; + (void)handle; + if (!initialized) return MOVE_ERROR_NOT_INIT; + if (!data) return MOVE_ERROR_INVALID_ARG; + return MOVE_NO_CONTROLLER_CONNECTED; } -int APS5_VABI sceMoveReadStateRecent(int32_t handle, int64_t timestamp, void* data, int32_t* out_count) { - NotImplemented_nid_no_patch(__func__); - return 0; + +int APS5_VABI sceMoveReadStateRecent(int32_t handle, int64_t timestamp, void* data, int32_t* outCount) { + (void)handle; + if (!initialized) return MOVE_ERROR_NOT_INIT; + if (timestamp < 0 || !data || !outCount) return MOVE_ERROR_INVALID_ARG; + return MOVE_NO_CONTROLLER_CONNECTED; } -int APS5_VABI sceMoveResetLightSphere(int32_t handle) { - NotImplemented_nid_no_patch(__func__); - return 0; + +int APS5_VABI sceMoveSetVibration(int32_t handle, uint8_t motor) { + (void)handle; + (void)motor; + if (!initialized) return MOVE_ERROR_NOT_INIT; + return MOVE_NO_CONTROLLER_CONNECTED; } + int APS5_VABI sceMoveSetLightSphere(int32_t handle, uint8_t red, uint8_t green, uint8_t blue) { - NotImplemented_nid_no_patch(__func__); - return 0; + (void)handle; + (void)red; + (void)green; + (void)blue; + if (!initialized) return MOVE_ERROR_NOT_INIT; + return MOVE_NO_CONTROLLER_CONNECTED; +} + +int APS5_VABI sceMoveResetLightSphere(int32_t handle) { + (void)handle; + if (!initialized) return MOVE_ERROR_NOT_INIT; + return MOVE_OK; } } diff --git a/core/libs/tests/GuestMove.cpp b/core/libs/tests/GuestMove.cpp new file mode 100644 index 0000000000..e928abcc82 --- /dev/null +++ b/core/libs/tests/GuestMove.cpp @@ -0,0 +1,110 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include + +extern "C" { +int APS5_VABI sceMoveInit(void); +int APS5_VABI sceMoveTerm(void); +int APS5_VABI sceMoveOpen(std::int32_t userId, std::int32_t type, std::int32_t index); +int APS5_VABI sceMoveClose(std::int32_t handle); +int APS5_VABI sceMoveGetDeviceInfo(std::int32_t handle, void* info); +int APS5_VABI sceMoveGetExtensionPortInfo(std::int32_t handle, void* data); +int APS5_VABI sceMoveReadStateLatest(std::int32_t handle, void* data); +int APS5_VABI sceMoveReadStateRecent(std::int32_t handle, std::int64_t timestamp, void* data, std::int32_t* outCount); +int APS5_VABI sceMoveSetVibration(std::int32_t handle, std::uint8_t motor); +int APS5_VABI sceMoveSetLightSphere(std::int32_t handle, std::uint8_t red, std::uint8_t green, std::uint8_t blue); +int APS5_VABI sceMoveResetLightSphere(std::int32_t handle); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +constexpr int kOk = 0; +constexpr int kNoControllerConnected = 1; +constexpr int kNotInit = static_cast(0x80EE0001); +constexpr int kAlreadyInit = static_cast(0x80EE0002); +constexpr int kInvalidArg = static_cast(0x80EE0003); +constexpr std::int32_t kUserId = 0x10000000; +constexpr std::uint8_t kFill = 0x5A; + +struct Buffer { + std::uint8_t bytes[128]; + + Buffer() { std::memset(bytes, kFill, sizeof(bytes)); } + + bool Untouched() const { + for (std::uint8_t byte : bytes) { + if (byte != kFill) return false; + } + return true; + } +}; + +void RequireNotInit(std::int32_t handle) { + Buffer buffer; + std::int32_t count = -7; + Require(sceMoveOpen(kUserId, 0, 0) == kNotInit); + Require(sceMoveClose(handle) == kNotInit); + Require(sceMoveGetDeviceInfo(handle, buffer.bytes) == kNotInit); + Require(sceMoveGetExtensionPortInfo(handle, buffer.bytes) == kNotInit); + Require(sceMoveReadStateLatest(handle, buffer.bytes) == kNotInit); + Require(sceMoveReadStateRecent(handle, 0, buffer.bytes, &count) == kNotInit); + Require(sceMoveSetVibration(handle, 0xFF) == kNotInit); + Require(sceMoveSetLightSphere(handle, 0xFF, 0, 0) == kNotInit); + Require(sceMoveResetLightSphere(handle) == kNotInit); + Require(sceMoveTerm() == kNotInit); + Require(buffer.Untouched()); + Require(count == -7); +} + +} + +int main() { + RequireNotInit(0); + + Require(sceMoveInit() == kOk); + Require(sceMoveInit() == kAlreadyInit); + + const std::int32_t first = sceMoveOpen(kUserId, 0, 0); + const std::int32_t second = sceMoveOpen(kUserId, 0, 1); + Require(first > 0); + Require(second > 0); + Require(first != second); + + Require(sceMoveGetDeviceInfo(first, nullptr) == kInvalidArg); + Require(sceMoveGetExtensionPortInfo(first, nullptr) == kInvalidArg); + Require(sceMoveReadStateLatest(first, nullptr) == kInvalidArg); + + Buffer buffer; + std::int32_t count = -7; + Require(sceMoveReadStateRecent(first, -1, buffer.bytes, &count) == kInvalidArg); + Require(sceMoveReadStateRecent(first, 0, nullptr, &count) == kInvalidArg); + Require(sceMoveReadStateRecent(first, 0, buffer.bytes, nullptr) == kInvalidArg); + + Require(sceMoveGetDeviceInfo(first, buffer.bytes) == kNoControllerConnected); + Require(sceMoveGetExtensionPortInfo(first, buffer.bytes) == kNoControllerConnected); + Require(sceMoveReadStateLatest(first, buffer.bytes) == kNoControllerConnected); + Require(sceMoveReadStateRecent(first, 0, buffer.bytes, &count) == kNoControllerConnected); + Require(buffer.Untouched()); + Require(count == -7); + + Require(sceMoveSetVibration(first, 0xFF) == kNoControllerConnected); + Require(sceMoveSetLightSphere(first, 0, 0xFF, 0) == kNoControllerConnected); + Require(sceMoveResetLightSphere(first) == kOk); + + Require(sceMoveClose(first) == kOk); + Require(sceMoveClose(second) == kOk); + Require(sceMoveTerm() == kOk); + + RequireNotInit(first); + + Require(sceMoveInit() == kOk); + const std::int32_t third = sceMoveOpen(kUserId, 0, 0); + Require(third > 0); + Require(third != first); + Require(third != second); + Require(sceMoveClose(third) == kOk); + Require(sceMoveTerm() == kOk); +} diff --git a/docs/dev/debt/libSceMove.md b/docs/dev/debt/libSceMove.md new file mode 100644 index 0000000000..7aa1514fab --- /dev/null +++ b/docs/dev/debt/libSceMove.md @@ -0,0 +1,5 @@ +# libSceMove technical debt + +### Unknown function info + +- [libSceMove](../../../core/libs/prx/libSceMove/Export.cpp) (libSceMove) - no PS Move controller is modelled, and the behaviour and error codes follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/move/move.cpp) (PS4), not measured on a PS5; signatures follow [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json). `sceMoveOpen` returns a new handle (`0x30B0100`, then every `0x100`) for any user, type and index, and no call checks a handle, so `sceMoveClose` and `sceMoveResetLightSphere` succeed for any handle. The device info, state, extension port, vibration and light sphere calls return 1 (no controller connected) and write nothing. Before `sceMoveInit` and after `sceMoveTerm` every other call returns `NOT_INIT` (`0x80EE0001`), and a null output or a negative timestamp returns `INVALID_ARG` (`0x80EE0003`). Closes when: measured on a PS5 From d8b425f1bfadfd2b0a9bc852b80a544d4b1c8f52 Mon Sep 17 00:00:00 2001 From: Federico Marras Date: Mon, 19 Oct 2026 20:25:28 +0200 Subject: [PATCH 152/269] feat(libSceNetCtlAp): implement the access-point service that never starts --- core/libs/CMakeLists.txt | 10 ++ core/libs/prx/libSceNetCtlAp/Export.cpp | 114 +++++++++++++++----- core/libs/tests/GuestNetCtlAp.cpp | 134 ++++++++++++++++++++++++ docs/dev/debt/libSceNetCtlAp.md | 12 +++ 4 files changed, 242 insertions(+), 28 deletions(-) create mode 100644 core/libs/tests/GuestNetCtlAp.cpp create mode 100644 docs/dev/debt/libSceNetCtlAp.md diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 74a5ac5009..47bb4869ea 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -398,6 +398,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test_executable(guest_netctl_ap_tests tests/GuestNetCtlAp.cpp) + target_include_directories(guest_netctl_ap_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_netctl_ap_tests PRIVATE libSceNetCtlAp libc) + configure_windows_unwind(guest_netctl_ap_tests) + add_test(NAME guest_netctl_ap COMMAND guest_netctl_ap_tests) + if(WIN32) + set_tests_properties(guest_netctl_ap PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_netctl_ap_dialog_tests tests/GuestNetCtlApDialog.cpp) target_include_directories(guest_netctl_ap_dialog_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_netctl_ap_dialog_tests PRIVATE libSceNetCtlApDialog libc) diff --git a/core/libs/prx/libSceNetCtlAp/Export.cpp b/core/libs/prx/libSceNetCtlAp/Export.cpp index 8791105967..d0bcb19845 100644 --- a/core/libs/prx/libSceNetCtlAp/Export.cpp +++ b/core/libs/prx/libSceNetCtlAp/Export.cpp @@ -1,72 +1,130 @@ +#include #include +#include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { + +constexpr int NET_CTL_AP_ERROR_NOT_STARTED = static_cast(0x8041298F); +constexpr int NET_CTL_AP_ERROR_INVALID_TYPE = static_cast(0x80412990); +constexpr int NET_CTL_AP_ERROR_INVALID_ADDR = static_cast(0x80412991); +constexpr int NET_CTL_AP_ERROR_NOT_INITIALIZED = static_cast(0x80412994); +constexpr int NET_CTL_AP_ERROR_CALLBACK_MAX = static_cast(0x80412995); +constexpr int NET_CTL_AP_ERROR_INVALID_ID = static_cast(0x80412996); +constexpr int NET_CTL_AP_ERROR_ID_NOT_FOUND = static_cast(0x80412997); +constexpr int NET_CTL_AP_ERROR_INVALID_SIZE = static_cast(0x80412998); +constexpr int NET_CTL_AP_STATE_STOPPED = 0; +constexpr int NET_CTL_AP_EVENT_TYPE_FIRST = 1; +constexpr int NET_CTL_AP_EVENT_TYPE_LAST = 4; +constexpr std::size_t NET_CTL_AP_INFO_SIZE = 152; +constexpr std::size_t NET_CTL_AP_CONNECT_INFO_SIZE = 184; +constexpr int MAX_CALLBACKS = 8; + +using NetCtlApCallback = void(APS5_VABI*)(int, void*); + +struct CallbackSlot { + NetCtlApCallback func = nullptr; + void* arg = nullptr; +}; + +std::mutex g_lock; +bool g_initialized = false; +CallbackSlot g_callbacks[MAX_CALLBACKS] = {}; + +bool ValidEventType(int eventType) { + return eventType >= NET_CTL_AP_EVENT_TYPE_FIRST && eventType <= NET_CTL_AP_EVENT_TYPE_LAST; +} + +} + extern "C" { int APS5_VABI sceNetCtlApInit() { - NotImplemented_nid_no_patch(__func__); + std::lock_guard lock(g_lock); + if (g_initialized) throw std::runtime_error("sceNetCtlApInit: a second initialization is not implemented"); + g_initialized = true; return 0; } void APS5_VABI sceNetCtlApTerm() { - NotImplemented_nid_no_patch(__func__); + std::lock_guard lock(g_lock); + g_initialized = false; + for (auto& slot : g_callbacks) slot = {}; } int APS5_VABI sceNetCtlApCheckCallback() { - NotImplemented_nid_no_patch(__func__); - return 0; + std::lock_guard lock(g_lock); + return g_initialized ? 0 : NET_CTL_AP_ERROR_NOT_INITIALIZED; } int APS5_VABI sceNetCtlApClearEvent(int eventType) { - (void)eventType; - NotImplemented_nid_no_patch(__func__); - return 0; + std::lock_guard lock(g_lock); + if (!g_initialized) return NET_CTL_AP_ERROR_NOT_INITIALIZED; + return ValidEventType(eventType) ? 0 : NET_CTL_AP_ERROR_INVALID_TYPE; } -int APS5_VABI sceNetCtlApRegisterCallback(void (*func)(int, void*), void* arg, int* cid) { - (void)func; - (void)arg; - (void)cid; - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNetCtlApRegisterCallback(NetCtlApCallback func, void* arg, int* cid) { + std::lock_guard lock(g_lock); + if (!g_initialized) return NET_CTL_AP_ERROR_NOT_INITIALIZED; + if (!func || !cid) return NET_CTL_AP_ERROR_INVALID_ADDR; + for (int index = 0; index < MAX_CALLBACKS; ++index) { + if (!g_callbacks[index].func) { + g_callbacks[index] = {func, arg}; + *cid = index; + return 0; + } + } + return NET_CTL_AP_ERROR_CALLBACK_MAX; } int APS5_VABI sceNetCtlApUnregisterCallback(int cid) { - (void)cid; - NotImplemented_nid_no_patch(__func__); + std::lock_guard lock(g_lock); + if (!g_initialized) return NET_CTL_AP_ERROR_NOT_INITIALIZED; + if (cid < 0 || cid >= MAX_CALLBACKS) return NET_CTL_AP_ERROR_INVALID_ID; + if (!g_callbacks[cid].func) return NET_CTL_AP_ERROR_ID_NOT_FOUND; + g_callbacks[cid] = {}; return 0; } int APS5_VABI sceNetCtlApGetResult(int eventType, int* errorCode) { - (void)eventType; - (void)errorCode; - NotImplemented_nid_no_patch(__func__); + 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; + *errorCode = 0; return 0; } int APS5_VABI sceNetCtlApGetInfo(void* info) { - (void)info; - NotImplemented_nid_no_patch(__func__); - return 0; + std::lock_guard lock(g_lock); + if (!g_initialized) return NET_CTL_AP_ERROR_NOT_INITIALIZED; + if (!info) return NET_CTL_AP_ERROR_INVALID_ADDR; + if (*static_cast(info) != NET_CTL_AP_INFO_SIZE) return NET_CTL_AP_ERROR_INVALID_SIZE; + return NET_CTL_AP_ERROR_NOT_STARTED; } int APS5_VABI sceNetCtlApGetState(int* state) { - (void)state; - NotImplemented_nid_no_patch(__func__); + std::lock_guard lock(g_lock); + if (!g_initialized) return NET_CTL_AP_ERROR_NOT_INITIALIZED; + if (!state) return NET_CTL_AP_ERROR_INVALID_ADDR; + *state = NET_CTL_AP_STATE_STOPPED; return 0; } int APS5_VABI sceNetCtlApStop() { - NotImplemented_nid_no_patch(__func__); - return 0; + std::lock_guard lock(g_lock); + 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; - (void)connectInfo; - NotImplemented_nid_no_patch(__func__); - return 0; + std::lock_guard lock(g_lock); + if (!g_initialized) return NET_CTL_AP_ERROR_NOT_INITIALIZED; + if (!connectInfo) return NET_CTL_AP_ERROR_INVALID_ADDR; + if (*static_cast(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 new file mode 100644 index 0000000000..fbb8b0f01d --- /dev/null +++ b/core/libs/tests/GuestNetCtlAp.cpp @@ -0,0 +1,134 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceNetCtlApInit(void); +void APS5_VABI sceNetCtlApTerm(void); +int APS5_VABI sceNetCtlApCheckCallback(void); +int APS5_VABI sceNetCtlApClearEvent(int eventType); +int APS5_VABI sceNetCtlApRegisterCallback(void (APS5_VABI *func)(int, void*), void* arg, int* cid); +int APS5_VABI sceNetCtlApUnregisterCallback(int cid); +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); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +constexpr int kErrNotStarted = static_cast(0x8041298F); +constexpr int kErrInvalidType = static_cast(0x80412990); +constexpr int kErrInvalidAddr = static_cast(0x80412991); +constexpr int kErrNotInitialized = static_cast(0x80412994); +constexpr int kErrCallbackMax = static_cast(0x80412995); +constexpr int kErrInvalidId = static_cast(0x80412996); +constexpr int kErrIdNotFound = static_cast(0x80412997); +constexpr int kErrInvalidSize = static_cast(0x80412998); + +int g_calls = 0; + +void APS5_VABI Callback(int, void*) { ++g_calls; } + +void RequireUninitialized() { + int value = 0x5a5a; + std::uint8_t block[184] = {}; + Require(sceNetCtlApCheckCallback() == kErrNotInitialized); + Require(sceNetCtlApClearEvent(1) == kErrNotInitialized); + Require(sceNetCtlApRegisterCallback(Callback, nullptr, &value) == kErrNotInitialized && value == 0x5a5a); + Require(sceNetCtlApUnregisterCallback(0) == kErrNotInitialized); + Require(sceNetCtlApGetResult(1, &value) == kErrNotInitialized && value == 0x5a5a); + Require(sceNetCtlApGetInfo(block) == kErrNotInitialized); + Require(sceNetCtlApGetState(&value) == kErrNotInitialized && value == 0x5a5a); + Require(sceNetCtlApStop() == kErrNotInitialized); + Require(sceNetCtlApGetConnectInfo(nullptr, block) == kErrNotInitialized); +} + +} + +int main() { + RequireUninitialized(); + Require(sceNetCtlApInit() == 0); + + int state = -1; + Require(sceNetCtlApGetState(&state) == 0 && state == 0); + Require(sceNetCtlApGetState(nullptr) == kErrInvalidAddr); + + std::uint8_t info[152]; + std::memset(info, 0x5a, sizeof(info)); + const std::uint64_t infoSize = sizeof(info); + std::memcpy(info, &infoSize, sizeof(infoSize)); + Require(sceNetCtlApGetInfo(info) == kErrNotStarted); + for (std::size_t i = sizeof(infoSize); i < sizeof(info); ++i) Require(info[i] == 0x5a); + Require(sceNetCtlApGetInfo(nullptr) == kErrInvalidAddr); + const std::uint64_t wrongInfoSize = sizeof(info) - 8; + std::memcpy(info, &wrongInfoSize, sizeof(wrongInfoSize)); + Require(sceNetCtlApGetInfo(info) == kErrInvalidSize); + + std::uint8_t connect[184]; + std::memset(connect, 0x5a, sizeof(connect)); + const std::uint64_t connectSize = sizeof(connect); + std::memcpy(connect, &connectSize, sizeof(connectSize)); + std::uint8_t option[] = "opt"; + Require(sceNetCtlApGetConnectInfo(option, connect) == kErrNotStarted); + Require(sceNetCtlApGetConnectInfo(nullptr, connect) == kErrNotStarted); + 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); + std::memcpy(connect, &wrongConnectSize, sizeof(wrongConnectSize)); + Require(sceNetCtlApGetConnectInfo(option, connect) == kErrInvalidSize); + + Require(sceNetCtlApStop() == kErrNotStarted); + + for (int type = 1; type <= 4; ++type) { + int code = -1; + Require(sceNetCtlApClearEvent(type) == 0); + Require(sceNetCtlApGetResult(type, &code) == 0 && code == 0); + Require(sceNetCtlApGetResult(type, nullptr) == kErrInvalidAddr); + } + for (const int type : {0, 5, -1}) { + int code = 0x5a5a; + Require(sceNetCtlApClearEvent(type) == kErrInvalidType); + Require(sceNetCtlApGetResult(type, &code) == kErrInvalidType && code == 0x5a5a); + } + + int cids[8]; + for (int i = 0; i < 8; ++i) { + cids[i] = -1; + Require(sceNetCtlApRegisterCallback(Callback, &cids[i], &cids[i]) == 0 && cids[i] == i); + } + int extra = -1; + Require(sceNetCtlApRegisterCallback(Callback, nullptr, &extra) == kErrCallbackMax && extra == -1); + Require(sceNetCtlApRegisterCallback(nullptr, nullptr, &extra) == kErrInvalidAddr); + Require(sceNetCtlApRegisterCallback(Callback, nullptr, nullptr) == kErrInvalidAddr); + Require(sceNetCtlApCheckCallback() == 0 && sceNetCtlApCheckCallback() == 0); + Require(g_calls == 0); + Require(sceNetCtlApUnregisterCallback(3) == 0); + Require(sceNetCtlApUnregisterCallback(3) == kErrIdNotFound); + Require(sceNetCtlApUnregisterCallback(-1) == kErrInvalidId); + Require(sceNetCtlApUnregisterCallback(8) == kErrInvalidId); + Require(sceNetCtlApRegisterCallback(Callback, nullptr, &extra) == 0 && extra == 3); + + bool threw = false; + try { + sceNetCtlApInit(); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw); + + sceNetCtlApTerm(); + RequireUninitialized(); + Require(sceNetCtlApInit() == 0); + int cid = -1; + Require(sceNetCtlApRegisterCallback(Callback, nullptr, &cid) == 0 && cid == 0); + Require(sceNetCtlApGetState(&state) == 0 && state == 0); + sceNetCtlApTerm(); + sceNetCtlApTerm(); + RequireUninitialized(); +} diff --git a/docs/dev/debt/libSceNetCtlAp.md b/docs/dev/debt/libSceNetCtlAp.md new file mode 100644 index 0000000000..012a82c476 --- /dev/null +++ b/docs/dev/debt/libSceNetCtlAp.md @@ -0,0 +1,12 @@ +# libSceNetCtlAp technical debt + +### Functional + +- [libSceNetCtlAp](../../../core/libs/prx/libSceNetCtlAp/Export.cpp) (libSceNetCtlAp) - the access point never starts: a title starts it through [libSceNetCtlApDialog](libSceNetCtlApDialog.md), which always reports the user as having cancelled. `sceNetCtlApGetState` reports Stopped (0), `sceNetCtlApGetInfo`, `sceNetCtlApGetConnectInfo` and `sceNetCtlApStop` return `NOT_STARTED` (`0x8041298F`) without writing, and no callback ever fires. Signatures, states, structure sizes (152 and 184 bytes), the 8-callback limit and error codes come from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/NetCtlAp.cs) + +### 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 From b0a9676844a94e917aef043e034a9fa987c841c7 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 16:57:27 +0000 Subject: [PATCH 153/269] feat(libSceNet): implement interface names, random values and net memory Implement sceNetGetIfName, sceNetGetRandom, sceNetMemoryAllocate and sceNetMemoryFree with the signatures shadPS4 uses, and cover them in guest_sce_net_tests. Co-Authored-By: Claude Opus 5.5 (1M context) --- core/libs/prx/libSceNet/Export.cpp | 44 +++++++++++++++++++------- core/libs/tests/GuestSceNet.cpp | 51 ++++++++++++++++++++++++++++++ docs/dev/debt/libSceNet.md | 8 ++--- 3 files changed, 88 insertions(+), 15 deletions(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 945f71079c..3ba78ecd98 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -36,6 +37,7 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestHeap.hpp" #include "prx/libkernel/Socket/include/SocketPoll.hpp" #include "prx/libkernel/Socket/include/SocketRuntime.hpp" #include "prx/libkernel/Time/include/Time.hpp" @@ -1938,9 +1940,14 @@ int APS5_VABI sceNetGetIfListOnce(void) { return 0; } -int APS5_VABI sceNetGetIfName(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +const char* APS5_VABI sceNetGetIfName(std::uint32_t index) { + static constexpr std::array names = { + "lo0", "eth0", "eth1", "dbg0", "wlan0", "wlan1", "gbe0", "bt0", "phone0", "pppoe0"}; + if (index >= names.size()) { + set_errno(NET_EINVAL); + return ""; + } + return names[index]; } int APS5_VABI sceNetGetIfnameNumList(void) { @@ -1953,8 +1960,12 @@ int APS5_VABI sceNetGetNameToIndex(void) { return 0; } -int APS5_VABI sceNetGetRandom(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceNetGetRandom(std::uint32_t* output) { + if (output == nullptr) return fail(NET_EINVAL); + static std::mutex mutex; + static std::random_device device; + const std::lock_guard lock(mutex); + *output = static_cast(device()); return 0; } @@ -1992,14 +2003,25 @@ int APS5_VABI sceNetIoctl(void) { return 0; } -int APS5_VABI sceNetMemoryAllocate(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +void* APS5_VABI sceNetMemoryAllocate(std::int64_t size, int flags) { + constexpr int zeroFill = 2; + if ((flags & ~zeroFill) != 0) + throw std::runtime_error("sceNetMemoryAllocate: flags " + std::to_string(flags) + " are not supported"); + if (size <= 0) return nullptr; + void* memory = nullptr; + try { + memory = GuestHeap::GuestHeapAllocate_nid_postfix(static_cast(size)); + } catch (const std::bad_alloc&) { + return nullptr; + } catch (const std::length_error&) { + return nullptr; + } + if ((flags & zeroFill) != 0) std::memset(memory, 0, static_cast(size)); + return memory; } -int APS5_VABI sceNetMemoryFree(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +void APS5_VABI sceNetMemoryFree(void* memory) { + GuestHeap::GuestHeapFree_nid_postfix(memory); } int APS5_VABI sceNetSetDnsInfoToKernel(void) { diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index 76d5b63109..dcc846c102 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -124,6 +124,10 @@ std::int64_t APS5_VABI sceNetRecvmsg(int, NetMsghdr*, int); int APS5_VABI sceNetPoolCreate(const char*, int, int); 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 sceNetMemoryFree(void*); } static void Require(bool condition) { @@ -260,6 +264,50 @@ static void CheckPoolStats() { Require(Failed(sceNetGetMemoryPoolStats(pool, &stats), 9)); } +static void CheckIfName() { + constexpr std::array names = { + "lo0", "eth0", "eth1", "dbg0", "wlan0", "wlan1", "gbe0", "bt0", "phone0", "pppoe0"}; + for (std::uint32_t index = 0; index < names.size(); ++index) { + Require(std::strcmp(sceNetGetIfName(index), names[index]) == 0); + } + *sceNetErrnoLoc() = 0; + const char* missing = sceNetGetIfName(10); + Require(missing != nullptr && missing[0] == '\0' && *sceNetErrnoLoc() == 22); + *sceNetErrnoLoc() = 0; + missing = sceNetGetIfName(0xffffffffu); + Require(missing != nullptr && missing[0] == '\0' && *sceNetErrnoLoc() == 22); +} + +static void CheckRandom() { + Require(Failed(sceNetGetRandom(nullptr), 22)); + std::array values{}; + for (auto& value : values) Require(sceNetGetRandom(&value) == 0); + Require(std::any_of(values.begin() + 1, values.end(), [&](std::uint32_t value) { return value != values[0]; })); +} + +static void CheckNetMemory() { + Require(sceNetMemoryAllocate(0, 0) == nullptr); + Require(sceNetMemoryAllocate(-1, 2) == nullptr); + auto* zeroed = static_cast(sceNetMemoryAllocate(4096, 2)); + Require(zeroed != nullptr); + Require(std::all_of(zeroed, zeroed + 4096, [](std::uint8_t byte) { return byte == 0; })); + std::memset(zeroed, 0xa5, 4096); + auto* plain = static_cast(sceNetMemoryAllocate(64, 0)); + Require(plain != nullptr && plain != zeroed); + std::memset(plain, 0x5a, 64); + Require(zeroed[4095] == 0xa5); + sceNetMemoryFree(plain); + sceNetMemoryFree(zeroed); + sceNetMemoryFree(nullptr); + bool threw = false; + try { + sceNetMemoryAllocate(16, 1); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw); +} + static void CheckUnboundSocketName() { for (const int family : {2, 28}) { for (const int type : {1, 2}) { @@ -1209,6 +1257,9 @@ int main() { Require(sceNetInit_nid_postfix() == 0); CheckEpollWritable(); CheckPoolStats(); + CheckIfName(); + CheckRandom(); + CheckNetMemory(); CheckNetstat(); CheckEmptyEpoll(); CheckDontWait(); diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index a0947b7725..1d782a9520 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -40,10 +40,10 @@ - [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) - 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) - 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` @@ -51,8 +51,8 @@ - [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), which forwards it to `sceNetInetPton` and does not know the flags either. Flags 0 converts as `sceNetInetPton` does; any other flags throw, since their meaning (an IPv6 scope or otherwise) is not known. Unverified on hardware - [sceNetIoctl](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetMemoryAllocate](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetMemoryFree](../../../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 From 5c6f6da7dcf7d91fabe3522b1795c3b3927cb5fb Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 18:30:55 +0000 Subject: [PATCH 154/269] feat(libkernel): accept pipe read ends in sceKernelAddReadEvent Co-Authored-By: Claude Opus 5.5 (1M context) --- core/libs/prx/libkernel/Equeue/Equeue.cpp | 12 +++-- core/libs/tests/GuestSocketEvents.cpp | 53 +++++++++++++++++++++++ docs/dev/debt/libkernel.md | 1 + 3 files changed, 62 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libkernel/Equeue/Equeue.cpp b/core/libs/prx/libkernel/Equeue/Equeue.cpp index 2b5f864d8e..ec37c3ac94 100644 --- a/core/libs/prx/libkernel/Equeue/Equeue.cpp +++ b/core/libs/prx/libkernel/Equeue/Equeue.cpp @@ -3,6 +3,7 @@ #include "prx/libc/include/General.hpp" #include "prx/libkernel/Socket/include/SocketRuntime.hpp" #include "prx/libkernel/File/include/File.hpp" +#include "prx/libkernel/File/include/HostStreams.hpp" #include #include @@ -630,13 +631,16 @@ int APS5_VABI sceKernelDeleteAmprSystemEvent(KernelEqueue eq, int id) { static int AddDescriptorEvent(KernelEqueue eq, int fd, std::size_t size, void* udata, int16_t filter) { const bool isSocket = GuestSockets::IsOpen(fd); - if (!isSocket && DescriptorIsOpen_nid_no_patch(fd)) { - NotImplemented_nid_no_patch(filter == EVFILT_READ ? "sceKernelAddReadEvent: non-socket descriptor" : "sceKernelAddWriteEvent: non-socket descriptor"); + std::int64_t available = 0; + bool eof = false; + const bool isPipe = !isSocket && filter == EVFILT_READ && !File::IsHostStream(fd) && GuestSockets::PipeReadState(fd, &available, &eof); + if (!isSocket && !isPipe && DescriptorIsOpen_nid_no_patch(fd)) { + NotImplemented_nid_no_patch(filter == EVFILT_READ ? "sceKernelAddReadEvent: descriptor that is neither a socket nor the read end of a pipe" : "sceKernelAddWriteEvent: non-socket descriptor"); } - if (size > 1) { + if (size > 1 && !isPipe) { NotImplemented_nid_no_patch(filter == EVFILT_READ ? "sceKernelAddReadEvent: low watermark" : "sceKernelAddWriteEvent: low watermark"); } - if (!isSocket) { + if (!isSocket && !isPipe) { return SCE_KERNEL_ERROR_EBADF; } return EqueueAddEvent_nid_postfix(eq, EqueueDescriptorEvent(fd, filter, EV_ADD, udata)); diff --git a/core/libs/tests/GuestSocketEvents.cpp b/core/libs/tests/GuestSocketEvents.cpp index 7bda0e3e06..1ef4eea859 100644 --- a/core/libs/tests/GuestSocketEvents.cpp +++ b/core/libs/tests/GuestSocketEvents.cpp @@ -16,6 +16,9 @@ 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); int APS5_VABI close_nid_postfix(int); +int APS5_VABI pipe_nid_postfix(int*); +std::int64_t APS5_VABI read_nid_postfix(int, void*, std::uint64_t); +std::int64_t APS5_VABI write_nid_postfix(int, const char*, std::int64_t); int APS5_VABI sceKernelCreateEqueue(KernelEqueue* eq, const char* name); int APS5_VABI sceKernelDeleteEqueue(KernelEqueue eq); int APS5_VABI sceKernelWaitEqueue(KernelEqueue eq, KernelEvent* ev, int num, int* out, const KernelUseconds* timo); @@ -44,7 +47,57 @@ static bool Rejects(int (APS5_VABI *add)(KernelEqueue, int, std::size_t, void*), return false; } +static void PipeReadEvents() { + KernelEqueue eq = 0; + Require(sceKernelCreateEqueue(&eq, "pipe") == SCE_OK); + int pipe[2]; + Require(pipe_nid_postfix(pipe) == 0); + const KernelUseconds shortTimeout = 20000; + const KernelUseconds timeout = 1000000; + int marker = 0; + int count = 0; + std::array events{}; + + Require(sceKernelAddReadEvent(eq, pipe[0], 64, &marker) == SCE_OK); + Require(sceKernelWaitEqueue(eq, events.data(), events.size(), &count, &shortTimeout) == SCE_KERNEL_ERROR_ETIMEDOUT); + Require(count == 0); + Require(write_nid_postfix(pipe[1], "abc", 3) == 3); + Require(sceKernelWaitEqueue(eq, events.data(), events.size(), &count, &timeout) == SCE_OK); + Require(count == 1 && events[0].ident == static_cast(pipe[0]) && events[0].filter == EVFILT_READ); + Require(events[0].data == 3 && events[0].udata == &marker && (events[0].flags & EV_EOF) == 0); + char buffer[4]{}; + Require(read_nid_postfix(pipe[0], buffer, sizeof(buffer)) == 3); + Require(sceKernelWaitEqueue(eq, events.data(), events.size(), &count, &shortTimeout) == SCE_KERNEL_ERROR_ETIMEDOUT); + + const auto start = std::chrono::steady_clock::now(); + std::thread writer([&pipe] { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + Require(write_nid_postfix(pipe[1], "d", 1) == 1); + }); + Require(sceKernelWaitEqueue(eq, events.data(), events.size(), &count, nullptr) == SCE_OK); + writer.join(); + Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(40)); + Require(count == 1 && events[0].data == 1); + + Require(close_nid_postfix(pipe[1]) == 0); + Require(sceKernelWaitEqueue(eq, events.data(), events.size(), &count, &timeout) == SCE_OK); + Require(count == 1 && events[0].data == 1 && (events[0].flags & EV_EOF) != 0); + Require(read_nid_postfix(pipe[0], buffer, sizeof(buffer)) == 1); + Require(sceKernelWaitEqueue(eq, events.data(), events.size(), &count, &timeout) == SCE_OK); + Require(count == 1 && events[0].data == 0 && (events[0].flags & EV_EOF) != 0); + Require(sceKernelDeleteReadEvent(eq, pipe[0]) == SCE_OK); + Require(sceKernelDeleteReadEvent(eq, pipe[0]) == SCE_KERNEL_ERROR_ENOENT); + + int oneWay[2]; + Require(pipe_nid_postfix(oneWay) == 0); + Require(Rejects(sceKernelAddWriteEvent, eq, oneWay[1], 1)); + Require(close_nid_postfix(oneWay[0]) == 0 && close_nid_postfix(oneWay[1]) == 0); + Require(close_nid_postfix(pipe[0]) == 0); + Require(sceKernelDeleteEqueue(eq) == SCE_OK); +} + int main() { + PipeReadEvents(); const int listener = socket_nid_postfix(2, 1, 6); Require(listener >= 0); 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 04009b5680..69f43861b1 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -131,6 +131,7 @@ - [pthread_attr_getinheritsched, pthread_attr_setinheritsched, pthread_attr_getscope, pthread_attr_setscope, pthread_attr_getstackaddr](../../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) follow FreeBSD 11 libthr ([thr_attr.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libthr/thread/thr_attr.c), [thr_create.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libthr/thread/thr_create.c)), not measured on the console: the scope defaults to `PTHREAD_SCOPE_SYSTEM` (2), only it and `PTHREAD_SCOPE_PROCESS` (0) are accepted and other values return `EINVAL`, and an inherit value other than `PTHREAD_INHERIT_SCHED` (4) or `PTHREAD_EXPLICIT_SCHED` (0) returns `ENOTSUP`. `pthread_attr_getstackaddr` returns the configured stack address pointer (null by default). A null output of `pthread_attr_getinheritsched` throws, since libthr writes through it unchecked. A thread keeps the scope it was created with, taken from the creating thread under `PTHREAD_INHERIT_SCHED`, and `pthread_attr_get_np` and `scePthreadAttrGet` report it, but every guest thread is a host thread at system scope, so process scope changes nothing - [Shared memory](../../../core/libs/prx/libkernel/File/src/SharedMemory.cpp) (libkernel) on Linux implements anonymous and named objects, bounded descriptor writes and seeks, and `shm_unlink` with effective read/write permission checks from FreeBSD 11. Named paths up to 1023 bytes use `/anyps5-shm-` plus SHA-1; collisions are not detected, and the namespace is shared across processes in the host mount namespace. Named creation applies the guest umask without changing the host mask. Linux procfs and `/dev/shm` are required. Operations use an advisory directory lock; external host operations bypass it, unrelated objects serialize, and descriptor operations require two spare host descriptors and compare device/inode with the initial descriptor identity on both the pinned-object and ordinary-file fallback paths. [Guest mappings](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) support shared and private mappings of these descriptors through the allocation tracker and pin a duplicate descriptor during validation and mapping; mapping requires one spare host descriptor; file-backed mappings use the write-watch scan fallback because aliases and descriptor writes bypass per-mapping tracking. On Windows, anonymous objects are delete-on-close temporary files and `mmap` maps them as [regular files](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp); named objects remain unsupported. Invalid non-null name pointers, console behaviour and title compatibility remain unverified. - [sceKernelAddReadEvent and sceKernelAddWriteEvent](../../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel): a read event on a listening socket, here and in `kevent`, reports the number of completed connections waiting to be accepted in `data`, as FreeBSD 11.4's [`filt_solisten`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L3384-L3392) reports `so_qlen`: on Linux a TCP listener's accept queue from `TCP_INFO` (`tcpi_unacked`, which Linux fills with the accept backlog for a listening socket), and elsewhere, or for a listener that is not TCP, 1 while a connection is waiting, as no host call reports that count. A write event reports the free space of the socket's send buffer in `data`, as FreeBSD 11.4's [`filt_sowrite`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L3358-L3382) reports `sbspace`: on Linux the host's `SO_SNDBUF` less the bytes `SIOCOUTQ` counts as queued, which include the host's per-packet overhead, so the space shrinks by somewhat more than the bytes sent. Windows has no query for that space and macOS is not wired up, so both report the whole `SO_SNDBUF`, which does not shrink as data is queued. Only guest sockets are supported; other descriptors and a low watermark above 1 throw +- [sceKernelAddReadEvent on pipes](../../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel) also takes the read end of a pipe, as `kevent` does through FreeBSD's `filt_piperead`, and ignores the size argument there. Host streams, other descriptors and a write event on a pipe throw - [close](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) and [setsockopt](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) reach a [libSceNet](../../../core/libs/prx/libSceNet/Export.cpp) socket through hooks libSceNet registers, as `select` polls them; the other libkernel socket calls do not. libSceNet numbers its sockets from 32 in its own table, so a CRT file descriptor and a socket can share a number; `close` then closes the socket. On the console both are one kernel descriptor space - [sceKernelPread, sceKernelPwrite, sceKernelPreadv, sceKernelPwritev](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) on Windows transfer at the given offset with an overlapped `ReadFile`/`WriteFile` and restore the descriptor's file pointer afterwards. Guest scalar transfers, positioned transfers, vectored transfers and seeks share a position lock across duplicated descriptors; a vector holds it for the whole operation. Independently opened descriptors and other processes do not share that lock, so vector transfers over their overlapping file ranges are not coordinated. Handles that are not disk files return `ESPIPE`. A pipe or console `readv` can return a short read when no more input is available; each iovec is transferred separately, so a failure after the first transfer returns the bytes already done - [sceKernelAioSubmitReadCommands, sceKernelAioSubmitWriteCommands and their Multiple variants](../../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) on Windows transfer through the same positioned path as `sceKernelPread`/`sceKernelPwrite` and restore the descriptor's file pointer afterwards; positioned transfers, plain transfers and seeks of one descriptor are serialized per descriptor (shared across duplicated descriptors), so a concurrent transfer observes a stable file pointer From c79d189655a3426f4b5f35e9c6c435f8e5402860 Mon Sep 17 00:00:00 2001 From: Federico Marras Date: Mon, 19 Oct 2026 20:02:16 +0200 Subject: [PATCH 155/269] test(relinker): run the outputs that stop at int3 without a core dump --- core/relinker/relinker/tests/signal_run.py | 13 +++++++++++++ .../relinker/tests/test_linux_entry_argv.py | 3 ++- .../relinker/tests/test_linux_fixed_base.py | 5 +++-- .../relinker/tests/test_linux_gnu_segments.py | 5 +++-- .../tests/test_linux_input_program_headers.py | 4 +++- .../tests/test_linux_program_header_order.py | 3 ++- core/relinker/relinker/tests/test_optional_rela.py | 3 ++- 7 files changed, 28 insertions(+), 8 deletions(-) create mode 100644 core/relinker/relinker/tests/signal_run.py diff --git a/core/relinker/relinker/tests/signal_run.py b/core/relinker/relinker/tests/signal_run.py new file mode 100644 index 0000000000..6118b5253d --- /dev/null +++ b/core/relinker/relinker/tests/signal_run.py @@ -0,0 +1,13 @@ +import subprocess + + +def without_core_dump(): + import resource + + _, hard = resource.getrlimit(resource.RLIMIT_CORE) + limit = 1 if hard == resource.RLIM_INFINITY or hard >= 1 else 0 + resource.setrlimit(resource.RLIMIT_CORE, (limit, limit)) + + +def run_without_core_dump(command, **options): + return subprocess.run(command, preexec_fn=without_core_dump, **options) diff --git a/core/relinker/relinker/tests/test_linux_entry_argv.py b/core/relinker/relinker/tests/test_linux_entry_argv.py index 82c043c19f..4dad8c6e07 100644 --- a/core/relinker/relinker/tests/test_linux_entry_argv.py +++ b/core/relinker/relinker/tests/test_linux_entry_argv.py @@ -8,6 +8,7 @@ import subprocess import sys import tempfile +from signal_run import run_without_core_dump from test_linux_load_alignment import PT_LOAD, PT_SCE_VERSION, fixture, loads ENTRY = 0x10 @@ -69,7 +70,7 @@ def main(): for executable in (output, relocated): executable.chmod(0o755) for arguments, expected in ((["Z"], -signal.SIGTRAP), (["Z", "extra"], -signal.SIGILL)): - executed = subprocess.run([str(executable), *arguments], capture_output=True, timeout=20) + executed = run_without_core_dump([str(executable), *arguments], capture_output=True, timeout=20) assert executed.returncode == expected, (executable.name, arguments, executed.returncode) for address, error in ((0x7FF00000, None), (0x80000000, "Entry stub call exceeds rel32 range")): source.write_bytes(far_load_fixture(address)) diff --git a/core/relinker/relinker/tests/test_linux_fixed_base.py b/core/relinker/relinker/tests/test_linux_fixed_base.py index e5904a65f7..84244f4533 100644 --- a/core/relinker/relinker/tests/test_linux_fixed_base.py +++ b/core/relinker/relinker/tests/test_linux_fixed_base.py @@ -8,6 +8,7 @@ import subprocess import sys import tempfile +from signal_run import run_without_core_dump from test_linux_load_alignment import PT_SCE_VERSION PT_NULL = 0 @@ -144,10 +145,10 @@ def main(): if sys.platform.startswith("linux") and platform.machine() in ("x86_64", "AMD64"): fixed_path.chmod(0o755) - executed = subprocess.run([str(fixed_path)], capture_output=True, timeout=20) + executed = run_without_core_dump([str(fixed_path)], capture_output=True, timeout=20) assert executed.returncode == -signal.SIGTRAP, ("the image did not run at the base with its relocation applied", executed.returncode) relocated_fixed_path.chmod(0o755) - executed = subprocess.run([str(relocated_fixed_path)], capture_output=True, timeout=20) + 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) for name, image, base, error in ( diff --git a/core/relinker/relinker/tests/test_linux_gnu_segments.py b/core/relinker/relinker/tests/test_linux_gnu_segments.py index 8452776a66..223c131881 100644 --- a/core/relinker/relinker/tests/test_linux_gnu_segments.py +++ b/core/relinker/relinker/tests/test_linux_gnu_segments.py @@ -7,6 +7,7 @@ import subprocess import sys import tempfile +from signal_run import run_without_core_dump from test_linux_load_alignment import fixture PT_GNU_STACK = 0x6474E551 @@ -84,7 +85,7 @@ def main(): found = {header[0]: header for header in headers(output.read_bytes())} assert (PT_GNU_RELRO in found) == keeps_relro, "PT_GNU_RELRO was kept without writable load coverage" output.chmod(output.stat().st_mode | 0o111) - execution = subprocess.run([str(output)], capture_output=True, text=True, timeout=20) + 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) synthesized_executable = executable_image(0x4000) @@ -98,7 +99,7 @@ def main(): 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 output.chmod(output.stat().st_mode | 0o111) - execution = subprocess.run([str(output)], capture_output=True, text=True, timeout=20) + 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) print("Linux GNU segment tests passed") diff --git a/core/relinker/relinker/tests/test_linux_input_program_headers.py b/core/relinker/relinker/tests/test_linux_input_program_headers.py index 7735779a79..5cda474d60 100644 --- a/core/relinker/relinker/tests/test_linux_input_program_headers.py +++ b/core/relinker/relinker/tests/test_linux_input_program_headers.py @@ -8,6 +8,8 @@ import subprocess import sys import tempfile +from signal_run import run_without_core_dump + PT_LOAD = 1 PT_DYNAMIC = 2 PT_INTERP = 3 @@ -69,7 +71,7 @@ def run_expecting_trap(name, output): if not (sys.platform.startswith("linux") and platform.machine() in ("x86_64", "AMD64")): return output.chmod(0o755) - executed = subprocess.run([str(output)], capture_output=True, timeout=20) + executed = run_without_core_dump([str(output)], capture_output=True, timeout=20) assert executed.returncode == -signal.SIGTRAP, (name, executed.returncode, executed.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 96d9c172bd..6d223a4135 100644 --- a/core/relinker/relinker/tests/test_linux_program_header_order.py +++ b/core/relinker/relinker/tests/test_linux_program_header_order.py @@ -8,6 +8,7 @@ import subprocess import sys import tempfile +from signal_run import run_without_core_dump from test_guest_intel_trampolines import main_fixture from test_guest_module_directories import module_with_symbol from test_linux_load_alignment import fixture as single_load_fixture @@ -78,7 +79,7 @@ def run_expecting_trap(name, output): if not (sys.platform.startswith("linux") and platform.machine() in ("x86_64", "AMD64")): return output.chmod(0o755) - executed = subprocess.run([str(output)], capture_output=True, timeout=20) + executed = run_without_core_dump([str(output)], capture_output=True, timeout=20) assert executed.returncode == -signal.SIGTRAP, (name, executed.returncode, executed.stderr) diff --git a/core/relinker/relinker/tests/test_optional_rela.py b/core/relinker/relinker/tests/test_optional_rela.py index be8870b3ec..5dcc15edc5 100644 --- a/core/relinker/relinker/tests/test_optional_rela.py +++ b/core/relinker/relinker/tests/test_optional_rela.py @@ -8,6 +8,7 @@ import subprocess import sys import tempfile +from signal_run import run_without_core_dump from test_dynamic_segment import fixture as base_fixture from test_linux_dynamic_strings import dynamic_strings, file_offset from test_linux_load_alignment import loads @@ -108,7 +109,7 @@ def main(): assert run.returncode == 42, (case.name, run.returncode, run.stderr) if target == "linux" and plt and sys.platform.startswith("linux") and platform.machine() in ("x86_64", "AMD64"): output.chmod(0o755) - run = subprocess.run([str(output)], capture_output=True, timeout=20) + run = run_without_core_dump([str(output)], capture_output=True, timeout=20) assert run.returncode == -signal.SIGTRAP, (case.name, run.returncode, run.stderr) invalid = (("missing-address", ((8, 24), (9, 24)), (), "DT_RELA"), From b7b9b7ad28255a3585cbe296a0ac5b687b141ec5 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Mon, 19 Oct 2026 18:47:23 +0000 Subject: [PATCH 156/269] test(libc): cover the final mspace trace slot --- core/libs/prx/libc/CMakeLists.txt | 13 ++++++++++++ core/libs/tests/GuestHeapTraceInfo.cpp | 29 ++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 core/libs/tests/GuestHeapTraceInfo.cpp diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index db26a5c925..e19041c74a 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -911,6 +911,19 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(guest_heap_trace_info_tests EXCLUDE_FROM_ALL ${LIBS_INCLUDE_DIR}/tests/GuestHeapTraceInfo.cpp) +target_include_directories(guest_heap_trace_info_tests PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(guest_heap_trace_info_tests PRIVATE libc) +configure_windows_unwind(guest_heap_trace_info_tests) +if(BUILD_TESTING) + set_target_properties(guest_heap_trace_info_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) + add_test(NAME guest_heap_trace_info COMMAND guest_heap_trace_info_tests) + if(WIN32) + set_tests_properties(guest_heap_trace_info PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + if(MINGW) if(BUILD_TESTING) enable_language(ASM) diff --git a/core/libs/tests/GuestHeapTraceInfo.cpp b/core/libs/tests/GuestHeapTraceInfo.cpp new file mode 100644 index 0000000000..56c18baf06 --- /dev/null +++ b/core/libs/tests/GuestHeapTraceInfo.cpp @@ -0,0 +1,29 @@ +#include "prx/libc/include/HeapDiagnostics.hpp" +#include +#include +#include + +namespace { + +void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "%s\n", message); + std::abort(); +} + +} + +int main() { + LibcHeapInfo info{sizeof(LibcHeapInfo), 0, 1, nullptr, nullptr}; + LibcHeapTraceInfo_nid_no_patch(&info); + Require(info.unknown2 == 0, "heap trace info did not clear unknown2"); + Require(info.mspace_atomic_id_mask != nullptr, "heap trace info returned no mspace mask"); + Require(info.mstate_table != nullptr, "heap trace info returned no mstate table"); + + constexpr std::uint64_t first = 0x0123456789abcdefull; + constexpr std::uint64_t last = 0xfedcba9876543210ull; + info.mstate_table[0] = first; + info.mstate_table[64] = last; + Require(info.mstate_table[0] == first, "heap trace info base slot is not writable"); + Require(info.mstate_table[64] == last, "heap trace info final 64-bit mask slot is not writable"); +} From 0a3af80de9e44d6ce0cebd52ceef61a4d09df904 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Mon, 19 Oct 2026 18:47:23 +0000 Subject: [PATCH 157/269] fix(libc): back every native mspace trace slot --- core/libs/prx/libc/src/HeapDiagnostics.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libc/src/HeapDiagnostics.cpp b/core/libs/prx/libc/src/HeapDiagnostics.cpp index 135c46f928..7c6dd60831 100644 --- a/core/libs/prx/libc/src/HeapDiagnostics.cpp +++ b/core/libs/prx/libc/src/HeapDiagnostics.cpp @@ -9,7 +9,7 @@ namespace { std::uint64_t mspaceAtomicIdMask = 0; -std::array mstateTable{}; +std::array mstateTable{}; static_assert(sizeof(LibcHeapInfo) == 32); static_assert(offsetof(LibcHeapInfo, mspace_atomic_id_mask) == 16); From ec2e056d506866826e0b8531d54a239d1c4fadaf Mon Sep 17 00:00:00 2001 From: Sergio <73033344+Hash-0x@users.noreply.github.com> Date: Mon, 19 Oct 2026 20:55:33 +0200 Subject: [PATCH 158/269] feat(libSceRtc): implement process-local clock setters --- core/libs/prx/libSceRtc/Export.cpp | 67 ++++++++++++++++++++---------- core/libs/tests/GuestRtc.cpp | 24 +++++++++++ docs/dev/debt/libSceRtc.md | 9 +++- 3 files changed, 77 insertions(+), 23 deletions(-) diff --git a/core/libs/prx/libSceRtc/Export.cpp b/core/libs/prx/libSceRtc/Export.cpp index 629ee77fa5..929e0f7a94 100644 --- a/core/libs/prx/libSceRtc/Export.cpp +++ b/core/libs/prx/libSceRtc/Export.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -31,6 +32,15 @@ constexpr std::uint64_t UNIX_EPOCH_TICK = UNIX_EPOCH_DAYS * TICKS_PER_DAY; constexpr std::uint64_t WIN32_EPOCH_TICK = WIN32_EPOCH_DAYS * TICKS_PER_DAY; constexpr std::uint64_t MAX_TICK = 3652059ull * TICKS_PER_DAY - 1; +struct ClockState { + std::uint64_t tick = 0; + std::chrono::steady_clock::time_point setAt{}; + bool isSet = false; +}; + +std::mutex clockMutex; +ClockState clocks[5]; + bool isLeapYear(std::int64_t year) { return (year % 4 == 0 && year % 100 != 0) || year % 400 == 0; } @@ -90,11 +100,31 @@ RtcDateTime fromTick(std::uint64_t tick) { static_cast(timeOfDay % TICKS_PER_MINUTE / TICKS_PER_SECOND), static_cast(timeOfDay % TICKS_PER_SECOND)}; } -std::uint64_t currentTick() { +std::uint64_t currentTick(std::size_t clock = 0) { + ClockState state; + { + std::lock_guard lock(clockMutex); + state = clocks[clock]; + } + if (state.isSet) { + const auto elapsed = std::chrono::duration_cast(std::chrono::steady_clock::now() - state.setAt).count(); + const auto elapsedTicks = static_cast(elapsed); + return elapsedTicks > std::numeric_limits::max() - state.tick + ? std::numeric_limits::max() : state.tick + elapsedTicks; + } + if (clock == 2 || clock == 3) return currentTick(1); const auto now = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); return UNIX_EPOCH_TICK + static_cast(now); } +int setCurrentTick(std::size_t clock, const RtcTick* tick) { + if (!tick) return SCE_RTC_ERROR_INVALID_POINTER; + if (tick->tick < UNIX_EPOCH_TICK) return SCE_RTC_ERROR_INVALID_VALUE; + std::lock_guard lock(clockMutex); + clocks[clock] = ClockState{tick->tick, std::chrono::steady_clock::now(), true}; + return 0; +} + std::int64_t localOffsetSeconds(std::uint64_t utcTick) { const auto elapsed = std::chrono::microseconds{static_cast(utcTick) - static_cast(UNIX_EPOCH_TICK)}; const auto seconds = static_cast(std::chrono::floor(elapsed).count()); @@ -359,7 +389,9 @@ int APS5_VABI sceRtcGetCurrentTick(RtcTick* tick) { } int APS5_VABI sceRtcGetCurrentNetworkTick(RtcTick* tick) { - return sceRtcGetCurrentTick(tick); + if (!tick) return SCE_RTC_ERROR_INVALID_POINTER; + tick->tick = currentTick(1); + return 0; } int APS5_VABI sceRtcGetCurrentClock(RtcDateTime* time, int time_zone_minutes) { @@ -645,44 +677,37 @@ int APS5_VABI sceRtcCompareTick(const RtcTick* first, const RtcTick* second) { } int APS5_VABI sceRtcGetCurrentRawNetworkTick(RtcTick* tick) { - return sceRtcGetCurrentTick(tick); + if (!tick) return SCE_RTC_ERROR_INVALID_POINTER; + tick->tick = currentTick(4); + return 0; } int APS5_VABI sceRtcGetCurrentAdNetworkTick(RtcTick* tick) { - return sceRtcGetCurrentTick(tick); + if (!tick) return SCE_RTC_ERROR_INVALID_POINTER; + tick->tick = currentTick(2); + return 0; } int APS5_VABI sceRtcGetCurrentDebugNetworkTick(RtcTick* tick) { - return sceRtcGetCurrentTick(tick); -} - -static int checkSettableTick(const RtcTick* tick) { if (!tick) return SCE_RTC_ERROR_INVALID_POINTER; - return tick->tick < UNIX_EPOCH_TICK ? SCE_RTC_ERROR_INVALID_VALUE : 0; + tick->tick = currentTick(3); + return 0; } int APS5_VABI sceRtcSetCurrentTick(const RtcTick* tick) { - if (const int result = checkSettableTick(tick); result != 0) return result; - NotImplemented_nid_no_patch(__func__); - return 0; + return setCurrentTick(0, tick); } int APS5_VABI sceRtcSetCurrentNetworkTick(const RtcTick* tick) { - if (const int result = checkSettableTick(tick); result != 0) return result; - NotImplemented_nid_no_patch(__func__); - return 0; + return setCurrentTick(1, tick); } int APS5_VABI sceRtcSetCurrentAdNetworkTick(const RtcTick* tick) { - if (const int result = checkSettableTick(tick); result != 0) return result; - NotImplemented_nid_no_patch(__func__); - return 0; + return setCurrentTick(2, tick); } int APS5_VABI sceRtcSetCurrentDebugNetworkTick(const RtcTick* tick) { - if (const int result = checkSettableTick(tick); result != 0) return result; - NotImplemented_nid_no_patch(__func__); - return 0; + return setCurrentTick(3, tick); } int APS5_VABI sceRtcSetConf(int first, int second, int third, int fourth) { diff --git a/core/libs/tests/GuestRtc.cpp b/core/libs/tests/GuestRtc.cpp index a1d820e72d..7ed06372c1 100644 --- a/core/libs/tests/GuestRtc.cpp +++ b/core/libs/tests/GuestRtc.cpp @@ -17,6 +17,7 @@ int APS5_VABI sceRtcGetTickResolution(void); int APS5_VABI sceRtcGetTick(const RtcDateTime*, RtcTick*); int APS5_VABI sceRtcSetTick(RtcDateTime*, const RtcTick*); int APS5_VABI sceRtcGetCurrentTick(RtcTick*); +int APS5_VABI sceRtcGetCurrentNetworkTick(RtcTick*); int APS5_VABI sceRtcGetCurrentClock(RtcDateTime*, int); int APS5_VABI sceRtcGetCurrentClockLocalTime(RtcDateTime*); int APS5_VABI sceRtcConvertUtcToLocalTime(const RtcTick*, RtcTick*); @@ -60,7 +61,12 @@ int APS5_VABI sceRtcFormatRFC3339Precise(char*, const RtcTick*, int, int); int APS5_VABI sceRtcFormatRFC3339PreciseLocalTime(char*, const RtcTick*, int); int APS5_VABI sceRtcCompareTick(const RtcTick*, const RtcTick*); int APS5_VABI sceRtcGetCurrentAdNetworkTick(RtcTick*); +int APS5_VABI sceRtcGetCurrentRawNetworkTick(RtcTick*); +int APS5_VABI sceRtcGetCurrentDebugNetworkTick(RtcTick*); int APS5_VABI sceRtcSetCurrentTick(const RtcTick*); +int APS5_VABI sceRtcSetCurrentNetworkTick(const RtcTick*); +int APS5_VABI sceRtcSetCurrentAdNetworkTick(const RtcTick*); +int APS5_VABI sceRtcSetCurrentDebugNetworkTick(const RtcTick*); } static void Require(bool value) { if (!value) std::abort(); } @@ -613,5 +619,23 @@ int main() { Require(sceRtcGetCurrentAdNetworkTick(nullptr) == invalidPointer); RtcTick tooEarly{unixEpochTick - 1}; Require(sceRtcSetCurrentTick(&tooEarly) == invalidValue && sceRtcSetCurrentTick(nullptr) == invalidPointer); + const RtcTick systemClock{leapDayTick}; + const RtcTick networkClock{leapDayTick + 10000000}; + const RtcTick adClock{leapDayTick + 20000000}; + const RtcTick debugClock{leapDayTick + 30000000}; + Require(sceRtcSetCurrentTick(&systemClock) == 0); + Require(sceRtcSetCurrentNetworkTick(&networkClock) == 0); + Require(sceRtcGetCurrentAdNetworkTick(¤t) == 0 && current.tick >= networkClock.tick && current.tick < networkClock.tick + 1000000); + Require(sceRtcGetCurrentDebugNetworkTick(¤t) == 0 && current.tick >= networkClock.tick && current.tick < networkClock.tick + 1000000); + Require(sceRtcSetCurrentAdNetworkTick(&adClock) == 0); + Require(sceRtcSetCurrentDebugNetworkTick(&debugClock) == 0); + Require(sceRtcSetCurrentNetworkTick(&tooEarly) == invalidValue && sceRtcSetCurrentNetworkTick(nullptr) == invalidPointer); + Require(sceRtcSetCurrentAdNetworkTick(&tooEarly) == invalidValue && sceRtcSetCurrentAdNetworkTick(nullptr) == invalidPointer); + Require(sceRtcSetCurrentDebugNetworkTick(&tooEarly) == invalidValue && sceRtcSetCurrentDebugNetworkTick(nullptr) == invalidPointer); + Require(sceRtcGetCurrentTick(¤t) == 0 && current.tick >= systemClock.tick && current.tick < systemClock.tick + 1000000); + Require(sceRtcGetCurrentNetworkTick(¤t) == 0 && current.tick >= networkClock.tick && current.tick < networkClock.tick + 1000000); + Require(sceRtcGetCurrentAdNetworkTick(¤t) == 0 && current.tick >= adClock.tick && current.tick < adClock.tick + 1000000); + Require(sceRtcGetCurrentDebugNetworkTick(¤t) == 0 && current.tick >= debugClock.tick && current.tick < debugClock.tick + 1000000); + Require(sceRtcGetCurrentRawNetworkTick(¤t) == 0 && current.tick > unixEpochTick); } } diff --git a/docs/dev/debt/libSceRtc.md b/docs/dev/debt/libSceRtc.md index 39dc328d7f..3fc3155064 100644 --- a/docs/dev/debt/libSceRtc.md +++ b/docs/dev/debt/libSceRtc.md @@ -10,7 +10,12 @@ - [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, 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 on other firmware and title contexts are unavailable. - [sceRtcFormatRFC2822, sceRtcFormatRFC2822LocalTime, sceRtcFormatRFC3339LocalTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - only shadPS4 (PS4) has bodies. A null tick returns `INVALID_POINTER` like the existing `sceRtcFormatRFC3339`, where shadPS4 formats the current time instead; offsets outside ±1439 minutes return `INVALID_VALUE` like `sceRtcFormatRFC3339`; the local-time variants use the host's offset at the given instant (shadPS4 uses `sceKernelGettimezone`, the current offset) - [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 -- [sceRtcGetCurrentRawNetworkTick, sceRtcGetCurrentAdNetworkTick, sceRtcGetCurrentDebugNetworkTick](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - the console reads clocks 0x13, 0x12 and 0x11 (the ad and debug clocks fall back to the network clock 0x10) and returns the clock time plus the Unix epoch in ticks, or `0x80B50001` when the clock is unsupported (errno 5). Those console clocks are not modelled; all three return the system clock like `sceRtcGetCurrentNetworkTick`. 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) - the console rejects a null pointer with `0x80B50002` and a tick before the Unix epoch with `0x80B50003`, then sets the system clock (clock 0, 0x10, 0x12 or 0x11) from the tick. A valid tick throws: the host clock is not set. sceRtcSetConf passes its third and fourth arguments to the same clock call with clock 0 and throws; what the pair selects is unknown +- [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 - [sceRtcFormatRFC3339Precise, sceRtcFormatRFC3339PreciseLocalTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - read from the disassembly of libSceRtc.sprx on firmware 13.60: a null tick means the current tick, a precision above 6 or a zone outside -1439 to 1439 minutes returns `0x80B50003`, and the fraction is the microseconds truncated to the requested number of digits. Not compared with console output. The output buffer size is unknown; at most 33 bytes are written - [sceRtcGetYear, sceRtcGetMonth, sceRtcGetDay, sceRtcGetHour, sceRtcGetMinute, sceRtcGetSecond, sceRtcGetMicrosecond, sceRtcSetYear, sceRtcSetMonth, sceRtcSetDay, sceRtcSetHour, sceRtcSetMinute, sceRtcSetSecond, sceRtcSetMicrosecond](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - inferred component behavior without confirmed hardware trace. Component setters validate individual boundaries (e.g. day 1..31) independently without cross-field calendar validation; getters return `0x80B50002` (`SCE_RTC_ERROR_INVALID_POINTER`) for a null pointer + +### Functional + +- The process-local clocks set by [sceRtcSetCurrentTick, sceRtcSetCurrentNetworkTick, sceRtcSetCurrentAdNetworkTick and sceRtcSetCurrentDebugNetworkTick](../../../core/libs/prx/libSceRtc/Export.cpp) are visible only through libSceRtc. The system clock and libkernel clock APIs keep reporting host time; cross-library clock consistency is not modelled. The console's clock interactions are not measured From f4a4b8aea94f74f1b142ccdc38862323f3c05b72 Mon Sep 17 00:00:00 2001 From: battleblob1 Date: Sat, 17 Oct 2026 06:59:25 +0300 Subject: [PATCH 159/269] fix(recompiler): replay every eight-bit stencil reference a pixel shader may export Without VK_EXT_shader_stencil_export (NVIDIA), stencil reference exports are emulated by replaying the draw once per value. An export the prover could not bound to at most 16 values was refused. The replay shader compares the export's low eight bits, so an unbounded export now replays all 256 values, and proven values are reduced to their low eight bits as the comparison sees them. Refs #4276 Co-Authored-By: Claude Opus 5.5 --- .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 4 +++ .../libSceAgcDriver/tests/MaskedSelect.cpp | 36 +++++++++++++++++++ .../tests/execution/StencilReplay.cpp | 7 ++-- .../src/SpirvMemory/SpirvEmitterHelpers.cpp | 2 +- core/shader/recompiler/StencilReplay.cpp | 8 +++-- docs/dev/debt/libSceAgcDriver.md | 2 +- 6 files changed, 52 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 1fb537a91b..13d64a0e3f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -2103,6 +2103,10 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& Require(!draw.indirect && !state.primitiveRestart && !state.rectList && !state.quadList && !state.stages.mesh && !state.stages.tessellation && state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, "stencil replay supports direct triangle lists without restart, mesh or tessellation"); Require(!state.depthBiasPerFace && !ShadersWriteMemory(shaders), "stencil replay refuses face splits and shader memory writes"); Require(std::ranges::all_of(shaders, [](const CompiledShader& shader) { return shader.program != nullptr && shader.program->stencilReplaySafe && (shader.stage == ShaderRecompiler::ShaderStage::Vertex || shader.stage == ShaderRecompiler::ShaderStage::Fragment); }), "stencil replay refuses stages with side effects, clocks, subgroup dependencies or primitive ID inputs"); + if (stencilReferences.size() == 256u) { + static std::once_flag unbounded; + std::call_once(unbounded, [] { APS5_LOG_CHARS_OUT("Stencil replay: a draw replays all 256 stencil reference values because its pixel shader's export is not bounded, recording 256 passes per triangle and instance"); }); + } for (const auto& shader : shaders) { if (shader.stage != ShaderRecompiler::ShaderStage::Vertex) continue; Require((shader.program->hostSubgroupSize == 1u || shader.program->hostSubgroupSize == context.subgroup.subgroupSize) && shader.program->spirv.size() >= 5u && shader.program->spirv[1] >= 0x00010000u && shader.program->spirv[1] < 0x00010600u, "stencil replay vertex subgroup contract differs from the physical device"); diff --git a/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp b/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp index 7b03f83944..ff2a3e40a7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp @@ -987,6 +987,42 @@ int main() { return StencilReferenceValues(b.program) == std::vector{0u, 255u} && !StencilReplaySafe(b.program); }); + passed &= run("an unprovable stencil reference replays every eight-bit value", [] { + Builder b; + b.program.Resources().stage = IrShaderStage::Pixel; + auto& zero = b.constant(0u); + auto& reference = b.emit(IrOpcode::UndefU32, IrType::U32, {}); + ExportInfo info{}; + info.kind = ExportTargetKind::MrtZ; + info.en = 2u; + info.vm = true; + b.program.Metadata().exportInfo.push_back(info); + ExportFlags flags{0u}; + std::uint64_t bits = 0; + std::memcpy(&bits, &flags, sizeof(flags)); + auto& value = b.emit(IrOpcode::CompositeConstructU32x4, IrType::U32x4, {&zero, &reference, &zero, &zero}); + b.emit(IrOpcode::SetAttribute, IrType::Void, {&value, &b.truth()}, bits); + const auto values = StencilReferenceValues(b.program); + return values.size() == 256u && values.front() == 0u && values.back() == 255u; + }); + + passed &= run("a proven stencil reference replays its eight-bit value", [] { + Builder b; + b.program.Resources().stage = IrShaderStage::Pixel; + auto& zero = b.constant(0u); + ExportInfo info{}; + info.kind = ExportTargetKind::MrtZ; + info.en = 2u; + info.vm = true; + b.program.Metadata().exportInfo.push_back(info); + ExportFlags flags{0u}; + std::uint64_t bits = 0; + std::memcpy(&bits, &flags, sizeof(flags)); + auto& value = b.emit(IrOpcode::CompositeConstructU32x4, IrType::U32x4, {&zero, &b.constant(300u), &zero, &zero}); + b.emit(IrOpcode::SetAttribute, IrType::Void, {&value, &b.truth()}, bits); + return StencilReferenceValues(b.program) == std::vector{44u}; + }); + passed &= run("stencil replay admits own-lane masks and their whole-quad expansion as export guards", [&] { Builder b; b.program.Resources().stage = IrShaderStage::Pixel; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StencilReplay.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StencilReplay.cpp index d6a3055e6d..3612fae6ef 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/StencilReplay.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StencilReplay.cpp @@ -321,9 +321,10 @@ int main() { } Require(refused, "unsafe primitive routing was not refused"); } - bool unknownRefused = false; - try { Draw(*device, ColumnPixelCode, exported, replace()); } catch (const std::exception& error) { unknownRefused = std::string(error.what()).find("proven set") != std::string::npos; } - Require(unknownRefused, "unbounded stencil reference was not refused"); + Draw(*device, ColumnPixelCode, exported, replace()); + Check(all, "unbounded stencil export"); + Draw(*device, PlainPixelCode, plain, equal(5u)); + Check([](std::uint32_t x, std::uint32_t) { return x == 5u; }, "unbounded stencil export replaces each column's own value"); bool earlyRefused = false; try { Draw(*device, alternating, 0x1012u, replace()); } catch (const std::exception& error) { earlyRefused = std::string(error.what()).find("late tests") != std::string::npos; } Require(earlyRefused, "early stencil testing was not refused"); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp index 1e420f0147..06b38ce674 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp @@ -412,7 +412,7 @@ void DefineOutputs(SpirvEmitterState& state) { case StageOutputKind::StencilRef: if (std::ranges::find(state.supportedCapabilities, static_cast(spv::CapabilityStencilExportEXT)) == state.supportedCapabilities.end()) { const auto values = StencilReferenceValues(state.program, PixelInfo(state).psStencilOperationValueExport ? 8u : 0u); - if (values.empty()) throw std::runtime_error("stencil reference export emulation requires a proven set of at most 16 values"); + if (values.empty()) throw std::runtime_error("stencil reference export emulation found no stencil export"); if (!StencilReferenceDefinedForKeptInvocations(state.program)) throw std::runtime_error("stencil reference export emulation requires a stencil export on every kept path"); if (PixelInfo(state).psEarlyZ || PixelInfo(state).psOrderedPixelShader || !StencilReplaySafe(state.program)) throw std::runtime_error("stencil reference export emulation requires a replay-safe pixel shader with late tests"); state.stencilReplay = true; diff --git a/core/shader/recompiler/StencilReplay.cpp b/core/shader/recompiler/StencilReplay.cpp index cd8c8d6416..fae2057358 100644 --- a/core/shader/recompiler/StencilReplay.cpp +++ b/core/shader/recompiler/StencilReplay.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -605,9 +606,12 @@ std::vector StencilReferenceValues(const IrProgram& program, std: if (info.kind != ExportTargetKind::MrtZ || (info.en & 2u) == 0u) continue; std::set visiting; const auto values = Values(program, instruction->Argument(0), visiting, 0u, 1u, instruction->Argument(1)); - if (!values) return {}; + if (!values) { + std::vector all(256u); + std::iota(all.begin(), all.end(), 0u); + return all; + } for (const auto value : *values) result.insert((value >> shift) & 0xffu); - if (result.size() > 16u) return {}; } } return {result.begin(), result.end()}; diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index eeb7c92bac..c5cdf6c3a3 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -61,7 +61,7 @@ - [Initial stencil state](../../../core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp) uses zero for `DB_STENCIL_CONTROL`, `DB_STENCILREFMASK` and `DB_STENCILREFMASK_BF` (context `0x10b`-`0x10d`), including after `CLEAR_STATE`. The public V0, V4, V5 and V7-V11 [register-default tables](../../../core/libs/prx/libSceAgc/Command/include/RegisterDefaultsData.hpp) supply these values. Linux's [GFX10 clear-state table](https://github.com/torvalds/linux/blob/master/drivers/gpu/drm/amd/amdgpu/clearstate_gfx10.h) agrees on the control register but uses `0x01000000` for the reference masks; the console reset values have not been measured. Planet Coaster (PPSA01736 v01.017) enables stencil tests without writing the control register. - [Type-0 and type-1 texture descriptors](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) (`TYPE` = 0 or 1 in dword 3) with a nonzero base are bound as null images, like a zero base. TYPE 0 comes from a gfx1036 (RDNA2) measurement in issue #4139 (image sample, load and resinfo return zero, no fault). TYPE 1 comes from a gfx1010 (RDNA1) measurement in #4139 and #5828 (`image_sample_lz`, `image_sample_b`, `image_load` and `image_get_resinfo` return zero, no fault) with format 22 only; TYPE 1 is not measured on RDNA2, with other formats or with depth-compare reads (`image_sample_c*`). Neither type was measured on the console's gfx1013. - [Multisampled color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) lose their guest-read guards and cannot be reused after a successful allocation removal or unmap covers their full guarded range. The next target, sampled or storage lookup removes retired cache owners; recorded work and sampled views keep their images alive until their last reference is released. Partial unmaps keep the guard; partial allocation reuse and guest access to live multisampled data remain unsupported. -- [Stencil reference export replay](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) on devices without `VK_EXT_shader_stencil_export` supports at most sixteen values proved from constant, select and phi exports, with an export proved on every kept path. It records instances, then their triangles, then the reference classes of each triangle. One pipeline filters fragments at shader termination against a dynamic push value before late stencil and depth tests. The [Vulkan stencil-reference replacement rule](https://docs.vulkan.org/refpages/latest/refpages/source/FragStencilRefEXT.html) discards bits above the attachment's stencil width; supported stencil attachments have eight bits. Numeric subgroup observations throw except bounded own-lane mask plumbing and group control with kept-lane and derivative-helper coverage proved. WQM inputs must exclude helper contributions, since [non-quad subgroup operations may treat helpers as inactive](https://docs.vulkan.org/glslext/latest/glslext/khr/GL_KHR_shader_subgroup.html). Readonly guarded SRT reads may report internal host faults; these draws wait and check faults before publishing results, and loops throw. Indirect draws, restart, strips, fans, tessellation, mesh, multisampling, early tests, ordered pixel execution, guest shader memory writes, other fault-producing memory access, clocks, primitive ID inputs, attachment feedback and pipeline face/channel splits throw. An exhausted stage push range throws. Equivalence to console ordering has not been measured on console hardware. +- [Stencil reference export replay](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) on devices without `VK_EXT_shader_stencil_export` replays the low eight bits of the values proved from constant, select and phi exports, with an export proved on every kept path. An export the prover cannot bound replays all 256 eight-bit values, one pass per value per triangle (logged once per process). It records instances, then their triangles, then the reference classes of each triangle. One pipeline filters fragments at shader termination against a dynamic push value before late stencil and depth tests. The [Vulkan stencil-reference replacement rule](https://docs.vulkan.org/refpages/latest/refpages/source/FragStencilRefEXT.html) discards bits above the attachment's stencil width; supported stencil attachments have eight bits. Numeric subgroup observations throw except bounded own-lane mask plumbing and group control with kept-lane and derivative-helper coverage proved. WQM inputs must exclude helper contributions, since [non-quad subgroup operations may treat helpers as inactive](https://docs.vulkan.org/glslext/latest/glslext/khr/GL_KHR_shader_subgroup.html). Readonly guarded SRT reads may report internal host faults; these draws wait and check faults before publishing results, and loops throw. Indirect draws, restart, strips, fans, tessellation, mesh, multisampling, early tests, ordered pixel execution, guest shader memory writes, other fault-producing memory access, clocks, primitive ID inputs, attachment feedback and pipeline face/channel splits throw. An exhausted stage push range throws. Equivalence to console ordering has not been measured on console hardware. - [32-bit depth-bits reads of guest memory](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver): a sampled 32_UINT or 32_SINT T# with a depth swizzle mode that no live depth surface matches reads the guest bytes through an R32_SFLOAT image, as the recompiler's depth-bits read expects, and the shader casts the fetched value back to the stored dword. On RX 6700 XT (gfx1031, AMD Windows driver 32.0.21045.5002) the read keeps float denormals, the sign bit alone and quiet and signaling NaN payloads bit for bit. A host that canonicalizes NaNs or flushes denormals on a 32-bit float fetch would change those dwords, and other drivers are not measured. A live depth surface whose extent or base level differs from the T# is not used; the read takes the guest bytes, as the 32-bit float and 16-bit views already do. - [Reused depth storage](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp): a storage view with another extent or a format that cannot hold the cached depth plane uses guest memory. Depth rendering is not written back to those guest bytes; this admission rule does not prove allocation reuse or preserve a depth writer through an incompatible view. A 2D storage view, or a one-layer 2D array view of layer 0, of level 0 with the plane's extent and format (32-bit float or the 32-bit depth-bits layout for the depth plane, 16-bit unorm for D16, 8-bit uint for the stencil plane) is a storage image that is [filled](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) from the depth image before every use and, when the shader writes it, copied back after the work: a whole-plane copy through a staging buffer each way. A dispatch of the 15-word 8x8x1 image clear kernel ([`MatchStorageImageClear`](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp): one colour read from the V# in `user[8..11]` stored to every texel of the T# in `user[0..7]`) that covers the whole plane and stores 0 or 1 through a 16-bit unorm view, 0 or 0xffff through a 16-bit uint view, or 0 or a normal float in (0, 1] through a 32-bit float or integer view leaves out the copy-in and replaces the copy-out by a clear of the depth image to that value; the dispatch still writes the storage image. The colour is read again when the copy-out is replaced, and a changed value keeps the copy-out. Indirect dispatches, partial last groups, dispatches that queue on the draw pipeline, other values and formats, a plane the kernel covers only partly and stencil planes keep both copies. A written view is also left pending like any other storage image, so the plane's content, drawn depth included, reaches guest memory on the next flush of that range. A 2D array view of more than one slice of the depth plane, or of a later slice, is a storage image over the slices from 0 to its last (`LayeredDepthStorage`); only the slices from its base slice to its last are copied: a slice that has a depth surface at the plane's address plus a whole depth slice per layer (`DepthSliceBytes`, as for the depth plane copies below) is copied from that surface before every use and back to it after the work, every bound slice each time, through a staging buffer; the other slices hold guest memory like any storage image. These images are kept per address, extent, slice count, base slice, format and tile mode until the device goes away, outside the storage cache's budget. Such a view needs a layer pitch equal to that slice size, and another view of the same memory (the 2D view of its first slice, a view from another base slice, or a sampled copy) is a separate image, copied in before and out after each use, so views that are written before a flush and share a slice do not merge: the later copy-out wins, so their results are not merged. A matching view still throws when it is a cube, a 2D array of more than one layer of the stencil plane or from a later slice, or another level, when the surface is multisampled, when it names the depth plane of a 16-bit depth with stencil surface the device stores in a wider format, and when it was written after another view of the same plane (another format, tile mode, address or dimension) replaced it, as two such views bound in one dispatch are. Stored depth values outside 0..1 are not clamped, and Vulkan leaves that copy undefined without `VK_EXT_depth_range_unrestricted`. `agc_driver_depth_storage_dispatch` pins these paths; none is measured on the console. From ad52647cd9dda60298f7eaeb2cca3b9521b54ef1 Mon Sep 17 00:00:00 2001 From: ntamas94 Date: Mon, 19 Oct 2026 16:22:18 +0200 Subject: [PATCH 160/269] fix(libSceNet): parse sceNetInetPtonEx IPv4 text as the PS4 does On PS4 firmware 10.71 sceNetInetPtonEx reads IPv4 text the way inet_aton does, not the way sceNetInetPton does: "1.2.3" is 1.2.0.3, "127.1" is 127.0.0.1, hex and octal parts are accepted, a part value wraps at 32 bits ("1.4294967296" is 1.0.0.0) and the text ends at the first whitespace. Two differences from BSD inet_aton were measured: a part starting with "09" is decimal ("0989" is 989), and an octal part containing 8 or 9 fails. The flags argument changed nothing for 0, every single bit up to 0x100, 0x10000, 0x80000000 and -1, so it is ignored instead of throwing. IPv6 still converts as sceNetInetPton does, which matches the console. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceNet/Export.cpp | 58 ++++++++++++++++++++++++++++-- core/libs/tests/GuestSceNet.cpp | 51 +++++++++++++++++++++++--- docs/dev/debt/libSceNet.md | 2 +- 3 files changed, 103 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 945f71079c..caa5ed31d4 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -550,6 +550,46 @@ int wait_for_local_address(int s, const Sock& socket, bool empty) { } #endif +bool parse_ipv4_parts(const char* text, std::uint32_t& address) { + std::array parts{}; + std::size_t count = 0; + std::uint32_t value = 0; + for (;;) { + if (*text < '0' || *text > '9') return false; + std::uint32_t base = 10; + if (*text == '0') { + ++text; + if (*text == 'x' || *text == 'X') { + base = 16; + ++text; + } else if (*text != '9') { + base = 8; + } + } + value = 0; + for (;; ++text) { + const char c = *text; + if (c >= '0' && c <= '9') { + if (base == 8 && c >= '8') return false; + value = value * base + static_cast(c - '0'); + } else if (base == 16 && ((c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) { + value = value * 16 + static_cast((c | 0x20) - 'a' + 10); + } else { + break; + } + } + if (*text != '.') break; + if (count == parts.size() || value > 0xff) return false; + parts[count++] = value; + ++text; + } + if (*text != '\0' && std::strchr(" \t\n\v\f\r", *text) == nullptr) return false; + if (value > (0xffffffffu >> (8 * count))) return false; + for (std::size_t i = 0; i < count; ++i) value |= parts[i] << (24 - 8 * i); + address = value; + return true; +} + } // namespace extern "C" { @@ -1982,9 +2022,21 @@ int APS5_VABI sceNetInitParam(void) { return 0; } -int APS5_VABI sceNetInetPtonEx(int af, const char* src, void* dst, int flags) { - if (flags != 0) throw std::runtime_error("sceNetInetPtonEx: flags " + std::to_string(flags) + " are not supported"); - return sceNetInetPton(af, src, dst); +int APS5_VABI sceNetInetPtonEx(int af, const char* src, void* dst, int) { + if (af != NET_AF_INET) { + return sceNetInetPton(af, src, dst); + } + if (src == nullptr || dst == nullptr) { + return fail(NET_EINVAL); + } + std::uint32_t address = 0; + if (!parse_ipv4_parts(src, address)) { + return 0; + } + const std::array bytes{static_cast(address >> 24), static_cast(address >> 16), + static_cast(address >> 8), static_cast(address)}; + std::memcpy(dst, bytes.data(), bytes.size()); + return 1; } int APS5_VABI sceNetIoctl(void) { diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index 76d5b63109..5200fbbf34 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -743,6 +743,48 @@ static void CheckInetPtonWhitespace() { } } +struct AtonCase { + const char* text; + std::uint32_t address; +}; + +static void CheckInetPtonExParts() { + for (const AtonCase& c : {AtonCase{"0", 0}, AtonCase{"1", 1}, AtonCase{"4294967295", 0xffffffff}, AtonCase{"4294967296", 0}, + AtonCase{"0xffffffff", 0xffffffff}, AtonCase{"0x100000000", 0}, AtonCase{"1.16777215", 0x01ffffff}, + AtonCase{"1.2.65535", 0x0102ffff}, AtonCase{"1.2.3.255", 0x010203ff}, AtonCase{"0x7F.1", 0x7f000001}, + AtonCase{"0X7f.1", 0x7f000001}, AtonCase{"0x", 0}, AtonCase{"0x.1.2.3", 0x00010203}, AtonCase{"0x8.1", 0x08000001}, AtonCase{"0x9", 9}, AtonCase{"1.2.3", 0x01020003}, + AtonCase{"127.1", 0x7f000001}, AtonCase{"010.0.0.1", 0x08000001}, AtonCase{"0377.1", 0xff000001}, + AtonCase{"00000000001.2.3.4", 0x01020304}, AtonCase{"1.2.3.4 x", 0x01020304}, AtonCase{"1.2.3.4\tx", 0x01020304}, + AtonCase{"1.2.3.4\v", 0x01020304}, AtonCase{"1 .2.3.4", 1}, AtonCase{"0.8", 8}, AtonCase{"00.1", 1}, + AtonCase{"09", 9}, AtonCase{"09.1", 0x09000001}, AtonCase{"1.09", 0x01000009}, AtonCase{"0.09", 9}, + AtonCase{"09.09", 0x09000009}, AtonCase{"0919", 919}, AtonCase{"0989", 989}, AtonCase{"0998", 998}, + AtonCase{"1.4294967296", 0x01000000}, AtonCase{"4294967297.1", 0x01000001}, AtonCase{"1.2.3.4294967297", 0x01020301}, + AtonCase{"18446744073709551617", 1}, AtonCase{"0x1ffffffff", 0xffffffff}, AtonCase{"0x100000001.1", 0x01000001}, + AtonCase{"1.0xffffff", 0x01ffffff}, AtonCase{"1.2.0xffff", 0x0102ffff}}) { + std::array address{}; + address.fill(0xaa); + *sceNetErrnoLoc() = 123; + Require(sceNetInetPtonEx(2, c.text, address.data(), 0) == 1 && *sceNetErrnoLoc() == 123); + Require(address[0] == (c.address >> 24) && address[1] == ((c.address >> 16) & 0xff) && address[2] == ((c.address >> 8) & 0xff) && + address[3] == (c.address & 0xff) && address[4] == 0xaa); + } + for (const char* text : {"", "1.16777216", "1.2.65536", "1.2.3.256", "256.1", "0400.1", "0xg.1", "08", "018", "019", "0199", "00919", + "080", "08.1.2.3", "1.08", "0.08", "0919.1", "09x", "1.0x1000000", "1.2.0x10000", "1.2.3.4.5", "1..2.3", + ".1.2.3", "1.2.3.", "1.2.3.4x", " 1.2.3.4", "1.2.3.4\x01", "1.2.3.4\xa0", "+1.2.3.4", "-1.2.3.4"}) { + std::array address{}; + address.fill(0xaa); + *sceNetErrnoLoc() = 123; + Require(sceNetInetPtonEx(2, text, address.data(), 0) == 0 && *sceNetErrnoLoc() == 123); + Require(std::all_of(address.begin(), address.end(), [](std::uint8_t byte) { return byte == 0xaa; })); + } + for (const char* text : {"::1 ", "::1x", " ::1", "1.2.3.4", "::1.2.3", "1:2:3:4:5:6:7:8:9"}) { + std::array address{}; + address.fill(0xaa); + Require(sceNetInetPtonEx(28, text, address.data(), 0) == 0); + Require(std::all_of(address.begin(), address.end(), [](std::uint8_t byte) { return byte == 0xaa; })); + } +} + static void CheckInetPtonEx() { std::array address{}; Require(sceNetInetPtonEx(2, "192.168.1.20", address.data(), 0) == 1); @@ -755,10 +797,10 @@ static void CheckInetPtonEx() { Require(Failed(sceNetInetPtonEx(99, "127.0.0.1", address.data(), 0), 47)); Require(Failed(sceNetInetPtonEx(2, nullptr, address.data(), 0), 22)); Require(Failed(sceNetInetPtonEx(2, "127.0.0.1", nullptr, 0), 22)); - for (const int flags : {1, -1}) { - bool threw = false; - try { sceNetInetPtonEx(2, "127.0.0.1", address.data(), flags); } catch (const std::runtime_error&) { threw = true; } - Require(threw); + for (const int flags : {1, 0x100, -1}) { + address.fill(0); + Require(sceNetInetPtonEx(2, "1.2.3", address.data(), flags) == 1); + Require(address[0] == 1 && address[1] == 2 && address[2] == 0 && address[3] == 3); } } @@ -1217,6 +1259,7 @@ int main() { CheckAddressText(28, "::1"); CheckAddressText(28, "1234:5678:9abc:def0:1234:5678:9abc:def0"); CheckInetPtonWhitespace(); + CheckInetPtonExParts(); CheckInetPtonEx(); CheckUsleep(); CheckSystemTime(); diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index a0947b7725..689d49d01f 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -49,7 +49,7 @@ - [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), which forwards it to `sceNetInetPton` and does not know the flags either. Flags 0 converts as `sceNetInetPton` does; any other flags throw, since their meaning (an IPv6 scope or otherwise) is not known. Unverified on hardware +- [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) - unknown signature - [sceNetMemoryFree](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature From 3b6f6359f76e725ffa311e698000de9dd06cb272 Mon Sep 17 00:00:00 2001 From: XairRrR Date: Fri, 16 Oct 2026 13:13:28 +0200 Subject: [PATCH 161/269] fix(agc): serve storage views of arrays past maxImageArrayLayers from their viewed slices A PS5 array may hold up to 8192 layers; Vulkan devices allow fewer (2048 on NVIDIA). A storage view of such an array created its whole image, with more layers than the device allows: vkCreateImage is then invalid (VUID-VkImageCreateInfo-arrayLayers-02256) and a store into a layer the device did not back faults the GPU. Uncharted: Legacy of Thieves Collection (PPSA05684) views a 16x16 RGBA8 array of up to 7809 layers in slices of a few hundred to 2048 in its orphanage chapter, which lost the device (Xid 109, write invalid) a few minutes in. StorageArrayWindow describes the viewed slices as an array of their own starting at the first viewed slice when the whole array is past the limit; a non-XOR swizzle does not depend on the slice index, so the slices detile alike. Views whose slices alone exceed the limit, XOR swizzles that mix in the slice, and any other image past the limit now throw instead of creating an invalid image. AI-assisted: yes --- .../Graphics/include/TextureTiling.hpp | 1 + .../Graphics/src/ShaderResources.cpp | 4 +- .../libSceAgcDriver/Graphics/src/Texture.cpp | 2 + .../Graphics/src/TextureTiling.cpp | 18 +++++++++ .../libSceAgcDriver/tests/TextureTiling.cpp | 37 +++++++++++++++++++ docs/dev/ARCHITECTURE.md | 1 + 6 files changed, 62 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp index 3cb7cfc14d..d0334a4579 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp @@ -76,6 +76,7 @@ struct SurfaceGeometry { SurfaceGeometry DescribeSurface(const GuestTextureResource& descriptor); bool LevelsFitAllocation(const GuestTextureResource& surface, std::uint32_t levels); GuestTextureResource ViewedArrayWindow(const GuestTextureResource& viewed); +GuestTextureResource StorageArrayWindow(const GuestTextureResource& viewed, std::uint32_t maxArrayLayers); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index ad59adeada..287889dce1 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -1095,7 +1095,9 @@ 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& viewed, 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) { + const auto viewed = StorageArrayWindow(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"); RequireNoMultisampleTarget(viewed.baseAddress, guestBytes != 0 ? static_cast(guestBytes) : 1u, "a storage image access"); if (auto depth = DepthSurfaceStorage(context, words, viewed, mip)) return depth; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index d7a68e3704..3d26daf16e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -335,6 +335,7 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex imageInfo.format = vkFormat; imageInfo.extent = {descriptor.width, descriptor.height, geometry.imageDepth}; imageInfo.mipLevels = descriptor.mipCount; + Require(geometry.imageLayers <= context.limits.maxImageArrayLayers, "a texture array with more layers than maxImageArrayLayers is not implemented"); imageInfo.arrayLayers = geometry.imageLayers; imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; @@ -1390,6 +1391,7 @@ StorageTexture::StorageTexture(const Context& context, TextureDetiler& detiler, imageInfo.format = vkFormat; imageInfo.extent = {descriptor.width, descriptor.height, geometry.imageDepth}; imageInfo.mipLevels = descriptor.mipCount; + Require(geometry.imageLayers <= context.limits.maxImageArrayLayers, "a texture array with more layers than maxImageArrayLayers is not implemented"); imageInfo.arrayLayers = geometry.imageLayers; imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp index 2c5ce048a6..b012b7971f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp @@ -486,6 +486,24 @@ GuestTextureResource ViewedArrayWindow(const GuestTextureResource& viewed) { return window; } +GuestTextureResource StorageArrayWindow(const GuestTextureResource& viewed, std::uint32_t maxArrayLayers) { + if (viewed.depthOrLastArray < maxArrayLayers) return viewed; + Require(viewed.dimension == TextureDimension::k2DArray || viewed.dimension == TextureDimension::k1DArray, "a storage image with more layers than maxImageArrayLayers that is not a 1D or 2D array is not implemented"); + Require(viewed.baseArray <= viewed.depthOrLastArray, "a storage view's first array slice lies past its last"); + Require(viewed.depthOrLastArray - viewed.baseArray < maxArrayLayers, "a storage view of more array slices than maxImageArrayLayers is not implemented"); + if (const auto xorMode = XorSwizzleMode(viewed.tileMode); xorMode != 0u) { + const auto* equation = FindTextureSwizzleEquation(xorMode, BytesPerElement(viewed.format)); + Require(equation != nullptr && std::none_of(equation->bits.begin(), equation->bits.end(), [](std::uint32_t bit) { return (bit & 0xff000000u) != 0; }), "a storage view past maxImageArrayLayers of a swizzle that depends on the slice is not implemented"); + } + const auto geometry = DescribeSurface(viewed); + Require(!geometry.thick && geometry.layers == viewed.depthOrLastArray + 1u, "a storage array past maxImageArrayLayers must be a thin array"); + auto window = viewed; + window.baseAddress = viewed.baseAddress + geometry.GuestLayerOffset(viewed.baseArray); + window.depthOrLastArray = viewed.depthOrLastArray - viewed.baseArray; + window.baseArray = 0; + return window; +} + std::vector ComputeElementMipLayout(TextureTileMode tileMode, std::uint32_t bytesPerElement, std::uint32_t width, std::uint32_t height, std::uint32_t mipCount) { Require(width != 0 && height != 0 && mipCount != 0 && mipCount <= 16u, "invalid surface mip chain"); if (tileMode == TextureTileMode::kLinear) return ComputeLinearMipLayout(bytesPerElement, 1u, 1u, width, height, mipCount); diff --git a/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp b/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp index 629685b1d5..f461cf0914 100644 --- a/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp @@ -440,4 +440,41 @@ void RunTextureTilingTests() { volume.baseArray = 0; Require(ViewedArrayWindow(volume).baseAddress == cube.baseAddress, "a volume keeps its descriptor"); } + + { + GuestTextureResource array{}; + array.baseAddress = 0x10000000ull; + array.width = 16; + array.height = 16; + array.depthOrLastArray = 3200; + array.baseArray = 1536; + array.mipCount = 2; + array.tileMode = TextureTileMode::kStandard64KB; + array.dimension = TextureDimension::k2DArray; + array.format = 56; + const auto geometry = DescribeSurface(array); + const auto window = StorageArrayWindow(array, 2048); + Require(window.baseAddress == array.baseAddress + 1536ull * geometry.layerBytes, "a storage view past maxImageArrayLayers must start at its first viewed slice"); + Require(window.baseArray == 0 && window.depthOrLastArray == 1664, "a storage view past maxImageArrayLayers must hold exactly its viewed slices"); + Require(DescribeSurface(window).layerBytes == geometry.layerBytes && DescribeSurface(window).mips[1].tiledOffset == geometry.mips[1].tiledOffset, "a viewed slice must keep the layout of the whole array's slice"); + + auto fitting = array; + fitting.depthOrLastArray = 2047; + const auto kept = StorageArrayWindow(fitting, 2048); + Require(kept.baseAddress == fitting.baseAddress && kept.baseArray == fitting.baseArray && kept.depthOrLastArray == fitting.depthOrLastArray, "an array within maxImageArrayLayers must keep its descriptor"); + + auto linear = array; + linear.tileMode = TextureTileMode::kLinear; + Require(StorageArrayWindow(linear, 2048).baseAddress == linear.baseAddress + 1536ull * DescribeSurface(linear).layerBytes, "a linear storage view past maxImageArrayLayers must start at its first viewed slice"); + + auto wide = array; + wide.baseArray = 1000; + reject([&] { static_cast(StorageArrayWindow(wide, 2048)); }, "more array slices than maxImageArrayLayers"); + auto xorSwizzle = array; + xorSwizzle.tileMode = TextureTileMode::kR64KBX; + reject([&] { static_cast(StorageArrayWindow(xorSwizzle, 2048)); }, "depends on the slice"); + auto volume = array; + volume.dimension = TextureDimension::k3D; + reject([&] { static_cast(StorageArrayWindow(volume, 2048)); }, "not a 1D or 2D array"); + } } diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index 09f3921608..9741fd4117 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -126,6 +126,7 @@ flowchart LR - A stage that `sceAgcDriverResolve*Abi` cannot prepare for the state the title creates its pipeline with (a compute shader that calls user-data pointers, a descriptor the resource plan rejects) is left to its first use like a stage deferred at registration: `ResolveShaderAbi` and `PrepareGraphicsStages` catch the failure inside their own preparation transaction, so the `ShaderPreparationScope` the title's call runs in still commits, log it once per shader, mark the shader deferred and record the ABI key, so a repeated call prepares nothing; `ResolvePreparedGraphics` skips a deferred stage's rectangles until a draw has prepared it. A draw or dispatch that cannot prepare the stage either fails as before. Driver failures, `ProcessShutdown` and a failure stored by the shader's registration preparation still propagate from the ABI call. - A compute shader whose `s_swappc_b64` calls a user-data pointer cannot be linked at registration, because the callee's address is only known at dispatch. `InvocationFor` in [`ShaderRegistry.cpp`](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) reads each callee from guest memory, appends one copy per call after the caller (`LinkUserDataCalls`) and prepares that linked code, so a different callee gets its own prepared entry. - A draw's `Graphics::Pipeline` comes from `CachedPipeline`. When the device has `VK_EXT_graphics_pipeline_library` with fast linking, `VK_EXT_extended_dynamic_state` and `VK_KHR_dynamic_rendering`, draws render with `vkCmdBeginRenderingKHR` instead of render passes and the pipeline is linked from four libraries (vertex input, pre-rasterization shaders, fragment shader, fragment output) kept per device in [`PipelineLibrary.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp). A library built for one pipeline is reused by the next: another blend, vertex layout or target format builds only that part (the shader libraries name no attachment format), and cull mode, front face, depth and stencil state are dynamic, so changing them only links. The fast link is what the draw waits for; a background thread then links the same libraries with link-time optimization and the pipeline binds that one from the next draw on. Push constants then use a 256-byte block with a 128-byte slot per side: the pre-rasterization stages stack from byte 0, the pixel shader always starts at byte 128 (`StagePushOffset`), so its library does not depend on how much push data its vertex shader uses. Pipelines with per-face depth bias, a separate alpha blend into an 8_8 target with the alternate component order (both draw twice), rect lists, geometry shaders or a stage without a variant id are created whole, still with dynamic rendering; devices without the extensions (or with `APS5_NO_GPL=1`) keep render passes and whole pipelines. +- A storage view of a thin array whose whole surface has more layers than the device's `maxImageArrayLayers` (2048 on NVIDIA; a PS5 array may hold up to 8192) is served from its viewed slices alone: `StorageArrayWindow` in [`TextureTiling.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp) describes them as an array of their own starting at the first viewed slice, which is exact because a non-XOR swizzle does not depend on the slice index. A view whose slices alone exceed the limit, a view of an XOR swizzle that mixes in the slice index, and any other image past the limit throw. - 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). From ae8500deb868b7171d1b6483af8b768c7feed4bb Mon Sep 17 00:00:00 2001 From: XairRrR Date: Fri, 16 Oct 2026 21:44:31 +0200 Subject: [PATCH 162/269] fix(agc): pass 3D storage volumes through StorageArrayWindow unchanged A volume's depthOrLastArray is its depth, bounded by maxImageDimension3D, not by maxImageArrayLayers: a 2049 to 4096 deep volume is a valid image and threw since the window was introduced. Reported by Dextroid17. AI-assisted: yes --- core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp | 2 +- core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp | 5 ++++- docs/dev/ARCHITECTURE.md | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp index b012b7971f..cd4c50c9ed 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp @@ -487,7 +487,7 @@ GuestTextureResource ViewedArrayWindow(const GuestTextureResource& viewed) { } GuestTextureResource StorageArrayWindow(const GuestTextureResource& viewed, std::uint32_t maxArrayLayers) { - if (viewed.depthOrLastArray < maxArrayLayers) return viewed; + if (viewed.depthOrLastArray < maxArrayLayers || viewed.dimension == TextureDimension::k3D) return viewed; Require(viewed.dimension == TextureDimension::k2DArray || viewed.dimension == TextureDimension::k1DArray, "a storage image with more layers than maxImageArrayLayers that is not a 1D or 2D array is not implemented"); Require(viewed.baseArray <= viewed.depthOrLastArray, "a storage view's first array slice lies past its last"); Require(viewed.depthOrLastArray - viewed.baseArray < maxArrayLayers, "a storage view of more array slices than maxImageArrayLayers is not implemented"); diff --git a/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp b/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp index f461cf0914..de470343ef 100644 --- a/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp @@ -475,6 +475,9 @@ void RunTextureTilingTests() { reject([&] { static_cast(StorageArrayWindow(xorSwizzle, 2048)); }, "depends on the slice"); auto volume = array; volume.dimension = TextureDimension::k3D; - reject([&] { static_cast(StorageArrayWindow(volume, 2048)); }, "not a 1D or 2D array"); + Require(StorageArrayWindow(volume, 2048).baseAddress == volume.baseAddress && StorageArrayWindow(volume, 2048).depthOrLastArray == volume.depthOrLastArray, "a volume deeper than maxImageArrayLayers is not an array and keeps its descriptor"); + auto cube = array; + cube.dimension = TextureDimension::kCube; + reject([&] { static_cast(StorageArrayWindow(cube, 2048)); }, "not a 1D or 2D array"); } } diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index 9741fd4117..739521bbf1 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -126,7 +126,7 @@ flowchart LR - A stage that `sceAgcDriverResolve*Abi` cannot prepare for the state the title creates its pipeline with (a compute shader that calls user-data pointers, a descriptor the resource plan rejects) is left to its first use like a stage deferred at registration: `ResolveShaderAbi` and `PrepareGraphicsStages` catch the failure inside their own preparation transaction, so the `ShaderPreparationScope` the title's call runs in still commits, log it once per shader, mark the shader deferred and record the ABI key, so a repeated call prepares nothing; `ResolvePreparedGraphics` skips a deferred stage's rectangles until a draw has prepared it. A draw or dispatch that cannot prepare the stage either fails as before. Driver failures, `ProcessShutdown` and a failure stored by the shader's registration preparation still propagate from the ABI call. - A compute shader whose `s_swappc_b64` calls a user-data pointer cannot be linked at registration, because the callee's address is only known at dispatch. `InvocationFor` in [`ShaderRegistry.cpp`](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) reads each callee from guest memory, appends one copy per call after the caller (`LinkUserDataCalls`) and prepares that linked code, so a different callee gets its own prepared entry. - A draw's `Graphics::Pipeline` comes from `CachedPipeline`. When the device has `VK_EXT_graphics_pipeline_library` with fast linking, `VK_EXT_extended_dynamic_state` and `VK_KHR_dynamic_rendering`, draws render with `vkCmdBeginRenderingKHR` instead of render passes and the pipeline is linked from four libraries (vertex input, pre-rasterization shaders, fragment shader, fragment output) kept per device in [`PipelineLibrary.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp). A library built for one pipeline is reused by the next: another blend, vertex layout or target format builds only that part (the shader libraries name no attachment format), and cull mode, front face, depth and stencil state are dynamic, so changing them only links. The fast link is what the draw waits for; a background thread then links the same libraries with link-time optimization and the pipeline binds that one from the next draw on. Push constants then use a 256-byte block with a 128-byte slot per side: the pre-rasterization stages stack from byte 0, the pixel shader always starts at byte 128 (`StagePushOffset`), so its library does not depend on how much push data its vertex shader uses. Pipelines with per-face depth bias, a separate alpha blend into an 8_8 target with the alternate component order (both draw twice), rect lists, geometry shaders or a stage without a variant id are created whole, still with dynamic rendering; devices without the extensions (or with `APS5_NO_GPL=1`) keep render passes and whole pipelines. -- A storage view of a thin array whose whole surface has more layers than the device's `maxImageArrayLayers` (2048 on NVIDIA; a PS5 array may hold up to 8192) is served from its viewed slices alone: `StorageArrayWindow` in [`TextureTiling.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp) describes them as an array of their own starting at the first viewed slice, which is exact because a non-XOR swizzle does not depend on the slice index. A view whose slices alone exceed the limit, a view of an XOR swizzle that mixes in the slice index, and any other image past the limit throw. +- A storage view of a thin array whose whole surface has more layers than the device's `maxImageArrayLayers` (2048 on NVIDIA; a PS5 array may hold up to 8192) is served from its viewed slices alone: `StorageArrayWindow` in [`TextureTiling.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp) describes them as an array of their own starting at the first viewed slice, which is exact because a non-XOR swizzle does not depend on the slice index. A 3D volume is not an array (its depth is bounded by `maxImageDimension3D`) and passes unchanged. A view whose slices alone exceed the limit, a view of an XOR swizzle that mixes in the slice index, and any other image past the limit throw. - 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). From ed599f525bb947cd786542d4f0a7f9166426e926 Mon Sep 17 00:00:00 2001 From: XairRrR Date: Sat, 17 Oct 2026 19:10:40 +0200 Subject: [PATCH 163/269] fix(agc): serve sampled views of arrays past maxImageArrayLayers from their viewed slices The sampled lookup kept the whole array's layer count: a texture over an array of more than maxImageArrayLayers layers read and snapshotted the whole surface and then threw in the Texture constructor, on every lookup, even when the descriptor viewed a few slices. A menu that samples a 16x16 array of up to 8065 layers through its image tables spent ~55 ms per frame reading those surfaces only to reject them. cachedTexture now applies the window the storage path uses (StorageArrayWindow, renamed ArrayLayerWindow since both paths use it): a view whose slices fit the limit is served as an array of its own starting at its first slice, and a view whose slices alone exceed the limit throws before anything is read. A 2D image is one layer whatever its last array field holds and is no longer refused by the storage path. agc_driver_image_array_window_past_layer_limit samples and loads three slices at 8100-8102 of an 8103-layer 1D array (before: "a texture array with more layers than maxImageArrayLayers is not implemented") and checks that a view of all its layers is refused with the window's error on a device whose limit is lower. AI-assisted: yes --- .../Graphics/include/TextureTiling.hpp | 2 +- .../Graphics/src/ShaderResources.cpp | 6 +- .../Graphics/src/TextureTiling.cpp | 15 +- .../libSceAgcDriver/tests/TextureTiling.cpp | 17 +- .../ImageArrayWindowPastLayerLimit.cpp | 147 ++++++++++++++++++ docs/dev/ARCHITECTURE.md | 2 +- 6 files changed, 171 insertions(+), 18 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/ImageArrayWindowPastLayerLimit.cpp diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp index d0334a4579..5699bf78e1 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp @@ -76,7 +76,7 @@ struct SurfaceGeometry { SurfaceGeometry DescribeSurface(const GuestTextureResource& descriptor); bool LevelsFitAllocation(const GuestTextureResource& surface, std::uint32_t levels); GuestTextureResource ViewedArrayWindow(const GuestTextureResource& viewed); -GuestTextureResource StorageArrayWindow(const GuestTextureResource& viewed, std::uint32_t maxArrayLayers); +GuestTextureResource ArrayLayerWindow(const GuestTextureResource& viewed, std::uint32_t maxArrayLayers); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 287889dce1..a8827c92fc 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -658,7 +658,9 @@ void reportGuestAllocation(const GuestTextureResource& resource, std::uint64_t g } // `guestBytes` is the surface size when the caller described the surface already (0: described here). -std::shared_ptr cachedTexture(const Context& context, std::span words, const GuestTextureResource& resource, VkComponentMapping components, std::uint64_t guestBytes = 0, bool depthCompare = false, VkImageViewType multisampleViewType = VK_IMAGE_VIEW_TYPE_2D) { +std::shared_ptr cachedTexture(const Context& context, std::span words, const GuestTextureResource& requested, VkComponentMapping components, std::uint64_t guestBytes = 0, bool depthCompare = false, VkImageViewType multisampleViewType = VK_IMAGE_VIEW_TYPE_2D) { + const auto resource = ArrayLayerWindow(requested, context.limits.maxImageArrayLayers); + if (resource.baseAddress != requested.baseAddress) guestBytes = 0; if (resource.dimension == TextureDimension::k2DMsaa) { Require(!depthCompare, "native multisampled depth comparison sampling is not implemented"); if (auto depth = DepthSurfaceTexture(context, words, resource, components, multisampleViewType)) { @@ -1096,7 +1098,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) { - const auto viewed = StorageArrayWindow(requested, context.limits.maxImageArrayLayers); + 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"); RequireNoMultisampleTarget(viewed.baseAddress, guestBytes != 0 ? static_cast(guestBytes) : 1u, "a storage image access"); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp index cd4c50c9ed..ff1bed0787 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp @@ -486,17 +486,18 @@ GuestTextureResource ViewedArrayWindow(const GuestTextureResource& viewed) { return window; } -GuestTextureResource StorageArrayWindow(const GuestTextureResource& viewed, std::uint32_t maxArrayLayers) { - if (viewed.depthOrLastArray < maxArrayLayers || viewed.dimension == TextureDimension::k3D) return viewed; - Require(viewed.dimension == TextureDimension::k2DArray || viewed.dimension == TextureDimension::k1DArray, "a storage image with more layers than maxImageArrayLayers that is not a 1D or 2D array is not implemented"); - Require(viewed.baseArray <= viewed.depthOrLastArray, "a storage view's first array slice lies past its last"); - Require(viewed.depthOrLastArray - viewed.baseArray < maxArrayLayers, "a storage view of more array slices than maxImageArrayLayers is not implemented"); +GuestTextureResource ArrayLayerWindow(const GuestTextureResource& viewed, std::uint32_t maxArrayLayers) { + const bool layered = viewed.dimension == TextureDimension::k2DArray || viewed.dimension == TextureDimension::k1DArray || viewed.dimension == TextureDimension::kCube; + if (viewed.depthOrLastArray < maxArrayLayers || !layered) return viewed; + Require(viewed.dimension != TextureDimension::kCube, "an image with more layers than maxImageArrayLayers that is not a 1D or 2D array is not implemented"); + Require(viewed.baseArray <= viewed.depthOrLastArray, "an image view's first array slice lies past its last"); + Require(viewed.depthOrLastArray - viewed.baseArray < maxArrayLayers, "an image view of more array slices than maxImageArrayLayers is not implemented"); if (const auto xorMode = XorSwizzleMode(viewed.tileMode); xorMode != 0u) { const auto* equation = FindTextureSwizzleEquation(xorMode, BytesPerElement(viewed.format)); - Require(equation != nullptr && std::none_of(equation->bits.begin(), equation->bits.end(), [](std::uint32_t bit) { return (bit & 0xff000000u) != 0; }), "a storage view past maxImageArrayLayers of a swizzle that depends on the slice is not implemented"); + Require(equation != nullptr && std::none_of(equation->bits.begin(), equation->bits.end(), [](std::uint32_t bit) { return (bit & 0xff000000u) != 0; }), "an image view past maxImageArrayLayers of a swizzle that depends on the slice is not implemented"); } const auto geometry = DescribeSurface(viewed); - Require(!geometry.thick && geometry.layers == viewed.depthOrLastArray + 1u, "a storage array past maxImageArrayLayers must be a thin array"); + Require(!geometry.thick && geometry.layers == viewed.depthOrLastArray + 1u, "an array past maxImageArrayLayers must be a thin array"); auto window = viewed; window.baseAddress = viewed.baseAddress + geometry.GuestLayerOffset(viewed.baseArray); window.depthOrLastArray = viewed.depthOrLastArray - viewed.baseArray; diff --git a/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp b/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp index de470343ef..cb439c6ffe 100644 --- a/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp @@ -453,31 +453,34 @@ void RunTextureTilingTests() { array.dimension = TextureDimension::k2DArray; array.format = 56; const auto geometry = DescribeSurface(array); - const auto window = StorageArrayWindow(array, 2048); + const auto window = ArrayLayerWindow(array, 2048); Require(window.baseAddress == array.baseAddress + 1536ull * geometry.layerBytes, "a storage view past maxImageArrayLayers must start at its first viewed slice"); Require(window.baseArray == 0 && window.depthOrLastArray == 1664, "a storage view past maxImageArrayLayers must hold exactly its viewed slices"); Require(DescribeSurface(window).layerBytes == geometry.layerBytes && DescribeSurface(window).mips[1].tiledOffset == geometry.mips[1].tiledOffset, "a viewed slice must keep the layout of the whole array's slice"); auto fitting = array; fitting.depthOrLastArray = 2047; - const auto kept = StorageArrayWindow(fitting, 2048); + const auto kept = ArrayLayerWindow(fitting, 2048); Require(kept.baseAddress == fitting.baseAddress && kept.baseArray == fitting.baseArray && kept.depthOrLastArray == fitting.depthOrLastArray, "an array within maxImageArrayLayers must keep its descriptor"); auto linear = array; linear.tileMode = TextureTileMode::kLinear; - Require(StorageArrayWindow(linear, 2048).baseAddress == linear.baseAddress + 1536ull * DescribeSurface(linear).layerBytes, "a linear storage view past maxImageArrayLayers must start at its first viewed slice"); + Require(ArrayLayerWindow(linear, 2048).baseAddress == linear.baseAddress + 1536ull * DescribeSurface(linear).layerBytes, "a linear storage view past maxImageArrayLayers must start at its first viewed slice"); auto wide = array; wide.baseArray = 1000; - reject([&] { static_cast(StorageArrayWindow(wide, 2048)); }, "more array slices than maxImageArrayLayers"); + reject([&] { static_cast(ArrayLayerWindow(wide, 2048)); }, "more array slices than maxImageArrayLayers"); auto xorSwizzle = array; xorSwizzle.tileMode = TextureTileMode::kR64KBX; - reject([&] { static_cast(StorageArrayWindow(xorSwizzle, 2048)); }, "depends on the slice"); + reject([&] { static_cast(ArrayLayerWindow(xorSwizzle, 2048)); }, "depends on the slice"); auto volume = array; volume.dimension = TextureDimension::k3D; - Require(StorageArrayWindow(volume, 2048).baseAddress == volume.baseAddress && StorageArrayWindow(volume, 2048).depthOrLastArray == volume.depthOrLastArray, "a volume deeper than maxImageArrayLayers is not an array and keeps its descriptor"); + Require(ArrayLayerWindow(volume, 2048).baseAddress == volume.baseAddress && ArrayLayerWindow(volume, 2048).depthOrLastArray == volume.depthOrLastArray, "a volume deeper than maxImageArrayLayers is not an array and keeps its descriptor"); + auto flat = array; + flat.dimension = TextureDimension::k2D; + Require(ArrayLayerWindow(flat, 2048).baseAddress == flat.baseAddress && ArrayLayerWindow(flat, 2048).depthOrLastArray == flat.depthOrLastArray, "a 2D image is one layer whatever its last array field holds and keeps its descriptor"); auto cube = array; cube.dimension = TextureDimension::kCube; - reject([&] { static_cast(StorageArrayWindow(cube, 2048)); }, "not a 1D or 2D array"); + reject([&] { static_cast(ArrayLayerWindow(cube, 2048)); }, "not a 1D or 2D array"); } } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageArrayWindowPastLayerLimit.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageArrayWindowPastLayerLimit.cpp new file mode 100644 index 0000000000..0d29ae08e4 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageArrayWindowPastLayerLimit.cpp @@ -0,0 +1,147 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Width = 8; +constexpr std::uint32_t Layers = 3; +constexpr std::uint32_t BaseArray = 8100; +constexpr std::uint32_t LastArray = BaseArray + Layers - 1u; +constexpr std::uint32_t Format8888UNorm = 56; +constexpr std::uint32_t Type1DArray = 12; +constexpr std::uint32_t ClampEdge = 2; + +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; +alignas(65536) std::array Texels{}; + +alignas(256) constexpr std::array Code{ + 0x34020084, 0xe0381000, 0x80000201, 0xbf8c3f70, 0xf09c0f20, 0x00820602, 0xf0001f20, 0x00020a04, + 0x34020085, 0xbf8c3f70, 0xe0781000, 0x80010601, 0xe0781010, 0x80010a01, 0xbf810000, +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::array TextureDescriptor(std::uint32_t baseArray = BaseArray) { + const auto address = static_cast(reinterpret_cast(Texels.data())); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format8888UNorm << 20u) | (((Width - 1u) & 3u) << 30u), + (Width - 1u) >> 2u, + 0xfacu | (Type1DArray << 28u), + LastArray | (baseArray << 16u), 0u, 0u, 0u, + }; +} + +std::array SamplerDescriptor() { + return {ClampEdge | (ClampEdge << 3u) | (ClampEdge << 6u), 0u, 0u, 0u}; +} + +std::array Texel(std::uint32_t x, std::uint32_t guestLayer) { + return {static_cast((guestLayer * Width + x) * 9u + 3u), static_cast(x * 30u + guestLayer % 7u), static_cast(guestLayer), 0xff}; +} + +void FillTexels() { + Texels.fill(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"); + for (std::uint32_t layer = 0; layer <= LastArray; ++layer) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto texel = Texel(x, layer); + std::copy(texel.begin(), texel.end(), Texels.begin() + static_cast(surface.GuestLayerOffset(layer) + mip.tiledOffset + x * 4u)); + } + } +} + +void FillInput() { + constexpr std::array layers{0.0f, 1.0f, 2.0f, 0.4f, 1.6f, 2.4f, -1.0f, 5.0f}; + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + Input[tid * 4u + 0u] = std::bit_cast(-0.2f + 0.045f * static_cast(tid)); + Input[tid * 4u + 1u] = std::bit_cast(layers[tid % layers.size()]); + Input[tid * 4u + 2u] = tid % Width; + Input[tid * 4u + 3u] = (tid / Width) % Layers; + } +} + +void Expect(std::uint32_t tid, const char* name, const float* actual, const std::array& expected) { + for (std::uint32_t component = 0; component < 4u; ++component) { + const float scaled = actual[component] * 255.0f; + Require(std::fabs(scaled - std::round(scaled)) < 1e-3f && std::lround(scaled) == expected[component], std::string(name) + ": thread " + std::to_string(tid) + " component " + std::to_string(component) + " is " + std::to_string(scaled) + "/255, expected " + std::to_string(expected[component]) + "/255"); + } +} + +void Run(AgcDriver::VulkanDevice& device, std::uint32_t baseArray = BaseArray) { + Output.fill(-1.0f); + std::vector userData(20, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(sizeof(Input))); + const auto output = BufferDescriptor(Output.data(), static_cast(sizeof(Output))); + const auto texture = TextureDescriptor(baseArray); + const auto sampler = SamplerDescriptor(); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + std::copy(texture.begin(), texture.end(), userData.begin() + 8); + std::copy(sampler.begin(), sampler.end(), userData.begin() + 16); + const std::span code(Code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const float u = std::bit_cast(Input[tid * 4u + 0u]); + const float layer = std::clamp(std::nearbyint(std::bit_cast(Input[tid * 4u + 1u])), 0.0f, static_cast(Layers - 1u)); + const auto x = static_cast(std::clamp(static_cast(std::floor(u * static_cast(Width))), 0, static_cast(Width) - 1)); + Expect(tid, "image_sample_lz on a window of a 1D array past maxImageArrayLayers", &Output[tid * 8u], Texel(x, baseArray + static_cast(layer))); + Expect(tid, "image_load on a window of a 1D array past maxImageArrayLayers", &Output[tid * 8u + 4u], Texel(Input[tid * 4u + 2u], baseArray + Input[tid * 4u + 3u])); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + FillTexels(); + FillInput(); + Run(*device); + try { + Run(*device, 0); + } catch (const std::exception& error) { + if (std::string(error.what()).find("an image view of more array slices than maxImageArrayLayers") == std::string::npos) throw; + } + std::puts("a window of a 1D array past maxImageArrayLayers was sampled and loaded from its viewed layers"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index 739521bbf1..574244a6da 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -126,7 +126,7 @@ flowchart LR - A stage that `sceAgcDriverResolve*Abi` cannot prepare for the state the title creates its pipeline with (a compute shader that calls user-data pointers, a descriptor the resource plan rejects) is left to its first use like a stage deferred at registration: `ResolveShaderAbi` and `PrepareGraphicsStages` catch the failure inside their own preparation transaction, so the `ShaderPreparationScope` the title's call runs in still commits, log it once per shader, mark the shader deferred and record the ABI key, so a repeated call prepares nothing; `ResolvePreparedGraphics` skips a deferred stage's rectangles until a draw has prepared it. A draw or dispatch that cannot prepare the stage either fails as before. Driver failures, `ProcessShutdown` and a failure stored by the shader's registration preparation still propagate from the ABI call. - A compute shader whose `s_swappc_b64` calls a user-data pointer cannot be linked at registration, because the callee's address is only known at dispatch. `InvocationFor` in [`ShaderRegistry.cpp`](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) reads each callee from guest memory, appends one copy per call after the caller (`LinkUserDataCalls`) and prepares that linked code, so a different callee gets its own prepared entry. - A draw's `Graphics::Pipeline` comes from `CachedPipeline`. When the device has `VK_EXT_graphics_pipeline_library` with fast linking, `VK_EXT_extended_dynamic_state` and `VK_KHR_dynamic_rendering`, draws render with `vkCmdBeginRenderingKHR` instead of render passes and the pipeline is linked from four libraries (vertex input, pre-rasterization shaders, fragment shader, fragment output) kept per device in [`PipelineLibrary.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp). A library built for one pipeline is reused by the next: another blend, vertex layout or target format builds only that part (the shader libraries name no attachment format), and cull mode, front face, depth and stencil state are dynamic, so changing them only links. The fast link is what the draw waits for; a background thread then links the same libraries with link-time optimization and the pipeline binds that one from the next draw on. Push constants then use a 256-byte block with a 128-byte slot per side: the pre-rasterization stages stack from byte 0, the pixel shader always starts at byte 128 (`StagePushOffset`), so its library does not depend on how much push data its vertex shader uses. Pipelines with per-face depth bias, a separate alpha blend into an 8_8 target with the alternate component order (both draw twice), rect lists, geometry shaders or a stage without a variant id are created whole, still with dynamic rendering; devices without the extensions (or with `APS5_NO_GPL=1`) keep render passes and whole pipelines. -- A storage view of a thin array whose whole surface has more layers than the device's `maxImageArrayLayers` (2048 on NVIDIA; a PS5 array may hold up to 8192) is served from its viewed slices alone: `StorageArrayWindow` in [`TextureTiling.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp) describes them as an array of their own starting at the first viewed slice, which is exact because a non-XOR swizzle does not depend on the slice index. A 3D volume is not an array (its depth is bounded by `maxImageDimension3D`) and passes unchanged. A view whose slices alone exceed the limit, a view of an XOR swizzle that mixes in the slice index, and any other image past the limit throw. +- A storage or sampled view of a thin array whose whole surface has more layers than the device's `maxImageArrayLayers` (2048 on NVIDIA; a PS5 array may hold up to 8192) is served from its viewed slices alone: `ArrayLayerWindow` in [`TextureTiling.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp) describes them as an array of their own starting at the first viewed slice, which is exact because a non-XOR swizzle does not depend on the slice index. A 3D volume is not an array (its depth is bounded by `maxImageDimension3D`) and a 2D image is one layer whatever its last array field holds; both pass unchanged. A view whose slices alone exceed the limit, a view of an XOR swizzle that mixes in the slice index, and any other image past the limit throw; a sampled lookup (`cachedTexture`) throws before it reads or snapshots the surface. - 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). From f408eb711ebaceb1e422d7f14a9afc2dee8a21b6 Mon Sep 17 00:00:00 2001 From: XairRrR Date: Mon, 19 Oct 2026 02:19:32 +0200 Subject: [PATCH 164/269] test(agc): give the mock device a maxImageArrayLayers limit Texture and storage images now refuse more array layers than the device's maxImageArrayLayers. The mock context left that limit at 0, so agc_depth_copy_kept_bytes failed creating its 8x8 storage image. Give it the 2048 layers real devices report. AI-assisted: yes --- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index aa15f4523f..08e79c883d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -4810,6 +4810,7 @@ AgcDriver::Graphics::Context mockContext() { context.limits.maxPerStageDescriptorStorageBuffers = 16; context.limits.maxPerStageResources = 128; context.limits.maxDescriptorSetStorageBuffers = 32; + context.limits.maxImageArrayLayers = 2048; return context; } From 881dfc1b16bf68ae789e4082487c0f0be716275e Mon Sep 17 00:00:00 2001 From: thuanquang Date: Tue, 20 Oct 2026 02:47:59 +0700 Subject: [PATCH 165/269] fix(tools): don't label "Unblocks: none ..." pull requests as unblocking a title title_ids took every title ID from the Unblocks field, so a field that starts with "none" and then names a title for context ("none by itself; foundation for PPSA27836 in #6570") got the unblocks title label. A field that starts with "none" now names no title. Co-Authored-By: Claude Opus 5.5 --- tools/pr_labels.py | 3 ++- tools/tests/test_pr_labels.py | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/tools/pr_labels.py b/tools/pr_labels.py index 0ca6f4724b..3d21c560f0 100644 --- a/tools/pr_labels.py +++ b/tools/pr_labels.py @@ -12,6 +12,7 @@ import performance_compare UNBLOCKS = re.compile(r'Unblocks:[ \t]*([^\r\n]*(?:\r?\n[ \t]*[-*][ \t]+(?!\[[ xX]\])[^\r\n]*)*)') TITLE_ID = re.compile(r"(?" @@ -39,7 +40,7 @@ def strip_comments(body): def title_ids(body): match = UNBLOCKS.search(strip_comments(body)) - return set(TITLE_ID.findall(match.group(1))) if match else set() + return set(TITLE_ID.findall(match.group(1))) if match and not NONE.match(match.group(1)) else set() def is_perf(title): diff --git a/tools/tests/test_pr_labels.py b/tools/tests/test_pr_labels.py index 73a0ccd559..ff1b26cf1d 100644 --- a/tools/tests/test_pr_labels.py +++ b/tools/tests/test_pr_labels.py @@ -38,6 +38,9 @@ class TitleIdTests(unittest.TestCase): ("- [x] Unblocks: PPSA01325 #3232\n", {"PPSA01325"}), ("- [ ] Unblocks: **PPSA01325 #3232, CUSA12345 #1**", {"PPSA01325", "CUSA12345"}), ("- [ ] Unblocks: none\nSeen on PPSA01325.\n", set()), + ("- [x] Unblocks: none by itself; foundation for PPSA27836 in #6570\n", set()), + ("- [x] Unblocks: None (PPSA03912 #5146 is already in game; this is a speedup)\n", set()), + ("- [x] Unblocks: PPSA01325 #3232 (none of the others)\n", {"PPSA01325"}), ("- [ ] Unblocks:\n - PPSA01325 #3232\n - PPSA00001\n- [ ] Depends on: none\n", {"PPSA01325", "PPSA00001"}), ("- [ ] Unblocks:\n- [ ] Depends on: PPSA01325\n", set()), ("- [ ] Unblocks: \n", set()), From baa581335f5ab79c84ae79ef377303b200ab655f Mon Sep 17 00:00:00 2001 From: thuanquang Date: Tue, 20 Oct 2026 02:49:18 +0700 Subject: [PATCH 166/269] ci: give the PR label job 30 minutes The first hourly pass over every open pull request after #5976 was cancelled at the 10-minute limit (run 39160516271). Co-Authored-By: Claude Opus 5.5 --- .github/workflows/pr-labels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-labels.yml b/.github/workflows/pr-labels.yml index 6d96cce3f8..15a3e8e3dc 100644 --- a/.github/workflows/pr-labels.yml +++ b/.github/workflows/pr-labels.yml @@ -16,7 +16,7 @@ concurrency: jobs: labels: runs-on: ubuntu-latest - timeout-minutes: 10 + timeout-minutes: 30 permissions: contents: read pull-requests: write From 7991f0130e04493d3e0ac392782636f9602e9918 Mon Sep 17 00:00:00 2001 From: eliasbruno124-dev Date: Mon, 19 Oct 2026 23:05:59 +0200 Subject: [PATCH 167/269] fix(tools): resolve unscoped registry imports from guest modules --- tools/import_audit.py | 4 +-- tools/tests/test_import_audit.py | 44 ++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/tools/import_audit.py b/tools/import_audit.py index 4887d6d288..eb35ea3595 100644 --- a/tools/import_audit.py +++ b/tools/import_audit.py @@ -557,13 +557,13 @@ def audit(imports, exports, stubs, libraries, modules, module_imports=None, modu for record in records.values(): nid, library = record["nid"], record["library"] providers = exports.get(nid, []) - if library is None: + if not library: providers = sorted(set(providers) | set((module_exports or {}).get(nid, []))) resolved_nid = nid if not providers: resolved_nid = nid_names.compute_nid(nid) providers = exports.get(resolved_nid, []) - if library is None: + if not library: providers = sorted(set(providers) | set((module_exports or {}).get(resolved_nid, []))) kind = classify(resolved_nid, library, providers, stubs, modules) record["class"] = kind diff --git a/tools/tests/test_import_audit.py b/tools/tests/test_import_audit.py index 98db7f5c7c..5997280cfe 100644 --- a/tools/tests/test_import_audit.py +++ b/tools/tests/test_import_audit.py @@ -890,6 +890,50 @@ class ImportAuditTests(unittest.TestCase): record, = json.loads(result.read_text())["imports"] self.assertEqual((record["nid"], record["class"], record["providers"]), (nid, "absent", [])) + def test_unscoped_registry_imports_use_guest_module_exports(self): + work = self.work + (work.libs / "libc.prx").write_bytes(exporting(nid_names.compute_nid("strlen"))) + provider = work.modules / "provider.prx.guest.prx" + registry, result = work.root / "game.registry.json", work.root / "audit.json" + names = [MESSAGE_NID + "#export#module", "strlen"] + symbols = [(name, 1, GLOBAL_FUNCTION) for name in names] + entries = [(MESSAGE_NID + "#import#module", ""), (MESSAGE_NID, ""), + (MESSAGE_NAME, ""), ("strlen", "")] + for layout, image in (("sections", build_elf(symbols)), + ("sysv", build_sectionless_elf(symbols)), + ("gnu", build_sectionless_elf(symbols, gnu_hash=True)), + ("pe", build_pe(sorted(names)))): + for unresolved in (False, True): + with self.subTest(layout=layout, unresolved=unresolved): + provider.write_bytes(image) + write_registry(registry, entries + ([("missing_function", "")] if unresolved else [])) + code, _, err = work.run(registry, "--json", str(result)) + self.assertEqual((code, err), (int(unresolved), "")) + data = json.loads(result.read_text()) + self.assertEqual(data["references"], 4 + int(unresolved)) + self.assertEqual(data["unique_by_class"], + {"implemented": 3, "stub": 0, "absent": int(unresolved), "module": 0}) + records = {record["nid"]: record for record in data["imports"]} + for symbol in (MESSAGE_NID, MESSAGE_NAME, "strlen"): + self.assertEqual(records[symbol]["providers"], [provider.name]) + self.assertFalse(records[symbol]["library_mismatch"]) + self.assertEqual(records[MESSAGE_NID]["references"], 2) + self.assertEqual(data["missing_libraries"], {}) + self.assertEqual(data["stops"][0]["nid"], "missing_function" if unresolved else None) + + def test_named_registry_import_does_not_use_unrelated_guest_export(self): + work = self.work + (work.libs / "libSceExpected.prx").write_bytes(exporting()) + (work.modules / "other.prx").write_bytes(exporting(MESSAGE_NID)) + registry, result = work.root / "game.registry.json", work.root / "audit.json" + write_registry(registry, [(MESSAGE_NID, "libSceExpected.prx")]) + code, _, err = work.run(registry, "--json", str(result)) + self.assertEqual((code, err), (1, "")) + data = json.loads(result.read_text()) + record, = data["imports"] + self.assertEqual((record["class"], record["providers"]), ("absent", [])) + self.assertEqual(data["stops"][0]["nid"], MESSAGE_NID) + def test_clean_audit_exits_zero_and_empty_registry_is_empty(self): work = self.work (work.libs / "libSceA.prx").write_bytes(exporting("AAAAAAAAAAA")) From ca76e540db4ac6169c7dd4f09708e9b933d3011c Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Mon, 19 Oct 2026 08:49:17 +0300 Subject: [PATCH 168/269] test(agc): export the front-face bits through a uint target as uint --- .../prx/libSceAgcDriver/tests/execution/FrontFaceAllBits.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/FrontFaceAllBits.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/FrontFaceAllBits.cpp index d3f9b23635..409e71f12c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/FrontFaceAllBits.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/FrontFaceAllBits.cpp @@ -67,7 +67,7 @@ void Draw(AgcDriver::VulkanDevice& device, bool allBits) { pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::FrontFace); pixel.frontFace = true; pixel.frontFaceAllBits = allBits; - pixel.targetOutputMode[0] = 1; + pixel.targetOutputMode[0] = 7; pixel.targetExportMapping.fill(0xe4u); const std::vector pixelUserData(8, 0u); const std::array pixelMemory{{{reinterpret_cast(PixelCode.data()), std::as_bytes(std::span(PixelCode))}}}; @@ -87,6 +87,7 @@ void Draw(AgcDriver::VulkanDevice& device, bool allBits) { AgcDriver::Graphics::State state{}; state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, WaveSize, WaveSize, std::nullopt, std::nullopt}; state.color = {reinterpret_cast(Pixels.data()), {Width, Height}, VK_FORMAT_R32_UINT, sizeof(Pixels), 0xe4u}; + state.color.uintExport = true; state.colors = {state.color}; state.hasColorTarget = true; state.renderExtent = {Width, Height}; From 3bdd430a325ea3c93c3bb394d05fb7077b65e61b Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Mon, 19 Oct 2026 08:49:17 +0300 Subject: [PATCH 169/269] docs(debt): note a float color export into a uint target is accepted --- docs/dev/debt/libSceAgcDriver.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index d3f2268145..4746ca7b5a 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -208,3 +208,4 @@ - [Sampled array windows](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp) (libSceAgcDriver) - a sampled 2D array, 1D array or cube view whose base array slice is past 0 is built from its viewed slices only (`ViewedArrayWindow`), at the guest address of its base slice. The window is taken only when the surface is thin, has no DCC metadata, and, for a 64 KiB XOR swizzle, when the base slice keeps every slice bit of the swizzle equation at zero (R_X 1-byte elements: a base slice that is a multiple of 16). Any other view still builds slices 0 to its last slice. A window and a whole-array view of the same surface are separate images over overlapping memory, kept coherent by the existing overlap rules, at the cost of the memory and uploads of both. Ghost of Yotei (PPSA26344) samples a 256x256 cube array at slices 4299..7232, built at 1467 MiB instead of about 3.8 GB. - [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. From 2f0ed638dc85180b2da06be6e76c88f6aea2082a Mon Sep 17 00:00:00 2001 From: juanpimg Date: Mon, 19 Oct 2026 23:13:23 +0200 Subject: [PATCH 170/269] feat(prx): declare the missing PS5 3.20 system exports AnyPS5 resolves a title's imports by NID, so a title that imports a system function no library provides fails to load with an unresolved import. The PS5 3.20 export inventory lists 193 such exports across 37 PS5 system libraries AnyPS5 implements but does not provide. Declare them as throwing stubs so those titles load and reach their next blocker. Each declaration calls NotImplemented_nid_no_patch and returns 0; none of them implements the function. Declarations with a credible prototype use it and cite the source with a pinned commit, and the rest are declared without parameters, which resolves the import without making an ABI claim. The technical-debt files record the limitation per library. libSceMbus and libSceSystemService are left out because pull request #7821 declares them. libSceAgc is left out because its exports are versioned, so the 3.20 NID names a different symbol from the one AnyPS5 ships. libSceAvPlayer.native is left out because it compiles libSceAvPlayer's Export.cpp. Names and NIDs come from the PS5 3.20 export inventory at https://github.com/DNNDHH/PS5-3.20_Libs/tree/b2a77c09cd71c01f34c790cfbe98c751a13262ad --- core/libs/prx/libSceAcm/Export.cpp | 10 ++++ core/libs/prx/libSceAjm.native/Export.cpp | 25 ++++++++ core/libs/prx/libSceAudiodec/Export.cpp | 15 +++++ .../prx/libSceAutoMounterClient/Export.cpp | 15 +++++ core/libs/prx/libSceAvPlayer/Export.cpp | 11 ++++ core/libs/prx/libSceBluetoothHid/Export.cpp | 45 +++++++++++++++ core/libs/prx/libSceContentExport/Export.cpp | 20 +++++++ core/libs/prx/libSceContentSearch/Export.cpp | 49 ++++++++++++++++ core/libs/prx/libSceDiscMap/Export.cpp | 5 ++ core/libs/prx/libSceErrorDialog/Export.cpp | 10 ++++ core/libs/prx/libSceFiber/Export.cpp | 31 ++++++++++ core/libs/prx/libSceImeDialog/Export.cpp | 25 ++++++++ core/libs/prx/libSceJpegDec/Export.cpp | 6 ++ core/libs/prx/libSceKeyboard/Export.cpp | 30 ++++++++++ core/libs/prx/libSceMouse/Export.cpp | 51 +++++++++++++++++ core/libs/prx/libSceNet/Export.cpp | 35 ++++++++++++ core/libs/prx/libSceNgs2/CMakeLists.txt | 1 + core/libs/prx/libSceNgs2/PendingExports.cpp | 57 +++++++++++++++++++ core/libs/prx/libSceNpAuth/Export.cpp | 20 +++++++ core/libs/prx/libSceNpCommerce/Export.cpp | 5 ++ .../prx/libSceNpEntitlementAccess/Export.cpp | 20 +++++++ core/libs/prx/libSceNpGameIntent/Export.cpp | 50 ++++++++++++++++ .../prx/libSceNpSessionSignaling/Export.cpp | 35 ++++++++++++ core/libs/prx/libSceNpWebApi2/Export.cpp | 50 ++++++++++++++++ core/libs/prx/libSceOpusCeltDec/Export.cpp | 26 +++++++++ core/libs/prx/libSceOpusDec/Export.cpp | 41 +++++++++++++ core/libs/prx/libScePad/Export.cpp | 25 ++++++++ core/libs/prx/libScePlayGo/Export.cpp | 15 +++++ core/libs/prx/libScePngDec/Export.cpp | 6 ++ core/libs/prx/libSceRudp/Export.cpp | 35 ++++++++++++ .../libs/prx/libSceSaveData.native/Export.cpp | 45 +++++++++++++++ core/libs/prx/libSceScreenShot/Export.cpp | 44 ++++++++++++++ core/libs/prx/libSceSysmodule/Export.cpp | 35 ++++++++++++ core/libs/prx/libSceSystemGesture/Export.cpp | 5 ++ core/libs/prx/libSceUsbStorage/Export.cpp | 21 +++++++ core/libs/prx/libSceVideodec/Export.cpp | 10 ++++ core/libs/prx/libSceVrTracker2/Export.cpp | 10 ++++ .../prx/libSceWebBrowserDialog/Export.cpp | 15 +++++ docs/dev/debt/libSceAcm.md | 2 +- docs/dev/debt/libSceAjm.native.md | 2 + docs/dev/debt/libSceAudiodec.md | 1 + docs/dev/debt/libSceAvPlayer.md | 2 + docs/dev/debt/libSceBluetoothHid.md | 2 + docs/dev/debt/libSceContentExport.md | 5 ++ docs/dev/debt/libSceContentSearch.md | 4 ++ docs/dev/debt/libSceDiscMap.md | 1 + docs/dev/debt/libSceErrorDialog.md | 4 ++ docs/dev/debt/libSceFiber.md | 2 + docs/dev/debt/libSceImeDialog.md | 1 + docs/dev/debt/libSceJpegDec.md | 1 + docs/dev/debt/libSceKeyboard.md | 1 + docs/dev/debt/libSceMouse.md | 4 ++ docs/dev/debt/libSceNet.md | 1 + docs/dev/debt/libSceNgs2.md | 6 ++ docs/dev/debt/libSceNpAuth.md | 2 + docs/dev/debt/libSceNpCommerce.md | 1 + docs/dev/debt/libSceNpEntitlementAccess.md | 1 + docs/dev/debt/libSceNpGameIntent.md | 1 + docs/dev/debt/libSceNpSessionSignaling.md | 2 +- docs/dev/debt/libSceNpWebApi2.md | 3 + docs/dev/debt/libSceOpusCeltDec.md | 1 + docs/dev/debt/libSceOpusDec.md | 4 ++ docs/dev/debt/libScePad.md | 1 + docs/dev/debt/libScePlayGo.md | 2 + docs/dev/debt/libScePngDec.md | 1 + docs/dev/debt/libSceRudp.md | 1 + docs/dev/debt/libSceSaveData.native.md | 1 + docs/dev/debt/libSceScreenShot.md | 2 +- docs/dev/debt/libSceSysmodule.md | 1 + docs/dev/debt/libSceSystemGesture.md | 4 ++ docs/dev/debt/libSceUsbStorage.md | 6 ++ docs/dev/debt/libSceVideodec.md | 6 ++ docs/dev/debt/libSceVrTracker2.md | 5 ++ docs/dev/debt/libSceWebBrowserDialog.md | 4 ++ 74 files changed, 1039 insertions(+), 3 deletions(-) create mode 100644 core/libs/prx/libSceNgs2/PendingExports.cpp create mode 100644 docs/dev/debt/libSceNgs2.md create mode 100644 docs/dev/debt/libSceUsbStorage.md create mode 100644 docs/dev/debt/libSceVideodec.md create mode 100644 docs/dev/debt/libSceVrTracker2.md diff --git a/core/libs/prx/libSceAcm/Export.cpp b/core/libs/prx/libSceAcm/Export.cpp index 97dfc19d91..f19d62123f 100644 --- a/core/libs/prx/libSceAcm/Export.cpp +++ b/core/libs/prx/libSceAcm/Export.cpp @@ -66,4 +66,14 @@ int APS5_VABI sceAcmBatchJobPriority(void) { return 0; } +int APS5_VABI sceAcm_ConvReverb_SharedIr() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAcm_IFFT() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceAjm.native/Export.cpp b/core/libs/prx/libSceAjm.native/Export.cpp index e6c8fcbd75..c458568944 100644 --- a/core/libs/prx/libSceAjm.native/Export.cpp +++ b/core/libs/prx/libSceAjm.native/Export.cpp @@ -42,4 +42,29 @@ int APS5_VABI sceAjmDecWVorbisCreateSetupPacket(void) { return 0; } +int APS5_VABI sceAjmBatchGetLog() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAjmInstanceAvailable() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +uint32_t APS5_VABI sceAjmInstanceCodecType(uint32_t instance_id) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAjmInstanceExtend(uint32_t context, uint32_t codec, uint64_t flags, uint32_t instance) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAjmInstanceSwitch(uint32_t context, uint32_t codec, uint64_t flags, uint32_t instance) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceAudiodec/Export.cpp b/core/libs/prx/libSceAudiodec/Export.cpp index fc1a113e4f..017762d18a 100644 --- a/core/libs/prx/libSceAudiodec/Export.cpp +++ b/core/libs/prx/libSceAudiodec/Export.cpp @@ -247,4 +247,19 @@ int32_t APS5_VABI sceAudiodecDecode(int32_t handle, AudiodecCtrl* ctrl) { return 0; } +int APS5_VABI sceAudiodecDecode2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudiodecDecode2WithPriority() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudiodecDecodeWithPriority() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceAutoMounterClient/Export.cpp b/core/libs/prx/libSceAutoMounterClient/Export.cpp index e55d28bc6a..3b3e49adb8 100644 --- a/core/libs/prx/libSceAutoMounterClient/Export.cpp +++ b/core/libs/prx/libSceAutoMounterClient/Export.cpp @@ -18,4 +18,19 @@ int APS5_VABI sceAutoMounterClientInit() { return 0; } +int APS5_VABI sceAutoMounterClientRegisterCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAutoMounterClientTerm() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAutoMounterClientUnregisterCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceAvPlayer/Export.cpp b/core/libs/prx/libSceAvPlayer/Export.cpp index 59ce8e72a8..ae444bc05c 100644 --- a/core/libs/prx/libSceAvPlayer/Export.cpp +++ b/core/libs/prx/libSceAvPlayer/Export.cpp @@ -8,6 +8,7 @@ #include #include "SceTypes.hpp" #include "prx/libSceAvPlayer/include/AvPlayer.hpp" +#include "prx/libc/include/General.hpp" using namespace AvPlayer; @@ -191,5 +192,15 @@ int APS5_VABI sceAvPlayerStreamCount(AvPlayerInternal* h) { return ToPlayer(h)->StreamCount(); } +int APS5_VABI sceAvPlayerPrintf(const char* format, ...) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvPlayerVprintf(uint8_t* format, void* args) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } #pragma GCC visibility pop diff --git a/core/libs/prx/libSceBluetoothHid/Export.cpp b/core/libs/prx/libSceBluetoothHid/Export.cpp index 46e728e4fd..e087939059 100644 --- a/core/libs/prx/libSceBluetoothHid/Export.cpp +++ b/core/libs/prx/libSceBluetoothHid/Export.cpp @@ -49,4 +49,49 @@ int APS5_VABI sceBluetoothHidSetOutputReport() { return 0; } +int APS5_VABI sceBluetoothHidDebugGetVersion() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBluetoothHidDisconnectDevice(std::uint32_t deviceId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBluetoothHidGetDeviceInfo(std::uint32_t deviceId, void* info) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBluetoothHidGetFeatureReport(void* report, std::uint32_t reportId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBluetoothHidGetReportDescriptor(std::uint64_t handle, void* buffer, std::uint64_t length) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBluetoothHidInterruptOutput(std::uint64_t handle, std::uint32_t reportId, void* data, std::uint64_t length) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBluetoothHidParamInitialize(void* param, std::uint64_t value) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBluetoothHidSetFeatureReport(std::uint64_t handle, std::uint32_t reportId, void* data, std::uint64_t length) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBluetoothHidThreadParamInitialize(std::uint32_t* param, std::uint32_t value1, std::uint64_t value2) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceContentExport/Export.cpp b/core/libs/prx/libSceContentExport/Export.cpp index f618daeea1..e0003995fc 100644 --- a/core/libs/prx/libSceContentExport/Export.cpp +++ b/core/libs/prx/libSceContentExport/Export.cpp @@ -91,4 +91,24 @@ int APS5_VABI sceContentExportTerm(void) { return 0; } +int APS5_VABI sceContentExportFromFileWithContentIdList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceContentExportFromFileWithTitleIdList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceContentExportInit(void* init_param) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceContentExportValidateContents() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceContentSearch/Export.cpp b/core/libs/prx/libSceContentSearch/Export.cpp index 9475f46b16..a0a7553444 100644 --- a/core/libs/prx/libSceContentSearch/Export.cpp +++ b/core/libs/prx/libSceContentSearch/Export.cpp @@ -141,4 +141,53 @@ int APS5_VABI sceContentSearchTerm(void) { return 0; } +int APS5_VABI sceContentBinderEnd() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceContentBinderGetNumOfDifference() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceContentBinderInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceContentBinderIsUpdated() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceContentBinderReset() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceContentBinderSearchDifference() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceContentBinderStart() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceContentBinderTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceContentSearchGetApplicationLastUpdateId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceContentSearchGetMyApplicationIndex() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceContentSearchGetNumOfApplication() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceContentSearchSearchApplication() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceDiscMap/Export.cpp b/core/libs/prx/libSceDiscMap/Export.cpp index e1f2638a83..80f2a899dc 100644 --- a/core/libs/prx/libSceDiscMap/Export.cpp +++ b/core/libs/prx/libSceDiscMap/Export.cpp @@ -29,4 +29,9 @@ std::int32_t APS5_VABI sceDiscMapUnknown_ioKMruft1ek(const char* path, std::int6 return SCE_DISC_MAP_ERROR_NO_BITMAP_INFO; } +int APS5_VABI sceDiscMapGetPackageSize(std::int64_t fflags, int* ret1, int* ret2) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceErrorDialog/Export.cpp b/core/libs/prx/libSceErrorDialog/Export.cpp index 17d76ba8e4..4f17a4e50d 100644 --- a/core/libs/prx/libSceErrorDialog/Export.cpp +++ b/core/libs/prx/libSceErrorDialog/Export.cpp @@ -92,4 +92,14 @@ int APS5_VABI sceErrorDialogTerminate(void) { return kErrOk; } +int APS5_VABI sceErrorDialogOpenDetail() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceErrorDialogOpenWithReport() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceFiber/Export.cpp b/core/libs/prx/libSceFiber/Export.cpp index 021ab5269c..8ecd838e42 100644 --- a/core/libs/prx/libSceFiber/Export.cpp +++ b/core/libs/prx/libSceFiber/Export.cpp @@ -565,5 +565,36 @@ int32_t APS5_VABI sceFiberStopContextSizeCheck(void) { return g_contextSizeCheck.compare_exchange_strong(expected, false) ? SCE_OK : SCE_FIBER_ERROR_STATE; } +int APS5_VABI _sceFiberAttachContextAndRun(FiberObject* fiber, void* addr_context, uint64_t size_context, + uint64_t arg_on_run, uint64_t* arg_on_return) { + NotImplemented_nid_no_patch(__func__); + return 0; } +int APS5_VABI _sceFiberAttachContextAndSwitch(FiberObject* fiber, void* addr_context, uint64_t size_context, + uint64_t arg_on_run, uint64_t* arg_on_run_out) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _sceFiberInitializeImpl(FiberObject* fiber, const char* name, GuestFiberEntry entry, + uint64_t arg_on_initialize, void* addr_context, uint64_t size_context, + const FiberOptParam* opt_param, uint32_t build_version) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _sceFiberInitializeWithInternalOptionImpl(FiberObject* fiber, const char* name, GuestFiberEntry entry, + uint64_t arg_on_initialize, void* addr_context, + uint64_t size_context, const FiberOptParam* opt_param, + uint32_t flags, uint32_t build_version) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiberRun(FiberObject* fiber, uint64_t arg_on_run, uint64_t* arg_on_return) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceImeDialog/Export.cpp b/core/libs/prx/libSceImeDialog/Export.cpp index 71df834abb..9eea28ac6d 100644 --- a/core/libs/prx/libSceImeDialog/Export.cpp +++ b/core/libs/prx/libSceImeDialog/Export.cpp @@ -175,4 +175,29 @@ int APS5_VABI sceImeDialogTerm(void) { return 0; } +int APS5_VABI sceImeDialogForTestFunction() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeDialogForceClose() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeDialogGetCurrentStarState(int64_t state) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeDialogInitInternal(Param* param, ExtendedParam* extended) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeDialogSetPanelPosition(int32_t posx, int32_t posy) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceJpegDec/Export.cpp b/core/libs/prx/libSceJpegDec/Export.cpp index 0d92c485bb..6c75331425 100644 --- a/core/libs/prx/libSceJpegDec/Export.cpp +++ b/core/libs/prx/libSceJpegDec/Export.cpp @@ -9,6 +9,7 @@ #include #include "Decoder/Jpeg.hpp" #include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" static_assert(sizeof(JpegDecCreateParam) == 0xC); static_assert(sizeof(JpegDecParseParam) == 0x10); @@ -173,4 +174,9 @@ int32_t APS5_VABI sceJpegDecQueryMemorySize(const JpegDecCreateParam* param) { return static_cast(MEMORY_SIZE); } +int APS5_VABI sceJpegDecDecodeWithInputControl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceKeyboard/Export.cpp b/core/libs/prx/libSceKeyboard/Export.cpp index d94e23040f..d8d408237a 100644 --- a/core/libs/prx/libSceKeyboard/Export.cpp +++ b/core/libs/prx/libSceKeyboard/Export.cpp @@ -98,6 +98,36 @@ int APS5_VABI sceKeyboardSetProcessPrivilege(void) { return 0; } +int APS5_VABI sceDbgKeyboardClose() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDbgKeyboardGetKey2Char() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDbgKeyboardInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDbgKeyboardOpen() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDbgKeyboardRead() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDbgKeyboardReadState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + void KeyboardPublishInput_nid_postfix(const KeyboardInputEvent& event) { Keyboard::Publish(event); } diff --git a/core/libs/prx/libSceMouse/Export.cpp b/core/libs/prx/libSceMouse/Export.cpp index 7278fa33eb..121b9d2e6a 100644 --- a/core/libs/prx/libSceMouse/Export.cpp +++ b/core/libs/prx/libSceMouse/Export.cpp @@ -1,4 +1,5 @@ #include "prx/libSceMouse/include/MouseState.hpp" +#include "prx/libc/include/General.hpp" extern "C" { @@ -30,4 +31,54 @@ bool MouseIsOpen_nid_no_patch() { return Mouse::IsOpen(); } +int APS5_VABI sceMouseConnectPort() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMouseDebugGetDeviceId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMouseDeviceOpen() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMouseDisconnectDevice() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMouseDisconnectPort() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMouseGetDeviceInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMouseMbusInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMouseSetHandType() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMouseSetPointerSpeed() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMouseSetProcessPrivilege() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 945f71079c..725bce08d5 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -2622,4 +2622,39 @@ int APS5_VABI sceNetEmulationGet(void) { return 0; } +int APS5_VABI sceNetBind(int socket, const void* addr, uint32_t addrlen) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetEtherStrton(const char* str, void* address) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +uint32_t APS5_VABI sceNetHtonl(uint32_t host32) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +uint16_t APS5_VABI sceNetHtons(uint16_t host16) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +uint32_t APS5_VABI sceNetNtohl(uint32_t net32) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +uint16_t APS5_VABI sceNetNtohs(uint16_t net16) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceNgs2/CMakeLists.txt b/core/libs/prx/libSceNgs2/CMakeLists.txt index 6d09bbbe29..41b6cfe96a 100644 --- a/core/libs/prx/libSceNgs2/CMakeLists.txt +++ b/core/libs/prx/libSceNgs2/CMakeLists.txt @@ -1,5 +1,6 @@ include(${CMAKE_CURRENT_SOURCE_DIR}/../libSceNgs2.native/Ngs2Library.cmake) add_sce_ngs2_library(libSceNgs2) +target_sources(libSceNgs2 PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/PendingExports.cpp) if(BUILD_TESTING) add_test_executable(ngs2_core_render_tests ${CMAKE_CURRENT_SOURCE_DIR}/../libSceNgs2.native/tests/Ngs2Render.cpp) diff --git a/core/libs/prx/libSceNgs2/PendingExports.cpp b/core/libs/prx/libSceNgs2/PendingExports.cpp new file mode 100644 index 0000000000..992e9b2449 --- /dev/null +++ b/core/libs/prx/libSceNgs2/PendingExports.cpp @@ -0,0 +1,57 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceNgs2CustomRackGetModuleInfo(std::uintptr_t rackHandle, std::uint32_t moduleIndex, void* outInfo, std::size_t infoSize) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNgs2FftInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNgs2FftProcess() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNgs2FftQuerySize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNgs2GetWaveformFrameInfo(void* format, std::uint32_t* outFrameSize, std::uint32_t* outNumFrameSamples, std::uint32_t* outUnitsPerFrame, std::uint32_t* outNumDelaySamples) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNgs2ParseWaveformUser(std::uintptr_t handler, std::uintptr_t userData, void* outInfo) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNgs2RackGetUserData(std::uintptr_t rackHandle, std::uintptr_t* outUserData) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNgs2RackSetUserData(std::uintptr_t rackHandle, std::uintptr_t userData) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNgs2SystemEnumHandles(std::uintptr_t* outHandles, std::uint32_t maxHandles) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNgs2SystemEnumRackHandles(std::uintptr_t systemHandle, std::uintptr_t* outHandles, std::uint32_t maxHandles) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNgs2VoiceGetMatrixInfo(std::uintptr_t voiceHandle, std::uint32_t matrixId, void* outInfo, std::size_t infoSize) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNgs2VoiceGetOwner(std::uintptr_t voiceHandle, std::uintptr_t* outOwnerRack, std::uint32_t* outVoiceIndex) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceNpAuth/Export.cpp b/core/libs/prx/libSceNpAuth/Export.cpp index 86734f9a5a..5c94291dea 100644 --- a/core/libs/prx/libSceNpAuth/Export.cpp +++ b/core/libs/prx/libSceNpAuth/Export.cpp @@ -136,4 +136,24 @@ int APS5_VABI sceNpAuthWaitAsync(int req_id, int* result) { return NP_POLL_ASYNC_FINISHED; } +int APS5_VABI sceNpAuthGetAuthorizationCode(int req_id, const void* param, void* auth_code, int* issuer_id) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpAuthGetAuthorizationCodeA(int req_id, const void* param, void* auth_code, int* issuer_id) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpAuthGetIdToken(int req_id, const void* param, void* token) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpAuthGetIdTokenA(int req_id, const void* param, void* token) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceNpCommerce/Export.cpp b/core/libs/prx/libSceNpCommerce/Export.cpp index 39fecd0fc0..2fd73aeb90 100644 --- a/core/libs/prx/libSceNpCommerce/Export.cpp +++ b/core/libs/prx/libSceNpCommerce/Export.cpp @@ -84,4 +84,9 @@ int APS5_VABI sceNpCommerceDialogOpen2(void) { return 0; } +int APS5_VABI sceNpCommerceDialogInitializeInternal() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceNpEntitlementAccess/Export.cpp b/core/libs/prx/libSceNpEntitlementAccess/Export.cpp index c40dbfd360..ba654ff02f 100644 --- a/core/libs/prx/libSceNpEntitlementAccess/Export.cpp +++ b/core/libs/prx/libSceNpEntitlementAccess/Export.cpp @@ -191,4 +191,24 @@ int APS5_VABI sceNpEntitlementAccessRequestUnifiedEntitlementInfoList() { return SCE_NP_ERROR_SIGNED_OUT; } +int APS5_VABI sceNpEntitlementAccessGetAddcontEntitlementInfoIndividual() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpEntitlementAccessPollConsumableEntitlementInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpEntitlementAccessRequestConsumableEntitlementInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpEntitlementAccessRequestConsumeEntitlement() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceNpGameIntent/Export.cpp b/core/libs/prx/libSceNpGameIntent/Export.cpp index 22fd576fae..cb36cdac0b 100644 --- a/core/libs/prx/libSceNpGameIntent/Export.cpp +++ b/core/libs/prx/libSceNpGameIntent/Export.cpp @@ -73,4 +73,54 @@ int APS5_VABI sceNpGameIntentVshTerminate(void) { return 0; } +int APS5_VABI sceNpAppLauncherInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpAppLauncherLaunchApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpAppLauncherTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpGameIntentGetUdsNpComId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpGameIntentLaunchApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpGameIntentLaunchApp2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpGameIntentNotifyAppLaunched() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSessionServiceGetJoinedPartySession() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSessionServiceGetRunningGamePlayerSession() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSessionServiceSendPlayerSessionInvitations() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceNpSessionSignaling/Export.cpp b/core/libs/prx/libSceNpSessionSignaling/Export.cpp index e75842f866..7dfa95ff73 100644 --- a/core/libs/prx/libSceNpSessionSignaling/Export.cpp +++ b/core/libs/prx/libSceNpSessionSignaling/Export.cpp @@ -109,4 +109,39 @@ int APS5_VABI sceNpSessionSignalingGetConnectionFromNetAddress2() { return 0; } +int APS5_VABI sceNpSessionSignalingCreateJoinActivate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSessionSignalingGetConnectionFromNetAddress() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSessionSignalingGetCustomProperty() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSessionSignalingGetGroupFromPeerAddress() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSessionSignalingInitializeWithPort() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSessionSignalingJoinActivate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSessionSignalingSetCustomProperty() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceNpWebApi2/Export.cpp b/core/libs/prx/libSceNpWebApi2/Export.cpp index 1157934220..df84382d86 100644 --- a/core/libs/prx/libSceNpWebApi2/Export.cpp +++ b/core/libs/prx/libSceNpWebApi2/Export.cpp @@ -204,4 +204,54 @@ int APS5_VABI sceNpWebApi2SetRequestTimeout(int64_t request_id, uint32_t timeout return 0; } +int APS5_VABI sceNpWebApi2AddMultipartPart() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApi2CreateMultipartRequest(int user_ctx_id, const char* api_group, const char* path, const char* method, int64_t* request_id) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApi2InitializeForPresence(int lib_http_ctx_id, uint64_t pool_size) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApi2IntCreateRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApi2IntInitialize(const void* args) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApi2IntInitialize2(const void* args) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApi2IntPushEventCreateCtxIndFilter() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApi2IntPushEventCreateDestPs4CtxIndFilter() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApi2SendMultipartRequest(int64_t request_id, int part_index, void* data, uint64_t data_size, void* resp_info_option) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApi2SetMultipartContentType() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceOpusCeltDec/Export.cpp b/core/libs/prx/libSceOpusCeltDec/Export.cpp index c81bf104bc..2b04cca7be 100644 --- a/core/libs/prx/libSceOpusCeltDec/Export.cpp +++ b/core/libs/prx/libSceOpusCeltDec/Export.cpp @@ -1,6 +1,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libSceOpusDec/include/OpusDecoder.hpp" #include +#include "prx/libc/include/General.hpp" namespace { @@ -26,4 +27,29 @@ int APS5_VABI sceOpusCeltDecDecode(void* state, const std::uint8_t* data, int by int APS5_VABI sceOpusCeltDecDestroy(void* state) { return library.Destroy(state); } +int APS5_VABI sceOpusCeltDecCreate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusCeltDecCtl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusCeltDecDecodeFloat() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusCeltDecDecodeFloatWithPriority() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusCeltDecDecodeWithPriority() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceOpusDec/Export.cpp b/core/libs/prx/libSceOpusDec/Export.cpp index e9ba02c9d5..a02f1d05be 100644 --- a/core/libs/prx/libSceOpusDec/Export.cpp +++ b/core/libs/prx/libSceOpusDec/Export.cpp @@ -1,6 +1,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libSceOpusDec/include/OpusDecoder.hpp" #include +#include "prx/libc/include/General.hpp" namespace { @@ -26,4 +27,44 @@ int APS5_VABI sceOpusDecDecode(void* state, const std::uint8_t* data, int bytes, int APS5_VABI sceOpusDecDestroy(void* state) { return library.Destroy(state); } +int APS5_VABI sceOpusDecCancel() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusDecCreate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusDecCtl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusDecDecodeFloat() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusDecDecodeFloatWithPriority() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusDecDecodeFloatWithPriorityAndTimeout() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusDecDecodeWithPriority() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusDecDecodeWithPriorityAndTimeout() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index 2bd8a89989..903cbccb0b 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -1076,4 +1076,29 @@ int APS5_VABI scePadControllerUpdateTerm(void) { return 0; } +int APS5_VABI scePadClose(int handle) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadGetInfo(void* info) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadOpen(int user_id, int type, int index, const void* param) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadRead(int handle, void* data, int num) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libScePlayGo/Export.cpp b/core/libs/prx/libScePlayGo/Export.cpp index 2bc5bfc67c..ea350d99c8 100644 --- a/core/libs/prx/libScePlayGo/Export.cpp +++ b/core/libs/prx/libScePlayGo/Export.cpp @@ -260,4 +260,19 @@ int APS5_VABI scePlayGoTerminate(void) { return 0; } +int APS5_VABI scePlayGoRequestNextChunk() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayGoSetLanguageMask(std::uint32_t handle, std::uint64_t languageMask) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayGoSnapshot() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libScePngDec/Export.cpp b/core/libs/prx/libScePngDec/Export.cpp index c4fa2c0bcd..419c7966f0 100644 --- a/core/libs/prx/libScePngDec/Export.cpp +++ b/core/libs/prx/libScePngDec/Export.cpp @@ -8,6 +8,7 @@ #include #include "Decoder/Png.hpp" #include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" static_assert(sizeof(PngDecCreateParam) == 0xC); static_assert(sizeof(PngDecParseParam) == 0x10); @@ -194,4 +195,9 @@ int32_t APS5_VABI scePngDecQueryMemorySize(const PngDecCreateParam* param) { return static_cast(MEMORY_SIZE); } +int APS5_VABI scePngDecDecodeWithInputControl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceRudp/Export.cpp b/core/libs/prx/libSceRudp/Export.cpp index a4861da3bd..218968c5de 100644 --- a/core/libs/prx/libSceRudp/Export.cpp +++ b/core/libs/prx/libSceRudp/Export.cpp @@ -313,4 +313,39 @@ int APS5_VABI sceRudpPollDestroy() { return 0; } +int APS5_VABI sceRudpAccept() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRudpFlush() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRudpGetLocalInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRudpInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRudpListen() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRudpPollCancel() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRudpSetMaxSegmentSize() { + 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 b5d52e5013..10e59ebedc 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -994,4 +994,49 @@ int APS5_VABI sceSaveDataPrepareForTransferring() { return 0; } +int APS5_VABI sceSaveDataCheckIpmiIfSize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataDebug() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataDebugCheckBackupData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataDebugCleanMount() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataDebugCompiledSdkVersion() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataDebugCreateSaveDataRoot() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataDebugProspero() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataDebugRemoveSaveDataRoot() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataDebugTarget() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceScreenShot/Export.cpp b/core/libs/prx/libSceScreenShot/Export.cpp index 41e538d9cc..ae99fefcf6 100644 --- a/core/libs/prx/libSceScreenShot/Export.cpp +++ b/core/libs/prx/libSceScreenShot/Export.cpp @@ -13,5 +13,49 @@ int APS5_VABI sceScreenShotSetOverlayImageWithOrigin() { NotImplemented_nid_no_patch(__func__); return 0; } +int APS5_VABI sceScreenShotCapture() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceScreenShotDisable() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceScreenShotDisableNotification() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceScreenShotEnable() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceScreenShotEnableNotification() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceScreenShotGetAppInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceScreenShotGetDrcParam() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceScreenShotIsDisabled() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceScreenShotIsVshScreenCaptureDisabled() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceScreenShotSetDrcParam() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceScreenShotSetParam() { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/core/libs/prx/libSceSysmodule/Export.cpp b/core/libs/prx/libSceSysmodule/Export.cpp index 94f6df2150..2a16d726e8 100644 --- a/core/libs/prx/libSceSysmodule/Export.cpp +++ b/core/libs/prx/libSceSysmodule/Export.cpp @@ -287,4 +287,39 @@ int APS5_VABI sceSysmoduleIsLoadedInternal(std::uint32_t id) { return 0; } +int APS5_VABI sceSysmoduleIsCalledFromSysModule() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSysmoduleIsCameraPreloaded() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSysmoduleLoadModuleByNameInternal(std::uint8_t* fname, std::uint64_t a1, std::uint64_t a2, std::uint64_t a3, std::uint64_t a4, std::uint64_t a5) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSysmoduleMapLibcForLibkernel() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSysmodulePreloadModuleForLibkernel() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSysmoduleUnloadModuleByNameInternal() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSysmoduleUnloadModuleInternalWithArg() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceSystemGesture/Export.cpp b/core/libs/prx/libSceSystemGesture/Export.cpp index 22aad137a6..2c0953dfcb 100644 --- a/core/libs/prx/libSceSystemGesture/Export.cpp +++ b/core/libs/prx/libSceSystemGesture/Export.cpp @@ -197,4 +197,9 @@ int APS5_VABI sceSystemGestureUpdateTouchRecognizerRectangle(int32_t gesture_han return 0; } +int APS5_VABI sceSystemGestureDebugGetVersion() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceUsbStorage/Export.cpp b/core/libs/prx/libSceUsbStorage/Export.cpp index befdd3e768..0704602439 100644 --- a/core/libs/prx/libSceUsbStorage/Export.cpp +++ b/core/libs/prx/libSceUsbStorage/Export.cpp @@ -76,4 +76,25 @@ int APS5_VABI sceUsbStorageGetdentsClose(int32_t fd) { return 0; } +int APS5_VABI sceUsbStorageClearFakeWriteProtection() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUsbStorageRequestMapWSB(uint32_t device_id, const char* subpath, int32_t flags, + int32_t unknown, char* out_mount_path, int32_t* out_status) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUsbStorageSetFakeMapLockForDebug() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUsbStorageSetFakeWriteProtection() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceVideodec/Export.cpp b/core/libs/prx/libSceVideodec/Export.cpp index 8f20f3fc69..ec7fdc8f81 100644 --- a/core/libs/prx/libSceVideodec/Export.cpp +++ b/core/libs/prx/libSceVideodec/Export.cpp @@ -26,4 +26,14 @@ int APS5_VABI sceVideodecReset(void* pCtrlIn) { return 0; } +int APS5_VABI sceVideodecMapMemory() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideodecQueryResourceInfo(const void* pCfgInfoIn, void* pRsrcInfoOut) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceVrTracker2/Export.cpp b/core/libs/prx/libSceVrTracker2/Export.cpp index 9e3639df41..dffcf15b78 100644 --- a/core/libs/prx/libSceVrTracker2/Export.cpp +++ b/core/libs/prx/libSceVrTracker2/Export.cpp @@ -65,4 +65,14 @@ int APS5_VABI sceVrTracker2UnregisterDevice() { return 0; } +int APS5_VABI sceVrTracker2GetControllerImage() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVrTracker2IrGetDebugRawImage() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceWebBrowserDialog/Export.cpp b/core/libs/prx/libSceWebBrowserDialog/Export.cpp index 01bf0684d8..9e1aa3f223 100644 --- a/core/libs/prx/libSceWebBrowserDialog/Export.cpp +++ b/core/libs/prx/libSceWebBrowserDialog/Export.cpp @@ -86,4 +86,19 @@ int APS5_VABI sceWebBrowserDialogResetCookie() { return 0; } +int APS5_VABI sceWebBrowserDialogGetEvent() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceWebBrowserDialogNavigate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceWebBrowserDialogSetZoom() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/docs/dev/debt/libSceAcm.md b/docs/dev/debt/libSceAcm.md index c5c9b90f30..e47e7f1ed3 100644 --- a/docs/dev/debt/libSceAcm.md +++ b/docs/dev/debt/libSceAcm.md @@ -6,5 +6,5 @@ ### Unknown function info +- [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). - diff --git a/docs/dev/debt/libSceAjm.native.md b/docs/dev/debt/libSceAjm.native.md index ca2298edef..0da9974647 100644 --- a/docs/dev/debt/libSceAjm.native.md +++ b/docs/dev/debt/libSceAjm.native.md @@ -2,6 +2,8 @@ ### Unknown function info +- [sceAjmBatchGetLog, sceAjmInstanceAvailable](../../../core/libs/prx/libSceAjm.native/Export.cpp) (libSceAjm.native) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAjm.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim. +- [sceAjmInstanceCodecType, 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. - [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 diff --git a/docs/dev/debt/libSceAudiodec.md b/docs/dev/debt/libSceAudiodec.md index 0e37f5e79f..86527a9841 100644 --- a/docs/dev/debt/libSceAudiodec.md +++ b/docs/dev/debt/libSceAudiodec.md @@ -2,6 +2,7 @@ ### Unknown function info +- [sceAudiodecDecode2, sceAudiodecDecode2WithPriority, sceAudiodecDecodeWithPriority](../../../core/libs/prx/libSceAudiodec/Export.cpp) (libSceAudiodec) - their names and NIDs are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAudiodec.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim - [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) diff --git a/docs/dev/debt/libSceAvPlayer.md b/docs/dev/debt/libSceAvPlayer.md index 148f5db253..46fa54bb75 100644 --- a/docs/dev/debt/libSceAvPlayer.md +++ b/docs/dev/debt/libSceAvPlayer.md @@ -7,6 +7,8 @@ ### Unknown function info +- [sceAvPlayerPrintf](../../../core/libs/prx/libSceAvPlayer/Export.cpp) (libSceAvPlayer) - its name and NID are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAvPlayer.c); the variadic prototype follows [shadPS4 `avplayer.cpp`](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/avplayer/avplayer.cpp#L179), is not measured on a console, and every call throws +- [sceAvPlayerVprintf](../../../core/libs/prx/libSceAvPlayer/Export.cpp) (libSceAvPlayer) - its name and NID are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAvPlayer.c); the `(byte*, void*)` prototype follows [SharpProspero `AvPlayer.cs`](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Media/AvPlayer.cs#L958), is not measured on a console, and every call throws - [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 diff --git a/docs/dev/debt/libSceBluetoothHid.md b/docs/dev/debt/libSceBluetoothHid.md index d5e47f67fa..578457f46d 100644 --- a/docs/dev/debt/libSceBluetoothHid.md +++ b/docs/dev/debt/libSceBluetoothHid.md @@ -6,4 +6,6 @@ ### Unknown function info +- [sceBluetoothHidDebugGetVersion, sceBluetoothHidDisconnectDevice, sceBluetoothHidGetDeviceInfo, sceBluetoothHidGetFeatureReport, sceBluetoothHidGetReportDescriptor, sceBluetoothHidInterruptOutput, sceBluetoothHidParamInitialize, sceBluetoothHidSetFeatureReport and sceBluetoothHidThreadParamInitialize](../../../core/libs/prx/libSceBluetoothHid/Export.cpp) (libSceBluetoothHid) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceBluetoothHid.c); parameter lists follow [SharpProspero's Bluetooth HID bindings](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Bluetooth/SceBluetoothHid.cs#L39-L112), not measured on a console. These exports are throwing declarations to resolve known imports. + - [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 diff --git a/docs/dev/debt/libSceContentExport.md b/docs/dev/debt/libSceContentExport.md index 1c8c0c1285..b70403b94b 100644 --- a/docs/dev/debt/libSceContentExport.md +++ b/docs/dev/debt/libSceContentExport.md @@ -1,5 +1,10 @@ # libSceContentExport technical debt +### Unknown function info + +- [sceContentExportFromFileWithContentIdList, sceContentExportFromFileWithTitleIdList, sceContentExportValidateContents](../../../core/libs/prx/libSceContentExport/Export.cpp) (libSceContentExport) - their names and NIDs are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceContentExport.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim +- [sceContentExportInit](../../../core/libs/prx/libSceContentExport/Export.cpp) (libSceContentExport) - its name and NID are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceContentExport.c); the single mutable parameter pointer follows [shadPS4 `content_export.cpp`](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/content_export/content_export.cpp#L28), is not measured on a console, and every call throws + ### 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 diff --git a/docs/dev/debt/libSceContentSearch.md b/docs/dev/debt/libSceContentSearch.md index 2222020f2e..56c59f62ad 100644 --- a/docs/dev/debt/libSceContentSearch.md +++ b/docs/dev/debt/libSceContentSearch.md @@ -1,5 +1,9 @@ # libSceContentSearch technical debt +### Unknown function info + +- [sceContentBinderEnd, sceContentBinderGetNumOfDifference, sceContentBinderInitialize, sceContentBinderIsUpdated, sceContentBinderReset, sceContentBinderSearchDifference, sceContentBinderStart, sceContentBinderTerminate, sceContentSearchGetApplicationLastUpdateId, sceContentSearchGetMyApplicationIndex, sceContentSearchGetNumOfApplication and sceContentSearchSearchApplication](../../../core/libs/prx/libSceContentSearch/Export.cpp) (libSceContentSearch) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceContentSearch.c), but no credible prototypes were found in the cited sources. They are declared without parameters solely to resolve known imports and make no ABI claim. + ### 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 diff --git a/docs/dev/debt/libSceDiscMap.md b/docs/dev/debt/libSceDiscMap.md index 29814e4aa3..4fcd7700ec 100644 --- a/docs/dev/debt/libSceDiscMap.md +++ b/docs/dev/debt/libSceDiscMap.md @@ -6,4 +6,5 @@ ### 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 diff --git a/docs/dev/debt/libSceErrorDialog.md b/docs/dev/debt/libSceErrorDialog.md index 3c596b30b0..b0beacc876 100644 --- a/docs/dev/debt/libSceErrorDialog.md +++ b/docs/dev/debt/libSceErrorDialog.md @@ -3,3 +3,7 @@ ### Silent stubs - [libSceErrorDialog](../../../core/libs/prx/libSceErrorDialog/Export.cpp) - the error dialog runs the state machine but shows nothing; the error code passed to `sceErrorDialogOpen` is only logged + +### Unknown function info + +- [sceErrorDialogOpenDetail, sceErrorDialogOpenWithReport](../../../core/libs/prx/libSceErrorDialog/Export.cpp) (libSceErrorDialog) - their names and NIDs are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceErrorDialog.c); the no-parameter prototypes follow [shadPS4 `error_dialog.cpp`](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/ime/error_dialog.cpp#L167-L172), are not measured on a console, and every call throws diff --git a/docs/dev/debt/libSceFiber.md b/docs/dev/debt/libSceFiber.md index 2e52af3853..900c12c1a3 100644 --- a/docs/dev/debt/libSceFiber.md +++ b/docs/dev/debt/libSceFiber.md @@ -6,5 +6,7 @@ ### Unknown function info +- [_sceFiberInitializeImpl, _sceFiberInitializeWithInternalOptionImpl](../../../core/libs/prx/libSceFiber/Export.cpp) (libSceFiber) - signatures follow shadPS4's [initialize implementations](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/fiber/fiber.cpp#L150-L153) and [regular initialize](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/fiber/fiber.cpp#L533-L538); their export aliases are mapped at [RegisterLib](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/fiber/fiber.cpp#L548-L551). Not measured on a console; the throwing declarations only resolve imports. +- [_sceFiberAttachContextAndRun, _sceFiberAttachContextAndSwitch, sceFiberRun](../../../core/libs/prx/libSceFiber/Export.cpp) (libSceFiber) - signatures follow shadPS4's [run implementation](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/fiber/fiber.cpp#L260-L261), [switch implementation](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/fiber/fiber.cpp#L336-L337) and [run declaration](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/fiber/fiber.h#L101); export aliases are mapped at [RegisterLib](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/fiber/fiber.cpp#L555-L563). Not measured on a console; the throwing declarations only resolve imports. - [_sceFiberGetThreadFramePointerAddress](../../../core/libs/prx/libSceFiber/Export.cpp) (libSceFiber, NID `0dy4JtMUcMQ`) forwards to `sceFiberGetThreadFramePointerAddress`; that the argument list matches is inferred from the export without the underscore, as for the underscore Ult exports, not measured - [Fiber stacks](../../../core/libs/prx/libSceFiber/Export.cpp) (libSceFiber) - a fiber whose context is smaller than 64 KiB runs on an mmap'd host stack of its context size plus 1 MiB, so host calls made from it (21 KiB deep when PPSA01341 reaches SPIRV-Tools from a 16 KiB context) do not overwrite the contexts packed next to it; a larger context is the stack, as on the console. PPSA05684 needs the second: its job fibers have 64 KiB and 512 KiB contexts, the 512 KiB ones at 1 MiB-aligned addresses, and find their job record by masking the stack pointer (`~0xfffff` for the 512 KiB ones), which a host stack breaks (the game waits forever in GameInit, or faults later in a 64 KiB fiber). The 64 KiB limit is chosen between these two titles, not measured: a title with small contexts that also derives state from the stack pointer, or one whose host calls exceed what is left of a large context, is not covered diff --git a/docs/dev/debt/libSceImeDialog.md b/docs/dev/debt/libSceImeDialog.md index 01003701d8..21da16020b 100644 --- a/docs/dev/debt/libSceImeDialog.md +++ b/docs/dev/debt/libSceImeDialog.md @@ -6,4 +6,5 @@ ### Unknown function info +- [sceImeDialogForTestFunction, sceImeDialogForceClose, sceImeDialogGetCurrentStarState, sceImeDialogInitInternal, sceImeDialogSetPanelPosition](../../../core/libs/prx/libSceImeDialog/Export.cpp) (libSceImeDialog) - their signatures follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/ime/ime_dialog.h#L34-L51), not measured on a console; the throwing declarations only resolve imports. - [sceImeDialogInit, sceImeDialogGetStatus, sceImeDialogGetResult, sceImeDialogAbort, sceImeDialogTerm](../../../core/libs/prx/libSceImeDialog/Export.cpp) (libSceImeDialog) - the statuses (none 0, running 1, finished 2), the end statuses (ok 0, user canceled 1, aborted 2) and the errors of `sceImeDialogInit` for an open dialog (`0x80bc0001`), a null parameter (`0x80bc0031`), a maximum text length outside 1 to 2048 (`0x80bc0016`) and a null text buffer (`0x80bc0017`) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/b2d7ec7d31f9ad9426db93afe079db8679b3e1e0/src/core/libraries/ime/ime_dialog.cpp); none is measured on a PS5. A result, abort or term on a dialog that is not finished, running or open returns `0x80bc0002`, where shadPS4 returns `0x80bc0105` to `0x80bc0107`, and `sceImeDialogTerm` also closes a running dialog, which shadPS4 refuses. The text reaches the title's buffer only when Enter accepts it diff --git a/docs/dev/debt/libSceJpegDec.md b/docs/dev/debt/libSceJpegDec.md index 342b2df171..ded09e70a6 100644 --- a/docs/dev/debt/libSceJpegDec.md +++ b/docs/dev/debt/libSceJpegDec.md @@ -2,6 +2,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 ### Functional diff --git a/docs/dev/debt/libSceKeyboard.md b/docs/dev/debt/libSceKeyboard.md index 40900b0e78..a0a894689e 100644 --- a/docs/dev/debt/libSceKeyboard.md +++ b/docs/dev/debt/libSceKeyboard.md @@ -6,4 +6,5 @@ ### 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 diff --git a/docs/dev/debt/libSceMouse.md b/docs/dev/debt/libSceMouse.md index a26decfa8d..19fe9ab3b5 100644 --- a/docs/dev/debt/libSceMouse.md +++ b/docs/dev/debt/libSceMouse.md @@ -1,5 +1,9 @@ # libSceMouse technical debt +### Unknown function info + +- [sceMouseConnectPort, sceMouseDebugGetDeviceId, sceMouseDeviceOpen, sceMouseDisconnectDevice, sceMouseDisconnectPort, sceMouseGetDeviceInfo, sceMouseMbusInit, sceMouseSetHandType, sceMouseSetPointerSpeed and sceMouseSetProcessPrivilege](../../../core/libs/prx/libSceMouse/Export.cpp) (libSceMouse) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceMouse.c); the no-argument prototypes follow [shadPS4's PS4 stubs](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/mouse/mouse.cpp#L34-L146), not measured on a console. These declarations have no parameters solely to resolve known imports and make no ABI claim. + ### 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 diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index a0947b7725..479bcec5b2 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -6,6 +6,7 @@ ### Unknown function info +- [sceNetBind, sceNetEtherStrton, sceNetHtonl, sceNetHtons, sceNetInit, sceNetNtohl, sceNetNtohs](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - their signatures follow [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 declarations only resolve imports. - [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 diff --git a/docs/dev/debt/libSceNgs2.md b/docs/dev/debt/libSceNgs2.md new file mode 100644 index 0000000000..548dfd5035 --- /dev/null +++ b/docs/dev/debt/libSceNgs2.md @@ -0,0 +1,6 @@ +# libSceNgs2 technical debt + +### Unknown function info + +- [sceNgs2FftInit, sceNgs2FftProcess, sceNgs2FftQuerySize](../../../core/libs/prx/libSceNgs2/PendingExports.cpp) (libSceNgs2) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNgs2.c), but no credible prototypes were found in the cited sources. They are declared without parameters solely to resolve known imports and make no ABI claim. +- [sceNgs2CustomRackGetModuleInfo, sceNgs2GetWaveformFrameInfo, sceNgs2ParseWaveformUser, sceNgs2RackGetUserData, sceNgs2RackSetUserData, sceNgs2SystemEnumHandles, sceNgs2SystemEnumRackHandles, sceNgs2VoiceGetMatrixInfo, sceNgs2VoiceGetOwner](../../../core/libs/prx/libSceNgs2/PendingExports.cpp) (libSceNgs2) - names and NIDs are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNgs2.c); signatures follow [SharpProspero's Ngs2 bindings](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/Ngs2.cs), not measured on a console. Opaque structure pointers use `void*`; these stubs throw for import resolution. diff --git a/docs/dev/debt/libSceNpAuth.md b/docs/dev/debt/libSceNpAuth.md index 8e344dfda7..c7043dc638 100644 --- a/docs/dev/debt/libSceNpAuth.md +++ b/docs/dev/debt/libSceNpAuth.md @@ -2,6 +2,8 @@ ### Unknown function info +- [sceNpAuthGetAuthorizationCode, sceNpAuthGetAuthorizationCodeA](../../../core/libs/prx/libSceNpAuth/Export.cpp) (libSceNpAuth) - their names and NIDs are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpAuth.c); the signatures follow [shadPS4 `np_auth.cpp`](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_auth/np_auth.cpp#L155-L190), are not measured on a console, and every call throws +- [sceNpAuthGetIdToken, sceNpAuthGetIdTokenA](../../../core/libs/prx/libSceNpAuth/Export.cpp) (libSceNpAuth) - their names and NIDs are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpAuth.c); prototypes follow [shadPS4 `np_auth.cpp`](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_auth/np_auth.cpp#L340-L375), are not measured on a console, and every call throws - [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, sceNpAuthGetAuthorizationCodeV3, sceNpAuthGetIdTokenV3, sceNpAuthPollAsync, sceNpAuthWaitAsync](../../../core/libs/prx/libSceNpAuth/Export.cpp) (libSceNpAuth) - request tracking, `ORBIS_NP_AUTH_ERROR_REQUEST_NOT_FOUND` (0x80550306) on unknown request 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 repeated calls on completed requests, and `ORBIS_NP_AUTH_ERROR_ABORTED` (0x80550304) after an abort follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/np/np_auth.cpp) (PS4); async requests return OK and defer `SCE_NP_ERROR_SIGNED_OUT` to `sceNpAuthPollAsync`/`sceNpAuthWaitAsync` while sync requests fail immediately; PlayStation Network is not contacted. The parameter `size` fields are not checked (`ORBIS_NP_AUTH_ERROR_INVALID_SIZE`, structure layouts unknown), the request table has no limit (`ORBIS_NP_AUTH_ERROR_REQUEST_MAX`), and console behavior on PS5 is 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 diff --git a/docs/dev/debt/libSceNpCommerce.md b/docs/dev/debt/libSceNpCommerce.md index 7a9d0ec642..ea9437be65 100644 --- a/docs/dev/debt/libSceNpCommerce.md +++ b/docs/dev/debt/libSceNpCommerce.md @@ -6,4 +6,5 @@ ### 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), is not measured on a console, 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. Call of Duty: Black Ops (PPSA34501) calls it at Zombies > Online diff --git a/docs/dev/debt/libSceNpEntitlementAccess.md b/docs/dev/debt/libSceNpEntitlementAccess.md index b457b050c8..2a9d2f3089 100644 --- a/docs/dev/debt/libSceNpEntitlementAccess.md +++ b/docs/dev/debt/libSceNpEntitlementAccess.md @@ -2,6 +2,7 @@ ### Unknown function info +- [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 diff --git a/docs/dev/debt/libSceNpGameIntent.md b/docs/dev/debt/libSceNpGameIntent.md index 681c6dbf8f..91a214fd5e 100644 --- a/docs/dev/debt/libSceNpGameIntent.md +++ b/docs/dev/debt/libSceNpGameIntent.md @@ -6,5 +6,6 @@ ### Unknown function info +- [sceNpAppLauncherInitialize, sceNpAppLauncherLaunchApp, sceNpAppLauncherTerminate, sceNpGameIntentGetUdsNpComId, sceNpGameIntentLaunchApp, sceNpGameIntentLaunchApp2, sceNpGameIntentNotifyAppLaunched, sceNpSessionServiceGetJoinedPartySession, sceNpSessionServiceGetRunningGamePlayerSession and sceNpSessionServiceSendPlayerSessionInvitations](../../../core/libs/prx/libSceNpGameIntent/Export.cpp) (libSceNpGameIntent) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpGameIntent.c), but no credible prototypes were found in the cited sources. They are declared without parameters solely to resolve known imports and make no ABI claim. - [sceNpGameIntentInitParamInit](../../../core/libs/prx/libSceNpGameIntent/Export.cpp) (libSceNpGameIntent) - struct layout matches [fpPS4](https://github.com/red-prig/fpPS4/blob/master/src/np/ps4_libscenpgameintent.pas#L17) `SceNpGameIntentInitParam`; setting `size`, zeroing reserved bytes, and throwing on null pointer follow Sony SDK `*ParamInit` conventions but are unverified on retail hardware - [sceNpGameIntentInfoInit](../../../core/libs/prx/libSceNpGameIntent/Export.cpp) (libSceNpGameIntent) - struct layout matches [fpPS4](https://github.com/red-prig/fpPS4/blob/master/src/np/ps4_libscenpgameintent.pas#L28) `SceNpGameIntentInfo`; setting `size`, `user_id = -1`, zeroing fields, and throwing on null pointer match `sceNpGameIntentReceiveIntent` defaults but are unverified on retail hardware diff --git a/docs/dev/debt/libSceNpSessionSignaling.md b/docs/dev/debt/libSceNpSessionSignaling.md index c23308b9fe..f5f1396a51 100644 --- a/docs/dev/debt/libSceNpSessionSignaling.md +++ b/docs/dev/debt/libSceNpSessionSignaling.md @@ -7,4 +7,4 @@ - [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. diff --git a/docs/dev/debt/libSceNpWebApi2.md b/docs/dev/debt/libSceNpWebApi2.md index c805a3decc..cccf4a5c69 100644 --- a/docs/dev/debt/libSceNpWebApi2.md +++ b/docs/dev/debt/libSceNpWebApi2.md @@ -6,6 +6,9 @@ ### Unknown function info +- [sceNpWebApi2AddMultipartPart, sceNpWebApi2IntCreateRequest, sceNpWebApi2IntPushEventCreateCtxIndFilter and sceNpWebApi2SetMultipartContentType](../../../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); no-argument prototypes follow [shadPS4's PS4 stubs](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_web_api2/np_web_api2.cpp), not measured on a console. These declarations have no parameters solely to resolve known imports and make no ABI claim. +- [sceNpWebApi2CreateMultipartRequest, sceNpWebApi2InitializeForPresence, sceNpWebApi2IntInitialize, sceNpWebApi2IntInitialize2 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 `sceNpWebApi2Initialize`, and the peak and current use are 0. The error codes are unknown, so a null output or an unknown context throws - [sceNpWebApi2PushEventSetHandleTimeout](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2) - imported by PPSA01317, which fails to load without it. The signature `(int libCtxId, int handleId, uint32_t timeout)` follows `sceNpWebApiSetHandleTimeout` of the PS4 libSceNpWebApi in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/np/np_web_api.cpp) and is unverified on the PS5. An unknown library context throws, as in `sceNpWebApi2GetMemoryPoolStats`, since its error code is unknown. The handle is not checked, as push event handles are not tracked (`sceNpWebApi2PushEventDeleteHandle` accepts any handle), and the timeout is not kept, as nothing waits on a handle offline diff --git a/docs/dev/debt/libSceOpusCeltDec.md b/docs/dev/debt/libSceOpusCeltDec.md index 53d4b848d7..66de7596b2 100644 --- a/docs/dev/debt/libSceOpusCeltDec.md +++ b/docs/dev/debt/libSceOpusCeltDec.md @@ -2,6 +2,7 @@ ### 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 ### Functional diff --git a/docs/dev/debt/libSceOpusDec.md b/docs/dev/debt/libSceOpusDec.md index 9273851ee1..fa6aa1dfd1 100644 --- a/docs/dev/debt/libSceOpusDec.md +++ b/docs/dev/debt/libSceOpusDec.md @@ -1,5 +1,9 @@ # libSceOpusDec technical debt +### Unknown function info + +- [sceOpusDecCancel, sceOpusDecCreate, sceOpusDecCtl, sceOpusDecDecodeFloat, sceOpusDecDecodeFloatWithPriority, sceOpusDecDecodeFloatWithPriorityAndTimeout, sceOpusDecDecodeWithPriority and sceOpusDecDecodeWithPriorityAndTimeout](../../../core/libs/prx/libSceOpusDec/Export.cpp) (libSceOpusDec) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceOpusDec.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim. + ### 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. diff --git a/docs/dev/debt/libScePad.md b/docs/dev/debt/libScePad.md index 7b1ac780bb..04077b9946 100644 --- a/docs/dev/debt/libScePad.md +++ b/docs/dev/debt/libScePad.md @@ -2,6 +2,7 @@ ### Unknown function info +- [scePadClose, scePadGetInfo, scePadInit, scePadOpen, scePadRead](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - their signatures follow [shadPS4's pad declarations](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/pad/pad.h#L271-L320), not measured on a console; the throwing declarations only resolve imports. - [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 diff --git a/docs/dev/debt/libScePlayGo.md b/docs/dev/debt/libScePlayGo.md index ef94e4f939..b314a9c941 100644 --- a/docs/dev/debt/libScePlayGo.md +++ b/docs/dev/debt/libScePlayGo.md @@ -2,5 +2,7 @@ ### Unknown function info +- [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) - its name and NID are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libScePlayGo.c); the `(uint32_t, uint64_t)` prototype follows [ChonkyStation4 `ScePlayGo.cpp`](https://github.com/liuk7071/ChonkyStation4/blob/c101a23ecbc15e56c6e30d5dd24d3ed8880a957a/ChonkyStation4/OS/Libraries/ScePlayGo/ScePlayGo.cpp#L240), is not measured on a console, and every call throws - [libScePlayGo](../../../core/libs/prx/libScePlayGo/Export.cpp) - the chunk set comes from `playgo-chunkdefs.xml` (every listed chunk plus chunks 0 through the default chunk), otherwise from `sce_sys/playgo-chunk.dat`, whose `plgx` header is read as holding the chunk count as a 16-bit value at offset 0x0A and ids 0 to count - 1 are taken as valid. The field was identified on four dumps, where exactly that many 0x20-byte entries follow from offset 0x100 (RIDE 4 PPSA01599: 35, Persona 5 Royal PPSA05110: 25, LEGO Horizon Adventures PPSA14632 and Kena PPSA01802: 1); the rest of the format is not decoded and ids are assumed to be contiguous. Every listed chunk is reported as installed (`LOCAL_FAST`). Without either file, a package with `sce_sys/playgo-scenario.json` (PPSA01748, whose dump lists no chunks) accepts every chunk id as installed while `scePlayGoGetChunkId` lists only chunk 0. A package with no `playgo-*` file in `sce_sys` (PPSA03001, PPSA03490 and PPSA01285 here) accepts chunk 0; querying any other id throws with a message naming the missing `playgo-chunk.dat` and `playgo-scenario.json`, since the incomplete dump cannot establish whether that id exists; `APS5_PLAYGO_UNLISTED_CHUNKS=installed` answers every such id as installed (`LOCAL_FAST`, which PPSA15388 needs to load its saves) and `=absent` answers `SCE_PLAYGO_ERROR_BAD_CHUNK_ID` (which ends PPSA03001's id walk), as the user's choice for that dump. A `sce_sys` that holds other `playgo-*` files (`playgo-ficm.dat`, `playgo-hash-table.dat`) without `playgo-chunk.dat` or `playgo-scenario.json`, or a `playgo-chunk.dat` without a `plgx` header and a nonzero count, throws naming the file, because no such dump is known. These answers are unverified on a console, as is the console's answer for an id outside a package's chunk table. - [libScePlayGo](../../../core/libs/prx/libScePlayGo/Export.cpp) - `scePlayGoGetSupportedOptionalChunk` and `scePlayGoGetOptionalChunk` report every bit set for any `type`, i.e. every optional chunk (language pack) supported and installed, like `scePlayGoGetLanguageMask`. The bit layout and the values of `type` are not known; PPSA01748 calls both with type 0 and hides every text language listed in `playgo-scenario.json` `chunkSupportedLanguages` when the bitmask is 0. The console's answer for a package that ships no optional chunks is unverified. diff --git a/docs/dev/debt/libScePngDec.md b/docs/dev/debt/libScePngDec.md index 5aece02801..6b8253d42a 100644 --- a/docs/dev/debt/libScePngDec.md +++ b/docs/dev/debt/libScePngDec.md @@ -3,3 +3,4 @@ ### 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), not measured on a console. diff --git a/docs/dev/debt/libSceRudp.md b/docs/dev/debt/libSceRudp.md index b161448f8f..d87634044f 100644 --- a/docs/dev/debt/libSceRudp.md +++ b/docs/dev/debt/libSceRudp.md @@ -13,3 +13,4 @@ - [sceRudpGetOption](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) takes `(int ctx_id, int option, void* value, uint32_t len)`, taken from the bundled `Media/Plugins/SceRudp.prx` of PPSA06888: its two exported wrappers pass their first three arguments through unchanged and set `len` to 4, the same shape as its `sceRudpSetOption` call, which passes a 4-byte value. The option codes, the defaults and the error results are unknown, so it still throws - 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 - PPSA31327 imports declared without parameters, signatures unknown: [sceRudpPollCreate, sceRudpPollControl, sceRudpPollWait, sceRudpPollDestroy](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp), imported by Yakuza 0 Director's Cut and not called before the main menu. Names from the NID table of [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/rudp/rudp.cpp) +- [sceRudpAccept, sceRudpFlush, sceRudpGetLocalInfo, sceRudpInit, 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. diff --git a/docs/dev/debt/libSceSaveData.native.md b/docs/dev/debt/libSceSaveData.native.md index ad4a4bf0b4..737ab2b777 100644 --- a/docs/dev/debt/libSceSaveData.native.md +++ b/docs/dev/debt/libSceSaveData.native.md @@ -10,6 +10,7 @@ - [sceSaveDataCancel](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - imported by PPSA23126, which fails to load without it; added without an implementation because its signature is unknown. Every save data call here finishes before it returns, so nothing is ever in flight to cancel, but what the console returns then is unknown - [sceSaveDataDirNameSearch](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - the sort keys (0 directory name, 1 user parameter, 2 blocks, 3 modification time, 5 free blocks) and orders (0 ascending, 1 descending) follow shadPS4, not a hardware run; saves with equal keys keep directory-name order before a descending reverse. A save's size is the `blocks` of the `sceSaveDataMount3` call that created it, kept beside the save in `.blocks`; a save without a kept size takes that of its next mount that names one, and otherwise reports `SAVE_DATA_BLOCKS_MAX`. The search infos (blocks, free blocks and 32 reserved bytes per returned name, following shadPS4's `OrbisSaveDataSearchInfo`), `sceSaveDataGetMountInfo` and the two block keys use that size and, as free, what the save's files leave of it, each file rounded up to 32 KiB blocks; the console's file system overhead is not known and not counted. 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 - [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 +- [sceSaveDataCheckIpmiIfSize, sceSaveDataDebug, sceSaveDataDebugCheckBackupData, sceSaveDataDebugCleanMount, sceSaveDataDebugCompiledSdkVersion, sceSaveDataDebugCreateSaveDataRoot, sceSaveDataDebugProspero, sceSaveDataDebugRemoveSaveDataRoot and sceSaveDataDebugTarget](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSaveData.native.c). The seven no-argument prototypes follow the PS4 stubs in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/save_data/savedata.cpp#L653-L725), not measured on a console; no credible prototypes were found for the other two. All declarations have no parameters solely to resolve known imports and make no ABI claim. ### Functional diff --git a/docs/dev/debt/libSceScreenShot.md b/docs/dev/debt/libSceScreenShot.md index 89867a84f6..46cf3b8867 100644 --- a/docs/dev/debt/libSceScreenShot.md +++ b/docs/dev/debt/libSceScreenShot.md @@ -2,4 +2,4 @@ ### Unknown function info -- [sceScreenShotSetOverlayImage and sceScreenShotSetOverlayImageWithOrigin](../../../core/libs/prx/libSceScreenShot/Export.cpp) (libSceScreenShot) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceScreenShot.c). The PS4 compatibility implementation also exposes the names but declares no parameters; no PS5 prototype was found. The declarations have no parameters solely to resolve known imports and make no ABI claim +- [sceScreenShotCapture, sceScreenShotDisable, sceScreenShotDisableNotification, sceScreenShotEnable, sceScreenShotEnableNotification, sceScreenShotGetAppInfo, sceScreenShotGetDrcParam, sceScreenShotIsDisabled, sceScreenShotIsVshScreenCaptureDisabled, sceScreenShotSetDrcParam and sceScreenShotSetParam, 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 declarations provide no parameters ([shadPS4, pinned commit](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/screenshot/screenshot.h)), and no credible PS5 prototypes were found. These declarations have no parameters solely to resolve known imports and make no ABI claim. diff --git a/docs/dev/debt/libSceSysmodule.md b/docs/dev/debt/libSceSysmodule.md index 5f59554ebf..ba8303c2b2 100644 --- a/docs/dev/debt/libSceSysmodule.md +++ b/docs/dev/debt/libSceSysmodule.md @@ -10,6 +10,7 @@ - [kModuleTable](../../../core/libs/prx/libSceSysmodule/ModuleTable.hpp) (libSceSysmodule) - module IDs registered from decrypted PS5 firmware 1.00–3.20 sysmodule lookup tables: - User-space IDs (0x00000005, 0x0000010a, 0x0000010e, 0x0000011b, 0x00000125, 0x00000126, 0x00000127, 0x00000128, 0x0000012a, 0x0000012b, 0x00000130) and internal IDs (0x8000007b, 0x8000007e, 0x8000007f, 0x80000080, 0x80000082, 0x80000083, 0x80000084, 0x80000085, 0x80000086, 0x80000087, 0x80000088, 0x80000089, 0x8000008e, 0x80000090, 0x80000091, 0x80000092, 0x80000093, 0x80000095, 0x80000097, 0x80000099, 0x8000009b, 0x8000009c, 0x8000009e, 0x8000009f, 0x800000a0, 0x800000a1, 0x800000a2, 0x800000a3, 0x800000a4, 0x800000a6, 0x800000a7, 0x800000a8, 0x800000a9, 0x800000ad, 0x800000ae, 0x800000af, 0x800000b0, 0x800000b2, 0x800000b3, 0x800000b4, 0x800000b5, 0x800000b6, 0x800000b7, 0x800000b9, 0x800000ba, 0x800000bb, 0x800000bc, 0x800000bd, 0x800000be, 0x800000bf, 0x800000c3); whether all mapped PRX modules exist as separate runtime files or are embedded in system daemons on retail consoles is unverified - Dual IDs for `libSceNgs2` (0x0000000b user / 0x80000090 internal) and `libSceHdrScopes` (0x00000109 user / 0x800000bd internal) reflect distinct public and internal sysmodule identifiers defined in PS5 firmware SDKs +- [sceSysmoduleIsCalledFromSysModule, sceSysmoduleIsCameraPreloaded, sceSysmoduleLoadModuleByNameInternal, sceSysmoduleMapLibcForLibkernel, sceSysmodulePreloadModuleForLibkernel, sceSysmoduleUnloadModuleByNameInternal and sceSysmoduleUnloadModuleInternalWithArg](../../../core/libs/prx/libSceSysmodule/Export.cpp) (libSceSysmodule) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSysmodule.c). `sceSysmoduleLoadModuleByNameInternal` takes `(uint8_t* fname, uint64_t a1, uint64_t a2, uint64_t a3, uint64_t a4, uint64_t a5)` from [SharpProspero's Sysmodule bindings](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Sysmodule/Sysmodule.cs#L149-L151), not measured on a console. The other exports had no credible prototypes and are declared without parameters solely to resolve known imports; they make no ABI claim. ### Functional diff --git a/docs/dev/debt/libSceSystemGesture.md b/docs/dev/debt/libSceSystemGesture.md index dadc3f4a07..69f1564847 100644 --- a/docs/dev/debt/libSceSystemGesture.md +++ b/docs/dev/debt/libSceSystemGesture.md @@ -3,3 +3,7 @@ ### Silent stubs - [System gesture recognizers](../../../core/libs/prx/libSceSystemGesture/Export.cpp) (libSceSystemGesture) - `sceSystemGestureGetPrimitiveTouchEvents`, `GetPrimitiveTouchEventsCount`, `GetTouchEvents`, `GetPrimitiveTouchEventByIndex` and the recognizer append, remove, reset and update calls report that no touch input exists, so a title never receives a gesture event. Handle and pointer arguments are still checked across all query entry points including `sceSystemGestureGetTouchEventByIndex`. Error codes follow the 0x80D1 values main already uses in this file; KytyPS5 uses 0x8089 (INVALID_ARGUMENT 0x80890001, INVALID_HANDLE 0x80890006), and which one the console returns is not verified. `sceSystemGestureGetTouchRecognizerInformation` returns the gesture type and rectangle given to `sceSystemGestureCreateTouchRecognizer` or `UpdateTouchRecognizerRectangle`, stored in the title's recognizer buffer in a layout private to this file, and always reports `updated_time` 0 because no recognizer is ever updated with input. It throws for a recognizer that was not created by `sceSystemGestureCreateTouchRecognizer` or has no rectangle, since the console's result in those cases is not known + +### Unknown function info + +- [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 diff --git a/docs/dev/debt/libSceUsbStorage.md b/docs/dev/debt/libSceUsbStorage.md new file mode 100644 index 0000000000..e1bc7fc86b --- /dev/null +++ b/docs/dev/debt/libSceUsbStorage.md @@ -0,0 +1,6 @@ +# libSceUsbStorage technical debt + +### Unknown function info + +- [sceUsbStorageClearFakeWriteProtection, sceUsbStorageSetFakeMapLockForDebug, sceUsbStorageSetFakeWriteProtection](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceUsbStorage.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim. +- [sceUsbStorageRequestMapWSB](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - its prototype follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/usbd/usbstorage.cpp#L245-L246), not measured on a console; the throwing declaration only resolves imports. diff --git a/docs/dev/debt/libSceVideodec.md b/docs/dev/debt/libSceVideodec.md new file mode 100644 index 0000000000..db2c20c9ed --- /dev/null +++ b/docs/dev/debt/libSceVideodec.md @@ -0,0 +1,6 @@ +# libSceVideodec technical debt + +### Unknown function info + +- [sceVideodecMapMemory](../../../core/libs/prx/libSceVideodec/Export.cpp) (libSceVideodec) - its name and NID are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceVideodec.c); the no-parameter prototype follows [shadPS4's videodec.cpp](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/videodec/videodec.cpp#L132), not measured on a console. +- [sceVideodecQueryResourceInfo](../../../core/libs/prx/libSceVideodec/Export.cpp) (libSceVideodec) - its `(const SceVideodecConfigInfo*, SceVideodecResourceInfo*)` prototype follows [ChonkyStation4's SceVideodec.cpp](https://github.com/liuk7071/ChonkyStation4/blob/c101a23ecbc15e56c6e30d5dd24d3ed8880a957a/ChonkyStation4/OS/Libraries/SceVideodec/SceVideodec.cpp#L31), not measured on a console. diff --git a/docs/dev/debt/libSceVrTracker2.md b/docs/dev/debt/libSceVrTracker2.md new file mode 100644 index 0000000000..d745734c6d --- /dev/null +++ b/docs/dev/debt/libSceVrTracker2.md @@ -0,0 +1,5 @@ +# libSceVrTracker2 technical debt + +### Unknown function info + +- [sceVrTracker2GetControllerImage, sceVrTracker2IrGetDebugRawImage](../../../core/libs/prx/libSceVrTracker2/Export.cpp) (libSceVrTracker2) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceVrTracker2.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim. diff --git a/docs/dev/debt/libSceWebBrowserDialog.md b/docs/dev/debt/libSceWebBrowserDialog.md index bc07026490..2986d30e80 100644 --- a/docs/dev/debt/libSceWebBrowserDialog.md +++ b/docs/dev/debt/libSceWebBrowserDialog.md @@ -3,3 +3,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. + +### 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), are not measured on a console, and every call throws From 4e897c120b7ee7ec410a75a17be1444d7a243e66 Mon Sep 17 00:00:00 2001 From: klosixgaming-arch Date: Mon, 19 Oct 2026 16:26:09 -0500 Subject: [PATCH 171/269] fix(libc): name guest assert/break in Windows crash reports Guest SCE_BREAK assertions and debug breaks terminate in int 0x41 (0xcd 0x41), which Windows reports as a read access violation of ~0ull. Name this trap in the crash report alongside int 0x45 (abort). --- .../src/specifics/windows/CrashReport.cpp | 6 +++-- core/libs/tests/WindowsExceptions.cpp | 22 +++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp index 3d685d640a..7dd8affa4c 100644 --- a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp +++ b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp @@ -382,8 +382,10 @@ LONG WINAPI ReportCrash(EXCEPTION_POINTERS* info) { const char* kind = record->ExceptionInformation[0] == 0 ? "read" : record->ExceptionInformation[0] == 1 ? "write" : "execute"; Report(" %s of 0x%016llx\n", kind, static_cast(record->ExceptionInformation[1])); std::uint8_t trap[2]; - if (record->ExceptionInformation[0] == 0 && record->ExceptionInformation[1] == ~0ull && ReadCode(context->Rip, trap, sizeof(trap)) == sizeof(trap) && trap[0] == 0xcd && trap[1] == 0x45) - Report(" guest abort() (int 0x45)\n"); + if (record->ExceptionInformation[0] == 0 && record->ExceptionInformation[1] == ~0ull && ReadCode(context->Rip, trap, sizeof(trap)) == sizeof(trap) && trap[0] == 0xcd) { + if (trap[1] == 0x45) Report(" guest abort() (int 0x45)\n"); + else if (trap[1] == 0x41) Report(" guest assert/break (int 0x41)\n"); + } } Report(" rax %016llx rbx %016llx rcx %016llx rdx %016llx\n", context->Rax, context->Rbx, context->Rcx, context->Rdx); Report(" rsi %016llx rdi %016llx rbp %016llx rsp %016llx\n", context->Rsi, context->Rdi, context->Rbp, context->Rsp); diff --git a/core/libs/tests/WindowsExceptions.cpp b/core/libs/tests/WindowsExceptions.cpp index d7adc8c92e..c83acd49c2 100644 --- a/core/libs/tests/WindowsExceptions.cpp +++ b/core/libs/tests/WindowsExceptions.cpp @@ -151,6 +151,26 @@ static void testGuestAbortReport() { throw std::runtime_error(std::string("guest abort child exited with ") + code + " and printed:\n" + output); } +[[gnu::noinline, noreturn]] static void GuestAssertProbe() { + __asm__ volatile("int $0x41"); + __builtin_unreachable(); +} + +static int RunAssertChild() { + std::fprintf(stderr, "assert probe\n"); + std::fflush(stderr); + GuestAssertProbe(); +} + +static void testGuestAssertReport() { + DWORD exitCode = 0; + const std::string output = RunChild(L"\" guest-assert", exitCode); + char code[16]; + std::snprintf(code, sizeof(code), "0x%08lx", static_cast(exitCode)); + if (exitCode != 0xC0000005 || output.find("guest assert/break (int 0x41)") == std::string::npos) + throw std::runtime_error(std::string("guest assert child exited with ") + code + " and printed:\n" + output); +} + static int destroyed; struct Guard { ~Guard() { ++destroyed; } }; static void ThrowNested() { @@ -299,11 +319,13 @@ int main(int argc, char** argv) { if (argc > 1) { if (std::strcmp(argv[1], "throw-uncaught") == 0) return RunThrowChild(); if (std::strcmp(argv[1], "guest-abort") == 0) return RunAbortChild(); + if (std::strcmp(argv[1], "guest-assert") == 0) return RunAssertChild(); return 100; } TestTypeInfoVtables(); testUnhandledThrowSite(); testGuestAbortReport(); + testGuestAssertReport(); testExceptionPointer(); testFutureException(); testMadeExceptionPointer(); From 148e5024b737cd2f5a4d41a0901bf074555e8a77 Mon Sep 17 00:00:00 2001 From: juanpimg Date: Mon, 19 Oct 2026 23:37:57 +0200 Subject: [PATCH 172/269] feat(prx): drop the exports that main and open pull requests provide The overlap audit against origin/main and the open pull requests found 39 of the 193 declared exports that must not be declared here: - 14 that origin/main already exports. The nid_patcher computes the NID of a symbol from its name, and a name ending in _nid_postfix exports the NID of the base name, so scePadOpen, sceNetInit and the rest are already there. - 23 that an open pull request provides: #6832, #6847, #6852, #7846, #7881, #7889, #7890, #6977 and #7690. - 2 whose technical-debt file an open pull request creates: #7463 and #8094. The scope is now 30 libraries and 154 exports. --- core/libs/prx/libSceAudiodec/Export.cpp | 15 ------- core/libs/prx/libSceAvPlayer/Export.cpp | 11 ----- core/libs/prx/libSceBluetoothHid/Export.cpp | 45 -------------------- core/libs/prx/libSceContentExport/Export.cpp | 20 --------- core/libs/prx/libSceFiber/Export.cpp | 12 ------ core/libs/prx/libSceNet/Export.cpp | 30 ------------- core/libs/prx/libSceNpAuth/Export.cpp | 10 ----- core/libs/prx/libScePad/Export.cpp | 25 ----------- core/libs/prx/libSceRudp/Export.cpp | 5 --- core/libs/prx/libSceSysmodule/Export.cpp | 5 --- core/libs/prx/libSceVideodec/Export.cpp | 10 ----- core/libs/prx/libSceVrTracker2/Export.cpp | 10 ----- docs/dev/debt/libSceAudiodec.md | 1 - docs/dev/debt/libSceAvPlayer.md | 2 - docs/dev/debt/libSceBluetoothHid.md | 2 - docs/dev/debt/libSceContentExport.md | 5 --- docs/dev/debt/libSceFiber.md | 4 +- docs/dev/debt/libSceNet.md | 2 +- docs/dev/debt/libSceNpAuth.md | 4 +- docs/dev/debt/libScePad.md | 1 - docs/dev/debt/libSceRudp.md | 2 +- docs/dev/debt/libSceSysmodule.md | 2 +- docs/dev/debt/libSceVideodec.md | 6 --- docs/dev/debt/libSceVrTracker2.md | 5 --- 24 files changed, 7 insertions(+), 227 deletions(-) delete mode 100644 docs/dev/debt/libSceVideodec.md delete mode 100644 docs/dev/debt/libSceVrTracker2.md diff --git a/core/libs/prx/libSceAudiodec/Export.cpp b/core/libs/prx/libSceAudiodec/Export.cpp index 017762d18a..fc1a113e4f 100644 --- a/core/libs/prx/libSceAudiodec/Export.cpp +++ b/core/libs/prx/libSceAudiodec/Export.cpp @@ -247,19 +247,4 @@ int32_t APS5_VABI sceAudiodecDecode(int32_t handle, AudiodecCtrl* ctrl) { return 0; } -int APS5_VABI sceAudiodecDecode2() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAudiodecDecode2WithPriority() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAudiodecDecodeWithPriority() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - } diff --git a/core/libs/prx/libSceAvPlayer/Export.cpp b/core/libs/prx/libSceAvPlayer/Export.cpp index ae444bc05c..59ce8e72a8 100644 --- a/core/libs/prx/libSceAvPlayer/Export.cpp +++ b/core/libs/prx/libSceAvPlayer/Export.cpp @@ -8,7 +8,6 @@ #include #include "SceTypes.hpp" #include "prx/libSceAvPlayer/include/AvPlayer.hpp" -#include "prx/libc/include/General.hpp" using namespace AvPlayer; @@ -192,15 +191,5 @@ int APS5_VABI sceAvPlayerStreamCount(AvPlayerInternal* h) { return ToPlayer(h)->StreamCount(); } -int APS5_VABI sceAvPlayerPrintf(const char* format, ...) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAvPlayerVprintf(uint8_t* format, void* args) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - } #pragma GCC visibility pop diff --git a/core/libs/prx/libSceBluetoothHid/Export.cpp b/core/libs/prx/libSceBluetoothHid/Export.cpp index e087939059..46e728e4fd 100644 --- a/core/libs/prx/libSceBluetoothHid/Export.cpp +++ b/core/libs/prx/libSceBluetoothHid/Export.cpp @@ -49,49 +49,4 @@ int APS5_VABI sceBluetoothHidSetOutputReport() { return 0; } -int APS5_VABI sceBluetoothHidDebugGetVersion() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceBluetoothHidDisconnectDevice(std::uint32_t deviceId) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceBluetoothHidGetDeviceInfo(std::uint32_t deviceId, void* info) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceBluetoothHidGetFeatureReport(void* report, std::uint32_t reportId) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceBluetoothHidGetReportDescriptor(std::uint64_t handle, void* buffer, std::uint64_t length) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceBluetoothHidInterruptOutput(std::uint64_t handle, std::uint32_t reportId, void* data, std::uint64_t length) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceBluetoothHidParamInitialize(void* param, std::uint64_t value) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceBluetoothHidSetFeatureReport(std::uint64_t handle, std::uint32_t reportId, void* data, std::uint64_t length) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceBluetoothHidThreadParamInitialize(std::uint32_t* param, std::uint32_t value1, std::uint64_t value2) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - } diff --git a/core/libs/prx/libSceContentExport/Export.cpp b/core/libs/prx/libSceContentExport/Export.cpp index e0003995fc..f618daeea1 100644 --- a/core/libs/prx/libSceContentExport/Export.cpp +++ b/core/libs/prx/libSceContentExport/Export.cpp @@ -91,24 +91,4 @@ int APS5_VABI sceContentExportTerm(void) { return 0; } -int APS5_VABI sceContentExportFromFileWithContentIdList() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceContentExportFromFileWithTitleIdList() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceContentExportInit(void* init_param) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceContentExportValidateContents() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - } diff --git a/core/libs/prx/libSceFiber/Export.cpp b/core/libs/prx/libSceFiber/Export.cpp index 8ecd838e42..81aec33202 100644 --- a/core/libs/prx/libSceFiber/Export.cpp +++ b/core/libs/prx/libSceFiber/Export.cpp @@ -577,13 +577,6 @@ int APS5_VABI _sceFiberAttachContextAndSwitch(FiberObject* fiber, void* addr_con return 0; } -int APS5_VABI _sceFiberInitializeImpl(FiberObject* fiber, const char* name, GuestFiberEntry entry, - uint64_t arg_on_initialize, void* addr_context, uint64_t size_context, - const FiberOptParam* opt_param, uint32_t build_version) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI _sceFiberInitializeWithInternalOptionImpl(FiberObject* fiber, const char* name, GuestFiberEntry entry, uint64_t arg_on_initialize, void* addr_context, uint64_t size_context, const FiberOptParam* opt_param, @@ -592,9 +585,4 @@ int APS5_VABI _sceFiberInitializeWithInternalOptionImpl(FiberObject* fiber, cons return 0; } -int APS5_VABI sceFiberRun(FiberObject* fiber, uint64_t arg_on_run, uint64_t* arg_on_return) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - } diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 725bce08d5..88635f3335 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -2622,39 +2622,9 @@ int APS5_VABI sceNetEmulationGet(void) { return 0; } -int APS5_VABI sceNetBind(int socket, const void* addr, uint32_t addrlen) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNetEtherStrton(const char* str, void* address) { NotImplemented_nid_no_patch(__func__); return 0; } -uint32_t APS5_VABI sceNetHtonl(uint32_t host32) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -uint16_t APS5_VABI sceNetHtons(uint16_t host16) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNetInit() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -uint32_t APS5_VABI sceNetNtohl(uint32_t net32) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -uint16_t APS5_VABI sceNetNtohs(uint16_t net16) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - } diff --git a/core/libs/prx/libSceNpAuth/Export.cpp b/core/libs/prx/libSceNpAuth/Export.cpp index 5c94291dea..1085d49538 100644 --- a/core/libs/prx/libSceNpAuth/Export.cpp +++ b/core/libs/prx/libSceNpAuth/Export.cpp @@ -136,21 +136,11 @@ int APS5_VABI sceNpAuthWaitAsync(int req_id, int* result) { return NP_POLL_ASYNC_FINISHED; } -int APS5_VABI sceNpAuthGetAuthorizationCode(int req_id, const void* param, void* auth_code, int* issuer_id) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpAuthGetAuthorizationCodeA(int req_id, const void* param, void* auth_code, int* issuer_id) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNpAuthGetIdToken(int req_id, const void* param, void* token) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpAuthGetIdTokenA(int req_id, const void* param, void* token) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index 903cbccb0b..2bd8a89989 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -1076,29 +1076,4 @@ int APS5_VABI scePadControllerUpdateTerm(void) { return 0; } -int APS5_VABI scePadClose(int handle) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI scePadGetInfo(void* info) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI scePadInit() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI scePadOpen(int user_id, int type, int index, const void* param) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI scePadRead(int handle, void* data, int num) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - } diff --git a/core/libs/prx/libSceRudp/Export.cpp b/core/libs/prx/libSceRudp/Export.cpp index 218968c5de..e1c283e658 100644 --- a/core/libs/prx/libSceRudp/Export.cpp +++ b/core/libs/prx/libSceRudp/Export.cpp @@ -328,11 +328,6 @@ int APS5_VABI sceRudpGetLocalInfo() { return 0; } -int APS5_VABI sceRudpInit() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceRudpListen() { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceSysmodule/Export.cpp b/core/libs/prx/libSceSysmodule/Export.cpp index 2a16d726e8..269f3ec024 100644 --- a/core/libs/prx/libSceSysmodule/Export.cpp +++ b/core/libs/prx/libSceSysmodule/Export.cpp @@ -317,9 +317,4 @@ int APS5_VABI sceSysmoduleUnloadModuleByNameInternal() { return 0; } -int APS5_VABI sceSysmoduleUnloadModuleInternalWithArg() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - } diff --git a/core/libs/prx/libSceVideodec/Export.cpp b/core/libs/prx/libSceVideodec/Export.cpp index ec7fdc8f81..8f20f3fc69 100644 --- a/core/libs/prx/libSceVideodec/Export.cpp +++ b/core/libs/prx/libSceVideodec/Export.cpp @@ -26,14 +26,4 @@ int APS5_VABI sceVideodecReset(void* pCtrlIn) { return 0; } -int APS5_VABI sceVideodecMapMemory() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceVideodecQueryResourceInfo(const void* pCfgInfoIn, void* pRsrcInfoOut) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - } diff --git a/core/libs/prx/libSceVrTracker2/Export.cpp b/core/libs/prx/libSceVrTracker2/Export.cpp index dffcf15b78..9e3639df41 100644 --- a/core/libs/prx/libSceVrTracker2/Export.cpp +++ b/core/libs/prx/libSceVrTracker2/Export.cpp @@ -65,14 +65,4 @@ int APS5_VABI sceVrTracker2UnregisterDevice() { return 0; } -int APS5_VABI sceVrTracker2GetControllerImage() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceVrTracker2IrGetDebugRawImage() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - } diff --git a/docs/dev/debt/libSceAudiodec.md b/docs/dev/debt/libSceAudiodec.md index 86527a9841..0e37f5e79f 100644 --- a/docs/dev/debt/libSceAudiodec.md +++ b/docs/dev/debt/libSceAudiodec.md @@ -2,7 +2,6 @@ ### Unknown function info -- [sceAudiodecDecode2, sceAudiodecDecode2WithPriority, sceAudiodecDecodeWithPriority](../../../core/libs/prx/libSceAudiodec/Export.cpp) (libSceAudiodec) - their names and NIDs are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAudiodec.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim - [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) diff --git a/docs/dev/debt/libSceAvPlayer.md b/docs/dev/debt/libSceAvPlayer.md index 46fa54bb75..148f5db253 100644 --- a/docs/dev/debt/libSceAvPlayer.md +++ b/docs/dev/debt/libSceAvPlayer.md @@ -7,8 +7,6 @@ ### Unknown function info -- [sceAvPlayerPrintf](../../../core/libs/prx/libSceAvPlayer/Export.cpp) (libSceAvPlayer) - its name and NID are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAvPlayer.c); the variadic prototype follows [shadPS4 `avplayer.cpp`](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/avplayer/avplayer.cpp#L179), is not measured on a console, and every call throws -- [sceAvPlayerVprintf](../../../core/libs/prx/libSceAvPlayer/Export.cpp) (libSceAvPlayer) - its name and NID are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAvPlayer.c); the `(byte*, void*)` prototype follows [SharpProspero `AvPlayer.cs`](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Media/AvPlayer.cs#L958), is not measured on a console, and every call throws - [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 diff --git a/docs/dev/debt/libSceBluetoothHid.md b/docs/dev/debt/libSceBluetoothHid.md index 578457f46d..d5e47f67fa 100644 --- a/docs/dev/debt/libSceBluetoothHid.md +++ b/docs/dev/debt/libSceBluetoothHid.md @@ -6,6 +6,4 @@ ### Unknown function info -- [sceBluetoothHidDebugGetVersion, sceBluetoothHidDisconnectDevice, sceBluetoothHidGetDeviceInfo, sceBluetoothHidGetFeatureReport, sceBluetoothHidGetReportDescriptor, sceBluetoothHidInterruptOutput, sceBluetoothHidParamInitialize, sceBluetoothHidSetFeatureReport and sceBluetoothHidThreadParamInitialize](../../../core/libs/prx/libSceBluetoothHid/Export.cpp) (libSceBluetoothHid) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceBluetoothHid.c); parameter lists follow [SharpProspero's Bluetooth HID bindings](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Bluetooth/SceBluetoothHid.cs#L39-L112), not measured on a console. These exports are throwing declarations to resolve known imports. - - [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 diff --git a/docs/dev/debt/libSceContentExport.md b/docs/dev/debt/libSceContentExport.md index b70403b94b..1c8c0c1285 100644 --- a/docs/dev/debt/libSceContentExport.md +++ b/docs/dev/debt/libSceContentExport.md @@ -1,10 +1,5 @@ # libSceContentExport technical debt -### Unknown function info - -- [sceContentExportFromFileWithContentIdList, sceContentExportFromFileWithTitleIdList, sceContentExportValidateContents](../../../core/libs/prx/libSceContentExport/Export.cpp) (libSceContentExport) - their names and NIDs are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceContentExport.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim -- [sceContentExportInit](../../../core/libs/prx/libSceContentExport/Export.cpp) (libSceContentExport) - its name and NID are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceContentExport.c); the single mutable parameter pointer follows [shadPS4 `content_export.cpp`](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/content_export/content_export.cpp#L28), is not measured on a console, and every call throws - ### 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 diff --git a/docs/dev/debt/libSceFiber.md b/docs/dev/debt/libSceFiber.md index 900c12c1a3..9ad010851f 100644 --- a/docs/dev/debt/libSceFiber.md +++ b/docs/dev/debt/libSceFiber.md @@ -6,7 +6,7 @@ ### Unknown function info -- [_sceFiberInitializeImpl, _sceFiberInitializeWithInternalOptionImpl](../../../core/libs/prx/libSceFiber/Export.cpp) (libSceFiber) - signatures follow shadPS4's [initialize implementations](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/fiber/fiber.cpp#L150-L153) and [regular initialize](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/fiber/fiber.cpp#L533-L538); their export aliases are mapped at [RegisterLib](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/fiber/fiber.cpp#L548-L551). Not measured on a console; the throwing declarations only resolve imports. -- [_sceFiberAttachContextAndRun, _sceFiberAttachContextAndSwitch, sceFiberRun](../../../core/libs/prx/libSceFiber/Export.cpp) (libSceFiber) - signatures follow shadPS4's [run implementation](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/fiber/fiber.cpp#L260-L261), [switch implementation](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/fiber/fiber.cpp#L336-L337) and [run declaration](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/fiber/fiber.h#L101); export aliases are mapped at [RegisterLib](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/fiber/fiber.cpp#L555-L563). Not measured on a console; the throwing declarations only resolve imports. +- [_sceFiberInitializeWithInternalOptionImpl](../../../core/libs/prx/libSceFiber/Export.cpp) (libSceFiber) - its signature follows shadPS4's [initialize implementations](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/fiber/fiber.cpp#L150-L153); its export alias is mapped at [RegisterLib](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/fiber/fiber.cpp#L548-L551). Not measured on a console; the throwing declaration only resolves the import. +- [_sceFiberAttachContextAndRun, _sceFiberAttachContextAndSwitch](../../../core/libs/prx/libSceFiber/Export.cpp) (libSceFiber) - signatures follow shadPS4's [run implementation](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/fiber/fiber.cpp#L260-L261) and [switch implementation](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/fiber/fiber.cpp#L336-L337); their export aliases are mapped at [RegisterLib](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/fiber/fiber.cpp#L555-L563). Not measured on a console; the throwing declarations only resolve imports. - [_sceFiberGetThreadFramePointerAddress](../../../core/libs/prx/libSceFiber/Export.cpp) (libSceFiber, NID `0dy4JtMUcMQ`) forwards to `sceFiberGetThreadFramePointerAddress`; that the argument list matches is inferred from the export without the underscore, as for the underscore Ult exports, not measured - [Fiber stacks](../../../core/libs/prx/libSceFiber/Export.cpp) (libSceFiber) - a fiber whose context is smaller than 64 KiB runs on an mmap'd host stack of its context size plus 1 MiB, so host calls made from it (21 KiB deep when PPSA01341 reaches SPIRV-Tools from a 16 KiB context) do not overwrite the contexts packed next to it; a larger context is the stack, as on the console. PPSA05684 needs the second: its job fibers have 64 KiB and 512 KiB contexts, the 512 KiB ones at 1 MiB-aligned addresses, and find their job record by masking the stack pointer (`~0xfffff` for the 512 KiB ones), which a host stack breaks (the game waits forever in GameInit, or faults later in a 64 KiB fiber). The 64 KiB limit is chosen between these two titles, not measured: a title with small contexts that also derives state from the stack pointer, or one whose host calls exceed what is left of a large context, is not covered diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index 479bcec5b2..fe3b9c2ce4 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -6,7 +6,7 @@ ### Unknown function info -- [sceNetBind, sceNetEtherStrton, sceNetHtonl, sceNetHtons, sceNetInit, sceNetNtohl, sceNetNtohs](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - their signatures follow [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 declarations only resolve imports. +- [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) - unknown signature - [sceNetGetStatisticsInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature diff --git a/docs/dev/debt/libSceNpAuth.md b/docs/dev/debt/libSceNpAuth.md index c7043dc638..0ad506cc90 100644 --- a/docs/dev/debt/libSceNpAuth.md +++ b/docs/dev/debt/libSceNpAuth.md @@ -2,8 +2,8 @@ ### Unknown function info -- [sceNpAuthGetAuthorizationCode, sceNpAuthGetAuthorizationCodeA](../../../core/libs/prx/libSceNpAuth/Export.cpp) (libSceNpAuth) - their names and NIDs are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpAuth.c); the signatures follow [shadPS4 `np_auth.cpp`](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_auth/np_auth.cpp#L155-L190), are not measured on a console, and every call throws -- [sceNpAuthGetIdToken, sceNpAuthGetIdTokenA](../../../core/libs/prx/libSceNpAuth/Export.cpp) (libSceNpAuth) - their names and NIDs are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpAuth.c); prototypes follow [shadPS4 `np_auth.cpp`](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_auth/np_auth.cpp#L340-L375), are not measured on a console, and every call throws +- [sceNpAuthGetAuthorizationCodeA](../../../core/libs/prx/libSceNpAuth/Export.cpp) (libSceNpAuth) - its name and NID are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpAuth.c); its signature follows [shadPS4 `np_auth.cpp`](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_auth/np_auth.cpp#L155-L190), is not measured on a console, and the throwing declaration only resolves the import +- [sceNpAuthGetIdTokenA](../../../core/libs/prx/libSceNpAuth/Export.cpp) (libSceNpAuth) - its name and NID are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpAuth.c); its prototype follows [shadPS4 `np_auth.cpp`](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_auth/np_auth.cpp#L340-L375), is not measured on a console, and the throwing declaration only resolves the import - [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, sceNpAuthGetAuthorizationCodeV3, sceNpAuthGetIdTokenV3, sceNpAuthPollAsync, sceNpAuthWaitAsync](../../../core/libs/prx/libSceNpAuth/Export.cpp) (libSceNpAuth) - request tracking, `ORBIS_NP_AUTH_ERROR_REQUEST_NOT_FOUND` (0x80550306) on unknown request 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 repeated calls on completed requests, and `ORBIS_NP_AUTH_ERROR_ABORTED` (0x80550304) after an abort follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/np/np_auth.cpp) (PS4); async requests return OK and defer `SCE_NP_ERROR_SIGNED_OUT` to `sceNpAuthPollAsync`/`sceNpAuthWaitAsync` while sync requests fail immediately; PlayStation Network is not contacted. The parameter `size` fields are not checked (`ORBIS_NP_AUTH_ERROR_INVALID_SIZE`, structure layouts unknown), the request table has no limit (`ORBIS_NP_AUTH_ERROR_REQUEST_MAX`), and console behavior on PS5 is 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 diff --git a/docs/dev/debt/libScePad.md b/docs/dev/debt/libScePad.md index 04077b9946..7b1ac780bb 100644 --- a/docs/dev/debt/libScePad.md +++ b/docs/dev/debt/libScePad.md @@ -2,7 +2,6 @@ ### Unknown function info -- [scePadClose, scePadGetInfo, scePadInit, scePadOpen, scePadRead](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - their signatures follow [shadPS4's pad declarations](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/pad/pad.h#L271-L320), not measured on a console; the throwing declarations only resolve imports. - [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 diff --git a/docs/dev/debt/libSceRudp.md b/docs/dev/debt/libSceRudp.md index d87634044f..fd1fc4868a 100644 --- a/docs/dev/debt/libSceRudp.md +++ b/docs/dev/debt/libSceRudp.md @@ -13,4 +13,4 @@ - [sceRudpGetOption](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) takes `(int ctx_id, int option, void* value, uint32_t len)`, taken from the bundled `Media/Plugins/SceRudp.prx` of PPSA06888: its two exported wrappers pass their first three arguments through unchanged and set `len` to 4, the same shape as its `sceRudpSetOption` call, which passes a 4-byte value. The option codes, the defaults and the error results are unknown, so it still throws - 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 - PPSA31327 imports declared without parameters, signatures unknown: [sceRudpPollCreate, sceRudpPollControl, sceRudpPollWait, sceRudpPollDestroy](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp), imported by Yakuza 0 Director's Cut and not called before the main menu. Names from the NID table of [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/rudp/rudp.cpp) -- [sceRudpAccept, sceRudpFlush, sceRudpGetLocalInfo, sceRudpInit, 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. +- [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. diff --git a/docs/dev/debt/libSceSysmodule.md b/docs/dev/debt/libSceSysmodule.md index ba8303c2b2..dd31c1f827 100644 --- a/docs/dev/debt/libSceSysmodule.md +++ b/docs/dev/debt/libSceSysmodule.md @@ -10,7 +10,7 @@ - [kModuleTable](../../../core/libs/prx/libSceSysmodule/ModuleTable.hpp) (libSceSysmodule) - module IDs registered from decrypted PS5 firmware 1.00–3.20 sysmodule lookup tables: - User-space IDs (0x00000005, 0x0000010a, 0x0000010e, 0x0000011b, 0x00000125, 0x00000126, 0x00000127, 0x00000128, 0x0000012a, 0x0000012b, 0x00000130) and internal IDs (0x8000007b, 0x8000007e, 0x8000007f, 0x80000080, 0x80000082, 0x80000083, 0x80000084, 0x80000085, 0x80000086, 0x80000087, 0x80000088, 0x80000089, 0x8000008e, 0x80000090, 0x80000091, 0x80000092, 0x80000093, 0x80000095, 0x80000097, 0x80000099, 0x8000009b, 0x8000009c, 0x8000009e, 0x8000009f, 0x800000a0, 0x800000a1, 0x800000a2, 0x800000a3, 0x800000a4, 0x800000a6, 0x800000a7, 0x800000a8, 0x800000a9, 0x800000ad, 0x800000ae, 0x800000af, 0x800000b0, 0x800000b2, 0x800000b3, 0x800000b4, 0x800000b5, 0x800000b6, 0x800000b7, 0x800000b9, 0x800000ba, 0x800000bb, 0x800000bc, 0x800000bd, 0x800000be, 0x800000bf, 0x800000c3); whether all mapped PRX modules exist as separate runtime files or are embedded in system daemons on retail consoles is unverified - Dual IDs for `libSceNgs2` (0x0000000b user / 0x80000090 internal) and `libSceHdrScopes` (0x00000109 user / 0x800000bd internal) reflect distinct public and internal sysmodule identifiers defined in PS5 firmware SDKs -- [sceSysmoduleIsCalledFromSysModule, sceSysmoduleIsCameraPreloaded, sceSysmoduleLoadModuleByNameInternal, sceSysmoduleMapLibcForLibkernel, sceSysmodulePreloadModuleForLibkernel, sceSysmoduleUnloadModuleByNameInternal and sceSysmoduleUnloadModuleInternalWithArg](../../../core/libs/prx/libSceSysmodule/Export.cpp) (libSceSysmodule) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSysmodule.c). `sceSysmoduleLoadModuleByNameInternal` takes `(uint8_t* fname, uint64_t a1, uint64_t a2, uint64_t a3, uint64_t a4, uint64_t a5)` from [SharpProspero's Sysmodule bindings](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Sysmodule/Sysmodule.cs#L149-L151), not measured on a console. The other exports had no credible prototypes and are declared without parameters solely to resolve known imports; they make no ABI claim. +- [sceSysmoduleIsCalledFromSysModule, sceSysmoduleIsCameraPreloaded, sceSysmoduleLoadModuleByNameInternal, sceSysmoduleMapLibcForLibkernel, sceSysmodulePreloadModuleForLibkernel and sceSysmoduleUnloadModuleByNameInternal](../../../core/libs/prx/libSceSysmodule/Export.cpp) (libSceSysmodule) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSysmodule.c). `sceSysmoduleLoadModuleByNameInternal` takes `(uint8_t* fname, uint64_t a1, uint64_t a2, uint64_t a3, uint64_t a4, uint64_t a5)` from [SharpProspero's Sysmodule bindings](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Sysmodule/Sysmodule.cs#L149-L151), not measured on a console. The other exports had no credible prototypes and are declared without parameters solely to resolve known imports; they make no ABI claim. ### Functional diff --git a/docs/dev/debt/libSceVideodec.md b/docs/dev/debt/libSceVideodec.md deleted file mode 100644 index db2c20c9ed..0000000000 --- a/docs/dev/debt/libSceVideodec.md +++ /dev/null @@ -1,6 +0,0 @@ -# libSceVideodec technical debt - -### Unknown function info - -- [sceVideodecMapMemory](../../../core/libs/prx/libSceVideodec/Export.cpp) (libSceVideodec) - its name and NID are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceVideodec.c); the no-parameter prototype follows [shadPS4's videodec.cpp](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/videodec/videodec.cpp#L132), not measured on a console. -- [sceVideodecQueryResourceInfo](../../../core/libs/prx/libSceVideodec/Export.cpp) (libSceVideodec) - its `(const SceVideodecConfigInfo*, SceVideodecResourceInfo*)` prototype follows [ChonkyStation4's SceVideodec.cpp](https://github.com/liuk7071/ChonkyStation4/blob/c101a23ecbc15e56c6e30d5dd24d3ed8880a957a/ChonkyStation4/OS/Libraries/SceVideodec/SceVideodec.cpp#L31), not measured on a console. diff --git a/docs/dev/debt/libSceVrTracker2.md b/docs/dev/debt/libSceVrTracker2.md deleted file mode 100644 index d745734c6d..0000000000 --- a/docs/dev/debt/libSceVrTracker2.md +++ /dev/null @@ -1,5 +0,0 @@ -# libSceVrTracker2 technical debt - -### Unknown function info - -- [sceVrTracker2GetControllerImage, sceVrTracker2IrGetDebugRawImage](../../../core/libs/prx/libSceVrTracker2/Export.cpp) (libSceVrTracker2) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceVrTracker2.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim. From 25434db37d814d4a66c22336d2239af2d378b516 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 21:41:26 +0000 Subject: [PATCH 173/269] docs(libkernel): mark pipe read events as the exception to sockets-only The entry before it still says sceKernelAddReadEvent and sceKernelAddWriteEvent take only guest sockets. The pipe entry now says it is the exception, without editing that entry, which an open change to the shared-memory entry next to it would conflict with. Co-Authored-By: Claude Opus 5.5 (1M context) --- 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 69f43861b1..f972aa5924 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -131,7 +131,7 @@ - [pthread_attr_getinheritsched, pthread_attr_setinheritsched, pthread_attr_getscope, pthread_attr_setscope, pthread_attr_getstackaddr](../../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) follow FreeBSD 11 libthr ([thr_attr.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libthr/thread/thr_attr.c), [thr_create.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libthr/thread/thr_create.c)), not measured on the console: the scope defaults to `PTHREAD_SCOPE_SYSTEM` (2), only it and `PTHREAD_SCOPE_PROCESS` (0) are accepted and other values return `EINVAL`, and an inherit value other than `PTHREAD_INHERIT_SCHED` (4) or `PTHREAD_EXPLICIT_SCHED` (0) returns `ENOTSUP`. `pthread_attr_getstackaddr` returns the configured stack address pointer (null by default). A null output of `pthread_attr_getinheritsched` throws, since libthr writes through it unchecked. A thread keeps the scope it was created with, taken from the creating thread under `PTHREAD_INHERIT_SCHED`, and `pthread_attr_get_np` and `scePthreadAttrGet` report it, but every guest thread is a host thread at system scope, so process scope changes nothing - [Shared memory](../../../core/libs/prx/libkernel/File/src/SharedMemory.cpp) (libkernel) on Linux implements anonymous and named objects, bounded descriptor writes and seeks, and `shm_unlink` with effective read/write permission checks from FreeBSD 11. Named paths up to 1023 bytes use `/anyps5-shm-` plus SHA-1; collisions are not detected, and the namespace is shared across processes in the host mount namespace. Named creation applies the guest umask without changing the host mask. Linux procfs and `/dev/shm` are required. Operations use an advisory directory lock; external host operations bypass it, unrelated objects serialize, and descriptor operations require two spare host descriptors and compare device/inode with the initial descriptor identity on both the pinned-object and ordinary-file fallback paths. [Guest mappings](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) support shared and private mappings of these descriptors through the allocation tracker and pin a duplicate descriptor during validation and mapping; mapping requires one spare host descriptor; file-backed mappings use the write-watch scan fallback because aliases and descriptor writes bypass per-mapping tracking. On Windows, anonymous objects are delete-on-close temporary files and `mmap` maps them as [regular files](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp); named objects remain unsupported. Invalid non-null name pointers, console behaviour and title compatibility remain unverified. - [sceKernelAddReadEvent and sceKernelAddWriteEvent](../../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel): a read event on a listening socket, here and in `kevent`, reports the number of completed connections waiting to be accepted in `data`, as FreeBSD 11.4's [`filt_solisten`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L3384-L3392) reports `so_qlen`: on Linux a TCP listener's accept queue from `TCP_INFO` (`tcpi_unacked`, which Linux fills with the accept backlog for a listening socket), and elsewhere, or for a listener that is not TCP, 1 while a connection is waiting, as no host call reports that count. A write event reports the free space of the socket's send buffer in `data`, as FreeBSD 11.4's [`filt_sowrite`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L3358-L3382) reports `sbspace`: on Linux the host's `SO_SNDBUF` less the bytes `SIOCOUTQ` counts as queued, which include the host's per-packet overhead, so the space shrinks by somewhat more than the bytes sent. Windows has no query for that space and macOS is not wired up, so both report the whole `SO_SNDBUF`, which does not shrink as data is queued. Only guest sockets are supported; other descriptors and a low watermark above 1 throw -- [sceKernelAddReadEvent on pipes](../../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel) also takes the read end of a pipe, as `kevent` does through FreeBSD's `filt_piperead`, and ignores the size argument there. Host streams, other descriptors and a write event on a pipe throw +- [sceKernelAddReadEvent on pipes](../../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel) is the one exception to the sockets-only rule above: it also takes the read end of a pipe, as `kevent` does through FreeBSD's `filt_piperead`, and ignores the size argument there. Host streams, other descriptors and a write event on a pipe throw - [close](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) and [setsockopt](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) reach a [libSceNet](../../../core/libs/prx/libSceNet/Export.cpp) socket through hooks libSceNet registers, as `select` polls them; the other libkernel socket calls do not. libSceNet numbers its sockets from 32 in its own table, so a CRT file descriptor and a socket can share a number; `close` then closes the socket. On the console both are one kernel descriptor space - [sceKernelPread, sceKernelPwrite, sceKernelPreadv, sceKernelPwritev](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) on Windows transfer at the given offset with an overlapped `ReadFile`/`WriteFile` and restore the descriptor's file pointer afterwards. Guest scalar transfers, positioned transfers, vectored transfers and seeks share a position lock across duplicated descriptors; a vector holds it for the whole operation. Independently opened descriptors and other processes do not share that lock, so vector transfers over their overlapping file ranges are not coordinated. Handles that are not disk files return `ESPIPE`. A pipe or console `readv` can return a short read when no more input is available; each iovec is transferred separately, so a failure after the first transfer returns the bytes already done - [sceKernelAioSubmitReadCommands, sceKernelAioSubmitWriteCommands and their Multiple variants](../../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) on Windows transfer through the same positioned path as `sceKernelPread`/`sceKernelPwrite` and restore the descriptor's file pointer afterwards; positioned transfers, plain transfers and seeks of one descriptor are serialized per descriptor (shared across duplicated descriptors), so a concurrent transfer observes a stable file pointer From c136ffa1a9f81561ef52008819a745e53707f92e Mon Sep 17 00:00:00 2001 From: Ararararagi Date: Mon, 19 Oct 2026 21:21:10 +0100 Subject: [PATCH 174/269] fix(libSceNpCppWebApi): name the exports the NID database resolves The 73 sceNpCppWebApiUnknown stubs carried an APS5_EXPORT alias because their names were placeholders. aerolib names all of them, and each name recomputes to the NID a title imports, so the alias is dropped and the loader derives the NID from the real symbol name, as the rest of the library does. Behaviour is unchanged: every one of them throws. The stubs are ordered by demangled name instead of by NID, so the Common template instantiations and the V1/V5 accessors sit together, as the rest of the file does. No symbol changes: the built .prx exports the same 359 NIDs as main. --- core/libs/prx/libSceNpCppWebApi/Export.cpp | 365 +++++++++------------ docs/dev/debt/libSceNpCppWebApi.md | 1 + 2 files changed, 147 insertions(+), 219 deletions(-) diff --git a/core/libs/prx/libSceNpCppWebApi/Export.cpp b/core/libs/prx/libSceNpCppWebApi/Export.cpp index 8bf9b62653..b2b6ff6b1c 100644 --- a/core/libs/prx/libSceNpCppWebApi/Export.cpp +++ b/core/libs/prx/libSceNpCppWebApi/Export.cpp @@ -1427,441 +1427,368 @@ int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_15DefaultResponseENS2 return 0; } -APS5_EXPORT("+gSMGXHgrPU", sceNpCppWebApiUnknown00); -int APS5_VABI sceNpCppWebApiUnknown00(void) { - NotImplemented_nid_no_patch("+gSMGXHgrPU"); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common13ConstIteratorINS2_12IntrusivePtrINS1_13InGameCatalog2V57ProductEEEED2Ev(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("-XNfwZTFkWw", sceNpCppWebApiUnknown01); -int APS5_VABI sceNpCppWebApiUnknown01(void) { - NotImplemented_nid_no_patch("-XNfwZTFkWw"); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common13ConstIteratorINS2_12IntrusivePtrINS1_17TitleCloudStorage2V110DataStatusEEEED2Ev(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("0f5+eBcgB14", sceNpCppWebApiUnknown02); -int APS5_VABI sceNpCppWebApiUnknown02(void) { - NotImplemented_nid_no_patch("0f5+eBcgB14"); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_13InGameCatalog2V59ContainerEEEEEEC1Ev(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("1iq5Jtw4jVs", sceNpCppWebApiUnknown03); -int APS5_VABI sceNpCppWebApiUnknown03(void) { - NotImplemented_nid_no_patch("1iq5Jtw4jVs"); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_13InGameCatalog2V59ContainerEEEEEEdeEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("1wpCXuFzH10", sceNpCppWebApiUnknown04); -int APS5_VABI sceNpCppWebApiUnknown04(void) { - NotImplemented_nid_no_patch("1wpCXuFzH10"); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_13InGameCatalog2V59ContainerEEEEEEptEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("4AFThLW28xQ", sceNpCppWebApiUnknown05); -int APS5_VABI sceNpCppWebApiUnknown05(void) { - NotImplemented_nid_no_patch("4AFThLW28xQ"); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_13InGameCatalog2V59ContainerEEEEEED1Ev(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("8-9Y4oS+OuE", sceNpCppWebApiUnknown06); -int APS5_VABI sceNpCppWebApiUnknown06(void) { - NotImplemented_nid_no_patch("8-9Y4oS+OuE"); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_13InGameCatalog2V57ProductEEEEEEptEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("8dWkWjo5EeE", sceNpCppWebApiUnknown07); -int APS5_VABI sceNpCppWebApiUnknown07(void) { - NotImplemented_nid_no_patch("8dWkWjo5EeE"); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_13InGameCatalog2V57ProductEEEEEED1Ev(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("BHvfIGdarBQ", sceNpCppWebApiUnknown08); -int APS5_VABI sceNpCppWebApiUnknown08(void) { - NotImplemented_nid_no_patch("BHvfIGdarBQ"); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_13InGameCatalog2V53SkuEEEEEEptEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("Fxxr5lYBfl4", sceNpCppWebApiUnknown09); -int APS5_VABI sceNpCppWebApiUnknown09(void) { - NotImplemented_nid_no_patch("Fxxr5lYBfl4"); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_13InGameCatalog2V53SkuEEEEEED1Ev(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("M1LnjRscx-w", sceNpCppWebApiUnknown10); -int APS5_VABI sceNpCppWebApiUnknown10(void) { - NotImplemented_nid_no_patch("M1LnjRscx-w"); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_17TitleCloudStorage2V110DataStatusEEEEEEdeEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("PsX657reZeo", sceNpCppWebApiUnknown11); -int APS5_VABI sceNpCppWebApiUnknown11(void) { - NotImplemented_nid_no_patch("PsX657reZeo"); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_17TitleCloudStorage2V110DataStatusEEEEEED1Ev(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("RvH9AZ5rCkQ", sceNpCppWebApiUnknown12); -int APS5_VABI sceNpCppWebApiUnknown12(void) { - NotImplemented_nid_no_patch("RvH9AZ5rCkQ"); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_13InGameCatalog2V59ContainerEEC1ERS7_(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("SR3BKonD8yk", sceNpCppWebApiUnknown13); -int APS5_VABI sceNpCppWebApiUnknown13(void) { - NotImplemented_nid_no_patch("SR3BKonD8yk"); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_13InGameCatalog2V59ContainerEEptEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("TSoQhYKyq5g", sceNpCppWebApiUnknown14); -int APS5_VABI sceNpCppWebApiUnknown14(void) { - NotImplemented_nid_no_patch("TSoQhYKyq5g"); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_13InGameCatalog2V59ContainerEED1Ev(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("TgsJjQqp+2k", sceNpCppWebApiUnknown15); -int APS5_VABI sceNpCppWebApiUnknown15(void) { - NotImplemented_nid_no_patch("TgsJjQqp+2k"); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_13InGameCatalog2V57ProductEEC1ERS7_(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("UHQxE3HhTXA", sceNpCppWebApiUnknown16); -int APS5_VABI sceNpCppWebApiUnknown16(void) { - NotImplemented_nid_no_patch("UHQxE3HhTXA"); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_13InGameCatalog2V57ProductEEptEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("ZN-glpk0Rug", sceNpCppWebApiUnknown17); -int APS5_VABI sceNpCppWebApiUnknown17(void) { - NotImplemented_nid_no_patch("ZN-glpk0Rug"); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_13InGameCatalog2V57ProductEED1Ev(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("ZQzXwokCnUE", sceNpCppWebApiUnknown18); -int APS5_VABI sceNpCppWebApiUnknown18(void) { - NotImplemented_nid_no_patch("ZQzXwokCnUE"); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_17TitleCloudStorage2V110DataStatusEEptEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("ZwjmbSr4Cxc", sceNpCppWebApiUnknown19); -int APS5_VABI sceNpCppWebApiUnknown19(void) { - NotImplemented_nid_no_patch("ZwjmbSr4Cxc"); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_17TitleCloudStorage2V132GetMultiDataStatusesResponseBodyEEC1Ev(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("b7leY-LNVnI", sceNpCppWebApiUnknown20); -int APS5_VABI sceNpCppWebApiUnknown20(void) { - NotImplemented_nid_no_patch("b7leY-LNVnI"); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_17TitleCloudStorage2V132GetMultiDataStatusesResponseBodyEEptEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("c3J9K9XbQqE", sceNpCppWebApiUnknown21); -int APS5_VABI sceNpCppWebApiUnknown21(void) { - NotImplemented_nid_no_patch("c3J9K9XbQqE"); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_17TitleCloudStorage2V132GetMultiDataStatusesResponseBodyEED1Ev(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("cSLbQUQO2Ns", sceNpCppWebApiUnknown22); -int APS5_VABI sceNpCppWebApiUnknown22(void) { - NotImplemented_nid_no_patch("cSLbQUQO2Ns"); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_17TitleCloudStorage2V15OwnerEEptEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("cqOxjL0ZfFA", sceNpCppWebApiUnknown23); -int APS5_VABI sceNpCppWebApiUnknown23(void) { - NotImplemented_nid_no_patch("cqOxjL0ZfFA"); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_17TitleCloudStorage2V15OwnerEED1Ev(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("d1eEjWR60wk", sceNpCppWebApiUnknown24); -int APS5_VABI sceNpCppWebApiUnknown24(void) { - NotImplemented_nid_no_patch("d1eEjWR60wk"); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_13InGameCatalog2V57ProductEEEEneERKS9_(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("epwr+cBCIFs", sceNpCppWebApiUnknown25); -int APS5_VABI sceNpCppWebApiUnknown25(void) { - NotImplemented_nid_no_patch("epwr+cBCIFs"); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_13InGameCatalog2V57ProductEEEEdeEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("eruduJ0KrT0", sceNpCppWebApiUnknown26); -int APS5_VABI sceNpCppWebApiUnknown26(void) { - NotImplemented_nid_no_patch("eruduJ0KrT0"); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_13InGameCatalog2V57ProductEEEEppEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("fG06a38iao8", sceNpCppWebApiUnknown27); -int APS5_VABI sceNpCppWebApiUnknown27(void) { - NotImplemented_nid_no_patch("fG06a38iao8"); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_17TitleCloudStorage2V110DataStatusEEEEneERKS9_(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("jfpSx14AeLc", sceNpCppWebApiUnknown28); -int APS5_VABI sceNpCppWebApiUnknown28(void) { - NotImplemented_nid_no_patch("jfpSx14AeLc"); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_17TitleCloudStorage2V110DataStatusEEEEdeEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("kBxwE4YfbIM", sceNpCppWebApiUnknown29); -int APS5_VABI sceNpCppWebApiUnknown29(void) { - NotImplemented_nid_no_patch("kBxwE4YfbIM"); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_17TitleCloudStorage2V110DataStatusEEEEppEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("kz2Z38yTLq0", sceNpCppWebApiUnknown30); -int APS5_VABI sceNpCppWebApiUnknown30(void) { - NotImplemented_nid_no_patch("kz2Z38yTLq0"); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS2_6VectorINS4_INS1_13InGameCatalog2V59ContainerEEEEEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("lB8driFKaoU", sceNpCppWebApiUnknown31); -int APS5_VABI sceNpCppWebApiUnknown31(void) { - NotImplemented_nid_no_patch("lB8driFKaoU"); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS2_6VectorINS4_INS1_13InGameCatalog2V59ContainerEEEEEEENS4_INS2_18ResponseHeaderBaseEEEE11getResponseERSB_(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("mCsV7izOkjY", sceNpCppWebApiUnknown32); -int APS5_VABI sceNpCppWebApiUnknown32(void) { - NotImplemented_nid_no_patch("mCsV7izOkjY"); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS2_6VectorINS4_INS1_13InGameCatalog2V59ContainerEEEEEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("n1yCLMxpseQ", sceNpCppWebApiUnknown33); -int APS5_VABI sceNpCppWebApiUnknown33(void) { - NotImplemented_nid_no_patch("n1yCLMxpseQ"); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_17TitleCloudStorage2V132GetMultiDataStatusesResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("nL9vSVq-29s", sceNpCppWebApiUnknown34); -int APS5_VABI sceNpCppWebApiUnknown34(void) { - NotImplemented_nid_no_patch("nL9vSVq-29s"); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_17TitleCloudStorage2V132GetMultiDataStatusesResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE11getResponseERS8_(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("nmz5JYKcMfY", sceNpCppWebApiUnknown35); -int APS5_VABI sceNpCppWebApiUnknown35(void) { - NotImplemented_nid_no_patch("nmz5JYKcMfY"); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_17TitleCloudStorage2V132GetMultiDataStatusesResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("oHRl7a+zdMU", sceNpCppWebApiUnknown36); -int APS5_VABI sceNpCppWebApiUnknown36(void) { - NotImplemented_nid_no_patch("oHRl7a+zdMU"); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS2_6VectorINS4_INS1_13InGameCatalog2V59ContainerEEEEEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("pSpO3hPNv64", sceNpCppWebApiUnknown37); -int APS5_VABI sceNpCppWebApiUnknown37(void) { - NotImplemented_nid_no_patch("pSpO3hPNv64"); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS2_6VectorINS4_INS1_13InGameCatalog2V59ContainerEEEEEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("q4UFICay6Hk", sceNpCppWebApiUnknown38); -int APS5_VABI sceNpCppWebApiUnknown38(void) { - NotImplemented_nid_no_patch("q4UFICay6Hk"); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_17TitleCloudStorage2V132GetMultiDataStatusesResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("qiD2PU2Jstc", sceNpCppWebApiUnknown39); -int APS5_VABI sceNpCppWebApiUnknown39(void) { - NotImplemented_nid_no_patch("qiD2PU2Jstc"); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_17TitleCloudStorage2V132GetMultiDataStatusesResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("tFe964qzGEM", sceNpCppWebApiUnknown40); -int APS5_VABI sceNpCppWebApiUnknown40(void) { - NotImplemented_nid_no_patch("tFe964qzGEM"); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_13InGameCatalog2V59ContainerEEEEixEm(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("tHaO36kincQ", sceNpCppWebApiUnknown41); -int APS5_VABI sceNpCppWebApiUnknown41(void) { - NotImplemented_nid_no_patch("tHaO36kincQ"); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_13InGameCatalog2V59ContainerEEEE4sizeEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("umm5m+mXiZs", sceNpCppWebApiUnknown42); -int APS5_VABI sceNpCppWebApiUnknown42(void) { - NotImplemented_nid_no_patch("umm5m+mXiZs"); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_13InGameCatalog2V57ProductEEEE5beginEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("uwhztB49KOQ", sceNpCppWebApiUnknown43); -int APS5_VABI sceNpCppWebApiUnknown43(void) { - NotImplemented_nid_no_patch("uwhztB49KOQ"); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_13InGameCatalog2V57ProductEEEE3endEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("w4K4nTYwhVE", sceNpCppWebApiUnknown44); -int APS5_VABI sceNpCppWebApiUnknown44(void) { - NotImplemented_nid_no_patch("w4K4nTYwhVE"); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_13InGameCatalog2V53SkuEEEE4sizeEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("wFcm6bgWB9Q", sceNpCppWebApiUnknown45); -int APS5_VABI sceNpCppWebApiUnknown45(void) { - NotImplemented_nid_no_patch("wFcm6bgWB9Q"); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_17TitleCloudStorage2V110DataStatusEEEE5beginEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("wp8+c84G5Xw", sceNpCppWebApiUnknown46); -int APS5_VABI sceNpCppWebApiUnknown46(void) { - NotImplemented_nid_no_patch("wp8+c84G5Xw"); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_17TitleCloudStorage2V110DataStatusEEEE3endEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("zjqTmP0ST9A", sceNpCppWebApiUnknown47); -int APS5_VABI sceNpCppWebApiUnknown47(void) { - NotImplemented_nid_no_patch("zjqTmP0ST9A"); +int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V59Container11getChildrenEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("zzO8ZGJ74ng", sceNpCppWebApiUnknown48); -int APS5_VABI sceNpCppWebApiUnknown48(void) { - NotImplemented_nid_no_patch("zzO8ZGJ74ng"); +int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainerC1Ev(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("12wY179+CE8", sceNpCppWebApiUnknown49); -int APS5_VABI sceNpCppWebApiUnknown49(void) { - NotImplemented_nid_no_patch("12wY179+CE8"); +int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainer10initializeEPNS1_6Common10LibContextE(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("3bjEFf8OhTs", sceNpCppWebApiUnknown50); -int APS5_VABI sceNpCppWebApiUnknown50(void) { - NotImplemented_nid_no_patch("3bjEFf8OhTs"); +int APS5_VABI _ZNK3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainer13isInitializedEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("6FK4IOnANkc", sceNpCppWebApiUnknown51); -int APS5_VABI sceNpCppWebApiUnknown51(void) { - NotImplemented_nid_no_patch("6FK4IOnANkc"); +int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainer15setserviceLabelEi(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("EJAOHYWUki4", sceNpCppWebApiUnknown52); -int APS5_VABI sceNpCppWebApiUnknown52(void) { - NotImplemented_nid_no_patch("EJAOHYWUki4"); +int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainer9terminateEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("FlagLhjAEmc", sceNpCppWebApiUnknown53); -int APS5_VABI sceNpCppWebApiUnknown53(void) { - NotImplemented_nid_no_patch("FlagLhjAEmc"); +int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainerD1Ev(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("G3h-NDHnyW4", sceNpCppWebApiUnknown54); -int APS5_VABI sceNpCppWebApiUnknown54(void) { - NotImplemented_nid_no_patch("G3h-NDHnyW4"); +int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi12getContainerEiRKNS4_23ParameterToGetContainerERNS1_6Common11TransactionINS8_12IntrusivePtrINS8_6VectorINSA_INS3_9ContainerEEEEEEENSA_INS8_18ResponseHeaderBaseEEEEE(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("H0XjNhflED4", sceNpCppWebApiUnknown55); -int APS5_VABI sceNpCppWebApiUnknown55(void) { - NotImplemented_nid_no_patch("H0XjNhflED4"); +int APS5_VABI _ZNK3sce2Np9CppWebApi13InGameCatalog2V57Product8getLabelEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("IbWx007Acn4", sceNpCppWebApiUnknown56); -int APS5_VABI sceNpCppWebApiUnknown56(void) { - NotImplemented_nid_no_patch("IbWx007Acn4"); +int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V57Product7getSkusEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("Is+nI7Hq9jU", sceNpCppWebApiUnknown57); -int APS5_VABI sceNpCppWebApiUnknown57(void) { - NotImplemented_nid_no_patch("Is+nI7Hq9jU"); +int APS5_VABI _ZNK3sce2Np9CppWebApi13InGameCatalog2V57Product7getTypeEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("Kc4x2uy0FFk", sceNpCppWebApiUnknown58); -int APS5_VABI sceNpCppWebApiUnknown58(void) { - NotImplemented_nid_no_patch("Kc4x2uy0FFk"); +int APS5_VABI _ZNK3sce2Np9CppWebApi13InGameCatalog2V57Product9skusIsSetEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("OK+Ggpve7B0", sceNpCppWebApiUnknown59); -int APS5_VABI sceNpCppWebApiUnknown59(void) { - NotImplemented_nid_no_patch("OK+Ggpve7B0"); +int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi23ParameterToDownloadData11setobjectIdEPKc(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("P3daBLFFREw", sceNpCppWebApiUnknown60); -int APS5_VABI sceNpCppWebApiUnknown60(void) { - NotImplemented_nid_no_patch("P3daBLFFREw"); +int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi37ParameterToGetMultiDataStatusesByUserC1Ev(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("RZcseJ3THfY", sceNpCppWebApiUnknown61); -int APS5_VABI sceNpCppWebApiUnknown61(void) { - NotImplemented_nid_no_patch("RZcseJ3THfY"); +int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi37ParameterToGetMultiDataStatusesByUser10initializeEPNS1_6Common10LibContextEPKcSA_(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("RbvteD35X-8", sceNpCppWebApiUnknown62); -int APS5_VABI sceNpCppWebApiUnknown62(void) { - NotImplemented_nid_no_patch("RbvteD35X-8"); +int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi37ParameterToGetMultiDataStatusesByUser9setfieldsEPKc(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("XCtf+23QT0k", sceNpCppWebApiUnknown63); -int APS5_VABI sceNpCppWebApiUnknown63(void) { - NotImplemented_nid_no_patch("XCtf+23QT0k"); +int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi37ParameterToGetMultiDataStatusesByUser9terminateEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("a4q15LI1a4E", sceNpCppWebApiUnknown64); -int APS5_VABI sceNpCppWebApiUnknown64(void) { - NotImplemented_nid_no_patch("a4q15LI1a4E"); +int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi37ParameterToGetMultiDataStatusesByUserD1Ev(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("eT4TQB7OsLA", sceNpCppWebApiUnknown65); -int APS5_VABI sceNpCppWebApiUnknown65(void) { - NotImplemented_nid_no_patch("eT4TQB7OsLA"); +int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi26getMultiDataStatusesByUserEiRKNS4_37ParameterToGetMultiDataStatusesByUserERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_32GetMultiDataStatusesResponseBodyEEENSA_INS8_18ResponseHeaderBaseEEEEE(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("g2dEqFnhFuw", sceNpCppWebApiUnknown66); -int APS5_VABI sceNpCppWebApiUnknown66(void) { - NotImplemented_nid_no_patch("g2dEqFnhFuw"); +int APS5_VABI _ZNK3sce2Np9CppWebApi17TitleCloudStorage2V110DataStatus11getDataSizeEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("lPAMjFZEpt8", sceNpCppWebApiUnknown67); -int APS5_VABI sceNpCppWebApiUnknown67(void) { - NotImplemented_nid_no_patch("lPAMjFZEpt8"); +int APS5_VABI _ZNK3sce2Np9CppWebApi17TitleCloudStorage2V110DataStatus11getObjectIdEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("mBagn+lW-iM", sceNpCppWebApiUnknown68); -int APS5_VABI sceNpCppWebApiUnknown68(void) { - NotImplemented_nid_no_patch("mBagn+lW-iM"); +int APS5_VABI _ZNK3sce2Np9CppWebApi17TitleCloudStorage2V110DataStatus8getOwnerEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("mkWsKEh0h0o", sceNpCppWebApiUnknown69); -int APS5_VABI sceNpCppWebApiUnknown69(void) { - NotImplemented_nid_no_patch("mkWsKEh0h0o"); +int APS5_VABI _ZNK3sce2Np9CppWebApi17TitleCloudStorage2V110DataStatus9getSlotIdEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("nE0ooeCMRm8", sceNpCppWebApiUnknown70); -int APS5_VABI sceNpCppWebApiUnknown70(void) { - NotImplemented_nid_no_patch("nE0ooeCMRm8"); +int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V132GetMultiDataStatusesResponseBody17getDataStatusListEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("qf2I9BlKXis", sceNpCppWebApiUnknown71); -int APS5_VABI sceNpCppWebApiUnknown71(void) { - NotImplemented_nid_no_patch("qf2I9BlKXis"); +int APS5_VABI _ZNK3sce2Np9CppWebApi17TitleCloudStorage2V132GetMultiDataStatusesResponseBody23getTotalDataStatusCountEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("xwy1I52dE8o", sceNpCppWebApiUnknown72); -int APS5_VABI sceNpCppWebApiUnknown72(void) { - NotImplemented_nid_no_patch("xwy1I52dE8o"); +int APS5_VABI _ZNK3sce2Np9CppWebApi17TitleCloudStorage2V15Owner12getAccountIdEv(void) { + NotImplemented_nid_no_patch(__func__); return 0; } } diff --git a/docs/dev/debt/libSceNpCppWebApi.md b/docs/dev/debt/libSceNpCppWebApi.md index 4ea6a15b4e..9352e4f30e 100644 --- a/docs/dev/debt/libSceNpCppWebApi.md +++ b/docs/dev/debt/libSceNpCppWebApi.md @@ -14,3 +14,4 @@ - [sce::Np::CppWebApi::Common::String](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - the 0x20-byte layout (32-bit reference count, buffer at 0x08, buffer size at 0x10, `LibContext*` at 0x18) comes from the module bundled with one title (PPSA21564); other SDK versions are not checked. `append` grows the buffer with the host allocator and the destructor frees it, instead of going through the `LibContext` allocator. `pushBack` into `Vector` stores a deep copy, so a destroyed temporary cannot invalidate the stored element; a title that byte-copies a `String` with a live buffer would still double-free, as nothing increments the reference count - [sce::Np::CppWebApi::Common::Vector](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - imported by PPSA03671, whose dump ships no `libSceNpCppWebApi.prx`. A `Vector` handle is an `IntrusivePtr` over a shared body; bodies created by `pushBack` use a host-side layout with the reference count at 0x30, matching the adopted-body convention measured on PPSA21564. The pointer/deleter constructor adopts a title-provided body released through the stored deleter; `pushBack` on such a body throws, since its element layout is unknown. `begin`, `end`, iterators and `operator[]` throw: the only `Vector`s the title reads back come from failed transactions, which never produce one - [sce::Np::CppWebApi::SessionManager2V1](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - imported by PPSA03671. Offline, the factories, `PlayerSessionsApi` calls and transaction `start`/`getResponse`/`readData` fail with `SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN`; parameter setters, `initialize`, `terminate`, constructors and destructors succeed without keeping state, as no request is ever sent. Response-body accessors (`getSessionId`, `getPlayerSessions`, `getInvitations`, the `IsSet` queries) throw: a failed request never produces a response object +- [libSceNpCppWebApi](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - 73 exports are named from aerolib, so each name recomputes to the NID a title imports (`tools/nid_names.py`), and each throws. They are the `Common` template members (`IntrusivePtr`, `Vector`, `Iterator`, `ConstIterator`, `Transaction`, `TransactionBase`) and the `TitleCloudStorage::V1` and `InGameCatalog::V5` accessors: a mangled name gives the class, the method and the argument types, but the layout of the object the call is made on, the values the caller reads back and the console's errors are unknown, so no call can be answered without writing to memory no source describes. No call site is recorded for any of them From 90acb96a2e5d92b3bc203fce4c5cc797d3835236 Mon Sep 17 00:00:00 2001 From: juanpimg Date: Mon, 19 Oct 2026 23:46:13 +0200 Subject: [PATCH 175/269] docs(debt): name the declared exports and state the ABI caveat --- docs/dev/debt/libSceAutoMounterClient.md | 2 +- docs/dev/debt/libSceErrorDialog.md | 2 +- docs/dev/debt/libSceImeDialog.md | 2 +- docs/dev/debt/libSceNpCommerce.md | 2 +- docs/dev/debt/libScePngDec.md | 2 +- docs/dev/debt/libSceScreenShot.md | 3 ++- docs/dev/debt/libSceWebBrowserDialog.md | 2 +- 7 files changed, 8 insertions(+), 7 deletions(-) diff --git a/docs/dev/debt/libSceAutoMounterClient.md b/docs/dev/debt/libSceAutoMounterClient.md index 10f8710cf9..b7200004e3 100644 --- a/docs/dev/debt/libSceAutoMounterClient.md +++ b/docs/dev/debt/libSceAutoMounterClient.md @@ -2,4 +2,4 @@ ### Unknown function info -- [libSceAutoMounterClient](../../../core/libs/prx/libSceAutoMounterClient/Export.cpp) - the three 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. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and the exports must not be called until their signatures are verified +- [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. diff --git a/docs/dev/debt/libSceErrorDialog.md b/docs/dev/debt/libSceErrorDialog.md index b0beacc876..1166c74424 100644 --- a/docs/dev/debt/libSceErrorDialog.md +++ b/docs/dev/debt/libSceErrorDialog.md @@ -6,4 +6,4 @@ ### Unknown function info -- [sceErrorDialogOpenDetail, sceErrorDialogOpenWithReport](../../../core/libs/prx/libSceErrorDialog/Export.cpp) (libSceErrorDialog) - their names and NIDs are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceErrorDialog.c); the no-parameter prototypes follow [shadPS4 `error_dialog.cpp`](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/ime/error_dialog.cpp#L167-L172), are not measured on a console, and every call throws +- [sceErrorDialogOpenDetail, sceErrorDialogOpenWithReport](../../../core/libs/prx/libSceErrorDialog/Export.cpp) (libSceErrorDialog) - their names and NIDs are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceErrorDialog.c); the no-parameter prototypes follow [shadPS4 `error_dialog.cpp`](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/ime/error_dialog.cpp#L167-L172) 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. diff --git a/docs/dev/debt/libSceImeDialog.md b/docs/dev/debt/libSceImeDialog.md index 21da16020b..6e641b31ce 100644 --- a/docs/dev/debt/libSceImeDialog.md +++ b/docs/dev/debt/libSceImeDialog.md @@ -6,5 +6,5 @@ ### Unknown function info -- [sceImeDialogForTestFunction, sceImeDialogForceClose, sceImeDialogGetCurrentStarState, sceImeDialogInitInternal, sceImeDialogSetPanelPosition](../../../core/libs/prx/libSceImeDialog/Export.cpp) (libSceImeDialog) - their signatures follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/ime/ime_dialog.h#L34-L51), not measured on a console; the throwing declarations only resolve imports. +- [sceImeDialogForTestFunction, sceImeDialogForceClose, sceImeDialogGetCurrentStarState, sceImeDialogInitInternal, sceImeDialogSetPanelPosition](../../../core/libs/prx/libSceImeDialog/Export.cpp) (libSceImeDialog) - their signatures follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/ime/ime_dialog.h#L34-L51), not measured on a console. They are declared without parameters solely to resolve known imports; this is not an ABI claim, and every call throws. - [sceImeDialogInit, sceImeDialogGetStatus, sceImeDialogGetResult, sceImeDialogAbort, sceImeDialogTerm](../../../core/libs/prx/libSceImeDialog/Export.cpp) (libSceImeDialog) - the statuses (none 0, running 1, finished 2), the end statuses (ok 0, user canceled 1, aborted 2) and the errors of `sceImeDialogInit` for an open dialog (`0x80bc0001`), a null parameter (`0x80bc0031`), a maximum text length outside 1 to 2048 (`0x80bc0016`) and a null text buffer (`0x80bc0017`) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/b2d7ec7d31f9ad9426db93afe079db8679b3e1e0/src/core/libraries/ime/ime_dialog.cpp); none is measured on a PS5. A result, abort or term on a dialog that is not finished, running or open returns `0x80bc0002`, where shadPS4 returns `0x80bc0105` to `0x80bc0107`, and `sceImeDialogTerm` also closes a running dialog, which shadPS4 refuses. The text reaches the title's buffer only when Enter accepts it diff --git a/docs/dev/debt/libSceNpCommerce.md b/docs/dev/debt/libSceNpCommerce.md index ea9437be65..8de5c9ca21 100644 --- a/docs/dev/debt/libSceNpCommerce.md +++ b/docs/dev/debt/libSceNpCommerce.md @@ -6,5 +6,5 @@ ### 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), is not measured on a console, and every call throws +- [sceNpCommerceDialogInitializeInternal](../../../core/libs/prx/libSceNpCommerce/Export.cpp) (libSceNpCommerce) - its name and NID are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpCommerce.c); the no-parameter prototype follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_commerce/np_commerce.cpp#L61) and 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. Call of Duty: Black Ops (PPSA34501) calls it at Zombies > Online diff --git a/docs/dev/debt/libScePngDec.md b/docs/dev/debt/libScePngDec.md index 6b8253d42a..2eca53adb5 100644 --- a/docs/dev/debt/libScePngDec.md +++ b/docs/dev/debt/libScePngDec.md @@ -3,4 +3,4 @@ ### 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), not measured on a console. +- [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. diff --git a/docs/dev/debt/libSceScreenShot.md b/docs/dev/debt/libSceScreenShot.md index 46cf3b8867..568cb899b6 100644 --- a/docs/dev/debt/libSceScreenShot.md +++ b/docs/dev/debt/libSceScreenShot.md @@ -2,4 +2,5 @@ ### Unknown function info -- [sceScreenShotCapture, sceScreenShotDisable, sceScreenShotDisableNotification, sceScreenShotEnable, sceScreenShotEnableNotification, sceScreenShotGetAppInfo, sceScreenShotGetDrcParam, sceScreenShotIsDisabled, sceScreenShotIsVshScreenCaptureDisabled, sceScreenShotSetDrcParam and sceScreenShotSetParam, 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 declarations provide no parameters ([shadPS4, pinned commit](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/screenshot/screenshot.h)), and no credible PS5 prototypes were found. These declarations have no parameters solely to resolve known imports and make no ABI claim. +- [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 diff --git a/docs/dev/debt/libSceWebBrowserDialog.md b/docs/dev/debt/libSceWebBrowserDialog.md index 2986d30e80..58b98bd389 100644 --- a/docs/dev/debt/libSceWebBrowserDialog.md +++ b/docs/dev/debt/libSceWebBrowserDialog.md @@ -6,4 +6,4 @@ ### 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), are not measured on a console, and every call throws +- [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 13227a33ed106831d41f50a430ba13ce545e4b04 Mon Sep 17 00:00:00 2001 From: erickerxhalli <313650569+erickerxhalli@users.noreply.github.com> Date: Mon, 19 Oct 2026 21:58:05 +0000 Subject: [PATCH 176/269] fix(libSceMove): throw for a handle that is not open Track the handles sceMoveOpen issues. A handle that was never opened, is closed or was opened before sceMoveTerm now throws instead of succeeding, since the console's INVALID_HANDLE result and its order against INVALID_ARG are not known. List the no-controller path under Silent stubs, as libSceHmd and libSceBluetoothHid do, and check the handle values in the test. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Arbzg9c2DA4DEFKVSvRgYv --- core/libs/prx/libSceMove/Export.cpp | 58 ++++++++++++++++++----------- core/libs/tests/GuestMove.cpp | 47 ++++++++++++++++++----- docs/dev/debt/libSceMove.md | 6 ++- 3 files changed, 79 insertions(+), 32 deletions(-) diff --git a/core/libs/prx/libSceMove/Export.cpp b/core/libs/prx/libSceMove/Export.cpp index f86a61a317..340a60d887 100644 --- a/core/libs/prx/libSceMove/Export.cpp +++ b/core/libs/prx/libSceMove/Export.cpp @@ -1,6 +1,8 @@ #include #include #include +#include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -14,84 +16,96 @@ constexpr int MOVE_ERROR_INVALID_ARG = static_cast(0x80EE0003); constexpr int32_t MOVE_FIRST_HANDLE = 0x30B0100; constexpr int32_t MOVE_HANDLE_STEP = 0x100; -std::atomic initialized{false}; -std::atomic nextHandle{MOVE_FIRST_HANDLE}; +std::mutex stateMutex; +bool initialized = false; +int32_t nextHandle = MOVE_FIRST_HANDLE; +std::unordered_set openHandles; + +int checkHandle(const char* function, int32_t handle) { + std::lock_guard lock(stateMutex); + if (!initialized) return MOVE_ERROR_NOT_INIT; + if (!openHandles.contains(handle)) NotImplemented_nid_no_patch(function); + return MOVE_OK; +} } extern "C" { int APS5_VABI sceMoveInit(void) { - bool expected = false; - return initialized.compare_exchange_strong(expected, true) ? MOVE_OK : MOVE_ERROR_ALREADY_INIT; + std::lock_guard lock(stateMutex); + if (initialized) return MOVE_ERROR_ALREADY_INIT; + initialized = true; + return MOVE_OK; } int APS5_VABI sceMoveTerm(void) { - return initialized.exchange(false) ? MOVE_OK : MOVE_ERROR_NOT_INIT; + std::lock_guard lock(stateMutex); + if (!initialized) return MOVE_ERROR_NOT_INIT; + initialized = false; + openHandles.clear(); + return MOVE_OK; } int APS5_VABI sceMoveOpen(int32_t userId, int32_t type, int32_t index) { (void)userId; (void)type; (void)index; + std::lock_guard lock(stateMutex); if (!initialized) return MOVE_ERROR_NOT_INIT; - return nextHandle.fetch_add(MOVE_HANDLE_STEP); + const int32_t handle = nextHandle; + nextHandle += MOVE_HANDLE_STEP; + openHandles.insert(handle); + return handle; } int APS5_VABI sceMoveClose(int32_t handle) { - (void)handle; + std::lock_guard lock(stateMutex); if (!initialized) return MOVE_ERROR_NOT_INIT; + if (openHandles.erase(handle) == 0) NotImplemented_nid_no_patch(__func__); return MOVE_OK; } int APS5_VABI sceMoveGetDeviceInfo(int32_t handle, void* info) { - (void)handle; - if (!initialized) return MOVE_ERROR_NOT_INIT; + if (const int result = checkHandle(__func__, handle); result != MOVE_OK) return result; if (!info) return MOVE_ERROR_INVALID_ARG; return MOVE_NO_CONTROLLER_CONNECTED; } int APS5_VABI sceMoveGetExtensionPortInfo(int32_t handle, void* data) { - (void)handle; - if (!initialized) return MOVE_ERROR_NOT_INIT; + if (const int result = checkHandle(__func__, handle); result != MOVE_OK) return result; if (!data) return MOVE_ERROR_INVALID_ARG; return MOVE_NO_CONTROLLER_CONNECTED; } int APS5_VABI sceMoveReadStateLatest(int32_t handle, void* data) { - (void)handle; - if (!initialized) return MOVE_ERROR_NOT_INIT; + if (const int result = checkHandle(__func__, handle); result != MOVE_OK) return result; if (!data) return MOVE_ERROR_INVALID_ARG; return MOVE_NO_CONTROLLER_CONNECTED; } int APS5_VABI sceMoveReadStateRecent(int32_t handle, int64_t timestamp, void* data, int32_t* outCount) { - (void)handle; - if (!initialized) return MOVE_ERROR_NOT_INIT; + if (const int result = checkHandle(__func__, handle); result != MOVE_OK) return result; if (timestamp < 0 || !data || !outCount) return MOVE_ERROR_INVALID_ARG; return MOVE_NO_CONTROLLER_CONNECTED; } int APS5_VABI sceMoveSetVibration(int32_t handle, uint8_t motor) { - (void)handle; (void)motor; - if (!initialized) return MOVE_ERROR_NOT_INIT; + if (const int result = checkHandle(__func__, handle); result != MOVE_OK) return result; return MOVE_NO_CONTROLLER_CONNECTED; } int APS5_VABI sceMoveSetLightSphere(int32_t handle, uint8_t red, uint8_t green, uint8_t blue) { - (void)handle; (void)red; (void)green; (void)blue; - if (!initialized) return MOVE_ERROR_NOT_INIT; + if (const int result = checkHandle(__func__, handle); result != MOVE_OK) return result; return MOVE_NO_CONTROLLER_CONNECTED; } int APS5_VABI sceMoveResetLightSphere(int32_t handle) { - (void)handle; - if (!initialized) return MOVE_ERROR_NOT_INIT; - return MOVE_OK; + return checkHandle(__func__, handle); } } diff --git a/core/libs/tests/GuestMove.cpp b/core/libs/tests/GuestMove.cpp index e928abcc82..6d35288a5a 100644 --- a/core/libs/tests/GuestMove.cpp +++ b/core/libs/tests/GuestMove.cpp @@ -2,6 +2,7 @@ #include #include #include +#include extern "C" { int APS5_VABI sceMoveInit(void); @@ -27,6 +28,8 @@ constexpr int kNotInit = static_cast(0x80EE0001); constexpr int kAlreadyInit = static_cast(0x80EE0002); constexpr int kInvalidArg = static_cast(0x80EE0003); constexpr std::int32_t kUserId = 0x10000000; +constexpr std::int32_t kFirstHandle = 0x30B0100; +constexpr std::int32_t kHandleStep = 0x100; constexpr std::uint8_t kFill = 0x5A; struct Buffer { @@ -42,6 +45,16 @@ struct Buffer { } }; +template +bool Throws(TCall call) { + try { + call(); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + void RequireNotInit(std::int32_t handle) { Buffer buffer; std::int32_t count = -7; @@ -59,19 +72,35 @@ void RequireNotInit(std::int32_t handle) { Require(count == -7); } +void RequireUnknownHandleThrows(std::int32_t handle) { + Buffer buffer; + std::int32_t count = -7; + Require(Throws([&] { sceMoveClose(handle); })); + Require(Throws([&] { sceMoveGetDeviceInfo(handle, buffer.bytes); })); + Require(Throws([&] { sceMoveGetExtensionPortInfo(handle, buffer.bytes); })); + Require(Throws([&] { sceMoveReadStateLatest(handle, buffer.bytes); })); + Require(Throws([&] { sceMoveReadStateRecent(handle, 0, buffer.bytes, &count); })); + Require(Throws([&] { sceMoveSetVibration(handle, 0xFF); })); + Require(Throws([&] { sceMoveSetLightSphere(handle, 0xFF, 0, 0); })); + Require(Throws([&] { sceMoveResetLightSphere(handle); })); + Require(buffer.Untouched()); + Require(count == -7); +} + } int main() { - RequireNotInit(0); + RequireNotInit(kFirstHandle); Require(sceMoveInit() == kOk); Require(sceMoveInit() == kAlreadyInit); + RequireUnknownHandleThrows(kFirstHandle); + const std::int32_t first = sceMoveOpen(kUserId, 0, 0); const std::int32_t second = sceMoveOpen(kUserId, 0, 1); - Require(first > 0); - Require(second > 0); - Require(first != second); + Require(first == kFirstHandle); + Require(second == kFirstHandle + kHandleStep); Require(sceMoveGetDeviceInfo(first, nullptr) == kInvalidArg); Require(sceMoveGetExtensionPortInfo(first, nullptr) == kInvalidArg); @@ -95,16 +124,16 @@ int main() { Require(sceMoveResetLightSphere(first) == kOk); Require(sceMoveClose(first) == kOk); - Require(sceMoveClose(second) == kOk); + RequireUnknownHandleThrows(first); + Require(sceMoveSetVibration(second, 0) == kNoControllerConnected); Require(sceMoveTerm() == kOk); - RequireNotInit(first); + RequireNotInit(second); Require(sceMoveInit() == kOk); + RequireUnknownHandleThrows(second); const std::int32_t third = sceMoveOpen(kUserId, 0, 0); - Require(third > 0); - Require(third != first); - Require(third != second); + Require(third == kFirstHandle + 2 * kHandleStep); Require(sceMoveClose(third) == kOk); Require(sceMoveTerm() == kOk); } diff --git a/docs/dev/debt/libSceMove.md b/docs/dev/debt/libSceMove.md index 7aa1514fab..8ef70c92b6 100644 --- a/docs/dev/debt/libSceMove.md +++ b/docs/dev/debt/libSceMove.md @@ -1,5 +1,9 @@ # libSceMove technical debt +### Silent stubs + +- [libSceMove](../../../core/libs/prx/libSceMove/Export.cpp) implements only the no-controller path: initialization and termination succeed, `sceMoveOpen` returns a new handle for any user, type and index, `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) - no PS Move controller is modelled, and the behaviour and error codes follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/move/move.cpp) (PS4), not measured on a PS5; signatures follow [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json). `sceMoveOpen` returns a new handle (`0x30B0100`, then every `0x100`) for any user, type and index, and no call checks a handle, so `sceMoveClose` and `sceMoveResetLightSphere` succeed for any handle. The device info, state, extension port, vibration and light sphere calls return 1 (no controller connected) and write nothing. Before `sceMoveInit` and after `sceMoveTerm` every other call returns `NOT_INIT` (`0x80EE0001`), and 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) - 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), not 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`). A handle that was never opened, is closed or was opened before `sceMoveTerm` throws: the console's `INVALID_HANDLE` (`0x80EE0004`) result and its order against `INVALID_ARG` are not known. Whether `sceMoveOpen` rejects an invalid user or a slot that is already open is not known. Closes when: measured on a PS5 From e91d7a16bb34474fd2457369b0920650b6959e89 Mon Sep 17 00:00:00 2001 From: davidamsellem Date: Mon, 19 Oct 2026 17:09:13 +0200 Subject: [PATCH 177/269] docs: measure integer image atomics on 32-bit surfaces on gfx1100 The entry stated that image atomics on 32_SINT and 32_FLOAT act on the raw dword with no measurement behind it. On gfx1100 the 13 integer image atomics return and store the same raw-dword results on 32_UINT, 32_SINT and 32_FLOAT, while 16_UINT and 8_8_8_8_UINT controls differ. gfx1100 has no float image atomics, so that part stays open. Co-Authored-By: Claude Opus 5.5 (1M context) --- 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 8c179efa90..7ab44cc957 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -63,7 +63,7 @@ - [Unnormalized S#s](../../../core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp) are accepted only when direct and used only by explicit-LOD `image_sample` without comparison, derivatives or `_a`, on a single-level, single-layer 1D or 2D view of mip 0, even when the image is also sampled through a normalized S#. - [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. +- [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. 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. That covers 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. 16_UINT and 8_8_8_8_UINT controls differ, so the format is read. gfx1100 has no float image atomics, so the float results and the console's gfx1013 remain 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 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` 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. From 5882b2d449a71210db7a7c17f0d447c39487602f Mon Sep 17 00:00:00 2001 From: davidamsellem Date: Tue, 20 Oct 2026 00:09:52 +0200 Subject: [PATCH 178/269] docs: cite the gfx1031 image atomic measurement, float atomics included --- 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 7ab44cc957..5767dd4ca9 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -63,7 +63,7 @@ - [Unnormalized S#s](../../../core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp) are accepted only when direct and used only by explicit-LOD `image_sample` without comparison, derivatives or `_a`, on a single-level, single-layer 1D or 2D view of mip 0, even when the image is also sampled through a normalized S#. - [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. 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. That covers 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. 16_UINT and 8_8_8_8_UINT controls differ, so the format is read. gfx1100 has no float image atomics, so the float results and the console's gfx1013 remain unmeasured. Other formats throw. +- [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 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` 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. From bf47697ec013f5a19c5153ac656772dfeedd1819 Mon Sep 17 00:00:00 2001 From: SuarezPM Date: Mon, 19 Oct 2026 19:19:02 -0300 Subject: [PATCH 179/269] feat(relinker): lower MCOMMIT and RDPRU for Intel with execution tests MCOMMIT becomes STC plus NOP padding and RDPRU goes through a runtime-checked stub returning the host TSC for MPERF/APERF while other selectors yield zero, with converter and execution tests. docs/dev/debt/relinker.md records the new lowering and its unverified assumptions. --- .../x86/Amd64OnlySubstitutionTable.hpp | 1 + .../src/x86/Amd64OnlyInstructionMatcher.cpp | 38 ++++- .../codegen/tests/Amd64OnlyConverterTests.cpp | 130 ++++++++++++++++-- docs/dev/debt/relinker.md | 2 +- 4 files changed, 153 insertions(+), 18 deletions(-) diff --git a/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTable.hpp b/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTable.hpp index f82d8a19f9..c3df3388cc 100644 --- a/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTable.hpp +++ b/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTable.hpp @@ -65,6 +65,7 @@ inline constexpr Entry kSha1msg2 = {"SHA1MSG2", nullptr, 0}; inline constexpr std::uint8_t kMovsStoreOpcode = 0x11; inline constexpr std::uint8_t kPshufbZero = 0x80; inline constexpr std::uint8_t kTrapFill = 0xCC; +inline constexpr std::uint8_t kRdpruTrampolineBytes[] = {0x9C, 0x85, 0xC9, 0x74, 0x0B, 0x83, 0xF9, 0x01, 0x74, 0x06, 0x31, 0xC0, 0x31, 0xD2, 0xEB, 0x02, 0x0F, 0x31, 0x9D}; inline constexpr std::size_t kStubAlignment = 16; inline constexpr std::size_t kRedZoneSpillFrame = 0x90; diff --git a/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp b/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp index c9128ecbaf..0825fa3986 100644 --- a/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp +++ b/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp @@ -108,6 +108,14 @@ Amd64OnlyMatch _inPlace(const Entry& entry, const std::size_t length, std::vecto return Amd64OnlyMatch{entry.Name, length, Amd64OnlyLowering::InPlace, std::move(replacement), {}, 0}; } +void _emitRdpru(StubBodyBuilder& body) { + body.Raw(std::span(kLeaRspBelowRedZone.Bytes, kLeaRspBelowRedZone.Size)); + body.Raw(std::span(kRdpruTrampolineBytes, sizeof(kRdpruTrampolineBytes))); + body.Raw(std::span(kLeaRspRestore.Bytes, kLeaRspRestore.Size)); +} + +constexpr std::uint8_t kMcommitStc = 0xF9; + void _emitScalarCompare(StubBodyBuilder& body, const DecodedInstruction& instr, const ScalarCompareOperands& operands) { const std::span bytes(instr.Data, instr.Length); if (operands.RipDisplacementOffset) @@ -180,6 +188,8 @@ private: [[nodiscard]] Amd64OnlyMatch _matchSha256(const DecodedInstruction& instr, std::span trailing) const; [[nodiscard]] Amd64OnlyMatch _matchSha1(const DecodedInstruction& instr, std::span trailing) const; [[nodiscard]] Amd64OnlyMatch _matchClzero(const DecodedInstruction& instr, std::span trailing) const; + [[nodiscard]] Amd64OnlyMatch _matchRdpru(const DecodedInstruction& instr, std::span trailing) const; + [[nodiscard]] Amd64OnlyMatch _matchMcommit(const DecodedInstruction& instr) const; [[nodiscard]] std::optional _matchRosetta(const DecodedInstruction& instr, std::span trailing) const; [[nodiscard]] const char* _emitOutOfLine(StubBodyBuilder& body, const DecodedInstruction& instr) const; }; @@ -222,6 +232,18 @@ Amd64OnlyMatch Amd64OnlyInstructionMatcher::_matchClzero(const DecodedInstructio return _trampoline(kClzero.Name, instr.Length, _outOfLine(instr.Length, trailing, [&](StubBodyBuilder& body) { _clzeroLowering.EmitOutOfLine(body, operands); })); } +Amd64OnlyMatch Amd64OnlyInstructionMatcher::_matchRdpru(const DecodedInstruction& instr, std::span trailing) const { + if (!_validWait(instr)) + return _unsupported(kRdpru, instr.Length); + return _trampoline(kRdpru.Name, instr.Length, _outOfLine(instr.Length, trailing, [](StubBodyBuilder& body) { _emitRdpru(body); })); +} + +Amd64OnlyMatch Amd64OnlyInstructionMatcher::_matchMcommit(const DecodedInstruction& instr) const { + if (!_validWait(instr)) + return _unsupported(kMcommit, instr.Length); + return _inPlace(kMcommit, instr.Length, {kMcommitStc}); +} + std::optional Amd64OnlyInstructionMatcher::_matchRosetta(const DecodedInstruction& instr, std::span trailing) const { if (_target != Amd64OnlyTarget::Rosetta) return std::nullopt; @@ -269,6 +291,18 @@ const char* Amd64OnlyInstructionMatcher::_emitOutOfLine(StubBodyBuilder& body, c _reciprocalLowering.EmitOutOfLine(body, *reciprocal); return _reciprocalEntry(*reciprocal).Name; } + if (instr.IsRdpru()) { + if (!_validWait(instr)) + return nullptr; + _emitRdpru(body); + return kRdpru.Name; + } + if (instr.IsMcommit()) { + if (!_validWait(instr)) + return nullptr; + body.Raw(std::span(&kMcommitStc, 1)); + return kMcommit.Name; + } if (_target == Amd64OnlyTarget::Rosetta && instr.IsRdpid()) { body.Raw(_rdpidReplacement(instr)); return kRdpid.Name; @@ -363,10 +397,10 @@ std::optional Amd64OnlyInstructionMatcher::Match( return _unsupported(kClzero, length); if (instr.IsRdpru()) - return _unsupported(kRdpru, length); + return _matchRdpru(instr, trailing); if (instr.IsMcommit()) - return _unsupported(kMcommit, length); + return _matchMcommit(instr); return _matchRosetta(instr, trailing); } diff --git a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp index e1a802b28c..8e44caf22a 100644 --- a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp +++ b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp @@ -350,7 +350,16 @@ void matcherSubstitutions() { require(prefixedClzero && prefixedClzero->Lowering == Codegen::Amd64OnlyLowering::Unsupported, "CLZERO with a 66, F2 or F3 prefix was not reported as unsupported"); } const auto rdpru = match({0x0F, 0x01, 0xFD}); - require(rdpru && rdpru->Lowering == Codegen::Amd64OnlyLowering::Unsupported && rdpru->InstructionName == "RDPRU", "RDPRU was not reported as unsupported"); + require(rdpru && rdpru->Lowering == Codegen::Amd64OnlyLowering::Trampoline && rdpru->InstructionName == "RDPRU" && rdpru->StubBody.size() == 40 && rdpru->ReturnBranchOffset == 35 && rdpru->Relocations.empty(), "RDPRU was not lowered through a runtime-checked stub"); + require(Bytes(rdpru->StubBody.begin(), rdpru->StubBody.begin() + 35) == Bytes{0x48, 0x8D, 0xA4, 0x24, 0x70, 0xFF, 0xFF, 0xFF, 0x9C, 0x85, 0xC9, 0x74, 0x0B, 0x83, 0xF9, 0x01, 0x74, 0x06, 0x31, 0xC0, 0x31, 0xD2, 0xEB, 0x02, 0x0F, 0x31, 0x9D, 0x48, 0x8D, 0xA4, 0x24, 0x90, 0x00, 0x00, 0x00}, "RDPRU stub does not frame the red zone or dispatch ECX to TSC or zero"); + const auto mcommit = match({0xF3, 0x0F, 0x01, 0xFA}); + require(mcommit && mcommit->Lowering == Codegen::Amd64OnlyLowering::InPlace && mcommit->InstructionName == "MCOMMIT" && mcommit->ReplacementBytes == Bytes{0xF9, 0x0F, 0x1F, 0x00}, "MCOMMIT was not replaced by STC plus NOP"); + const auto prefixedMcommit = match({0x2E, 0xF3, 0x0F, 0x01, 0xFA}); + require(prefixedMcommit && prefixedMcommit->Lowering == Codegen::Amd64OnlyLowering::InPlace && prefixedMcommit->ReplacementBytes == Bytes{0xF9, 0x0F, 0x1F, 0x40, 0x00}, "Prefixed MCOMMIT was not padded to its length"); + const auto lockedMcommit = match({0xF0, 0xF3, 0x0F, 0x01, 0xFA}); + require(lockedMcommit && lockedMcommit->Lowering == Codegen::Amd64OnlyLowering::Unsupported, "LOCK MCOMMIT was replaced instead of failing"); + const auto lockedRdpru = match({0xF0, 0x0F, 0x01, 0xFD}); + require(lockedRdpru && lockedRdpru->Lowering == Codegen::Amd64OnlyLowering::Unsupported, "LOCK RDPRU was replaced instead of failing"); const auto registerForm = match({0x66, 0x0F, 0x79, 0xCA}); require(registerForm && registerForm->Lowering == Codegen::Amd64OnlyLowering::Trampoline && registerForm->InstructionName == "EXTRQ register form", "EXTRQ register form was not lowered through a stub"); const auto insertqRegisterForm = match({0xF2, 0x0F, 0x79, 0xCA}); @@ -490,12 +499,12 @@ void converterSegment() { auto branchInside = file; branchInside[0x207] = 0x02; requireFailure([&] { (void)converter->Convert(branchInside, {segmentHeader(20)}); }, "Branch into an AMD-only instruction was accepted"); - auto rdpru = file; - rdpru[0x20F] = 0x0F; - rdpru[0x210] = 0x01; - rdpru[0x211] = 0xFD; - rdpru[0x212] = 0x90; - requireFailure([&] { (void)converter->Convert(rdpru, {segmentHeader(20)}); }, "RDPRU was silently kept"); + auto lockedRdpru = file; + lockedRdpru[0x20F] = 0xF0; + lockedRdpru[0x210] = 0x0F; + lockedRdpru[0x211] = 0x01; + lockedRdpru[0x212] = 0xFD; + requireFailure([&] { (void)converter->Convert(lockedRdpru, {segmentHeader(20)}); }, "LOCK RDPRU was accepted"); auto registerForm = file; const Bytes extrqRegister = {0x66, 0x0F, 0x79, 0xCA}; std::copy(extrqRegister.begin(), extrqRegister.end(), registerForm.begin() + 0x20F); @@ -612,15 +621,16 @@ void converterSha1() { void converterMonitorWait() { const auto converter = Codegen::MakeAmd64OnlyConverter(); Bytes file(0x300, 0xCC); - const Bytes text = {0x0F, 0x01, 0xFA, 0x0F, 0x01, 0xFB, 0x2E, 0x0F, 0x01, 0xFB, 0xC3}; + const Bytes text = {0x0F, 0x01, 0xFA, 0x0F, 0x01, 0xFB, 0x2E, 0x0F, 0x01, 0xFB, 0xF3, 0x0F, 0x01, 0xFA, 0xC3}; std::copy(text.begin(), text.end(), file.begin() + 0x200); const auto result = converter->Convert(file, {segmentHeader(text.size())}); - require(result.ReplacedCount == 3 && result.Trampolines.empty() && result.Reports.size() == 3, "MONITORX/MWAITX were not replaced in place"); + require(result.ReplacedCount == 4 && result.Trampolines.empty() && result.Reports.size() == 4, "MONITORX/MWAITX/MCOMMIT were not replaced in place"); require(result.Reports[0].InstructionName == "MONITORX" && result.Reports[1].InstructionName == "MWAITX" && result.Reports[2].Offset == 0x206 && result.Reports[2].ReplacementLength == 4 && result.Reports[2].Lowering == Codegen::Amd64OnlyLowering::InPlace, "MONITORX/MWAITX reports are wrong"); + require(result.Reports[3].InstructionName == "MCOMMIT" && result.Reports[3].Offset == 0x20A && result.Reports[3].ReplacementLength == 4 && result.Reports[3].Lowering == Codegen::Amd64OnlyLowering::InPlace, "MCOMMIT report is wrong"); auto expected = file; - const Bytes replaced = {0x0F, 0x1F, 0x00, 0xF3, 0x90, 0x90, 0xF3, 0x90, 0x66, 0x90, 0xC3}; + const Bytes replaced = {0x0F, 0x1F, 0x00, 0xF3, 0x90, 0x90, 0xF3, 0x90, 0x66, 0x90, 0xF9, 0x0F, 0x1F, 0x00, 0xC3}; std::copy(replaced.begin(), replaced.end(), expected.begin() + 0x200); - require(result.Bytes == expected, "MONITORX/MWAITX were replaced with the wrong bytes"); + require(result.Bytes == expected, "MONITORX/MWAITX/MCOMMIT were replaced with the wrong bytes"); } void converterClzero() { @@ -1202,10 +1212,10 @@ void converterFailureOffsets() { auto movntsRegister = file; movntsRegister[0x212] = 0xC1; require(failureOffset([&] { (void)converter->Convert(movntsRegister, {segmentHeader(20)}); }, "MOVNTSS register form was accepted") == 0x20F, "MOVNTSS failure does not carry the file offset"); - auto rdpru = file; - const Bytes rdpruBytes = {0x0F, 0x01, 0xFD, 0x90}; - std::copy(rdpruBytes.begin(), rdpruBytes.end(), rdpru.begin() + 0x20F); - require(failureOffset([&] { (void)converter->Convert(rdpru, {segmentHeader(20)}); }, "RDPRU was accepted") == 0x20F, "Unsupported instruction failure does not carry the file offset"); + auto lockedWait = file; + const Bytes lockedWaitBytes = {0xF0, 0x0F, 0x01, 0xFB}; + std::copy(lockedWaitBytes.begin(), lockedWaitBytes.end(), lockedWait.begin() + 0x20F); + require(failureOffset([&] { (void)converter->Convert(lockedWait, {segmentHeader(20)}); }, "LOCK MWAITX was accepted") == 0x20F, "Unsupported instruction failure does not carry the file offset"); } Bytes elfFixture(const Bytes& text) { @@ -1794,6 +1804,92 @@ void ripRelativeExecution() { require(movedRun.Rax == movedRun.SiteAddress + 4096 + 0x30, "Moved RIP-relative LEA did not compute the original address"); unchanged(movedRun, (1u << 1) | (1u << 3), "Stub with moved RIP-relative instructions clobbered a register or RFLAGS"); } + +void rdpruExecution() { + const auto matcher = Codegen::MakeAmd64OnlyInstructionMatcher(); + const Bytes site = {0x0F, 0x01, 0xFD}; + const auto match = matcher->Match(site.data(), site.size()); + require(match && match->Lowering == Codegen::Amd64OnlyLowering::Trampoline, "RDPRU stub was not produced"); + auto body = match->StubBody; + const auto ret = body.size(); + body.push_back(0xC3); + const auto displacement = static_cast(ret - (match->ReturnBranchOffset + 5)); + std::memcpy(body.data() + match->ReturnBranchOffset + 1, &displacement, sizeof(displacement)); + void* code = mmap(nullptr, 4096, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + require(code != MAP_FAILED, "cannot map executable memory for the RDPRU stub"); + std::memcpy(code, body.data(), body.size()); + for (const std::uint32_t selector : {0u, 1u, 2u, 0xFFFFFFFFu}) { + std::uint64_t raxOut = 0; + std::uint64_t rdxOut = 0; + std::uint64_t rcxOut = 0; + std::uint64_t flagsOut = 0; + std::uint64_t canaryOut = 0; + asm volatile( + "sub $128, %%rsp\n\t" + "movabs $0xA5A5A5A5A5A5A5A5, %%rax\n\t" + "lea -136(%%rsp), %%rdi\n\t" + "mov $16, %%ecx\n\t" + "rep stosq\n\t" + "movl %[selector], %%ecx\n\t" + "pushq $0x8D7\n\t" + "popfq\n\t" + "call *%[code]\n\t" + "mov %%rax, %[rax]\n\t" + "mov %%rdx, %[rdx]\n\t" + "mov %%rcx, %[rcx]\n\t" + "pushfq\n\t" + "popq %[flags]\n\t" + "xor %%r8, %%r8\n\t" + "lea -136(%%rsp), %%rsi\n\t" + "mov $16, %%ecx\n\t" + "1:\n\t" + "xor (%%rsi), %%r8\n\t" + "add $8, %%rsi\n\t" + "dec %%ecx\n\t" + "jnz 1b\n\t" + "mov %%r8, %[canary]\n\t" + "add $128, %%rsp\n\t" + : [rax] "=r"(raxOut), [rdx] "=r"(rdxOut), [rcx] "=r"(rcxOut), [flags] "=r"(flagsOut), [canary] "=r"(canaryOut) + : [selector] "r"(selector), [code] "r"(code) + : "rax", "rcx", "rdx", "rsi", "rdi", "r8", "memory", "cc"); + require(canaryOut == 0, "RDPRU stub clobbered the red zone"); + require(rcxOut == selector, "RDPRU stub changed ecx"); + require((flagsOut & 0x8D5) == (0x8D7 & 0x8D5), "RDPRU stub changed RFLAGS"); + if (selector <= 1) + require((raxOut | rdxOut) != 0, "RDPRU MPERF/APERF selector did not yield the TSC approximation"); + else + require(raxOut == 0 && rdxOut == 0, "RDPRU with an undefined selector did not yield zero"); + } + munmap(code, 4096); +} + +void mcommitExecution() { + const auto matcher = Codegen::MakeAmd64OnlyInstructionMatcher(); + const Bytes site = {0xF3, 0x0F, 0x01, 0xFA}; + const auto match = matcher->Match(site.data(), site.size()); + require(match && match->Lowering == Codegen::Amd64OnlyLowering::InPlace, "MCOMMIT lowering was not produced"); + auto body = match->ReplacementBytes; + body.push_back(0xC3); + void* code = mmap(nullptr, 4096, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + require(code != MAP_FAILED, "cannot map executable memory for the MCOMMIT lowering"); + std::memcpy(code, body.data(), body.size()); + for (const std::uint64_t flagsIn : {std::uint64_t{0x8D6}, std::uint64_t{0x8D7}}) { + std::uint64_t flagsOut = 0; + asm volatile( + "mov %[in], %%rax\n\t" + "pushq %%rax\n\t" + "popfq\n\t" + "call *%[code]\n\t" + "pushfq\n\t" + "popq %[out]\n\t" + : [out] "=r"(flagsOut) + : [in] "r"(flagsIn), [code] "r"(code) + : "rax", "memory", "cc"); + require((flagsOut & 1) == 1, "MCOMMIT lowering did not report committed"); + require((flagsOut & 0x8D4) == (flagsIn & 0x8D4), "MCOMMIT lowering changed flags other than CF"); + } + munmap(code, 4096); +} #else void reciprocalExecution() {} void registerFormExecution() {} @@ -1802,6 +1898,8 @@ void sha256Execution() {} void clzeroExecution() {} void immediateFormExecution() {} void ripRelativeExecution() {} +void rdpruExecution() {} +void mcommitExecution() {} #endif #if defined(__x86_64__) && (defined(__linux__) || defined(__APPLE__)) @@ -1867,6 +1965,8 @@ int main() { clzeroExecution(); reciprocalExecution(); ripRelativeExecution(); + rdpruExecution(); + mcommitExecution(); converterSegment(); converterRipRelativeFollower(); converterSha256(); diff --git a/docs/dev/debt/relinker.md b/docs/dev/debt/relinker.md index d49cedbfcb..daed55afa1 100644 --- a/docs/dev/debt/relinker.md +++ b/docs/dev/debt/relinker.md @@ -11,7 +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. -- `--to-intel` does not lower RDPRU/MCOMMIT; the [matcher](../../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp) fails the relink instead. +- `--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. - 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 0018091176ef9b0a6d989ce101ed68924b346488 Mon Sep 17 00:00:00 2001 From: Alexandros Pournaras Date: Mon, 19 Oct 2026 18:38:34 +0300 Subject: [PATCH 180/269] fix(libSceVideoOut): count window-title FPS on the host clock --- core/libs/prx/libSceVideoOut/src/DisplayWindow.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceVideoOut/src/DisplayWindow.cpp b/core/libs/prx/libSceVideoOut/src/DisplayWindow.cpp index 751d3706fd..35c6c86917 100644 --- a/core/libs/prx/libSceVideoOut/src/DisplayWindow.cpp +++ b/core/libs/prx/libSceVideoOut/src/DisplayWindow.cpp @@ -3,9 +3,9 @@ #include "prx/libSceAgcDriver/Execution/include/AspectFit.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" #include "prx/libkernel/AppMetadata/include/AppMetadata.hpp" -#include "prx/libkernel/Time/include/Time.hpp" #include "Decoder/Png.hpp" #include "SDL_vulkan.h" +#include #include #include #include @@ -107,15 +107,15 @@ void DisplayWindow::DrawableSize(std::uint32_t& width, std::uint32_t& height) co void DisplayWindow::UpdateTitle() { require(window != nullptr, "window must exist before updating title"); static const AppTitle title = GetAppTitle_nid_postfix(); - static std::uint64_t fpsStart = sceKernelGetProcessTimeCounter(); + static auto fpsStart = std::chrono::steady_clock::now(); static std::uint64_t fpsFrames = 0; static double currentFps = 0.0; - const auto now = sceKernelGetProcessTimeCounter(); - const auto frequency = sceKernelGetProcessTimeCounterFrequency(); + const auto now = std::chrono::steady_clock::now(); frameNum++; fpsFrames++; - if (now - fpsStart >= frequency * 2) { - currentFps = static_cast(fpsFrames) * static_cast(frequency) / static_cast(now - fpsStart); + const double elapsed = std::chrono::duration(now - fpsStart).count(); + if (elapsed >= 2.0) { + currentFps = static_cast(fpsFrames) / elapsed; fpsStart = now; fpsFrames = 0; } From 86093098635770a348ec21e800ae9dd63ae210d7 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Tue, 20 Oct 2026 00:27:22 +0200 Subject: [PATCH 181/269] fix(tools): match import_audit module names like the relinker Co-Authored-By: Claude Sonnet 5.5 --- tools/import_audit.py | 25 +++++++++++++++++++++++++ tools/tests/test_import_audit.py | 20 ++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/tools/import_audit.py b/tools/import_audit.py index 4887d6d288..b257380a50 100644 --- a/tools/import_audit.py +++ b/tools/import_audit.py @@ -534,6 +534,30 @@ def check_conservation(references, records): raise AuditError(f"conservation failed: {references} references in, {classified} classified, unknown classes {unknown}") +MODULE_FILE_SUFFIXES = (".debug_prx", ".suprx", ".sprx", ".prx") + + +def module_stem(name, fold): + for suffix in MODULE_FILE_SUFFIXES: + if len(name) > len(suffix) and name.endswith(suffix): + name = name[:-len(suffix)] + break + return name.lower() if fold else name + + +class ModuleNames(set): + """Title module names, matched like the relinker: by the name without its .prx, .sprx, .suprx or .debug_prx suffix, and + ignoring case for Windows output.""" + + def __init__(self, names=(), fold=False): + super().__init__(names) + self.fold = fold + self.stems = {module_stem(name, fold) for name in self} + + def __contains__(self, library): + return isinstance(library, str) and module_stem(library, self.fold) in self.stems + + def classify(nid, library, providers, stubs, modules): if library and library in modules: return "module" @@ -731,6 +755,7 @@ def main(argv=None): module_imports, module_exports = read_modules(args.modules) modules = set(module_imports) modules |= {name.removesuffix(".guest.prx") for name in modules if name.endswith(".guest.prx")} + modules = ModuleNames(modules, fold=bool(windows)) records, missing = audit(imports, exports, source_stubs(args.source), libraries, modules, module_imports, module_exports, needed_libraries=needed) missing_hosts = audit_host_dependencies(host_dependencies, libraries, modules, windows) if args.names: diff --git a/tools/tests/test_import_audit.py b/tools/tests/test_import_audit.py index 98db7f5c7c..b020ba6890 100644 --- a/tools/tests/test_import_audit.py +++ b/tools/tests/test_import_audit.py @@ -585,6 +585,26 @@ class ImportAuditTests(unittest.TestCase): self.assertEqual(records[("AAAAAAAAAAA", "provider.data")]["providers"], ["libSceA.prx"]) self.assertEqual(records[("BBBBBBBBBBB", "consumer.prx")]["providers"], ["provider.data"]) + def test_module_matches_imports_that_name_another_module_suffix(self): + work = self.work + (work.libs / "libc.prx").write_bytes(exporting()) + (work.modules / "shipping.prx").write_bytes(exporting("AAAAAAAAAAA")) + registry, result = work.root / "game.json", work.root / "audit.json" + write_registry(registry, [("AAAAAAAAAAA", "shipping.debug_prx")]) + code, _, err = work.run(registry, "--json", str(result)) + self.assertEqual((code, err), (0, "")) + data = json.loads(result.read_text()) + self.assertEqual(data["missing_libraries"], {}) + record = next(record for record in data["imports"] if record["library"] == "shipping.debug_prx") + self.assertEqual(record["class"], "module") + + def test_module_names_ignore_case_for_windows_output_only(self): + imports = [("AAAAAAAAAAA", "SDK-Shipping.debug_prx", False)] + records, missing = import_audit.audit(imports, {}, {}, set(), import_audit.ModuleNames({"sdk-shipping.prx"}, fold=True)) + self.assertEqual((records[0]["class"], missing), ("module", {})) + records, missing = import_audit.audit(imports, {}, {}, set(), import_audit.ModuleNames({"sdk-shipping.prx"})) + self.assertEqual((records[0]["class"], missing), ("absent", {"SDK-Shipping.debug_prx": 1})) + def test_converted_module_matches_imports_of_its_base_name(self): work = self.work (work.libs / "libc.prx").write_bytes(exporting()) From 95ea06e80b543946f5566dd37dab89db503ab1a5 Mon Sep 17 00:00:00 2001 From: erickerxhalli <313650569+erickerxhalli@users.noreply.github.com> Date: Mon, 19 Oct 2026 22:31:58 +0000 Subject: [PATCH 182/269] fix(libSceMove): throw when opening for another user or an open slot sceMoveOpen returned a new handle for any user, type and index, although the console's result for a user other than the initial user and for a type and index that are already open is not known. Both now throw, as libSceHmd2 does for another user. Record which shadPS4 behaviour the handle and open checks add to. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Arbzg9c2DA4DEFKVSvRgYv --- core/libs/prx/libSceMove/Export.cpp | 20 +++++++++++++------- core/libs/tests/GuestMove.cpp | 7 ++++++- docs/dev/debt/libSceMove.md | 5 +++-- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libSceMove/Export.cpp b/core/libs/prx/libSceMove/Export.cpp index 340a60d887..df4c7fcf1a 100644 --- a/core/libs/prx/libSceMove/Export.cpp +++ b/core/libs/prx/libSceMove/Export.cpp @@ -1,10 +1,10 @@ -#include #include #include #include -#include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libSceUserService/UserService.hpp" namespace { @@ -19,7 +19,12 @@ constexpr int32_t MOVE_HANDLE_STEP = 0x100; std::mutex stateMutex; bool initialized = false; int32_t nextHandle = MOVE_FIRST_HANDLE; -std::unordered_set openHandles; +struct Slot { + int32_t type; + int32_t index; +}; + +std::unordered_map openHandles; int checkHandle(const char* function, int32_t handle) { std::lock_guard lock(stateMutex); @@ -48,14 +53,15 @@ int APS5_VABI sceMoveTerm(void) { } int APS5_VABI sceMoveOpen(int32_t userId, int32_t type, int32_t index) { - (void)userId; - (void)type; - (void)index; std::lock_guard lock(stateMutex); if (!initialized) return MOVE_ERROR_NOT_INIT; + if (userId != USER_SERVICE_INITIAL_USER_ID) NotImplemented_nid_no_patch(__func__); + for (const auto& [openHandle, slot] : openHandles) { + if (slot.type == type && slot.index == index) NotImplemented_nid_no_patch(__func__); + } const int32_t handle = nextHandle; nextHandle += MOVE_HANDLE_STEP; - openHandles.insert(handle); + openHandles.emplace(handle, Slot{type, index}); return handle; } diff --git a/core/libs/tests/GuestMove.cpp b/core/libs/tests/GuestMove.cpp index 6d35288a5a..db2b1bc998 100644 --- a/core/libs/tests/GuestMove.cpp +++ b/core/libs/tests/GuestMove.cpp @@ -101,6 +101,8 @@ int main() { const std::int32_t second = sceMoveOpen(kUserId, 0, 1); Require(first == kFirstHandle); Require(second == kFirstHandle + kHandleStep); + Require(Throws([] { sceMoveOpen(kUserId, 0, 0); })); + Require(Throws([] { sceMoveOpen(kUserId + 1, 0, 2); })); Require(sceMoveGetDeviceInfo(first, nullptr) == kInvalidArg); Require(sceMoveGetExtensionPortInfo(first, nullptr) == kInvalidArg); @@ -125,6 +127,9 @@ int main() { Require(sceMoveClose(first) == kOk); RequireUnknownHandleThrows(first); + const std::int32_t reopened = sceMoveOpen(kUserId, 0, 0); + Require(reopened == kFirstHandle + 2 * kHandleStep); + Require(sceMoveClose(reopened) == kOk); Require(sceMoveSetVibration(second, 0) == kNoControllerConnected); Require(sceMoveTerm() == kOk); @@ -133,7 +138,7 @@ int main() { Require(sceMoveInit() == kOk); RequireUnknownHandleThrows(second); const std::int32_t third = sceMoveOpen(kUserId, 0, 0); - Require(third == kFirstHandle + 2 * kHandleStep); + Require(third == kFirstHandle + 3 * kHandleStep); Require(sceMoveClose(third) == kOk); Require(sceMoveTerm() == kOk); } diff --git a/docs/dev/debt/libSceMove.md b/docs/dev/debt/libSceMove.md index 8ef70c92b6..ec7666f106 100644 --- a/docs/dev/debt/libSceMove.md +++ b/docs/dev/debt/libSceMove.md @@ -2,8 +2,9 @@ ### Silent stubs -- [libSceMove](../../../core/libs/prx/libSceMove/Export.cpp) implements only the no-controller path: initialization and termination succeed, `sceMoveOpen` returns a new handle for any user, type and index, `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 +- [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), not 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`). A handle that was never opened, is closed or was opened before `sceMoveTerm` throws: the console's `INVALID_HANDLE` (`0x80EE0004`) result and its order against `INVALID_ARG` are not known. Whether `sceMoveOpen` rejects an invalid user or a slot that is already open is not known. Closes when: measured on a PS5 +- [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` throws: the console's `INVALID_HANDLE` (`0x80EE0004`) result and its order against `INVALID_ARG` are not known. `sceMoveOpen` throws for a user other than the initial user and for a type and index that are already open, whose console results are not known, and accepts any type and index. Closes when: measured on a PS5 From 5f6dee64c494f3e3ee5100a26e80f4a7ae49a1da Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Mon, 19 Oct 2026 18:55:33 +0200 Subject: [PATCH 183/269] fix(libSceRtc): make sceRtcTickAdd* wrap and ignore range like the console The seven fixed-unit additions (Ticks, Microseconds, Seconds, Minutes, Hours, Days, Weeks) now only check the two pointers and store the source plus the amount modulo 2^64; they no longer return 0x80B50003 for a source above 9999-12-31 or a result outside the tick range. sceRtcTickAddMonths and sceRtcTickAddYears no longer reject a source tick above the 9999 limit. They split the tick into a 16-bit year, add the months in 32 bits and divide by 12 truncating toward zero. They return 0 without writing when the 16-bit target year is outside 1 to 9999 or the month sum is negative and not a multiple of 12; otherwise they write the result. Observed by executing the code of libSceRtc.sprx from PS5 firmware 3.20 and 4.03 in an emulator with its imports mocked (entry points 0x32c0-0x3440, 0x3470, 0x37f0 on 3.20; 0x3370-0x34f0, 0x3520, 0x3890 on 4.03), not on a console. Both firmwares give identical results on the same inputs. Current firmware was not checked. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libSceRtc/Export.cpp | 38 ++-- core/libs/tests/GuestRtc.cpp | 292 +++++++++++++++++++++++++---- 2 files changed, 281 insertions(+), 49 deletions(-) diff --git a/core/libs/prx/libSceRtc/Export.cpp b/core/libs/prx/libSceRtc/Export.cpp index 629ee77fa5..27c11fea51 100644 --- a/core/libs/prx/libSceRtc/Export.cpp +++ b/core/libs/prx/libSceRtc/Export.cpp @@ -126,16 +126,22 @@ int addTicks(RtcTick* dst, const RtcTick* src, std::int64_t count, std::int64_t return 0; } -int addMonths(RtcTick* dst, const RtcTick* src, std::int64_t months) { +int addTicksWrapping(RtcTick* dst, const RtcTick* src, std::int64_t count, std::int64_t unit) { + if (!dst || !src) return SCE_RTC_ERROR_INVALID_POINTER; + dst->tick = src->tick + static_cast(count) * static_cast(unit); + return 0; +} + +int addMonths(RtcTick* dst, const RtcTick* src, std::int32_t months) { if (!dst || !src) return SCE_RTC_ERROR_INVALID_POINTER; - if (src->tick > MAX_TICK) return SCE_RTC_ERROR_INVALID_VALUE; RtcDateTime time = fromTick(src->tick); - const std::int64_t monthIndex = time.year * 12 + (time.month - 1) + months; - const std::int64_t year = monthIndex / 12; - if (monthIndex < 12 || year > 9999) return SCE_RTC_ERROR_INVALID_VALUE; - time.year = static_cast(year); - time.month = static_cast(monthIndex % 12 + 1); - const int lastDay = daysInMonth(time.year, time.month); + const auto index = static_cast(static_cast(time.year) * 12u + time.month + static_cast(months) - 1u); + const auto year = static_cast(index / 12); + const std::int32_t month = index % 12; + if (year < 1 || year > 9999 || month < 0) return 0; + time.year = year; + time.month = static_cast(month + 1); + const int lastDay = daysInMonth(year, time.month); if (time.day > lastDay) time.day = static_cast(lastDay); dst->tick = toTick(time); return 0; @@ -595,31 +601,31 @@ int APS5_VABI sceRtcParseDateTime(RtcTick* utc, const char* date_time) { } int APS5_VABI sceRtcTickAddTicks(RtcTick* dst, const RtcTick* src, int64_t ticks) { - return addTicks(dst, src, ticks, 1); + return addTicksWrapping(dst, src, ticks, 1); } int APS5_VABI sceRtcTickAddMicroseconds(RtcTick* dst, const RtcTick* src, int64_t usec) { - return addTicks(dst, src, usec, 1); + return addTicksWrapping(dst, src, usec, 1); } int APS5_VABI sceRtcTickAddSeconds(RtcTick* dst, const RtcTick* src, int64_t seconds) { - return addTicks(dst, src, seconds, TICKS_PER_SECOND); + return addTicksWrapping(dst, src, seconds, TICKS_PER_SECOND); } int APS5_VABI sceRtcTickAddMinutes(RtcTick* dst, const RtcTick* src, int64_t minutes) { - return addTicks(dst, src, minutes, TICKS_PER_MINUTE); + return addTicksWrapping(dst, src, minutes, TICKS_PER_MINUTE); } int APS5_VABI sceRtcTickAddHours(RtcTick* dst, const RtcTick* src, int32_t hours) { - return addTicks(dst, src, hours, TICKS_PER_HOUR); + return addTicksWrapping(dst, src, hours, TICKS_PER_HOUR); } int APS5_VABI sceRtcTickAddDays(RtcTick* dst, const RtcTick* src, int32_t days) { - return addTicks(dst, src, days, TICKS_PER_DAY); + return addTicksWrapping(dst, src, days, TICKS_PER_DAY); } int APS5_VABI sceRtcTickAddWeeks(RtcTick* dst, const RtcTick* src, int32_t weeks) { - return addTicks(dst, src, weeks, 7 * TICKS_PER_DAY); + return addTicksWrapping(dst, src, weeks, 7 * TICKS_PER_DAY); } int APS5_VABI sceRtcTickAddMonths(RtcTick* dst, const RtcTick* src, int32_t months) { @@ -627,7 +633,7 @@ int APS5_VABI sceRtcTickAddMonths(RtcTick* dst, const RtcTick* src, int32_t mont } int APS5_VABI sceRtcTickAddYears(RtcTick* dst, const RtcTick* src, int16_t years) { - return addMonths(dst, src, static_cast(years) * 12); + return addMonths(dst, src, static_cast(years) * 12); } int APS5_VABI sceRtcInit(void) { diff --git a/core/libs/tests/GuestRtc.cpp b/core/libs/tests/GuestRtc.cpp index a1d820e72d..02a9ede36b 100644 --- a/core/libs/tests/GuestRtc.cpp +++ b/core/libs/tests/GuestRtc.cpp @@ -80,6 +80,31 @@ static bool Equal(const RtcDateTime& left, const RtcDateTime& right) { && left.minute == right.minute && left.second == right.second && left.microsecond == right.microsecond; } +enum class Add { Ticks, Microseconds, Seconds, Minutes, Hours, Days, Weeks, Months, Years }; + +struct AddCase { + Add function; + std::uint64_t source; + std::int64_t amount; + bool written; + std::uint64_t result; +}; + +static int CallAdd(Add function, RtcTick* destination, const RtcTick* source, std::int64_t amount) { + switch (function) { + case Add::Ticks: return sceRtcTickAddTicks(destination, source, amount); + case Add::Microseconds: return sceRtcTickAddMicroseconds(destination, source, amount); + case Add::Seconds: return sceRtcTickAddSeconds(destination, source, amount); + case Add::Minutes: return sceRtcTickAddMinutes(destination, source, amount); + case Add::Hours: return sceRtcTickAddHours(destination, source, static_cast(amount)); + case Add::Days: return sceRtcTickAddDays(destination, source, static_cast(amount)); + case Add::Weeks: return sceRtcTickAddWeeks(destination, source, static_cast(amount)); + case Add::Months: return sceRtcTickAddMonths(destination, source, static_cast(amount)); + case Add::Years: return sceRtcTickAddYears(destination, source, static_cast(amount)); + } + std::abort(); +} + int main() { constexpr int invalidPointer = static_cast(0x80B50002); constexpr int invalidValue = static_cast(0x80B50003); @@ -431,12 +456,9 @@ int main() { Require(sceRtcGetTick(&endOfJanuary, &source) == 0); Require(sceRtcTickAddMonths(&result, &source, 1) == 0); Require(sceRtcSetTick(&converted, &result) == 0 && Equal(converted, RtcDateTime{2024, 2, 29, 8, 0, 0, 0})); - Require(sceRtcTickAddMonths(&result, &source, -12 * 2024) == invalidValue); source.tick = 5; - Require(sceRtcTickAddTicks(&result, &source, -6) == invalidValue); Require(sceRtcTickAddTicks(&result, &source, -5) == 0 && result.tick == 0); source.tick = maxTick; - Require(sceRtcTickAddTicks(&result, &source, 1) == invalidValue); Require(sceRtcTickAddTicks(nullptr, &source, 1) == invalidPointer); source.tick = leapDayTick; result.tick = 123; @@ -446,33 +468,8 @@ int main() { Require(sceRtcTickAddWeeks(&result, &source, 1) == 0 && result.tick == leapDayTick + 604800000000ull); Require(sceRtcSetTick(&converted, &result) == 0 && converted.month == 3 && converted.day == 7); result.tick = 123; - Require(sceRtcTickAddMinutes(&result, &source, std::numeric_limits::max()) == invalidValue && result.tick == 123); - Require(sceRtcTickAddWeeks(&result, &source, std::numeric_limits::max()) == invalidValue && result.tick == 123); Require(sceRtcTickAddHours(nullptr, &source, 1) == invalidPointer); Require(sceRtcTickAddMicroseconds(&result, nullptr, 1) == invalidPointer); - source.tick = maxTick; - Require(sceRtcTickAddHours(&result, &source, 1) == invalidValue && result.tick == 123); - Require(sceRtcTickAddMicroseconds(&result, &source, 1) == invalidValue && result.tick == 123); - source.tick = 0; - Require(sceRtcTickAddMinutes(&result, &source, -1) == invalidValue && result.tick == 123); - - const std::uint64_t invalidTicks[] = {maxTick + 1, std::numeric_limits::max()}; - for (std::uint64_t invalidTick : invalidTicks) { - source.tick = invalidTick; - result.tick = 123; - Require(sceRtcTickAddTicks(&result, &source, 0) == invalidValue && result.tick == 123); - Require(sceRtcTickAddTicks(&result, &source, 1) == invalidValue && result.tick == 123); - Require(sceRtcTickAddTicks(&result, &source, -1) == invalidValue && result.tick == 123); - Require(sceRtcTickAddSeconds(&result, &source, 0) == invalidValue && result.tick == 123); - Require(sceRtcTickAddDays(&result, &source, -1) == invalidValue && result.tick == 123); - Require(sceRtcTickAddMonths(&result, &source, 0) == invalidValue && result.tick == 123); - Require(sceRtcTickAddYears(&result, &source, -1) == invalidValue && result.tick == 123); - Require(sceRtcTickAddTicks(&source, &source, 0) == invalidValue && source.tick == invalidTick); - Require(sceRtcTickAddMonths(&source, &source, -1) == invalidValue && source.tick == invalidTick); - Require(sceRtcTickAddTicks(nullptr, &source, 0) == invalidPointer); - Require(sceRtcTickAddMonths(nullptr, &source, 0) == invalidPointer); - } - source.tick = maxTick; Require(sceRtcTickAddTicks(&source, &source, 0) == 0 && source.tick == maxTick); Require(sceRtcTickAddTicks(&source, &source, -1) == 0 && source.tick == maxTick - 1); @@ -480,13 +477,242 @@ int main() { Require(sceRtcTickAddMonths(&source, &source, 0) == 0 && source.tick == maxTick); Require(sceRtcTickAddYears(&result, &source, -1) == 0); Require(sceRtcSetTick(&converted, &result) == 0 && Equal(converted, RtcDateTime{9998, 12, 31, 23, 59, 59, 999999})); - result.tick = 123; - Require(sceRtcTickAddMonths(&result, &source, 1) == invalidValue && result.tick == 123); - Require(sceRtcTickAddTicks(&result, &source, std::numeric_limits::min()) == invalidValue && result.tick == 123); - Require(sceRtcTickAddSeconds(&result, &source, std::numeric_limits::max()) == invalidValue && result.tick == 123); source.tick = 0; Require(sceRtcTickAddMonths(&source, &source, 0) == 0 && source.tick == 0); - Require(sceRtcTickAddMonths(&result, &source, -1) == invalidValue && result.tick == 123); + + constexpr std::int64_t min64 = std::numeric_limits::min(); + constexpr std::int64_t max64 = std::numeric_limits::max(); + constexpr std::uint64_t addSentinel = 0xA5A5A5A5A5A5A5A5ull; + const AddCase addCases[] = { + {Add::Ticks, 0, -1, true, 0xffffffffffffffffull}, + {Add::Ticks, 1ull, -1, true, 0x0000000000000000ull}, + {Add::Ticks, 5, -1, true, 0x0000000000000004ull}, + {Add::Ticks, leapDayTick, 1, true, 0x00e2d2837bd62609ull}, + {Add::Ticks, maxTick - 1ull, 1, true, 0x0461040bcb9f1fffull}, + {Add::Ticks, maxTick, 1, true, 0x0461040bcb9f2000ull}, + {Add::Ticks, maxTick + 1, 1, true, 0x0461040bcb9f2001ull}, + {Add::Ticks, ~0ull, 1, true, 0x0000000000000000ull}, + {Add::Ticks, 0, max64, true, 0x7fffffffffffffffull}, + {Add::Ticks, 0, min64, true, 0x8000000000000000ull}, + {Add::Ticks, maxTick, max64, true, 0x8461040bcb9f1ffeull}, + {Add::Ticks, ~0ull, min64, true, 0x7fffffffffffffffull}, + {Add::Microseconds, 0, -1, true, 0xffffffffffffffffull}, + {Add::Microseconds, 1ull, -1, true, 0x0000000000000000ull}, + {Add::Microseconds, 5, -1, true, 0x0000000000000004ull}, + {Add::Microseconds, leapDayTick, 1, true, 0x00e2d2837bd62609ull}, + {Add::Microseconds, maxTick - 1ull, 1, true, 0x0461040bcb9f1fffull}, + {Add::Microseconds, maxTick, 1, true, 0x0461040bcb9f2000ull}, + {Add::Microseconds, maxTick + 1, 1, true, 0x0461040bcb9f2001ull}, + {Add::Microseconds, ~0ull, 1, true, 0x0000000000000000ull}, + {Add::Microseconds, 0, max64, true, 0x7fffffffffffffffull}, + {Add::Microseconds, 0, min64, true, 0x8000000000000000ull}, + {Add::Microseconds, maxTick, max64, true, 0x8461040bcb9f1ffeull}, + {Add::Microseconds, ~0ull, min64, true, 0x7fffffffffffffffull}, + {Add::Seconds, 0, -1, true, 0xfffffffffff0bdc0ull}, + {Add::Seconds, 1000000ull, -1, true, 0x0000000000000000ull}, + {Add::Seconds, 999999ull, -1, true, 0xffffffffffffffffull}, + {Add::Seconds, leapDayTick, 1, true, 0x00e2d2837be56848ull}, + {Add::Seconds, maxTick - 1000000ull, 1, true, 0x0461040bcb9f1fffull}, + {Add::Seconds, maxTick + 1 - 1000000ull, 1, true, 0x0461040bcb9f2000ull}, + {Add::Seconds, maxTick + 1, 1, true, 0x0461040bcbae6240ull}, + {Add::Seconds, 0ull - 1000000ull, 1, true, 0x0000000000000000ull}, + {Add::Seconds, 0, max64, true, 0xfffffffffff0bdc0ull}, + {Add::Seconds, 0, min64, true, 0x0000000000000000ull}, + {Add::Seconds, maxTick, max64, true, 0x0461040bcb8fddbfull}, + {Add::Seconds, ~0ull, min64, true, 0xffffffffffffffffull}, + {Add::Minutes, 0, -1, true, 0xfffffffffc6c7900ull}, + {Add::Minutes, 60000000ull, -1, true, 0x0000000000000000ull}, + {Add::Minutes, 59999999ull, -1, true, 0xffffffffffffffffull}, + {Add::Minutes, leapDayTick, 1, true, 0x00e2d2837f69ad08ull}, + {Add::Minutes, maxTick - 60000000ull, 1, true, 0x0461040bcb9f1fffull}, + {Add::Minutes, maxTick + 1 - 60000000ull, 1, true, 0x0461040bcb9f2000ull}, + {Add::Minutes, maxTick + 1, 1, true, 0x0461040bcf32a700ull}, + {Add::Minutes, 0ull - 60000000ull, 1, true, 0x0000000000000000ull}, + {Add::Minutes, 0, max64, true, 0xfffffffffc6c7900ull}, + {Add::Minutes, 0, min64, true, 0x0000000000000000ull}, + {Add::Minutes, maxTick, max64, true, 0x0461040bc80b98ffull}, + {Add::Minutes, ~0ull, min64, true, 0xffffffffffffffffull}, + {Add::Hours, 0, -1, true, 0xffffffff296c5c00ull}, + {Add::Hours, 3600000000ull, -1, true, 0x0000000000000000ull}, + {Add::Hours, 3599999999ull, -1, true, 0xffffffffffffffffull}, + {Add::Hours, leapDayTick, 1, true, 0x00e2d2845269ca08ull}, + {Add::Hours, maxTick - 3600000000ull, 1, true, 0x0461040bcb9f1fffull}, + {Add::Hours, maxTick + 1 - 3600000000ull, 1, true, 0x0461040bcb9f2000ull}, + {Add::Hours, maxTick + 1, 1, true, 0x0461040ca232c400ull}, + {Add::Hours, 0ull - 3600000000ull, 1, true, 0x0000000000000000ull}, + {Add::Hours, 0, 2147483647, true, 0x6b49d1ff296c5c00ull}, + {Add::Hours, 0, -2147483648, true, 0x94b62e0000000000ull}, + {Add::Hours, maxTick, 2147483647, true, 0x6faad60af50b7bffull}, + {Add::Hours, ~0ull, -2147483648, true, 0x94b62dffffffffffull}, + {Add::Days, 0, -1, true, 0xffffffebe228a000ull}, + {Add::Days, 86400000000ull, -1, true, 0x0000000000000000ull}, + {Add::Days, 86399999999ull, -1, true, 0xffffffffffffffffull}, + {Add::Days, leapDayTick, 1, true, 0x00e2d29799ad8608ull}, + {Add::Days, maxTick - 86400000000ull, 1, true, 0x0461040bcb9f1fffull}, + {Add::Days, maxTick + 1 - 86400000000ull, 1, true, 0x0461040bcb9f2000ull}, + {Add::Days, maxTick + 1, 1, true, 0x0461041fe9768000ull}, + {Add::Days, 0ull - 86400000000ull, 1, true, 0x0000000000000000ull}, + {Add::Days, 0, 2147483647, true, 0x0eebafebe228a000ull}, + {Add::Days, 0, -2147483648, true, 0xf114500000000000ull}, + {Add::Days, maxTick, 2147483647, true, 0x134cb3f7adc7bfffull}, + {Add::Days, ~0ull, -2147483648, true, 0xf1144fffffffffffull}, + {Add::Weeks, 0, -1, true, 0xffffff732f1c6000ull}, + {Add::Weeks, 604800000000ull, -1, true, 0x0000000000000000ull}, + {Add::Weeks, 604799999999ull, -1, true, 0xffffffffffffffffull}, + {Add::Weeks, leapDayTick, 1, true, 0x00e2d3104cb9c608ull}, + {Add::Weeks, maxTick - 604800000000ull, 1, true, 0x0461040bcb9f1fffull}, + {Add::Weeks, maxTick + 1 - 604800000000ull, 1, true, 0x0461040bcb9f2000ull}, + {Add::Weeks, maxTick + 1, 1, true, 0x046104989c82c000ull}, + {Add::Weeks, 0ull - 604800000000ull, 1, true, 0x0000000000000000ull}, + {Add::Weeks, 0, 2147483647, true, 0x6871cf732f1c6000ull}, + {Add::Weeks, 0, -2147483648, true, 0x978e300000000000ull}, + {Add::Weeks, maxTick, 2147483647, true, 0x6cd2d37efabb7fffull}, + {Add::Weeks, ~0ull, -2147483648, true, 0x978e2fffffffffffull}, + {Add::Months, 0, -1, false, 0}, + {Add::Months, 0, 0, true, 0x0000000000000000ull}, + {Add::Months, 0, 1, true, 0x0000026f9d14a000ull}, + {Add::Months, 0, -12, false, 0}, + {Add::Months, 0, 12, true, 0x00001cae8c13e000ull}, + {Add::Months, 0, 119988, false, 0}, + {Add::Months, 0, 2147483647, false, 0}, + {Add::Months, 0, -2147483648, false, 0}, + {Add::Months, 0, -1239376452, true, 0x017942765fc16000ull}, + {Add::Months, 0, -1239376451, false, 0}, + {Add::Months, at(1, 2, 1, 0, 0, 0), -1, true, 0x0000000000000000ull}, + {Add::Months, at(1, 2, 1, 0, 0, 0), 0, true, 0x0000026f9d14a000ull}, + {Add::Months, at(1, 2, 1, 0, 0, 0), 1, true, 0x000004a2e0a32000ull}, + {Add::Months, at(1, 2, 1, 0, 0, 0), -12, false, 0}, + {Add::Months, at(1, 2, 1, 0, 0, 0), 12, true, 0x00001f1e29288000ull}, + {Add::Months, at(1, 2, 1, 0, 0, 0), 119988, false, 0}, + {Add::Months, at(1, 2, 1, 0, 0, 0), 2147483647, false, 0}, + {Add::Months, at(1, 2, 1, 0, 0, 0), -2147483648, false, 0}, + {Add::Months, at(1, 2, 1, 0, 0, 0), -1239376452, false, 0}, + {Add::Months, at(1, 2, 1, 0, 0, 0), -1239376451, false, 0}, + {Add::Months, at(2024, 1, 31, 8, 0, 0), -1, true, 0x00e2cdc8a612a000ull}, + {Add::Months, at(2024, 1, 31, 8, 0, 0), 0, true, 0x00e2d03843274000ull}, + {Add::Months, at(2024, 1, 31, 8, 0, 0), 1, true, 0x00e2d27fa48d2000ull}, + {Add::Months, at(2024, 1, 31, 8, 0, 0), -12, true, 0x00e2b389b7136000ull}, + {Add::Months, at(2024, 1, 31, 8, 0, 0), 12, true, 0x00e2ecfaed128000ull}, + {Add::Months, at(2024, 1, 31, 8, 0, 0), 119988, false, 0}, + {Add::Months, at(2024, 1, 31, 8, 0, 0), 2147483647, false, 0}, + {Add::Months, at(2024, 1, 31, 8, 0, 0), -2147483648, false, 0}, + {Add::Months, at(2024, 1, 31, 8, 0, 0), -1239376452, true, 0x025c12c2c0c00000ull}, + {Add::Months, at(2024, 1, 31, 8, 0, 0), -1239376451, false, 0}, + {Add::Months, leapDayTick, -1, true, 0x00e2d013dec18608ull}, + {Add::Months, leapDayTick, 0, true, 0x00e2d2837bd62608ull}, + {Add::Months, leapDayTick, 1, true, 0x00e2d4cadd3c0608ull}, + {Add::Months, leapDayTick, -12, true, 0x00e2b5c0d1eae608ull}, + {Add::Months, leapDayTick, 12, true, 0x00e2ef3207ea0608ull}, + {Add::Months, leapDayTick, 119988, false, 0}, + {Add::Months, leapDayTick, 2147483647, false, 0}, + {Add::Months, leapDayTick, -2147483648, false, 0}, + {Add::Months, leapDayTick, -1239376452, false, 0}, + {Add::Months, leapDayTick, -1239376451, false, 0}, + {Add::Months, at(9999, 1, 1, 0, 0, 0), -1, true, 0x0460e4eda276a000ull}, + {Add::Months, at(9999, 1, 1, 0, 0, 0), 0, true, 0x0460e75d3f8b4000ull}, + {Add::Months, at(9999, 1, 1, 0, 0, 0), 1, true, 0x0460e9ccdc9fe000ull}, + {Add::Months, at(9999, 1, 1, 0, 0, 0), -12, true, 0x0460caaeb3776000ull}, + {Add::Months, at(9999, 1, 1, 0, 0, 0), 12, false, 0}, + {Add::Months, at(9999, 1, 1, 0, 0, 0), 119988, false, 0}, + {Add::Months, at(9999, 1, 1, 0, 0, 0), 2147483647, false, 0}, + {Add::Months, at(9999, 1, 1, 0, 0, 0), -2147483648, false, 0}, + {Add::Months, at(9999, 1, 1, 0, 0, 0), -1239376452, false, 0}, + {Add::Months, at(9999, 1, 1, 0, 0, 0), -1239376451, false, 0}, + {Add::Months, maxTick, -1, true, 0x0461019c2e8a7fffull}, + {Add::Months, maxTick, 0, true, 0x0461040bcb9f1fffull}, + {Add::Months, maxTick, 1, false, 0}, + {Add::Months, maxTick, -12, true, 0x0460e75d3f8b3fffull}, + {Add::Months, maxTick, 12, false, 0}, + {Add::Months, maxTick, 119988, false, 0}, + {Add::Months, maxTick, 2147483647, false, 0}, + {Add::Months, maxTick, -2147483648, false, 0}, + {Add::Months, maxTick, -1239376452, false, 0}, + {Add::Months, maxTick, -1239376451, false, 0}, + {Add::Months, maxTick + 1, -1, true, 0x0461019c2e8a8000ull}, + {Add::Months, maxTick + 1, 0, false, 0}, + {Add::Months, maxTick + 1, 1, false, 0}, + {Add::Months, maxTick + 1, -12, true, 0x0460e75d3f8b4000ull}, + {Add::Months, maxTick + 1, 12, false, 0}, + {Add::Months, maxTick + 1, 119988, false, 0}, + {Add::Months, maxTick + 1, 2147483647, false, 0}, + {Add::Months, maxTick + 1, -2147483648, false, 0}, + {Add::Months, maxTick + 1, -1239376452, false, 0}, + {Add::Months, maxTick + 1, -1239376451, false, 0}, + {Add::Months, ~0ull, -1, false, 0}, + {Add::Months, ~0ull, 0, false, 0}, + {Add::Months, ~0ull, 1, false, 0}, + {Add::Months, ~0ull, -12, false, 0}, + {Add::Months, ~0ull, 12, false, 0}, + {Add::Months, ~0ull, 119988, true, 0x02122fd805921fffull}, + {Add::Months, ~0ull, 2147483647, false, 0}, + {Add::Months, ~0ull, -2147483648, false, 0}, + {Add::Months, ~0ull, -1239376452, false, 0}, + {Add::Months, ~0ull, -1239376451, false, 0}, + {Add::Years, 0, -1, false, 0}, + {Add::Years, 0, 0, true, 0x0000000000000000ull}, + {Add::Years, 0, 1, true, 0x00001cae8c13e000ull}, + {Add::Years, 0, 9998, true, 0x0460e75d3f8b4000ull}, + {Add::Years, 0, 32767, false, 0}, + {Add::Years, 0, -32768, false, 0}, + {Add::Years, at(1, 2, 1, 0, 0, 0), -1, false, 0}, + {Add::Years, at(1, 2, 1, 0, 0, 0), 0, true, 0x0000026f9d14a000ull}, + {Add::Years, at(1, 2, 1, 0, 0, 0), 1, true, 0x00001f1e29288000ull}, + {Add::Years, at(1, 2, 1, 0, 0, 0), 9998, true, 0x0460e9ccdc9fe000ull}, + {Add::Years, at(1, 2, 1, 0, 0, 0), 32767, false, 0}, + {Add::Years, at(1, 2, 1, 0, 0, 0), -32768, false, 0}, + {Add::Years, at(2024, 1, 31, 8, 0, 0), -1, true, 0x00e2b389b7136000ull}, + {Add::Years, at(2024, 1, 31, 8, 0, 0), 0, true, 0x00e2d03843274000ull}, + {Add::Years, at(2024, 1, 31, 8, 0, 0), 1, true, 0x00e2ecfaed128000ull}, + {Add::Years, at(2024, 1, 31, 8, 0, 0), 9998, false, 0}, + {Add::Years, at(2024, 1, 31, 8, 0, 0), 32767, false, 0}, + {Add::Years, at(2024, 1, 31, 8, 0, 0), -32768, false, 0}, + {Add::Years, leapDayTick, -1, true, 0x00e2b5c0d1eae608ull}, + {Add::Years, leapDayTick, 0, true, 0x00e2d2837bd62608ull}, + {Add::Years, leapDayTick, 1, true, 0x00e2ef3207ea0608ull}, + {Add::Years, leapDayTick, 9998, false, 0}, + {Add::Years, leapDayTick, 32767, false, 0}, + {Add::Years, leapDayTick, -32768, false, 0}, + {Add::Years, at(9999, 1, 1, 0, 0, 0), -1, true, 0x0460caaeb3776000ull}, + {Add::Years, at(9999, 1, 1, 0, 0, 0), 0, true, 0x0460e75d3f8b4000ull}, + {Add::Years, at(9999, 1, 1, 0, 0, 0), 1, false, 0}, + {Add::Years, at(9999, 1, 1, 0, 0, 0), 9998, false, 0}, + {Add::Years, at(9999, 1, 1, 0, 0, 0), 32767, false, 0}, + {Add::Years, at(9999, 1, 1, 0, 0, 0), -32768, false, 0}, + {Add::Years, maxTick, -1, true, 0x0460e75d3f8b3fffull}, + {Add::Years, maxTick, 0, true, 0x0461040bcb9f1fffull}, + {Add::Years, maxTick, 1, false, 0}, + {Add::Years, maxTick, 9998, false, 0}, + {Add::Years, maxTick, 32767, false, 0}, + {Add::Years, maxTick, -32768, false, 0}, + {Add::Years, maxTick + 1, -1, true, 0x0460e75d3f8b4000ull}, + {Add::Years, maxTick + 1, 0, false, 0}, + {Add::Years, maxTick + 1, 1, false, 0}, + {Add::Years, maxTick + 1, 9998, false, 0}, + {Add::Years, maxTick + 1, 32767, false, 0}, + {Add::Years, maxTick + 1, -32768, false, 0}, + {Add::Years, ~0ull, -1, false, 0}, + {Add::Years, ~0ull, 0, false, 0}, + {Add::Years, ~0ull, 1, false, 0}, + {Add::Years, ~0ull, 9998, true, 0x02121329797e3fffull}, + {Add::Years, ~0ull, 32767, false, 0}, + {Add::Years, ~0ull, -32768, false, 0}, + }; + for (const AddCase& entry : addCases) { + RtcTick destination{addSentinel}; + const RtcTick origin{entry.source}; + Require(CallAdd(entry.function, &destination, &origin, entry.amount) == 0); + Require(destination.tick == (entry.written ? entry.result : addSentinel)); + RtcTick inPlace{entry.source}; + Require(CallAdd(entry.function, &inPlace, &inPlace, entry.amount) == 0); + Require(inPlace.tick == (entry.written ? entry.result : entry.source)); + } + for (Add function : {Add::Ticks, Add::Microseconds, Add::Seconds, Add::Minutes, Add::Hours, Add::Days, Add::Weeks, Add::Months, Add::Years}) { + RtcTick destination{addSentinel}; + const RtcTick origin{5}; + Require(CallAdd(function, nullptr, &origin, 1) == invalidPointer); + Require(CallAdd(function, &destination, nullptr, 1) == invalidPointer && destination.tick == addSentinel); + Require(CallAdd(function, nullptr, nullptr, 1) == invalidPointer); + } RtcTick now{}; Require(sceRtcGetCurrentTick(&now) == 0 && now.tick > leapDayTick); From 0afb0c58dd3bec3ab2e43d610fe45b23f6f05bbc Mon Sep 17 00:00:00 2001 From: erickerxhalli <313650569+erickerxhalli@users.noreply.github.com> Date: Mon, 19 Oct 2026 23:07:47 +0000 Subject: [PATCH 184/269] test(libSceMove): register guest_move away from the netctl tests Move the guest_move registration in core/libs/CMakeLists.txt next to the voice tests so it no longer shares lines with the netctl access point tests that another open pull request adds. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Arbzg9c2DA4DEFKVSvRgYv --- core/libs/CMakeLists.txt | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 3823e0eef0..ffaa4e4c3a 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -354,6 +354,16 @@ if(BUILD_TESTING) "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) + configure_windows_unwind(guest_move_tests) + add_test(NAME guest_move COMMAND guest_move_tests) + if(WIN32) + set_tests_properties(guest_move PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_offline_imports_tests tests/GuestOfflineImports.cpp) target_include_directories(guest_offline_imports_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_offline_imports_tests PRIVATE libSceHttp libSceNpEntitlementAccess libSceRudp libc) @@ -398,16 +408,6 @@ if(BUILD_TESTING) "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) - configure_windows_unwind(guest_move_tests) - add_test(NAME guest_move COMMAND guest_move_tests) - if(WIN32) - set_tests_properties(guest_move PROPERTIES ENVIRONMENT_MODIFICATION - "PATH=path_list_prepend:$") - endif() - add_test_executable(guest_netctl_ap_dialog_tests tests/GuestNetCtlApDialog.cpp) target_include_directories(guest_netctl_ap_dialog_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_netctl_ap_dialog_tests PRIVATE libSceNetCtlApDialog libc) From 3f18855d3e1803cb024436c054fbb468e75c092b Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Tue, 20 Oct 2026 01:15:06 +0200 Subject: [PATCH 185/269] fix(tools): report an unusable build directory in check_test_registration Co-Authored-By: Claude Sonnet 5.5 --- tools/check_test_registration.py | 7 +++++-- tools/tests/test_test_registration.py | 21 +++++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/tools/check_test_registration.py b/tools/check_test_registration.py index da156692b5..e60d16422d 100644 --- a/tools/check_test_registration.py +++ b/tools/check_test_registration.py @@ -129,8 +129,11 @@ if __name__ == "__main__": parser.add_argument("build", type=Path, nargs="?", default=Path("build"), help="configured build directory") parser.add_argument("--list", action="store_true", help="print every test executable and whether ctest runs it") args = parser.parse_args() - every = sorted(built(args.build)) - run = registered(args.build) + try: + every = sorted(built(args.build)) + run = registered(args.build) + except (OSError, ValueError, subprocess.CalledProcessError) as error: + parser.error(str(error)) if args.list: for name in every: print(f"{'run' if name in run else 'NOT RUN'} {name}") diff --git a/tools/tests/test_test_registration.py b/tools/tests/test_test_registration.py index b53754fcb7..9cdba56fa3 100644 --- a/tools/tests/test_test_registration.py +++ b/tools/tests/test_test_registration.py @@ -1,6 +1,7 @@ from pathlib import Path import os import shutil +import subprocess import sys import tempfile import unittest @@ -236,5 +237,25 @@ class RegistrationTests(unittest.TestCase): check_test_registration.built(build) run.assert_called_once() + def run_tool(self, build): + return subprocess.run([sys.executable, "-I", str(Path(check_test_registration.__file__)), str(build)], + capture_output=True, text=True, timeout=30) + + def test_missing_build_directory_is_reported_without_a_traceback(self): + with tempfile.TemporaryDirectory() as directory: + result = self.run_tool(Path(directory) / "no build") + self.assertEqual(result.returncode, 2) + self.assertIn("error:", result.stderr) + self.assertIn("CMakeCache.txt", result.stderr) + self.assertNotIn("Traceback", result.stderr) + + def test_unsupported_generator_is_reported_without_a_traceback(self): + with tempfile.TemporaryDirectory() as directory: + (Path(directory) / "CMakeCache.txt").write_text("CMAKE_GENERATOR:INTERNAL=Unix Makefiles\n", encoding="utf-8") + result = self.run_tool(directory) + self.assertEqual(result.returncode, 2) + self.assertIn("requires a Ninja generator", result.stderr) + self.assertNotIn("Traceback", result.stderr) + if __name__ == "__main__": unittest.main() From e827ddf45e175a50232daecf7afb05381c965b1e Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Tue, 20 Oct 2026 01:17:14 +0200 Subject: [PATCH 186/269] fix(tools): name the unreadable snapshot in progress --compare Co-Authored-By: Claude Sonnet 5.5 --- tools/progress.py | 8 +++++++- tools/tests/test_progress.py | 14 ++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/tools/progress.py b/tools/progress.py index 72113313d8..6f8f165cb6 100644 --- a/tools/progress.py +++ b/tools/progress.py @@ -473,7 +473,13 @@ def main(argv=None): if args.compare: if args.module or args.json: parser.error("--compare does not combine with --module or --json") - base, head = (json.loads(path.read_text(encoding="utf-8")) for path in args.compare) + snapshots = [] + for path in args.compare: + try: + snapshots.append(json.loads(path.read_text(encoding="utf-8"))) + except (OSError, ValueError) as error: + parser.error(f"{path}: {error}") + base, head = snapshots print(report(base, head), end="") return 0 if not args.output and not (args.module or args.json): diff --git a/tools/tests/test_progress.py b/tools/tests/test_progress.py index 84c3479696..9755ae2139 100644 --- a/tools/tests/test_progress.py +++ b/tools/tests/test_progress.py @@ -387,5 +387,19 @@ class ProgressTests(unittest.TestCase): self.assertIn("--compare does not combine with --module or --json", stderr.getvalue()) + def test_compare_names_a_snapshot_it_cannot_read(self): + with tempfile.TemporaryDirectory() as directory: + good = Path(directory) / "good.json" + good.write_text("{}", encoding="utf-8") + broken = Path(directory) / "broken snapshot.json" + broken.write_text("not json", encoding="utf-8") + for name, path in (("invalid", broken), ("missing", Path(directory) / "missing snapshot.json")): + with self.subTest(name): + stderr = io.StringIO() + with patch("sys.stderr", stderr), self.assertRaises(SystemExit) as raised: + progress.main(["--compare", str(good), str(path)]) + self.assertEqual(raised.exception.code, 2) + self.assertIn(str(path), stderr.getvalue()) + if __name__ == "__main__": unittest.main() From 81b28cef614d3c6e58086b510a37f13dccc24daf Mon Sep 17 00:00:00 2001 From: Th3EvilGod <117441689+Th3EvilGod@users.noreply.github.com> Date: Tue, 20 Oct 2026 03:21:28 +0400 Subject: [PATCH 187/269] fix(libc): use precise Windows thread sleep --- core/libs/prx/libc/src/Threads.cpp | 8 ++++++++ core/libs/tests/GuestAmprThreads.cpp | 18 ++++++++++++++++++ docs/dev/debt/libc.md | 2 +- 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libc/src/Threads.cpp b/core/libs/prx/libc/src/Threads.cpp index 167360e72c..cb30fcea94 100644 --- a/core/libs/prx/libc/src/Threads.cpp +++ b/core/libs/prx/libc/src/Threads.cpp @@ -1,5 +1,8 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libc/include/General.hpp" +#ifdef _WIN32 +#include "prx/libc/include/PreciseWait.hpp" +#endif #include #include #include @@ -291,11 +294,16 @@ int APS5_VABI _Thrd_sleep_nid_postfix(const Xtime* target, Xtime*) { --remaining.tv_sec; } remaining.tv_sec = std::min(remaining.tv_sec, longestSleepSeconds); +#ifdef _WIN32 + PreciseSleepHundredNanos(static_cast(remaining.tv_sec) * 10000000ULL + + (static_cast(remaining.tv_nsec) + 99ULL) / 100ULL); +#else if (nanosleep(&remaining, nullptr) != 0) { if (errno == EINTR) continue; throw std::runtime_error("_Thrd_sleep: nanosleep failed"); } +#endif } } diff --git a/core/libs/tests/GuestAmprThreads.cpp b/core/libs/tests/GuestAmprThreads.cpp index 264ff62a6d..0c669decd2 100644 --- a/core/libs/tests/GuestAmprThreads.cpp +++ b/core/libs/tests/GuestAmprThreads.cpp @@ -1,6 +1,7 @@ #include "SceTypes.hpp" #include "prx/libc/include/exceptions/Runtime.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include #include @@ -188,6 +189,23 @@ static void TestThreadSleep() { Require(_Thrd_sleep_nid_postfix(&negative, nullptr) == 0); Require(RealtimeNanos() >= borrowed.sec * NanosPerSecond + borrowed.nsec); +#ifdef _WIN32 + for (const auto duration : {100000LL, 2000000LL}) { + std::array samples{}; + for (auto& sample : samples) { + const auto start = std::chrono::steady_clock::now(); + const auto shortTarget = XtimeAfter(duration); + Require(_Thrd_sleep_nid_postfix(&shortTarget, nullptr) == ThrdSuccess); + sample = std::chrono::steady_clock::now() - start; + Require(RealtimeNanos() >= shortTarget.sec * NanosPerSecond + shortTarget.nsec); + } + std::sort(samples.begin(), samples.end()); + const auto median = samples[samples.size() / 2]; + std::fprintf(stderr, "_Thrd_sleep(%lld ns) median: %lld ns\n", duration, static_cast(median.count())); + Require(median < std::chrono::milliseconds(12)); + } +#endif + bool rejected = false; try { _Thrd_sleep_nid_postfix(nullptr, nullptr); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index ebf631971a..508d0aa658 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -10,7 +10,7 @@ - [_Mtx_trylock](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `k6pGNMwJB08`) - returns busy for a non-recursive mutex already owned by the caller, assumed to match `_Mtx_lock`'s ownership handling; exact console behavior is unverified. - [nearbyintf](../../../core/libs/prx/libc/src/Math.cpp) (libc, NID `c+4r-T-tEIc`) uses the host rounding mode, not the console's; console rounding mode state remains unverified. - [__std_find_trivial_unsized_N](../../../core/libs/prx/libc/src/VectorAlgorithms.cpp) (libc) - matches the `libc.prx` bundled with PPSA08427 (disassembly, and that binary run side by side on random inputs): when nothing matches between `first` and the end of its 32-byte-aligned block, the console rescans that whole block without masking and so can return a match up to 31 bytes before `first` -- [_Thrd_sleep](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `jfRI3snge3o`) follows the Dinkumware threads library shipped with [Visual C++ 2013](https://github.com/adityaramesh/concurrency_runtime/tree/master/VS2013_SP2/crt/src/thr) (`xtime.c`) and C11 Annex K, without console verification. It reads a 16-byte xtime (64-bit seconds and nanoseconds) on the guest `CLOCK_REALTIME`, normalizes the nanoseconds and sleeps through `nanosleep`, retrying on `EINTR`, so it is a cancellation point; a null xtime or one whose normalization overflows throws. The console's `_Thrd_sleep` (disassembled from a title's `libc.prx`) also takes an optional `xtime*` of remaining time and returns `int`; here the sleep is never cut short, so it returns 0 and leaves the remaining time untouched. +- [_Thrd_sleep](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `jfRI3snge3o`) follows the Dinkumware threads library shipped with [Visual C++ 2013](https://github.com/adityaramesh/concurrency_runtime/tree/master/VS2013_SP2/crt/src/thr) (`xtime.c`) and C11 Annex K, without console verification. It reads a 16-byte xtime (64-bit seconds and nanoseconds) on the guest `CLOCK_REALTIME`, normalizes the nanoseconds and sleeps through `nanosleep` on POSIX, retrying on `EINTR`, or high-resolution waitable timers on Windows; a null xtime or one whose normalization overflows throws. Guest thread cancellation is not integrated with this wait. The console's `_Thrd_sleep` (disassembled from a title's `libc.prx`) also takes an optional `xtime*` of remaining time and returns `int`; here the sleep is never cut short, so it returns 0 and leaves the remaining time untouched. - [_Mtx_init_with_name](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `tgioGpKtmbE`) forwards to `_Mtx_init` and ignores the mutex name parameter. - [_Cnd_timedwait](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `McaImWKXong`) follows the console's `_Cnd_timedwait` (disassembled from a title's `libc.prx`), where xtime is an absolute deadline converted against the current time: it returns `_Thrd_timedout` (2) on expiration and `_Thrd_success` (0) on notification. The console returns 4 for other internal pthread errors while this implementation throws. - [fegetround, fesetround, fetestexcept, feclearexcept, fegetexceptflag and fesetexceptflag](../../../core/libs/prx/libc/src/FloatingPoint.cpp) (libSceLibcInternal, implemented in libc) follow FreeBSD 11.4 amd64 [fenv.h](https://github.com/freebsd/freebsd-src/blob/4554bee35405b36ea19e9e60d6abe6ea534e36e0/lib/msun/x86/fenv.h): rounding modes use x87 control bits 10-11, exception flags include denormals (`0x3f`), and `fexcept_t` is 16 bits. They access the current host thread's x87 and MXCSR state. This console ABI is assumed, as for `fegetenv`, not measured on hardware. Null flag pointers throw instead of faulting. From 04c16c74f9187366a8ea09fd944dc2eeb9458786 Mon Sep 17 00:00:00 2001 From: Retr0 Date: Tue, 20 Oct 2026 01:59:53 +0300 Subject: [PATCH 188/269] fix(libkernel): mark both socketpair ends close-on-exec for SOCK_CLOEXEC socketpair accepted SOCK_CLOEXEC but left both descriptors out of the close-on-exec set, so F_GETFD read 0 on them. FreeBSD 11's kern_socketpair opens both files with O_CLOEXEC when the type carries SOCK_CLOEXEC, as socket already does here. --- core/libs/prx/libkernel/Socket/src/Udp.cpp | 5 +++++ core/libs/tests/GuestSocketPair.cpp | 2 ++ 2 files changed, 7 insertions(+) diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 718b58c187..79290f3401 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -1077,6 +1077,11 @@ int APS5_VABI socketpair_nid_postfix(int family, int type, int protocol, int* de GuestSockets::Close(firstDescriptor); return Fail(error); } + if ((type & GuestCloseOnExec) != 0) { + std::lock_guard registry(socketsMutex); + closeOnExecDescriptors.insert(firstDescriptor); + closeOnExecDescriptors.insert(secondDescriptor); + } descriptors[0] = firstDescriptor; descriptors[1] = secondDescriptor; return 0; diff --git a/core/libs/tests/GuestSocketPair.cpp b/core/libs/tests/GuestSocketPair.cpp index 51d909e85d..9a40c92040 100644 --- a/core/libs/tests/GuestSocketPair.cpp +++ b/core/libs/tests/GuestSocketPair.cpp @@ -192,6 +192,7 @@ int main(int argc, char** argv) { Require(std::strcmp(received, ping) == 0); Require(fcntl_nid_postfix(pair[1], 3) == 2); + Require(fcntl_nid_postfix(pair[0], 1) == 0 && fcntl_nid_postfix(pair[1], 1) == 0); Require(fcntl_nid_postfix(pair[1], 4, 4) == 0); Require(fcntl_nid_postfix(pair[1], 3) == 6); Require(recv_nid_postfix(pair[1], received, sizeof(received), 0) == -1 && *__error_nid_postfix() == 35); @@ -209,6 +210,7 @@ int main(int argc, char** argv) { Require(socketpair_nid_postfix(Unix, Stream | NonBlocking | CloseOnExec, 0, pair) == 0); Require(fcntl_nid_postfix(pair[0], 3) == 6 && fcntl_nid_postfix(pair[1], 3) == 6); + Require(fcntl_nid_postfix(pair[0], 1) == 1 && fcntl_nid_postfix(pair[1], 1) == 1); Require(recv_nid_postfix(pair[0], received, sizeof(received), 0) == -1 && *__error_nid_postfix() == 35); Require(Exchange(pair[1], pair[0], "wake up")); Require(close_nid_postfix(pair[0]) == 0 && close_nid_postfix(pair[1]) == 0); From 95c90a54ef74418e420bb6fb67765aa752561548 Mon Sep 17 00:00:00 2001 From: Celegans12 <53881228+Celegans12@users.noreply.github.com> Date: Tue, 20 Oct 2026 01:41:16 +0200 Subject: [PATCH 189/269] test(agc): compare a remapped import with the one alive before the remap agc_trimmed_import fails on Windows with NVIDIA and AMD drivers at the 1440 KiB unmap case: after the pages are mapped again, the import of the whole range has the same VkDeviceMemory handle as the first import. In that case the first import is not kept: its kept side has a new backing serial, so the trimmed memory is released and freed by the recorder syncs of the checks that follow. The driver then hands the freed handle value to the new import of the whole range, so the comparison with the first import's handle fails even though that memory no longer exists. Compare the remapped import with the kept side's import instead, which is still alive or held by the recorder when the whole range is imported again. In the cases that keep the trimmed import this is the first import, as before. AI-assisted: yes --- core/libs/prx/libSceAgcDriver/tests/Recorder.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index a5322a1921..2b218c6a68 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -2590,6 +2590,7 @@ 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"); requireImportServesGuest(context, recorder, *kept, begin, end, ++salt, "the larger side of " + name); @@ -2609,7 +2610,7 @@ 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 != original, "after " + name + " the pages mapped again are served from the import that pinned the old ones"); + Require(remapped->memory != keptMemory, "after " + name + " the pages mapped again are served from the import made before they were mapped again"); 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"); } From 995c10ec6b762a60931378f3748396f9fd366813 Mon Sep 17 00:00:00 2001 From: "Kang, Harry Zixuan" Date: Mon, 19 Oct 2026 12:20:26 -0700 Subject: [PATCH 190/269] fix(build): allow static MinGW runtimes in relinker-only tests --- CMakeLists.txt | 3 +++ docs/dev/BUILD.md | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 3334461358..ee0779b12b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -44,6 +44,9 @@ if(BUILD_TESTING) get_filename_component(testCompilerDirectory "${CMAKE_CXX_COMPILER}" DIRECTORY) foreach(runtimeName IN ITEMS libgcc_s_seh-1.dll libstdc++-6.dll libwinpthread-1.dll) if(NOT EXISTS "${testCompilerDirectory}/${runtimeName}") + if(ANYPS5_RELINKER_ONLY) + continue() + endif() message(FATAL_ERROR "Cannot locate MinGW runtime: ${runtimeName}") endif() file(COPY "${testCompilerDirectory}/${runtimeName}" DESTINATION "${ANYPS5_TEST_OUTPUT_DIRECTORY}") diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index 3d1e5fd416..549b6063c3 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 the MinGW-w64 toolchain below. 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 WinLibs requirement below applies to the full build. 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. From 906b2527b8de2403ea991f2c99aca5c354206719 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Tue, 13 Oct 2026 06:44:09 +0200 Subject: [PATCH 191/269] fix(libc): convert the character argument of strrchr to char strrchr passed its int argument to the host strrchr unchanged. On Windows the host compares it with the signed char bytes of the string, so a byte of 0x80 to 0xFF passed as an unsigned value (0xE9) or a value above 255 (0x12F) was not found, where FreeBSD converts the argument to char first. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libc/src/Strings.cpp | 2 +- core/libs/tests/GuestStrings.cpp | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libc/src/Strings.cpp b/core/libs/prx/libc/src/Strings.cpp index 40c192a30d..009d04f3c3 100644 --- a/core/libs/prx/libc/src/Strings.cpp +++ b/core/libs/prx/libc/src/Strings.cpp @@ -153,7 +153,7 @@ const char* APS5_VABI strchr_nid_postfix(const char* s, int c) { } char* APS5_VABI strrchr_nid_postfix(const char* s, int c) { - return std::strrchr(const_cast(s), c); + return std::strrchr(const_cast(s), static_cast(c)); } char* APS5_VABI rindex_nid_postfix(const char* s, int c) { diff --git a/core/libs/tests/GuestStrings.cpp b/core/libs/tests/GuestStrings.cpp index e843751cd4..2aedfac173 100644 --- a/core/libs/tests/GuestStrings.cpp +++ b/core/libs/tests/GuestStrings.cpp @@ -19,6 +19,8 @@ std::size_t APS5_VABI strnlen_nid_postfix(const char*, std::size_t); std::size_t APS5_VABI strnlen_s_nid_postfix(const char*, std::size_t); char* APS5_VABI strncat_nid_postfix(char*, const char*, std::size_t); char* APS5_VABI strpbrk_nid_postfix(const char*, const char*); +const char* APS5_VABI strchr_nid_postfix(const char*, int); +char* APS5_VABI strrchr_nid_postfix(const char*, int); std::size_t APS5_VABI strcspn_nid_postfix(const char*, const char*); std::size_t APS5_VABI strlcat_nid_postfix(char*, const char*, std::size_t); char* APS5_VABI stpcpy_nid_postfix(char*, const char*); @@ -262,9 +264,24 @@ static void CheckBcopy() { bcopy_nid_postfix(nullptr, nullptr, 0); } +static void CheckCharacterConversionInSearches() { + const char bytes[] = "a\xe9" "b\xe9" "c"; + Require(strchr_nid_postfix(bytes, 0xe9) == bytes + 1); + Require(strchr_nid_postfix(bytes, 0x1e9) == bytes + 1); + Require(strchr_nid_postfix(bytes, -23) == bytes + 1); + Require(strrchr_nid_postfix(bytes, 0xe9) == bytes + 3); + Require(strrchr_nid_postfix(bytes, 0x1e9) == bytes + 3); + Require(strrchr_nid_postfix(bytes, -23) == bytes + 3); + const char path[] = "a/b/c"; + Require(strrchr_nid_postfix(path, 0x12f) == path + 3); + Require(strrchr_nid_postfix(path, 0) == path + 5); + Require(strrchr_nid_postfix(path, 'z') == nullptr); +} + int main() { CheckBcopy(); if (!CheckDirnameLengths()) return 1; + CheckCharacterConversionInSearches(); CheckBoundsCheckedFunctions(); CheckCaseInsensitiveComparison(); CheckSscanfS(); From 89b04548c80316ce2b641303eb746fd911810bb1 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Tue, 20 Oct 2026 02:06:42 +0200 Subject: [PATCH 192/269] fix(tools): name the failing command when pr_overlap cannot run git or gh pr_overlap.py let a failed git or gh call escape as a bare CalledProcessError traceback that names the command but drops the error text, so a mistyped --base such as upstream/main (refs/heads/upstream/main does not exist) read as an unexplained crash. The script now exits with "pr_overlap: failed with exit code N: ". Co-Authored-By: Claude Sonnet 5.5 --- CMakeLists.txt | 2 ++ tools/pr_overlap.py | 37 +++++++++++++++--------- tools/tests/test_pr_overlap_errors.py | 41 +++++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 14 deletions(-) create mode 100644 tools/tests/test_pr_overlap_errors.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 3334461358..d636461642 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -202,6 +202,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_wrapped_exports.py) add_test(NAME pr_overlap_own_branch COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_own_branch.py) + add_test(NAME pr_overlap_errors COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_errors.py) add_test(NAME nid_database_cache COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_nid_names.py) add_test(NAME release_package COMMAND ${Python3_EXECUTABLE} diff --git a/tools/pr_overlap.py b/tools/pr_overlap.py index eefdfc5ae1..1d299dbba7 100644 --- a/tools/pr_overlap.py +++ b/tools/pr_overlap.py @@ -239,6 +239,12 @@ def write_reports(out, rows, blob): (out / f"{number}.md").write_text(report(found, blob), encoding="utf-8") +def describe_failure(error): + command = " ".join(str(part) for part in error.cmd) + message = (error.stderr or "").strip() or "no error output" + return f"pr_overlap: {command} failed with exit code {error.returncode}: {message}" + + if __name__ == "__main__": parser = argparse.ArgumentParser(description="report open pull requests that overlap each other") parser.add_argument("--base", default="main") @@ -251,20 +257,23 @@ if __name__ == "__main__": parser.error("pass exactly one of --out or --branch") if args.jobs < 1: parser.error("--jobs must be at least 1") - REPO = args.repo or upstream() - prs = open_prs(args.base) - fetch(args.base, prs) - if args.branch: - sha = git("rev-parse", args.branch).strip() - prs = own(prs, sha) - prs[0] = {"ref": sha, "author": None, "depends": set()} - scan_all(prs, args.jobs) - rows = {n: {} for n in prs} - pairs = [(0, n) for n in prs if n] if args.branch else combinations(prs, 2) - for a, b in pairs: - if found := overlaps(prs, a, b): - rows[a][b] = rows[b][a] = found - blob = f'https://github.com/{REPO}/blob/{git("rev-parse", "refs/pr/base").strip()}' + try: + REPO = args.repo or upstream() + prs = open_prs(args.base) + fetch(args.base, prs) + if args.branch: + sha = git("rev-parse", args.branch).strip() + prs = own(prs, sha) + prs[0] = {"ref": sha, "author": None, "depends": set()} + scan_all(prs, args.jobs) + rows = {n: {} for n in prs} + pairs = [(0, n) for n in prs if n] if args.branch else combinations(prs, 2) + for a, b in pairs: + if found := overlaps(prs, a, b): + rows[a][b] = rows[b][a] = found + blob = f'https://github.com/{REPO}/blob/{git("rev-parse", "refs/pr/base").strip()}' + except subprocess.CalledProcessError as error: + raise SystemExit(describe_failure(error)) if args.branch: print(report(rows[0], blob) or "No overlap with open pull requests.", end="") raise SystemExit diff --git a/tools/tests/test_pr_overlap_errors.py b/tools/tests/test_pr_overlap_errors.py new file mode 100644 index 0000000000..c5bb5e0b8e --- /dev/null +++ b/tools/tests/test_pr_overlap_errors.py @@ -0,0 +1,41 @@ +import runpy +import subprocess +import sys +import unittest +from pathlib import Path +from unittest.mock import patch + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import pr_overlap + +SCRIPT = Path(pr_overlap.__file__) +MISSING_REF = "fatal: couldn't find remote ref refs/heads/upstream/main" + + +def fake_run(command, **kwargs): + if command[0] == "gh": + return subprocess.CompletedProcess(command, 0, stdout="[]", stderr="") + return subprocess.CompletedProcess(command, 128, stdout=b"", stderr=MISSING_REF.encode()) + + +class ErrorReportTests(unittest.TestCase): + def test_failure_names_the_command_the_exit_code_and_the_git_message(self): + error = subprocess.CalledProcessError(128, ["git", "fetch", "-q"], output="", stderr=MISSING_REF + "\n") + message = pr_overlap.describe_failure(error) + self.assertEqual(message, "pr_overlap: git fetch -q failed with exit code 128: " + MISSING_REF) + + def test_failure_without_output_says_so(self): + error = subprocess.CalledProcessError(1, ["gh", "api"], output="", stderr="") + self.assertEqual(pr_overlap.describe_failure(error), "pr_overlap: gh api failed with exit code 1: no error output") + + def test_unknown_base_branch_exits_with_the_git_message_instead_of_a_traceback(self): + argv = ["pr_overlap.py", "--repo", "owner/project", "--base", "upstream/main", "--branch", "HEAD"] + with patch.object(subprocess, "run", side_effect=fake_run), patch.object(sys, "argv", argv): + with self.assertRaises(SystemExit) as raised: + runpy.run_path(str(SCRIPT), run_name="__main__") + self.assertIn("exit code 128", str(raised.exception.code)) + self.assertIn(MISSING_REF, str(raised.exception.code)) + + +if __name__ == "__main__": + unittest.main() From cd09badb2b0b19bd3fd512a6da68e316ea8b9eb0 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Tue, 13 Oct 2026 05:55:32 +0200 Subject: [PATCH 193/269] fix(libkernel): fail a short or long socket address like FreeBSD bind, connect and sendto answered EFAULT for an address length below 2 and EAFNOSUPPORT for an AF_INET address below 16 or an AF_INET6 address below 28 bytes. FreeBSD 11.4's getsockaddr fails a length below 2 with EINVAL and above SOCK_MAXADDRLEN (255) with ENAMETOOLONG, and the protocols compare the length with sizeof(struct sockaddr_in) or sockaddr_in6 and fail with EINVAL. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libkernel/CMakeLists.txt | 10 +++ core/libs/prx/libkernel/Socket/src/Udp.cpp | 5 +- core/libs/tests/GuestSocketAddressLength.cpp | 71 ++++++++++++++++++++ docs/dev/debt/libkernel.md | 1 + 4 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 core/libs/tests/GuestSocketAddressLength.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 714c8d43fa..287af0552c 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -1323,6 +1323,16 @@ if(BUILD_TESTING) set_tests_properties(guest_udp PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_socket_address_length_tests ${LIBS_INCLUDE_DIR}/tests/GuestSocketAddressLength.cpp) + target_include_directories(guest_socket_address_length_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_socket_address_length_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_socket_address_length_tests) + add_test(NAME guest_socket_address_length COMMAND guest_socket_address_length_tests) + set_tests_properties(guest_socket_address_length PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_socket_address_length PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_socket_read_write_tests ${LIBS_INCLUDE_DIR}/tests/GuestSocketReadWrite.cpp) target_include_directories(guest_socket_read_write_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_socket_read_write_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 718b58c187..aea4786b28 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -232,8 +232,11 @@ std::shared_ptr Lookup(int descriptor) { return {}; } bool Address(const void* input, std::uint32_t length, sockaddr_storage& native, socklen_t& size) { - if (!input || length < 2) { Fail(14); return false; } + if (length > 255) { Fail(63); return false; } + if (length < 2) { Fail(22); return false; } + if (!input) { Fail(14); 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) { auto& v4 = reinterpret_cast(native); v4.sin_family = AF_INET; diff --git a/core/libs/tests/GuestSocketAddressLength.cpp b/core/libs/tests/GuestSocketAddressLength.cpp new file mode 100644 index 0000000000..30e758d389 --- /dev/null +++ b/core/libs/tests/GuestSocketAddressLength.cpp @@ -0,0 +1,71 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI socket_nid_postfix(int, int, int); +int APS5_VABI bind_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI connect_nid_postfix(int, const void*, std::uint32_t); +std::int64_t APS5_VABI sendto_nid_postfix(int, const void*, std::uint64_t, int, const void*, std::uint32_t); +int APS5_VABI close_nid_postfix(int); +int* APS5_VABI __error_nid_postfix(); +} + +namespace { + +constexpr int Stream = 1; +constexpr int Datagram = 2; +constexpr int ErrorFault = 14; +constexpr int ErrorInvalid = 22; +constexpr int ErrorNameTooLong = 63; + +void Require(bool condition, const char* what) { + if (condition) return; + std::fprintf(stderr, "%s (errno %d)\n", what, *__error_nid_postfix()); + std::abort(); +} + +bool Fails(long result, int error) { return result == -1 && *__error_nid_postfix() == error; } + +std::array Loopback() { + std::array address{16, 2}; + address[4] = 127; + address[7] = 1; + return address; +} + +void ShortAddressesAreInvalid() { + const int stream = socket_nid_postfix(2, Stream, 0); + const int datagram = socket_nid_postfix(2, Datagram, 0); + Require(stream >= 0 && datagram >= 0, "socket failed"); + const auto address = Loopback(); + Require(Fails(connect_nid_postfix(stream, address.data(), 1), ErrorInvalid), "connect with a length below the size of sa_family must fail with EINVAL (22)"); + Require(Fails(bind_nid_postfix(stream, address.data(), 0), ErrorInvalid), "bind with length 0 must fail with EINVAL (22)"); + Require(Fails(connect_nid_postfix(stream, address.data(), 3), ErrorInvalid), "connect with an AF_INET address shorter than sockaddr_in must fail with EINVAL (22)"); + Require(Fails(bind_nid_postfix(stream, address.data(), 15), ErrorInvalid), "bind with an AF_INET address shorter than sockaddr_in must fail with EINVAL (22)"); + Require(Fails(sendto_nid_postfix(datagram, "x", 1, 0, address.data(), 8), ErrorInvalid), "sendto with an AF_INET address shorter than sockaddr_in must fail with EINVAL (22)"); + std::array v6{28, 28}; + Require(Fails(connect_nid_postfix(socket_nid_postfix(28, Stream, 0), v6.data(), 20), ErrorInvalid), "connect with an AF_INET6 address shorter than sockaddr_in6 must fail with EINVAL (22)"); + Require(Fails(bind_nid_postfix(stream, address.data(), 256), ErrorNameTooLong), "an address longer than SOCK_MAXADDRLEN (255) must fail with ENAMETOOLONG (63)"); + Require(Fails(bind_nid_postfix(stream, nullptr, address.size()), ErrorFault), "a null address must fail with EFAULT (14)"); + Require(close_nid_postfix(stream) == 0 && close_nid_postfix(datagram) == 0, "close failed"); +} + +void FullAddressesStillWork() { + const int stream = socket_nid_postfix(2, Stream, 0); + Require(stream >= 0, "socket failed"); + const auto address = Loopback(); + Require(bind_nid_postfix(stream, address.data(), address.size()) == 0, "bind of a full sockaddr_in failed"); + Require(close_nid_postfix(stream) == 0, "close failed"); +} + +} + +int main() { + ShortAddressesAreInvalid(); + FullAddressesStillWork(); + return 0; +} diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index e6bdd3c6fc..5b2efe0532 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -175,6 +175,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 these calls still fail with `EOPNOTSUPP` for it - [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 - [Socket descriptor numbers](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel, libSceNet) - `socket`, `accept`, `socketpair`, `dup`, `sceNetSocket` and `sceNetAccept` take the lowest free number of the host descriptor table that files use, as FreeBSD 11.4 `fdalloc` takes the lowest free entry of the one process table (`kern_descrip.c`), so every number stays below `FD_SETSIZE` (1024) while fewer than 1024 descriptors are open. The number holds a zero-access `NUL` handle on Windows and a duplicate of the host socket on Linux, and the socket tables map it to the socket. The host table also holds the runtime's own files, so the numbers differ from the console's; the first number `socket` and `sceNetSocket` return on a console and its `getdtablesize` are not measured. Paths that bypass the socket tables see the placeholder: libc `fdopen` on a socket opens it, and libkernel `close` of a libSceNet socket releases the number but leaves the libSceNet socket open. Palworld (PPSA23126), The Mound (PPSA26223) and Dying Light 2 (PPSA02262) set socket bits in a 1024-bit `fd_set` - [fstat on a guest socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) fills the fields FreeBSD 11's soo_stat and pru_sense_null fill and zeroes the rest: S_IFSOCK, the read bits while the receive side is open or data is queued, the write bits while the send side is open, st_size and st_blksize. Only this process's shutdown closes a side here; a peer's shutdown, close or reset does not clear the bits as FreeBSD's socantrcvmore and socantsendmore do. st_size is the host's FIONREAD, which for a Linux datagram socket is the size of the next datagram where FreeBSD counts every queued byte, and st_blksize is the host's SO_SNDBUF, which Linux reports as twice the size that was set. A socketpair end reports st_dev and st_ino 0, where FreeBSD's uipc_sense reports NODEV and a per-socket number From a957b8c1064d65c86d9040c7cc52f249765bc9ca Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Wed, 14 Oct 2026 00:42:19 +0200 Subject: [PATCH 194/269] test(libkernel): check and close the IPv6 socket of guest_socket_address_length Co-Authored-By: Claude Sonnet 5.5 --- core/libs/tests/GuestSocketAddressLength.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/core/libs/tests/GuestSocketAddressLength.cpp b/core/libs/tests/GuestSocketAddressLength.cpp index 30e758d389..930a8626d6 100644 --- a/core/libs/tests/GuestSocketAddressLength.cpp +++ b/core/libs/tests/GuestSocketAddressLength.cpp @@ -48,10 +48,12 @@ void ShortAddressesAreInvalid() { Require(Fails(bind_nid_postfix(stream, address.data(), 15), ErrorInvalid), "bind with an AF_INET address shorter than sockaddr_in must fail with EINVAL (22)"); Require(Fails(sendto_nid_postfix(datagram, "x", 1, 0, address.data(), 8), ErrorInvalid), "sendto with an AF_INET address shorter than sockaddr_in must fail with EINVAL (22)"); std::array v6{28, 28}; - Require(Fails(connect_nid_postfix(socket_nid_postfix(28, Stream, 0), v6.data(), 20), ErrorInvalid), "connect with an AF_INET6 address shorter than sockaddr_in6 must fail with EINVAL (22)"); + const int stream6 = socket_nid_postfix(28, Stream, 0); + Require(stream6 >= 0, "socket failed"); + Require(Fails(connect_nid_postfix(stream6, v6.data(), 20), ErrorInvalid), "connect with an AF_INET6 address shorter than sockaddr_in6 must fail with EINVAL (22)"); Require(Fails(bind_nid_postfix(stream, address.data(), 256), ErrorNameTooLong), "an address longer than SOCK_MAXADDRLEN (255) must fail with ENAMETOOLONG (63)"); Require(Fails(bind_nid_postfix(stream, nullptr, address.size()), ErrorFault), "a null address must fail with EFAULT (14)"); - Require(close_nid_postfix(stream) == 0 && close_nid_postfix(datagram) == 0, "close failed"); + Require(close_nid_postfix(stream) == 0 && close_nid_postfix(datagram) == 0 && close_nid_postfix(stream6) == 0, "close failed"); } void FullAddressesStillWork() { From 9abcd45861b4339bad8497ee5bcbcb13ce2b9a41 Mon Sep 17 00:00:00 2001 From: Rengundom Date: Mon, 19 Oct 2026 19:37:53 -0500 Subject: [PATCH 195/269] fix(trace): fail a benchmark file replace from the move, not the stream The Windows build failed agc_performance_trace: ReplaceGzipFile made the write fail by putting a directory at .tmp, so the std::ios_base::failure came out of libstdc++'s ofstream and could not unwind to the test's catch on Windows. The test now blocks the move instead, with a directory at the destination: MoveFileExW (or rename) fails and ReplaceGzip throws std::system_error from the driver's own code. ReplaceGzip also removes its temporary file when the move fails, and the test checks that the blocked destination, the missing temporary file and the other benchmark file are as expected. Co-Authored-By: Claude Opus 5.5 --- .../prx/libSceAgcDriver/Execution/src/TraceFile.cpp | 9 +++++++-- .../prx/libSceAgcDriver/tests/PerformanceTrace.cpp | 12 +++++++++--- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/TraceFile.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/TraceFile.cpp index 38d606fb44..b8263c881e 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/TraceFile.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/TraceFile.cpp @@ -58,11 +58,16 @@ void TraceFile::ReplaceGzip(const std::filesystem::path& path, std::string_view auto temporary = path; temporary += ".tmp"; WriteGzip(temporary, text); + std::error_code error; #ifdef _WIN32 - if (::MoveFileExW(temporary.c_str(), path.c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH) == 0) throw std::system_error(static_cast(::GetLastError()), std::system_category(), "MoveFileExW " + temporary.string()); + if (::MoveFileExW(temporary.c_str(), path.c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH) == 0) error = std::error_code(static_cast(::GetLastError()), std::system_category()); #else - std::filesystem::rename(temporary, path); + std::filesystem::rename(temporary, path, error); #endif + if (!error) return; + std::error_code removeError; + std::filesystem::remove(temporary, removeError); + throw std::system_error(error, "replacing " + path.string()); } void TraceFile::AppendLine(const std::filesystem::path& path, std::string_view line) { diff --git a/core/libs/prx/libSceAgcDriver/tests/PerformanceTrace.cpp b/core/libs/prx/libSceAgcDriver/tests/PerformanceTrace.cpp index 550af95fc4..355d95097f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/PerformanceTrace.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/PerformanceTrace.cpp @@ -480,12 +480,18 @@ void ReplaceGzipFile() { AgcDriver::TraceFile::ReplaceGzip(path, "{\"first\":1}\n"); AgcDriver::TraceFile::ReplaceGzip(path, "{\"second\":2}\n"); Check(ReadGzip(path) == "{\"second\":2}\n" && !std::filesystem::exists(temporary), "the benchmark file was not replaced"); - std::filesystem::create_directory(temporary); + const auto blocked = directory / "aps5-bench-blocked.json.gz"; + auto blockedTemporary = blocked; + blockedTemporary += ".tmp"; + std::filesystem::create_directory(blocked); bool failed = false; - try { AgcDriver::TraceFile::ReplaceGzip(path, "{\"third\":3}\n"); } catch (const std::exception&) { failed = true; } + try { AgcDriver::TraceFile::ReplaceGzip(blocked, "{\"third\":3}\n"); } catch (const std::system_error&) { failed = true; } + const bool blockedKept = std::filesystem::is_directory(blocked); + const bool temporaryLeft = std::filesystem::exists(blockedTemporary); const auto kept = ReadGzip(path); std::filesystem::remove_all(directory); - Check(failed && kept == "{\"second\":2}\n", "a failed rewrite damaged the previous benchmark file"); + Check(failed && blockedKept && !temporaryLeft, "a replace that could not move its file over the destination did not fail cleanly or left its temporary file"); + Check(kept == "{\"second\":2}\n", "a failed replace of another file damaged the benchmark file"); } void BenchSwitch() { From 6cbfe3e4c5f5a2b40a653fd1b28e1a68c42f61f3 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Tue, 20 Oct 2026 02:41:11 +0200 Subject: [PATCH 196/269] docs(debt): say that the RX 6400 device loss of the CMASK clear pass is not reproduced Co-Authored-By: Claude Sonnet 5.5 --- docs/dev/debt/libSceAgcDriver.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 2cf456bc83..478b5cd30b 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -132,7 +132,7 @@ - [PA_SC_CONSERVATIVE_RASTERIZATION_CNTL](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - 0x6001 uses Vulkan overestimation on devices that add at most 1/256 pixel and keep degenerate triangles; the console's uncertainty region and degenerate handling are unmeasured. A value with `OVER_RAST_ENABLE` and `UNDER_RAST_ENABLE` clear is decoded as disabled even when `UNCERTAINTY_REGION_MODE` or the `OUTER_`/`INNER_UNCERTAINTY_EDGERULE_OVERRIDE` bits (19:16) are set, as PPSA01474 writes 0xe6000; that these fields have no effect without over- or underestimation is assumed from their names, not measured. With `PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES` = 0 the `PERSP_CENTROID` and `LINEAR_CENTROID` inputs get the pixel-centre barycentrics, as without conservative rasterization ([shader debt](shader.md)): the one rasterized sample sits at the pixel centre, so every centroid the rasterizer could pick for an overestimated pixel is that point; that it does not move the centroid towards the primitive when the sample is uncovered is inferred, not measured. With more rasterized samples, and when a single-sample draw is raised to a multisampled color target, centroid inputs throw. - [Descriptor pool chain](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - each pool holds two storage buffer descriptors more than a set can take, as a workaround: RADV (Mesa 26.0 and later) lets a `FREE_DESCRIPTOR_SET` pool place a set up to 32 bytes past the pool's buffer ([`util_vma_heap_init`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/vulkan/radv_descriptor_pool.c) is given `bo_size + RADV_POOL_HEAP_OFFSET` as a size), and with a page-multiple pool size those bytes are an unmapped page - [CMASK fast clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - each fast-cleared CMASK tile (code 0) is stored as `CB_COLOR_CLEAR_WORD` into its 8x8 texels at the first draw into the target or at its fast-clear eliminate or DCC decompress pass, not as the color block writes the tile, and the CMASK is then stored as expanded (0xF): a texture or copy read of the target after that draw and before the title's eliminate sees the clear value in tiles no draw wrote, where the console keeps their previous bytes. Tile addresses follow the pipe-aligned SW_64KB_Z_X CMASK equation for 16 pipes; other CMASK swizzle modes, a cleared tile of a DCC target and a fast clear of a 3D or array color target throw; base mip 0 of a 2D mipmapped color target is supported (offset follows AMD Addrlib `Gfx10Lib::HwlComputeCmaskInfo` reverse mip-chain ordering preceding base mip 0, unverified on PS5 hardware; mipmapped targets with 8 or 16 bpp throw until `dsMipmapHtileFix` mip-tail shrinkage is modelled). A tile with a code other than 0 and 0xF keeps its texels like an expanded tile: PPSA18391 (engine: Clickteam Fusion) fast clears a target whose CMASK it never initialized, so some of its nibbles hold leftover values such as 1 and 10; what the console does with such codes on a single-sample target is unverified on hardware. Single-sample views with `viewMip > 0` skip CMASK fast-clear decoding since CMASK metadata only covers base mip 0 on AMD hardware (Mesa `si_state.c` similarly clears `FAST_CLEAR` for level > 0). -- [CMASK clear pass on AMD without pipeline libraries](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetiler.cpp) (libSceAgcDriver) - the compute pass that stores a fast-cleared CMASK into a resident render target (`DispatchCmaskClear`) is unverified on AMD's Windows driver with `APS5_NO_GPL=1`: on an RX 6400 (driver 32.0.21033.3005, Windows 10 19045) `agc_driver_cmask_fast_clear` loses the device in its `SW_64KB_R_X` pass (`device-fault` read invalid at 0xbeefdeadc000, then Vulkan result -4); with pipeline libraries on, and on an NVIDIA RTX 4080 SUPER in both modes, it passes. On a Radeon 860M (driver 32.0.13058.8001, Windows 11) the same pass keeps the stale 0x5A5A5A5A instead of the clear value, without a device loss. The reading shader and the reason pipeline libraries matter are not found (#5150). Closes when: the cause is found, or `ctest -R '^agc_driver_cmask_fast_clear$' --repeat until-fail:5` passes with `ANYPS5_GPU=Radeon` and `APS5_NO_GPL=1` on AMD. +- [CMASK clear pass on AMD without pipeline libraries](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetiler.cpp) (libSceAgcDriver) - the compute pass that stores a fast-cleared CMASK into a resident render target (`DispatchCmaskClear`) is unverified on AMD's Windows driver with `APS5_NO_GPL=1`: on a Radeon 860M (driver 32.0.13058.8001, Windows 11) `agc_driver_cmask_fast_clear` keeps the stale 0x5A5A5A5A in its `SW_64KB_R_X` pass instead of the clear value, while it passes with pipeline libraries on and on an NVIDIA RTX 4080 SUPER in both modes. An RX 6400 (driver 32.0.21033.3005) lost the device (read invalid at 0xbeefdeadc000) in an earlier build tree, but clean rebuilds of four main commits of 19 October pass there (three of three runs each), so that failure is not reproduced. The cause is not found (#5150). Closes when: the cause is found, or `ctest -R '^agc_driver_cmask_fast_clear$' --repeat until-fail:5` passes with `ANYPS5_GPU=Radeon` and `APS5_NO_GPL=1` on AMD. - [HTILE depth fast clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - HTILE is not modeled. Only RADV's `clear_htile_mask` compute kernel (17 GFX10 words, 64x1x1 groups) is recognized, and only when it leaves ZMASK at 0 (`user[4]` and `user[5]` low nibbles both 0); the dispatch still runs, and the host depth image is cleared to `DB_DEPTH_CLEAR` the next time the surface with that `DB_HTILE_DATA_BASE` address is bound. The address match is exact, so a clear of a different HTILE range does not hit. HTILE initialization, the stencil-only update, and fast clears made by the SDK's own paths, CP DMA fills or other kernels are not covered and leave the depth image as it was. The kernel words are the ones the PR author saw RADV submit; they are not compared with Mesa's source. A second kernel, the 9-word dword fill PPSA01802 (Kena: Bridge of Spirits) clears its depth HTILE with (64x1x1 groups, one `buffer_store_format_x` per thread through the stride-4 V# in `user[0..3]`, the value read by `s_buffer_load_dword` through the V# in `user[4..7]`), is taken as a fill of that range when the store format is 32_UINT, 32_SINT or 32_FLOAT and the value V# covers the dword (NUM_RECORDS times its stride, or NUM_RECORDS bytes when the stride is 0) ([`MatchMemoryValueFill`](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp), then the whole-HTILE and ZMask rules of `NoteDepthMetadataFill`). The value is read from guest memory when the dispatch is decoded, so a value that earlier GPU work in the same submission has not written back yet is not seen. When the [fill HLE](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp) takes the dispatch instead (an output descriptor of one 32-bit value per element, a value descriptor that covers the loaded dword, a 16-byte aligned range), the value is read again when the fill is recorded, after the labels queued before it, and the HTILE fill is noted with that value. - [HTILE written by other kernels](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - when a depth target is bound and its HTILE was written since it was last read, the words over its whole pipe-aligned metadata (`HtileMetadataBytes`, the extent `HtileFillCovers` takes) are read, outside the surfaces mutex since the read waits for recorded work: when every one marks the depth (ZMask 0) or the stencil (SMem 0) cleared, the surface takes that fast clear and the words are stored back as expanded (depth only: ZMask 15 over the full Z range; with stencil: ZMask 15, Z range and SMem, SR1 and SR0 3), so only the title's next clear clears it again. PPSA29141 clears its depth every frame with a compute kernel the fill hooks do not recognize (`InvokeHTileClearComputeShader` in its `ClearMRT` pass); without this its depth kept every earlier frame and moving objects left black trails. PPSA25344 (Unreal Engine 5) reads its HTILE after the depth prepass in its Nanite depth export, which takes a tile with ZMask 0 as holding the clear value; without the expanded words the export put Nanite depth over the prepass and the Nanite material pass painted over the meshes in front. The word layouts are Mesa radv's for GFX10. Metadata with some tiles cleared is not modelled, and the compression state draws leave in HTILE is not tracked, so a title reading its HTILE after a draw sees the stored expanded words. A recognized HTILE fill or clear makes the next bind read the words again. A slice other than 0 of a depth array is read at its own HTILE (`DB_HTILE_DATA_BASE` plus the slice times `HtileMetadataBytes`) but the HTILE word layout of slices 1 and up is not modelled (PPSA05684's tile masks need X8 and Y8 to take Z0 and Z1 as well, slices 4 and up unmeasured), which neither the check that every word is cleared nor the store-back of each word in place needs, so such a slice takes the clear like slice 0. A depth-only HTILE whose tiles all have ZMask 0 but different Min/Max Z (per-tile clear values, PPSA05684's cascades) is taken as a whole-surface clear to `DB_DEPTH_CLEAR`, the value the DB's depth test reads for a cleared tile (`HTILE_Z_CLEAR_REG` in Mesa's `src/amd/common/ac_descriptors.h`: the clear word "only clears HiZ", "the DB register contain[s] the full clear values"); the HiZ test before it, which rejects against each tile's own Min/Max Z range (the HTILE definitions in the same file), is not modelled, so a draw that HiZ would reject over a tile cleared with Min/Max Z 0 is tested against `DB_DEPTH_CLEAR` instead. A multisampled depth target, and any target whose HTILE overlaps a resident native depth guard, skips this read and store-back and goes to the native multisample depth owner, which keeps its own refusal for a fast-clear transition. - [RADV fill-memory kernel](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp) (libSceAgcDriver) - the [fill HLE](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp) takes RADV's 16-byte `fill_memory` compute kernel (`MatchClampedGlobalFill`: 11 GFX10.3 words, 64x1x1 groups, six user SGPRs with the address in `user[2..3]`, the largest store offset in `user[4]` and the value in `user[5]`; each thread stores 16 bytes at `min(thread * 16, user[4])`) as a fill of `min(threads * 16, user[4] + 16)` bytes. RADV initializes and fast-clears DCC keys and other metadata with it, so the key and metadata tracking sees such a fill where it is in the command stream; run as a dispatch, its global stores name no range, and a key fill was only seen once it had landed, after later writes to the surface. Only a 16-byte aligned address and largest offset are taken. RADV's 4-byte variant (fills below 16 bytes), other Mesa versions' encodings and other compilers still run as dispatches. The kernel words are the ones vkQuake's PS5 RADV build (Mesa 25.2) submitted; they are not compared with the NIR builder's output for other targets. From d5d17788aef7d5885c26a2435cdfadec086ee18b Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Tue, 13 Oct 2026 06:15:28 +0200 Subject: [PATCH 197/269] fix(libkernel): fail a socket call on an open non-socket descriptor with ENOTSOCK bind, listen, accept, connect, shutdown, getsockname, getsockopt, setsockopt, send, recv and the other calls that look up a guest socket answered EBADF for a descriptor that is open but not a socket, for example a pipe end. FreeBSD 11.4's getsock_cap fails these with ENOTSOCK and keeps EBADF for a closed or invalid descriptor. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libkernel/CMakeLists.txt | 10 +++ core/libs/prx/libkernel/Socket/src/Udp.cpp | 4 +- core/libs/tests/GuestSocketOnNonSocket.cpp | 73 ++++++++++++++++++++++ 3 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 core/libs/tests/GuestSocketOnNonSocket.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 714c8d43fa..14f4a9c8fb 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -1407,6 +1407,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endforeach() + add_test_executable(guest_socket_on_non_socket_tests ${LIBS_INCLUDE_DIR}/tests/GuestSocketOnNonSocket.cpp) + target_include_directories(guest_socket_on_non_socket_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_socket_on_non_socket_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_socket_on_non_socket_tests) + add_test(NAME guest_socket_on_non_socket COMMAND guest_socket_on_non_socket_tests) + set_tests_properties(guest_socket_on_non_socket PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_socket_on_non_socket PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_dup_tests ${LIBS_INCLUDE_DIR}/tests/GuestDup.cpp) target_include_directories(guest_dup_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_dup_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 718b58c187..3df9074620 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -228,7 +228,7 @@ std::shared_ptr Lookup(int descriptor) { std::lock_guard lock(socketsMutex); const auto found = sockets.find(descriptor); if (found != sockets.end()) return found->second; - Fail(9); + Fail(descriptor >= 0 && DescriptorIsOpen_nid_no_patch(descriptor) ? 38 : 9); return {}; } bool Address(const void* input, std::uint32_t length, sockaddr_storage& native, socklen_t& size) { @@ -1177,7 +1177,7 @@ int APS5_VABI bind_nid_postfix(int descriptor, const void* address, std::uint32_ } int APS5_VABI shutdown_nid_postfix(int descriptor, int how) { const auto socket = Lookup(descriptor); - if (!socket) return Fail(9); + if (!socket) return -1; if (how < 0 || how > 2) return Fail(22); if (::shutdown(socket->value, how)) return Fail(NativeError()); std::lock_guard lock(socket->modeMutex); diff --git a/core/libs/tests/GuestSocketOnNonSocket.cpp b/core/libs/tests/GuestSocketOnNonSocket.cpp new file mode 100644 index 0000000000..953bd473d0 --- /dev/null +++ b/core/libs/tests/GuestSocketOnNonSocket.cpp @@ -0,0 +1,73 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include + +extern "C" { +int APS5_VABI bind_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI listen_nid_postfix(int, int); +int APS5_VABI accept_nid_postfix(int, void*, std::uint32_t*); +int APS5_VABI connect_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI shutdown_nid_postfix(int, int); +int APS5_VABI getsockname_nid_postfix(int, void*, std::uint32_t*); +int APS5_VABI setsockopt_nid_postfix(int, int, int, const void*, std::uint32_t); +int APS5_VABI getsockopt_nid_postfix(int, int, 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 pipe_nid_postfix(int*); +int APS5_VABI close_nid_postfix(int); +int* APS5_VABI __error_nid_postfix(); +} + +namespace { + +constexpr int ErrorBadDescriptor = 9; +constexpr int ErrorNotSocket = 38; + +void Require(bool condition, const char* what) { + if (condition) return; + std::fprintf(stderr, "%s (errno %d)\n", what, *__error_nid_postfix()); + std::abort(); +} + +bool Fails(long result, int error) { return result == -1 && *__error_nid_postfix() == error; } + +void CallsOnAnOpenPipeAreNotSocketCalls() { + int ends[2]; + Require(pipe_nid_postfix(ends) == 0, "pipe failed"); + std::array address{16, 2}; + std::uint32_t size = address.size(); + char buffer[4]; + int option = 0; + std::uint32_t optionSize = sizeof(option); + Require(Fails(bind_nid_postfix(ends[0], address.data(), address.size()), ErrorNotSocket), "bind on a pipe must fail with ENOTSOCK (38)"); + Require(Fails(listen_nid_postfix(ends[0], 1), ErrorNotSocket), "listen on a pipe must fail with ENOTSOCK (38)"); + Require(Fails(accept_nid_postfix(ends[0], nullptr, nullptr), ErrorNotSocket), "accept on a pipe must fail with ENOTSOCK (38)"); + Require(Fails(connect_nid_postfix(ends[0], address.data(), address.size()), ErrorNotSocket), "connect on a pipe must fail with ENOTSOCK (38)"); + Require(Fails(shutdown_nid_postfix(ends[0], 1), ErrorNotSocket), "shutdown on a pipe must fail with ENOTSOCK (38)"); + Require(Fails(getsockname_nid_postfix(ends[0], address.data(), &size), ErrorNotSocket), "getsockname on a pipe must fail with ENOTSOCK (38)"); + Require(Fails(setsockopt_nid_postfix(ends[0], 0xffff, 4, &option, sizeof(option)), ErrorNotSocket), "setsockopt on a pipe must fail with ENOTSOCK (38)"); + Require(Fails(getsockopt_nid_postfix(ends[0], 0xffff, 4, &option, &optionSize), ErrorNotSocket), "getsockopt on a pipe must fail with ENOTSOCK (38)"); + Require(Fails(send_nid_postfix(ends[1], "x", 1, 0), ErrorNotSocket), "send on a pipe must fail with ENOTSOCK (38)"); + Require(Fails(recv_nid_postfix(ends[0], buffer, sizeof(buffer), 0), ErrorNotSocket), "recv on a pipe must fail with ENOTSOCK (38)"); + Require(close_nid_postfix(ends[0]) == 0 && close_nid_postfix(ends[1]) == 0, "close failed"); +} + +void ClosedAndInvalidDescriptorsAreBad() { + int ends[2]; + Require(pipe_nid_postfix(ends) == 0, "pipe failed"); + Require(close_nid_postfix(ends[0]) == 0 && close_nid_postfix(ends[1]) == 0, "close failed"); + std::array address{16, 2}; + Require(Fails(bind_nid_postfix(ends[0], address.data(), address.size()), ErrorBadDescriptor), "bind on a closed descriptor must fail with EBADF (9)"); + Require(Fails(listen_nid_postfix(-1, 1), ErrorBadDescriptor), "listen on -1 must fail with EBADF (9)"); + Require(Fails(connect_nid_postfix(1 << 20, address.data(), address.size()), ErrorBadDescriptor), "connect on a descriptor that was never open must fail with EBADF (9)"); +} + +} + +int main() { + CallsOnAnOpenPipeAreNotSocketCalls(); + ClosedAndInvalidDescriptorsAreBad(); + return 0; +} From 6bc6c444e02769a65b3894342b4d51c68631c50e Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Tue, 13 Oct 2026 09:50:10 +0200 Subject: [PATCH 198/269] fix(libc): fail an oversized guest path name with ENAMETOOLONG stat, open, mkdir, rename and the other calls that resolve a guest path answered ENOENT (or EIO for rename) for a name longer than 255 bytes or a path of 1024 bytes or more, because the host path just did not exist. FreeBSD 11.4's namei fails a component above NAME_MAX and a path of MAXPATHLEN bytes with ENAMETOOLONG. Check both limits on the guest string before it is resolved; FilesystemError already maps the error to 63. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libc/src/General.cpp | 14 +++++ core/libs/prx/libkernel/CMakeLists.txt | 10 ++++ core/libs/tests/GuestPathNameLength.cpp | 72 +++++++++++++++++++++++++ docs/dev/debt/libc.md | 1 + 4 files changed, 97 insertions(+) create mode 100644 core/libs/tests/GuestPathNameLength.cpp diff --git a/core/libs/prx/libc/src/General.cpp b/core/libs/prx/libc/src/General.cpp index 67444c12b6..d9a6eed039 100644 --- a/core/libs/prx/libc/src/General.cpp +++ b/core/libs/prx/libc/src/General.cpp @@ -246,6 +246,20 @@ std::vector WithTrailingSeparator(std::vector names, b } std::filesystem::path Resolve(WorkingDirectory& state, const char* path, bool module = false, bool archived = false) { std::string text(path); + constexpr std::size_t MaxPath = 1024; + constexpr std::size_t MaxName = 255; + if (text.size() >= MaxPath) throw std::filesystem::filesystem_error("Guest path is too long", std::make_error_code(std::errc::filename_too_long)); + std::size_t name = 0; + for (std::size_t index = 0; index < text.size(); ++index) { + name = text[index] == '/' || text[index] == '\\' ? 0 : name + 1; + if (name <= MaxName) continue; + const std::size_t end = index + 1 - name; + std::error_code status; + if (end == 0 || std::filesystem::is_directory(Resolve(state, text.substr(0, end).c_str()), status)) { + throw std::filesystem::filesystem_error("Guest path name is too long", std::make_error_code(std::errc::filename_too_long)); + } + break; + } for (auto& character : text) if (character == '\\') character = '/'; #ifdef _WIN32 // Preserve the existing ability to pass explicit native drive paths. diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 714c8d43fa..23c1b16dbb 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -1642,6 +1642,16 @@ if(BUILD_TESTING) target_compile_options(guest_positioned_close_tests PRIVATE -fasynchronous-unwind-tables) endif() endif() + add_test_executable(guest_path_name_length_tests ${LIBS_INCLUDE_DIR}/tests/GuestPathNameLength.cpp) + target_include_directories(guest_path_name_length_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_path_name_length_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_path_name_length_tests) + add_test(NAME guest_path_name_length COMMAND guest_path_name_length_tests) + set_tests_properties(guest_path_name_length PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_path_name_length PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_file_vector_tests ${LIBS_INCLUDE_DIR}/tests/GuestFileVector.cpp) target_include_directories(guest_file_vector_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_file_vector_tests PRIVATE libkernel libc) diff --git a/core/libs/tests/GuestPathNameLength.cpp b/core/libs/tests/GuestPathNameLength.cpp new file mode 100644 index 0000000000..ec3df8764f --- /dev/null +++ b/core/libs/tests/GuestPathNameLength.cpp @@ -0,0 +1,72 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include + +extern "C" { +int APS5_VABI open_nid_postfix(const char*, int, int); +int APS5_VABI close_nid_postfix(int); +int APS5_VABI mkdir_nid_postfix(const char*, int); +int APS5_VABI rmdir_nid_postfix(const char*); +int APS5_VABI unlink_nid_postfix(const char*); +int APS5_VABI rename_nid_postfix(const char*, const char*); +int APS5_VABI stat_nid_postfix(const char*, void*); +int APS5_VABI access_nid_postfix(const char*, int); +int* APS5_VABI __error_nid_postfix(); +} + +namespace { + +constexpr int ErrorNoEntry = 2; +constexpr int ErrorNameTooLong = 63; + +void Require(bool condition, const char* what) { + if (condition) return; + std::fprintf(stderr, "%s (errno %d)\n", what, *__error_nid_postfix()); + std::abort(); +} + +bool Fails(long result, int error) { return result == -1 && *__error_nid_postfix() == error; } + +bool TooLong(const std::string& path) { + unsigned char status[256]; + return Fails(stat_nid_postfix(path.c_str(), status), ErrorNameTooLong); +} + +} + +int main() { + unsigned char status[256]; + const std::string longName(256, 'n'); + const std::string longPath(1100, 'p'); + const std::string maxName(255, 'm'); + Require(Fails(stat_nid_postfix(longName.c_str(), status), ErrorNameTooLong), "stat of a name above NAME_MAX (255) must fail with ENAMETOOLONG (63)"); + rmdir_nid_postfix("anyps5_name_length"); + Require(mkdir_nid_postfix("anyps5_name_length", 0755) == 0, "mkdir of the test directory failed"); + Require(Fails(stat_nid_postfix(("anyps5_name_length/" + longName).c_str(), status), ErrorNameTooLong), "stat of a long final component below an existing directory must fail with ENAMETOOLONG (63)"); + Require(Fails(stat_nid_postfix(("anyps5_name_length/missing/" + longName).c_str(), status), ErrorNoEntry), "stat of a long component below a missing directory must fail with ENOENT (2), the lookup stops there first"); + const std::string below = "anyps5_name_length/"; + const std::string name255(255, 'n'); + Require(!TooLong(below + name255), "a final component of exactly 255 bytes must not be too long"); + Require(!TooLong(below + name255 + "/x"), "a middle component of exactly 255 bytes must not be too long"); + Require(TooLong(below + longName + "/x"), "a middle component of 256 bytes must fail with ENAMETOOLONG (63)"); + Require(TooLong(below + longName + "/"), "a long component followed by a trailing slash must fail with ENAMETOOLONG (63)"); + Require(!TooLong(below + std::string(200, 'n') + "\\" + std::string(100, 'n')), "a backslash separates components here, so 200 + 100 bytes around it are accepted (FreeBSD would refuse the 301-byte name)"); + const std::string block = name255 + "/"; + Require(!TooLong(block + block + block + name255), "a path of exactly 1023 bytes must not be too long"); + Require(TooLong(block + block + block + block), "a path of exactly 1024 bytes must fail with ENAMETOOLONG (63)"); + Require(rmdir_nid_postfix("anyps5_name_length") == 0, "rmdir of the test directory failed"); + Require(Fails(stat_nid_postfix((longName + "/x").c_str(), status), ErrorNameTooLong), "stat of a long first component must fail with ENAMETOOLONG (63)"); + Require(Fails(stat_nid_postfix(longPath.c_str(), status), ErrorNameTooLong), "stat of a path of 1100 bytes must fail with ENAMETOOLONG (63)"); + Require(Fails(access_nid_postfix(longName.c_str(), 0), ErrorNameTooLong), "access of a long name must fail with ENAMETOOLONG (63)"); + Require(Fails(unlink_nid_postfix(longName.c_str()), ErrorNameTooLong), "unlink of a long name must fail with ENAMETOOLONG (63)"); + Require(Fails(mkdir_nid_postfix(longName.c_str(), 0755), ErrorNameTooLong), "mkdir of a long name must fail with ENAMETOOLONG (63)"); + Require(Fails(rmdir_nid_postfix(longName.c_str()), ErrorNameTooLong), "rmdir of a long name must fail with ENAMETOOLONG (63)"); + Require(Fails(open_nid_postfix(longName.c_str(), 0, 0), ErrorNameTooLong), "open of a long name must fail with ENAMETOOLONG (63)"); + Require(Fails(rename_nid_postfix(longName.c_str(), "anyps5_name_length_target"), ErrorNameTooLong), "rename from a long name must fail with ENAMETOOLONG (63)"); + Require(Fails(rename_nid_postfix("anyps5_name_length_source", longName.c_str()), ErrorNameTooLong), "rename to a long name must fail with ENAMETOOLONG (63)"); + + Require(Fails(stat_nid_postfix(maxName.c_str(), status), ErrorNoEntry), "a name of exactly 255 bytes is not too long"); + return 0; +} diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index d9b2e588a8..acf5a4edc1 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -61,6 +61,7 @@ - [getc_unlocked, putc_unlocked, getchar_unlocked, putchar_unlocked](../../../core/libs/prx/libc/src/StandardStreams.cpp) (libc) - byte I/O uses the host unlocked operations; guest EOF/error flags and macro counters follow `FileStream`. Linux status queries use `feof_unlocked`/`ferror_unlocked` because [glibc `feof`](https://github.com/bminor/glibc/blob/d2097651cc57834dbfcaa102ddfacae0d86cfb66/libio/feof.c) and [`ferror`](https://github.com/bminor/glibc/blob/d2097651cc57834dbfcaa102ddfacae0d86cfb66/libio/ferror.c) can acquire the stream lock. Windows uses [the CRT no-lock byte operations](https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/fgetc-nolock-fgetwc-nolock) and `feof`/`ferror`, verified under a held stream lock on UCRT. Console behavior and other host CRTs remain unverified. - [fgetws, getwc, getwchar, putwc, putwchar](../../../core/libs/prx/libc/src/FormattingWide.cpp) (libc) - wide stream I/O encodes and decodes wide characters as UTF-8 on the underlying byte stream; `fgetws` reads characters into the destination buffer until newline or EOF and null-terminates, returning null on stream error. Console wide stream buffering and encoding details remain unverified. - [Closed standard descriptors](../../../core/libs/prx/libc/src/StandardStreams.cpp) (libc) - on Linux and macOS, a descriptor 0, 1 or 2 that is closed when libc loads is opened on `/dev/null` (read-only for 0, write-only for 1 and 2) by a static initializer of libc, which runs before the libraries that depend on libc initialize and before the title starts. Within libc this relies on no static initializer opening a descriptor before it, which `guest_closed_standard_descriptors` checks. glibc does the same for set-user-ID programs ([`check_fds.c`](https://sourceware.org/git/?p=glibc.git;a=blob;f=csu/check_fds.c)). Without it the first descriptor a library opens, the write watch's `userfaultfd` for one, becomes the guest's standard input or output. What the console gives a title as its standard descriptors is not measured, and on Windows a process started without standard handles is not handled. +- [Guest path names](../../../core/libs/prx/libc/src/General.cpp) (libc) are checked against FreeBSD 11.4's `NAME_MAX` (255 bytes per component) and `MAXPATHLEN` (1024 bytes with the terminating NUL) before they are resolved, and a longer name fails with `ENAMETOOLONG`, as `namei` does. The host's own limit still applies below that: on Windows a path whose host form exceeds `MAX_PATH` fails with `ENOENT`. A path through a regular file (`file/x`, `file/`) is not always `ENOTDIR` on the calls that resolve it. None of this was measured on a console. - [ftrylockfile](../../../core/libs/prx/libc/src/RuntimeSupport.cpp) (libc) - tries the host CRT stream lock that `flockfile`, `funlockfile` and each locking host stdio call on the stream take, so it returns -1 while another thread holds the stream through `flockfile` or is inside such a call, and 0 when it acquires the lock or already owns it (recursively, released by as many `funlockfile` calls), as FreeBSD 11 [`_ftrylockfile`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/_flock_stub.c#L89-L104) does. A libc export holds the lock only for each host call it makes, not across several host calls or while it updates the guest `FILE` prefix. Linux uses glibc `ftrylockfile`. UCRT has no try variant of `_lock_file`, so Windows calls `TryEnterCriticalSection` on the `CRITICAL_SECTION` that `_lock_file` enters, found through the private UCRT stream layout (`__crt_stdio_stream_data` in `ucrt/inc/corecrt_internal_stdio.h`, unchanged from Windows SDK 10.0.19041 to 10.0.26100); another CRT or a change of that layout breaks it. A null or closed stream throws where FreeBSD dereferences it - [fgetln](../../../core/libs/prx/libc/src/StandardStreams.cpp) (libSceLibcInternal, implemented in libc) throws: in the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x5FE80) the export is a `hlt` instruction, which faults in user mode and ends the process - [Wide-character conversions](../../../core/libs/prx/libc/src/Multibyte.cpp) (libc) - The wide `wcsto*` functions parse the ASCII prefix of the guest string, as the C locale accepts only ASCII white space and digits. [`wcsrtombs`](../../../core/libs/prx/libc/src/Multibyte.cpp) uses the guest's single-byte C locale through `wcrtomb`; other encodings and shift states are not supported From 1ccbbc2faafe371970b8c88fcb2d80a7ac616817 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Wed, 14 Oct 2026 01:00:26 +0200 Subject: [PATCH 199/269] fix(libkernel): fail chflags on an oversized path name with ENAMETOOLONG instead of throwing Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libkernel/File/src/Stdio.cpp | 4 +++- core/libs/tests/GuestPathNameLength.cpp | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index dee7b33df1..d068f8f5d9 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -617,7 +617,9 @@ int APS5_VABI chmod_nid_postfix(const char* path, int mode) { int APS5_VABI chflags_nid_postfix(const char* path, std::uint64_t flags) { if (const int error = PathError(path)) return PosixFailure(error); - if (const int error = LookupError(ResolvePath_nid_no_patch(path))) return PosixFailure(error); + std::filesystem::path native; + if (const int error = ResolvePathError_nid_no_patch(path, native)) return PosixFailure(error); + if (const int error = LookupError(native)) return PosixFailure(error); std::uint64_t known = 0; for (const auto& entry : GUEST_FILE_FLAGS) known |= entry.first; if ((flags & ~known) != 0) return PosixFailure(GUEST_EOPNOTSUPP); diff --git a/core/libs/tests/GuestPathNameLength.cpp b/core/libs/tests/GuestPathNameLength.cpp index ec3df8764f..e07722661b 100644 --- a/core/libs/tests/GuestPathNameLength.cpp +++ b/core/libs/tests/GuestPathNameLength.cpp @@ -13,6 +13,7 @@ int APS5_VABI unlink_nid_postfix(const char*); int APS5_VABI rename_nid_postfix(const char*, const char*); int APS5_VABI stat_nid_postfix(const char*, void*); int APS5_VABI access_nid_postfix(const char*, int); +int APS5_VABI chflags_nid_postfix(const char*, std::uint64_t); int* APS5_VABI __error_nid_postfix(); } @@ -60,6 +61,7 @@ int main() { Require(Fails(stat_nid_postfix((longName + "/x").c_str(), status), ErrorNameTooLong), "stat of a long first component must fail with ENAMETOOLONG (63)"); Require(Fails(stat_nid_postfix(longPath.c_str(), status), ErrorNameTooLong), "stat of a path of 1100 bytes must fail with ENAMETOOLONG (63)"); Require(Fails(access_nid_postfix(longName.c_str(), 0), ErrorNameTooLong), "access of a long name must fail with ENAMETOOLONG (63)"); + Require(Fails(chflags_nid_postfix(longName.c_str(), 0), ErrorNameTooLong), "chflags of a long name must fail with ENAMETOOLONG (63)"); Require(Fails(unlink_nid_postfix(longName.c_str()), ErrorNameTooLong), "unlink of a long name must fail with ENAMETOOLONG (63)"); Require(Fails(mkdir_nid_postfix(longName.c_str(), 0755), ErrorNameTooLong), "mkdir of a long name must fail with ENAMETOOLONG (63)"); Require(Fails(rmdir_nid_postfix(longName.c_str()), ErrorNameTooLong), "rmdir of a long name must fail with ENAMETOOLONG (63)"); From 1ab777e2579bc5199080b75207a93cd13c593d5d Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Wed, 14 Oct 2026 01:00:42 +0200 Subject: [PATCH 200/269] docs(libc): list the backslash separator and the sceNgs2ParseWaveformFile exception among the path name length differences 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 acf5a4edc1..40ad0aaac7 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -61,7 +61,7 @@ - [getc_unlocked, putc_unlocked, getchar_unlocked, putchar_unlocked](../../../core/libs/prx/libc/src/StandardStreams.cpp) (libc) - byte I/O uses the host unlocked operations; guest EOF/error flags and macro counters follow `FileStream`. Linux status queries use `feof_unlocked`/`ferror_unlocked` because [glibc `feof`](https://github.com/bminor/glibc/blob/d2097651cc57834dbfcaa102ddfacae0d86cfb66/libio/feof.c) and [`ferror`](https://github.com/bminor/glibc/blob/d2097651cc57834dbfcaa102ddfacae0d86cfb66/libio/ferror.c) can acquire the stream lock. Windows uses [the CRT no-lock byte operations](https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/fgetc-nolock-fgetwc-nolock) and `feof`/`ferror`, verified under a held stream lock on UCRT. Console behavior and other host CRTs remain unverified. - [fgetws, getwc, getwchar, putwc, putwchar](../../../core/libs/prx/libc/src/FormattingWide.cpp) (libc) - wide stream I/O encodes and decodes wide characters as UTF-8 on the underlying byte stream; `fgetws` reads characters into the destination buffer until newline or EOF and null-terminates, returning null on stream error. Console wide stream buffering and encoding details remain unverified. - [Closed standard descriptors](../../../core/libs/prx/libc/src/StandardStreams.cpp) (libc) - on Linux and macOS, a descriptor 0, 1 or 2 that is closed when libc loads is opened on `/dev/null` (read-only for 0, write-only for 1 and 2) by a static initializer of libc, which runs before the libraries that depend on libc initialize and before the title starts. Within libc this relies on no static initializer opening a descriptor before it, which `guest_closed_standard_descriptors` checks. glibc does the same for set-user-ID programs ([`check_fds.c`](https://sourceware.org/git/?p=glibc.git;a=blob;f=csu/check_fds.c)). Without it the first descriptor a library opens, the write watch's `userfaultfd` for one, becomes the guest's standard input or output. What the console gives a title as its standard descriptors is not measured, and on Windows a process started without standard handles is not handled. -- [Guest path names](../../../core/libs/prx/libc/src/General.cpp) (libc) are checked against FreeBSD 11.4's `NAME_MAX` (255 bytes per component) and `MAXPATHLEN` (1024 bytes with the terminating NUL) before they are resolved, and a longer name fails with `ENAMETOOLONG`, as `namei` does. The host's own limit still applies below that: on Windows a path whose host form exceeds `MAX_PATH` fails with `ENOENT`. A path through a regular file (`file/x`, `file/`) is not always `ENOTDIR` on the calls that resolve it. None of this was measured on a console. +- [Guest path names](../../../core/libs/prx/libc/src/General.cpp) (libc) are checked against FreeBSD 11.4's `NAME_MAX` (255 bytes per component) and `MAXPATHLEN` (1024 bytes with the terminating NUL) before they are resolved, and a longer name fails with `ENAMETOOLONG`, as `namei` does. The host's own limit still applies below that: on Windows a path whose host form exceeds `MAX_PATH` fails with `ENOENT`. A backslash counts as a path separator for the component length, because the guest path resolver turns it into `/` before the host call, while FreeBSD treats it as an ordinary name character, so a 301-byte name that contains one is accepted here. `sceNgs2ParseWaveformFile` resolves its path without a try block, so a long component ends in an uncaught `filesystem_error` there where a missing file ended in a `runtime_error` (both end the title). A path through a regular file (`file/x`, `file/`) is not always `ENOTDIR` on the calls that resolve it. None of this was measured on a console. - [ftrylockfile](../../../core/libs/prx/libc/src/RuntimeSupport.cpp) (libc) - tries the host CRT stream lock that `flockfile`, `funlockfile` and each locking host stdio call on the stream take, so it returns -1 while another thread holds the stream through `flockfile` or is inside such a call, and 0 when it acquires the lock or already owns it (recursively, released by as many `funlockfile` calls), as FreeBSD 11 [`_ftrylockfile`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/_flock_stub.c#L89-L104) does. A libc export holds the lock only for each host call it makes, not across several host calls or while it updates the guest `FILE` prefix. Linux uses glibc `ftrylockfile`. UCRT has no try variant of `_lock_file`, so Windows calls `TryEnterCriticalSection` on the `CRITICAL_SECTION` that `_lock_file` enters, found through the private UCRT stream layout (`__crt_stdio_stream_data` in `ucrt/inc/corecrt_internal_stdio.h`, unchanged from Windows SDK 10.0.19041 to 10.0.26100); another CRT or a change of that layout breaks it. A null or closed stream throws where FreeBSD dereferences it - [fgetln](../../../core/libs/prx/libc/src/StandardStreams.cpp) (libSceLibcInternal, implemented in libc) throws: in the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x5FE80) the export is a `hlt` instruction, which faults in user mode and ends the process - [Wide-character conversions](../../../core/libs/prx/libc/src/Multibyte.cpp) (libc) - The wide `wcsto*` functions parse the ASCII prefix of the guest string, as the C locale accepts only ASCII white space and digits. [`wcsrtombs`](../../../core/libs/prx/libc/src/Multibyte.cpp) uses the guest's single-byte C locale through `wcrtomb`; other encodings and shift states are not supported From 9899ba0fa2a61db7259aae560840b34cba15ecba Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Tue, 13 Oct 2026 09:57:06 +0200 Subject: [PATCH 201/269] fix(libc): fail a rename of a path that ends in a dot component with EINVAL rename and sceKernelRename resolved "dir/." to "dir" and renamed the directory. FreeBSD 11.4's lookup fails a last component of "." for DELETE and RENAME with EINVAL, for the source and the destination alike. Check the guest string before it is resolved: a last component of "." (also under trailing slashes) fails with EINVAL when its parent exists, ENOENT when it does not and ENOTDIR when it is not a directory. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libc/include/RenameDot.hpp | 26 +++++++ core/libs/prx/libc/src/Rename.cpp | 3 + core/libs/prx/libkernel/CMakeLists.txt | 7 ++ core/libs/prx/libkernel/File/src/Stdio.cpp | 3 + core/libs/tests/GuestRenameDot.cpp | 80 ++++++++++++++++++++++ docs/dev/debt/libc.md | 1 + 6 files changed, 120 insertions(+) create mode 100644 core/libs/prx/libc/include/RenameDot.hpp create mode 100644 core/libs/tests/GuestRenameDot.cpp diff --git a/core/libs/prx/libc/include/RenameDot.hpp b/core/libs/prx/libc/include/RenameDot.hpp new file mode 100644 index 0000000000..a43a778412 --- /dev/null +++ b/core/libs/prx/libc/include/RenameDot.hpp @@ -0,0 +1,26 @@ +#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_RENAMEDOT_HPP +#define CORE_LIBS_PRX_LIBC_INCLUDE_RENAMEDOT_HPP + +#include +#include +#include "FilesystemError.hpp" + +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 == '\\'; }; + while (length > 1 && separator(path[length - 1])) --length; + std::size_t start = length; + while (start > 0 && !separator(path[start - 1])) --start; + const std::size_t name = length - start; + 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::filesystem::path resolved; + if (const int error = ResolvePathError_nid_no_patch(parent.c_str(), resolved)) return error; + std::error_code status; + if (!std::filesystem::exists(resolved, status)) return 2; + return std::filesystem::is_directory(resolved, status) ? 22 : 20; +} + +#endif diff --git a/core/libs/prx/libc/src/Rename.cpp b/core/libs/prx/libc/src/Rename.cpp index f5d9545b5c..f9dede035b 100644 --- a/core/libs/prx/libc/src/Rename.cpp +++ b/core/libs/prx/libc/src/Rename.cpp @@ -1,4 +1,5 @@ #include "prx/libc/include/FilesystemError.hpp" +#include "prx/libc/include/RenameDot.hpp" #include "prx/libc/include/General.hpp" #include #include @@ -96,6 +97,8 @@ static int RenamePath_nid_no_patch(const std::filesystem::path& source, const st extern "C" int RenameGuestPath_nid_no_patch(const char* from, const char* to) { if (!from || !to) { errno = 14; return -1; } if (!*from || !*to) { errno = 2; return -1; } + if (const int dot = RenameDotError_nid_no_patch(from)) { errno = dot; return -1; } + if (const int dot = RenameDotError_nid_no_patch(to)) { errno = dot; return -1; } try { const auto source = ResolvePath_nid_no_patch(from); const auto destination = ResolvePath_nid_no_patch(to); diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 714c8d43fa..104572fa3a 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -1659,6 +1659,13 @@ if(BUILD_TESTING) set_tests_properties(guest_fstat_fault PROPERTIES TIMEOUT 15) if(WIN32) set_tests_properties(guest_fstat_fault PROPERTIES ENVIRONMENT_MODIFICATION + add_test_executable(guest_rename_dot_tests ${LIBS_INCLUDE_DIR}/tests/GuestRenameDot.cpp) + target_include_directories(guest_rename_dot_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_rename_dot_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_rename_dot_tests) + add_test(NAME guest_rename_dot COMMAND guest_rename_dot_tests) + set_tests_properties(guest_rename_dot PROPERTIES TIMEOUT 30) + set_tests_properties(guest_rename_dot PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test_executable(guest_fstatfs_tests ${LIBS_INCLUDE_DIR}/tests/GuestFstatfs.cpp) diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index dee7b33df1..875c259884 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -5,6 +5,7 @@ #include "SceTypes.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" @@ -1332,6 +1333,8 @@ int APS5_VABI sceKernelRename(const char* from, const char* to) { if (!from || !to) return SceErrorFromErrno(GUEST_EFAULT); const int result = RenameGuestPath_nid_no_patch(from, to); return result == 0 ? 0 : SceKernelError(errno); + if (const int dot = RenameDotError_nid_no_patch(from)) return SceKernelError(dot); + if (const int dot = RenameDotError_nid_no_patch(to)) return SceKernelError(dot); } int APS5_VABI sceKernelRmdir(const char* path) { diff --git a/core/libs/tests/GuestRenameDot.cpp b/core/libs/tests/GuestRenameDot.cpp new file mode 100644 index 0000000000..ccebfad2e9 --- /dev/null +++ b/core/libs/tests/GuestRenameDot.cpp @@ -0,0 +1,80 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include + +extern "C" { +int APS5_VABI mkdir_nid_postfix(const char*, int); +int APS5_VABI rmdir_nid_postfix(const char*); +int APS5_VABI rename_nid_postfix(const char*, const char*); +int APS5_VABI sceKernelRename(const char*, const char*); +int APS5_VABI stat_nid_postfix(const char*, void*); +int* APS5_VABI __error_nid_postfix(); +} + +namespace { + +constexpr int ErrorNoEntry = 2; +constexpr int ErrorInvalid = 22; +constexpr char Source[] = "anyps5_rename_dot_source"; +constexpr char Target[] = "anyps5_rename_dot_target"; +constexpr char Moved[] = "anyps5_rename_dot_moved"; + +void Cleanup() { + rmdir_nid_postfix(Source); + rmdir_nid_postfix(Target); + rmdir_nid_postfix(Moved); +} + +void Require(bool condition, const char* what) { + if (condition) return; + std::fprintf(stderr, "%s (errno %d)\n", what, *__error_nid_postfix()); + Cleanup(); + std::abort(); +} + +bool Fails(long result, int error) { return result == -1 && *__error_nid_postfix() == error; } + +bool Exists(const char* path) { + unsigned char status[256]; + return stat_nid_postfix(path, status) == 0; +} + +} + +int main() { + Cleanup(); + Require(mkdir_nid_postfix(Source, 0755) == 0 && mkdir_nid_postfix(Target, 0755) == 0, "mkdir failed"); + const std::string dotted = std::string(Source) + "/."; + const std::string dottedSlash = std::string(Source) + "/./"; + const std::string intoTarget = std::string(Target) + "/."; + Require(Fails(rename_nid_postfix(dotted.c_str(), Moved), ErrorInvalid), "rename from dir/. must fail with EINVAL (22)"); + Require(Fails(rename_nid_postfix(dottedSlash.c_str(), Moved), ErrorInvalid), "rename from dir/./ must fail with EINVAL (22)"); + Require(Exists(Source) && !Exists(Moved), "a failed rename from dir/. must leave the directory in place"); + Require(Fails(rename_nid_postfix(Source, intoTarget.c_str()), ErrorInvalid), "rename to dir/. must fail with EINVAL (22)"); + Require(Fails(rename_nid_postfix(".", Moved), ErrorInvalid), "rename of . must fail with EINVAL (22)"); + Require(Fails(rename_nid_postfix(Source, "."), ErrorInvalid), "rename onto . must fail with EINVAL (22)"); + Require(Fails(rename_nid_postfix("anyps5_rename_dot_missing/.", Moved), ErrorNoEntry), "rename from missing/. must fail with ENOENT (2)"); + const std::string dotDot = std::string(Source) + "/.."; + const std::string dotDotSlash = std::string(Source) + "/../"; + Require(Fails(rename_nid_postfix(dotDot.c_str(), Moved), ErrorInvalid), "rename from dir/.. must fail with EINVAL (22)"); + Require(Fails(rename_nid_postfix(dotDotSlash.c_str(), Moved), ErrorInvalid), "rename from dir/../ must fail with EINVAL (22)"); + Require(Fails(rename_nid_postfix(Source, (std::string(Target) + "/..").c_str()), ErrorInvalid), "rename to dir/.. must fail with EINVAL (22)"); + Require(Fails(rename_nid_postfix("..", Moved), ErrorInvalid), "rename of .. must fail with EINVAL (22)"); + Require(Fails(rename_nid_postfix("anyps5_rename_dot_missing/..", Moved), ErrorNoEntry), "rename from missing/.. must fail with ENOENT (2)"); + const std::string backslashDot = std::string(Source) + "\\."; + const std::string backslashDotDot = std::string(Source) + "\\.."; + Require(Fails(rename_nid_postfix(backslashDot.c_str(), Moved), ErrorInvalid), "rename from dir\\. must fail with EINVAL (22), the guest resolver turns the backslash into a separator"); + Require(Fails(rename_nid_postfix(backslashDotDot.c_str(), Moved), ErrorInvalid), "rename from dir\\.. must fail with EINVAL (22)"); + Require(Exists(Source) && !Exists(Moved), "failed renames from dir\\. and dir\\.. must leave the directory in place"); + Require(sceKernelRename(dotDot.c_str(), Moved) == static_cast(0x80020016u), "sceKernelRename from dir/.. must return EINVAL"); + Require(sceKernelRename(dotted.c_str(), Moved) == static_cast(0x80020016u), "sceKernelRename from dir/. must return EINVAL"); + Require(sceKernelRename(Source, intoTarget.c_str()) == static_cast(0x80020016u), "sceKernelRename to dir/. must return EINVAL"); + Require(Exists(Source) && Exists(Target) && !Exists(Moved), "failed renames must change nothing"); + + Require(rename_nid_postfix(Source, Moved) == 0 && Exists(Moved) && !Exists(Source), "an ordinary rename must still work"); + Require(sceKernelRename(Moved, Source) == 0 && Exists(Source) && !Exists(Moved), "an ordinary sceKernelRename must still work"); + Cleanup(); + return 0; +} diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index d9b2e588a8..6935b338c3 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -68,6 +68,7 @@ - [strtod, strtof, strtold](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - `NAN(n-char-sequence)` converts to the quiet NaN that `nan` and `nanf` return, negated after a minus sign, and ignores the sequence as they ignore their tag. This is inferred from C17 7.12.11.2p2, which makes `nan("n")` equivalent to `strtod("NAN(n)", NULL)`, and not observed on the console. No console `nanl` was observed, so `strtold` returns the x87 default quiet NaN (exponent `0x7fff`, significand `0xc000000000000000`) - [remquol](../../../core/libs/prx/libc/src/MathExact.cpp) (libc) leaves `*quo` unchanged when the result is NaN; the `libc.prx` bundled with PPSA08427 writes a value that depends on earlier x87 state (`remquol(1, 0)` stored 0 on a first call and 4 on later ones) - [Long double math in MathExact.cpp](../../../core/libs/prx/libc/src/MathExact.cpp) (libc) was compared with the `libc.prx` bundled with PPSA08427 only for canonical 80-bit encodings; unnormals, pseudo-denormals and pseudo-infinities passed to arithmetic functions (not the classifiers) follow the host x87 instead of the console +- [rename, sceKernelRename](../../../core/libs/prx/libc/include/RenameDot.hpp) (libc, libkernel) fail a last path component of `.` or `..` with `EINVAL`, after FreeBSD 11.4's [`lookup`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_lookup.c), which refuses `.` and, through its `ISDOTDOT` and `ISLASTCN` flags, `..` as the last component for the `DELETE` and `RENAME` operations. `rmdir("dir/.")`, `unlink("dir/.")` and the same calls with `..` follow the same lookup rule in FreeBSD but are not changed here: the guest still resolves the dot component away and acts on `dir` or its parent. None of this was measured on a console. - [nan and nanf](../../../core/libs/prx/libc/src/Math.cpp) (libc) return fixed quiet NaN values (`0x7fc00000` and `0x7ff8000000000000`), ignoring the tag argument as observed in the console's `libc.prx` (both exports load a global constant via `mov rax,[rip+..]; vmovsd xmm0,[rax]; ret`); VLC (PPSA85300) stops on nanf at start-up after strsep. - [jrand48 and mrand48](../../../core/libs/prx/libc/src/Random.cpp) (libSceLibcInternal, implemented in libc) return the high 32 bits of the new 48-bit state zero-extended to 64 bits, as the console's `libSceLibcInternal.sprx` does when called from a homebrew payload process on a PS5 (firmware 12.70): a result with bit 31 set comes back between 0x80000000 and 0xFFFFFFFF, as FreeBSD 11.0's [`jrand48`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/lib/libc/gen/jrand48.c#L23) and [`mrand48`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/lib/libc/gen/mrand48.c#L25) return it; FreeBSD 11.1 and later cast it to `int32_t` ([`jrand48`](https://github.com/freebsd/freebsd-src/blob/release/11.1.0/lib/libc/gen/jrand48.c#L26), [`mrand48`](https://github.com/freebsd/freebsd-src/blob/release/11.1.0/lib/libc/gen/mrand48.c#L28)), which makes it negative. `lrand48` and `nrand48` matched FreeBSD there. Whether a title's own libc does the same is unverified - [seed48 and lcong48](../../../core/libs/prx/libc/src/Random.cpp) (libSceLibcInternal, implemented in libc) follow the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x6F460 and 0x6F420): the multiplier and addend are shared by the whole `rand48` family, including `erand48`, `nrand48` and `jrand48` on a caller's seed; `lcong48` sets them with the seed, and `srand48` and `seed48` restore 0x5DEECE66D and 0xB. Unlike FreeBSD, `seed48` returns a static copy of the new seed rather than the previous one. A null argument, which the console dereferences, throws From 49940998cddecae71b835710dcc32fe74f6fb8cd Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Wed, 14 Oct 2026 01:33:20 +0200 Subject: [PATCH 202/269] docs(libc): list the lookup order and the backslash separator among the rename dot component differences Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libkernel/CMakeLists.txt | 3 +++ docs/dev/debt/libc.md | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 104572fa3a..129fbe7c20 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -1659,12 +1659,15 @@ if(BUILD_TESTING) set_tests_properties(guest_fstat_fault PROPERTIES TIMEOUT 15) if(WIN32) set_tests_properties(guest_fstat_fault PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_rename_dot_tests ${LIBS_INCLUDE_DIR}/tests/GuestRenameDot.cpp) target_include_directories(guest_rename_dot_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_rename_dot_tests PRIVATE libkernel libc) configure_windows_unwind(guest_rename_dot_tests) add_test(NAME guest_rename_dot COMMAND guest_rename_dot_tests) set_tests_properties(guest_rename_dot PROPERTIES TIMEOUT 30) + if(WIN32) set_tests_properties(guest_rename_dot PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 6935b338c3..8f3b21ca2b 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -68,7 +68,7 @@ - [strtod, strtof, strtold](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - `NAN(n-char-sequence)` converts to the quiet NaN that `nan` and `nanf` return, negated after a minus sign, and ignores the sequence as they ignore their tag. This is inferred from C17 7.12.11.2p2, which makes `nan("n")` equivalent to `strtod("NAN(n)", NULL)`, and not observed on the console. No console `nanl` was observed, so `strtold` returns the x87 default quiet NaN (exponent `0x7fff`, significand `0xc000000000000000`) - [remquol](../../../core/libs/prx/libc/src/MathExact.cpp) (libc) leaves `*quo` unchanged when the result is NaN; the `libc.prx` bundled with PPSA08427 writes a value that depends on earlier x87 state (`remquol(1, 0)` stored 0 on a first call and 4 on later ones) - [Long double math in MathExact.cpp](../../../core/libs/prx/libc/src/MathExact.cpp) (libc) was compared with the `libc.prx` bundled with PPSA08427 only for canonical 80-bit encodings; unnormals, pseudo-denormals and pseudo-infinities passed to arithmetic functions (not the classifiers) follow the host x87 instead of the console -- [rename, sceKernelRename](../../../core/libs/prx/libc/include/RenameDot.hpp) (libc, libkernel) fail a last path component of `.` or `..` with `EINVAL`, after FreeBSD 11.4's [`lookup`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_lookup.c), which refuses `.` and, through its `ISDOTDOT` and `ISLASTCN` flags, `..` as the last component for the `DELETE` and `RENAME` operations. `rmdir("dir/.")`, `unlink("dir/.")` and the same calls with `..` follow the same lookup rule in FreeBSD but are not changed here: the guest still resolves the dot component away and acts on `dir` or its parent. None of this was measured on a console. +- [rename, sceKernelRename](../../../core/libs/prx/libc/include/RenameDot.hpp) (libc, libkernel) fail a last path component of `.` or `..` with `EINVAL`, after FreeBSD 11.4's [`lookup`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_lookup.c), which refuses `.` and, through its `ISDOTDOT` and `ISLASTCN` flags, `..` as the last component for the `DELETE` and `RENAME` operations. `rmdir("dir/.")`, `unlink("dir/.")` and the same calls with `..` follow the same lookup rule in FreeBSD but are not changed here: the guest still resolves the dot component away and acts on `dir` or its parent. Both arguments are checked before any host access, so `rename` of a missing source onto `dir/.` answers `EINVAL`, where FreeBSD's [`kern_renameat`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_syscalls.c) looks the source up first and answers `ENOENT`. A backslash counts as a path separator here (the guest path resolver turns it into `/`); FreeBSD treats it as an ordinary name character. None of this was measured on a console. - [nan and nanf](../../../core/libs/prx/libc/src/Math.cpp) (libc) return fixed quiet NaN values (`0x7fc00000` and `0x7ff8000000000000`), ignoring the tag argument as observed in the console's `libc.prx` (both exports load a global constant via `mov rax,[rip+..]; vmovsd xmm0,[rax]; ret`); VLC (PPSA85300) stops on nanf at start-up after strsep. - [jrand48 and mrand48](../../../core/libs/prx/libc/src/Random.cpp) (libSceLibcInternal, implemented in libc) return the high 32 bits of the new 48-bit state zero-extended to 64 bits, as the console's `libSceLibcInternal.sprx` does when called from a homebrew payload process on a PS5 (firmware 12.70): a result with bit 31 set comes back between 0x80000000 and 0xFFFFFFFF, as FreeBSD 11.0's [`jrand48`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/lib/libc/gen/jrand48.c#L23) and [`mrand48`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/lib/libc/gen/mrand48.c#L25) return it; FreeBSD 11.1 and later cast it to `int32_t` ([`jrand48`](https://github.com/freebsd/freebsd-src/blob/release/11.1.0/lib/libc/gen/jrand48.c#L26), [`mrand48`](https://github.com/freebsd/freebsd-src/blob/release/11.1.0/lib/libc/gen/mrand48.c#L28)), which makes it negative. `lrand48` and `nrand48` matched FreeBSD there. Whether a title's own libc does the same is unverified - [seed48 and lcong48](../../../core/libs/prx/libc/src/Random.cpp) (libSceLibcInternal, implemented in libc) follow the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x6F460 and 0x6F420): the multiplier and addend are shared by the whole `rand48` family, including `erand48`, `nrand48` and `jrand48` on a caller's seed; `lcong48` sets them with the seed, and `srand48` and `seed48` restore 0x5DEECE66D and 0xB. Unlike FreeBSD, `seed48` returns a static copy of the new seed rather than the previous one. A null argument, which the console dereferences, throws From 58f0e89ca353671960a1f38c31a3765ae93d60f5 Mon Sep 17 00:00:00 2001 From: Muhammad Wasif Ali Wasif <76695420+wachi35676@users.noreply.github.com> Date: Tue, 20 Oct 2026 05:46:31 +0500 Subject: [PATCH 203/269] fix(agc): keep resident DCC targets watched under aligned block imports Since host imports group adjacent ranges into aligned blocks (#6457), an import can span both 64 KiB blocks that an earlier import left unwatched and the metadata of resident multisampled DCC targets. ImportWatched with staysWatched took the whole import out of the write watch when any part of it was not watched before. It now walks and compares each run of watched blocks of the import on its own, and unwatches only the blocks that were not watched or whose walk failed. A resident DCC target took the generation of its metadata from a walk of the metadata's pages alone. CPU stores to the rest of the metadata's 64 KiB block made before that walk were stamped later by the import's walk, so the block read as written, and the import window marked the metadata page as possibly stored over: the next draw refused the target. The generations now come from a walk of the whole blocks. agc_write_tracking and agc_driver_msaa_dcc_clear_* gain regression tests for both. Fixes #8103 Co-Authored-By: Claude Opus 5.5 --- .../Execution/src/GuestMemory.cpp | 64 ++++++++++++------- .../Graphics/src/MultisampleTarget.cpp | 12 +++- .../libSceAgcDriver/tests/WriteTracking.cpp | 32 ++++++++++ .../tests/execution/AlphaToCoverage.cpp | 51 +++++++++++++++ 4 files changed, 133 insertions(+), 26 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index dc2c695011..9b789b5c5c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -1514,39 +1514,55 @@ bool ImportWatched(std::uint64_t address, std::size_t bytes, const std::function const auto first = address & ~(page - 1); const auto stop = (address + bytes + page - 1) & ~(page - 1); const bool covered = tracker.covers(first, static_cast(stop - first)); - std::vector saved; - std::vector slots; - if (covered && staysWatched) { - const auto length = static_cast(stop - first); - CheckRange(reinterpret_cast(first), length, 1, false); - const auto* source = reinterpret_cast(first); - slots.resize(length / page); - std::size_t kept = 0; - for (std::size_t index = 0; index < slots.size(); ++index) { - const bool zero = std::memcmp(source + index * page, ZeroPage.data(), page) == 0; - slots[index] = zero ? WindowCopy::Zero : static_cast(kept++); - } - saved.reserve(kept * page); - 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); - } - } - const bool before = covered && walkWrites(tracker, first, stop, StampKind::Cpu, true); - bool imported = false; - { + const auto runImport = [&] { #ifdef __APPLE__ const GuestArena::HostWrite open(reinterpret_cast(first), static_cast(stop - first)); #endif - imported = import(); - } - if (!imported) return false; + return import(); + }; if (!staysWatched) { + const bool before = covered && walkWrites(tracker, first, stop, StampKind::Cpu, true); + if (!runImport()) return false; if (!before || !walkWrites(tracker, first, stop, StampKind::ImportWindow, true)) unwatchLocked(tracker, first, static_cast(stop - first)); unwatchLocked(tracker, address, bytes); return true; } + std::vector> spans; + if (covered) spans.emplace_back(first, stop); + else { + for (auto begin = first; begin < stop;) { + const auto end = std::min((begin / WriteBlockBytes + 1) * WriteBlockBytes, stop); + if (tracker.covers(begin, static_cast(end - begin))) { + if (!spans.empty() && spans.back().second == begin) spans.back().second = end; + else spans.emplace_back(begin, end); + } + begin = end; + } + } + std::vector slots(static_cast((stop - first) / page), WindowCopy::Zero); + std::size_t kept = 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++); + } + } + 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}; - if (!before || !walkWrites(tracker, first, stop, StampKind::Cpu, true, &window)) unwatchLocked(tracker, first, static_cast(stop - first)); + auto unwatchedFrom = first; + for (const auto& [begin, end] : spans) { + if (!walkWrites(tracker, begin, end, StampKind::Cpu, true, &window)) continue; + if (unwatchedFrom < begin) unwatchLocked(tracker, unwatchedFrom, static_cast(begin - unwatchedFrom)); + unwatchedFrom = end; + } + if (unwatchedFrom < stop) unwatchLocked(tracker, unwatchedFrom, static_cast(stop - unwatchedFrom)); return true; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp index 11bcccdaf3..01f22449f5 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -36,6 +36,14 @@ std::uint32_t MultisampledBlockLog2(ColorTileMode mode) { } } +std::uint64_t collectBlocks(std::uint64_t address, std::size_t bytes) { + constexpr std::uint64_t block = 65536; + const auto first = address & ~(block - 1); + const auto stop = (address + bytes + block - 1) & ~(block - 1); + const auto generation = GuestMemory::CollectWritesUncached(first, static_cast(stop - first)); + return generation != 0 ? generation : GuestMemory::CollectWritesUncached(address, bytes); +} + 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; } @@ -365,7 +373,7 @@ private: std::vector metadata(dccRanges.begin() + 1, dccRanges.end()); for (auto& range : metadata) { if (!GuestMemory::Watched(range.address, range.bytes)) return false; - range.generation = GuestMemory::CollectWritesUncached(range.address, range.bytes); + range.generation = collectBlocks(range.address, range.bytes); if (range.generation == 0) return false; } clear = MultisampledDccClear(use, std::vector(dccRanges[1].bytes, std::byte{*keys}), std::vector(use.cmaskBytes, std::byte{cmaskKeys})); @@ -386,7 +394,7 @@ private: for (auto& range : dccRanges) { GuestMemory::CheckRange(reinterpret_cast(range.address), range.bytes, 256); Require(GuestMemory::Watched(range.address, range.bytes), "initial multisampled DCC contents require watched guest memory"); - range.generation = GuestMemory::CollectWritesUncached(range.address, range.bytes); + range.generation = collectBlocks(range.address, range.bytes); Require(range.generation != 0, "initial multisampled DCC contents have no write generation"); } for (std::size_t first = 0; first < dccRanges.size(); ++first) { diff --git a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp index 2d4f10f07c..14d6423661 100644 --- a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp @@ -1044,6 +1044,37 @@ void CheckImportWindowZeroPages() { Require(changed[4] == BlockWritten && WrittenSince(base + 4 * Block, Block, synced), "a store of new bytes after the zero pages of an import window was missed"); } +void CheckImportWindowOverUnwatchedBlock() { + void* memory = AllocateWatched(4 * Block); + const auto base = reinterpret_cast(memory); + auto* bytes = static_cast(memory); + std::memset(memory, 0x11, 4 * Block); + Unwatch(base + Block, Block); + const auto synced = CollectWritesUncached(base + 2 * Block, 2 * Block); + const auto syncedFirst = CollectWritesUncached(base, Block); + Require(synced != 0 && syncedFirst != 0, "the watched blocks around the unwatched one are not collected"); + bytes[0x500] = 0x44; + Require(ImportWatched(base, 4 * Block, [&] { + bytes[0x100] = 0x11; + bytes[2 * Block + 0x100] = 0x11; + bytes[3 * Block + 0x2000] = 0x22; + return true; + }), "the import over an unwatched block failed"); + Require(!Watched(base + Block, Block), "an import window made an unwatched block watched"); + Require(Watched(base, Block) && Watched(base + 2 * Block, 2 * Block), "an import window over an unwatched block took the watched blocks out of the write watch"); + const std::array generations{synced, synced}; + std::array changed{}; + std::array cpu{}; + Require(ChangedBlocks(base + 2 * Block, 2 * Block, generations, changed, cpu), "the watched blocks after an unwatched block are not tracked"); + Require(changed[0] == BlockUnchanged && UnchangedSince(base + 2 * Block, Block, synced), "a page an import window over an unwatched block resolved without changing its bytes reads as written"); + Require(changed[1] == BlockWritten && WrittenSince(base + 3 * Block, Block, synced), "a store of new bytes within an import window over an unwatched block was missed"); + Require(WrittenSince(base, Block, syncedFirst), "a store before an import window over an unwatched block was missed"); + const auto after = CollectWritesUncached(base, Block); + bytes[0x100] = 0x33; + static_cast(CollectWritesUncached(base, Block)); + Require(after != 0 && !UnchangedSince(base, Block, after), "a store after an import window over an unwatched block was missed"); +} + void ProtectPage(std::uint64_t address, bool readable) { #ifdef _WIN32 DWORD previous = 0; @@ -1327,6 +1358,7 @@ int main() { CheckImportWindow(); CheckImportWindowZeroPages(); CheckImportWindowOtherThread(); + CheckImportWindowOverUnwatchedBlock(); CheckStoreAcrossImports(); CheckUnwatchedImport(); CheckCommittedRanges(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp index d871a86171..b36b4ee481 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp @@ -818,6 +818,56 @@ void DccRefillTests(AgcDriver::VulkanDevice& device, std::uint32_t selectedSampl } } +void DccImportWindowTests(AgcDriver::VulkanDevice& device, std::uint32_t selectedSamples) { + if (!AgcDriver::GuestMemory::WriteWatched()) return; + const ColorTargetLayout layout(Width, Height, ColorTileMode::RenderTarget, 8); + std::vector> blocks; + struct ClearTargets { + AgcDriver::VulkanDevice& device; + ~ClearTargets() { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + device.SubmitRecorded(true); + device.WaitIdle(); + AgcDriver::Graphics::ClearMultisampleTargets(device.Device()); + AgcDriver::Graphics::ClearCachedTextures(device.Device()); + } + } clearTargets{device}; + const Color written{0.25f, 0.25f, 0.25f, 0.25f}; + for (const auto samples : {2u, 4u, 8u}) { + if (selectedSamples != 0 && samples != selectedSamples) continue; + blocks.push_back(std::make_unique(layout.Bytes() * samples)); + auto& source = *blocks.back(); + std::memset(source.data, 0, source.bytes); + blocks.push_back(std::make_unique(BlockBytes)); + auto& metadata = *blocks.back(); + std::memset(metadata.data, 0, BlockBytes); + auto target = DecodeTarget(source, samples); + target.dccAddress = metadata.Address(); + target.dccPipeAligned = true; + target.dccAlphaOnMsb = true; + Draw(device, target, false, Float32Export, Program(written, Float32Export, 1u), false, true); + Require(AgcDriver::GuestMemory::ImportWatched(metadata.Address(), BlockBytes, [&] { + auto* first = reinterpret_cast(metadata.data); + *first = *first; + return true; + }), "the import over resident multisampled DCC metadata failed"); + Draw(device, target, false, Float32Export, Program(written, Float32Export, 1u), false, true); + Require(AgcDriver::GuestMemory::ImportWatched(metadata.Address(), BlockBytes, [&] { + auto* first = reinterpret_cast(metadata.data); + *first = static_cast(*first ^ 0xffu); + return true; + }), "the second import over resident multisampled DCC metadata failed"); + bool refused = false; + try { + Draw(device, target, false, Float32Export, Program(written, Float32Export, 1u), false, true); + } catch (const std::runtime_error& error) { + refused = std::string_view(error.what()).find("writes to a resident multisampled DCC") != std::string_view::npos; + } + Require(refused, "a store of new bytes to resident multisampled DCC metadata within an import window was not refused"); + programCount = 0; + } +} + int main(int argc, char** argv) { try { const auto device = OpenVulkanTestDevice(); @@ -828,6 +878,7 @@ int main(int argc, char** argv) { try { DccFastClearTests(*device, samples); DccRefillTests(*device, samples); + DccImportWindowTests(*device, samples); } catch (const std::runtime_error& error) { if (std::string_view(error.what()) != "AGC graphics: the device cannot render color with the target's sample count") throw; std::printf("skipped, the device lacks the requested multisample color attachments: %s\n", error.what()); From dd945495e3bc0c03c0c2e12e07770e54c9e923e2 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Tue, 20 Oct 2026 03:06:59 +0200 Subject: [PATCH 204/269] fix(libkernel): check truncate length and utimes times before the path FreeBSD 11.4 kern_truncate returns EINVAL for a negative length before it looks up the path, and getutimes rejects an out-of-range tv_usec before namei in utimes and lutimes. The guest calls checked a NULL or empty path first, so truncate(NULL, -1) and utimes("", {usec -1}) returned EFAULT or ENOENT where FreeBSD returns EINVAL. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libkernel/File/src/Stdio.cpp | 20 +++++++++----------- core/libs/tests/GuestFileTimestamps.cpp | 9 +++++++++ core/libs/tests/GuestFilesystem.cpp | 6 ++++++ 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index dee7b33df1..3cf63385f9 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -1381,10 +1381,14 @@ int APS5_VABI fchmod_nid_postfix(int d, int mode) { return PosixResult(sceKernelFchmod(d, static_cast(mode))); } +static bool InvalidTimes(const KernelTimeval* times) { + return times != nullptr && (times[0].tv_usec < 0 || times[0].tv_usec >= 1000000 || times[1].tv_usec < 0 || times[1].tv_usec >= 1000000); +} + int APS5_VABI sceKernelTruncate_nid_postfix(const char* path, std::int64_t length) { + if (length < 0) return SceErrorFromErrno(GUEST_EINVAL); if (!path) return SceErrorFromErrno(GUEST_EFAULT); if (*path == '\0') return SceErrorFromErrno(GUEST_ENOENT); - if (length < 0) return SceErrorFromErrno(GUEST_EINVAL); std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); std::error_code error; @@ -1397,18 +1401,15 @@ int APS5_VABI sceKernelTruncate_nid_postfix(const char* path, std::int64_t lengt } int APS5_VABI truncate_nid_postfix(const char* path, std::int64_t length) { + if (length < 0) return PosixFailure(GUEST_EINVAL); if (const int error = PathError(path)) return PosixFailure(error); return PosixResult(sceKernelTruncate_nid_postfix(path, length)); } int APS5_VABI sceKernelUtimes_nid_postfix(const char* path, const KernelTimeval* times) { + if (InvalidTimes(times)) return SCE_KERNEL_ERROR_EINVAL; if (!path) return SceErrorFromErrno(GUEST_EFAULT); if (*path == '\0') return SceErrorFromErrno(GUEST_ENOENT); - if (times != nullptr) { - for (int i = 0; i < 2; ++i) { - if (times[i].tv_usec < 0 || times[i].tv_usec >= 1000000) return SCE_KERNEL_ERROR_EINVAL; - } - } std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); if (NativeUtimes(native, times) != 0) return SceErrorFromErrno(errno); @@ -1416,17 +1417,14 @@ int APS5_VABI sceKernelUtimes_nid_postfix(const char* path, const KernelTimeval* } int APS5_VABI utimes_nid_postfix(const char* path, const KernelTimeval* times) { + if (InvalidTimes(times)) return PosixFailure(GUEST_EINVAL); if (const int error = PathError(path)) return PosixFailure(error); return PosixResult(sceKernelUtimes_nid_postfix(path, times)); } int APS5_VABI lutimes_nid_postfix(const char* path, const KernelTimeval* times) { + if (InvalidTimes(times)) return PosixFailure(GUEST_EINVAL); if (const int error = PathError(path)) return PosixFailure(error); - if (times != nullptr) { - for (int i = 0; i < 2; ++i) { - if (times[i].tv_usec < 0 || times[i].tv_usec >= 1000000) return PosixFailure(GUEST_EINVAL); - } - } std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return PosixFailure(error); if (NativeUtimes(native, times, false) != 0) return PosixResult(SceErrorFromErrno(errno)); diff --git a/core/libs/tests/GuestFileTimestamps.cpp b/core/libs/tests/GuestFileTimestamps.cpp index b1e91115f8..62750ec5f3 100644 --- a/core/libs/tests/GuestFileTimestamps.cpp +++ b/core/libs/tests/GuestFileTimestamps.cpp @@ -91,6 +91,15 @@ int main() { } } + const KernelTimeval invalidBeforePath[2]{{1000000000, -1}, {1000000001, 0}}; + for (const char* pathless : {static_cast(nullptr), ""}) { + Require(utimes_nid_postfix(pathless, invalidBeforePath) == -1 && *__error_nid_postfix() == 22, "utimes checks microseconds before the path"); + Require(lutimes_nid_postfix(pathless, invalidBeforePath) == -1 && *__error_nid_postfix() == 22, "lutimes checks microseconds before the path"); + Require(sceKernelUtimes_nid_postfix(pathless, invalidBeforePath) == static_cast(0x80020016u), "sceKernelUtimes checks microseconds before the path"); + } + Require(utimes_nid_postfix(nullptr, nullptr) == -1 && *__error_nid_postfix() == 14, "utimes still reports EFAULT for a null path"); + Require(utimes_nid_postfix("", nullptr) == -1 && *__error_nid_postfix() == 2, "utimes still reports ENOENT for an empty path"); + #ifdef _WIN32 const auto handle = CreateFileW(std::filesystem::path(path).c_str(), FILE_WRITE_ATTRIBUTES | FILE_READ_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, 0, nullptr); diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index 1921031174..d3fc1a7ab6 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -45,6 +45,7 @@ int APS5_VABI sceKernelFdatasync(int); int APS5_VABI sceKernelWriteThrottlingStatus(std::uint64_t*); int APS5_VABI sceKernelFtruncate(int, long long); int APS5_VABI sceKernelTruncate_nid_postfix(const char*, long long); +int APS5_VABI truncate_nid_postfix(const char*, long long); int APS5_VABI sceKernelUtimes_nid_postfix(const char*, const void*); int APS5_VABI open_nid_postfix(const char*, int, int); int APS5_VABI creat_nid_postfix(const char*, int); @@ -817,6 +818,11 @@ int main() { const auto cwdPermissions = std::filesystem::status(".").permissions(); Require(sceKernelChmod_nid_postfix("", static_cast(cwdPermissions) & 07777) == static_cast(0x80020002u)); Require(sceKernelTruncate_nid_postfix("", 0) == static_cast(0x80020002u)); + Require(sceKernelTruncate_nid_postfix(nullptr, -1) == static_cast(0x80020016u)); + Require(sceKernelTruncate_nid_postfix("", -1) == static_cast(0x80020016u)); + Require(truncate_nid_postfix(nullptr, -1) == -1 && *__error_nid_postfix() == 22); + Require(truncate_nid_postfix("", -1) == -1 && *__error_nid_postfix() == 22); + Require(sceKernelTruncate_nid_postfix(nullptr, 0) == static_cast(0x8002000Eu)); const auto cwdWriteTime = std::filesystem::last_write_time("."); const int emptyUtimes = sceKernelUtimes_nid_postfix("", nullptr); std::error_code restoreTimeError; From 16a7cece3b8154ab607fae361c7826319360807b Mon Sep 17 00:00:00 2001 From: priyanshrv-netizen <275132450+priyanshrv-netizen@users.noreply.github.com> Date: Mon, 19 Oct 2026 22:48:24 +0530 Subject: [PATCH 205/269] feat(libc): implement j0, j1, jn, y0, y1 and yn The double Bessel functions follow the system libc.prx: FreeBSD msun e_j0.c, e_j1.c and e_jn.c with every multiply-add the console fuses done as one FMA, and the console's constant results for zero and negative arguments of the second kind. Co-Authored-By: Claude Opus 5.5 (1M context) --- core/libs/prx/libc/CMakeLists.txt | 10 + core/libs/prx/libc/src/Bessel.cpp | 424 ++++++++++++++++++++++++++++++ core/libs/tests/GuestBessel.cpp | 187 +++++++++++++ docs/dev/debt/libc.md | 1 + 4 files changed, 622 insertions(+) create mode 100644 core/libs/prx/libc/src/Bessel.cpp create mode 100644 core/libs/tests/GuestBessel.cpp diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index db26a5c925..4a2bf8df2a 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -42,6 +42,7 @@ target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/VectorAlgorithms.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Math.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/MathExact.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/Bessel.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Random.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/FloatBuiltins.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/HostEntropy.cpp @@ -342,6 +343,15 @@ if(BUILD_TESTING) set_tests_properties(guest_math_exact PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_bessel_tests ${LIBS_INCLUDE_DIR}/tests/GuestBessel.cpp) + target_include_directories(guest_bessel_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_bessel_tests PRIVATE libc) + configure_windows_unwind(guest_bessel_tests) + add_test(NAME guest_bessel COMMAND guest_bessel_tests) + if(WIN32) + set_tests_properties(guest_bessel PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_libc_threads_tests ${LIBS_INCLUDE_DIR}/tests/GuestLibcThreads.cpp) target_include_directories(guest_libc_threads_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_libc_threads_tests PRIVATE libc libkernel) diff --git a/core/libs/prx/libc/src/Bessel.cpp b/core/libs/prx/libc/src/Bessel.cpp new file mode 100644 index 0000000000..7ba3585ad3 --- /dev/null +++ b/core/libs/prx/libc/src/Bessel.cpp @@ -0,0 +1,424 @@ +#include +#include +#include +#include +#include +#include + +#include "prx/libc/include/general/VabiMacros.hpp" + +namespace { + +constexpr double One = 1.0; +constexpr double Huge = 1.0e300; +constexpr double InverseSqrtPi = 5.64189583547756279280e-01; +constexpr double TwoOverPi = 6.36619772367581382433e-01; +constexpr double LogMax = 7.09782712893383973096e+02; + +constexpr double PZeroNumerators[4][6] = { + {0.00000000000000000000e+00, -7.03124999999900357484e-02, -8.08167041275349795626e+00, -2.57063105679704847262e+02, -2.48521641009428822144e+03, -5.25304380490729545272e+03}, + {-1.14125464691894502584e-11, -7.03124940873599280078e-02, -4.15961064470587782438e+00, -6.76747652265167261021e+01, -3.31231299649172967747e+02, -3.46433388365604912451e+02}, + {-2.54704601771951915620e-09, -7.03119616381481654654e-02, -2.40903221549529611423e+00, -2.19659774734883086467e+01, -5.80791704701737572236e+01, -3.14479470594888503854e+01}, + {-8.87534333032526411254e-08, -7.03030995483624743247e-02, -1.45073846780952986357e+00, -7.63569613823527770791e+00, -1.11931668860356747786e+01, -3.23364579351335335033e+00}, +}; +constexpr double PZeroDenominators[4][5] = { + {1.16534364619668181717e+02, 3.83374475364121826715e+03, 4.05978572648472545552e+04, 1.16752972564375915681e+05, 4.76277284146730962675e+04}, + {6.07539382692300335975e+01, 1.05125230595704579173e+03, 5.97897094333855784498e+03, 9.62544514357774460223e+03, 2.40605815922939109441e+03}, + {3.58560338055209726349e+01, 3.61513983050303863820e+02, 1.19360783792111533330e+03, 1.12799679856907414432e+03, 1.73580930813335754692e+02}, + {2.22202997532088808441e+01, 1.36206794218215208048e+02, 2.70470278658083486789e+02, 1.53875394208320329881e+02, 1.46576176948256193810e+01}, +}; +constexpr double QZeroNumerators[4][6] = { + {0.00000000000000000000e+00, 7.32421874999935051953e-02, 1.17682064682252693899e+01, 5.57673380256401856059e+02, 8.85919720756468632317e+03, 3.70146267776887834771e+04}, + {1.84085963594515531381e-11, 7.32421766612684765896e-02, 5.83563508962056953777e+00, 1.35111577286449829671e+02, 1.02724376596164097464e+03, 1.98997785864605384631e+03}, + {4.37741014089738620906e-09, 7.32411180042911447163e-02, 3.34423137516170720929e+00, 4.26218440745412650017e+01, 1.70808091340565596283e+02, 1.66733948696651168575e+02}, + {1.50444444886983272379e-07, 7.32234265963079278272e-02, 1.99819174093815998816e+00, 1.44956029347885735348e+01, 3.16662317504781540833e+01, 1.62527075710929267416e+01}, +}; +constexpr double QZeroDenominators[4][6] = { + {1.63776026895689824414e+02, 8.09834494656449805916e+03, 1.42538291419120476348e+05, 8.03309257119514397345e+05, 8.40501579819060512818e+05, -3.43899293537866615225e+05}, + {8.27766102236537761883e+01, 2.07781416421392987104e+03, 1.88472887785718085070e+04, 5.67511122894947329769e+04, 3.59767538425114471465e+04, -5.35434275601944773371e+03}, + {4.87588729724587182091e+01, 7.09689221056606015736e+02, 3.70414822620111362994e+03, 6.46042516752568917582e+03, 2.51633368920368957333e+03, -1.49247451836156386662e+02}, + {3.03655848355219184498e+01, 2.69348118608049844624e+02, 8.44783757595320139444e+02, 8.82935845112488550512e+02, 2.12666388511798828631e+02, -5.31095493882666946917e+00}, +}; +constexpr double POneNumerators[4][6] = { + {0.00000000000000000000e+00, 1.17187499999988647970e-01, 1.32394806593073575129e+01, 4.12051854307378562225e+02, 3.87474538913960532227e+03, 7.91447954031891731574e+03}, + {1.31990519556243522749e-11, 1.17187493190614097638e-01, 6.80275127868432871736e+00, 1.08308182990189109773e+02, 5.17636139533199752805e+02, 5.28715201363337541807e+02}, + {3.02503916137373618024e-09, 1.17186865567253592491e-01, 3.93297750033315640650e+00, 3.51194035591636932736e+01, 9.10550110750781271918e+01, 4.85590685197364919645e+01}, + {1.07710830106873743082e-07, 1.17176219462683348094e-01, 2.36851496667608785174e+00, 1.22426109148261232917e+01, 1.76939711271687727390e+01, 5.07352312588818499250e+00}, +}; +constexpr double POneDenominators[4][5] = { + {1.14207370375678408436e+02, 3.65093083420853463394e+03, 3.69562060269033463555e+04, 9.76027935934950801311e+04, 3.08042720627888811578e+04}, + {5.92805987221131331921e+01, 9.91401418733614377743e+02, 5.35326695291487976647e+03, 7.84469031749551231769e+03, 1.50404688810361062679e+03}, + {3.47913095001251519989e+01, 3.36762458747825746741e+02, 1.04687139975775130551e+03, 8.90811346398256432622e+02, 1.03787932439639277504e+02}, + {2.14364859363821409488e+01, 1.25290227168402751090e+02, 2.32276469057162813669e+02, 1.17679373287147100768e+02, 8.36463893371618283368e+00}, +}; +constexpr double QOneNumerators[4][6] = { + {0.00000000000000000000e+00, -1.02539062499992714161e-01, -1.62717534544589987888e+01, -7.59601722513950107896e+02, -1.18498066702429587167e+04, -4.84385124285750353010e+04}, + {-2.08979931141764104297e-11, -1.02539050241375426231e-01, -8.05644828123936029840e+00, -1.83669607474888380239e+02, -1.37319376065508163265e+03, -2.61244440453215656817e+03}, + {-5.07831226461766561369e-09, -1.02537829820837089745e-01, -4.61011581139473403113e+00, -5.78472216562783643212e+01, -2.28244540737631695038e+02, -2.19210128478909325622e+02}, + {-1.78381727510958865572e-07, -1.02517042607985553460e-01, -2.75220568278187460720e+00, -1.96636162643703720221e+01, -4.23253133372830490089e+01, -2.13719211703704061733e+01}, +}; +constexpr double QOneDenominators[4][6] = { + {1.61395369700722909556e+02, 7.82538599923348465381e+03, 1.33875336287249578163e+05, 7.19657723683240939863e+05, 6.66601232617776375264e+05, -2.94490264303834643215e+05}, + {8.12765501384335777857e+01, 1.99179873460485964642e+03, 1.74684851924908907677e+04, 4.98514270910352279316e+04, 2.79480751638918118260e+04, -4.71918354795128470869e+03}, + {4.76651550323729509273e+01, 6.73865112676699709482e+02, 3.38015286679526343505e+03, 5.54772909720722782367e+03, 1.90311919338810798763e+03, -1.35201191444307340817e+02}, + {2.95333629060523854548e+01, 2.52981549982190529136e+02, 7.57502834868645436472e+02, 7.39393205320467245656e+02, 1.55949003336666123687e+02, -4.95949898822628210127e+00}, +}; + +constexpr double J0Numerator[] = {1.56249999999999947958e-02, -1.89979294238854721751e-04, 1.82954049532700665670e-06, -4.61832688532103189199e-09}; +constexpr double J0Denominator[] = {1.56191029464890010492e-02, 1.16926784663337450260e-04, 5.13546550207318111446e-07, 1.16614003333790000205e-09}; +constexpr double Y0Numerator[] = { + -7.38042951086872317523e-02, 1.76666452509181115538e-01, -1.38185671945596898896e-02, 3.47453432093683650238e-04, + -3.81407053724364161125e-06, 1.95590137035022920206e-08, -3.98205194132103398453e-11, +}; +constexpr double Y0Denominator[] = {1.27304834834123699328e-02, 7.60068627350353253702e-05, 2.59150851840457805467e-07, 4.41110311332675467403e-10}; +constexpr double J1Numerator[] = {-6.25000000000000000000e-02, 1.40705666955189706048e-03, -1.59955631084035597520e-05, 4.96727999609584448412e-08}; +constexpr double J1Denominator[] = { + 1.91537599538363460805e-02, 1.85946785588630915560e-04, 1.17718464042623683263e-06, 5.04636257076217042715e-09, 1.23542274426137913908e-11, +}; +constexpr double Y1Numerator[] = { + -1.96057090646238940668e-01, 5.04438716639811282616e-02, -1.91256895875763547298e-03, 2.35252600561610495928e-05, -9.19099158039878874504e-08, +}; +constexpr double Y1Denominator[] = { + 1.99167318236649903973e-02, 2.02552581025135171496e-04, 1.35608801097516229404e-06, 6.22741452364621501295e-09, 1.66559246207992079114e-11, +}; + +std::uint32_t HighWord(double x) { + return static_cast(std::bit_cast(x) >> 32); +} + +std::uint32_t LowWord(double x) { + return static_cast(std::bit_cast(x)); +} + +double SecondKindAtZero() { + return -std::numeric_limits::infinity(); +} + +double SecondKindOfNegative() { + return std::bit_cast(0x7ff8000000000000ull); +} + +double Signed(std::uint32_t value) { + return static_cast(static_cast(value)); +} + +double SquareRoot(double x) { + return _mm_cvtsd_f64(_mm_sqrt_sd(_mm_setzero_pd(), _mm_set_sd(x))); +} + +const bool HardwareFma = [] { + __builtin_cpu_init(); + return __builtin_cpu_supports("fma") != 0; +}(); + +[[gnu::target("fma")]] double HardwareFused(double a, double b, double c) { + return _mm_cvtsd_f64(_mm_fmadd_sd(_mm_set_sd(a), _mm_set_sd(b), _mm_set_sd(c))); +} + +double Fused(double a, double b, double c) { + return HardwareFma ? HardwareFused(a, b, c) : std::fma(a, b, c); +} + +template +double Horner(double z, const double (&coefficients)[Count]) { + double result = coefficients[Count - 1]; + for (std::size_t index = Count - 1; index-- > 0;) result = Fused(z, result, coefficients[index]); + return result; +} + +std::size_t AsymptoticRange(std::uint32_t magnitude) { + if (magnitude >= 0x40200000u) return 0; + if (magnitude >= 0x40122e8bu) return 1; + if (magnitude >= 0x4006db6du) return 2; + return 3; +} + +template +double Asymptotic(double x, const double (&numerator)[NumeratorCount], const double (&denominator)[DenominatorCount]) { + const double z = One / (x * x); + return Horner(z, numerator) / Fused(z, Horner(z, denominator), One); +} + +double PZero(double x, std::uint32_t magnitude) { + const std::size_t range = AsymptoticRange(magnitude); + return Asymptotic(x, PZeroNumerators[range], PZeroDenominators[range]) + One; +} + +double QZero(double x, std::uint32_t magnitude) { + const std::size_t range = AsymptoticRange(magnitude); + return (Asymptotic(x, QZeroNumerators[range], QZeroDenominators[range]) + -0.125) / x; +} + +double POne(double x, std::uint32_t magnitude) { + const std::size_t range = AsymptoticRange(magnitude); + return Asymptotic(x, POneNumerators[range], POneDenominators[range]) + One; +} + +double QOne(double x, std::uint32_t magnitude) { + const std::size_t range = AsymptoticRange(magnitude); + return (Asymptotic(x, QOneNumerators[range], QOneDenominators[range]) + 0.375) / x; +} + +} + +extern "C" { + +double APS5_VABI j0_nid_postfix(double x) { + const std::uint32_t magnitude = HighWord(x) & 0x7fffffffu; + if (magnitude >= 0x7ff00000u) return One / (x * x); + const double absolute = std::fabs(x); + if (magnitude >= 0x40000000u) { + const double s = std::sin(absolute); + const double c = std::cos(x); + double ss = s - c; + double cc = c + s; + if (magnitude < 0x7fe00000u) { + const double z = -std::cos(absolute + absolute); + if (s * c < 0.0) cc = z / ss; + else ss = z / cc; + } + if (magnitude > 0x48000000u) return cc * InverseSqrtPi / SquareRoot(absolute); + const double u = PZero(absolute, magnitude); + const double v = QZero(absolute, magnitude); + return Fused(u, cc, -(ss * v)) * InverseSqrtPi / SquareRoot(absolute); + } + if (Huge + absolute > One && magnitude < 0x3f200000u) { + if (magnitude < 0x3e400000u) return One; + return Fused(x * x, -0.25, One); + } + const double z = x * x; + const double r = z * Horner(z, J0Numerator); + const double s = Fused(z, Horner(z, J0Denominator), One); + if (magnitude < 0x3ff00000u) return Fused(z, r / s + -0.25, One); + const double u = absolute * 0.5; + return Fused(One + u, One - u, z * (r / s)); +} + +double APS5_VABI y0_nid_postfix(double x) { + const std::uint32_t high = HighWord(x); + const std::uint32_t magnitude = high & 0x7fffffffu; + if (magnitude >= 0x7ff00000u) return One / Fused(x, x, x); + if ((magnitude | LowWord(x)) == 0) return SecondKindAtZero(); + if ((high & 0x80000000u) != 0) return SecondKindOfNegative(); + if (magnitude >= 0x40000000u) { + const double s = std::sin(x); + const double c = std::cos(x); + double ss = s - c; + double cc = s + c; + if (magnitude < 0x7fe00000u) { + const double z = -std::cos(x + x); + if (s * c < 0.0) cc = z / ss; + else ss = z / cc; + } + if (magnitude > 0x48000000u) return ss * InverseSqrtPi / SquareRoot(x); + const double u = PZero(x, magnitude); + const double v = QZero(x, magnitude); + return Fused(u, ss, cc * v) * InverseSqrtPi / SquareRoot(x); + } + if (magnitude <= 0x3e400000u) return Fused(TwoOverPi, std::log(x), Y0Numerator[0]); + const double z = x * x; + const double u = Horner(z, Y0Numerator); + const double v = Fused(z, Horner(z, Y0Denominator), One); + return Fused(std::log(x) * j0_nid_postfix(x), TwoOverPi, u / v); +} + +double APS5_VABI j1_nid_postfix(double x) { + const std::uint32_t high = HighWord(x); + const std::uint32_t magnitude = high & 0x7fffffffu; + if (magnitude >= 0x7ff00000u) return One / x; + if (magnitude >= 0x40000000u) { + const double y = std::fabs(x); + const double s = std::sin(y); + const double c = std::cos(x); + double ss = -s - c; + double cc = s - c; + if (magnitude < 0x7fe00000u) { + const double z = std::cos(y + y); + if (s * c > 0.0) cc = z / ss; + else ss = z / cc; + } + double result; + if (magnitude > 0x48000000u) { + result = cc * InverseSqrtPi / SquareRoot(y); + } else { + const double u = POne(y, magnitude); + const double v = QOne(y, magnitude); + result = Fused(u, cc, -(ss * v)) * InverseSqrtPi / SquareRoot(y); + } + return (high & 0x80000000u) != 0 ? -result : result; + } + if (Huge + x > One && magnitude < 0x3e400000u) return x * 0.5; + const double z = x * x; + const double r = z * Horner(z, J1Numerator); + const double s = Fused(z, Horner(z, J1Denominator), One); + return Fused(x, 0.5, r * x / s); +} + +double APS5_VABI y1_nid_postfix(double x) { + const std::uint32_t high = HighWord(x); + const std::uint32_t magnitude = high & 0x7fffffffu; + if (magnitude >= 0x7ff00000u) return One / Fused(x, x, x); + if ((magnitude | LowWord(x)) == 0) return SecondKindAtZero(); + if ((high & 0x80000000u) != 0) return SecondKindOfNegative(); + if (magnitude >= 0x40000000u) { + const double s = std::sin(x); + const double c = std::cos(x); + double ss = -s - c; + double cc = s - c; + if (magnitude < 0x7fe00000u) { + const double z = std::cos(x + x); + if (s * c > 0.0) cc = z / ss; + else ss = z / cc; + } + if (magnitude > 0x48000000u) return ss * InverseSqrtPi / SquareRoot(x); + const double u = POne(x, magnitude); + const double v = QOne(x, magnitude); + return Fused(u, ss, cc * v) * InverseSqrtPi / SquareRoot(x); + } + if (magnitude <= 0x3c900000u) return -TwoOverPi / x; + const double z = x * x; + const double u = Horner(z, Y1Numerator); + const double v = Fused(z, Horner(z, Y1Denominator), One); + return Fused(x, u / v, Fused(std::log(x), j1_nid_postfix(x), -One / x) * TwoOverPi); +} + +double APS5_VABI jn_nid_postfix(int order, double x) { + const std::uint32_t high = HighWord(x); + const std::uint32_t low = LowWord(x); + const std::uint32_t magnitude = high & 0x7fffffffu; + if ((magnitude | (low != 0 ? 1u : 0u)) > 0x7ff00000u) return x + x; + const double argument = order < 0 ? -x : x; + const std::uint32_t n = order < 0 ? 0u - static_cast(order) : static_cast(order); + if (n == 1) return j1_nid_postfix(argument); + if (n == 0) return j0_nid_postfix(argument); + const bool negative = ((high ^ static_cast(order)) & 0x80000000u) != 0; + double result = 0.0; + if (magnitude < 0x7ff00000u && (magnitude | low) != 0) { + const double absolute = std::fabs(x); + const double orderValue = Signed(n); + if (absolute >= orderValue) { + if (magnitude >= 0x52d00000u) { + double sum; + switch (n & 3u) { + case 0: sum = std::sin(absolute) + std::cos(argument); break; + case 1: sum = std::sin(absolute) - std::cos(argument); break; + case 2: sum = -std::cos(argument) - std::sin(absolute); break; + default: sum = std::cos(argument) - std::sin(absolute); break; + } + result = sum * InverseSqrtPi / SquareRoot(absolute); + } else { + double previous = j0_nid_postfix(absolute); + result = j1_nid_postfix(absolute); + std::uint32_t twice = 2; + for (std::uint32_t remaining = n - 1; remaining != 0; --remaining) { + const double next = Fused(result, Signed(twice) / absolute, -previous); + previous = result; + result = next; + twice += 2; + } + } + } else if (magnitude < 0x3e100000u) { + if (n <= 33u) { + const double half = absolute * 0.5; + double power = half; + double factorial = One; + for (std::uint32_t index = 2; index <= n; ++index) { + power = half * power; + factorial *= Signed(index); + } + result = power / factorial; + } + } else { + const double h = 2.0 / absolute; + const double w = Signed(2u * n) / absolute; + double q0 = w; + double z = h + w; + double q1 = Fused(w, z, -One); + std::uint32_t k = 1; + while (q1 < 1.0e9) { + z = h + z; + ++k; + const double next = Fused(z, q1, -q0); + q0 = q1; + q1 = next; + } + const std::int32_t lowest = static_cast(2u * n); + std::uint32_t index = 2u * (k + n); + double t = 0.0; + do { + t = One / (Signed(index) / absolute - t); + index -= 2u; + } while (static_cast(index) >= lowest); + const double bound = std::log(std::fabs(h * orderValue)) * orderValue; + double a = t; + double b = One; + double di = Signed(2u * n - 2u); + std::int32_t remaining = static_cast(n); + const bool rescale = !(bound < LogMax); + do { + const double next = b * di / absolute - a; + di += -2.0; + a = b; + b = next; + if (rescale && b > 1.0e100) { + t /= b; + a /= b; + b = One; + } + --remaining; + } while (remaining > 1); + const double z0 = j0_nid_postfix(absolute); + const double z1 = j1_nid_postfix(absolute); + result = std::fabs(z0) >= std::fabs(z1) ? z0 * t / b : t * z1 / a; + } + } + const double flipped = negative ? -result : result; + return (n & 1u) != 0 ? flipped : result; +} + +double APS5_VABI yn_nid_postfix(int order, double x) { + const std::uint32_t high = HighWord(x); + const std::uint32_t low = LowWord(x); + const std::uint32_t magnitude = high & 0x7fffffffu; + if ((magnitude | (low != 0 ? 1u : 0u)) > 0x7ff00000u) return x + x; + if ((magnitude | low) == 0) return SecondKindAtZero(); + if ((high & 0x80000000u) != 0) return SecondKindOfNegative(); + std::uint32_t n = static_cast(order); + std::int32_t sign = 1; + if (order < 0) { + n = 0u - n; + sign = 1 - static_cast((n << 1) & 2u); + } + if (n == 1) return static_cast(sign) * y1_nid_postfix(x); + if (n == 0) return y0_nid_postfix(x); + if (magnitude == 0x7ff00000u) return 0.0; + double result; + if (magnitude >= 0x52d00000u) { + const double s = std::sin(x); + const double c = std::cos(x); + double sum; + switch (n & 3u) { + case 0: sum = s - c; break; + case 1: sum = -s - c; break; + case 2: sum = c - s; break; + default: sum = s + c; break; + } + result = sum * InverseSqrtPi / SquareRoot(x); + } else { + double previous = y0_nid_postfix(x); + result = y1_nid_postfix(x); + std::uint32_t twice = 2; + std::int32_t index = 2; + while (HighWord(result) != 0xfff00000u) { + const double next = Fused(result, Signed(twice) / x, -previous); + previous = result; + result = next; + if (index >= static_cast(n)) break; + ++index; + twice += 2; + } + } + return sign > 0 ? result : -result; +} + +} diff --git a/core/libs/tests/GuestBessel.cpp b/core/libs/tests/GuestBessel.cpp new file mode 100644 index 0000000000..75c94ec4ac --- /dev/null +++ b/core/libs/tests/GuestBessel.cpp @@ -0,0 +1,187 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +double APS5_VABI j0_nid_postfix(double); +double APS5_VABI j1_nid_postfix(double); +double APS5_VABI jn_nid_postfix(int, double); +double APS5_VABI y0_nid_postfix(double); +double APS5_VABI y1_nid_postfix(double); +double APS5_VABI yn_nid_postfix(int, double); +int* APS5_VABI __error_nid_postfix(); +} + +static void Require(bool value) { if (!value) std::abort(); } + +static double Value(std::uint64_t bits) { return std::bit_cast(bits); } + +static std::uint64_t Bits(double value) { return std::bit_cast(value); } + +struct UnaryCase { std::uint64_t input; std::uint64_t output; }; +struct OrderCase { int order; std::uint64_t input; std::uint64_t output; }; + +static void CheckUnary(const char* name, double (APS5_VABI *function)(double), const UnaryCase* cases, std::size_t count) { + for (std::size_t index = 0; index < count; ++index) { + *__error_nid_postfix() = 12345; + const double result = function(Value(cases[index].input)); + if (Bits(result) != cases[index].output || *__error_nid_postfix() != 12345) { + std::fprintf(stderr, "Guest %s failed for %016llx: %016llx\n", name, static_cast(cases[index].input), + static_cast(Bits(result))); + std::abort(); + } + } +} + +static void CheckOrder(const char* name, double (APS5_VABI *function)(int, double), const OrderCase* cases, std::size_t count) { + for (std::size_t index = 0; index < count; ++index) { + *__error_nid_postfix() = 12345; + const double result = function(cases[index].order, Value(cases[index].input)); + if (Bits(result) != cases[index].output || *__error_nid_postfix() != 12345) { + std::fprintf(stderr, "Guest %s failed for %d, %016llx: %016llx\n", name, cases[index].order, + static_cast(cases[index].input), static_cast(Bits(result))); + std::abort(); + } + } +} + +static void CheckFirstKind() { + const UnaryCase zero[] = { + {0x0000000000000000ull, 0x3ff0000000000000ull}, {0x8000000000000000ull, 0x3ff0000000000000ull}, {0x3ddb7cdfd9d7bdbbull, 0x3ff0000000000000ull}, + {0x3e40000000000000ull, 0x3ff0000000000000ull}, {0x3eb0000000000000ull, 0x3feffffffffff800ull}, {0x3f1fffffffffffffull, 0x3feffffffe000000ull}, + {0x3f20000000000000ull, 0x3feffffffe000000ull}, {0x3fd0000000000000ull, 0x3fef807fc72aa864ull}, {0xbfe0000000000000ull, 0x3fee07f1d54c3f34ull}, + {0x3fe8000000000000ull, 0x3feba7df6a752a19ull}, {0x3fefffffffffffffull, 0x3fe87c7fdbd7b8f0ull}, {0x3ff0000000000000ull, 0x3fe87c7fdbd7b8f0ull}, + {0xbff8000000000000ull, 0x3fe060e46ce9651bull}, {0x3fffffffffffffffull, 0x3fcca873fb24cefcull}, {0x7ff0000000000000ull, 0x0000000000000000ull}, + {0xfff0000000000000ull, 0x0000000000000000ull}, {0x7ff8000000000123ull, 0x7ff8000000000123ull}, {0x3fee4317b35275e1ull, 0x3fe93c4854d71c27ull}, + {0x3fe7084d7429a0caull, 0x3febfce08c913631ull}, {0x3ff896d4ffdc2a13ull, 0x3fdf6faac7e7d3ceull}, {0x3ff28d079588dbb8ull, 0x3fe61dc352e1dc33ull}, + {0x3ffe7d3678a4d9a8ull, 0x3fd1d44a54769967ull}, + }; + CheckUnary("j0", j0_nid_postfix, zero, std::size(zero)); + const UnaryCase one[] = { + {0x0000000000000000ull, 0x0000000000000000ull}, {0x8000000000000000ull, 0x8000000000000000ull}, {0x3ddb7cdfd9d7bdbbull, 0x3dcb7cdfd9d7bdbbull}, + {0x3e40000000000000ull, 0x3e30000000000000ull}, {0x3eb0000000000000ull, 0x3e9ffffffffffc00ull}, {0x3f1fffffffffffffull, 0x3f0ffffffeffffffull}, + {0x3f20000000000000ull, 0x3f0fffffff000000ull}, {0x3fd0000000000000ull, 0x3fbfc02a9c749ee9ull}, {0xbfe0000000000000ull, 0xbfcf02a71f4870d7ull}, + {0x3fe8000000000000ull, 0x3fd65a01d66b68bdull}, {0x3fefffffffffffffull, 0x3fdc29c9ee970c6cull}, {0x3ff0000000000000ull, 0x3fdc29c9ee970c6cull}, + {0xbff8000000000000ull, 0xbfe1da9da9d6fc81ull}, {0x3fffffffffffffffull, 0x3fe27487958371f0ull}, {0x7ff0000000000000ull, 0x0000000000000000ull}, + {0xfff0000000000000ull, 0x8000000000000000ull}, {0x7ff8000000000123ull, 0x7ff8000000000123ull}, {0x3fecd8b073b795c0ull, 0x3fda038c68b70948ull}, + {0x3fffe99bfa2fcbc2ull, 0x3fe2775e2433b1d8ull}, {0x3ffa917f997cc231ull, 0x3fe267a06350cc21ull}, + }; + CheckUnary("j1", j1_nid_postfix, one, std::size(one)); +} + +static void CheckSecondKind() { + const UnaryCase zero[] = { + {0x0000000000000000ull, 0xfff0000000000000ull}, {0x8000000000000000ull, 0xfff0000000000000ull}, {0xbff0000000000000ull, 0x7ff8000000000000ull}, + {0x8000000000000001ull, 0x7ff8000000000000ull}, {0x7ff0000000000000ull, 0x0000000000000000ull}, + }; + CheckUnary("y0", y0_nid_postfix, zero, std::size(zero)); + const UnaryCase one[] = { + {0x0000000000000000ull, 0xfff0000000000000ull}, {0x8000000000000000ull, 0xfff0000000000000ull}, {0xbff0000000000000ull, 0x7ff8000000000000ull}, + {0x8000000000000001ull, 0x7ff8000000000000ull}, {0x7ff0000000000000ull, 0x0000000000000000ull}, {0x3bc79ca10c924223ull, 0xc40b9be638027fc9ull}, + {0x3c90000000000000ull, 0xc3445f306dc9c883ull}, {0x0000000000000001ull, 0xfff0000000000000ull}, + }; + CheckUnary("y1", y1_nid_postfix, one, std::size(one)); + Require(std::isnan(y0_nid_postfix(-std::numeric_limits::infinity())) && std::isnan(y1_nid_postfix(-std::numeric_limits::infinity()))); + Require(std::isnan(y0_nid_postfix(std::numeric_limits::quiet_NaN())) && std::isnan(y1_nid_postfix(std::numeric_limits::quiet_NaN()))); + for (const double input : {0.0, -0.0, -1.0, -std::numeric_limits::denorm_min()}) { + std::feclearexcept(FE_ALL_EXCEPT); + y0_nid_postfix(input); + y1_nid_postfix(input); + yn_nid_postfix(3, input); + Require(std::fetestexcept(FE_DIVBYZERO | FE_INVALID) == 0); + } +} + +static void CheckIntegerOrders() { + const OrderCase first[] = { + {2, 0x3ddb7cdfd9d7bdbbull, 0x3b979ca10c924224ull}, {5, 0xbddb7cdfd9d7bdbbull, 0xb4cfed4f3ec0c466ull}, {33, 0x3e00000000000000ull, 0x0000000000000000ull}, + {34, 0x3e00000000000000ull, 0x0000000000000000ull}, {-3, 0x3e00000000000000ull, 0xb9c5555555555555ull}, {2, 0x3ff8000000000000ull, 0x3fcdb50c80d5039aull}, + {5, 0x3ff0000000000000ull, 0x3f305e3d487e8377ull}, {10, 0x3fe0000000000000ull, 0x3d52637b5990470aull}, {-3, 0x3ff4000000000000ull, 0xbfa2e068dde7f417ull}, + {3, 0xbff4000000000000ull, 0xbfa2e068dde7f417ull}, {4, 0xbff4000000000000ull, 0x3f781279da92ee26ull}, {40, 0x3ff8000000000000ull, 0x34f2a1a3311d8462ull}, + {300, 0x3ff0000000000000ull, 0x0000000000000000ull}, {3, 0x8000000000000000ull, 0x8000000000000000ull}, {3, 0xfff0000000000000ull, 0x8000000000000000ull}, + {2, 0xfff0000000000000ull, 0x0000000000000000ull}, {7, 0x7ff8000000000123ull, 0x7ff8000000000123ull}, {-1, 0x3fe0000000000000ull, 0xbfcf02a71f4870d7ull}, + {0, 0xbfe0000000000000ull, 0x3fee07f1d54c3f34ull}, {3, 0x3fe91c9d97f75a72ull, 0x3f83d672272043afull}, {3, 0x3ff33bf06c96fafdull, 0x3fa0ea842d47fdfdull}, + {3, 0x3ffa0a4f9419df40ull, 0x3fb36db8f4e44fabull}, {3, 0x3ffb2f9912a3a9a6ull, 0x3fb5c4cf9b0be24full}, + }; + CheckOrder("jn", jn_nid_postfix, first, std::size(first)); + const OrderCase second[] = { + {2, 0x7ff0000000000000ull, 0x0000000000000000ull}, {-3, 0x7ff0000000000000ull, 0x0000000000000000ull}, {3, 0x0000000000000000ull, 0xfff0000000000000ull}, + {-4, 0x8000000000000000ull, 0xfff0000000000000ull}, {3, 0xbff0000000000000ull, 0x7ff8000000000000ull}, {5, 0x7ff8000000000123ull, 0x7ff8000000000123ull}, + {INT_MIN, 0x7ff0000000000000ull, 0x0000000000000000ull}, + }; + CheckOrder("yn", yn_nid_postfix, second, std::size(second)); + for (const double x : {0.75, 2.5, 3.0, 17.25, 1.0e5, 0x1p303}) { + Require(Bits(jn_nid_postfix(0, x)) == Bits(j0_nid_postfix(x)) && Bits(jn_nid_postfix(1, x)) == Bits(j1_nid_postfix(x))); + Require(Bits(jn_nid_postfix(-1, x)) == Bits(j1_nid_postfix(-x)) && Bits(jn_nid_postfix(-3, x)) == Bits(-jn_nid_postfix(3, x))); + Require(Bits(jn_nid_postfix(4, -x)) == Bits(jn_nid_postfix(4, x)) && Bits(jn_nid_postfix(5, -x)) == Bits(-jn_nid_postfix(5, x))); + Require(Bits(yn_nid_postfix(0, x)) == Bits(y0_nid_postfix(x)) && Bits(yn_nid_postfix(1, x)) == Bits(y1_nid_postfix(x))); + Require(Bits(yn_nid_postfix(-1, x)) == Bits(-y1_nid_postfix(x)) && Bits(yn_nid_postfix(-4, x)) == Bits(yn_nid_postfix(4, x))); + if (x < 0x1p302) Require(Bits(yn_nid_postfix(INT_MIN, x)) == Bits(yn_nid_postfix(2, x))); + } +} + +static bool Near(double actual, double expected, double tolerance) { + return std::fabs(actual - expected) <= std::fabs(expected) * tolerance; +} + +struct ReferenceCase { int order; double x; double value; }; + +static void CheckValues() { + const ReferenceCase first[] = { + {0, 0.3, 0.97762624653829609}, {0, 1.7, 0.39798485944610949}, {0, 2.5, -0.048383776468197996}, {0, 7.25, 0.29199692419177900}, + {0, 25.0, 0.096266783275958116}, {0, 123.5, -0.070484621663252289}, {1, 0.3, 0.14831881627310401}, {1, 1.7, 0.57776523152902322}, + {1, 2.5, 0.49709410246427404}, {1, 7.25, 0.068581700653131745}, {1, 25.0, -0.12535024958028990}, {1, 123.5, -0.013948992295178035}, + {2, 0.3, 0.011165861949063964}, {2, 1.7, 0.28173894235274136}, {2, 2.5, 0.44605905843961723}, {2, 7.25, -0.27307783435643231}, + {2, 25.0, -0.10629480324238131}, {2, 123.5, 0.070258727051184628}, {3, 0.3, 0.00055934304774884612}, {3, 1.7, 0.085149926948015264}, + {3, 2.5, 0.21660039103911352}, {3, 7.25, -0.21924533340150819}, {3, 25.0, 0.10834308106150890}, {3, 123.5, 0.016224578596430978}, + {7, 0.3, 3.3805443102187481e-10}, {7, 1.7, 5.8087210824311728e-5}, {7, 2.5, 0.00077655318753348495}, {7, 7.25, 0.25934915542688296}, + {7, 25.0, -0.010168168212703074}, {7, 123.5, 0.027311804641119322}, + }; + const ReferenceCase second[] = { + {0, 0.3, -0.80727357780451947}, {0, 1.7, 0.45202700018163463}, {0, 2.5, 0.49807035961523189}, {0, 7.25, 0.048422529865202829}, + {0, 25.0, -0.12724943226800614}, {0, 123.5, -0.013663522162735081}, {1, 0.3, -2.2931051383885290}, {1, 1.7, -0.28472624506406838}, + {1, 2.5, 0.14591813796678580}, {1, 7.25, -0.28934799419758705}, {1, 25.0, -0.098829964783237410}, {1, 123.5, 0.070429882264699084}, + {2, 0.3, -14.480094011452341}, {2, 1.7, -0.78699905319818566}, {2, 2.5, -0.38133584924180325}, {2, 7.25, -0.12824266619557167}, + {2, 25.0, 0.11934303508534715}, {2, 123.5, 0.014804087057709964}, {3, 0.3, -190.77481501430935}, {3, 1.7, -1.5670362330493096}, + {3, 2.5, -0.75605549675367100}, {3, 7.25, 0.21859341974485785}, {3, 25.0, 0.11792485039689295}, {3, 123.5, -0.069950397663639652}, + {7, 0.3, -1.3463966616414596e+8}, {7, 1.7, -807.61347285004828}, {7, 2.5, -62.829855967240050}, {7, 7.25, -0.35448064613382143}, + {7, 25.0, 0.16252257251113247}, {7, 123.5, -0.066461555200665692}, + }; + for (const auto& test : first) { + const double result = test.order == 0 ? j0_nid_postfix(test.x) : test.order == 1 ? j1_nid_postfix(test.x) : jn_nid_postfix(test.order, test.x); + if (!Near(result, test.value, 1e-12) || !Near(jn_nid_postfix(test.order, -test.x), test.order % 2 != 0 ? -test.value : test.value, 1e-12)) { + std::fprintf(stderr, "Guest J%d(%g) = %.17g, expected %.17g\n", test.order, test.x, result, test.value); + std::abort(); + } + } + for (const auto& test : second) { + const double result = test.order == 0 ? y0_nid_postfix(test.x) : test.order == 1 ? y1_nid_postfix(test.x) : yn_nid_postfix(test.order, test.x); + if (!Near(result, test.value, 1e-12) || !Near(yn_nid_postfix(test.order, test.x), test.value, 1e-12)) { + std::fprintf(stderr, "Guest Y%d(%g) = %.17g, expected %.17g\n", test.order, test.x, result, test.value); + std::abort(); + } + } + *__error_nid_postfix() = 12345; + for (const double x : {0x1p-30, 0.5, 3.0, 1.0e10, 0x1p310, std::numeric_limits::max()}) { + Require(std::isfinite(j0_nid_postfix(x)) && std::isfinite(j1_nid_postfix(x)) && std::isfinite(y0_nid_postfix(x)) && std::isfinite(y1_nid_postfix(x))); + Require(std::isfinite(jn_nid_postfix(6, x)) && !std::isnan(yn_nid_postfix(6, x))); + } + Require(*__error_nid_postfix() == 12345); + *__error_nid_postfix() = 0; +} + +int main() { + CheckFirstKind(); + CheckSecondKind(); + CheckIntegerOrders(); + CheckValues(); + return 0; +} diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index d9b2e588a8..3c97cefab7 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -76,6 +76,7 @@ - [acoshl, asinhl, atanhl, atan2l, cbrtl, erfl, erfcl, expm1l, hypotl, log1pl, log2l, lgammal, tgammal, j0f, j1f, jnf, y0f, y1f and ynf](../../../core/libs/prx/libc/src/Math.cpp) (libc) return the host C library's results (the float Bessel functions through its double ones), not those of the console's own code, so the last bits can differ (`y0f`, `y1f`, `ynf` follow the console at `+-0`). As in the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x56410), `lgammal` does not store the sign in `signgam`; `hypotl` returns infinity for an infinite argument even when the other is a NaN, as C11 F.10.4.3 requires. [hypot3, hypot3f and hypot3l](../../../core/libs/prx/libc/src/Math.cpp) follow that module (0x51D30, 0x520D0, 0x52420): the first NaN of x, y and z is returned unchanged, any infinity gives positive infinity, a zero argument gives the two-argument hypot of the other two, and otherwise the largest magnitude is combined with the hypot of the other two - [asinl](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the `asinl` of the `libc.prx` bundled with PPSA28319 (disassembly), FreeBSD msun's [`e_asinl.c`](https://github.com/freebsd/freebsd-src/blob/9f0432041b8fb69832f10c32da50a67f34feeb61/lib/msun/src/e_asinl.c) with the ld80 [`invtrig.c`](https://github.com/freebsd/freebsd-src/blob/9f0432041b8fb69832f10c32da50a67f34feeb61/lib/msun/ld80/invtrig.c) coefficients, which the binary holds as the same 80-bit constants. For |x| > 1, the infinities and NaN, Sony's error helper raises `FE_INVALID` and sets `errno` to `EDOM` before returning `(x - x) / (x - x)`, so a NaN argument also sets `EDOM`; msun itself sets no `errno`. 20 million random arguments in (-1, 1) give the same bits as that code run on a Windows host. The result needs the x87 precision control at 64 bits, as FreeBSD amd64 starts threads (control word `0x37f`), but guest threads do not set it: on Windows a thread started by `_beginthreadex` runs at 53 bits (`0x27f`), where `asinl(0.75)` returns mantissa `0xd91a98ae3406e000` instead of `0xd91a98ae3406e041`. The system `libSceLibcInternal` that RetroArch (PPSA99169) imports is assumed to have the same code - [atanhf](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the `atanhf` of the `libc.prx` bundled with PPSA28319 (disassembly): FreeBSD msun's [`e_atanhf.c`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/e_atanhf.c) calling msun's [`log1pf`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/s_log1pf.c), whose paths for the positive arguments it passes are inlined here; the exported `log1pf` is still the host's. For |x| > 1, the infinities and NaN, Sony's error helper raises `FE_INVALID` and sets `errno` to `EDOM` before returning `(x - x) / (x - x)`, and for |x| = 1 it raises `FE_DIVBYZERO` and sets `errno` to `ERANGE` before returning `x / 0`; msun itself sets no `errno`. That code, run on a Windows host, gives the same bits, `errno` and floating-point exceptions as this implementation for all 2^32 arguments; it was not run on Linux. That the system `libSceLibcInternal`, which Dusklight (PPSA99305) imports while bundling a zero-return `libc.prx`, has the same code is assumed +- [j0, j1, jn, y0, y1 and yn](../../../core/libs/prx/libc/src/Bessel.cpp) (libc) call the host's `sin`, `cos`, `sincos` and `log`, so the last bits can differ from the console. The rest follows the system `libc.prx` built as `J03093718` (disassembly), which given the console's own `sin`, `cos`, `sincos` and `log` gave the same bits and exception flags for 90 million arguments: msun's [`e_j0.c`, `e_j1.c` and `e_jn.c`](https://github.com/freebsd/freebsd-src/tree/release/10.0.0/lib/msun/src) with each multiply-add the console fuses done as one FMA. As in that release, before FreeBSD commit `186f620727`, `y0`, `y1` and `yn` return `-inf` for ±0 and `0x7ff8000000000000` for a negative argument without raising an exception. The console's `y0f`, `y1f` and `ynf` raise none either, unlike [ours](../../../core/libs/prx/libc/src/Math.cpp), which raise `FE_DIVBYZERO` at ±0. The order wraps at 32 bits as on the console: `jn(1 << 30, x)` never returns for 2^-29 <= |x| < 2^30 - [getenv, setenv, unsetenv, putenv](../../../core/libs/prx/libc/src/Environment.cpp) (libc) keep one guest environment that starts empty, like the `environ` libkernel exports, and never reads or changes the host's. A title sees only the variables it sets itself; host variables such as `HOME` and `PATH` name host paths a guest cannot use (PS5MC, PPSA99420, exits with `XBMC_Run returned -1` when it gets the host's `HOME`). Whether the console starts a title with any variables set is not measured - [mergesort](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libSceLibcInternal, implemented in libc) checks its arguments like the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x762D0): an element size under 4 sets `errno` to `EINVAL` (22) and returns -1, an empty array returns 0, and a failed `malloc` of the count times the size plus 8 bytes returns -1. The sort is a stable bottom-up merge, not FreeBSD's natural merge with its insertion sort for short runs, so the comparator is called in a different order and number of times; the result is the same for a consistent comparator. A null base or comparator and a size product that overflows, which the console does not check, throw - On macOS the [write watch](../../../core/libs/prx/libc/src/GuestWriteWatch.cpp) write-protects watched pages and records the first write to each from a SIGBUS/SIGSEGV handler, because macOS has no counterpart to `userfaultfd` write-protect or `MEM_WRITE_WATCH`. The protection is real, so: From c1c416fc4319bff2d95af89b85d48c839e748b06 Mon Sep 17 00:00:00 2001 From: To3akaRin <131595336+To3akaRin@users.noreply.github.com> Date: Tue, 20 Oct 2026 09:09:09 +0800 Subject: [PATCH 206/269] docs(build): translate quick syntax checks into English --- docs/dev/BUILD.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index 3c290710dd..2fd3cbfa5a 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -84,15 +84,15 @@ SDL and FreeType settings forced by the root `CMakeLists.txt` cannot be overridd With `ANYPS5_USE_SYSTEM_DEPS=ON` those libraries come from the system (on Debian and Ubuntu: `libsdl2-dev`, `libfreetype-dev`, `libvulkan-dev`, `spirv-headers`, `glslang-dev`, `glslang-tools` and `spirv-tools`). A missing package, Vulkan-Headers older than 1.4.310 (the agc driver uses `VK_KHR_maintenance8`), or an SDL2 without the HIDAPI joystick driver (sdl2-compat over SDL3, or a build with HIDAPI disabled, which breaks the pad and disk-audio tests), fails the configure with an error and never falls back to the submodule. The forced SDL and FreeType settings from the root `CMakeLists.txt` do not apply with the flag ON: the system SDL2 comes with whatever its distribution enabled. The prx libraries are shared objects, so SDL2 is linked from the system as a shared library; the default build keeps its own static SDL2. `ffmpeg-core`, `libjpeg-turbo`, `stb`, `VulkanMemoryAllocator` and `LibAtrac9` always stay bundled and still need `git submodule update --init`. -## 快速语法检查 +## Quick syntax checks -配置构建目录后,可使用 `tools/check_compile.py` 检查指定源文件。工具从 `compile_commands.json` 或 Ninja 读取编译命令,保留编译选项,并去掉对象文件及依赖文件输出参数。`-MF`、`-MT`、`-MQ` 和 `-MJ` 支持分离及连写形式,避免在 `-Werror` 下因无效的依赖输出参数误报失败。 +After configuring a build directory, use `tools/check_compile.py` to check the given source files. The tool reads their compile commands from `compile_commands.json` or Ninja, preserving compilation options while dropping object and dependency output options. It handles `-MF`, `-MT`, `-MQ` and `-MJ` with separate or attached values so that unused output options do not cause false failures under `-Werror`. ```sh python3 tools/check_compile.py --build build-relinker core/relinker/main.cpp ``` -真实编译错误仍返回失败;语法检查不替代完整构建和测试。 +Real compile errors still fail. Syntax checks do not replace a full build and test run. ## Write watch tests From 7535eb45fcc007520ac89ca6ec107f280488b6bd Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Tue, 20 Oct 2026 03:11:14 +0200 Subject: [PATCH 207/269] fix(libSceLibcInternal): start fdopendir at the descriptor position and set FD_CLOEXEC fdopendir listed the directory from its first entry whatever position the descriptor had, and left FD_CLOEXEC unchanged. FreeBSD 11.4 fdopendir sets FD_CLOEXEC on the descriptor and preserves the initial seek position (lib/libc/gen/opendir.c). The Windows directory handle now skips the entries the descriptor already consumed, the Linux handle duplicates the descriptor, and the test requires both. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libSceLibcInternal/Directory.cpp | 7 ++++++- core/libs/prx/libc/include/GuestDirectory.hpp | 2 +- core/libs/prx/libc/src/Directory.cpp | 17 ++++++++++++++++- .../File/include/DirectoryDescriptor.hpp | 1 + .../libkernel/File/src/DirectoryDescriptor.cpp | 10 ++++++++++ core/libs/tests/GuestFdopendir.cpp | 17 +++++++++++++++++ docs/dev/debt/libSceLibcInternal.md | 2 +- 7 files changed, 52 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceLibcInternal/Directory.cpp b/core/libs/prx/libSceLibcInternal/Directory.cpp index 937f52fd97..48a1614e85 100644 --- a/core/libs/prx/libSceLibcInternal/Directory.cpp +++ b/core/libs/prx/libSceLibcInternal/Directory.cpp @@ -1,16 +1,21 @@ #include "prx/libc/include/GuestDirectory.hpp" #include "prx/libkernel/File/include/DirectoryDescriptor.hpp" #include +#include extern "C" int APS5_VABI close_nid_postfix(int d); +extern "C" int APS5_VABI fcntl_nid_postfix(int descriptor, int command, ...); extern "C" int* APS5_VABI __error_nid_postfix(); extern "C" void* APS5_VABI fdopendir_nid_postfix(int fd) { + constexpr int SetDescriptorFlags = 2; + constexpr int CloseOnExec = 1; + if (fcntl_nid_postfix(fd, SetDescriptorFlags, CloseOnExec) == -1) return nullptr; int error = 0; const char* path = GetDirectoryDescriptorPath_nid_no_patch(fd, &error); if (path == nullptr) { *__error_nid_postfix() = error == EBADF ? 9 : 20; return nullptr; } - return OpenDescriptorDirectory_nid_no_patch(path, fd, close_nid_postfix); + return OpenDescriptorDirectory_nid_no_patch(path, fd, close_nid_postfix, GetDirectoryDescriptorPosition_nid_no_patch(fd)); } diff --git a/core/libs/prx/libc/include/GuestDirectory.hpp b/core/libs/prx/libc/include/GuestDirectory.hpp index 092289d03a..3f83ab1a18 100644 --- a/core/libs/prx/libc/include/GuestDirectory.hpp +++ b/core/libs/prx/libc/include/GuestDirectory.hpp @@ -14,4 +14,4 @@ struct GuestDirectoryEntry { static_assert(offsetof(GuestDirectoryEntry, name) == 8); static_assert(sizeof(GuestDirectoryEntry) == 264); -extern "C" void* APS5_VABI OpenDescriptorDirectory_nid_no_patch(const char* hostPath, int descriptor, int (APS5_VABI* closeDescriptor)(int)); +extern "C" void* APS5_VABI OpenDescriptorDirectory_nid_no_patch(const char* hostPath, int descriptor, int (APS5_VABI* closeDescriptor)(int), std::uint64_t position); diff --git a/core/libs/prx/libc/src/Directory.cpp b/core/libs/prx/libc/src/Directory.cpp index f86c502cfe..6746b399f9 100644 --- a/core/libs/prx/libc/src/Directory.cpp +++ b/core/libs/prx/libc/src/Directory.cpp @@ -3,6 +3,7 @@ #include "prx/libc/include/FilesystemError.hpp" #include "prx/libc/include/AppArchive.hpp" #include +#include #include #include #include @@ -178,12 +179,26 @@ void APS5_VABI seekdir_nid_postfix(void* handle, std::int64_t location) { SeekNative(directory.native, static_cast(location)); } -void* APS5_VABI OpenDescriptorDirectory_nid_no_patch(const char* hostPath, int descriptor, int (APS5_VABI* closeDescriptor)(int)) { +void* APS5_VABI OpenDescriptorDirectory_nid_no_patch(const char* hostPath, int descriptor, int (APS5_VABI* closeDescriptor)(int), std::uint64_t position) { try { auto directory = std::make_unique(); directory->path = std::filesystem::path(reinterpret_cast(hostPath)); +#ifdef _WIN32 directory->native = OpenNative(directory->path); if (!directory->native) { errno = DirectoryError(errno); return nullptr; } + std::filesystem::path component; + std::string name; + std::uint64_t inode = 0; + for (std::uint64_t skipped = 0; skipped < position; ++skipped) { + if (!ReadNative(directory->native, component, name, inode)) break; + } +#else + (void)position; + const int duplicate = ::dup(descriptor); + if (duplicate < 0) { errno = DirectoryError(errno); return nullptr; } + directory->native = ::fdopendir(duplicate); + if (!directory->native) { const int error = errno; ::close(duplicate); errno = DirectoryError(error); return nullptr; } +#endif directory->descriptor = descriptor; directory->closeDescriptor = closeDescriptor; return directory.release(); diff --git a/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp b/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp index f6ab25f51a..159adb1852 100644 --- a/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp +++ b/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp @@ -6,6 +6,7 @@ #include extern "C" const char* GetDirectoryDescriptorPath_nid_no_patch(int fd, int* error); +extern "C" std::uint64_t GetDirectoryDescriptorPosition_nid_no_patch(int fd); extern "C" int OpenInDirectory_nid_no_patch(int directory, const char* path, int flags, std::uint16_t mode); namespace File { diff --git a/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp b/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp index ae7e0f4117..cca49c2431 100644 --- a/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp +++ b/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp @@ -227,3 +227,13 @@ extern "C" const char* GetDirectoryDescriptorPath_nid_no_patch(int fd, int* erro *error = 0; return path.c_str(); } + +extern "C" std::uint64_t GetDirectoryDescriptorPosition_nid_no_patch(int fd) { +#ifdef _WIN32 + const auto position = File::SeekDirectoryDescriptor(fd, 0, 1); + return position && *position > 0 ? static_cast(*position) : 0; +#else + (void)fd; + return 0; +#endif +} diff --git a/core/libs/tests/GuestFdopendir.cpp b/core/libs/tests/GuestFdopendir.cpp index 4e6cfffa82..551ad063f0 100644 --- a/core/libs/tests/GuestFdopendir.cpp +++ b/core/libs/tests/GuestFdopendir.cpp @@ -2,6 +2,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libkernel/File/include/File.hpp" #include +#include #include #include #include @@ -14,6 +15,8 @@ GuestDirectoryEntry* APS5_VABI readdir_nid_postfix(void*); int APS5_VABI closedir_nid_postfix(void*); int APS5_VABI open_nid_postfix(const char*, int, int); int APS5_VABI close_nid_postfix(int); +int APS5_VABI fcntl_nid_postfix(int, int, ...); +std::int64_t APS5_VABI lseek_nid_postfix(int, std::int64_t, int); int* APS5_VABI __error_nid_postfix(); } static void Require(bool value) { if (!value) std::abort(); } @@ -25,7 +28,9 @@ int main() { { std::ofstream file(root / "sample.txt"); file << "test"; } const int fd = open_nid_postfix(root.string().c_str(), 0x20000, 0); Require(fd >= 0); + Require(fcntl_nid_postfix(fd, 1) == 0); void* directory = fdopendir_nid_postfix(fd); + Require(fcntl_nid_postfix(fd, 1) == 1); Require(directory != nullptr); std::map entries; while (auto* entry = readdir_nid_postfix(directory)) { @@ -41,8 +46,20 @@ int main() { const int file = open_nid_postfix((root / "sample.txt").string().c_str(), 0, 0); Require(file >= 0); Require(fdopendir_nid_postfix(file) == nullptr && *__error_nid_postfix() == 20); + Require(fcntl_nid_postfix(file, 1) == 1); Require(DescriptorIsOpen_nid_no_patch(file)); Require(close_nid_postfix(file) == 0); +#ifdef _WIN32 + const int advanced = open_nid_postfix(root.string().c_str(), 0x20000, 0); + Require(advanced >= 0); + Require(lseek_nid_postfix(advanced, 2, 0) == 2); + void* rest = fdopendir_nid_postfix(advanced); + Require(rest != nullptr); + std::map remaining; + while (auto* entry = readdir_nid_postfix(rest)) remaining[entry->name] = entry->type; + Require(remaining.size() == 2 && remaining.count("sample.txt") == 1 && remaining.count("subdirectory") == 1); + Require(closedir_nid_postfix(rest) == 0); +#endif std::filesystem::remove(root / "sample.txt"); std::filesystem::remove(root / "subdirectory"); std::filesystem::remove(root); diff --git a/docs/dev/debt/libSceLibcInternal.md b/docs/dev/debt/libSceLibcInternal.md index 7b5f325b65..ff8c271f1f 100644 --- a/docs/dev/debt/libSceLibcInternal.md +++ b/docs/dev/debt/libSceLibcInternal.md @@ -10,7 +10,7 @@ - [makecontext](../../../core/libs/prx/libSceLibcInternal/UserContext.cpp) (libSceLibcInternal) follows FreeBSD 11's `lib/libc/amd64/gen/makecontext.c`: at most six arguments, a stack of at least `MINSIGSTKSZ` (2048) bytes, and `exit(0)` when the function returns with no `uc_link`. The console's version is not verified - [makecontext](../../../core/libs/prx/libSceLibcInternal/UserContext.cpp) (libSceLibcInternal) on Windows pins its stack writable until a later `makecontext` stack overlaps it, since nothing tells the library when the guest frees the stack - [Thread exit destructors](../../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) run when a libkernel thread finishes; on the main or a host thread only `_sceLibcInternalThreadDtors` runs them, as `exit` does, else they are dropped -- [fdopendir](../../../core/libs/prx/libSceLibcInternal/Directory.cpp) (libSceLibcInternal) - the directory stream starts at the directory's first entry, while FreeBSD's [`fdopendir`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/lib/libc/gen/opendir.c#L67-L74) reads from the descriptor's current offset, and `FD_CLOEXEC` is not set on the descriptor. As in FreeBSD, a descriptor that is not open fails with `EBADF`, one that is not a directory with `ENOTDIR` (both leave it open), and `closedir` closes it +- [fdopendir](../../../core/libs/prx/libSceLibcInternal/Directory.cpp) (libSceLibcInternal) follows FreeBSD 11's [`fdopendir`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/lib/libc/gen/opendir.c#L67-L74): it sets `FD_CLOEXEC` on the descriptor first, and the stream starts at the descriptor's current position instead of the first entry. On Windows the stream skips the entries the descriptor's cursor (`lseek` or `getdirentries`) has passed, which assumes that the descriptor and the stream list a directory in the same host order; on Linux the stream is opened on a duplicate of the descriptor, which shares its position. The Linux path is not run by the tests here. As in FreeBSD, a descriptor that is not open fails with `EBADF`, one that is not a directory with `ENOTDIR` (both leave it open, and `FD_CLOEXEC` stays set on the latter), and `closedir` closes it - [fstatvfs](../../../core/libs/prx/libSceLibcInternal/FilesystemStatistics.cpp) (libSceLibcInternal) converts `_fstatfs` to the 88-byte `struct statvfs` as FreeBSD 9.2's `sfs2svfs` does (`f_bsize` from the transfer size, `f_frsize` from the block size, `f_favail` equal to `f_ffree`, `f_fsid` 0, `ST_RDONLY` and `ST_NOSUID` only). Differences: `f_namemax` is the `statfs` value, not `fpathconf(_PC_NAME_MAX)`; negative `f_bavail` and `f_ffree` wrap to unsigned without `EOVERFLOW`, as the same-width copy in FreeBSD does; inode counts are 0 on Windows, inherited from `_fstatfs`; a null output returns `EFAULT` after the descriptor is validated, where FreeBSD would fault. Layout and conversion are unverified against the PS5 library (PS5MC imports `fstatvfs` but was not run) - [posix_spawn and posix_spawnp](../../../core/libs/prx/libSceLibcInternal/Spawn.cpp) (libSceLibcInternal) create no process. FreeBSD 11.4 returns the error the child's exec leaves ([`_posix_spawn_thr`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/posix_spawn.c#L227-L231), [`do_posix_spawn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/posix_spawn.c#L295-L303)), and `posix_spawnp` searches `PATH`, or `_PATH_DEFPATH` when it is unset, ending with `ENOENT` when no directory holds the program ([`_execvpe`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/exec.c#L272-L281), [`execvPe`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/exec.c#L257-L260)). So when the program is not in the guest file system both return `ENOENT` and leave the process ID unwritten; when it is there they throw. PPSA99305 (Dusklight) reaches this when SDL looks for `zenity` to show a file dialog. The attributes and file actions are not applied, so one that would fail in the child is not reported, and whether the console's application sandbox lets the child be created at all (FreeBSD returns that error first) is unverified. The attribute and file action calls keep FreeBSD 11.4's objects. Null or uninitialized objects, which FreeBSD dereferences, throw. - [getopt, getopt_long](../../../core/libs/prx/libSceLibcInternal/Getopt.cpp) (libSceLibcInternal) follow FreeBSD 11 [getopt.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdlib/getopt.c) and [getopt_long.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdlib/getopt_long.c) with `GNU_COMPATIBLE`: `getopt` does not permute, `getopt_long` permutes unless `POSIXLY_CORRECT` is set in the guest environment or the option string starts with `+`, and the exported `opterr`, `optind`, `optopt`, `optreset` and `optarg` start at 1, 1, 0, 0 and null. Warnings go to the guest's current `stderr` prefixed with `__progname`, where FreeBSD's `warnx` keeps the `stderr` of its first call. A null `getopt` option string, a null argument vector or argument, a negative `optind` and an `optind` moved inside a pending permutation throw where FreeBSD faults. `getopt_long_only` is not exported. That the console's `libSceLibcInternal` uses this FreeBSD code is unverified From 40cd706127c4e3a8251c91555daf86746792b287 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 20 Oct 2026 01:26:54 +0000 Subject: [PATCH 208/269] fix(libc): enforce mspace trace table capacity --- core/libs/prx/libc/src/HeapDiagnostics.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libc/src/HeapDiagnostics.cpp b/core/libs/prx/libc/src/HeapDiagnostics.cpp index 7c6dd60831..04324b8d39 100644 --- a/core/libs/prx/libc/src/HeapDiagnostics.cpp +++ b/core/libs/prx/libc/src/HeapDiagnostics.cpp @@ -9,7 +9,11 @@ namespace { std::uint64_t mspaceAtomicIdMask = 0; -std::array mstateTable{}; +constexpr std::size_t MspaceTableCapacity = 65; +std::array mstateTable{}; + +static_assert(MspaceTableCapacity == 65); +static_assert(mstateTable.size() == MspaceTableCapacity); static_assert(sizeof(LibcHeapInfo) == 32); static_assert(offsetof(LibcHeapInfo, mspace_atomic_id_mask) == 16); From a11756851eededc9acbb9caaee9eb1b8d8a1458b Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Tue, 20 Oct 2026 03:29:01 +0200 Subject: [PATCH 209/269] fix(libkernel): fail flock on an invalid descriptor with EBADF and on a pipe with EOPNOTSUPP flock on a descriptor that is not open threw a runtime_error from the failed Windows lock call, ending the title. FreeBSD 11.4 sys_flock fails it with EBADF (fget) and a descriptor that is not a vnode, such as a pipe, with EOPNOTSUPP. The guest flock now checks the descriptor first, on Windows through the OS handle and elsewhere through fstat. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libkernel/File/src/Stdio.cpp | 9 +++++++++ core/libs/tests/GuestFilesystem.cpp | 7 +++++++ 2 files changed, 16 insertions(+) diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index dee7b33df1..e7183cd586 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -765,6 +765,15 @@ int APS5_VABI dup2_nid_postfix(int from, int to) { int APS5_VABI flock_nid_postfix(int d, int operation) { if (GuestSockets::IsOpen(d)) return PosixFailure(GUEST_EOPNOTSUPP); File::RefuseArchivedFile(d, "flock"); +#ifdef _WIN32 + const auto handle = reinterpret_cast(WithoutParameterHandler([d] { return ::_get_osfhandle(d); })); + if (handle == INVALID_HANDLE_VALUE) return PosixFailure(GUEST_EBADF); + if (::GetFileType(handle) == FILE_TYPE_PIPE) return PosixFailure(GUEST_EOPNOTSUPP); +#else + struct stat status; + if (::fstat(d, &status) != 0) return PosixFailure(GUEST_EBADF); + if (S_ISFIFO(status.st_mode)) return PosixFailure(GUEST_EOPNOTSUPP); +#endif const int type = operation & 8 ? 8 : operation & 2 ? 2 : operation & 1 ? 1 : 0; if (type == 0) return PosixFailure(GUEST_EBADF); if (NativeFlock(d, type | (operation & 4)) != 0) { diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index 1921031174..1aa610d2cb 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -972,6 +972,13 @@ int main() { Require(flock_nid_postfix(other, 2 | 4) == 0); Require(flock_nid_postfix(other, 8) == 0); Require(flock_nid_postfix(locked, 2 | 4) == 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}; + Require(pipe_nid_postfix(flockPipe) == 0); + Require(flock_nid_postfix(flockPipe[0], 2 | 4) == -1 && *__error_nid_postfix() == 45); + Require(flock_nid_postfix(flockPipe[1], 8) == -1 && *__error_nid_postfix() == 45); + Require(close_nid_postfix(flockPipe[0]) == 0 && close_nid_postfix(flockPipe[1]) == 0); Require(close_nid_postfix(locked) == 0); const int recycled = open_nid_postfix(presentName.c_str(), 0, 0); Require(recycled >= 0 && flock_nid_postfix(recycled, 8) == 0); From da6b8561c3b62ff3cbc6aeb9dfc2ddb47e9a3a5d Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Tue, 20 Oct 2026 03:32:02 +0200 Subject: [PATCH 210/269] fix(libkernel): fail fsync and fdatasync on a pipe with EINVAL On Windows fsync and fdatasync of a pipe descriptor answered EBADF for the read end and 0 for the write end. FreeBSD 11.4 kern_fsync reaches the descriptor through getvnode, which fails a descriptor that is not a vnode with EINVAL. NativeSync now refuses a pipe handle with EINVAL before it commits. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libkernel/File/src/Stdio.cpp | 2 ++ core/libs/tests/GuestFilesystem.cpp | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index dee7b33df1..a50842556a 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -186,6 +186,8 @@ static int NativeSync(int descriptor) { handle = CreateFileW(directory->c_str(), GENERIC_WRITE, share, nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); } else { const int previous = errno; + const auto pipe = reinterpret_cast(WithoutParameterHandler([descriptor] { return ::_get_osfhandle(descriptor); })); + if (pipe != INVALID_HANDLE_VALUE && GetFileType(pipe) == FILE_TYPE_PIPE) { errno = EINVAL; return -1; } if (::_commit(descriptor) == 0) return 0; const auto original = reinterpret_cast(::_get_osfhandle(descriptor)); if (original == INVALID_HANDLE_VALUE || GetFileType(original) != FILE_TYPE_DISK) { errno = EBADF; return -1; } diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index 1921031174..cfed34b224 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -780,6 +780,14 @@ int main() { Require(sceKernelFdatasync(socket) == static_cast(0x80020016u)); Require(fsync_nid_postfix(socket) == -1 && *__error_nid_postfix() == 22); Require(sceKernelFsync(socket) == static_cast(0x80020016u)); + int syncPipe[2] = {-1, -1}; + Require(pipe_nid_postfix(syncPipe) == 0); + for (const int end : syncPipe) { + Require(fsync_nid_postfix(end) == -1 && *__error_nid_postfix() == 22); + Require(fdatasync_nid_postfix(end) == -1 && *__error_nid_postfix() == 22); + Require(sceKernelFsync(end) == static_cast(0x80020016u) && sceKernelFdatasync(end) == static_cast(0x80020016u)); + } + Require(close_nid_postfix(syncPipe[0]) == 0 && close_nid_postfix(syncPipe[1]) == 0); Require(close_nid_postfix(socket) == 0); Require(fchmod_nid_postfix(socket, 0600) == -1 && *__error_nid_postfix() == 9); Require(futimes_nid_postfix(socket, nullptr) == -1 && *__error_nid_postfix() == 9); From 45940f27ccb6febfeba259a79266d165212a37f3 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Tue, 20 Oct 2026 03:43:59 +0200 Subject: [PATCH 211/269] fix(libkernel): honour MSG_DONTWAIT in the socket send and receive calls on Windows send, sendto, sendmsg, recv, recvfrom and recvmsg failed with EOPNOTSUPP for MSG_DONTWAIT on Windows, where FreeBSD 11.4 sosend_generic and soreceive_generic make the one call non-blocking and fail with EWOULDBLOCK when it would wait. Winsock has no per-call flag, so the call now asks WSAPoll with a zero timeout whether the socket is ready and fails with EAGAIN when it is not. The Windows branches of the socket tests that expected EOPNOTSUPP now run the same checks as the other hosts. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libkernel/Socket/src/Udp.cpp | 16 ++++++++++------ core/libs/tests/GuestSocketTimeouts.cpp | 2 -- core/libs/tests/GuestUdp.cpp | 7 ------- docs/dev/debt/libkernel.md | 2 +- 4 files changed, 11 insertions(+), 16 deletions(-) diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 718b58c187..0868fed530 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -69,13 +69,8 @@ int Fail(int error) { *__error_nid_postfix() = error; return -1; } constexpr int GuestPeek = 0x2; constexpr int GuestDontWait = 0x80; constexpr int GuestNoSignal = 0x20000; -#ifdef _WIN32 -constexpr int GuestSendFlags = GuestNoSignal; -constexpr int GuestReceiveFlags = GuestPeek; -#else constexpr int GuestSendFlags = GuestNoSignal | GuestDontWait; constexpr int GuestReceiveFlags = GuestPeek | GuestDontWait; -#endif int NativeSendFlags(int flags) { #ifdef _WIN32 static_cast(flags); @@ -631,7 +626,16 @@ int SocketSendFlags(Socket& socket, int flags) { return NativeSendFlags(socket.options.noSignal ? flags | GuestNoSignal : flags); } int WaitForTransfer(Socket& socket, bool send, int flags) { - if ((flags & GuestDontWait) != 0) return 0; + if ((flags & GuestDontWait) != 0) { +#ifdef _WIN32 + NativePollDescriptor entry{}; + entry.fd = socket.value; + entry.events = static_cast(send ? POLLWRNORM : POLLRDNORM); + return WSAPoll(&entry, 1, 0) == 0 ? 35 : 0; +#else + return 0; +#endif + } std::int64_t timeout; { std::lock_guard lock(socket.modeMutex); diff --git a/core/libs/tests/GuestSocketTimeouts.cpp b/core/libs/tests/GuestSocketTimeouts.cpp index b475463949..2ff28a2173 100644 --- a/core/libs/tests/GuestSocketTimeouts.cpp +++ b/core/libs/tests/GuestSocketTimeouts.cpp @@ -119,13 +119,11 @@ int main() { Msghdr message{nullptr, 0, &part, 1, nullptr, 0, 0}; Require(Fails(recvmsg_nid_postfix(datagram, &message, 0), WouldBlock)); Require(Fails(read_nid_postfix(datagram, &byte, sizeof(byte)), WouldBlock)); -#ifndef _WIN32 Require(Set(datagram, ReceiveTimeout, 60, 0) == 0); Require(Fails(recv_nid_postfix(datagram, &byte, sizeof(byte), DontWait), WouldBlock)); Require(Fails(recvfrom_nid_postfix(datagram, &byte, sizeof(byte), DontWait, nullptr, nullptr), WouldBlock)); Require(Fails(recvmsg_nid_postfix(datagram, &message, DontWait), WouldBlock)); Require(Set(datagram, ReceiveTimeout, 0, 60000) == 0); -#endif Require(sendto_nid_postfix(datagram, "d", 1, 0, own.data(), own.size()) == 1); Require(recv_nid_postfix(datagram, &byte, sizeof(byte), 0) == 1 && byte == 'd'); diff --git a/core/libs/tests/GuestUdp.cpp b/core/libs/tests/GuestUdp.cpp index e9ffebd9a7..f9e50c0e20 100644 --- a/core/libs/tests/GuestUdp.cpp +++ b/core/libs/tests/GuestUdp.cpp @@ -110,12 +110,6 @@ static void CheckDontWait() { char buffer[16]{}; Iovec vector{buffer, sizeof(buffer)}; Msghdr message{nullptr, 0, &vector, 1, nullptr, 0, 0}; -#ifdef _WIN32 - Require(recv_nid_postfix(receiver, buffer, sizeof(buffer), 0x80) == -1 && *__error_nid_postfix() == 45); - Require(recvfrom_nid_postfix(receiver, buffer, sizeof(buffer), 0x80, nullptr, nullptr) == -1 && *__error_nid_postfix() == 45); - Require(recvmsg_nid_postfix(receiver, &message, 0x80) == -1 && *__error_nid_postfix() == 45); - Require(sendto_nid_postfix(sender, "x", 1, 0x80, address.data(), address.size()) == -1 && *__error_nid_postfix() == 45); -#else const auto start = std::chrono::steady_clock::now(); Require(recv_nid_postfix(receiver, buffer, sizeof(buffer), 0x80) == -1 && *__error_nid_postfix() == 35); Require(recvfrom_nid_postfix(receiver, buffer, sizeof(buffer), 0x80, nullptr, nullptr) == -1 && *__error_nid_postfix() == 35); @@ -134,7 +128,6 @@ static void CheckDontWait() { std::memset(buffer, 0, sizeof(buffer)); Require(recvmsg_nid_postfix(receiver, &message, 0) == sizeof(datagram) && std::strcmp(buffer, datagram) == 0); Require(recv_nid_postfix(receiver, buffer, sizeof(buffer), 0x80) == -1 && *__error_nid_postfix() == 35); -#endif Require(recv_nid_postfix(receiver, buffer, sizeof(buffer), 0x1) == -1 && *__error_nid_postfix() == 45); Require(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 e6bdd3c6fc..bcab569bbb 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -173,7 +173,7 @@ - [socketpair](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - the pair is the host's `AF_UNIX` pair on Linux and, on Windows, a loopback TCP connection for `SOCK_STREAM` or two connected UDP sockets for `SOCK_DGRAM`; `SOCK_SEQPACKET` throws on Windows. A datagram sent to a full receiver gets `ENOBUFS` on the console, waits or gets `EAGAIN` on Linux and is dropped on Windows, and on Windows a datagram `recv` after the peer closed reports `ECONNRESET` instead of the console's `ENOTCONN`, and a stream `send` after the peer closed reports `ECONNABORTED` instead of the console's `EPIPE`. `read` and `write` on a pair end go to the host like on every guest socket. `getpeername` on a pair end succeeds after the peer closed, where the console returns `ENOTCONN`, and `connect`, `bind` and `sendto` with an address on a pair end report an address-family error instead of the console's `EISCONN`. On Windows `poll` on a stream end reports `POLLHUP` without `POLLIN` after the peer's `shutdown(SHUT_WR)` or `close`, where the console reports `POLLIN`, with `POLLHUP` only after `close` - [UDP recv/recvfrom](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) and [sceNetRecv/sceNetRecvfrom](../../../core/libs/prx/libSceNet/Export.cpp) translate Windows WSAEMSGSIZE into the copied byte count for datagram sockets, including MSG_PEEK, and retain the sender address. This follows FreeBSD 11.4 recv(2) and Winsock's documented truncated-datagram behavior; console behavior and existing title impact are unverified. - [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 these calls still fail with `EOPNOTSUPP` for it +- [MSG_DONTWAIT](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) in `send`, `sendto`, `sendmsg`, `recv`, `recvfrom` and `recvmsg` makes that one call non-blocking through the host's `MSG_DONTWAIT`, as FreeBSD 11.4's `soreceive_generic` returns `EWOULDBLOCK` for it (`sys/kern/uipc_socket.c`). Winsock has no per-call non-blocking flag, so on Windows the call first asks `WSAPoll` with a zero timeout whether the socket is ready and fails with `EAGAIN` when it is not. The check and the transfer are two host calls: a second thread that takes the data in between can leave a `recv` blocked, and a `send` of more bytes than the socket buffer holds can still block until it has sent them, where FreeBSD's `sosend_generic` returns a partial count - [send, sendto](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - send and sendto without an address on an unconnected datagram socket map Windows `WSAENOTCONN` to FreeBSD's `EDESTADDRREQ` (39). - [read, write and vector I/O](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) on a guest socket use `recv` and `send` without flags; `sceKernelReadv` and `sceKernelWritev` gather or scatter through one socket operation, preserving stream short reads and datagram boundaries. The positioned `pread`, `pwrite`, `preadv` and `pwritev` forms, and their `sceKernel` forms, return `ESPIPE` for an open guest socket and `EBADF` for a closed one. `EPIPE` from a write raises the host's `SIGPIPE` on Linux unless the socket has `SO_NOSIGPIPE` set, and the guest cannot ignore it: `signal(SIGPIPE, SIG_IGN)` and `sigaction` record the disposition, as they do for every number from 1 to 128, but only the six signals with host equivalents (2, 4, 6, 8, 11 and 15) take effect on the host or are delivered, so the host `SIGPIPE` is not stopped; nothing is raised on Windows; a `write` longer than `INT_MAX` bytes is `EMSGSIZE` where the console sends it. AIO on a guest socket still goes to the host, which has no such descriptor - [Socket descriptor numbers](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel, libSceNet) - `socket`, `accept`, `socketpair`, `dup`, `sceNetSocket` and `sceNetAccept` take the lowest free number of the host descriptor table that files use, as FreeBSD 11.4 `fdalloc` takes the lowest free entry of the one process table (`kern_descrip.c`), so every number stays below `FD_SETSIZE` (1024) while fewer than 1024 descriptors are open. The number holds a zero-access `NUL` handle on Windows and a duplicate of the host socket on Linux, and the socket tables map it to the socket. The host table also holds the runtime's own files, so the numbers differ from the console's; the first number `socket` and `sceNetSocket` return on a console and its `getdtablesize` are not measured. Paths that bypass the socket tables see the placeholder: libc `fdopen` on a socket opens it, and libkernel `close` of a libSceNet socket releases the number but leaves the libSceNet socket open. Palworld (PPSA23126), The Mound (PPSA26223) and Dying Light 2 (PPSA02262) set socket bits in a 1024-bit `fd_set` From 92ea0b9ce3a54c5b32f90e544f86aa085abe2a5b Mon Sep 17 00:00:00 2001 From: JJenkx Date: Mon, 19 Oct 2026 21:45:35 -0400 Subject: [PATCH 212/269] test(relinker): wait for the exited diagnostic runner to release its fixtures windows_dependency_diagnostics rewrites the fixture images right after the diagnostic runner exits. The runner ends with an unhandled RaiseException of STATUS_DLL_NOT_FOUND, and Windows fault handling keeps the exited process's image files open for a short time after its handle is signaled: on Windows 11 22621 the Restart Manager names the exited runner as the holder and the file opens 37 to 153 ms later. The rewrite then fails with ERROR_SHARING_VIOLATION, and the final removal of the fixtures with access denied. Retry opening a fixture while it fails with a sharing violation and the removal while it fails with access denied, for at most 10 s each; every other failure still fails the test at once. Co-Authored-By: Claude Opus 5.5 --- .../WindowsDependencyDiagnosticsTests.cpp | 38 ++++++++++++++++--- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/core/relinker/elfpatcher/tests/WindowsDependencyDiagnosticsTests.cpp b/core/relinker/elfpatcher/tests/WindowsDependencyDiagnosticsTests.cpp index eb7a8d7e36..fa6c1d4085 100644 --- a/core/relinker/elfpatcher/tests/WindowsDependencyDiagnosticsTests.cpp +++ b/core/relinker/elfpatcher/tests/WindowsDependencyDiagnosticsTests.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -57,6 +58,33 @@ void checkRuntimeDependencies() { check({"libSceVideoOut.prx", "libSceVideoOut.prx"}, {"libSceVideoOut.prx"}); } +template +void openFixture(TStream& stream, const Fs::path& path, std::ios::openmode mode) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + for (;;) { + errno = 0; + _set_doserrno(0); + stream.clear(); + stream.open(path, mode); + if (stream.is_open() || _doserrno != ERROR_SHARING_VIOLATION || std::chrono::steady_clock::now() >= deadline) + return; + Sleep(10); + } +} + +void removeFixtures(const Fs::path& directory) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + for (;;) { + std::error_code error; + Fs::remove_all(directory, error); + if (!error) + return; + if (error != std::errc::permission_denied || std::chrono::steady_clock::now() >= deadline) + throw Fs::filesystem_error("cannot remove diagnostic fixtures", directory, error); + Sleep(10); + } +} + void writeFile(const Fs::path& path, const std::vector& bytes) { const auto fail = [&](const char* phase) { const auto error = errno; @@ -66,9 +94,8 @@ void writeFile(const Fs::path& path, const std::vector& bytes) { " native_error=" + std::to_string(nativeError) + " bytes=" + std::to_string(bytes.size())); }; - errno = 0; - _set_doserrno(0); - std::ofstream stream(path, std::ios::binary); + std::ofstream stream; + openFixture(stream, path, std::ios::binary); if (!stream.is_open()) fail("open"); if (!stream.write(reinterpret_cast(bytes.data()), static_cast(bytes.size()))) @@ -251,7 +278,8 @@ int main() { createImage(root, {{"KERNEL32.dll", "RelinkerDiagnosticsMissingExport"}}, {}); expectDiagnostic(runner, {"Symbol: RelinkerDiagnosticsMissingExport"}); { - std::fstream corrupt(root, std::ios::binary | std::ios::in | std::ios::out); + std::fstream corrupt; + openFixture(corrupt, root, std::ios::binary | std::ios::in | std::ios::out); const std::array invalidRva{static_cast(0xf0), static_cast(0xff), static_cast(0xff), static_cast(0xff)}; corrupt.seekp(LoadRva + 0x300); if (!corrupt.write(invalidRva.data(), invalidRva.size())) @@ -272,7 +300,7 @@ int main() { createImage(root, {{middle.filename().string(), "Middle"}}, {}); writeFile(middle, elf); expectDiagnostic(runner, {"dependency module is not a Windows PE image: " + middle.string(), "Importer: " + root.string(), "Chain: " + runner.string() + " -> " + root.string() + " -> " + middle.string() + "\n"}); - Fs::remove_all(directory); + removeFixtures(directory); std::cout << "Windows dependency machine-code tests passed\n"; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; From d672e8d54af64fd9c3544c11a10ad62a2fa33e3f Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Tue, 20 Oct 2026 03:48:18 +0200 Subject: [PATCH 213/269] fix(libkernel): fail listen on a connected socket with EINVAL listen on a connected stream socket failed with EISCONN, the Winsock error. FreeBSD 11.4 solisten_proto_check fails a socket that is connected, connecting or disconnecting with EINVAL. The guest listen now reports EINVAL for the host's EISCONN, and the connect-listening test checks it. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libkernel/Socket/src/Udp.cpp | 4 +++- core/libs/tests/GuestSocketConnectListening.cpp | 7 +++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 718b58c187..c6fe0c742f 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -1109,7 +1109,9 @@ int APS5_VABI listen_nid_postfix(int descriptor, int backlog) { const auto socket = Lookup(descriptor); if (!socket) return -1; if (socket->type != 1) return Fail(45); - return ::listen(socket->value, backlog) ? Fail(NativeError()) : 0; + if (::listen(socket->value, backlog) == 0) return 0; + const int error = NativeError(); + return Fail(error == 56 ? 22 : error); } int APS5_VABI accept_nid_postfix(int descriptor, void* address, std::uint32_t* length) { const auto listener = Lookup(descriptor); diff --git a/core/libs/tests/GuestSocketConnectListening.cpp b/core/libs/tests/GuestSocketConnectListening.cpp index 9bfffa3172..0f42c12bdb 100644 --- a/core/libs/tests/GuestSocketConnectListening.cpp +++ b/core/libs/tests/GuestSocketConnectListening.cpp @@ -20,6 +20,7 @@ namespace { constexpr int Inet = 2; constexpr int Stream = 1; constexpr int ErrorNotSupported = 45; +constexpr int ErrorInvalid = 22; void Require(bool condition, const char* what) { if (condition) return; @@ -57,6 +58,12 @@ int main() { Require(connect_nid_postfix(listener, address.data(), 1) == -1 && *__error_nid_postfix() == shortError, "connect on a listening socket with a 1-byte address must fail as on any socket, not with EOPNOTSUPP"); Require(nullError != ErrorNotSupported && shortError != ErrorNotSupported, "address errors must not be EOPNOTSUPP"); + const int connected = socket_nid_postfix(Inet, Stream, 0); + Require(connected >= 0 && connect_nid_postfix(connected, address.data(), address.size()) == 0, "connect to the listener failed"); + *__error_nid_postfix() = 0; + Require(listen_nid_postfix(connected, 1) == -1 && *__error_nid_postfix() == ErrorInvalid, + "listen on a connected socket must fail with EINVAL (22)"); + Require(close_nid_postfix(connected) == 0, "close failed"); Require(close_nid_postfix(plain) == 0, "close failed"); std::array wrongFamily{28, 28}; *__error_nid_postfix() = 0; From d6b5e3f6b87e627553901bd67efbf938e4a9543a Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Tue, 20 Oct 2026 03:59:03 +0200 Subject: [PATCH 214/269] fix(libc): fail fopen and freopen of a directory for writing with EISDIR on Windows fopen and freopen of a directory in a write mode failed with EACCES on Windows, the host's answer. FreeBSD 11.4 fopen opens the path with open(2), which fails a directory opened for writing with EISDIR (kern_openat, vn_open_cred). A failed write-mode open of a directory now reports EISDIR. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libc/src/FileAndHeap.cpp | 10 ++++++++-- core/libs/tests/GuestFilesystem.cpp | 3 +++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index 3ad3615c15..f81ef3fc5e 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -301,6 +301,12 @@ FileStream* APS5_VABI fdopen_nid_postfix(int descriptor, const char* mode) { } catch (const std::bad_alloc&) { errno = 12; return nullptr; } } +static int OpenFailure(const std::filesystem::path& path, bool writes, int hostError) { + const int error = GuestErrnoFromHost_nid_no_patch(hostError); + std::error_code status; + return writes && error == 13 && std::filesystem::is_directory(path, status) ? 21 : error; +} + 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. @@ -331,7 +337,7 @@ FileStream* APS5_VABI freopen_nid_postfix(const char* filename, const char* mode if (!path.empty() && parsed->writes) RecordWrittenPath_nid_no_patch(path); return stream; } - const int error = GuestErrnoFromHost_nid_no_patch(errno); + const int error = OpenFailure(path, parsed->writes, errno); if (stream->IsDynamic()) delete stream; errno = error; return nullptr; @@ -363,7 +369,7 @@ FileStream* APS5_VABI fopen_nid_postfix(const char* filename, const char* mode) if (const int error = ResolvePathError_nid_no_patch(filename, fpath)) { errno = error; return nullptr; } std::unique_ptr handle(OpenNative(fpath, parsed->native), std::fclose); if (!handle) { - errno = GuestErrnoFromHost_nid_no_patch(errno); + errno = OpenFailure(fpath, parsed->writes, errno); return nullptr; } // APS5_LOG_OUT("success: \"%s\"", abs_path.c_str()); diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index 1921031174..3454744bfe 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -543,6 +543,9 @@ int main() { std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); Require(mkdir_nid_postfix(root.string().c_str(), 0700) == 0); Require(mkdir_nid_postfix(root.string().c_str(), 0700) == -1 && *__error_nid_postfix() == 17); + for (const char* openMode : {"w", "wb", "r+", "a"}) { + Require(fopen_nid_postfix(root.string().c_str(), openMode) == nullptr && *__error_nid_postfix() == 21); + } 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"; From 086e98a42a3423ddd467e07b89b5ae6b742a4710 Mon Sep 17 00:00:00 2001 From: hugodearaujo Date: Mon, 19 Oct 2026 23:11:53 -0300 Subject: [PATCH 215/269] fix(agc): decode cube descriptors with slices past their last whole cube # Conflicts: # docs/dev/debt/libSceAgcDriver.md --- .../Graphics/src/GuestTextureResource.cpp | 4 +- .../libSceAgcDriver/Graphics/src/Texture.cpp | 8 +-- .../tests/GuestTextureResource.cpp | 11 +++- .../prx/libSceAgcDriver/tests/Recorder.cpp | 66 +++++++++++++++++++ docs/dev/debt/libSceAgcDriver.md | 2 +- 5 files changed, 81 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp index c07a32acc8..3c81c9c97e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp @@ -203,8 +203,8 @@ GuestTextureResource DecodeTextureResource(std::span words) Require(width == height, "guest cube texture descriptor is not square"); Require(baseArray <= depth, "guest cube texture descriptor has a base array past its last array slice"); if (baseArray == depth) depth += 5u; - if ((depth - baseArray + 1u) % 6u != 0) { - Require(false, "guest cube texture descriptor does not contain a multiple of 6 array slices" + + if (depth - baseArray + 1u < 6u) { + Require(false, "guest cube texture descriptor holds fewer than 6 array slices" + describe() + " (base array " + std::to_string(baseArray) + ")"); } break; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 1c4afad236..588b810ba5 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -525,7 +525,7 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex const auto viewLevelCount = std::min(descriptor.lastLevel, descriptor.mipCount - 1u) - descriptor.baseLevel + 1u; const auto viewLayerCount = geometry.imageLayers - descriptor.baseArray; if (descriptor.dimension == TextureDimension::kCube) { - Require(viewLayerCount % 6u == 0, "guest cube texture view does not contain a multiple of 6 array slices"); + Require(viewLayerCount >= 6u, "guest cube texture view holds fewer than 6 array slices"); } VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; @@ -632,7 +632,7 @@ Texture::Texture(const Context& context, const std::shared_ptr& const auto viewLevelCount = std::min(descriptor.lastLevel, descriptor.mipCount - 1u) - descriptor.baseLevel + 1u; const auto viewLayerCount = geometry.imageLayers - descriptor.baseArray; if (descriptor.dimension == TextureDimension::kCube) { - Require(viewLayerCount % 6u == 0, "guest cube texture view does not contain a multiple of 6 array slices"); + Require(viewLayerCount >= 6u, "guest cube texture view holds fewer than 6 array slices"); } VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; viewInfo.image = image; @@ -747,7 +747,7 @@ Texture::Texture(const Context& context, const std::shared_ptr& const auto viewLevelCount = std::min(descriptor.lastLevel, descriptor.mipCount - 1u) - descriptor.baseLevel + 1u; const auto viewLayerCount = geometry.imageLayers - descriptor.baseArray; if (descriptor.dimension == TextureDimension::kCube) { - Require(viewLayerCount % 6u == 0, "guest cube texture view does not contain a multiple of 6 array slices"); + Require(viewLayerCount >= 6u, "guest cube texture view holds fewer than 6 array slices"); } VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; viewInfo.image = image; @@ -1014,7 +1014,7 @@ Texture::Texture(const Context& context, const std::shared_ptr& const auto viewLevelCount = std::min(descriptor.lastLevel, descriptor.mipCount - 1u) - descriptor.baseLevel + 1u; const auto viewLayerCount = std::min(geometry.imageLayers, source->ImageLayers()) - descriptor.baseArray; if (descriptor.dimension == TextureDimension::kCube) { - Require(viewLayerCount % 6u == 0, "guest cube texture view does not contain a multiple of 6 array slices"); + Require(viewLayerCount >= 6u, "guest cube texture view holds fewer than 6 array slices"); } VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; viewInfo.image = source->Image(); diff --git a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp index 8410c5d9cd..db38d183f5 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp @@ -290,9 +290,14 @@ void RunGuestTextureResourceTests() { Fields cubeBadArray = cube; cubeBadArray.baseArray = 6; rejectFields(cubeBadArray, "base array past its last array slice"); - Fields cubeNotMultiple = cube; - cubeNotMultiple.depth = 4; - rejectFields(cubeNotMultiple, "multiple of 6"); + Fields cubeExtraSlice = cube; + cubeExtraSlice.depth = 6; + result = DecodeTextureResource(pack(cubeExtraSlice)); + Require(result.baseArray == 0 && result.depthOrLastArray == 6, "a cube descriptor with a slice past its last whole cube lost a slice"); + Require(DescribeSurface(result).imageLayers == 7, "a cube allocation with a slice past its last whole cube omitted it"); + Fields cubePartial = cube; + cubePartial.depth = 4; + rejectFields(cubePartial, "fewer than 6 array slices"); Fields multisampled = base; multisampled.typeRaw = 14; diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index f3b0ad345c..4fca55c01d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -8360,6 +8360,70 @@ void singleCubeTests(const Device& device, Recorder& recorder) { } } +void cubeExtraSliceTests(const Device& device, Recorder& recorder) { + auto context = device.GetContext(); + TextureDetiler detiler(context); + context.detiler = &detiler; + SampleProgram program(context, recorder, SAMPLE_ARRAY_SPV); + const VkComponentMapping identity{}; + std::array words{0x1000u, (56u << 20u) | (3u << 30u), 15u | (63u << 14u), 0xb0000facu, 6u, 0, 0, 0}; + auto resource = DecodeTextureResource(words); + const auto geometry = DescribeSurface(resource); + Require(geometry.imageLayers == 7u, "a cube with a slice past its last whole cube does not allocate it"); + std::vector memory(static_cast(geometry.guestBytes) + 256); + auto* surface = reinterpret_cast((reinterpret_cast(memory.data()) + 255) & ~std::uintptr_t{255}); + for (std::uint32_t layer = 0; layer < geometry.imageLayers; ++layer) { + std::memset(surface + geometry.GuestLayerOffset(layer), 16 * (layer + 1), static_cast(geometry.layerBytes)); + } + resource.baseAddress = reinterpret_cast(surface); + const std::span snapshot(surface, static_cast(geometry.guestBytes)); + Texture texture(context, detiler, resource, identity, snapshot); + auto storage = StorageTexture::Create(context, detiler, resource, 0); + recorder.Keep(storage); + Texture storageView(context, storage, resource, identity); + const auto wordCube = [&words](std::vector& backing, std::uint32_t format, const std::function& word) { + auto cubeWords = words; + cubeWords[1] = (format << 20u) | (3u << 30u); + auto described = DecodeTextureResource(cubeWords); + const auto layout = DescribeSurface(described); + backing.assign(static_cast(layout.guestBytes) + 256, std::byte{}); + auto* base = reinterpret_cast((reinterpret_cast(backing.data()) + 255) & ~std::uintptr_t{255}); + for (std::uint32_t layer = 0; layer < layout.imageLayers; ++layer) { + const auto value = word(layer); + for (std::uint64_t offset = 0; offset < layout.layerBytes; offset += sizeof(value)) std::memcpy(base + layout.GuestLayerOffset(layer) + offset, &value, sizeof(value)); + } + described.baseAddress = reinterpret_cast(base); + return described; + }; + std::vector depthMemory; + const auto depth = wordCube(depthMemory, 22u, [](std::uint32_t layer) { + const float value = static_cast(layer + 1) / 8.0f; + std::uint32_t bits = 0; + std::memcpy(&bits, &value, sizeof(bits)); + return bits; + }); + auto depthStorage = StorageTexture::Create(context, detiler, depth, 0); + recorder.Keep(depthStorage); + Require(Texture::CanDepthCopyFrom(*depthStorage, depth), "a cube with a slice past its last whole cube cannot take a depth copy"); + Texture depthCopy(context, depthStorage, depth, identity, Texture::DepthCopyTag{}); + depthCopy.RefreshDepthCopy(); + constexpr std::array unsignedFloats{960u, 1024u, 1056u, 1088u, 1104u, 1120u, 1136u}; + std::vector repackMemory; + const auto repacked = wordCube(repackMemory, 43u, [&unsignedFloats](std::uint32_t layer) { return unsignedFloats[layer] << 21u; }); + auto repackStorage = StorageTexture::Create(context, detiler, repacked, 0); + recorder.Keep(repackStorage); + Require(Texture::CanRepackCopyFrom(*repackStorage, repacked), "a cube with a slice past its last whole cube cannot take a repacked copy"); + Texture repackCopy(context, repackStorage, repacked, identity, Texture::RepackCopyTag{}); + repackCopy.RefreshDepthCopy(); + for (std::uint32_t layer = 0; layer < geometry.imageLayers; ++layer) { + const auto expected = 16.0f * (layer + 1) / 255.0f; + expectRed(program.Red(texture.View(), texture.Layout(), 0, static_cast(layer)), expected, "a cube snapshot with a slice past its last whole cube sampled the wrong slice"); + expectRed(program.Red(storageView.View(), storageView.Layout(), 0, static_cast(layer)), expected, "a cube storage view with a slice past its last whole cube sampled the wrong slice"); + expectRed(program.Red(depthCopy.View(), depthCopy.Layout(), 0, static_cast(layer)), static_cast(layer + 1) / 8.0f, "a cube depth copy with a slice past its last whole cube sampled the wrong slice"); + expectRed(program.Red(repackCopy.View(), repackCopy.Layout(), 0, static_cast(layer)), static_cast(layer + 1), "a cube repacked copy with a slice past its last whole cube sampled the wrong slice"); + } +} + struct SampledViewProbe { Context& context; PFN_vkGetDeviceProcAddr resolver; @@ -9705,6 +9769,7 @@ int main(int argc, char** argv) { } if (argc == 2 && std::string_view(argv[1]) == "--cube-only") { singleCubeTests(device, recorder); + cubeExtraSliceTests(device, recorder); std::cout << "Single cube snapshot and storage sampling tests passed\n"; return 0; } @@ -9980,6 +10045,7 @@ int main(int argc, char** argv) { firstLayerViewTests(device, recorder); sampledStorageBaseLevelUsageTests(device, recorder); singleCubeTests(device, recorder); + cubeExtraSliceTests(device, recorder); atomicViewTests(device, recorder); metadataPassTests(device, recorder); cmaskPassTests(device, recorder); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 259f6530b9..3e18ef4b7a 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -138,7 +138,7 @@ - [HTILE written by other kernels](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - when a depth target is bound and its HTILE was written since it was last read, the words over its whole pipe-aligned metadata (`HtileMetadataBytes`, the extent `HtileFillCovers` takes) are read, outside the surfaces mutex since the read waits for recorded work: when every one marks the depth (ZMask 0) or the stencil (SMem 0) cleared, the surface takes that fast clear and the words are stored back as expanded (depth only: ZMask 15 over the full Z range; with stencil: ZMask 15, Z range and SMem, SR1 and SR0 3), so only the title's next clear clears it again. PPSA29141 clears its depth every frame with a compute kernel the fill hooks do not recognize (`InvokeHTileClearComputeShader` in its `ClearMRT` pass); without this its depth kept every earlier frame and moving objects left black trails. PPSA25344 (Unreal Engine 5) reads its HTILE after the depth prepass in its Nanite depth export, which takes a tile with ZMask 0 as holding the clear value; without the expanded words the export put Nanite depth over the prepass and the Nanite material pass painted over the meshes in front. The word layouts are Mesa radv's for GFX10. Metadata with some tiles cleared is not modelled, and the compression state draws leave in HTILE is not tracked, so a title reading its HTILE after a draw sees the stored expanded words. A recognized HTILE fill or clear makes the next bind read the words again. A slice other than 0 of a depth array is read at its own HTILE (`DB_HTILE_DATA_BASE` plus the slice times `HtileMetadataBytes`) but the HTILE word layout of slices 1 and up is not modelled (PPSA05684's tile masks need X8 and Y8 to take Z0 and Z1 as well, slices 4 and up unmeasured), which neither the check that every word is cleared nor the store-back of each word in place needs, so such a slice takes the clear like slice 0. A depth-only HTILE whose tiles all have ZMask 0 but different Min/Max Z (per-tile clear values, PPSA05684's cascades) is taken as a whole-surface clear to `DB_DEPTH_CLEAR`, the value the DB's depth test reads for a cleared tile (`HTILE_Z_CLEAR_REG` in Mesa's `src/amd/common/ac_descriptors.h`: the clear word "only clears HiZ", "the DB register contain[s] the full clear values"); the HiZ test before it, which rejects against each tile's own Min/Max Z range (the HTILE definitions in the same file), is not modelled, so a draw that HiZ would reject over a tile cleared with Min/Max Z 0 is tested against `DB_DEPTH_CLEAR` instead. A multisampled depth target, and any target whose HTILE overlaps a resident native depth guard, skips this read and store-back and goes to the native multisample depth owner, which keeps its own refusal for a fast-clear transition. - [RADV fill-memory kernel](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp) (libSceAgcDriver) - the [fill HLE](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp) takes RADV's 16-byte `fill_memory` compute kernel (`MatchClampedGlobalFill`: 11 GFX10.3 words, 64x1x1 groups, six user SGPRs with the address in `user[2..3]`, the largest store offset in `user[4]` and the value in `user[5]`; each thread stores 16 bytes at `min(thread * 16, user[4])`) as a fill of `min(threads * 16, user[4] + 16)` bytes. RADV initializes and fast-clears DCC keys and other metadata with it, so the key and metadata tracking sees such a fill where it is in the command stream; run as a dispatch, its global stores name no range, and a key fill was only seen once it had landed, after later writes to the surface. Only a 16-byte aligned address and largest offset are taken. RADV's 4-byte variant (fills below 16 bytes), other Mesa versions' encodings and other compilers still run as dispatches. The kernel words are the ones vkQuake's PS5 RADV build (Mesa 25.2) submitted; they are not compared with the NIR builder's output for other targets. - [Host imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver) on Linux skip ranges that are not mapped read-write, which are copied instead. The NVIDIA driver (595.91.07) refuses `VK_EXT_external_memory_host` imports of read-only or executable mappings (and shared mappings of regular files) with `VK_ERROR_OUT_OF_DEVICE_MEMORY`, after which every `vkAllocateMemory` on the device fails the same way. Read-only ranges are never imported, even on drivers that accept them. Shared mappings of regular files are not checked for. On Windows, an alias of adjacent registered ranges is refused if any page is not a shared view; a range made entirely of shared views is aliased run by run, including across sections or nonconsecutive section offsets. Adjacent registered ranges are joined into one import: the largest aligned power-of-two block, from twice the import alignment up to the 8 MiB window, that holds the requested address and that they fill exactly, so a block only grows by doubling as its window fills. A block that would exceed the budget, would replace an import a build holds, or fails (a refused Windows alias, `vkAllocateMemory`) steps down to the next smaller block; a failed block is not tried again until one of its ranges changes. Ranges larger than the window are imported one at a time. A buffer bound across adjacent ranges that are each larger than the window, or bound exactly over adjacent ranges of any size (from the first one's start to the last one's end), joins them into one import when it is bound, and their own imports are retired; a buffer that crosses any other two imports is copied. A heap grown in pieces re-imports its joined run only when a buffer is bound exactly over all of its pieces, or when a sparse descriptor heap over it is bound: a heap a shader binds whole while only the pieces from its base are mapped is bound in place from the import of that run, its views ending at the import's end as the copy's do, once the previous bind of that address saw the same last mapped byte (a heap that grows between every two of its binds stays copied; one bound more often than it grows is joined again after each growth) and at most 64 times per joined run base; past that, with a message, it is not joined again: it is bound in place while an existing import covers its run and copied otherwise, until no registered range starts at that base or the device changes. Ranges larger than the window are taken to be registered whole and never grown in pieces: a title that grows a heap in pieces larger than 8 MiB re-imports the joined run each time a bound buffer crosses into a new piece. The 8 MiB window is a choice: unbounded runs were re-imported whole on every growth of a heap mapped in small pieces, and the NVIDIA driver (610.57.04, 615.78.08) then refused imports or lost the device. Not measured on RADV. -- [Cube texture descriptors](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) with equal `BASE_ARRAY` and `DEPTH` expose six faces starting at `BASE_ARRAY`. Measured with `image_load`, `image_sample` and `image_get_resinfo` on RX 6600 (gfx1032), using 64x64 R32_UINT faces and base slices 0, 5 and 6. Other ranges containing fewer than six faces or incomplete cube arrays still throw: hardware load bounds and filtered-sample addressing differ. Out-of-range cube-array sampling remains unimplemented. Samplers with `DISABLE_CUBE_WRAP` are created with `VK_SAMPLER_CREATE_NON_SEAMLESS_CUBE_MAP_BIT_EXT`, the flag RADV maps to that bit (Mesa `src/amd/vulkan/radv_sampler.c`, `src/amd/common/ac_descriptors.c`); without `VK_EXT_non_seamless_cube_map` a cube texture sampled through such a sampler throws. PS5 equivalence has not been measured on console hardware. +- [Cube texture descriptors](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) with equal `BASE_ARRAY` and `DEPTH` expose six faces starting at `BASE_ARRAY`. Measured with `image_load`, `image_sample` and `image_get_resinfo` on RX 6600 (gfx1032), using 64x64 R32_UINT faces and base slices 0, 5 and 6. Other ranges containing fewer than six faces still throw: hardware load bounds and filtered-sample addressing differ. A range of six faces or more that does not end on a whole cube keeps every slice up to `DEPTH`, the bound loads use on hardware; sampling the cube it cuts short is out-of-range cube-array sampling. Out-of-range cube-array sampling remains unimplemented. Samplers with `DISABLE_CUBE_WRAP` are created with `VK_SAMPLER_CREATE_NON_SEAMLESS_CUBE_MAP_BIT_EXT`, the flag RADV maps to that bit (Mesa `src/amd/vulkan/radv_sampler.c`, `src/amd/common/ac_descriptors.c`); without `VK_EXT_non_seamless_cube_map` a cube texture sampled through such a sampler throws. PS5 equivalence has not been measured on console hardware. - [Array layer views](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) (libSceAgcDriver) - `ArrayLayersServeView` lets a 2D texture at a 2D array image's base address, or a 2D array texture over its first layers, view that image (`Texture::CanCopyFrom`). Mesa-built homebrew (ps5-opengl) writes the view's last slice plus one to `CB_COLOR_ATTRIB3.MIP0_DEPTH`, so a slice-0 render target is a two-layer array image, and the title samples it as a plain 2D texture (SuperTuxKart PPSA69421 drew a black 3D scene and minimap without this). The per-layer layout of a mipmapped array is assumed equal to a standalone surface's; it is not measured on the console. A 2D texture at a later slice's address still gets an image of its own. - [Multisampled CMASK](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a multisampled color target's CMASK is decoded under CB_COLOR_INFO FAST_CLEAR (bit 13, as for a single-sample target) or COMPRESSION (bit 14). Which bit a multisampled target sets is inferred: its fast clears were seen on GTA V's multisampled targets through their CMASK fills, not in a register dump. A COMPRESSION-only target's zero CMASK is not read as cleared (inferred: zero means cleared only under FAST_CLEAR), so only a fill after its first use clears it. A slice of a multisampled array is its own image at its own address with its own CB_COLOR0_CMASK (inferred from the register layout). Its CMASK is main's whole-metablock size. - [Multisampled CMASK state](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - a multisampled target has no guest texels, so its CMASK state comes from fills rather than from a read at each draw as a single-sample target's does. A fill of the whole CMASK with 0 clears the host image at the target's next use, one with 0xffffffff marks it expanded, and any other pattern, or a fill that covers only part of the CMASK, throws at the next use (inferred: main's rule that a mixed CMASK is unmodeled, applied to the one host image). A FAST_CLEAR target starts from its CMASK keys at its first use, and a COMPRESSION-only target starts expanded. Copies by DMA into the CMASK are not seen, and a later use with another CMASK address throws. The clear does not rewrite the CMASK bytes as expanded, so a new target over the same CMASK (a different extent, so a different image) seeds from them again and clears once more. From a3ac9fd9e470929d3aae54c8427c44c53fb87f31 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Mon, 19 Oct 2026 22:14:40 -0400 Subject: [PATCH 216/269] test(relinker): retry removing fixtures an exited Windows program still holds windows_named_imports, windows_trace_imports, windows_guest_weak_imports and guest_module_identity run converted programs that stop on a missing import, then remove their temporary directory. Windows keeps the modules of such an exited process open for a short time after the process handle is signaled, so under parallel ctest the removal fails with PermissionError on a .prx that program loaded: with Defender real-time protection off, 3 or 4 of these tests failed in each of three runs of ctest --repeat until-fail:10 -j 16 over the 69 tests that run converted programs. Add fixture_directory to guest_import_fixtures, a temporary directory whose removal retries on PermissionError for at most 10 s, and use it in the four tests. Their checks are unchanged. Co-Authored-By: Claude Opus 5.5 --- .../relinker/tests/guest_import_fixtures.py | 21 +++++++++++++++++++ .../tests/test_guest_module_identity.py | 4 ++-- .../tests/test_windows_guest_weak_imports.py | 4 ++-- .../tests/test_windows_named_imports.py | 5 ++--- .../tests/test_windows_trace_imports.py | 4 ++-- 5 files changed, 29 insertions(+), 9 deletions(-) diff --git a/core/relinker/relinker/tests/guest_import_fixtures.py b/core/relinker/relinker/tests/guest_import_fixtures.py index f1b264bf12..45c69cbfb4 100644 --- a/core/relinker/relinker/tests/guest_import_fixtures.py +++ b/core/relinker/relinker/tests/guest_import_fixtures.py @@ -1,8 +1,29 @@ +import contextlib +import shutil import struct +import tempfile +import time from test_guest_intel_trampolines import main_fixture +@contextlib.contextmanager +def fixture_directory(prefix): + directory = tempfile.mkdtemp(prefix=prefix) + try: + yield directory + finally: + deadline = time.monotonic() + 10 + while True: + try: + shutil.rmtree(directory) + break + except PermissionError: + if time.monotonic() >= deadline: + raise + time.sleep(0.05) + + def dynamic(image, offset, phoff, tags): for index, tag in enumerate(tags + [(0, 0)]): struct.pack_into(' Date: Fri, 16 Oct 2026 08:51:38 -0500 Subject: [PATCH 217/269] feat(libSceUlt): export missing firmware functions from database --- core/libs/prx/libSceUlt/Export.cpp | 225 +++++++++++++++++++++++++++++ core/libs/tests/GuestUltThread.cpp | 25 ++++ docs/dev/debt/libSceUlt.md | 2 +- 3 files changed, 251 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceUlt/Export.cpp b/core/libs/prx/libSceUlt/Export.cpp index 506333c24e..898ae6a376 100644 --- a/core/libs/prx/libSceUlt/Export.cpp +++ b/core/libs/prx/libSceUlt/Export.cpp @@ -713,4 +713,229 @@ int APS5_VABI _sceUltUlthreadCreate(void* ulthread, const char* name, UltUlthrea return sceUltUlthreadCreate(ulthread, name, entry, arg, context, sizeContext, runtime, optParam, buildVersion); } +int APS5_VABI sceUltConditionVariableDestroy() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltConditionVariableSignal() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltConditionVariableSignalAll() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltConditionVariableWait() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltGetConditionVariableInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltGetMutexInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltGetQueueDataResourcePoolInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltGetQueueInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltGetReaderWriterLockInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltGetSemaphoreInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltGetUlthreadInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltGetUlthreadRuntimeInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltGetWaitingQueueResourcePoolInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltMutexTryLock() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltReaderWriterLockDestroy() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltReaderWriterLockLockRead() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltReaderWriterLockLockWrite() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltReaderWriterLockTryLockRead() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltReaderWriterLockTryLockWrite() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltReaderWriterLockUnlockRead() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltReaderWriterLockUnlockWrite() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltUlthreadExit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltUlthreadGetSelf() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltUlthreadYield() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltAbstractThreadFactoryCreateThread() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltAbstractThreadFactoryGetNumMaxThreads() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltAbstractThreadFactoryJoinThread() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltKernelThreadFactoryFinalize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltKernelThreadFactoryGetFactoryPointer() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltKernelThreadFactoryInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltModuleFinalize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltModuleInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltUlthreadFactoryFinalize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltUlthreadFactoryGetFactoryPointer() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltUlthreadFactoryGetWorkAreaSize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltUlthreadFactoryInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltConditionVariableCreate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltConditionVariableOptParamInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltQueueDataResourcePoolOptParamInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltQueueOptParamInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltReaderWriterLockCreate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltReaderWriterLockOptParamInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltSemaphoreOptParamInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltUlthreadOptParamInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltWaitingQueueResourcePoolOptParamInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/tests/GuestUltThread.cpp b/core/libs/tests/GuestUltThread.cpp index b076be3735..b4d9e523ba 100644 --- a/core/libs/tests/GuestUltThread.cpp +++ b/core/libs/tests/GuestUltThread.cpp @@ -6,6 +6,7 @@ #include #include #include +#include extern "C" { int APS5_VABI sceUltInitialize(); @@ -36,6 +37,9 @@ int APS5_VABI sceUltWaitingQueueResourcePoolDestroy(void*); int APS5_VABI sceUltSemaphoreDestroy(void*); int APS5_VABI sceUltQueueDestroy(void*); int APS5_VABI sceUltQueueDataResourcePoolDestroy(void*); +int APS5_VABI sceUltConditionVariableDestroy(); +int APS5_VABI sceUltReaderWriterLockDestroy(); +int APS5_VABI sceUltGetConditionVariableInfo(); } static constexpr int Ok = 0; @@ -106,6 +110,27 @@ static void Validation() { CHECK(sceUltUlthreadJoin(&thread, &status) == Ok && status == 0x27); CHECK(sceUltFinalize() == Ok); CHECK(sceUltUlthreadRuntimeGetWorkAreaSize(1, 1) == 256u + 16u * 1024u); + bool threwCv = false; + try { + sceUltConditionVariableDestroy(); + } catch (const std::runtime_error&) { + threwCv = true; + } + CHECK(threwCv); + bool threwRwl = false; + try { + sceUltReaderWriterLockDestroy(); + } catch (const std::runtime_error&) { + threwRwl = true; + } + CHECK(threwRwl); + bool threwInfo = false; + try { + sceUltGetConditionVariableInfo(); + } catch (const std::runtime_error&) { + threwInfo = true; + } + CHECK(threwInfo); } static void TryJoin() { diff --git a/docs/dev/debt/libSceUlt.md b/docs/dev/debt/libSceUlt.md index 95e0067125..4b049f53ba 100644 --- a/docs/dev/debt/libSceUlt.md +++ b/docs/dev/debt/libSceUlt.md @@ -6,7 +6,7 @@ - [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 - +- [sceUltConditionVariableDestroy, sceUltConditionVariableSignal, sceUltConditionVariableSignalAll, sceUltConditionVariableWait, sceUltGetConditionVariableInfo, sceUltGetMutexInfo, sceUltGetQueueDataResourcePoolInfo, sceUltGetQueueInfo, sceUltGetReaderWriterLockInfo, sceUltGetSemaphoreInfo, sceUltGetUlthreadInfo, sceUltGetUlthreadRuntimeInfo, sceUltGetWaitingQueueResourcePoolInfo, sceUltMutexTryLock, sceUltReaderWriterLockDestroy, sceUltReaderWriterLockLockRead, sceUltReaderWriterLockLockWrite, sceUltReaderWriterLockTryLockRead, sceUltReaderWriterLockTryLockWrite, sceUltReaderWriterLockUnlockRead, sceUltReaderWriterLockUnlockWrite, sceUltUlthreadExit, sceUltUlthreadGetSelf, sceUltUlthreadYield, sceUltAbstractThreadFactoryCreateThread, sceUltAbstractThreadFactoryGetNumMaxThreads, sceUltAbstractThreadFactoryJoinThread, sceUltKernelThreadFactoryFinalize, sceUltKernelThreadFactoryGetFactoryPointer, sceUltKernelThreadFactoryInitialize, sceUltModuleFinalize, sceUltModuleInitialize, sceUltUlthreadFactoryFinalize, sceUltUlthreadFactoryGetFactoryPointer, sceUltUlthreadFactoryGetWorkAreaSize, sceUltUlthreadFactoryInitialize, sceUltConditionVariableCreate, sceUltConditionVariableOptParamInitialize, sceUltQueueDataResourcePoolOptParamInitialize, sceUltQueueOptParamInitialize, sceUltReaderWriterLockCreate, sceUltReaderWriterLockOptParamInitialize, sceUltSemaphoreOptParamInitialize, sceUltUlthreadOptParamInitialize, sceUltWaitingQueueResourcePoolOptParamInitialize](../../../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 ### 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`. From e42631544100e9b24798ad0ce33b98324db8414e Mon Sep 17 00:00:00 2001 From: al3w0f205 Date: Mon, 19 Oct 2026 21:57:31 -0500 Subject: [PATCH 218/269] fix(libc): check access mode before the path --- core/libs/prx/libc/src/Filesystem.cpp | 2 +- core/libs/tests/GuestAccess.cpp | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libc/src/Filesystem.cpp b/core/libs/prx/libc/src/Filesystem.cpp index cff29e87ec..14546d3d46 100644 --- a/core/libs/prx/libc/src/Filesystem.cpp +++ b/core/libs/prx/libc/src/Filesystem.cpp @@ -12,8 +12,8 @@ #endif extern "C" int APS5_VABI access_nid_postfix(const char* path, int mode) { - if (!path) { errno = 14; return -1; } if (mode < 0 || (mode & ~7)) { errno = 22; return -1; } + if (!path) { errno = 14; return -1; } if (!*path) { errno = 2; return -1; } try { const auto resolved = ResolvePath_nid_no_patch(path); diff --git a/core/libs/tests/GuestAccess.cpp b/core/libs/tests/GuestAccess.cpp index 0cce786ad5..f4fa4fb8e8 100644 --- a/core/libs/tests/GuestAccess.cpp +++ b/core/libs/tests/GuestAccess.cpp @@ -38,6 +38,10 @@ int main() { Require(access_nid_postfix(nullptr, 0) == -1 && *__error_nid_postfix() == 14); Require(access_nid_postfix("sample.txt", 8) == -1 && *__error_nid_postfix() == 22); Require(access_nid_postfix("sample.txt", -1) == -1 && *__error_nid_postfix() == 22); + Require(access_nid_postfix(nullptr, 8) == -1 && *__error_nid_postfix() == 22); + Require(access_nid_postfix(nullptr, -1) == -1 && *__error_nid_postfix() == 22); + Require(access_nid_postfix("", 8) == -1 && *__error_nid_postfix() == 22); + Require(access_nid_postfix("", -1) == -1 && *__error_nid_postfix() == 22); #ifdef _WIN32 Require(SetFileAttributesW(file.c_str(), FILE_ATTRIBUTE_READONLY)); Require(access_nid_postfix("sample.txt", 4) == 0); From a8c5a3afc8ad043fa03a520f215e03f09e09829a Mon Sep 17 00:00:00 2001 From: al3w0f205 Date: Mon, 19 Oct 2026 22:14:06 -0500 Subject: [PATCH 219/269] fix(libSceLibcInternal): check null output before path in statvfs --- core/libs/prx/libSceLibcInternal/PathStatistics.cpp | 2 +- core/libs/tests/GuestStatvfs.cpp | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceLibcInternal/PathStatistics.cpp b/core/libs/prx/libSceLibcInternal/PathStatistics.cpp index 102ff774bb..c66f3dcc3b 100644 --- a/core/libs/prx/libSceLibcInternal/PathStatistics.cpp +++ b/core/libs/prx/libSceLibcInternal/PathStatistics.cpp @@ -140,7 +140,7 @@ int OpenMetadata(const std::filesystem::path& path, bool directory) { } extern "C" int APS5_VABI statvfs_nid_postfix(const char* path, LibcStatvfs* output) { - if (path == nullptr) return Fail(14); + if (path == nullptr || output == nullptr) return Fail(14); if (*path == '\0') return Fail(2); const int saved = errno; const auto last = std::string_view(path).back(); diff --git a/core/libs/tests/GuestStatvfs.cpp b/core/libs/tests/GuestStatvfs.cpp index ad1f8e5de0..2019801dc0 100644 --- a/core/libs/tests/GuestStatvfs.cpp +++ b/core/libs/tests/GuestStatvfs.cpp @@ -102,6 +102,8 @@ int main() { RemovePathAlias_nid_no_patch("/anyps5-statvfs-unavailable"); Require(statvfs_nid_postfix("/anyps5-statvfs-test/file.txt", nullptr) == -1); Require(*__error_nid_postfix() == 14); + Require(statvfs_nid_postfix("/anyps5-statvfs-test/missing", nullptr) == -1); + Require(*__error_nid_postfix() == 14); const auto unicodeFile = root / std::filesystem::path(u8"\u65e5.txt"); { From 20c425aabe6cb069ed40fb531b45c466993b2b0d Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Tue, 20 Oct 2026 16:14:09 +1300 Subject: [PATCH 220/269] ci(release): skip the precise-wait test in the Linux release run The hosted ubuntu-latest runners oversleep PreciseSleepUs(50), so precise_wait fails there and, with --stop-on-failure, every tag-push Release stops before it packages anything. The Build workflow still runs the test on every pull request; only the Linux step of release.yml excludes it. --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7fccd9c6a6..4815a27314 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -86,7 +86,7 @@ jobs: run: cmake --build build - name: Run tests (Linux) if: matrix.platform == 'linux' - run: ctest --test-dir build --output-on-failure --timeout 120 --stop-on-failure --no-tests=error + run: ctest --test-dir build --output-on-failure --timeout 120 --stop-on-failure --no-tests=error -E '^precise_wait$' env: ANYPS5_REQUIRE_VULKAN: 1 GALLIUM_OVERRIDE_CPU_CAPS: avx From f5b56825755504712582d473a2846c9d0e48455e Mon Sep 17 00:00:00 2001 From: jagrat Singh Date: Tue, 20 Oct 2026 08:45:30 +0530 Subject: [PATCH 221/269] feat(libSceSystemGesture): implement sceSystemGestureGetTouchEventByEventID --- core/libs/prx/libSceSystemGesture/Export.cpp | 11 +++++------ core/libs/tests/GuestSystemGesture.cpp | 4 ++++ docs/dev/debt/libSceSystemGesture.md | 2 +- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceSystemGesture/Export.cpp b/core/libs/prx/libSceSystemGesture/Export.cpp index 404c9d9d67..a73e142c78 100644 --- a/core/libs/prx/libSceSystemGesture/Export.cpp +++ b/core/libs/prx/libSceSystemGesture/Export.cpp @@ -200,12 +200,11 @@ int APS5_VABI sceSystemGestureGetPrimitiveTouchEventsCount(int32_t gesture_handl } int APS5_VABI sceSystemGestureGetTouchEventByEventID(int32_t gesture_handle, const SystemGestureTouchRecognizer* recognizer, uint32_t eventId, SystemGestureTouchEvent* event) { - (void)gesture_handle; - (void)recognizer; - (void)eventId; - (void)event; - NotImplemented_nid_no_patch(__func__); - return 0; + (void)eventId; + (void)event; + if (gesture_handle != GESTURE_HANDLE) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; + if (!recognizer) return SCE_SYSTEM_GESTURE_ERROR_INVALID_ARGUMENT; + return SCE_SYSTEM_GESTURE_ERROR_INDEX_OUT_OF_ARRAY; } int APS5_VABI sceSystemGestureGetTouchEventByIndex(int32_t gesture_handle, const SystemGestureTouchRecognizer* recognizer, uint32_t index, SystemGestureTouchEvent* event) { diff --git a/core/libs/tests/GuestSystemGesture.cpp b/core/libs/tests/GuestSystemGesture.cpp index a1bd7ec72c..01d5123939 100644 --- a/core/libs/tests/GuestSystemGesture.cpp +++ b/core/libs/tests/GuestSystemGesture.cpp @@ -16,6 +16,7 @@ int APS5_VABI sceSystemGestureGetPrimitiveTouchEventsCount(std::int32_t); int APS5_VABI sceSystemGestureGetTouchEvents(std::int32_t, const void*, void*, std::uint32_t, std::uint32_t*); int APS5_VABI sceSystemGestureGetPrimitiveTouchEventByIndex(std::int32_t, std::uint32_t, void*); int APS5_VABI sceSystemGestureGetTouchEventByIndex(std::int32_t, const void*, std::uint32_t, void*); +int APS5_VABI sceSystemGestureGetTouchEventByEventID(std::int32_t, const void*, std::uint32_t, void*); int APS5_VABI sceSystemGestureAppendTouchRecognizer(std::int32_t, void*); int APS5_VABI sceSystemGestureCreateTouchRecognizer(std::int32_t, void*, std::int32_t, const void*, const void*); int APS5_VABI sceSystemGestureGetTouchRecognizerInformation(std::int32_t, const void*, void*); @@ -240,6 +241,9 @@ int main() { Require(sceSystemGestureGetTouchEventByIndex(handle, &recognizer, 0, buffer.data()) == indexOutOfArray); Require(sceSystemGestureGetTouchEventByIndex(handle + 1, &recognizer, 0, buffer.data()) == invalidHandle); Require(sceSystemGestureGetTouchEventByIndex(handle, nullptr, 0, buffer.data()) == invalidArgument); + Require(sceSystemGestureGetTouchEventByEventID(handle, &recognizer, 0, buffer.data()) == indexOutOfArray); + Require(sceSystemGestureGetTouchEventByEventID(handle + 1, &recognizer, 0, buffer.data()) == invalidHandle); + Require(sceSystemGestureGetTouchEventByEventID(handle, nullptr, 0, buffer.data()) == invalidArgument); Require(buffer == original); Require(sceSystemGestureAppendTouchRecognizer(handle, &recognizer) == 0); diff --git a/docs/dev/debt/libSceSystemGesture.md b/docs/dev/debt/libSceSystemGesture.md index 75be9ed8c8..0bbcd0f2d4 100644 --- a/docs/dev/debt/libSceSystemGesture.md +++ b/docs/dev/debt/libSceSystemGesture.md @@ -2,7 +2,7 @@ ### Silent stubs -- [System gesture recognizers](../../../core/libs/prx/libSceSystemGesture/Export.cpp) (libSceSystemGesture) - `sceSystemGestureGetTouchEvents`, `GetTouchEventsCount`, `GetTouchEventByIndex` 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. 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 ### Unknown function info From bcbcf55b5b5fa073267c6189533d2d35debf3d77 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Tue, 20 Oct 2026 03:08:42 +0200 Subject: [PATCH 222/269] fix(libkernel): extend a file with ftruncate without writing zero bytes on Windows ftruncate, truncate and sceKernelFtruncate extended a file through _chsize_s, which writes the whole new length as zero bytes. FreeBSD extends the file without writing data (kern_ftruncate -> ffs_truncate) and fails a length the file system cannot hold with EFBIG. NativeFtruncate now sets the end of file with SetFileInformationByHandle and maps ERROR_DISK_FULL to ENOSPC and ERROR_FILE_TOO_LARGE/ERROR_INVALID_PARAMETER to EFBIG. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libkernel/CMakeLists.txt | 10 +++ core/libs/prx/libkernel/File/src/Stdio.cpp | 12 ++- core/libs/tests/GuestTruncateExtend.cpp | 90 ++++++++++++++++++++++ docs/dev/debt/libkernel.md | 1 + 4 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 core/libs/tests/GuestTruncateExtend.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index b242c7f8d1..fabbadfbda 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -1701,6 +1701,16 @@ if(BUILD_TESTING) set_tests_properties(guest_rename_dot PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_truncate_extend_tests ${LIBS_INCLUDE_DIR}/tests/GuestTruncateExtend.cpp) + target_include_directories(guest_truncate_extend_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_truncate_extend_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_truncate_extend_tests) + add_test(NAME guest_truncate_extend COMMAND guest_truncate_extend_tests) + set_tests_properties(guest_truncate_extend PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_truncate_extend PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_fstatfs_tests ${LIBS_INCLUDE_DIR}/tests/GuestFstatfs.cpp) target_include_directories(guest_fstatfs_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_fstatfs_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 6db90b54b0..08a7079950 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -137,7 +137,17 @@ static int NativeFtruncate(int descriptor, std::int64_t length) { if (handle == INVALID_HANDLE_VALUE) return EBADF; if (::GetFileType(handle) != FILE_TYPE_DISK) return EINVAL; if (WindowsHandleOnlyAppends_nid_no_patch(WindowsHandleAccess_nid_no_patch(handle, descriptor, "ftruncate"))) return TruncateThroughWriter(handle, length); - return static_cast(WithoutParameterHandler([descriptor, length] { return ::_chsize_s(descriptor, length); })); + FILE_END_OF_FILE_INFO end{}; + end.EndOfFile.QuadPart = length; + if (::SetFileInformationByHandle(handle, FileEndOfFileInfo, &end, sizeof(end))) return 0; + switch (::GetLastError()) { + case ERROR_ACCESS_DENIED: return EACCES; + case ERROR_DISK_FULL: + case ERROR_HANDLE_DISK_FULL: return ENOSPC; + case ERROR_FILE_TOO_LARGE: + case ERROR_INVALID_PARAMETER: return EFBIG; + default: return EINVAL; + } } static int NativeTruncate(const std::filesystem::path& path, std::int64_t length) { const int descriptor = ::_wopen(path.c_str(), _O_WRONLY | _O_BINARY); diff --git a/core/libs/tests/GuestTruncateExtend.cpp b/core/libs/tests/GuestTruncateExtend.cpp new file mode 100644 index 0000000000..8f04b83125 --- /dev/null +++ b/core/libs/tests/GuestTruncateExtend.cpp @@ -0,0 +1,90 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#endif + +extern "C" { +int APS5_VABI open_nid_postfix(const char*, int, int); +int APS5_VABI close_nid_postfix(int); +std::int64_t APS5_VABI write_nid_postfix(int, const void*, std::uint64_t); +std::int64_t APS5_VABI pread_nid_postfix(int, void*, std::uint64_t, std::int64_t); +std::int64_t APS5_VABI lseek_nid_postfix(int, std::int64_t, int); +int APS5_VABI ftruncate_nid_postfix(int, std::int64_t); +int APS5_VABI truncate_nid_postfix(const char*, std::int64_t); +int APS5_VABI unlink_nid_postfix(const char*); +int* APS5_VABI __error_nid_postfix(); +} + +namespace { + +constexpr int ReadOnly = 0; +constexpr int ReadWrite = 2; +constexpr int Create = 0x200; +constexpr std::int64_t Length = 64ll * 1024 * 1024; +constexpr char FileName[] = "anyps5_truncate_extend_file"; + +void Cleanup() { + unlink_nid_postfix(FileName); +} + +void Require(bool condition, const char* what) { + if (condition) return; + std::fprintf(stderr, "%s (errno %d)\n", what, *__error_nid_postfix()); + Cleanup(); + std::abort(); +} + +std::uint64_t BytesWritten() { +#ifdef _WIN32 + IO_COUNTERS counters{}; + return GetProcessIoCounters(GetCurrentProcess(), &counters) ? counters.WriteTransferCount : 0; +#else + return 0; +#endif +} + +} + +int main() { + Cleanup(); + const int descriptor = open_nid_postfix(FileName, Create | ReadWrite, 0644); + Require(descriptor >= 0, "open failed"); + Require(write_nid_postfix(descriptor, "head", 4) == 4, "write failed"); + + const std::uint64_t before = BytesWritten(); + Require(ftruncate_nid_postfix(descriptor, Length) == 0, "ftruncate to 64 MiB failed"); + const std::uint64_t written = BytesWritten() - before; + std::printf("extending by 64 MiB wrote %llu bytes\n", static_cast(written)); +#ifdef _WIN32 + Require(written < static_cast(Length) / 16, "extending a file must not write its new length as zero bytes: the process wrote about as many bytes as the new length"); +#endif + + Require(lseek_nid_postfix(descriptor, 0, 2) == Length, "the extended file has the wrong size"); + Require(lseek_nid_postfix(descriptor, 0, 1) == Length, "ftruncate must not move the file offset"); + char head[4] = {}; + char tail[8] = {1, 1, 1, 1, 1, 1, 1, 1}; + Require(pread_nid_postfix(descriptor, head, 4, 0) == 4 && std::memcmp(head, "head", 4) == 0, "extending must keep the existing bytes"); + Require(pread_nid_postfix(descriptor, tail, 8, Length - 8) == 8 && tail[0] == 0 && tail[7] == 0, "the extension must read as zero bytes"); + + Require(ftruncate_nid_postfix(descriptor, 2) == 0, "shrinking failed"); + Require(lseek_nid_postfix(descriptor, 0, 2) == 2, "the shrunk file has the wrong size"); + Require(ftruncate_nid_postfix(descriptor, -1) == -1 && *__error_nid_postfix() == 22, "a negative length must fail with EINVAL (22)"); + Require(close_nid_postfix(descriptor) == 0, "close failed"); + + const int readOnly = open_nid_postfix(FileName, ReadOnly, 0); + Require(readOnly >= 0, "reopen failed"); + Require(ftruncate_nid_postfix(readOnly, 8) == -1 && *__error_nid_postfix() == 22, "ftruncate on a read-only descriptor must fail with EINVAL (22)"); + Require(close_nid_postfix(readOnly) == 0, "close failed"); + + Require(truncate_nid_postfix(FileName, Length) == 0, "truncate to 64 MiB failed"); + const int check = open_nid_postfix(FileName, ReadOnly, 0); + Require(check >= 0 && lseek_nid_postfix(check, 0, 2) == Length, "truncate by path left the wrong size"); + Require(close_nid_postfix(check) == 0, "close failed"); + Cleanup(); + return 0; +} diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index caf7cea368..4066a982bf 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -126,6 +126,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 +- [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. - [/dev/random and /dev/urandom](../../../core/libs/prx/libkernel/File/src/RandomDevice.cpp) (libkernel) opened for reading return a descriptor backed by the host null device whose reads are filled with host OS entropy (`getentropy`, `rand_s` on Windows); both paths behave the same, never block, and positioned reads ignore the offset (FreeBSD `random(4)`). Only the exact paths are recognised, `fstat`, `lseek` and `mmap` see the null device, and opening them for writing throws - [pthread_attr_getinheritsched, pthread_attr_setinheritsched, pthread_attr_getscope, pthread_attr_setscope, pthread_attr_getstackaddr](../../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) follow FreeBSD 11 libthr ([thr_attr.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libthr/thread/thr_attr.c), [thr_create.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libthr/thread/thr_create.c)), not measured on the console: the scope defaults to `PTHREAD_SCOPE_SYSTEM` (2), only it and `PTHREAD_SCOPE_PROCESS` (0) are accepted and other values return `EINVAL`, and an inherit value other than `PTHREAD_INHERIT_SCHED` (4) or `PTHREAD_EXPLICIT_SCHED` (0) returns `ENOTSUP`. `pthread_attr_getstackaddr` returns the configured stack address pointer (null by default). A null output of `pthread_attr_getinheritsched` throws, since libthr writes through it unchecked. A thread keeps the scope it was created with, taken from the creating thread under `PTHREAD_INHERIT_SCHED`, and `pthread_attr_get_np` and `scePthreadAttrGet` report it, but every guest thread is a host thread at system scope, so process scope changes nothing - [Shared memory](../../../core/libs/prx/libkernel/File/src/SharedMemory.cpp) (libkernel) on Linux implements anonymous and named objects, bounded descriptor writes and seeks, and `shm_unlink` with effective read/write permission checks from FreeBSD 11. Named paths up to 1023 bytes use `/anyps5-shm-` plus SHA-1; collisions are not detected, and the namespace is shared across processes in the host mount namespace. Named creation applies the guest umask without changing the host mask. Linux procfs and `/dev/shm` are required. Operations use an advisory directory lock; external host operations bypass it, unrelated objects serialize, and descriptor operations require two spare host descriptors and compare device/inode with the initial descriptor identity on both the pinned-object and ordinary-file fallback paths. [Guest mappings](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) support shared and private mappings of these descriptors through the allocation tracker and pin a duplicate descriptor during validation and mapping; mapping requires one spare host descriptor; file-backed mappings use the write-watch scan fallback because aliases and descriptor writes bypass per-mapping tracking. On Windows, anonymous objects are delete-on-close temporary files and `mmap` maps them as [regular files](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp); named objects remain unsupported. Invalid non-null name pointers, console behaviour and title compatibility remain unverified. From cb6cd768ef70def83eec7ba1d8866e2ab05c1909 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Tue, 20 Oct 2026 04:11:00 +0200 Subject: [PATCH 223/269] fix(libkernel): fail open, stat, lstat, access and unlink through a regular file with ENOTDIR on Windows A path whose directory part is a regular file, such as file/child, failed with ENOENT in open, stat, lstat, access and unlink on Windows, where FreeBSD 11.4 lookup fails it with ENOTDIR (vfs_cache_lookup, sys/kern/vfs_cache.c). mkdir and rmdir already did. A not-found error of these calls now asks whether the nearest existing ancestor is not a directory and reports ENOTDIR then. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libc/include/FilesystemError.hpp | 14 ++++++++++++++ core/libs/prx/libc/src/Filesystem.cpp | 1 + core/libs/prx/libkernel/File/src/Open.cpp | 6 +++--- core/libs/prx/libkernel/File/src/Stdio.cpp | 2 +- core/libs/tests/GuestFilesystem.cpp | 11 +++++++++++ 5 files changed, 30 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libc/include/FilesystemError.hpp b/core/libs/prx/libc/include/FilesystemError.hpp index 37a4f7ea0e..bb50c8d70e 100644 --- a/core/libs/prx/libc/include/FilesystemError.hpp +++ b/core/libs/prx/libc/include/FilesystemError.hpp @@ -23,6 +23,20 @@ inline int FilesystemError(const std::error_code& error) { return 5; } +inline bool HasNonDirectoryPrefix_nid_no_patch(const std::filesystem::path& native) { + std::error_code error; + const auto trimmed = native.has_filename() ? native : native.parent_path(); + for (auto prefix = trimmed.parent_path(); prefix.has_relative_path(); prefix = prefix.parent_path()) { + const auto status = std::filesystem::status(prefix, error); + if (std::filesystem::exists(status)) return !std::filesystem::is_directory(status); + } + return false; +} + +inline int NotFoundError_nid_no_patch(const std::filesystem::path& native) { + return HasNonDirectoryPrefix_nid_no_patch(native) ? 20 : 2; +} + std::error_code RenameNative_nid_no_patch(const std::filesystem::path& source, const std::filesystem::path& destination, bool replacesExisting); inline int ResolvePathError_nid_no_patch(const char* path, std::filesystem::path& resolved, bool module = false, bool archived = false) { diff --git a/core/libs/prx/libc/src/Filesystem.cpp b/core/libs/prx/libc/src/Filesystem.cpp index cff29e87ec..c9a1370f47 100644 --- a/core/libs/prx/libc/src/Filesystem.cpp +++ b/core/libs/prx/libc/src/Filesystem.cpp @@ -21,6 +21,7 @@ extern "C" int APS5_VABI access_nid_postfix(const char* path, int mode) { const DWORD attributes = GetFileAttributesW(resolved.c_str()); if (attributes == INVALID_FILE_ATTRIBUTES) { errno = FilesystemError(std::error_code(static_cast(GetLastError()), std::system_category())); + if (errno == 2) errno = NotFoundError_nid_no_patch(resolved); return -1; } if (mode == 0) return 0; diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index b7c15b3403..ea387b611f 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -228,7 +228,7 @@ static int OpenNative(const std::filesystem::path& native, int flags, std::uint1 } #endif if (fd < 0) { - return SceErrorFromErrno(errno); + return SceErrorFromErrno(errno == ENOENT ? NotFoundError_nid_no_patch(native) : errno); } if ((flags & SCE_KERNEL_O_ACCMODE) != SCE_KERNEL_O_RDONLY || (flags & (SCE_KERNEL_O_CREAT | SCE_KERNEL_O_TRUNC))) RecordWrittenPath_nid_no_patch(native); @@ -370,7 +370,7 @@ int APS5_VABI sceKernelStat(const char* path, FileStat* sb) { if (error == std::errc::not_a_directory) return SceErrorFromErrno(GuestEnotdir); if (error == std::errc::filename_too_long || error == std::errc::too_many_symbolic_link_levels) return SceKernelError(FilesystemError(error)); if (!std::filesystem::exists(status)) { - return SceErrorFromErrno(2); + return SceErrorFromErrno(NotFoundError_nid_no_patch(native)); } const std::string_view guestPath(path); if (!guestPath.empty() && guestPath.back() == '/' && !std::filesystem::is_directory(status)) return SceErrorFromErrno(GuestEnotdir); @@ -388,7 +388,7 @@ int APS5_VABI sceKernelUnlink(const char* path) { std::error_code error; if (std::filesystem::is_directory(std::filesystem::symlink_status(native, error))) return SceErrorFromErrno(GuestEperm); if (NativeUnlink(native) != 0) { - return SceErrorFromErrno(errno); + return SceErrorFromErrno(errno == ENOENT ? NotFoundError_nid_no_patch(native) : errno); } RecordWrittenPath_nid_no_patch(native); return 0; diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 6db90b54b0..b040a94444 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -964,7 +964,7 @@ int APS5_VABI lstat_nid_postfix(const char* path, FileStat* sb) { if (const int error = PathError(path)) return PosixFailure(error); std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return PosixFailure(error); - if (!File::FillLinkStat(native, sb)) return PosixResult(SceErrorFromErrno(errno)); + if (!File::FillLinkStat(native, sb)) return PosixResult(SceErrorFromErrno(errno == ENOENT ? NotFoundError_nid_no_patch(native) : errno)); return 0; } diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index 1f72127476..d4412a2eb8 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -1145,6 +1145,17 @@ int main() { Require(rmdir_nid_postfix((missingName + "/").c_str()) == -1 && *__error_nid_postfix() == 2); Require(sceKernelRmdir((presentName + "/").c_str()) == static_cast(0x80020014u)); Require(sceKernelRmdir((missingName + "/").c_str()) == static_cast(0x80020002u)); + const auto missingChild = missingName + "/child"; + for (const auto* blocked : {&underFile, &deepUnderFile, &missingChild}) { + const int expected = blocked == &missingChild ? 2 : 20; + FileStat blockedStatus{}; + Require(open_nid_postfix(blocked->c_str(), 0, 0) == -1 && *__error_nid_postfix() == expected); + Require(open_nid_postfix(blocked->c_str(), 0x202, 0644) == -1 && *__error_nid_postfix() == expected); + Require(stat_nid_postfix(blocked->c_str(), &blockedStatus) == -1 && *__error_nid_postfix() == expected); + Require(lstat_nid_postfix(blocked->c_str(), &blockedStatus) == -1 && *__error_nid_postfix() == expected); + Require(access_nid_postfix(blocked->c_str(), 0) == -1 && *__error_nid_postfix() == expected); + Require(unlink_nid_postfix(blocked->c_str()) == -1 && *__error_nid_postfix() == expected); + } #ifndef _WIN32 const auto linkedDirectory = root / "linked-directory"; const auto directoryLink = root / "directory-link"; From 7f32f426277e54338593278a5e67ef1367ed8032 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Tue, 20 Oct 2026 04:18:08 +0200 Subject: [PATCH 224/269] fix(libkernel): fail chmod, truncate, utimes and rename through a regular file with ENOTDIR on Windows A path whose directory part is a regular file, such as file/child, failed with ENOENT in chmod, truncate, utimes and rename on Windows, where FreeBSD 11.4 fails it with ENOTDIR (vfs_cache_lookup, sys/kern/vfs_cache.c). The not-found error of these calls now asks whether the nearest existing ancestor is not a directory, as open, stat, lstat, access and unlink do. For rename the source ancestor is checked first and the destination ancestor only while the source exists. A test line that expected ENOENT for truncate of a path below a regular file now expects ENOTDIR. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libc/src/Rename.cpp | 6 +++++- core/libs/prx/libkernel/File/src/Stdio.cpp | 6 +++--- core/libs/tests/GuestFilesystem.cpp | 7 ++++++- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libc/src/Rename.cpp b/core/libs/prx/libc/src/Rename.cpp index f9dede035b..89ef0a55a2 100644 --- a/core/libs/prx/libc/src/Rename.cpp +++ b/core/libs/prx/libc/src/Rename.cpp @@ -102,7 +102,11 @@ extern "C" int RenameGuestPath_nid_no_patch(const char* from, const char* to) { try { const auto source = ResolvePath_nid_no_patch(from); const auto destination = ResolvePath_nid_no_patch(to); - const int error = RenamePath_nid_no_patch(source, destination); + int error = RenamePath_nid_no_patch(source, destination); + if (error == 2) { + std::error_code status; + if (HasNonDirectoryPrefix_nid_no_patch(source) || (std::filesystem::exists(source, status) && HasNonDirectoryPrefix_nid_no_patch(destination))) error = 20; + } if (error) { errno = error; return -1; } RecordWrittenPath_nid_no_patch(source); RecordWrittenPath_nid_no_patch(destination); diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index b040a94444..1fa2653433 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -614,7 +614,7 @@ int APS5_VABI chmod_nid_postfix(const char* path, int mode) { if (const int error = PathError(path)) return PosixFailure(error); std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return PosixFailure(error); - if (NativeChmod(native, mode & 07777) != 0) return PosixResult(SceErrorFromErrno(errno)); + if (NativeChmod(native, mode & 07777) != 0) return PosixResult(SceErrorFromErrno(errno == ENOENT ? NotFoundError_nid_no_patch(native) : errno)); return 0; } @@ -1409,7 +1409,7 @@ int APS5_VABI sceKernelTruncate_nid_postfix(const char* path, std::int64_t lengt if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); std::error_code error; const auto status = std::filesystem::status(native, error); - if (!std::filesystem::exists(status)) return SceErrorFromErrno(GUEST_ENOENT); + if (!std::filesystem::exists(status)) return SceErrorFromErrno(NotFoundError_nid_no_patch(native)); if (std::filesystem::is_directory(status)) return SceErrorFromErrno(GUEST_EISDIR); if (const int failure = NativeTruncate(native, length)) return SceErrorFromErrno(failure); RecordWrittenPath_nid_no_patch(native); @@ -1428,7 +1428,7 @@ int APS5_VABI sceKernelUtimes_nid_postfix(const char* path, const KernelTimeval* if (*path == '\0') return SceErrorFromErrno(GUEST_ENOENT); std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); - if (NativeUtimes(native, times) != 0) return SceErrorFromErrno(errno); + if (NativeUtimes(native, times) != 0) return SceErrorFromErrno(errno == ENOENT ? NotFoundError_nid_no_patch(native) : errno); return 0; } diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index d4412a2eb8..fd2bcbc681 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -736,7 +736,7 @@ int main() { Require(std::filesystem::file_size(sized) == 6); { std::ifstream stream(sized); std::string contents; std::getline(stream, contents); Require(contents == "012345"); } - Require(sceKernelTruncate_nid_postfix((sized / "missing").string().c_str(), 6) == static_cast(0x80020002u)); + Require(sceKernelTruncate_nid_postfix((sized / "missing").string().c_str(), 6) == static_cast(0x80020014u)); Require(sceKernelUtimes_nid_postfix(sized.string().c_str(), nullptr) == 0); std::FILE* native = std::fopen(sized.string().c_str(), "r+b"); Require(native != nullptr); @@ -1155,6 +1155,11 @@ int main() { Require(lstat_nid_postfix(blocked->c_str(), &blockedStatus) == -1 && *__error_nid_postfix() == expected); Require(access_nid_postfix(blocked->c_str(), 0) == -1 && *__error_nid_postfix() == expected); Require(unlink_nid_postfix(blocked->c_str()) == -1 && *__error_nid_postfix() == expected); + Require(chmod_nid_postfix(blocked->c_str(), 0644) == -1 && *__error_nid_postfix() == expected); + Require(sceKernelTruncate_nid_postfix(blocked->c_str(), 0) == static_cast(0x80020000u | static_cast(expected))); + Require(sceKernelUtimes_nid_postfix(blocked->c_str(), nullptr) == static_cast(0x80020000u | static_cast(expected))); + Require(rename_nid_postfix(blocked->c_str(), (root / "renamed-target").string().c_str()) == -1 && *__error_nid_postfix() == expected); + if (blocked != &missingChild) Require(rename_nid_postfix(presentName.c_str(), blocked->c_str()) == -1 && *__error_nid_postfix() == expected); } #ifndef _WIN32 const auto linkedDirectory = root / "linked-directory"; From a39effcef93d9ae8072fbb307d239a1c04c4155e Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Tue, 20 Oct 2026 03:53:38 +0200 Subject: [PATCH 225/269] fix(libSceNet): honour MSG_DONTWAIT in the send and receive calls on Windows sceNetSend, sceNetSendto, sceNetSendmsg, sceNetRecv, sceNetRecvfrom and sceNetRecvmsg failed with EOPNOTSUPP for MSG_DONTWAIT on Windows, where FreeBSD 11.4 sosend_generic and soreceive_generic make the one call non-blocking and fail with EWOULDBLOCK when it would wait. Winsock has no per-call flag, so the call now asks WSAPoll with a zero timeout whether the socket is ready and fails with EAGAIN when it is not. The Windows branch of the test that expected EOPNOTSUPP now runs the checks of the other hosts. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libSceNet/Export.cpp | 23 ++++++++++++++++++++--- core/libs/tests/GuestSceNet.cpp | 7 ------- docs/dev/debt/libSceNet.md | 2 +- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 8fdc52dffe..ab69f4e4e3 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -488,12 +488,24 @@ int receive_error(const Sock& socket) { } bool message_flags_supported(int flags, int accepted) { -#ifdef _WIN32 - accepted &= ~NET_MSG_DONTWAIT; -#endif return (flags & ~accepted) == 0; } +bool would_block(const Sock& socket, int flags, bool send) { +#ifdef _WIN32 + if ((flags & NET_MSG_DONTWAIT) == 0) return false; + WSAPOLLFD entry{}; + entry.fd = socket.native->value; + entry.events = send ? POLLWRNORM : POLLRDNORM; + return WSAPoll(&entry, 1, 0) == 0; +#else + static_cast(socket); + static_cast(flags); + static_cast(send); + return false; +#endif +} + int native_message_flags(int flags) { int native = (flags & NET_MSG_PEEK) ? MSG_PEEK : 0; #ifndef _WIN32 @@ -992,6 +1004,7 @@ int64_t APS5_VABI sceNetRecv(int s, void* buf, size_t len, int flags) { #endif const GuestArena::HostWrite destination(len == 0 ? nullptr : buf, len); if (!destination.Open()) return fail(NET_EFAULT); + if (would_block(socket, flags, false)) return fail(NET_EAGAIN); const int native_flags = native_message_flags(flags); const int result = ::recv(socket.native->value, static_cast(buf), static_cast(len), native_flags); #ifdef _WIN32 @@ -1029,6 +1042,7 @@ int64_t APS5_VABI sceNetRecvfrom(int s, void* buf, size_t len, int flags, void* if (!destination.Open()) return fail(NET_EFAULT); sockaddr_storage peer{}; NativeLength peer_length = sizeof(peer); + if (would_block(socket, flags, false)) return fail(NET_EAGAIN); const int native_flags = native_message_flags(flags); int result = ::recvfrom(socket.native->value, static_cast(buf), static_cast(len), native_flags, from ? reinterpret_cast(&peer) : nullptr, from ? &peer_length : nullptr); @@ -1051,6 +1065,7 @@ int64_t APS5_VABI sceNetSend(int s, const void* buf, size_t len, int flags) { if (it == g_socks.end()) return fail(NET_EBADF); socket = it->second; } + if (would_block(socket, flags, true)) return fail(NET_EAGAIN); const int result = ::send(socket.native->value, static_cast(buf), static_cast(len), NATIVE_SEND_FLAGS | native_message_flags(flags)); return result >= 0 ? result : fail(native_error()); } @@ -1066,6 +1081,7 @@ int64_t APS5_VABI sceNetSendto(int s, const void* buf, size_t len, int flags, co if (it == g_socks.end()) return fail(NET_EBADF); socket = it->second; } + if (would_block(socket, flags, true)) return fail(NET_EAGAIN); int result; if (!to) { if (tolen != 0) return fail(NET_EINVAL); @@ -1132,6 +1148,7 @@ int64_t APS5_VABI sceNetRecvmsg(int s, NetMsghdr* msg, int flags) { return 0; } #endif + if (would_block(socket, flags, false)) return fail(NET_EAGAIN); std::vector buffer; try { buffer.resize(static_cast(total) + (datagram ? 1u : 0u)); diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index e8ade712ba..08eb6daf7f 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -466,12 +466,6 @@ static void CheckDontWait() { char buffer[16]{}; NetIovec in[] = {{buffer, sizeof(buffer)}}; NetMsghdr receive{nullptr, 0, in, 1, nullptr, 0, 0}; -#ifdef _WIN32 - Require(Failed(sceNetRecv(receiver, buffer, sizeof(buffer), DontWait), 45)); - Require(Failed(sceNetRecvfrom(receiver, buffer, sizeof(buffer), DontWait, nullptr, nullptr), 45)); - Require(Failed(sceNetRecvmsg(receiver, &receive, DontWait), 45)); - Require(Failed(sceNetSendto(sender, "x", 1, DontWait, address.data(), address.size()), 45)); -#else const auto start = std::chrono::steady_clock::now(); Require(Failed(sceNetRecv(receiver, buffer, sizeof(buffer), DontWait), 35)); Require(Failed(sceNetRecvfrom(receiver, buffer, sizeof(buffer), DontWait, nullptr, nullptr), 35)); @@ -489,7 +483,6 @@ static void CheckDontWait() { Require(sceNetSend(sender, datagram, sizeof(datagram), DontWait) == sizeof(datagram)); Require(sceNetRecvmsg(receiver, &receive, 0) == sizeof(datagram) && std::strcmp(buffer, datagram) == 0); Require(Failed(sceNetRecv(receiver, buffer, sizeof(buffer), DontWait), 35)); -#endif Require(Failed(sceNetRecv(receiver, buffer, sizeof(buffer), 0x1), 45)); Require(Failed(sceNetSend(sender, "x", 1, Peek), 45)); Require(sceNetSocketClose(receiver) == 0 && sceNetSocketClose(sender) == 0); diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index 06832be45a..295e47760f 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -189,7 +189,7 @@ - [sceNetGetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) reads `SO_ERROR` from the host socket. Winsock does not clear a socket's pending error when it is read, so on Windows the library reports a pending error once and shows it again only after a new `sceNetConnect`; a second failure with the same code on the same socket without a `sceNetConnect` in between would be hidden. - [sceNetSetsockopt, sceNetGetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) set `TCP_NODELAY` (`IPPROTO_TCP`, 1) on the host socket of a stream socket and read it back as `TF_NODELAY` (4) or 0, as FreeBSD 11.4's [`tcp_default_ctloutput`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/netinet/tcp_usrreq.c#L1626-L1650) does. Every other `IPPROTO_TCP` option (`TCP_MAXSEG`, `TCP_NOPUSH` and `TCP_KEEPIDLE` among them, which FreeBSD implements) fails with `ENOPROTOOPT`, and the `IPPROTO_TCP` level on a socket that is not a stream socket fails with `EINVAL`, as [`ip_ctloutput`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/netinet/ip_output.c#L943-L944) answers it. A set on a connection that FreeBSD's [`tcp_ctloutput`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/netinet/tcp_usrreq.c#L1498-L1502) refuses with `ECONNRESET` (refused, reset or in `TIME_WAIT`) succeeds here because the host accepts it. A null value with a nonzero length fails with `EINVAL`, where FreeBSD's `kern_setsockopt` returns `EFAULT`, and a get into less than an `int` fails with `EINVAL`, where `sooptcopyout` truncates. None of this was measured on a console. - [sceNetSendmsg and sceNetRecvmsg](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) copy the scatter/gather list through one host buffer and accept only the flags `sceNetSend`/`sceNetRecv` accept. Sending control data throws; a receive never returns control data (`msg_controllen` 0), since no socket option that queues it is supported -- [MSG_DONTWAIT](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) in `sceNetSend`, `sceNetSendto`, `sceNetRecv`, `sceNetRecvfrom`, `sceNetRecvmsg` and `sceNetSendmsg` makes that one call non-blocking through the host's `MSG_DONTWAIT`, as FreeBSD 11.4's `soreceive_generic` returns `EWOULDBLOCK` for it (`sys/kern/uipc_socket.c`); the value `0x80` is FreeBSD's (`sys/sys/socket.h`) and [shadPS4's](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/net/net_types.h#L104). Winsock has no per-call non-blocking flag, so on Windows these calls still fail with `EOPNOTSUPP` for it +- [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 - [sceNetResolverStartNtoa6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) resolves a host name to its first IPv6 address through the host resolver (`getaddrinfo` with `AF_INET6`) and writes the 16-byte address; the `(rid, hostname, addr, timeout, retry, flags)` signature follows `sceNetResolverStartNtoa`. A name with no IPv6 address fails with `RESOLVER_ENODNS` (`0x804101E1`) like a failed lookup; whether the console returns IPv4-mapped addresses instead is not verified. Timeout and retry are ignored as in `sceNetResolverStartNtoa`, and any nonzero flags throw because their meaning for this call is not known. Unverified on hardware. From ba0755337d11d628b51e58e681c9587468278082 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Tue, 20 Oct 2026 05:53:52 +0200 Subject: [PATCH 226/269] fix(libSceNet): pass MSG_DONTWAIT to the wait for a local address on Windows sceNetRecv, sceNetRecvfrom and sceNetRecvmsg on a datagram socket without a local address waited in wait_for_local_address until the socket was bound, connected or written to, because only a non-blocking socket made that wait fail. FreeBSD 11.4 soreceive_dgram fails a MSG_DONTWAIT receive without data with EWOULDBLOCK. The flags now reach wait_for_local_address and the test covers an unbound socket. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libSceNet/Export.cpp | 10 +++++----- core/libs/tests/GuestSceNet.cpp | 8 ++++++++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index ab69f4e4e3..b059de1ff5 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -542,11 +542,11 @@ int wait_for_datagram(const Sock& socket, std::chrono::steady_clock::time_point return WSAPoll(&descriptor, 1, static_cast(std::min(remaining, INT_MAX))) == 0 ? NET_EAGAIN : 0; } -int wait_for_local_address(int s, const Sock& socket, bool empty) { +int wait_for_local_address(int s, const Sock& socket, bool empty, int flags) { if (socket.type == NET_SOCK_STREAM || socket.bound) return 0; const bool named = has_local_address(socket.native->value); if (!named && (socket.read_shut || empty)) return RECEIVE_NOTHING; - if (!named && socket.nonblock) return NET_EAGAIN; + if (!named && (socket.nonblock || (flags & NET_MSG_DONTWAIT) != 0)) return NET_EAGAIN; const auto deadline = socket.rcv_timeout_us > 0 ? std::chrono::steady_clock::now() + std::chrono::microseconds(socket.rcv_timeout_us) : std::chrono::steady_clock::time_point::max(); @@ -1000,7 +1000,7 @@ int64_t APS5_VABI sceNetRecv(int s, void* buf, size_t len, int flags) { socket = it->second; } #ifdef _WIN32 - if (const int wait = wait_for_local_address(s, socket, len == 0)) return wait == RECEIVE_NOTHING ? 0 : fail(wait); + if (const int wait = wait_for_local_address(s, socket, len == 0, flags)) return wait == RECEIVE_NOTHING ? 0 : fail(wait); #endif const GuestArena::HostWrite destination(len == 0 ? nullptr : buf, len); if (!destination.Open()) return fail(NET_EFAULT); @@ -1028,7 +1028,7 @@ int64_t APS5_VABI sceNetRecvfrom(int s, void* buf, size_t len, int flags, void* const GuestArena::HostWrite lengthOutput(from == nullptr ? nullptr : fromlen, from == nullptr ? 0 : sizeof(*fromlen)); if (!lengthOutput.Open()) return fail(NET_EFAULT); #ifdef _WIN32 - if (const int wait = wait_for_local_address(s, socket, len == 0)) { + if (const int wait = wait_for_local_address(s, socket, len == 0, flags)) { if (wait != RECEIVE_NOTHING) return fail(wait); if (from) *fromlen = 0; return 0; @@ -1140,7 +1140,7 @@ int64_t APS5_VABI sceNetRecvmsg(int s, NetMsghdr* msg, int flags) { } const bool datagram = socket.type != NET_SOCK_STREAM; #ifdef _WIN32 - if (const int wait = wait_for_local_address(s, socket, total == 0)) { + if (const int wait = wait_for_local_address(s, socket, total == 0, flags)) { if (wait != RECEIVE_NOTHING) return fail(wait); msg->flags = 0; if (msg->name) msg->name_length = 0; diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index 08eb6daf7f..d53666559e 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -467,6 +467,14 @@ static void CheckDontWait() { NetIovec in[] = {{buffer, sizeof(buffer)}}; NetMsghdr receive{nullptr, 0, in, 1, nullptr, 0, 0}; const auto start = std::chrono::steady_clock::now(); + const int unbound = sceNetSocket("dontwait-unbound", 2, 2, 17); + Require(unbound >= 0); + const auto unboundStart = std::chrono::steady_clock::now(); + Require(Failed(sceNetRecv(unbound, buffer, sizeof(buffer), DontWait), 35)); + Require(Failed(sceNetRecvfrom(unbound, buffer, sizeof(buffer), DontWait, nullptr, nullptr), 35)); + Require(Failed(sceNetRecvmsg(unbound, &receive, DontWait), 35)); + Require(std::chrono::steady_clock::now() - unboundStart < std::chrono::seconds(1)); + Require(sceNetSocketClose(unbound) == 0); Require(Failed(sceNetRecv(receiver, buffer, sizeof(buffer), DontWait), 35)); Require(Failed(sceNetRecvfrom(receiver, buffer, sizeof(buffer), DontWait, nullptr, nullptr), 35)); Require(Failed(sceNetRecvmsg(receiver, &receive, DontWait), 35)); From 63dbd310ba0ea2ec74dcb5e31f3661711b6838a8 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Sun, 11 Oct 2026 20:18:06 -0400 Subject: [PATCH 227/269] feat(ult): bind condition variables to the current mutex lifecycle --- core/libs/prx/libSceUlt/CMakeLists.txt | 13 ++ core/libs/prx/libSceUlt/Export.cpp | 120 ++++++++++-- core/libs/prx/libSceUlt/SceUltTypes.hpp | 15 ++ core/libs/tests/GuestUltCondition.cpp | 238 ++++++++++++++++++++++++ core/libs/tests/GuestUltThread.cpp | 10 +- docs/dev/debt/libSceUlt.md | 4 +- 6 files changed, 375 insertions(+), 25 deletions(-) create mode 100644 core/libs/tests/GuestUltCondition.cpp diff --git a/core/libs/prx/libSceUlt/CMakeLists.txt b/core/libs/prx/libSceUlt/CMakeLists.txt index e8cb567f4d..25fa2dad49 100644 --- a/core/libs/prx/libSceUlt/CMakeLists.txt +++ b/core/libs/prx/libSceUlt/CMakeLists.txt @@ -3,6 +3,19 @@ target_include_directories(libSceUlt PRIVATE ${LIBS_INCLUDE_DIR}) configure_windows_unwind(libSceUlt) if(BUILD_TESTING) + add_test_executable(guest_ult_condition_tests ${LIBS_INCLUDE_DIR}/tests/GuestUltCondition.cpp) + target_include_directories(guest_ult_condition_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_ult_condition_tests PRIVATE libSceUlt libc) + configure_windows_unwind(guest_ult_condition_tests) + foreach(testCase IN ITEMS validation handoff signal-one stress finalize-queued finalize-reacquire) + add_test(NAME guest_ult_condition_${testCase} COMMAND guest_ult_condition_tests ${testCase}) + set_tests_properties(guest_ult_condition_${testCase} PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_ult_condition_${testCase} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() + add_test_executable(guest_ult_queue_tests ${LIBS_INCLUDE_DIR}/tests/GuestUltQueue.cpp) target_include_directories(guest_ult_queue_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_ult_queue_tests PRIVATE libSceUlt libc) diff --git a/core/libs/prx/libSceUlt/Export.cpp b/core/libs/prx/libSceUlt/Export.cpp index 898ae6a376..7d2af42297 100644 --- a/core/libs/prx/libSceUlt/Export.cpp +++ b/core/libs/prx/libSceUlt/Export.cpp @@ -23,6 +23,7 @@ namespace { std::mutex gMutex; std::unordered_map> gMutexes; +std::unordered_map> gConditions; std::unordered_map> gSemaphores; std::unordered_map gResourcePools; std::unordered_map> gQueueDataPools; @@ -145,6 +146,13 @@ int APS5_VABI sceUltFinalize() { state->_alive = false; mutexes.push_back(state); } + for (const auto& [object, condition] : gConditions) { + std::lock_guard stateLock(condition->_mutex->_mutex); + condition->_alive = false; + --condition->_mutex->_conditions; + for (auto* waiter : condition->_queue) waiter->_available.notify_one(); + } + gConditions.clear(); gMutexes.clear(); gResourcePools.clear(); for (auto& entry : gQueues) { @@ -208,7 +216,7 @@ int APS5_VABI sceUltMutexDestroy(void* mutex) { } auto state = it->second; std::lock_guard stateLock(state->_mutex); - if (state->_lockCount != 0 || state->_waiters != 0) { + if (state->_lockCount != 0 || state->_waiters != 0 || state->_conditions != 0 || state->_conditionWaiters != 0) { return ULT_ERROR_BUSY; } state->_alive = false; @@ -276,6 +284,101 @@ int APS5_VABI sceUltMutexUnlock(void* mutex) { return ULT_OK; } +int APS5_VABI _sceUltConditionVariableCreate(void* condition, const char*, void* mutex, const void* optParam, std::uint32_t) { + if (condition == nullptr || mutex == nullptr) return ULT_ERROR_NULL; + if (optParam != nullptr) throw std::runtime_error("ULT condition variable options are not implemented"); + std::lock_guard lock(gMutex); + const auto mutexIt = gMutexes.find(mutex); + if (mutexIt == gMutexes.end()) return ULT_ERROR_INVALID; + if (gConditions.contains(condition)) return ULT_ERROR_STATE; + auto state = std::make_shared(); + state->_mutex = mutexIt->second; + std::lock_guard stateLock(state->_mutex->_mutex); + if (!state->_mutex->_alive) return ULT_ERROR_STATE; + gConditions.emplace(condition, state); + ++state->_mutex->_conditions; + return ULT_OK; +} + +int APS5_VABI sceUltConditionVariableDestroy(void* condition) { + std::lock_guard lock(gMutex); + const auto it = gConditions.find(condition); + if (it == gConditions.end()) return condition == nullptr ? ULT_ERROR_NULL : ULT_ERROR_STATE; + auto state = it->second; + std::lock_guard stateLock(state->_mutex->_mutex); + if (state->_waiters != 0) return ULT_ERROR_BUSY; + state->_alive = false; + --state->_mutex->_conditions; + gConditions.erase(it); + return ULT_OK; +} + +int APS5_VABI sceUltConditionVariableSignal(void* condition) { + std::shared_ptr state; + { + std::lock_guard lock(gMutex); + const auto it = gConditions.find(condition); + if (it == gConditions.end()) return condition == nullptr ? ULT_ERROR_NULL : ULT_ERROR_STATE; + state = it->second; + } + std::lock_guard stateLock(state->_mutex->_mutex); + if (!state->_alive || !state->_mutex->_alive) return ULT_ERROR_STATE; + if (!state->_queue.empty()) { + auto* waiter = state->_queue.front(); + state->_queue.pop_front(); + waiter->_signaled = true; + waiter->_available.notify_one(); + } + return ULT_OK; +} + +int APS5_VABI sceUltConditionVariableWait(void* condition) { + std::shared_ptr state; + { + std::lock_guard lock(gMutex); + const auto it = gConditions.find(condition); + if (it == gConditions.end()) return condition == nullptr ? ULT_ERROR_NULL : ULT_ERROR_STATE; + state = it->second; + } + const auto held = state->_mutex; + const auto owner = std::this_thread::get_id(); + std::unique_lock lock(held->_mutex); + if (!state->_alive || !held->_alive || held->_owner != owner || held->_lockCount == 0) return ULT_ERROR_STATE; + if (held->_lockCount != 1) throw std::runtime_error("ULT condition wait with a recursively held mutex is not implemented"); + UltConditionWaiter waiter; + state->_queue.push_back(&waiter); + ++state->_waiters; + ++held->_conditionWaiters; + held->_lockCount = 0; + held->_owner = {}; + held->_available.notify_one(); + try { + waiter._available.wait(lock, [&] { return waiter._signaled || !state->_alive || !held->_alive; }); + if (held->_alive && state->_alive) { + ++held->_waiters; + try { + held->_available.wait(lock, [&] { return !held->_alive || held->_lockCount == 0; }); + } catch (...) { + --held->_waiters; + throw; + } + --held->_waiters; + } + } catch (...) { + state->_queue.remove(&waiter); + --state->_waiters; + --held->_conditionWaiters; + throw; + } + state->_queue.remove(&waiter); + --state->_waiters; + --held->_conditionWaiters; + if (!held->_alive || !state->_alive) return ULT_ERROR_STATE; + held->_owner = owner; + held->_lockCount = 1; + return ULT_OK; +} + int APS5_VABI sceUltWaitingQueueResourcePoolGetWorkAreaSize(std::uint32_t numThreads, std::uint32_t numSyncObjects) { return static_cast(alignUp(static_cast(numThreads + numSyncObjects) * 256u, 8u)); } @@ -713,26 +816,11 @@ int APS5_VABI _sceUltUlthreadCreate(void* ulthread, const char* name, UltUlthrea return sceUltUlthreadCreate(ulthread, name, entry, arg, context, sizeContext, runtime, optParam, buildVersion); } -int APS5_VABI sceUltConditionVariableDestroy() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceUltConditionVariableSignal() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceUltConditionVariableSignalAll() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceUltConditionVariableWait() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceUltGetConditionVariableInfo() { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceUlt/SceUltTypes.hpp b/core/libs/prx/libSceUlt/SceUltTypes.hpp index 995972b952..0da430382c 100644 --- a/core/libs/prx/libSceUlt/SceUltTypes.hpp +++ b/core/libs/prx/libSceUlt/SceUltTypes.hpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -25,11 +26,25 @@ struct UltMutexState { std::thread::id _owner{}; std::uint32_t _lockCount = 0; std::uint32_t _waiters = 0; + std::uint32_t _conditions = 0; + std::uint32_t _conditionWaiters = 0; bool _alive = true; std::uint32_t _attribute = 0; void* _waitingPool = nullptr; }; +struct UltConditionWaiter { + std::condition_variable _available; + bool _signaled = false; +}; + +struct UltConditionState { + std::shared_ptr _mutex; + std::list _queue; + std::uint32_t _waiters = 0; + bool _alive = true; +}; + struct UltSemaphoreState { std::mutex _mutex; std::condition_variable _available; diff --git a/core/libs/tests/GuestUltCondition.cpp b/core/libs/tests/GuestUltCondition.cpp new file mode 100644 index 0000000000..51741e4aa2 --- /dev/null +++ b/core/libs/tests/GuestUltCondition.cpp @@ -0,0 +1,238 @@ +#include "SceTypes.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceUltFinalize(); +int APS5_VABI sceUltMutexCreate(void*, const char*, void*, const UltMutexOptParam*, std::uint32_t); +int APS5_VABI sceUltMutexLock(void*); +int APS5_VABI sceUltMutexUnlock(void*); +int APS5_VABI sceUltMutexDestroy(void*); +int APS5_VABI _sceUltConditionVariableCreate(void*, const char*, void*, const void*, std::uint32_t); +int APS5_VABI sceUltConditionVariableWait(void*); +int APS5_VABI sceUltConditionVariableSignal(void*); +int APS5_VABI sceUltConditionVariableDestroy(void*); +} + +constexpr int Null = -2139029503; +constexpr int Invalid = -2139029500; +constexpr int State = -2139029498; +constexpr int Busy = -2139029497; + +static void Require(bool ok, int line) { + if (!ok) { + std::fprintf(stderr, "ULT condition check failed at line %d\n", line); + std::abort(); + } +} + +#define CHECK(value) Require((value), __LINE__) + +struct alignas(8) Object { std::array bytes{}; }; + +struct Fixture { + Object mutex; + Object condition; + + Fixture() { + CHECK(sceUltMutexCreate(&mutex, "mutex", nullptr, nullptr, 0x12000000) == 0); + CHECK(_sceUltConditionVariableCreate(&condition, "condition", &mutex, nullptr, 0x12000000) == 0); + } + + void Lock() { CHECK(sceUltMutexLock(&mutex) == 0); } + void Unlock() { CHECK(sceUltMutexUnlock(&mutex) == 0); } + void Wait() { CHECK(sceUltConditionVariableWait(&condition) == 0); } + void Signal() { CHECK(sceUltConditionVariableSignal(&condition) == 0); } + + ~Fixture() { + CHECK(sceUltConditionVariableDestroy(&condition) == 0); + CHECK(sceUltMutexDestroy(&mutex) == 0); + } +}; + +static void Validation() { + Fixture f; + Object unused; + CHECK(sceUltMutexDestroy(nullptr) == Null); + CHECK(sceUltMutexDestroy(&unused) == State); + CHECK(_sceUltConditionVariableCreate(nullptr, nullptr, nullptr, nullptr, 0) == Null); + CHECK(_sceUltConditionVariableCreate(&unused, nullptr, &unused, nullptr, 0) == Invalid); + CHECK(_sceUltConditionVariableCreate(&f.condition, nullptr, &f.mutex, nullptr, 0) == State); + CHECK(sceUltConditionVariableDestroy(nullptr) == Null); + CHECK(sceUltConditionVariableDestroy(&unused) == State); + CHECK(sceUltConditionVariableSignal(nullptr) == Null); + CHECK(sceUltConditionVariableSignal(&unused) == State); + CHECK(sceUltConditionVariableWait(nullptr) == Null); + CHECK(sceUltConditionVariableWait(&unused) == State); + CHECK(_sceUltConditionVariableCreate(&unused, nullptr, nullptr, nullptr, 0) == Null); + CHECK(sceUltConditionVariableWait(&f.condition) == State); + CHECK(sceUltMutexUnlock(&f.mutex) == State); + 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(sceUltMutexCreate(&f.mutex, nullptr, nullptr, nullptr, 0) == State); + auto other = std::async(std::launch::async, [&] { + CHECK(sceUltMutexUnlock(&f.mutex) == State); + CHECK(sceUltConditionVariableWait(&f.condition) == State); + }); + other.get(); + f.Lock(); + unsupported = false; + try { f.Wait(); } catch (const std::runtime_error&) { unsupported = true; } + CHECK(unsupported); + f.Unlock(); + CHECK(sceUltMutexDestroy(&f.mutex) == Busy); + f.Unlock(); + CHECK(sceUltMutexDestroy(&f.mutex) == Busy); + CHECK(sceUltConditionVariableDestroy(&f.condition) == 0); + CHECK(sceUltMutexDestroy(&f.mutex) == 0); + CHECK(sceUltMutexLock(&f.mutex) == State); + CHECK(sceUltMutexCreate(&f.mutex, nullptr, nullptr, nullptr, 0) == 0); + CHECK(sceUltConditionVariableDestroy(&f.condition) == State); + CHECK(sceUltConditionVariableSignal(&f.condition) == State); + CHECK(_sceUltConditionVariableCreate(&f.condition, nullptr, &f.mutex, nullptr, 0) == 0); +} + +static void Await(const std::atomic& count, int expected) { + const auto end = std::chrono::steady_clock::now() + std::chrono::seconds(3); + while (count.load() != expected) { + CHECK(std::chrono::steady_clock::now() < end); + std::this_thread::yield(); + } +} + +static void Handoff() { + Fixture f; + f.Signal(); + std::atomic entered = 0; + int payload = 0; + auto waiter = std::async(std::launch::async, [&] { + f.Lock(); + entered = 1; + f.Wait(); + CHECK(payload == 42); + CHECK(sceUltMutexDestroy(&f.mutex) == Busy); + f.Unlock(); + }); + Await(entered, 1); + f.Lock(); + CHECK(sceUltConditionVariableDestroy(&f.condition) == Busy); + CHECK(sceUltMutexDestroy(&f.mutex) == Busy); + CHECK(waiter.wait_for(std::chrono::milliseconds(30)) == std::future_status::timeout); + f.Unlock(); + f.Lock(); + payload = 42; + f.Signal(); + CHECK(waiter.wait_for(std::chrono::milliseconds(30)) == std::future_status::timeout); + CHECK(sceUltConditionVariableDestroy(&f.condition) == Busy); + f.Unlock(); + CHECK(waiter.wait_for(std::chrono::seconds(3)) == std::future_status::ready); + waiter.get(); +} + +static void SignalOne() { + Fixture f; + std::atomic entered = 0, finished = 0; + std::array, 3> waiters; + for (auto& waiter : waiters) waiter = std::async(std::launch::async, [&] { + f.Lock(); + ++entered; + f.Wait(); + ++finished; + f.Unlock(); + }); + Await(entered, 3); + f.Lock(); + f.Signal(); + f.Unlock(); + Await(finished, 1); + std::this_thread::sleep_for(std::chrono::milliseconds(30)); + CHECK(finished == 1); + f.Lock(); + f.Signal(); + f.Signal(); + f.Unlock(); + for (auto& waiter : waiters) { + CHECK(waiter.wait_for(std::chrono::seconds(3)) == std::future_status::ready); + waiter.get(); + } + CHECK(finished == 3); +} + +static void Stress() { + Fixture f; + Object consumed; + CHECK(_sceUltConditionVariableCreate(&consumed, nullptr, &f.mutex, nullptr, 0) == 0); + int payload = 0; + auto consumer = std::async(std::launch::async, [&] { + for (int expected = 1; expected <= 5000; ++expected) { + f.Lock(); + while (payload == 0) f.Wait(); + CHECK(payload == expected); + payload = 0; + CHECK(sceUltConditionVariableSignal(&consumed) == 0); + f.Unlock(); + } + }); + for (int value = 1; value <= 5000; ++value) { + f.Lock(); + while (payload != 0) CHECK(sceUltConditionVariableWait(&consumed) == 0); + payload = value; + f.Signal(); + f.Unlock(); + } + CHECK(consumer.wait_for(std::chrono::seconds(3)) == std::future_status::ready); + consumer.get(); + CHECK(sceUltConditionVariableDestroy(&consumed) == 0); +} + +static void Finalize(bool signaled) { + Object mutex, condition; + CHECK(sceUltMutexCreate(&mutex, "mutex", nullptr, nullptr, 0) == 0); + CHECK(_sceUltConditionVariableCreate(&condition, "condition", &mutex, nullptr, 0) == 0); + std::atomic entered = 0; + auto waiter = std::async(std::launch::async, [&] { + CHECK(sceUltMutexLock(&mutex) == 0); + entered = 1; + CHECK(sceUltConditionVariableWait(&condition) == State); + CHECK(sceUltMutexUnlock(&mutex) == State); + }); + Await(entered, 1); + CHECK(sceUltMutexLock(&mutex) == 0); + if (signaled) CHECK(sceUltConditionVariableSignal(&condition) == 0); + CHECK(waiter.wait_for(std::chrono::milliseconds(30)) == std::future_status::timeout); + CHECK(sceUltConditionVariableDestroy(&condition) == Busy); + CHECK(sceUltFinalize() == 0); + CHECK(waiter.wait_for(std::chrono::seconds(3)) == std::future_status::ready); + waiter.get(); + CHECK(sceUltConditionVariableWait(&condition) == State); + CHECK(sceUltMutexLock(&mutex) == State); + CHECK(sceUltMutexCreate(&mutex, "reopened", nullptr, nullptr, 0) == 0); + CHECK(_sceUltConditionVariableCreate(&condition, "reopened", &mutex, nullptr, 0) == 0); + CHECK(sceUltConditionVariableDestroy(&condition) == 0); + CHECK(sceUltMutexDestroy(&mutex) == 0); +} + +int main(int argc, char** argv) { + if (argc != 2) return 1; + const std::string test = argv[1]; + if (test == "validation") Validation(); + else if (test == "handoff") Handoff(); + else if (test == "signal-one") SignalOne(); + else if (test == "stress") Stress(); + else if (test == "finalize-queued") Finalize(false); + else if (test == "finalize-reacquire") Finalize(true); + else return 1; + CHECK(sceUltFinalize() == 0); + std::puts("ULT condition tests passed"); +} diff --git a/core/libs/tests/GuestUltThread.cpp b/core/libs/tests/GuestUltThread.cpp index b4d9e523ba..c89c38791d 100644 --- a/core/libs/tests/GuestUltThread.cpp +++ b/core/libs/tests/GuestUltThread.cpp @@ -37,7 +37,7 @@ int APS5_VABI sceUltWaitingQueueResourcePoolDestroy(void*); int APS5_VABI sceUltSemaphoreDestroy(void*); int APS5_VABI sceUltQueueDestroy(void*); int APS5_VABI sceUltQueueDataResourcePoolDestroy(void*); -int APS5_VABI sceUltConditionVariableDestroy(); +int APS5_VABI sceUltConditionVariableDestroy(void*); int APS5_VABI sceUltReaderWriterLockDestroy(); int APS5_VABI sceUltGetConditionVariableInfo(); } @@ -110,13 +110,7 @@ static void Validation() { CHECK(sceUltUlthreadJoin(&thread, &status) == Ok && status == 0x27); CHECK(sceUltFinalize() == Ok); CHECK(sceUltUlthreadRuntimeGetWorkAreaSize(1, 1) == 256u + 16u * 1024u); - bool threwCv = false; - try { - sceUltConditionVariableDestroy(); - } catch (const std::runtime_error&) { - threwCv = true; - } - CHECK(threwCv); + CHECK(sceUltConditionVariableDestroy(nullptr) == -2139029503); bool threwRwl = false; try { sceUltReaderWriterLockDestroy(); diff --git a/docs/dev/debt/libSceUlt.md b/docs/dev/debt/libSceUlt.md index 4b049f53ba..bbb48c53ce 100644 --- a/docs/dev/debt/libSceUlt.md +++ b/docs/dev/debt/libSceUlt.md @@ -2,11 +2,13 @@ ### Unknown function info +- [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 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 -- [sceUltConditionVariableDestroy, sceUltConditionVariableSignal, sceUltConditionVariableSignalAll, sceUltConditionVariableWait, sceUltGetConditionVariableInfo, sceUltGetMutexInfo, sceUltGetQueueDataResourcePoolInfo, sceUltGetQueueInfo, sceUltGetReaderWriterLockInfo, sceUltGetSemaphoreInfo, sceUltGetUlthreadInfo, sceUltGetUlthreadRuntimeInfo, sceUltGetWaitingQueueResourcePoolInfo, sceUltMutexTryLock, sceUltReaderWriterLockDestroy, sceUltReaderWriterLockLockRead, sceUltReaderWriterLockLockWrite, sceUltReaderWriterLockTryLockRead, sceUltReaderWriterLockTryLockWrite, sceUltReaderWriterLockUnlockRead, sceUltReaderWriterLockUnlockWrite, sceUltUlthreadExit, sceUltUlthreadGetSelf, sceUltUlthreadYield, sceUltAbstractThreadFactoryCreateThread, sceUltAbstractThreadFactoryGetNumMaxThreads, sceUltAbstractThreadFactoryJoinThread, sceUltKernelThreadFactoryFinalize, sceUltKernelThreadFactoryGetFactoryPointer, sceUltKernelThreadFactoryInitialize, sceUltModuleFinalize, sceUltModuleInitialize, sceUltUlthreadFactoryFinalize, sceUltUlthreadFactoryGetFactoryPointer, sceUltUlthreadFactoryGetWorkAreaSize, sceUltUlthreadFactoryInitialize, sceUltConditionVariableCreate, sceUltConditionVariableOptParamInitialize, sceUltQueueDataResourcePoolOptParamInitialize, sceUltQueueOptParamInitialize, sceUltReaderWriterLockCreate, sceUltReaderWriterLockOptParamInitialize, sceUltSemaphoreOptParamInitialize, sceUltUlthreadOptParamInitialize, sceUltWaitingQueueResourcePoolOptParamInitialize](../../../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 +- [sceUltConditionVariableSignalAll, sceUltGetConditionVariableInfo, sceUltGetMutexInfo, sceUltGetQueueDataResourcePoolInfo, sceUltGetQueueInfo, sceUltGetReaderWriterLockInfo, sceUltGetSemaphoreInfo, sceUltGetUlthreadInfo, sceUltGetUlthreadRuntimeInfo, sceUltGetWaitingQueueResourcePoolInfo, sceUltMutexTryLock, sceUltReaderWriterLockDestroy, sceUltReaderWriterLockLockRead, sceUltReaderWriterLockLockWrite, sceUltReaderWriterLockTryLockRead, sceUltReaderWriterLockTryLockWrite, sceUltReaderWriterLockUnlockRead, sceUltReaderWriterLockUnlockWrite, sceUltUlthreadExit, sceUltUlthreadGetSelf, sceUltUlthreadYield, sceUltAbstractThreadFactoryCreateThread, sceUltAbstractThreadFactoryGetNumMaxThreads, sceUltAbstractThreadFactoryJoinThread, sceUltKernelThreadFactoryFinalize, sceUltKernelThreadFactoryGetFactoryPointer, sceUltKernelThreadFactoryInitialize, sceUltModuleFinalize, sceUltModuleInitialize, sceUltUlthreadFactoryFinalize, sceUltUlthreadFactoryGetFactoryPointer, sceUltUlthreadFactoryGetWorkAreaSize, sceUltUlthreadFactoryInitialize, sceUltConditionVariableCreate, sceUltConditionVariableOptParamInitialize, sceUltQueueDataResourcePoolOptParamInitialize, sceUltQueueOptParamInitialize, sceUltReaderWriterLockCreate, sceUltReaderWriterLockOptParamInitialize, sceUltSemaphoreOptParamInitialize, sceUltUlthreadOptParamInitialize, sceUltWaitingQueueResourcePoolOptParamInitialize](../../../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 ### 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`. From afec282b0f4c7595045a3cd9e23ddded22458ce0 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Mon, 19 Oct 2026 20:16:56 -0400 Subject: [PATCH 228/269] fix(libkernel): report ELOOP from sceKernelStat for Windows symbolic link loops On Windows std::filesystem::status reports a symbolic link that resolves to itself as missing, because the CRT maps ERROR_CANT_RESOLVE_FILENAME to ENOENT, so sceKernelStat returned 0x80020002 where the console and the Linux build return SCE_KERNEL_ERROR_ELOOP. When the status says the path does not exist, open it once following links and return ELOOP when Windows fails with ERROR_CANT_RESOLVE_FILENAME. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/File/src/Open.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index ea387b611f..38ff5b2e6f 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -112,6 +112,13 @@ static std::filesystem::path NativeMetadataPath(const std::filesystem::path& pat return native.starts_with(L"\\\\") ? std::filesystem::path(L"\\\\?\\UNC\\" + native.substr(2)) : std::filesystem::path(L"\\\\?\\" + native); } +static bool NativeSymbolicLinkLoop(const std::filesystem::path& path) { + const auto handle = CreateFileW(path.c_str(), FILE_READ_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); + if (handle == INVALID_HANDLE_VALUE) return GetLastError() == ERROR_CANT_RESOLVE_FILENAME; + CloseHandle(handle); + return false; +} static int MapFlags(int sceFlags) { int f = 0; const int acc = sceFlags & SCE_KERNEL_O_ACCMODE; @@ -370,6 +377,9 @@ int APS5_VABI sceKernelStat(const char* path, FileStat* sb) { if (error == std::errc::not_a_directory) return SceErrorFromErrno(GuestEnotdir); if (error == std::errc::filename_too_long || error == std::errc::too_many_symbolic_link_levels) return SceKernelError(FilesystemError(error)); if (!std::filesystem::exists(status)) { +#ifdef _WIN32 + if (NativeSymbolicLinkLoop(metadataPath)) return SCE_KERNEL_ERROR_ELOOP; +#endif return SceErrorFromErrno(NotFoundError_nid_no_patch(native)); } const std::string_view guestPath(path); From e3125e43afd017990ff0ea07e8e32dd744cec0da Mon Sep 17 00:00:00 2001 From: dantdmdarealguy Date: Mon, 12 Oct 2026 18:15:14 -0500 Subject: [PATCH 229/269] feat(agc,videoout): save the displayed frame as a PNG with a screenshot key F12 (the new Screenshot input action) requests a capture of the next presented display buffer. The presenter records it with the existing APS5_DUMP_FRAMES GPU readback at full resolution, and the frame dump writer thread encodes it with decoder_png into anyps5-screenshots/ beside the executable, or ANYPS5_SCREENSHOTS. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 12 ++- .../Execution/include/Driver.hpp | 1 + .../Execution/include/Screenshot.hpp | 18 ++++ .../Execution/include/VulkanDevice.hpp | 1 + .../Execution/src/Driver/EntryPoints.cpp | 5 ++ .../Execution/src/Screenshot.cpp | 77 ++++++++++++++++ .../Execution/src/VulkanDevice.cpp | 58 +++++++++--- .../prx/libSceAgcDriver/tests/Screenshot.cpp | 88 +++++++++++++++++++ .../prx/libScePad/include/InputMapping.hpp | 1 + .../prx/libScePad/include/KeyboardMouse.hpp | 1 + .../prx/libScePad/include/PadInputTypes.hpp | 3 +- .../prx/libSceVideoOut/src/InputMapping.cpp | 6 +- .../prx/libSceVideoOut/src/KeyboardMouse.cpp | 2 + core/libs/prx/libSceVideoOut/src/PadInput.cpp | 2 + .../prx/libSceVideoOut/tests/InputMapping.cpp | 19 ++++ .../libSceVideoOut/tests/KeyboardMouse.cpp | 6 ++ docs/dev/ENVIRONMENT.md | 1 + docs/user/INPUT_MAPPING.md | 3 +- docs/user/USAGE.md | 4 + 19 files changed, 293 insertions(+), 15 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/Execution/include/Screenshot.hpp create mode 100644 core/libs/prx/libSceAgcDriver/Execution/src/Screenshot.cpp create mode 100644 core/libs/prx/libSceAgcDriver/tests/Screenshot.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index fbd9b9d9c6..c93a640130 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -266,6 +266,7 @@ set(agcDriverSources ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/PresentationScaler.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/PresentationPass.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/PerformanceOverlay.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Screenshot.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Draw.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/DepthSurface.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/MultisampleTarget.cpp @@ -358,6 +359,7 @@ add_shader_recompiler(shader_recompiler_agc_driver libc target_link_libraries(libSceAgcDriver PRIVATE shader_recompiler_agc_driver + decoder_png SDL2-static libc libkernel @@ -367,6 +369,7 @@ target_link_libraries(libSceAgcDriver PRIVATE if(APS5_ENABLE_TRACY) target_link_libraries(libSceAgcDriver PUBLIC TracyClient) endif() +target_link_options(libSceAgcDriver PRIVATE LINKER:--exclude-libs,$) if(BUILD_TESTING) add_test_executable(agc_driver_draw_program_trace_tests @@ -1124,6 +1127,12 @@ target_link_libraries(agc_placeholder_images_tests PRIVATE shader_recompiler_agc set_target_properties(agc_placeholder_images_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_placeholder_images_tests) +add_test_executable(agc_screenshot_tests EXCLUDE_FROM_ALL tests/Screenshot.cpp Execution/src/Screenshot.cpp) +target_include_directories(agc_screenshot_tests PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(agc_screenshot_tests PRIVATE decoder_png SDL2-static libc) +set_target_properties(agc_screenshot_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_screenshot_tests) + add_test_executable(agc_shader_device_profile_tests EXCLUDE_FROM_ALL tests/ShaderDeviceProfile.cpp Execution/src/ShaderDeviceProfile.cpp) target_include_directories(agc_shader_device_profile_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) target_link_libraries(agc_shader_device_profile_tests PRIVATE shader_recompiler_agc_driver libc) @@ -2037,7 +2046,7 @@ if(BUILD_TESTING) if(WIN32) set_tests_properties(agc_shader_bindless agc_shader_selected_records_image_limit agc_shader_selected_records_pair_limit agc_shader_selected_records_direct_image_limit agc_shader_selected_records_direct_pair_limit agc_shader_srt_read_patching agc_shader_type_zero_image_materialization agc_shader_frontend_pair agc_shader_frontend_pair_off agc_shader_layout_pair agc_shader_layout_pair_off agc_shader_entry_lane_proof_budget PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() - foreach(agcTest IN ITEMS agc_vertex_input_tests agc_mesh_output_budget_tests agc_shared_write_tests agc_driver_tests agc_shader_memory_tests agc_structurizer_tests agc_spirv_module_tests agc_dpp_write_tests agc_masked_select_tests agc_ir_reclaim_tests agc_vector_window_tests agc_constant_bit_test_tests agc_integer_narrowing_tests agc_scalar_select_mask_tests agc_vop1_control_decode_tests agc_validate_program_tests agc_pass_hazards_tests agc_barrier_batch_tests agc_shader_disk_cache_tests agc_shader_device_profile_tests agc_placeholder_images_tests agc_shader_control_flow_tests agc_driver_flip_tests agc_driver_pm4_tests agc_driver_graphics_tests agc_driver_bda_device_tests agc_driver_mesh_tests agc_driver_cmask_fast_clear_tests agc_formatted_buffer_once_tests agc_formatted_buffer_byte_stores_tests ${agcExecutionTests} agc_fma_mix_once_tests agc_f16_single_convert_tests) + foreach(agcTest IN ITEMS agc_vertex_input_tests agc_mesh_output_budget_tests agc_shared_write_tests agc_driver_tests agc_shader_memory_tests agc_structurizer_tests agc_spirv_module_tests agc_dpp_write_tests agc_masked_select_tests agc_ir_reclaim_tests agc_vector_window_tests agc_constant_bit_test_tests agc_integer_narrowing_tests agc_scalar_select_mask_tests agc_vop1_control_decode_tests agc_validate_program_tests agc_pass_hazards_tests agc_barrier_batch_tests agc_shader_disk_cache_tests agc_screenshot_tests agc_shader_device_profile_tests agc_placeholder_images_tests agc_shader_control_flow_tests agc_driver_flip_tests agc_driver_pm4_tests agc_driver_graphics_tests agc_driver_bda_device_tests agc_driver_mesh_tests agc_driver_cmask_fast_clear_tests agc_formatted_buffer_once_tests agc_formatted_buffer_byte_stores_tests ${agcExecutionTests} agc_fma_mix_once_tests agc_f16_single_convert_tests) if(agcTest STREQUAL "agc_driver_constant_store_tests" OR agcTest STREQUAL "agc_driver_flat_apertures_tests" OR agcTest STREQUAL "agc_driver_pipeline_optimized_readback_tests") continue() endif() @@ -2555,6 +2564,7 @@ if(AGC_BUILD_VISUAL_TEST) target_link_libraries(agc_driver_visual_test PRIVATE shader_recompiler_agc_driver + decoder_png SDL2-static libc libkernel diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver.hpp index 25bb3d485a..52582c1143 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver.hpp @@ -17,6 +17,7 @@ extern "C" void AgcDriverRaiseWorkerThreadPriority_nid_postfix(const char* role) extern "C" void AgcDriverLockVulkanLoader_nid_postfix(); extern "C" void AgcDriverUnlockVulkanLoader_nid_postfix(); extern "C" void AgcDriverShutdown_nid_postfix(); +extern "C" void AgcDriverRequestScreenshot_nid_postfix(); extern "C" void AgcDriverSuspendPoint_nid_postfix(); extern "C" std::uint64_t AgcDriverDispatchesAdoptedAhead_nid_postfix(); extern "C" void AgcDriverRegisterShader_nid_postfix(const Shader* shader); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Screenshot.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Screenshot.hpp new file mode 100644 index 0000000000..2d8c1c8ec2 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Screenshot.hpp @@ -0,0 +1,18 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_SCREENSHOT_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_SCREENSHOT_HPP + +#include +#include +#include +#include +#include + +namespace AgcDriver::Screenshot { + +void Request(); +bool Take(); +std::filesystem::path Write(std::string_view titleId, std::uint32_t width, std::uint32_t height, std::span bgra); + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp index 1b5e098a87..252764922f 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp @@ -361,6 +361,7 @@ private: // registration: keeps, the template data refresh, barriers, bind, push, dispatch, marks. void recordDispatch(RecordedDispatch& record); bool present(std::uint32_t width, std::uint32_t height, bool opaque, std::span pixels, const DisplayBuffer* display = nullptr, const std::shared_ptr& resident = nullptr, VkFilter residentFilter = VK_FILTER_LINEAR, bool dumpFrame = false, bool residentConvert = false, const VkClearColorValue* uniform = nullptr, std::uint32_t residentLayer = 0, bool passthrough = false); + bool takeScreenshot(bool dumpFrame); void recreateSwapchain(std::uint32_t width, std::uint32_t height, bool force); void refreshSurfaceFormats(); void setHdrMetadata(); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp index bf6d16754a..1012187b38 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp @@ -1,5 +1,6 @@ #include "prx/libSceAgcDriver/Execution/include/ThreadPriority.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Screenshot.hpp" #include namespace AgcDriver { @@ -83,6 +84,10 @@ extern "C" void AgcDriverShutdown_nid_postfix() { AgcDriver::Shutdown(); } +extern "C" void AgcDriverRequestScreenshot_nid_postfix() { + AgcDriver::Screenshot::Request(); +} + extern "C" void AgcDriverRegisterShader_nid_postfix(const Shader* shader) try { AgcDriver::RegisterShader(shader); } catch (const ProcessShutdown&) { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Screenshot.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Screenshot.cpp new file mode 100644 index 0000000000..6a930d4e30 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Screenshot.cpp @@ -0,0 +1,77 @@ +#include "prx/libSceAgcDriver/Execution/include/Screenshot.hpp" +#include "Decoder/Png.hpp" +#include "SDL_filesystem.h" +#include "SDL_error.h" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +std::atomic requested{false}; + +std::filesystem::path directory() { + const char* configured = std::getenv("ANYPS5_SCREENSHOTS"); + if (configured != nullptr && configured[0] != '\0') return configured; + const std::unique_ptr basePath(SDL_GetBasePath(), SDL_free); + if (!basePath) throw std::runtime_error(std::string("Screenshot: cannot locate the executable: ") + SDL_GetError()); + return std::filesystem::path(reinterpret_cast(basePath.get())) / "anyps5-screenshots"; +} + +std::string localTimestamp() { + const std::time_t now = std::time(nullptr); + std::tm local{}; +#ifdef _WIN32 + if (localtime_s(&local, &now) != 0) throw std::runtime_error("Screenshot: cannot read the local time"); +#else + if (localtime_r(&now, &local) == nullptr) throw std::runtime_error("Screenshot: cannot read the local time"); +#endif + char text[32]; + if (std::strftime(text, sizeof(text), "%Y%m%d-%H%M%S", &local) == 0) throw std::runtime_error("Screenshot: cannot format the local time"); + return text; +} + +std::filesystem::path unusedPath(const std::filesystem::path& folder, const std::string& stem) { + for (int index = 0; index < 1000; ++index) { + const auto path = folder / (index == 0 ? stem + ".png" : stem + "-" + std::to_string(index) + ".png"); + if (!std::filesystem::exists(path)) return path; + } + throw std::runtime_error("Screenshot: no unused file name for " + stem + " in " + folder.string()); +} + +} + +void AgcDriver::Screenshot::Request() { + requested.store(true); +} + +bool AgcDriver::Screenshot::Take() { + return requested.exchange(false); +} + +std::filesystem::path AgcDriver::Screenshot::Write(std::string_view titleId, std::uint32_t width, std::uint32_t height, std::span bgra) { + if (titleId.empty()) throw std::invalid_argument("Screenshot: empty title ID"); + const auto pixelCount = static_cast(width) * height; + if (width == 0 || height == 0 || bgra.size() != pixelCount * 4) throw std::invalid_argument("Screenshot: invalid BGRA8 frame"); + std::vector rgb(pixelCount * 3); + for (std::size_t pixel = 0; pixel < pixelCount; ++pixel) { + rgb[pixel * 3] = std::to_integer(bgra[pixel * 4 + 2]); + rgb[pixel * 3 + 1] = std::to_integer(bgra[pixel * 4 + 1]); + rgb[pixel * 3 + 2] = std::to_integer(bgra[pixel * 4]); + } + const auto png = Decoder::Png::Encode(rgb, width, height, 3); + const auto folder = directory(); + std::filesystem::create_directories(folder); + const auto path = unusedPath(folder, std::string(titleId) + "-" + localTimestamp()); + std::ofstream file; + file.exceptions(std::ios::failbit | std::ios::badbit); + file.open(path, std::ios::binary); + file.write(reinterpret_cast(png.data()), static_cast(png.size())); + file.close(); + return path; +} diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index d642cf0344..f63c30b6bc 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -24,6 +24,8 @@ #include "prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp" #include "prx/libSceAgcDriver/Execution/include/PresentationPass.hpp" #include "prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp" +#include "prx/libSceAgcDriver/Execution/include/Screenshot.hpp" +#include "prx/libkernel/AppMetadata/include/AppMetadata.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp" #include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" @@ -155,24 +157,30 @@ public: if (worker.joinable()) worker.join(); } - void Enqueue(int index, std::uint32_t width, std::uint32_t height, std::uint32_t scale, std::vector pixels) { + 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); require(!stopping, "frame dump writer has stopped"); require(pixels.size() <= maxBytes - queuedBytes, "frame dump queue exceeded 64 MiB"); - if (!frameDumpOutput.Enabled_nid_no_patch()) return; + if (!screenshot && !frameDumpOutput.Enabled_nid_no_patch()) return; if (!worker.joinable()) worker = std::thread([this] { run(); }); const auto bytes = pixels.size(); - pending.push_back({index, width, height, scale, std::move(pixels)}); + pending.push_back({index, width, height, scale, screenshot, std::move(pixels)}); queuedBytes += bytes; changed.notify_one(); } + bool Idle() { + std::lock_guard lock(mutex); + return queuedBytes == 0; + } + private: struct Frame { int index; std::uint32_t width; std::uint32_t height; std::uint32_t scale; + bool screenshot; std::vector pixels; }; @@ -187,7 +195,8 @@ private: batch.swap(pending); } for (const auto& frame : batch) { - WriteFrameBmp(frame.index, frame.width, frame.height, frame.pixels, frame.scale); + if (frame.screenshot) std::fprintf(stderr, "[gpu] screenshot saved: %s\n", Screenshot::Write(GetAppTitleId_nid_postfix().value, frame.width, frame.height, frame.pixels).string().c_str()); + else WriteFrameBmp(frame.index, frame.width, frame.height, frame.pixels, frame.scale); std::lock_guard lock(mutex); queuedBytes -= frame.pixels.size(); } @@ -449,6 +458,7 @@ struct VulkanDevice::State { std::uint32_t dumpHeight = 0; std::uint32_t dumpScale = 1; bool dumpRecorded = false; + bool dumpScreenshot = false; int dumpIndex = 0; std::uint32_t imageIndex = 0; bool inFlight = false; @@ -480,6 +490,7 @@ struct VulkanDevice::State { bool queuePending = false; std::uint32_t queueIndex = 0; int nextDumpIndex = 0; + bool nextDumpScreenshot = false; FrameDumpWriter dumpWriter; template @@ -678,6 +689,11 @@ struct VulkanDevice::State { require(slot.dumpBuffer != nullptr, "frame dump was not recorded"); const auto full = slot.dumpBuffer->Bytes(); const auto index = slot.dumpIndex; + if (slot.dumpScreenshot) { + require(full.size() == static_cast(slot.dumpWidth) * slot.dumpHeight * 4, "invalid screenshot pixels"); + dumpWriter.Enqueue(index, slot.dumpWidth, slot.dumpHeight, 1, true, std::vector(full.begin(), full.end())); + return; + } const auto scale = slot.dumpScale; require(scale != 0 && full.size() == static_cast(slot.dumpWidth) * slot.dumpHeight * 4, "invalid frame dump pixels"); const auto width = (slot.dumpWidth + scale - 1) / scale; @@ -688,7 +704,7 @@ struct VulkanDevice::State { } static const auto start = std::chrono::steady_clock::now(); std::fprintf(stderr, "[gpu] frame_%03d.bmp: %ux%u display read back on the GPU at %.1f s\n", index, slot.dumpWidth, slot.dumpHeight, std::chrono::duration(std::chrono::steady_clock::now() - start).count()); - dumpWriter.Enqueue(index, width, height, 1, std::move(pixels)); + dumpWriter.Enqueue(index, width, height, 1, false, std::move(pixels)); } // Blocks on the fence, or polls it every 50 us with APS5_PRESENT_POLL_FENCE=1 (the difference @@ -3062,6 +3078,13 @@ std::optional> CompressedClearPixel(const DisplayBuffer return DisplayBufferClearPixel(buffer, keys); } +std::vector DisplayPixels(const DisplayBuffer& buffer, const std::optional>& cleared) { + if (!cleared) return ReadDisplayBuffer(buffer); + std::vector pixels(static_cast(buffer.width) * buffer.height * cleared->size()); + for (std::size_t offset = 0; offset < pixels.size(); offset += cleared->size()) std::memcpy(pixels.data() + offset, cleared->data(), cleared->size()); + return pixels; +} + // Debug aid: APS5_NO_RESIDENT_PRESENT=1 always presents through guest memory. bool NoResidentPresent() { static const bool no = std::getenv("APS5_NO_RESIDENT_PRESENT") != nullptr; @@ -3113,10 +3136,14 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) { dumpFrame = true; } } - if (!present(buffer.width, buffer.height, true, pixels, nullptr, nullptr, VK_FILTER_LINEAR, dumpFrame, false, nullptr, 0, passthrough)) { + const bool screenshot = takeScreenshot(dumpFrame); + auto shot = screenshot && passthrough ? ReadDisplayBuffer(buffer) : std::vector{}; + if (!present(buffer.width, buffer.height, true, pixels, nullptr, nullptr, VK_FILTER_LINEAR, dumpFrame || (screenshot && !passthrough), false, nullptr, 0, passthrough)) { if (dumpFrame) --dumps.dumped; + if (screenshot) Screenshot::Request(); return false; } + if (!shot.empty()) state->dumpWriter.Enqueue(0, buffer.width, buffer.height, 1, true, std::move(shot)); return true; } static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; @@ -3162,9 +3189,7 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) { bool dumpFrame = false; if (frameDumpOutput.Enabled_nid_no_patch() && dumps.dumped < dumps.limit && ++dumps.presents % static_cast(dumps.every) == 0) { if (dumps.cpu || (passthrough && resident == nullptr)) { - auto full = cleared ? std::vector(static_cast(buffer.width) * buffer.height * cleared->size()) : ReadDisplayBuffer(buffer); - for (std::size_t offset = 0; cleared && offset < full.size(); offset += cleared->size()) std::memcpy(full.data() + offset, cleared->data(), cleared->size()); - WriteFrameBmp(dumps.dumped++, buffer.width, buffer.height, full, DumpScale()); + WriteFrameBmp(dumps.dumped++, buffer.width, buffer.height, DisplayPixels(buffer, cleared), DumpScale()); } else { state->nextDumpIndex = dumps.dumped++; dumpFrame = true; @@ -3175,6 +3200,9 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) { lastReport = std::chrono::steady_clock::now(); AgcDriver::ProfilePrint_nid_no_patch("[flip] (cumulative): %llu presents from the resident image (%llu refreshed first), through guest memory: %llu not pending, %llu unsuitable; %llu GPU frame dumps\n", static_cast(residentPresents), static_cast(refreshedPresents), static_cast(notPending), static_cast(unsuitable), static_cast(gpuDumps)); } + const bool screenshot = takeScreenshot(dumpFrame); + const bool cpuScreenshot = screenshot && passthrough && resident == nullptr; + auto shot = cpuScreenshot ? DisplayPixels(buffer, cleared) : std::vector{}; CaptureTrace::Log("present dump=%d address=%llx width=%u height=%u resident=%d generation=%llu", dumpFrame ? state->nextDumpIndex : -1, static_cast(buffer.address), buffer.width, buffer.height, resident != nullptr, static_cast(resident ? resident->Generation() : 0)); if (passthrough && resident == nullptr) ensureHdrScaler(DisplayTexelFormat(buffer.pixelFormat)); VkClearColorValue uniform{}; @@ -3184,14 +3212,21 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) { const auto channel = [&](std::size_t index) { return static_cast(std::to_integer((*cleared)[index])) / 255.0f; }; uniform = {{channel(2), channel(1), channel(0), channel(3)}}; } - if (!present(buffer.width, buffer.height, true, {}, cleared ? nullptr : &buffer, resident, filter, dumpFrame, convert, cleared ? &uniform : nullptr, layer, passthrough)) { + if (!present(buffer.width, buffer.height, true, {}, cleared ? nullptr : &buffer, resident, filter, dumpFrame || (screenshot && !cpuScreenshot), convert, cleared ? &uniform : nullptr, layer, passthrough)) { // A dropped frame (swapchain out of date) keeps the dump numbering contiguous. if (dumpFrame) --dumps.dumped; + if (screenshot) Screenshot::Request(); return false; } + if (cpuScreenshot) state->dumpWriter.Enqueue(0, buffer.width, buffer.height, 1, true, std::move(shot)); return true; } +bool VulkanDevice::takeScreenshot(bool dumpFrame) { + state->nextDumpScreenshot = !dumpFrame && state->dumpWriter.Idle() && Screenshot::Take(); + return state->nextDumpScreenshot; +} + bool VulkanDevice::AcquireImage() { PerformanceTimer timing("Vulkan.Acquire"); require(state->swapchain != VK_NULL_HANDLE, "device has no swapchain"); @@ -3365,7 +3400,7 @@ bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaqu else scaler->RecordBlit(commands, barrier.image, state->extent.width, state->extent.height); } if (dumpFrame) { - const auto scale = DumpScale(); + const auto scale = state->nextDumpScreenshot ? 1 : DumpScale(); if (drawn || direct) { const auto extent = PresentationPass::DumpExtent(width, height, scale); slot.dumpWidth = extent.width; @@ -3433,6 +3468,7 @@ bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaqu slot.shared = shared; // Only a submitted readback is written after the fence (a failed submit tears the device down). slot.dumpRecorded = dumpFrame; + slot.dumpScreenshot = dumpFrame && state->nextDumpScreenshot; slot.dumpIndex = state->nextDumpIndex; slot.recorderSerial = batchesAtBlit; slot.previousSerial = state->lastPresentSerial; diff --git a/core/libs/prx/libSceAgcDriver/tests/Screenshot.cpp b/core/libs/prx/libSceAgcDriver/tests/Screenshot.cpp new file mode 100644 index 0000000000..e6202ceb1a --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/Screenshot.cpp @@ -0,0 +1,88 @@ +#include "prx/libSceAgcDriver/Execution/include/Screenshot.hpp" +#include "Decoder/Png.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +void setScreenshotDirectory(const std::filesystem::path& directory) { +#ifdef _WIN32 + require(_putenv_s("ANYPS5_SCREENSHOTS", directory.string().c_str()) == 0, "set ANYPS5_SCREENSHOTS"); +#else + require(::setenv("ANYPS5_SCREENSHOTS", directory.string().c_str(), 1) == 0, "set ANYPS5_SCREENSHOTS"); +#endif +} + +std::vector readFile(const std::filesystem::path& path) { + std::ifstream file(path, std::ios::binary); + require(file.good(), "open the written screenshot"); + return {std::istreambuf_iterator(file), std::istreambuf_iterator()}; +} + +void requestsMerge() { + require(!AgcDriver::Screenshot::Take(), "no screenshot is requested at start"); + AgcDriver::Screenshot::Request(); + AgcDriver::Screenshot::Request(); + require(AgcDriver::Screenshot::Take(), "a requested screenshot is taken"); + require(!AgcDriver::Screenshot::Take(), "repeated requests are taken once"); +} + +void writesRgbPng(const std::filesystem::path& directory) { + const std::array bgra{std::byte{0x10}, std::byte{0x20}, std::byte{0x30}, std::byte{0x00}, std::byte{0xA0}, std::byte{0xB0}, std::byte{0xC0}, std::byte{0x7F}}; + const auto first = AgcDriver::Screenshot::Write("TEST00001", 2, 1, bgra); + const auto second = AgcDriver::Screenshot::Write("TEST00001", 2, 1, bgra); + require(first.parent_path() == directory, "the screenshot is written to ANYPS5_SCREENSHOTS"); + require(first.extension() == ".png" && first.filename().string().starts_with("TEST00001-"), "the screenshot is named after the title ID"); + require(first != second && std::filesystem::exists(second), "a second screenshot does not replace the first"); + const auto image = Decoder::Png::Decode(readFile(first)); + require(image.has_value() && image->width == 2 && image->height == 1, "the screenshot decodes at the frame extent"); + const std::vector expected{0x30, 0x20, 0x10, 0xFF, 0xC0, 0xB0, 0xA0, 0xFF}; + require(image->pixels == expected, "the screenshot holds the frame's colors as opaque RGB"); +} + +void rejectsInvalidFrames() { + const std::array pixel{}; + const auto rejects = [](auto write) { + try { + write(); + } catch (const std::invalid_argument&) { + return true; + } + return false; + }; + require(rejects([&] { AgcDriver::Screenshot::Write("TEST00001", 2, 1, pixel); }), "a frame smaller than its extent is rejected"); + require(rejects([&] { AgcDriver::Screenshot::Write("TEST00001", 0, 1, {}); }), "an empty frame is rejected"); + require(rejects([&] { AgcDriver::Screenshot::Write("", 1, 1, pixel); }), "an empty title ID is rejected"); +} + +} + +int main() try { + const auto root = std::filesystem::temp_directory_path() / ("anyps5_screenshot-" + std::to_string(std::random_device{}())); + const auto directory = root / "nested"; + std::filesystem::remove_all(root); + setScreenshotDirectory(directory); + requestsMerge(); + writesRgbPng(directory); + rejectsInvalidFrames(); + std::filesystem::remove_all(root); + std::cout << "agc screenshot tests passed\n"; + return 0; +} catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; +} diff --git a/core/libs/prx/libScePad/include/InputMapping.hpp b/core/libs/prx/libScePad/include/InputMapping.hpp index fe2d7a0339..ec4da4ab47 100644 --- a/core/libs/prx/libScePad/include/InputMapping.hpp +++ b/core/libs/prx/libScePad/include/InputMapping.hpp @@ -39,6 +39,7 @@ struct InputConfiguration { inline constexpr std::array InputMapping{ InputBinding{SDL_SCANCODE_F11, MouseButton::None, InputControl::ToggleFullscreen}, InputBinding{SDL_SCANCODE_F10, MouseButton::None, InputControl::TogglePerformanceOverlay}, + InputBinding{SDL_SCANCODE_F12, MouseButton::None, InputControl::Screenshot}, InputBinding{SDL_SCANCODE_RETURN, MouseButton::None, InputControl::Button, PadButton::Cross}, InputBinding{SDL_SCANCODE_SPACE, MouseButton::None, InputControl::Button, PadButton::Cross}, InputBinding{SDL_SCANCODE_ESCAPE, MouseButton::None, InputControl::Button, PadButton::Options}, diff --git a/core/libs/prx/libScePad/include/KeyboardMouse.hpp b/core/libs/prx/libScePad/include/KeyboardMouse.hpp index dbe86e49e1..14002488e1 100644 --- a/core/libs/prx/libScePad/include/KeyboardMouse.hpp +++ b/core/libs/prx/libScePad/include/KeyboardMouse.hpp @@ -54,6 +54,7 @@ private: struct InputRequests { bool toggleFullscreen = false; bool togglePerformanceOverlay = false; + bool screenshot = false; bool toggleMouse = false; bool releaseMouse = false; }; diff --git a/core/libs/prx/libScePad/include/PadInputTypes.hpp b/core/libs/prx/libScePad/include/PadInputTypes.hpp index 891d3eb710..f8e544e092 100644 --- a/core/libs/prx/libScePad/include/PadInputTypes.hpp +++ b/core/libs/prx/libScePad/include/PadInputTypes.hpp @@ -23,7 +23,8 @@ enum class InputControl { Shake, ToggleMouse, ToggleFullscreen, - TogglePerformanceOverlay + TogglePerformanceOverlay, + Screenshot }; enum class PadButton : std::uint32_t { diff --git a/core/libs/prx/libSceVideoOut/src/InputMapping.cpp b/core/libs/prx/libSceVideoOut/src/InputMapping.cpp index e45f4593b1..dc9bb16da1 100644 --- a/core/libs/prx/libSceVideoOut/src/InputMapping.cpp +++ b/core/libs/prx/libSceVideoOut/src/InputMapping.cpp @@ -55,7 +55,8 @@ constexpr auto actions = std::array{ InputAction{"Shake", Pad::InputControl::Shake, Pad::PadButton::None}, InputAction{"ToggleMouse", Pad::InputControl::ToggleMouse, Pad::PadButton::None}, InputAction{"ToggleFullscreen", Pad::InputControl::ToggleFullscreen, Pad::PadButton::None}, - InputAction{"TogglePerformanceOverlay", Pad::InputControl::TogglePerformanceOverlay, Pad::PadButton::None} + InputAction{"TogglePerformanceOverlay", Pad::InputControl::TogglePerformanceOverlay, Pad::PadButton::None}, + InputAction{"Screenshot", Pad::InputControl::Screenshot, Pad::PadButton::None} }; std::string upper(std::string_view value) { @@ -117,6 +118,9 @@ Pad::InputBinding parseBinding(const InputAction& action, std::string_view sourc if (action.control == Pad::InputControl::TogglePerformanceOverlay) { throw std::runtime_error("TogglePerformanceOverlay can only use a keyboard key"); } + if (action.control == Pad::InputControl::Screenshot) { + throw std::runtime_error("Screenshot can only use a keyboard key"); + } return {SDL_SCANCODE_UNKNOWN, parseMouseButton(value), action.control, action.button, 0}; } if (type == "WHEEL") { diff --git a/core/libs/prx/libSceVideoOut/src/KeyboardMouse.cpp b/core/libs/prx/libSceVideoOut/src/KeyboardMouse.cpp index ed1e1de32d..1ef93610d0 100644 --- a/core/libs/prx/libSceVideoOut/src/KeyboardMouse.cpp +++ b/core/libs/prx/libSceVideoOut/src/KeyboardMouse.cpp @@ -263,6 +263,7 @@ Pad::InputRequests Pad::KeyboardMouse::HandleEvent(const SDL_Event& event, Clock if (!matches) continue; if (binding.control == InputControl::ToggleFullscreen && keyboard && down && !pressed[index]) requests.toggleFullscreen = true; if (binding.control == InputControl::TogglePerformanceOverlay && keyboard && down && !pressed[index]) requests.togglePerformanceOverlay = true; + if (binding.control == InputControl::Screenshot && keyboard && down && !pressed[index]) requests.screenshot = true; if (binding.control == InputControl::ToggleMouse && down && !pressed[index]) requests.toggleMouse = true; pressed[index] = down; } @@ -302,6 +303,7 @@ PadInputState Pad::KeyboardMouse::Compose(const PadInputState& controller) const case InputControl::ToggleMouse: break; case InputControl::ToggleFullscreen: break; case InputControl::TogglePerformanceOverlay: break; + case InputControl::Screenshot: break; } } const auto input = stickInput(); diff --git a/core/libs/prx/libSceVideoOut/src/PadInput.cpp b/core/libs/prx/libSceVideoOut/src/PadInput.cpp index a48381cf06..a11513d02c 100644 --- a/core/libs/prx/libSceVideoOut/src/PadInput.cpp +++ b/core/libs/prx/libSceVideoOut/src/PadInput.cpp @@ -12,6 +12,7 @@ #include "prx/libSceMouse/include/MouseState.hpp" #include "prx/libSceVideoOut/include/DisplayWindow.hpp" #include "prx/libSceAgcDriver/Execution/include/Presentation.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" #include "prx/libScePad/include/PadState.hpp" #include "prx/libScePad/include/PadInputTypes.hpp" #include "prx/libc/include/General.hpp" @@ -230,6 +231,7 @@ void PadInput::HandleEvent(const SDL_Event& event, DisplayWindow& window) { const auto requests = keyboardMouse.HandleEvent(event, std::chrono::steady_clock::now()); if (requests.toggleFullscreen && window.Handle() != nullptr && event.key.windowID == SDL_GetWindowID(window.Handle())) window.ToggleFullscreen(); if (requests.togglePerformanceOverlay && window.Handle() != nullptr && event.key.windowID == SDL_GetWindowID(window.Handle())) AgcDriverTogglePerformanceOverlay_nid_postfix(); + if (requests.screenshot && window.Handle() != nullptr && event.key.windowID == SDL_GetWindowID(window.Handle())) AgcDriverRequestScreenshot_nid_postfix(); if (requests.toggleMouse) setMouseMode(!keyboardMouse.MouseMode()); if (event.type != SDL_MOUSEMOTION) publish(window.FrameNumber()); } diff --git a/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp b/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp index ce7f276384..3497443fcc 100644 --- a/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp +++ b/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp @@ -92,6 +92,24 @@ void performanceOverlayBinding() { require(rejects("TogglePerformanceOverlay = MOUSE:Left\n"), "the performance overlay toggle refuses a mouse button"); } +void screenshotBinding() { + useConfig("Cross = KEY:F\n"); + bool onF12 = false; + for (const auto& binding : Pad::LoadInputMapping().bindings) onF12 = onF12 || (binding.control == Pad::InputControl::Screenshot && binding.key == SDL_SCANCODE_F12); + require(onF12, "F12 takes a screenshot by default"); + useConfig("Screenshot = KEY:F9\n"); + bool onF9 = false; + bool stillOnF12 = false; + for (const auto& binding : Pad::LoadInputMapping().bindings) { + if (binding.control != Pad::InputControl::Screenshot) continue; + onF9 = onF9 || binding.key == SDL_SCANCODE_F9; + stillOnF12 = stillOnF12 || binding.key == SDL_SCANCODE_F12; + } + require(onF9 && !stillOnF12, "the screenshot key moves to another key"); + require(rejects("Screenshot = MOUSE:Left\n"), "the screenshot key refuses a mouse button"); + require(rejects("Screenshot = WHEEL:Up\n"), "the screenshot key refuses the mouse wheel"); +} + void mouseLookDefault() { useConfig("Cross = KEY:F\n"); const auto settings = Pad::LoadInputMapping().settings; @@ -262,6 +280,7 @@ int main() try { settingsParse(); settingsReject(); performanceOverlayBinding(); + screenshotBinding(); mouseLookDefault(); mouseLookParse(); mouseLookReject(); diff --git a/core/libs/prx/libSceVideoOut/tests/KeyboardMouse.cpp b/core/libs/prx/libSceVideoOut/tests/KeyboardMouse.cpp index eb763d6a25..e4d9c6892f 100644 --- a/core/libs/prx/libSceVideoOut/tests/KeyboardMouse.cpp +++ b/core/libs/prx/libSceVideoOut/tests/KeyboardMouse.cpp @@ -464,6 +464,12 @@ void testToggleRequests() { const auto overlay = keyboardMouse.HandleEvent(KeyEvent(SDL_SCANCODE_F10, true), now); check(overlay.togglePerformanceOverlay && !overlay.toggleFullscreen, "F10 asks to toggle the performance overlay"); check(!keyboardMouse.HandleEvent(KeyEvent(SDL_SCANCODE_F10, false), now).togglePerformanceOverlay, "releasing F10 asks for nothing"); + const auto screenshot = keyboardMouse.HandleEvent(KeyEvent(SDL_SCANCODE_F12, true), now); + check(screenshot.screenshot && !screenshot.toggleFullscreen && !screenshot.togglePerformanceOverlay, "F12 asks for a screenshot"); + auto repeated = KeyEvent(SDL_SCANCODE_F12, true); + repeated.key.repeat = 1; + check(!keyboardMouse.HandleEvent(repeated, now).screenshot, "a repeated F12 asks for nothing"); + check(!keyboardMouse.HandleEvent(KeyEvent(SDL_SCANCODE_F12, false), now).screenshot, "releasing F12 asks for nothing"); } void testDefaultL2() { diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index ec41501797..e37c808be2 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -73,6 +73,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `ANYPS5_REQUIRE_DESCRIPTOR_BINDING_FLAGS` | presence | `core/libs/prx/libSceAgcDriver/tests/ComputePipelineReplay.cpp` | | fails the replay test instead of accepting a device that refuses descriptor binding flags | | `ANYPS5_REQUIRE_ROBUST2` | presence | `core/libs/prx/libSceAgcDriver/tests/execution/LdsMixedWidthAtomics.cpp` | | fails the LDS lock-budget test when the device does not enable robustBufferAccess2, instead of only printing it | | `ANYPS5_REQUIRE_VULKAN` | presence | `core/libs/prx/libSceAgcDriver/tests/BdaDevice.cpp` | | fails a test instead of skipping when no usable Vulkan device is present | +| `ANYPS5_SCREENSHOTS` | string | `core/libs/prx/libSceAgcDriver/Execution/src/Screenshot.cpp` | | directory the screenshot key saves its PNG files to, defaulting to anyps5-screenshots beside the executable | | `APS5_ADDRESS_COPY_MAX_MIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | 64 | upper bound in MiB on the guest memory one address-based build copies; exceeding it aborts | | `APS5_ALL_BUFFERS_WRITTEN` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | notes every shader buffer element as written, as before bufferWritten existed | | `APS5_ASSERT_GPU_LOCK` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp` | | aborts when a guarded path is reached without holding GuestMemory::GpuMutex | diff --git a/docs/user/INPUT_MAPPING.md b/docs/user/INPUT_MAPPING.md index 74221f95ab..79d8f05b53 100644 --- a/docs/user/INPUT_MAPPING.md +++ b/docs/user/INPUT_MAPPING.md @@ -16,7 +16,7 @@ While a title has a mouse open (`sceMouseOpen`) and the window is focused, AnyPS Game-controller bindings are enabled automatically and are not affected by keyboard or mouse overrides in this file. -Supported actions are `Cross`, `Circle`, `Triangle`, `Square`, `L1`, `R1`, `L2`, `R2`, `L3`, `R3`, `Options`, `Up`, `Right`, `Down`, `Left`, `LeftStickLeft`, `LeftStickRight`, `LeftStickUp`, `LeftStickDown`, `RightStickLeft`, `RightStickRight`, `RightStickUp`, `RightStickDown`, `TouchLeft`, `TouchRight`, `StickModifier`, `Shake`, `ToggleMouse`, `ToggleFullscreen` (F11), and `TogglePerformanceOverlay` (F10, the [performance overlay](USAGE.md#performance-overlay)). `ToggleFullscreen` and `TogglePerformanceOverlay` accept only keyboard keys. +Supported actions are `Cross`, `Circle`, `Triangle`, `Square`, `L1`, `R1`, `L2`, `R2`, `L3`, `R3`, `Options`, `Up`, `Right`, `Down`, `Left`, `LeftStickLeft`, `LeftStickRight`, `LeftStickUp`, `LeftStickDown`, `RightStickLeft`, `RightStickRight`, `RightStickUp`, `RightStickDown`, `TouchLeft`, `TouchRight`, `StickModifier`, `Shake`, `ToggleMouse`, `ToggleFullscreen` (F11), `TogglePerformanceOverlay` (F10, the [performance overlay](USAGE.md#performance-overlay)), and `Screenshot` (F12, saves the next displayed frame as described in [screenshots](USAGE.md#screenshots)). `ToggleFullscreen`, `TogglePerformanceOverlay` and `Screenshot` accept only keyboard keys. ### Default bindings @@ -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 | +| `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: diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 8cfd7a1f84..89f0498c29 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -255,6 +255,10 @@ When a dump has neither `sce_sys/playgo-chunk.dat` nor `sce_sys/playgo-scenario. Leave the variable unset to keep the explicit error. Any other value also stops with an error. Dumps that include PlayGo metadata are unaffected. These title-specific results come from emulator runs and have not been verified against a console. +### Screenshots + +Press F12 in the game window to save the next displayed frame as a PNG at the resolution of the game's display buffer, without the window's scaling, letterbox or [performance overlay](#performance-overlay). A BT.2100 PQ display buffer is saved converted to SDR, also while it is shown through [HDR output](#hdr-output). Files are named `-<YYYYMMDD-HHMMSS>.png` and written to an `anyps5-screenshots/` directory beside the output executable, which is created when needed; set `ANYPS5_SCREENSHOTS` to use another directory. The saved path is printed to stderr. Presses while the previous screenshot is still being written are merged into one. The key is the `Screenshot` action of the [input mapping](INPUT_MAPPING.md). A screenshot that cannot be written stops the game with the error. + ### GPU selection The game runs on the first Vulkan 1.1 device with graphics and compute queues and swapchain presentation, preferring a discrete GPU over an integrated one. Set `ANYPS5_GPU` to a part of a device name, compared without regard to case, to run on another device; the names are printed at start-up in the `Physical device candidate` lines. When no usable device contains the text, the start fails and the error lists the device names. From 972fb8498872f3f1cb6df596eaafc53571d18b04 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Tue, 20 Oct 2026 04:21:45 +0200 Subject: [PATCH 230/269] fix(libc): fail chdir, remove, opendir and fopen through a regular file with ENOTDIR on Windows A path whose directory part is a regular file, such as file/child, failed with ENOENT in chdir, remove, opendir, fopen and freopen on Windows, where FreeBSD 11.4 fails it with ENOTDIR (vfs_cache_lookup, sys/kern/vfs_cache.c). The not-found error of these calls now asks whether the nearest existing ancestor is not a directory, as open, stat, lstat, access and unlink do. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --- core/libs/prx/libc/src/Directory.cpp | 6 +++++- core/libs/prx/libc/src/FileAndHeap.cpp | 4 +++- core/libs/prx/libc/src/Filesystem.cpp | 1 + core/libs/prx/libc/src/General.cpp | 3 ++- core/libs/tests/GuestFilesystem.cpp | 6 ++++++ 5 files changed, 17 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libc/src/Directory.cpp b/core/libs/prx/libc/src/Directory.cpp index 6746b399f9..2ea8cc0946 100644 --- a/core/libs/prx/libc/src/Directory.cpp +++ b/core/libs/prx/libc/src/Directory.cpp @@ -117,7 +117,11 @@ void* APS5_VABI opendir_nid_postfix(const char* path) { if (const int error = ResolvePathError_nid_no_patch(path, directory->path)) { errno = error; return nullptr; } AppArchiveRefuseMergedListing_nid_no_patch(directory->path, path); directory->native = OpenNative(directory->path); - if (!directory->native) { errno = DirectoryError(errno); return nullptr; } + if (!directory->native) { + errno = DirectoryError(errno); + if (errno == 2) errno = NotFoundError_nid_no_patch(directory->path); + return nullptr; + } return directory.release(); } catch (const std::bad_alloc&) { errno = 12; return nullptr; } catch (const std::filesystem::filesystem_error&) { errno = 5; return nullptr; } diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index f81ef3fc5e..4b8f5328ba 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -337,7 +337,8 @@ FileStream* APS5_VABI freopen_nid_postfix(const char* filename, const char* mode if (!path.empty() && parsed->writes) RecordWrittenPath_nid_no_patch(path); return stream; } - const int error = OpenFailure(path, parsed->writes, errno); + int error = OpenFailure(path, parsed->writes, errno); + if (error == 2) error = NotFoundError_nid_no_patch(path); if (stream->IsDynamic()) delete stream; errno = error; return nullptr; @@ -370,6 +371,7 @@ FileStream* APS5_VABI fopen_nid_postfix(const char* filename, const char* mode) std::unique_ptr<std::FILE, int (*)(std::FILE*)> handle(OpenNative(fpath, parsed->native), std::fclose); if (!handle) { errno = OpenFailure(fpath, parsed->writes, errno); + if (errno == 2) errno = NotFoundError_nid_no_patch(fpath); return nullptr; } // APS5_LOG_OUT("success: \"%s\"", abs_path.c_str()); diff --git a/core/libs/prx/libc/src/Filesystem.cpp b/core/libs/prx/libc/src/Filesystem.cpp index fa4431ad0f..510477eb7a 100644 --- a/core/libs/prx/libc/src/Filesystem.cpp +++ b/core/libs/prx/libc/src/Filesystem.cpp @@ -111,6 +111,7 @@ extern "C" int APS5_VABI remove_nid_postfix(const char* path) { #endif if (error || !removed) { errno = error ? FilesystemError(error) : 2; + if (errno == 2) errno = NotFoundError_nid_no_patch(resolved); return -1; } RecordWrittenPath_nid_no_patch(resolved); diff --git a/core/libs/prx/libc/src/General.cpp b/core/libs/prx/libc/src/General.cpp index d9a6eed039..7b86b2c269 100644 --- a/core/libs/prx/libc/src/General.cpp +++ b/core/libs/prx/libc/src/General.cpp @@ -31,6 +31,7 @@ #include <unistd.h> #endif #include "prx/libc/include/General.hpp" +#include "prx/libc/include/FilesystemError.hpp" #include "prx/libc/include/AppArchive.hpp" #include "prx/libc/include/ApplicationHeap.hpp" @@ -598,7 +599,7 @@ extern "C" int APS5_VABI chdir_nid_postfix(const char* path) { const auto resolved = Resolve(state, path, false, true); AppArchiveRefuseOnly_nid_no_patch(resolved, path, "changing the working directory to"); const int error = SetWorkingDirectory(state, resolved); - if (error) { errno = error; return -1; } + if (error) { errno = error == 2 ? NotFoundError_nid_no_patch(resolved) : error; return -1; } return 0; } catch (const std::bad_alloc&) { errno = 12; return -1; } catch (const std::filesystem::filesystem_error& error) { errno = DirectoryFailure(error.code()); return -1; } diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index fd2bcbc681..7ad1e67c4b 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -1160,6 +1160,12 @@ int main() { Require(sceKernelUtimes_nid_postfix(blocked->c_str(), nullptr) == static_cast<int>(0x80020000u | static_cast<unsigned>(expected))); Require(rename_nid_postfix(blocked->c_str(), (root / "renamed-target").string().c_str()) == -1 && *__error_nid_postfix() == expected); if (blocked != &missingChild) Require(rename_nid_postfix(presentName.c_str(), blocked->c_str()) == -1 && *__error_nid_postfix() == expected); + Require(remove_nid_postfix(blocked->c_str()) == -1 && *__error_nid_postfix() == expected); + Require(opendir_nid_postfix(blocked->c_str()) == nullptr && *__error_nid_postfix() == expected); + Require(chdir_nid_postfix(blocked->c_str()) == -1 && *__error_nid_postfix() == expected); + for (const char* openMode : {"r", "w"}) { + Require(fopen_nid_postfix(blocked->c_str(), openMode) == nullptr && *__error_nid_postfix() == expected); + } } #ifndef _WIN32 const auto linkedDirectory = root / "linked-directory"; From 56ab8dcf7b93b99957c9ff5ba62006febc0bb133 Mon Sep 17 00:00:00 2001 From: maximaxi01 <dev@hotlinemessina.local> Date: Mon, 19 Oct 2026 00:55:51 -0400 Subject: [PATCH 231/269] fix(tools): prevent configure seeds from overwriting their cache --- tools/configure_seed.py | 9 ++-- tools/tests/test_configure_seed.py | 81 ++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 3 deletions(-) diff --git a/tools/configure_seed.py b/tools/configure_seed.py index 76dcf3527b..465ad3b15d 100644 --- a/tools/configure_seed.py +++ b/tools/configure_seed.py @@ -34,11 +34,14 @@ def main(): parser.add_argument("--exclude", action="append", default=[], metavar="GLOB", help="omit matching check names after inclusion; patterns are case-sensitive") args = parser.parse_args() - found = {name: entry for name, entry in checks(args.cache).items() + cache, seed = Path(args.cache), Path(args.seed) + if cache.resolve() == seed.resolve() or (cache.exists() and seed.exists() and cache.samefile(seed)): + parser.error("seed output would overwrite the input cache") + found = {name: entry for name, entry in checks(cache).items() if (not args.include or any(fnmatch.fnmatchcase(name, pattern) for pattern in args.include)) and not any(fnmatch.fnmatchcase(name, pattern) for pattern in args.exclude)} - Path(args.seed).write_text("".join(f"set({quote(name)} {quote(value)} CACHE INTERNAL {quote(doc)})\n" - for name, (value, doc) in sorted(found.items())), encoding="utf-8") + seed.write_text("".join(f"set({quote(name)} {quote(value)} CACHE INTERNAL {quote(doc)})\n" + for name, (value, doc) in sorted(found.items())), encoding="utf-8") print(f"{len(found)} check results") return 0 diff --git a/tools/tests/test_configure_seed.py b/tools/tests/test_configure_seed.py index 486b7340b3..c1d13c4c89 100644 --- a/tools/tests/test_configure_seed.py +++ b/tools/tests/test_configure_seed.py @@ -1,3 +1,4 @@ +import os import shutil import subprocess import sys @@ -24,6 +25,86 @@ set(MINI_OPTION "value" CACHE STRING "An option, not a check result") """ +class ConfigureSeedPathTests(unittest.TestCase): + def setUp(self): + self.directory = tempfile.TemporaryDirectory() + self.addCleanup(self.directory.cleanup) + self.root = Path(self.directory.name) + self.cache = self.root / "CMakeCache.txt" + self.original = b"//Path to a program.\nCMAKE_AR:FILEPATH=/usr/bin/ar\n//Test HAVE_FEATURE\nHAVE_FEATURE:INTERNAL=1\n" + self.cache.write_bytes(self.original) + + def run_seed(self, seed, *options): + return subprocess.run([sys.executable, str(TOOL), str(self.cache), str(seed), *options], + capture_output=True, text=True) + + def assert_rejected(self, seed, *options): + result = self.run_seed(seed, *options) + self.assertEqual(result.returncode, 2, result.stderr) + self.assertEqual(result.stdout, "") + self.assertIn("seed output would overwrite the input cache", result.stderr) + self.assertNotIn("Traceback", result.stderr) + self.assertEqual(self.cache.read_bytes(), self.original) + self.assertEqual(Path(seed).read_bytes(), self.original) + + def test_same_path_preserves_cache(self): + self.assert_rejected(self.cache) + + def test_normalized_path_preserves_cache(self): + (self.root / "nested").mkdir() + self.assert_rejected(self.root / "nested" / ".." / self.cache.name) + + def test_hard_link_preserves_cache(self): + seed = self.root / "seed.cmake" + os.link(self.cache, seed) + self.assert_rejected(seed) + + def test_symbolic_link_preserves_cache(self): + seed = self.root / "seed.cmake" + try: + seed.symlink_to(self.cache) + except OSError as error: + self.skipTest(f"symbolic links unavailable: {error}") + self.assert_rejected(seed) + + def test_distinct_output_preserves_cache(self): + seed = self.root / "seed.cmake" + for existing in (False, True): + with self.subTest(existing=existing): + if existing: + seed.write_text("old seed", encoding="utf-8") + result = self.run_seed(seed) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stdout, "1 check results\n") + self.assertEqual(seed.read_text(encoding="utf-8"), + 'set("HAVE_FEATURE" "1" CACHE INTERNAL "Test HAVE_FEATURE")\n') + self.assertEqual(self.cache.read_bytes(), self.original) + + def test_filters_do_not_allow_overwriting_cache(self): + for options in (("--include", "HAVE_*"), ("--include", "NO_MATCH_*"), ("--exclude", "HAVE_*")): + with self.subTest(options=options): + self.assert_rejected(self.cache, *options) + + def test_filters_preserve_cache_with_distinct_output(self): + self.original += b"//Test OTHER_CHECK\nOTHER_CHECK:INTERNAL=0\n" + self.cache.write_bytes(self.original) + seed = self.root / "seed.cmake" + feature = 'set("HAVE_FEATURE" "1" CACHE INTERNAL "Test HAVE_FEATURE")\n' + other = 'set("OTHER_CHECK" "0" CACHE INTERNAL "Test OTHER_CHECK")\n' + cases = ( + (("--include", "HAVE_*", "--include", "OTHER_*"), feature + other, 2), + (("--include", "*", "--exclude", "OTHER_*"), feature, 1), + (("--include", "have_*"), "", 0), + ) + for options, expected, count in cases: + with self.subTest(options=options): + result = self.run_seed(seed, *options) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stdout, f"{count} check results\n") + self.assertEqual(seed.read_text(encoding="utf-8"), expected) + self.assertEqual(self.cache.read_bytes(), self.original) + + @unittest.skipUnless(shutil.which("ninja") and (shutil.which("gcc") or shutil.which("cc")), "needs ninja and a C compiler") class ConfigureSeedTests(unittest.TestCase): def test_check_names_with_spaces_round_trip_through_cmake(self): From 9c919b3aac289c705785fa96a0dd07699679aabb Mon Sep 17 00:00:00 2001 From: maximaxi01 <dev@hotlinemessina.local> Date: Mon, 19 Oct 2026 16:54:15 -0400 Subject: [PATCH 232/269] test(tools): split configure seed test groups --- CMakeLists.txt | 6 ++- tools/tests/test_configure_seed.py | 60 ++++++++++++++++-------------- 2 files changed, 37 insertions(+), 29 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 0ac1337ddb..18690f9566 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -187,7 +187,11 @@ if(BUILD_TESTING) add_test(NAME check_main_sync COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_main_sync.py) add_test(NAME configure_seed COMMAND ${Python3_EXECUTABLE} - ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_configure_seed.py) + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_configure_seed.py ConfigureSeedTests) + add_test(NAME configure_seed_paths COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_configure_seed.py ConfigureSeedPathTests) + add_test(NAME configure_seed_round_trips COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_configure_seed.py ConfigureSeedRoundTripTests) add_test(NAME stubgen COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_stubgen.py) add_test(NAME debt_split COMMAND ${Python3_EXECUTABLE} diff --git a/tools/tests/test_configure_seed.py b/tools/tests/test_configure_seed.py index c1d13c4c89..9c1b936b20 100644 --- a/tools/tests/test_configure_seed.py +++ b/tools/tests/test_configure_seed.py @@ -106,16 +106,7 @@ class ConfigureSeedPathTests(unittest.TestCase): @unittest.skipUnless(shutil.which("ninja") and (shutil.which("gcc") or shutil.which("cc")), "needs ninja and a C compiler") -class ConfigureSeedTests(unittest.TestCase): - def test_check_names_with_spaces_round_trip_through_cmake(self): - cache = self.root / "named-check.txt" - cache.write_text('//Test HAVE FEATURE\nHAVE FEATURE:INTERNAL=1\n', encoding="utf-8") - output = self.configure("named-check", "-C", str(self.seed(cache, "named-check.cmake"))) - self.assertNotRegex(output, r"CMake Error") - seeded = self.root / "named-check" / "CMakeCache.txt" - self.assertEqual(configure_seed.checks(seeded)["HAVE FEATURE"][0], "1") - self.assertNotIn("HAVE:INTERNAL=", seeded.read_text(encoding="utf-8")) - +class ConfigureSeedProject(unittest.TestCase): @classmethod def setUpClass(cls): cls.directory = tempfile.TemporaryDirectory() @@ -139,31 +130,14 @@ class ConfigureSeedTests(unittest.TestCase): subprocess.run([sys.executable, str(TOOL), str(cache), str(seed)], check=True, capture_output=True) return seed - def test_seeded_configure_runs_no_check_and_gets_the_same_results(self): - self.assertRegex(self.fresh, r"Looking for|Performing Test") - output = self.configure("seeded", "-C", str(self.seed(self.root / "fresh" / "CMakeCache.txt", "seed.cmake"))) - self.assertNotRegex(output, r"Looking for|Performing Test") - fresh = configure_seed.checks(self.root / "fresh" / "CMakeCache.txt") - self.assertEqual(configure_seed.checks(self.root / "seeded" / "CMakeCache.txt"), fresh) - self.assertEqual(fresh["MINI_HAVE_STDIO_H"][0], "1") - self.assertEqual(fresh["MINI_FAILS"][0], "") +class ConfigureSeedTests(ConfigureSeedProject): def test_only_check_results_are_seeded(self): found = configure_seed.checks(self.root / "fresh" / "CMakeCache.txt") self.assertEqual({n for n in found if n.startswith("MINI_")}, {"MINI_HAVE_STDIO_H", "MINI_HAVE_MISSING_H", "MINI_HAVE_PRINTF", "MINI_COMPILES", "MINI_FAILS"}) self.assertNotIn("CMAKE_C_COMPILER", found) - def test_values_with_cmake_syntax_round_trip(self): - cache = self.root / "odd" / "CMakeCache.txt" - cache.parent.mkdir() - value = r'a;b "c" \d ${e} $f' - cache.write_text(f"//Test MINI_ODD\nMINI_ODD:INTERNAL={value}\n", encoding="utf-8") - self.configure("odd-seeded", "-C", str(self.seed(cache, "odd.cmake"))) - seeded = (self.root / "odd-seeded" / "CMakeCache.txt").read_text(encoding="utf-8") - self.assertIn(f"MINI_ODD:INTERNAL={value}\n", seeded) - self.assertEqual(configure_seed.checks(self.root / "odd-seeded" / "CMakeCache.txt")["MINI_ODD"][0], value) - def test_cache_without_checks_writes_an_empty_seed(self): cache = self.root / "empty" / "CMakeCache.txt" cache.parent.mkdir() @@ -193,6 +167,36 @@ class ConfigureSeedTests(unittest.TestCase): self.assertEqual(seed.read_text(encoding="utf-8"), "") +class ConfigureSeedRoundTripTests(ConfigureSeedProject): + def test_check_names_with_spaces_round_trip_through_cmake(self): + cache = self.root / "named-check.txt" + cache.write_text('//Test HAVE FEATURE\nHAVE FEATURE:INTERNAL=1\n', encoding="utf-8") + output = self.configure("named-check", "-C", str(self.seed(cache, "named-check.cmake"))) + self.assertNotRegex(output, r"CMake Error") + seeded = self.root / "named-check" / "CMakeCache.txt" + self.assertEqual(configure_seed.checks(seeded)["HAVE FEATURE"][0], "1") + self.assertNotIn("HAVE:INTERNAL=", seeded.read_text(encoding="utf-8")) + + def test_seeded_configure_runs_no_check_and_gets_the_same_results(self): + self.assertRegex(self.fresh, r"Looking for|Performing Test") + output = self.configure("seeded", "-C", str(self.seed(self.root / "fresh" / "CMakeCache.txt", "seed.cmake"))) + self.assertNotRegex(output, r"Looking for|Performing Test") + fresh = configure_seed.checks(self.root / "fresh" / "CMakeCache.txt") + self.assertEqual(configure_seed.checks(self.root / "seeded" / "CMakeCache.txt"), fresh) + self.assertEqual(fresh["MINI_HAVE_STDIO_H"][0], "1") + self.assertEqual(fresh["MINI_FAILS"][0], "") + + def test_values_with_cmake_syntax_round_trip(self): + cache = self.root / "odd" / "CMakeCache.txt" + cache.parent.mkdir() + value = r'a;b "c" \d ${e} $f' + cache.write_text(f"//Test MINI_ODD\nMINI_ODD:INTERNAL={value}\n", encoding="utf-8") + self.configure("odd-seeded", "-C", str(self.seed(cache, "odd.cmake"))) + seeded = (self.root / "odd-seeded" / "CMakeCache.txt").read_text(encoding="utf-8") + self.assertIn(f"MINI_ODD:INTERNAL={value}\n", seeded) + self.assertEqual(configure_seed.checks(self.root / "odd-seeded" / "CMakeCache.txt")["MINI_ODD"][0], value) + + class ChecksParsingTests(unittest.TestCase): def test_checks_reads_cache_with_utf8_bom(self): From d96a010710e3fad87a82d949a3b42b00d2571212 Mon Sep 17 00:00:00 2001 From: maximaxi01 <dev@hotlinemessina.local> Date: Tue, 20 Oct 2026 02:00:46 -0400 Subject: [PATCH 233/269] test(tools): keep configure seed BOM checks registered --- CMakeLists.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 18690f9566..094bf25cf3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -192,6 +192,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_configure_seed.py ConfigureSeedPathTests) add_test(NAME configure_seed_round_trips COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_configure_seed.py ConfigureSeedRoundTripTests) + add_test(NAME configure_seed_parsing COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_configure_seed.py ChecksParsingTests) add_test(NAME stubgen COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_stubgen.py) add_test(NAME debt_split COMMAND ${Python3_EXECUTABLE} From a05db60c8713e308f4bdfd10a28e56e946a59759 Mon Sep 17 00:00:00 2001 From: Lindamea1 <322051987+Lindamea1@users.noreply.github.com> Date: Mon, 19 Oct 2026 14:21:33 +0100 Subject: [PATCH 234/269] fix(libSceNet): fill the address and port fields in sceNetGetSockInfo sceNetGetSockInfo returned 0 for a known socket and wrote nothing, so callers read uninitialised bytes as the socket's endpoints. Monster Hunter Wilds (PPSA24191) calls it after bind, connect and accept and stores the local and remote address and port it reads back. It now writes one 0xA0-byte record in the layout SharpProspero and shadPS4 give: the socket id, the socket type, the local and remote IPv4 address and port of the host socket in network byte order, the SELF and NONBLOCK status flags, and the receive and send buffer sizes sceNetGetsockopt reports for the socket. The other fields are 0. It returns 1, the number of records written, and EINVAL for a null record or an n below 1, as shadPS4 does. An s below 0 lists the sockets as shadPS4 does: with a null record it returns their number, otherwise it writes up to n records in socket order and returns how many it wrote. Unlike shadPS4, which lists IPv6 sockets with zero addresses and ignores flags, an IPv6 socket fails with EAFNOSUPPORT and is left out of the count and the list, and a flags bit other than 0x40 fails with EINVAL. SharpProspero names 0x40 SockInfoTimeWait; closed sockets leave the table at once, so it lists the same sockets. docs/dev/debt/libSceNet.md lists the fields left 0 and these differences. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libSceNet/Export.cpp | 114 +++++++++++++++++++++++++++-- core/libs/tests/GuestSceNet.cpp | 109 +++++++++++++++++++++++++++ docs/dev/debt/libSceNet.md | 1 + 3 files changed, 219 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index d8240c8bc2..420add6cce 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -1381,12 +1381,116 @@ int APS5_VABI sceNetGetpeername(int s, void* addr, uint32_t* addrlen) { return native_to_guest_address(native, addr, addrlen) ? 0 : fail(NET_EAFNOSUPPORT); } +constexpr int NET_SOCKINFO_F_SELF = 0x1; +constexpr int NET_SOCKINFO_F_NONBLOCK = 0x200000; +constexpr int NET_SOCKINFO_TIME_WAIT = 0x40; + +struct NetSockInfo { + char name[32]; + std::int32_t pid; + std::int32_t s; + std::int8_t socket_type; + std::int8_t policy; + std::int8_t priority; + std::int8_t reserved8; + std::int32_t recv_queue_length; + std::int32_t send_queue_length; + std::uint32_t local_adr; + std::uint32_t remote_adr; + std::uint16_t local_port; + std::uint16_t remote_port; + std::uint16_t local_vport; + std::uint16_t remote_vport; + std::int32_t state; + std::int32_t flags; + std::int32_t tx_bps; + std::int32_t rx_bps; + std::int32_t max_tx_bps; + std::int32_t max_rx_bps; + std::int32_t tx_vbps; + std::int32_t rx_vbps; + std::int32_t recv_buffer_size; + std::int32_t send_buffer_size; + std::int32_t reserved6[8]; + std::int32_t tx_drops; + std::int32_t rx_drops; + std::int32_t tx_wait; + std::int32_t reserved[2]; +}; +static_assert(sizeof(NetSockInfo) == 0xA0 && offsetof(NetSockInfo, s) == 0x24 && offsetof(NetSockInfo, local_adr) == 0x34 && + offsetof(NetSockInfo, local_port) == 0x3C && offsetof(NetSockInfo, recv_buffer_size) == 0x64); + +static int sock_buffer_size(const Sock& socket, bool send, std::int32_t& value) { + value = send ? socket.snd_buffer : socket.rcv_buffer; + if (value != 0) return 0; + NativeLength size = sizeof(value); + return ::getsockopt(socket.native->value, SOL_SOCKET, send ? SO_SNDBUF : SO_RCVBUF, reinterpret_cast<char*>(&value), &size) == 0 ? 0 : native_error(); +} + +static int fill_sock_info(int s, NetSockInfo& record) { + Sock socket; + { + std::lock_guard lk(g_mutex); + const auto it = g_socks.find(s); + if (it == g_socks.end()) return NET_EBADF; + socket = it->second; + } + if (socket.family != NET_AF_INET) return NET_EAFNOSUPPORT; + sockaddr_in local{}; + NativeLength length = sizeof(local); + if (::getsockname(socket.native->value, reinterpret_cast<sockaddr*>(&local), &length) != 0) { +#ifdef _WIN32 + if (WSAGetLastError() != WSAEINVAL) return native_error(); +#else + return native_error(); +#endif + } + sockaddr_in remote{}; + length = sizeof(remote); + if (::getpeername(socket.native->value, reinterpret_cast<sockaddr*>(&remote), &length) != 0) { + const int error = native_error(); + if (error != NET_ENOTCONN) return error; + } + record = {}; + if (const int error = sock_buffer_size(socket, false, record.recv_buffer_size)) return error; + if (const int error = sock_buffer_size(socket, true, record.send_buffer_size)) return error; + record.s = s; + record.socket_type = static_cast<std::int8_t>(socket.type); + record.local_adr = local.sin_addr.s_addr; + record.remote_adr = remote.sin_addr.s_addr; + record.local_port = local.sin_port; + record.remote_port = remote.sin_port; + record.flags = NET_SOCKINFO_F_SELF | (socket.nonblock ? NET_SOCKINFO_F_NONBLOCK : 0); + return 0; +} + int APS5_VABI sceNetGetSockInfo(int s, void* info, int n, int flags) { - (void)info; - (void)n; - (void)flags; - std::lock_guard lk(g_mutex); - return g_socks.count(s) != 0 ? 0 : fail(NET_EBADF); + if ((flags & ~NET_SOCKINFO_TIME_WAIT) != 0) return fail(NET_EINVAL); + std::vector<int> listed; + if (s < 0) { + std::lock_guard lk(g_mutex); + for (const auto& [descriptor, socket] : g_socks) { + if (socket.family == NET_AF_INET) listed.push_back(descriptor); + } + } + if (!info) return s < 0 ? static_cast<int>(listed.size()) : fail(NET_EINVAL); + if (n < 1) return fail(NET_EINVAL); + NetSockInfo record; + if (s >= 0) { + if (const int error = fill_sock_info(s, record)) return fail(error); + std::memcpy(info, &record, sizeof(record)); + return 1; + } + int written = 0; + for (const int descriptor : listed) { + if (written == n) break; + const int error = fill_sock_info(descriptor, record); + if (error == NET_EBADF || error == NET_EAFNOSUPPORT) continue; + if (error != 0) return fail(error); + std::memcpy(static_cast<char*>(info) + static_cast<std::size_t>(written) * sizeof(record), &record, sizeof(record)); + ++written; + } + return written; } int APS5_VABI sceNetEpollCreate(const char* name, int flags) { diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index a3de378043..0dfa72da62 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -26,6 +26,7 @@ #include <stdexcept> #include <string> #include <thread> +#include <vector> extern "C" { extern const std::uint8_t in6addr_any_nid_postfix[16]; @@ -39,6 +40,7 @@ int APS5_VABI sceNetBind_nid_postfix(int, const void*, std::uint32_t); int APS5_VABI sceNetListen(int, int); int APS5_VABI sceNetGetsockname(int, void*, std::uint32_t*); int APS5_VABI sceNetGetpeername(int, void*, std::uint32_t*); +int APS5_VABI sceNetGetSockInfo(int, void*, int, int); int APS5_VABI sceNetConnect(int, const void*, std::uint32_t); int APS5_VABI sceNetAccept(int, void*, std::uint32_t*); std::int64_t APS5_VABI sceNetSend(int, const void*, std::size_t, int); @@ -1230,6 +1232,112 @@ static void CheckCloseKernelSocket() { Require(Failed(sceNetSocketClose(kernel), 9)); } +static std::array<std::uint8_t, 0xa0> RequireSockInfo(int socket, int type, int flags, const std::array<std::uint8_t, 16>& local, + const std::array<std::uint8_t, 16>& remote) { + std::array<std::uint8_t, 0x140> records; + records.fill(0xa5); + Require(sceNetGetSockInfo(socket, records.data(), 2, 0) == 1); + std::array<std::uint8_t, 0xa0> expected{}; + std::memcpy(expected.data() + 0x24, &socket, sizeof(socket)); + expected[0x28] = static_cast<std::uint8_t>(type); + std::memcpy(expected.data() + 0x34, local.data() + 4, 4); + std::memcpy(expected.data() + 0x38, remote.data() + 4, 4); + std::memcpy(expected.data() + 0x3c, local.data() + 2, 2); + std::memcpy(expected.data() + 0x3e, remote.data() + 2, 2); + std::memcpy(expected.data() + 0x48, &flags, sizeof(flags)); + for (const auto [offset, option] : {std::array{0x64, 0x1002}, std::array{0x68, 0x1001}}) { + int size = 0; + std::uint32_t length = sizeof(size); + Require(sceNetGetsockopt(socket, 0xffff, option, &size, &length) == 0 && size > 0); + std::memcpy(expected.data() + offset, &size, sizeof(size)); + } + Require(std::equal(expected.begin(), expected.end(), records.begin())); + Require(std::all_of(records.begin() + 0xa0, records.end(), [](auto byte) { return byte == 0xa5; })); + return expected; +} + +static std::vector<int> RequireSockInfoList(int flags) { + const int count = sceNetGetSockInfo(-1, nullptr, 0, flags); + Require(count >= 0); + std::vector<std::uint8_t> records(static_cast<std::size_t>(count + 1) * 0xa0, 0xa5); + Require(sceNetGetSockInfo(-1, records.data(), count + 1, flags) == count); + Require(std::all_of(records.end() - 0xa0, records.end(), [](auto byte) { return byte == 0xa5; })); + std::vector<int> sockets; + for (int index = 0; index < count; ++index) { + const auto listed = records.begin() + index * 0xa0; + int socket = 0; + std::memcpy(&socket, &listed[0x24], sizeof(socket)); + Require(sockets.empty() || socket > sockets.back()); + std::array<std::uint8_t, 0xa0> single; + Require(sceNetGetSockInfo(socket, single.data(), 1, 0) == 1 && std::equal(single.begin(), single.end(), listed)); + sockets.push_back(socket); + } + const int one = std::min(count, 1); + std::array<std::uint8_t, 0x140> first; + first.fill(0xa5); + Require(sceNetGetSockInfo(-1, first.data(), 1, flags) == one && std::equal(first.begin(), first.begin() + one * 0xa0, records.begin())); + Require(std::all_of(first.begin() + one * 0xa0, first.end(), [](auto byte) { return byte == 0xa5; })); + return sockets; +} + +static void CheckSockInfo() { + const auto earlier = RequireSockInfoList(0); + const std::array<std::uint8_t, 16> unnamed{}; + const int datagram = sceNetSocket("sockinfo-datagram", 2, 2, 17); + Require(datagram >= 0); + RequireSockInfo(datagram, 2, 1, unnamed, unnamed); + std::array<std::uint8_t, 16> datagramAddress{16, 2, 0, 0, 127, 0, 0, 1}; + Require(sceNetBind_nid_postfix(datagram, datagramAddress.data(), datagramAddress.size()) == 0); + std::uint32_t size = datagramAddress.size(); + Require(sceNetGetsockname(datagram, datagramAddress.data(), &size) == 0 && size == 16); + RequireSockInfo(datagram, 2, 1, datagramAddress, unnamed); + const int listener = sceNetSocket("sockinfo-listener", 2, 1, 6); + std::array<std::uint8_t, 16> listening{16, 2, 0, 0, 127, 0, 0, 1}; + Require(listener >= 0 && sceNetBind_nid_postfix(listener, listening.data(), listening.size()) == 0 && sceNetListen(listener, 1) == 0); + size = listening.size(); + Require(sceNetGetsockname(listener, listening.data(), &size) == 0 && size == 16); + RequireSockInfo(listener, 1, 1, listening, unnamed); + const int client = sceNetSocket("sockinfo-client", 2, 1, 6); + Require(client >= 0 && sceNetConnect(client, listening.data(), listening.size()) == 0); + const int accepted = sceNetAccept(listener, nullptr, nullptr); + Require(accepted >= 0); + std::array<std::uint8_t, 16> clientAddress{}; + size = clientAddress.size(); + Require(sceNetGetsockname(client, clientAddress.data(), &size) == 0 && size == 16); + RequireSockInfo(client, 1, 1, clientAddress, listening); + RequireSockInfo(accepted, 1, 1, listening, clientAddress); + Require(sceNetConnect(datagram, listening.data(), listening.size()) == 0); + RequireSockInfo(datagram, 2, 1, datagramAddress, listening); + const int nonblocking = 1; + const std::array<int, 2> buffers{65536, 32768}; + Require(sceNetSetsockopt(datagram, 0xffff, 0x1200, &nonblocking, sizeof(nonblocking)) == 0); + Require(sceNetSetsockopt(datagram, 0xffff, 0x1002, &buffers[0], sizeof(buffers[0])) == 0); + Require(sceNetSetsockopt(datagram, 0xffff, 0x1001, &buffers[1], sizeof(buffers[1])) == 0); + const auto configured = RequireSockInfo(datagram, 2, 0x200001, datagramAddress, listening); + Require(std::memcmp(configured.data() + 0x64, buffers.data(), sizeof(buffers)) == 0); + std::array<std::uint8_t, 0xa0> record; + record.fill(0xa5); + Require(Failed(sceNetGetSockInfo(datagram, nullptr, 1, 0), 22)); + Require(Failed(sceNetGetSockInfo(datagram, record.data(), 0, 0), 22)); + Require(Failed(sceNetGetSockInfo(-1, record.data(), 0, 0), 22)); + for (const int flags : {1, 0x41, 0x1000}) { + Require(Failed(sceNetGetSockInfo(datagram, record.data(), 1, flags), 22)); + Require(Failed(sceNetGetSockInfo(-1, nullptr, 0, flags), 22)); + } + const int inet6 = sceNetSocket("sockinfo-inet6", 28, 2, 17); + Require(inet6 >= 0 ? Failed(sceNetGetSockInfo(inet6, record.data(), 1, 0), 47) : *sceNetErrnoLoc() == 47); + Require(std::all_of(record.begin(), record.end(), [](auto byte) { return byte == 0xa5; })); + std::vector<int> expected = earlier; + expected.insert(expected.end(), {datagram, listener, client, accepted}); + std::sort(expected.begin(), expected.end()); + Require(RequireSockInfoList(0) == expected && RequireSockInfoList(0x40) == expected); + Require(sceNetGetSockInfo(datagram, record.data(), 1, 0x40) == 1 && std::equal(configured.begin(), configured.end(), record.begin())); + if (inet6 >= 0) Require(sceNetSocketClose(inet6) == 0); + for (const int descriptor : {accepted, client, listener, datagram}) Require(sceNetSocketClose(descriptor) == 0); + Require(Failed(sceNetGetSockInfo(datagram, record.data(), 1, 0), 9)); + Require(RequireSockInfoList(0) == earlier); +} + static void CheckDatagramDisconnect() { const int peer = sceNetSocket("disconnect-peer", 2, 2, 17); const int other = sceNetSocket("disconnect-other", 2, 2, 17); @@ -1313,6 +1421,7 @@ int main() { } CheckDatagramDisconnect(); CheckCloseKernelSocket(); + CheckSockInfo(); const int closed = sceNetSocket("closed-by-kernel", 2, 2, 17); Require(closed >= 0); diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index 45d2f7f933..777bc225c8 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -188,6 +188,7 @@ - [sceNetSetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) stores `SO_ONESBCAST` (`0x10000`, [shadPS4's `ORBIS_NET_SO_ONESBCAST`](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/net/net_types.h)) per socket and `sceNetGetsockopt` reads back the value that was set, as shadPS4 does; PPSA34501 sets it and crashes later when it fails with `EOPNOTSUPP`. Sends ignore it. FreeBSD 11.4's [ip(4)](https://man.freebsd.org/cgi/man.cgi?manpath=FreeBSD+11.4-RELEASE&query=ip&sektion=4) documents the like-named `IPPROTO_IP` option `IP_ONESBCAST`: a datagram sent to an interface's directed broadcast address, such as 192.168.2.255, leaves through that interface with its destination rewritten to 255.255.255.255, and only on a socket allowed to set `SO_BROADCAST`. Here host routing selects the outgoing interface and the destination is not rewritten. shadPS4 also leaves sends unchanged, and its [TODO](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/net/net.cpp#L2467-L2470) reads the option the other way round: a send to 255.255.255.255 would be converted to the interface's broadcast address. Whether the console's `SOL_SOCKET` option behaves like `IP_ONESBCAST`, and what it reads back, is not verified on hardware. FreeBSD 11.4's `sogetopt()` reads several boolean socket options back as their flag bit (`so_options & optname`, `sys/kern/uipc_socket.c`), not as the value set; whether the console does so for `0x10000` is unknown - [sceNetGetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) reads `SO_ERROR` from the host socket. Winsock does not clear a socket's pending error when it is read, so on Windows the library reports a pending error once and shows it again only after a new `sceNetConnect`; a second failure with the same code on the same socket without a `sceNetConnect` in between would be hidden. - [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 - [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 From eb78be80366816b800cad41a7fc9ec4e1431b17e Mon Sep 17 00:00:00 2001 From: primesoul-web <primesoul-web@users.noreply.github.com> Date: Tue, 20 Oct 2026 07:24:09 +0000 Subject: [PATCH 235/269] fix(libSceIme): drop the filtered commit when the session changed while the filter ran --- core/libs/prx/libSceIme/Export.cpp | 7 ++++++- core/libs/tests/GuestImeOpen.cpp | 24 ++++++++++++++++++++++++ docs/dev/debt/libSceIme.md | 6 +++--- 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceIme/Export.cpp b/core/libs/prx/libSceIme/Export.cpp index b2d0f2a4a6..ea355e802e 100644 --- a/core/libs/prx/libSceIme/Export.cpp +++ b/core/libs/prx/libSceIme/Export.cpp @@ -66,6 +66,7 @@ struct Session { std::deque<ImeEvent> events; std::deque<ImeHostInput> hostInputs; std::uint64_t id = 0; + std::uint64_t revision = 0; }; struct DeferredFilter { @@ -478,6 +479,7 @@ int APS5_VABI sceImeSetCaret(const Caret* caret) { if (!caret) return ErrorInvalidAddress; if (caret->index > g_session->text.size()) return ErrorInvalidParam; g_session->caret = caret->index; + ++g_session->revision; return 0; } @@ -495,6 +497,7 @@ int APS5_VABI sceImeSetText(const char16_t* text, uint32_t length) { const std::size_t count = TextLength(text, std::min(length, session.param.max_text_length)); if (!ValidText(text, count, (session.param.option & OptionMultiline) != 0)) return ErrorInvalidText; session.text.assign(text, count); + ++session.revision; if (session.caret > session.text.size()) session.caret = static_cast<uint32_t>(session.text.size()); SyncBuffers(session); return 0; @@ -521,6 +524,7 @@ int APS5_VABI sceImeUpdate(EventHandler handler) { std::uint32_t filterMaxLength = 0; bool filterMultiline = false; std::uint64_t sessionId = 0; + std::uint64_t sessionRevision = 0; { std::lock_guard lock(g_imeMutex); if (!g_session || g_session->param.handler != handler) return 0; @@ -535,6 +539,7 @@ int APS5_VABI sceImeUpdate(EventHandler handler) { filterMaxLength = g_session->param.max_text_length; filterMultiline = (g_session->param.option & OptionMultiline) != 0; sessionId = g_session->id; + sessionRevision = g_session->revision; } } else if (!g_session->events.empty()) { event = g_session->events.front(); @@ -549,7 +554,7 @@ int APS5_VABI sceImeUpdate(EventHandler handler) { std::u16string filtered; const bool accepted = RunTextFilter(filter, filterText, filterMaxLength, filterMultiline, &filtered); std::lock_guard lock(g_imeMutex); - if (g_session && g_session->id == sessionId) { + if (g_session && g_session->id == sessionId && g_session->revision == sessionRevision) { CommitFilteredEdit(*g_session, accepted ? &filtered : nullptr, filterOldText); } continue; diff --git a/core/libs/tests/GuestImeOpen.cpp b/core/libs/tests/GuestImeOpen.cpp index 1437400528..a1ce465687 100644 --- a/core/libs/tests/GuestImeOpen.cpp +++ b/core/libs/tests/GuestImeOpen.cpp @@ -126,6 +126,7 @@ int APS5_VABI ExtNoop(const ImeKeycode*, std::uint16_t*, std::uint32_t*, void*) } bool g_sawCaptureInsideFilter = false; +int g_setInsideRc = -1; int32_t APS5_VABI LockingFilter(char16_t* outText, uint32_t* outTextLength, const char16_t* sourceText, uint32_t sourceTextLength) { g_sawCaptureInsideFilter = ImeCapturesKeyboard_nid_postfix(); @@ -136,6 +137,15 @@ int32_t APS5_VABI LockingFilter(char16_t* outText, uint32_t* outTextLength, cons return 0; } +int32_t APS5_VABI SetInsideFilter(char16_t* outText, uint32_t* outTextLength, const char16_t* sourceText, uint32_t sourceTextLength) { + g_setInsideRc = sceImeSetText(u"zz", 2); + for (uint32_t i = 0; i < sourceTextLength; ++i) { + outText[i] = sourceText[i]; + } + *outTextLength = sourceTextLength; + return 0; +} + void Require(bool value, const char* message) { if (value) return; std::fprintf(stderr, "%s\n", message); @@ -470,6 +480,20 @@ void CheckTextFilter() { RequireText(Drain().at(0), EventUpdateText, u"c", 1, 0, 1); Require(g_sawCaptureInsideFilter, "the filter can call back into sceIme*"); Require(sceImeClose_nid_postfix() == 0, "close the locking session"); + + buffers = Buffers{}; + param = MakeParam(buffers, 8); + std::memcpy(buffers.text, u"ab", sizeof(u"ab")); + param.filter = reinterpret_cast<ImeTextFilter>(&SetInsideFilter); + Require(sceImeOpen_nid_postfix(¶m, nullptr) == 0, "open with a filter that sets the text"); + Drain(); + g_setInsideRc = -1; + Publish(ImeHostInputKind::Text, "c"); + Require(Drain().empty(), "an edit whose filter set the text sends no event"); + Require(g_setInsideRc == 0, "sceImeSetText inside the filter returns 0"); + Require(std::u16string(buffers.text) == u"zz", "the setText inside the filter wins"); + Require(std::u16string(reinterpret_cast<const char16_t*>(buffers.work)) == u"zz", "the setText inside the filter wins in the work copy"); + Require(sceImeClose_nid_postfix() == 0, "close the set-inside session"); } void CheckHostEvents() { diff --git a/docs/dev/debt/libSceIme.md b/docs/dev/debt/libSceIme.md index 0b1ac53637..bcce37a189 100644 --- a/docs/dev/debt/libSceIme.md +++ b/docs/dev/debt/libSceIme.md @@ -2,7 +2,7 @@ ### Silent stubs -- [sceImeOpen](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) shows no panel: the text is typed on the host keyboard, which [ImeTextInput](../../../core/libs/prx/libSceVideoOut/src/ImeTextInput.cpp) forwards from the game window (SDL text input, Backspace, Delete, Left, Right, Home, End, Enter and Escape), and the controller cannot type. While a panel is open, key presses do not reach the keyboard-to-pad bindings of [PadInput](../../../core/libs/prx/libSceVideoOut/src/PadInput.cpp), as the console's panel takes the controller; key releases still do. Minecraft Preview (PPSA19639, #1277) opens it when entering a world +- [sceImeOpen](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) shows no panel: the text is typed on the host keyboard, which [ImeTextInput](../../../core/libs/prx/libSceVideoOut/src/ImeTextInput.cpp) forwards from the game window (SDL text input, Backspace, Delete, Left, Right, Home, End, Enter and Escape), and the controller cannot type. While a panel is open, key presses do not reach the keyboard-to-pad bindings of [PadInput](../../../core/libs/prx/libSceVideoOut/src/PadInput.cpp), as the console's panel takes the controller; key releases still do. Minecraft Preview (PPSA19639, #1277) opens it when entering a world. The external keyboard filter is accepted and never called because no keycode events are produced ### Unknown function info @@ -55,7 +55,7 @@ ### Functional -- [sceImeKeyboardOpen, sceImeKeyboardClose](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - opening a keyboard that is already open for the same user returns `0x80bc0001` (busy) and closing one that is not open returns `0x80bc0002`, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/ime.cpp) does; it tracks one keyboard for all users, while the open state is kept per user id here, and the PS5 result is unverified +- [sceImeKeyboardOpen, sceImeKeyboardClose](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - opening a keyboard that is already open for the same user returns `0x80bc0001` (busy) and closing one that is not open returns `0x80bc0002`, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/1211958b4eb38256cc0c5c7ba92d668820c05389/src/libs/ime.cpp) does; it tracks one keyboard for all users, while the open state is kept per user id here, and the PS5 result is unverified - [sceImeKeyboardGetInfo](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) returns `0x80bc0023` (no resource id) for every id while a keyboard is open, since the IME keyboard never reports a connected device. [sceImeKeyboardSetMode](../../../core/libs/prx/libSceIme/Export.cpp) validates the mode bits but the mode has no effect, as no IME keyboard events are delivered. - [sceImeKeyboardGetResourceId](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) reports that no USB keyboard is connected (`0x80bc0004`, as [shadPS4](https://github.com/shadps4-emu/shadPS4) does) because IME keyboard events are not delivered to the handler, although [libSceKeyboard](../../../core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp) exposes the host keyboard -- [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. 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 +- [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 and the edit 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 From cfdaf2dbb8312c265fdb491b1469a849e432b9cd Mon Sep 17 00:00:00 2001 From: Sebastian <115291+slider@users.noreply.github.com> Date: Mon, 19 Oct 2026 15:45:02 +0000 Subject: [PATCH 236/269] feat(libkernel): implement fchflags Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QnkpF2oEMofseVz9Z7WEAC --- core/libs/prx/libkernel/File/src/Stdio.cpp | 48 +++++++++++++++++----- core/libs/tests/GuestFilesystem.cpp | 32 +++++++++++++++ docs/dev/debt/libkernel.md | 1 + 3 files changed, 70 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index df106b1b56..4d786eb038 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -497,18 +497,27 @@ static int NativeFstatfs(int descriptor, KernelStatfs* fs) { fs->f_namemax = nameMax; return 0; } + +static bool NativeIsPipe(int descriptor) { + const auto handle = reinterpret_cast<HANDLE>(::_get_osfhandle(descriptor)); + return handle != INVALID_HANDLE_VALUE && ::GetFileType(handle) == FILE_TYPE_PIPE; +} #else #include <sys/statvfs.h> #include <sys/vfs.h> -static int NativeFstatfs(int descriptor, KernelStatfs* fs) { +static bool NativeIsPipe(int descriptor) { constexpr long PipeFilesystem = 0x50495045; + struct stat file{}; + struct statfs kind{}; + return ::fstat(descriptor, &file) == 0 && S_ISFIFO(file.st_mode) && ::fstatfs(descriptor, &kind) == 0 + && static_cast<long>(kind.f_type) == PipeFilesystem; +} + +static int NativeFstatfs(int descriptor, KernelStatfs* fs) { struct stat file{}; if (::fstat(descriptor, &file) != 0) return SceErrorFromErrno(errno) & 0xffff; if (S_ISSOCK(file.st_mode) && (::fcntl(descriptor, F_GETFL) & O_PATH) == 0) return GUEST_EINVAL; - if (S_ISFIFO(file.st_mode)) { - struct statfs kind{}; - if (::fstatfs(descriptor, &kind) == 0 && static_cast<long>(kind.f_type) == PipeFilesystem) return GUEST_EINVAL; - } + if (NativeIsPipe(descriptor)) return GUEST_EINVAL; struct statvfs host{}; if (::fstatvfs(descriptor, &host) != 0) return SceErrorFromErrno(errno) & 0xffff; if (host.f_flag & ST_RDONLY) fs->f_flags |= KERNEL_MNT_RDONLY; @@ -618,6 +627,16 @@ static std::string FileFlagNames(std::uint64_t flags) { return names; } +static int SetFileFlags(const char* function, const std::string& target, std::uint64_t flags) { + std::uint64_t known = 0; + for (const auto& entry : GUEST_FILE_FLAGS) known |= entry.first; + if ((flags & ~known) != 0) return GUEST_EOPNOTSUPP; + if ((flags & GUEST_SF_SNAPSHOT) != 0) return GUEST_EPERM; + if (flags != 0) + throw std::runtime_error(std::string(function) + ": setting " + FileFlagNames(flags) + " on " + target + " is not supported, the host keeps no BSD file flags"); + return 0; +} + extern "C" { int APS5_VABI chmod_nid_postfix(const char* path, int mode) { @@ -633,12 +652,19 @@ int APS5_VABI chflags_nid_postfix(const char* path, std::uint64_t flags) { std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return PosixFailure(error); if (const int error = LookupError(native)) return PosixFailure(error); - std::uint64_t known = 0; - for (const auto& entry : GUEST_FILE_FLAGS) known |= entry.first; - if ((flags & ~known) != 0) return PosixFailure(GUEST_EOPNOTSUPP); - if ((flags & GUEST_SF_SNAPSHOT) != 0) return PosixFailure(GUEST_EPERM); - if (flags != 0) - throw std::runtime_error("chflags: setting " + FileFlagNames(flags) + " on " + path + " is not supported, the host keeps no BSD file flags"); + if (const int error = SetFileFlags("chflags", path, flags)) return PosixFailure(error); + return 0; +} + +int APS5_VABI fchflags_nid_postfix(int d, std::uint64_t flags) { + if (GuestSockets::IsOpen(d) || File::IsSharedMemoryDescriptor_nid_no_patch(d)) return PosixFailure(GUEST_EINVAL); + File::RefuseArchivedFile(d, "fchflags"); + if (!File::IsVnode(d)) { + if (!DescriptorIsOpen_nid_no_patch(d)) return PosixFailure(GUEST_EBADF); + if (NativeIsPipe(d)) return PosixFailure(GUEST_EINVAL); + throw std::runtime_error("fchflags: descriptor " + std::to_string(d) + " is neither a file, a directory, a pipe, a socket nor shared memory"); + } + if (const int error = SetFileFlags("fchflags", "descriptor " + std::to_string(d), flags)) return PosixFailure(error); return 0; } diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index 7ad1e67c4b..93481dfeac 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -35,6 +35,7 @@ int APS5_VABI sceKernelFchmod(int, unsigned short); int APS5_VABI fchmod_nid_postfix(int, int); int APS5_VABI chmod_nid_postfix(const char*, int); int APS5_VABI chflags_nid_postfix(const char*, std::uint64_t); +int APS5_VABI fchflags_nid_postfix(int, std::uint64_t); int APS5_VABI futimes_nid_postfix(int, const KernelTimeval*); int APS5_VABI sceKernelFutimes(int, const KernelTimeval*); int APS5_VABI socket_nid_postfix(int, int, int); @@ -132,6 +133,15 @@ static bool ChflagsThrows(const std::filesystem::path& path, std::uint64_t flags return false; } +static bool FchflagsThrows(int descriptor, std::uint64_t flags, const char* names) { + try { + fchflags_nid_postfix(descriptor, flags); + } catch (const std::runtime_error& error) { + return std::strstr(error.what(), names) != nullptr; + } + return false; +} + static std::string CaptureStderr(const std::function<void()>& action) { std::FILE* capture = std::tmpfile(); Require(capture != nullptr); @@ -565,6 +575,28 @@ int main() { Require(chflags_nid_postfix(file.string().c_str(), 0x200000) == -1 && *__error_nid_postfix() == 1); Require(ChflagsThrows(file, 0x2, "UF_IMMUTABLE")); Require(ChflagsThrows(file, 0x1000 | 0x40000, "UF_READONLY|SF_APPEND")); + const int flagged = open_nid_postfix(file.string().c_str(), 0, 0); + Require(flagged >= 0 && fchflags_nid_postfix(flagged, 0) == 0); + Require(fchflags_nid_postfix(flagged, 0x20) == -1 && *__error_nid_postfix() == 45); + Require(fchflags_nid_postfix(flagged, 1ULL << 32) == -1 && *__error_nid_postfix() == 45); + Require(fchflags_nid_postfix(flagged, ~0ULL) == -1 && *__error_nid_postfix() == 45); + Require(fchflags_nid_postfix(flagged, 0x200000) == -1 && *__error_nid_postfix() == 1); + Require(FchflagsThrows(flagged, 0x2, "UF_IMMUTABLE")); + Require(FchflagsThrows(flagged, 0x1000 | 0x40000, "UF_READONLY|SF_APPEND")); + Require(close_nid_postfix(flagged) == 0); + Require(fchflags_nid_postfix(flagged, 0) == -1 && *__error_nid_postfix() == 9); + Require(fchflags_nid_postfix(flagged, ~0ULL) == -1 && *__error_nid_postfix() == 9); + Require(fchflags_nid_postfix(-1, 0) == -1 && *__error_nid_postfix() == 9); + const int flaggedRoot = sceKernelOpen(root.string().c_str(), 0x20000, 0); + Require(flaggedRoot >= 0 && fchflags_nid_postfix(flaggedRoot, 0) == 0 && sceKernelClose(flaggedRoot) == 0); + int flaggedPipe[2]; + Require(pipe_nid_postfix(flaggedPipe) == 0); + Require(fchflags_nid_postfix(flaggedPipe[0], 0) == -1 && *__error_nid_postfix() == 22); + Require(fchflags_nid_postfix(flaggedPipe[1], ~0ULL) == -1 && *__error_nid_postfix() == 22); + Require(close_nid_postfix(flaggedPipe[0]) == 0 && close_nid_postfix(flaggedPipe[1]) == 0); + const int flaggedSocket = socket_nid_postfix(2, 2, 0); + Require(flaggedSocket >= 0 && fchflags_nid_postfix(flaggedSocket, 0) == -1 && *__error_nid_postfix() == 22); + Require(close_nid_postfix(flaggedSocket) == 0); const auto held = root / "held.txt"; const auto heldName = held.string(); { std::ofstream stream(held); stream << "held"; } diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 75d78f132b..9d2edf5790 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -219,6 +219,7 @@ - [sceKernelGetPageTableStats](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) reports 8192 entries on each side and counts them as a PS5 (firmware 13.60) counted them for a payload process: one cpu entry for each 2 MiB granule that holds a committed mapping, whatever its protection (none included), and one gpu entry for each 1 GiB region that holds a GPU-accessible mapping, plus one for each 2 MiB granule of such a mapping that is not a whole granule of direct memory at a 2 MiB-aligned physical address. A granule or region shared by several mappings counts once, and reservations do not count. On the console the first GPU mapping of a process also takes three gpu entries and one cpu entry that stay after it is unmapped, which is not modelled, and only the ranges whose protection is recorded here are counted. A null pointer returns `SCE_KERNEL_ERROR_EFAULT`, where the console faults. Resonance: A Plague Tale Legacy (PPSA30601) calls it at start-up (#4067) - [clock_settime](../../../core/libs/prx/libkernel/Time/Time.cpp) (libkernel) validates like FreeBSD 11.4's [`kern_clock_settime`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_time.c#L396-L412) for a root caller: `EFAULT` for a null time, `EINVAL` for any clock but `CLOCK_REALTIME`, a negative second count or a nanosecond count outside 0 to 999999999. A valid `CLOCK_REALTIME` request throws, naming the requested time, since the host clock is never set and no guest clock offset is kept. FreeBSD checks `PRIV_CLOCK_SETTIME` first, which a jailed process fails with `EPERM`; whether a title holds it on the console is unverified. PS5MC (PPSA99420) imports it; no call site is known - [chflags](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) follows FreeBSD 11.4's [`kern_chflagsat`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_syscalls.c#L2366-L2383) and UFS's [`ufs_setattr`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/ufs/ufs/ufs_vnops.c#L509-L556) for a root caller: the path lookup reports `EFAULT`, `ENOENT` and `ENOTDIR`, a flag outside the 17 UFS stores (`VNOVAL` included) `EOPNOTSUPP`, and `SF_SNAPSHOT` `EPERM`. Neither host keeps BSD file flags, so every file is taken to have none (`stat` reports `st_flags` 0): flags 0 succeed without touching the file, and any other flag throws, naming the flags. `UF_READONLY`, `UF_HIDDEN`, `UF_SYSTEM` and `UF_ARCHIVE` are FreeBSD's names for the Windows attributes but are not mapped to them: UFS stores them without enforcing them, `FILE_ATTRIBUTE_READONLY` already carries the owner write bit of `chmod` on Windows, and Linux has no equivalent a process can set without `CAP_LINUX_IMMUTABLE`. The console's filesystem types, `EROFS` on read-only mounts and the change-time update are not modelled +- [fchflags](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) has the gaps of `chflags`: a valid non-zero flag throws, and the console's filesystem types, `EROFS` and the change-time update are not modelled. Character devices, the random device, other host descriptors and files read from app0.zar throw, since their console filesystems and the privilege check for devices are not modelled. On Windows, anonymous shared memory objects are temporary files that are not told apart from regular files, so they are treated as files instead of reporting `EINVAL` as FreeBSD 11.4's [`getvnode`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_syscalls.c#L3850-L3877) does. - [getpriority](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) follows FreeBSD 11.4's [`sys_getpriority`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_resource.c#L93-L165) for the only guest process, which is taken to run at nice 0: `PRIO_PROCESS` and `PRIO_PGRP` with 0 or its own pid (the guest is taken to lead its own process group, see `kill`) and `PRIO_USER` with 0 or its user id 0 return 0, any other id fails with `ESRCH` and any other `which` with `EINVAL`. As in FreeBSD's libc, where `getpriority` is the bare system call, errno is left unchanged on success. The nice value of a title process on the console is unverified, and the host nice of the process or of the threads `APS5_THREAD_PRIORITY=high` raises is not reported. `setpriority` accepts the same target ids (any other id fails with `ESRCH`, any other `which` with `EINVAL`), clamps the value to FreeBSD's `PRIO_MIN`-`PRIO_MAX` (-20 to 20) and stores it as the guest process's nice value, which `getpriority` returns; the host scheduling class is not changed, and whether a title process may lower the value without privilege is unverified - [rtprio_thread, pthread_setconcurrency, pthread_getconcurrency](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - `rtprio_thread` follows FreeBSD 11.4's `rtprio_thread(2)`. Every guest thread here runs under `SCHED_FIFO` or `SCHED_RR` (priority 256-767), whose rtprio class is real-time; the console's mapping from that priority to the rtprio `prio` range (0-31) is unmeasured, so `RTP_LOOKUP` and `RTP_SET` throw for such a thread rather than report an impossible priority. A thread that is not real-time reports `RTP_PRIO_NORMAL` (3) with 0, and `RTP_SET` accepts only that value. Another function fails with `EINVAL`, a null `rtprio` with `EFAULT`, a thread other than the calling one throws, and an id that is not a live guest thread with `ESRCH`. `pthread_setconcurrency` stores 0 or more and returns `EINVAL` for a negative level (the error itself, as FreeBSD's libthr does, with errno untouched); `pthread_getconcurrency` returns the level, which is a hint and changes nothing about the host's concurrency. Morrowind (`mshivam019/morrowind-ps5`) imports all three at start-up - [sceKernelVirtualQuery](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) returns [VirtualQueryInfo](../../../core/libs/SceTypes.hpp) with a one-byte flag allocation unit so Windows and SysV hosts return the same 72-byte representation, with the name at byte 33 and GPU mask at byte 65. This preserves the existing SysV representation; the console offsets beyond the address, protection and size fields observed in Astro Bot's AGC calls are not hardware-measured. Size and offset assertions plus a raw-byte named-mapping regression guard the host ABI. From eca73e1e40c77b9654e64da45daa56edcb421a92 Mon Sep 17 00:00:00 2001 From: Leonardo Feitosa <2416980+LeoFeitosa@users.noreply.github.com> Date: Tue, 20 Oct 2026 03:58:46 -0300 Subject: [PATCH 237/269] revert(agc): track DCC storage images with more than one mip as one unit --- .../Graphics/include/TextureTiling.hpp | 1 - .../libSceAgcDriver/Graphics/src/Texture.cpp | 2 +- .../Graphics/src/TextureTiling.cpp | 4 ---- .../libSceAgcDriver/tests/TextureTiling.cpp | 21 ------------------- docs/dev/debt/libSceAgcDriver.md | 1 - 5 files changed, 1 insertion(+), 28 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp index dbff513454..3cb7cfc14d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp @@ -75,7 +75,6 @@ struct SurfaceGeometry { }; SurfaceGeometry DescribeSurface(const GuestTextureResource& descriptor); bool LevelsFitAllocation(const GuestTextureResource& surface, std::uint32_t levels); -bool TracksBlockUnits(const GuestTextureResource& surface, const SurfaceGeometry& geometry); GuestTextureResource ViewedArrayWindow(const GuestTextureResource& viewed); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 1c4afad236..b86391ad80 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -1373,7 +1373,7 @@ StorageTexture::StorageTexture(const Context& context, TextureDetiler& detiler, const bool layered = geometry.layers >= 2 && !geometry.thick && geometry.imageDepth == 1 && geometry.layers == geometry.imageLayers && geometry.layerBytes * geometry.layers == guestBytes && geometry.layerBytes % 65536 == 0 && descriptor.baseAddress % 65536 == 0; // Tile blocks divide the stamp blocks, so at an aligned base every unit is whole tile blocks // of its (layer, mip) slices (sliceWindows). - blockUnits = LayerTrackingEnabled() && BlockTrackingEnabled() && TracksBlockUnits(descriptor, geometry); + blockUnits = LayerTrackingEnabled() && BlockTrackingEnabled() && !geometry.thick && descriptor.tileMode != TextureTileMode::kLinear && descriptor.baseAddress % 65536 == 0; trackedLayers = blockUnits ? static_cast<std::uint32_t>((guestBytes + 65535) / 65536) : layered && LayerTrackingEnabled() ? geometry.layers : 1u; trackedLayerBytes = blockUnits ? 65536 : guestBytes / trackedLayers; layerGeneration.assign(trackedLayers, 0); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp index 56233ce130..2c5ce048a6 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp @@ -466,10 +466,6 @@ bool LevelsFitAllocation(const GuestTextureResource& surface, std::uint32_t leve return true; } -bool TracksBlockUnits(const GuestTextureResource& surface, const SurfaceGeometry& geometry) { - return !geometry.thick && surface.tileMode != TextureTileMode::kLinear && surface.baseAddress % 65536 == 0 && (surface.dccAddress == 0 || surface.mipCount == 1); -} - GuestTextureResource ViewedArrayWindow(const GuestTextureResource& viewed) { const bool layered = viewed.dimension == TextureDimension::k2DArray || viewed.dimension == TextureDimension::k1DArray || viewed.dimension == TextureDimension::kCube; if (!layered || viewed.baseArray == 0 || viewed.baseArray > viewed.depthOrLastArray || viewed.dccAddress != 0) return viewed; diff --git a/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp b/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp index 7cbca37e6e..629685b1d5 100644 --- a/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp @@ -141,27 +141,6 @@ void requireSliceAddresses(const GuestTextureResource& resource, std::uint32_t b } void RunTextureTilingTests() { - { - GuestTextureResource surface{}; - surface.baseAddress = 0x30000000; - surface.width = 1920; - surface.height = 1080; - surface.mipCount = 6; - surface.tileMode = TextureTileMode::kR64KBX; - surface.dimension = TextureDimension::k2D; - surface.format = 71; - const auto tracks = [](const GuestTextureResource& resource) { return TracksBlockUnits(resource, DescribeSurface(resource)); }; - Require(tracks(surface), "a tiled surface without DCC metadata must be tracked as 64 KiB blocks"); - surface.dccAddress = 0x31000000; - Require(!tracks(surface), "a DCC surface with more than one mip must be tracked as one unit"); - surface.mipCount = 1; - Require(tracks(surface), "a DCC surface with one mip must be tracked as 64 KiB blocks"); - surface.baseAddress += 0x1000; - Require(!tracks(surface), "a surface off a 64 KiB boundary must be tracked as one unit"); - surface.baseAddress -= 0x1000; - surface.tileMode = TextureTileMode::kLinear; - Require(!tracks(surface), "a linear surface must be tracked as one unit"); - } { const auto mips = ComputeMipLayout(TextureTileMode::kLinear, 1, 4, 4, 2); Require(mips.size() == 2, "linear mip chain must contain the requested mip count"); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 3484dadee4..e25bb03535 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -96,7 +96,6 @@ - [Draw input snapshots](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver): `PrepareDrawBindings` caps read-only input buffer snapshots at 64 MiB (overridable via `APS5_DRAW_SNAPSHOT_MAX_MIB`). Larger inputs (streaming heaps, bindless pools) or draws exceeding the cumulative budget skip the per-draw snapshot and bind in place, avoiding multi-gigabyte CPU copy stalls (such as Ratchet & Clank: Rift Apart PPSA01474 stalling at frame ~380 copying 36 GB every 5 s). If the title rewrites those large ranges on the CPU before GPU completion, the in-place binding observes the modification, as verified by `drawSnapshotCapTests`. - [Pending storage-image results](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) are stored when something reads their memory, not when the GPU work that wrote them completes. A title that waits for that work, frees the image and reuses its memory can have the new data overwritten by the late store (#1970). A title's own CPU reads are not seen either, so a title that reads the results after waiting for a label reads stale memory. Explicit-size file writes (`write`, `pwrite`, `writev`, `pwritev`, AIO and `fwrite`) synchronize their source ranges through the [host-read hook](../../../core/libs/prx/libc/src/GuestArena.cpp); other host readers are not covered. `APS5_STORE_AT_FLIP=1` stores every pending result at each flip and `APS5_STORE_AT_LABEL=1` before each label the GPU writes; both store every pending image, not only the ones the waited-for work wrote, and neither is on by default. - [Pending storage-image results over memory outside the write watch](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) leave to the CPU the 64 KiB blocks whose bytes differ from the ones saved at the upload, the fill, the image copy, the previous store or the moment their range left the watch (`Unwatch`, or a host import that does not stay watched), and the blocks the watch saw the CPU write before that moment. A store made on the GPU saves its bytes through a host-visible copy that becomes the saved bytes when its batch completes; a store or a refresh that needs them earlier waits for that batch. A unit without a write generation saves the guest bytes when results land in it outside the watch. One whose range leaves the watch while its results are pending saves its bytes through the unwatch hook, which also runs when a failed collect takes a range out of the watch. A range that leaves the watch without the hook running over it (an import fallback that unwatched without saving) leaves its pending results without saved bytes; storing such results throws. A unit whose results were stored while its blocks were watched keeps only a generation; when its blocks leave the watch while nothing is pending, the hook saves nothing, and results that land in it afterwards have neither saved bytes nor a watch, so storing them throws too. -- [Storage images with DCC metadata and more than one mip](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) are tracked as one unit, not as 64 KiB blocks (`blockUnits`). Tracked as blocks, the 1920x1080 six-mip DCC surfaces of PPSA28329's main menu lose their results: the menu background is missing or shows stale tiles. Storing such a surface whole at every write-back also avoids it, at a lower frame rate. Which step of the block path loses the results is not identified. - Windows host-pointer imports of the guest mapping itself are compared instead of write-watched, except writable imports of private `MEM_WRITE_WATCH` memory when the import probe keeps the write watch and a settle queue exists: those of 1 MiB or more stay watched, and smaller ones return to the watch once their settle has completed, unless a storage image overlaps them (see Pages under an import window): a driver can mark imported pages dirty after the synchronous import window. Shared memory imported through a separate writable alias retains protection-based tracking on the guest views; driver access to the import alias does not disarm those views. [GuestMemory](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) excludes every overlapping 64 KiB tracker block while its backing is retained. A full block replaced by fresh private backing regains tracking and gets a new generation; protection changes, partial replacements and shared remappings do not restore it. Excluded blocks keep their write-watch marks as a hint only (`UntrackedHint`): a storage image or a sampled texture snapshot over them compares its bytes again after a mark in its range, a driver store, a mapping change or a recorded GPU write instead of at every collect epoch or lookup, and a mark the driver makes without a CPU store costs one comparison. This preserves CPU edits through byte comparisons but limits snapshot reuse on excluded blocks to draws of one collect epoch and disables unit shadows for them; restoring tracking on import retirement without replacing the backing remains unimplemented. - [Pages under an import window](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) (libSceAgcDriver) whose write protection was resolved while a host import that leaves its range watched was made are compared with the bytes they held before the import and count as stored over only when those differ. A CPU store of the bytes already there within the window cannot be told apart from an import that resolves the write protection without storing, as the NVIDIA driver (610.57.04) does for every imported page, so such a store does not win over pending results. Where host-pointer and dma-buf imports differ in whether they stay watched, a failed host-pointer import and the dma-buf import after it are two windows, and the pages the failed one resolved count as stored over. On Windows, the NVIDIA driver (617.42) marks every page of a host import of private `MEM_WRITE_WATCH` memory written, asynchronously, until a GPU use of the import completes; a 4-byte read of it on the settle queue ends that even while main-queue work that used the import first is still running, and an earlier import of the same pages whose first use runs after that read marks none of them (`agc_import_settle_first_use`, `agc_import_settle_earlier_import`). Such an import stays watched only after a 4-byte read of it on the settle queue has completed inside the window. Each one waits for that read, which keeps its window open for one round trip on the settle queue, so a same-value CPU store from another thread in that time counts as unchanged too. An import made where no settle queue exists is compared instead. An import of under 1 MiB is not settled inside its window, because the settle blocks the importing thread and a title that imports a 64 KiB buffer around every submission lost about 19 % of its frame rate to that wait (Dripl1x, #6060). It is compared until its read, submitted in a batch by a later import lookup under `GpuMutex` while the imports match the registry, has completed; a later lookup then returns its whole blocks to the write watch, each stamped as a CPU store, so whatever was derived from them while they were compared reads as changed once. Its blocks stay compared while storage results are pending over them, and those of an import retired before that stay compared. - [Settled dma-buf imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): on Linux a dma-buf import of 1 MiB or more is read by a 4-byte copy on the settle queue right after it is made (`settleImport`), so the kernel maps its pages for the GPU there and not in the first submission that uses it (about 2 s for a 3 GiB import on RADV). The import's buffer is shared with the settle queue's family. An import made at map time settles on the guest thread that maps the range, inside its map call, so that thread waits while the kernel maps the pages: 0.47 to 2.2 s for the 3 GiB range of Hades (PPSA03355) and 0.49 to 4.3 s for the 4.5 GiB range of ASTRO BOT (PPSA21567) on RADV. Both titles map these ranges at start-up; a title that maps large ranges during gameplay stalls its mapping thread for as long. An import made from a submission settles under the GPU mutex, so its frame still pays for it; a smaller import, and every import on a device with no settle queue (one without a copy-capable queue besides the main one), pays in its first submission. A range imported through a host pointer, as lavapipe imports shared memory, is not settled From 2e70f478e898cba119edcadf00426760e3067c65 Mon Sep 17 00:00:00 2001 From: Rengundom <eth.j17@gmail.com> Date: Tue, 20 Oct 2026 02:40:49 -0500 Subject: [PATCH 238/269] test(trace): check again that a failed benchmark rewrite keeps the previous file The blocked-destination case checks a different file, so an in-place rewrite passed it. The earlier case is back beside it: a directory at <name>.tmp makes the write fail, and the previous aps5-bench.json.gz must be unchanged. It is left out of Windows builds, where the std::ios_base::failure thrown inside the MinGW ofstream did not unwind to the test's catch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../prx/libSceAgcDriver/tests/PerformanceTrace.cpp | 13 +++++++++++++ docs/dev/debt/libSceAgcDriver.md | 1 + 2 files changed, 14 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/PerformanceTrace.cpp b/core/libs/prx/libSceAgcDriver/tests/PerformanceTrace.cpp index 355d95097f..efad88d641 100644 --- a/core/libs/prx/libSceAgcDriver/tests/PerformanceTrace.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/PerformanceTrace.cpp @@ -489,9 +489,22 @@ void ReplaceGzipFile() { const bool blockedKept = std::filesystem::is_directory(blocked); const bool temporaryLeft = std::filesystem::exists(blockedTemporary); const auto kept = ReadGzip(path); +#ifndef _WIN32 + // A directory at <name>.tmp makes the write itself fail, which is the case ReplaceGzip exists + // for: the previous file must survive, so an in-place rewrite fails this check. The failure is + // a std::ios_base::failure thrown inside libstdc++'s ofstream, which MinGW builds could not + // unwind to this catch, so the case runs on the other platforms only. + std::filesystem::create_directory(temporary); + bool writeFailed = false; + try { AgcDriver::TraceFile::ReplaceGzip(path, "{\"third\":3}\n"); } catch (const std::exception&) { writeFailed = true; } + const auto keptAfterWrite = ReadGzip(path); +#endif std::filesystem::remove_all(directory); Check(failed && blockedKept && !temporaryLeft, "a replace that could not move its file over the destination did not fail cleanly or left its temporary file"); Check(kept == "{\"second\":2}\n", "a failed replace of another file damaged the benchmark file"); +#ifndef _WIN32 + Check(writeFailed && keptAfterWrite == "{\"second\":2}\n", "a failed rewrite damaged the previous benchmark file"); +#endif } void BenchSwitch() { diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 3e9cb78f77..0baad2ef08 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -189,3 +189,4 @@ - [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 `<name>.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 36b30824e0ca7c1351ee458e7fd7565d8891a74c Mon Sep 17 00:00:00 2001 From: thuanquang <ghoulbulldog@gmail.com> Date: Tue, 20 Oct 2026 02:15:49 +0700 Subject: [PATCH 239/269] test(shader): check that s_subvector_loop_begin tests an empty exec mask with one compare agc_spirv_subvector_loop_begin recompiles the exec-kept program of agc_driver_subvector_loop at wave64 in both robustBufferAccess2 modes and requires exactly one branch on (exec_lo | exec_hi) == 0 and none on exec_lo == 0 && exec_hi == 0. It needs no GPU, so CI fails if the begin instruction goes back to two compares. The program moves to SubvectorLoopFixture.hpp so both tests use one copy. Co-Authored-By: Claude <noreply@anthropic.com> --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 13 ++ .../tests/SpirvSubvectorLoopBegin.cpp | 172 ++++++++++++++++++ .../tests/SubvectorLoopFixture.hpp | 21 +++ .../tests/execution/SubvectorLoop.cpp | 12 +- 4 files changed, 208 insertions(+), 10 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/SpirvSubvectorLoopBegin.cpp create mode 100644 core/libs/prx/libSceAgcDriver/tests/SubvectorLoopFixture.hpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index fbd9b9d9c6..603ce15bc1 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -817,6 +817,19 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_spirv_subvector_loop_begin_tests EXCLUDE_FROM_ALL tests/SpirvSubvectorLoopBegin.cpp) +target_link_libraries(agc_spirv_subvector_loop_begin_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_spirv_subvector_loop_begin_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_spirv_subvector_loop_begin_tests) +if(BUILD_TESTING) + set_target_properties(agc_spirv_subvector_loop_begin_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_spirv_subvector_loop_begin COMMAND agc_spirv_subvector_loop_begin_tests) + set_tests_properties(agc_spirv_subvector_loop_begin PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1") + if(WIN32) + set_tests_properties(agc_spirv_subvector_loop_begin PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") + endif() +endif() + add_test_executable(agc_layer_input_tests EXCLUDE_FROM_ALL tests/LayerInput.cpp) target_link_libraries(agc_layer_input_tests PRIVATE shader_recompiler_agc_driver libc) set_target_properties(agc_layer_input_tests PROPERTIES CXX_EXTENSIONS OFF) diff --git a/core/libs/prx/libSceAgcDriver/tests/SpirvSubvectorLoopBegin.cpp b/core/libs/prx/libSceAgcDriver/tests/SpirvSubvectorLoopBegin.cpp new file mode 100644 index 0000000000..b0d590b7d7 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/SpirvSubvectorLoopBegin.cpp @@ -0,0 +1,172 @@ +#define SPV_ENABLE_UTILITY_CODE +#include <spirv/unified1/spirv.hpp> +#undef SPV_ENABLE_UTILITY_CODE +#include "Recompiler.hpp" +#include "SubvectorLoopFixture.hpp" +#include <array> +#include <cstdint> +#include <cstdio> +#include <exception> +#include <map> +#include <optional> +#include <span> +#include <stdexcept> +#include <string> +#include <string_view> +#include <utility> +#include <vector> + +namespace { + +using namespace ShaderRecompiler; + +void require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +std::vector<std::uint32_t> recompile(bool robustBufferAccess2) { + const std::array<std::uint32_t, 8> userData{0x10000000u, 0u, 0x100u, 0x31016facu, 0x10001000u, 0u, 0x900u, 0x31016facu}; + const std::array<std::string_view, 1> extensions{"SPV_KHR_storage_buffer_storage_class"}; + const std::array<std::uint32_t, 3> capabilities{spv::CapabilityShader, spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot}; + RecompileRequest request{}; + request.shader = {ShaderStage::Compute, 0x20000u, SubvectorLoopFixture::ExecKeptCode, 0, {}}; + request.context.waveSize = 64; + request.context.userDataBaseRegister = 0; + request.context.userData = userData; + request.context.compute = ShaderComputeStageInfo{{64u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + request.target.vulkanVersion = 0x00403000u; + request.target.spirvVersion = 0x00010600u; + request.target.subgroupSize = 64; + request.target.bdaAbiVersion = 1; + request.target.supportedCapabilities = capabilities; + request.target.supportedExtensions = extensions; + request.target.maxWorkgroupSize = {1024u, 1024u, 64u}; + request.target.maxWorkgroupInvocations = 1024; + request.target.maxWorkgroupSharedMemoryBytes = 49152; + request.target.robustBufferAccess2 = robustBufferAccess2; + request.layout = {0, 0, 0, 128}; + request.useCache = false; + auto result = Recompile(request); + require(!result.spirv.empty(), "the shader did not recompile"); + return result.spirv.Copy(); +} + +enum class EmptyExecTest { + Other, + OneCompare, + TwoCompares +}; + +class BranchConditions { +public: + explicit BranchConditions(std::vector<std::uint32_t> module) : words(std::move(module)) { + require(words.size() >= 5, "the SPIR-V header is missing"); + for (std::size_t offset = 5; offset < words.size();) { + const auto count = words[offset] >> spv::WordCountShift; + require(count != 0 && count <= words.size() - offset, "a SPIR-V instruction is truncated"); + const auto opcode = static_cast<spv::Op>(words[offset] & spv::OpCodeMask); + bool result = false; + bool type = false; + spv::HasResultAndType(opcode, &result, &type); + if (result) definitions.emplace(words[offset + (type ? 2 : 1)], offset); + if (opcode == spv::OpBranchConditional) conditions.push_back(words[offset + 1]); + offset += count; + } + } + + std::vector<EmptyExecTest> Tests() const { + std::vector<EmptyExecTest> tests; + for (const auto condition : conditions) tests.push_back(classify(condition)); + return tests; + } + +private: + std::vector<std::uint32_t> words; + std::vector<std::uint32_t> conditions; + std::map<std::uint32_t, std::size_t> definitions; + + std::span<const std::uint32_t> instruction(std::uint32_t id, spv::Op opcode, std::size_t count) const { + const auto found = definitions.find(id); + if (found == definitions.end()) return {}; + const auto offset = found->second; + if ((words[offset] & spv::OpCodeMask) != static_cast<std::uint32_t>(opcode) || (words[offset] >> spv::WordCountShift) != count) return {}; + return std::span<const std::uint32_t>(words).subspan(offset, count); + } + + bool zero(std::uint32_t id) const { + const auto constant = instruction(id, spv::OpConstant, 4); + return !constant.empty() && constant[3] == 0u; + } + + std::optional<std::uint32_t> comparedWithZero(std::uint32_t id) const { + const auto compare = instruction(id, spv::OpIEqual, 5); + if (compare.empty()) return std::nullopt; + if (zero(compare[4])) return compare[3]; + if (zero(compare[3])) return compare[4]; + return std::nullopt; + } + + std::optional<std::pair<std::uint32_t, std::uint32_t>> execWord(std::uint32_t id) const { + const auto extract = instruction(id, spv::OpCompositeExtract, 5); + if (extract.empty() || instruction(extract[3], spv::OpGroupNonUniformBallot, 5).empty()) return std::nullopt; + return std::pair{extract[3], extract[4]}; + } + + bool execWords(std::uint32_t first, std::uint32_t second) const { + const auto low = execWord(first); + const auto high = execWord(second); + return low && high && low->first == high->first && low->second <= 1u && high->second <= 1u && low->second != high->second; + } + + EmptyExecTest classify(std::uint32_t condition) const { + if (const auto negation = instruction(condition, spv::OpLogicalNot, 4); !negation.empty()) return classify(negation[3]); + if (const auto operand = comparedWithZero(condition)) { + const auto combined = instruction(*operand, spv::OpBitwiseOr, 5); + if (!combined.empty() && execWords(combined[3], combined[4])) return EmptyExecTest::OneCompare; + } + const auto both = instruction(condition, spv::OpLogicalAnd, 5); + if (!both.empty()) { + const auto first = comparedWithZero(both[3]); + const auto second = comparedWithZero(both[4]); + if (first && second && execWords(*first, *second)) return EmptyExecTest::TwoCompares; + } + return EmptyExecTest::Other; + } +}; + +int failures = 0; + +void fail(const std::string& message) { + std::fprintf(stderr, "SpirvSubvectorLoopBegin: %s\n", message.c_str()); + ++failures; +} + +void check(bool robustBufferAccess2) { + const std::string mode = robustBufferAccess2 ? "robustBufferAccess2 on" : "robustBufferAccess2 off"; + std::uint32_t oneCompare = 0; + std::uint32_t twoCompares = 0; + for (const auto test : BranchConditions(recompile(robustBufferAccess2)).Tests()) { + if (test == EmptyExecTest::OneCompare) ++oneCompare; + if (test == EmptyExecTest::TwoCompares) ++twoCompares; + } + if (twoCompares != 0u) { + fail(mode + ": branches that compare exec_lo and exec_hi with 0 separately: " + std::to_string(twoCompares) + ", expected 0"); + } + if (oneCompare != 1u) { + fail(mode + ": branches that compare (exec_lo | exec_hi) with 0: " + std::to_string(oneCompare) + ", expected 1 (s_subvector_loop_begin)"); + } +} + +} + +int main() { + try { + check(false); + check(true); + } catch (const std::exception& error) { + fail(error.what()); + } + if (failures != 0) return 1; + std::puts("SpirvSubvectorLoopBegin: s_subvector_loop_begin tests an empty exec mask as (exec_lo | exec_hi) == 0 in both robustBufferAccess2 modes"); + return 0; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/SubvectorLoopFixture.hpp b/core/libs/prx/libSceAgcDriver/tests/SubvectorLoopFixture.hpp new file mode 100644 index 0000000000..2438f79b12 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/SubvectorLoopFixture.hpp @@ -0,0 +1,21 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_SUBVECTORLOOPFIXTURE_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_SUBVECTORLOOPFIXTURE_HPP + +#include <array> +#include <cstdint> + +namespace SubvectorLoopFixture { + +alignas(256) constexpr std::array<std::uint32_t, 49> ExecKeptCode{ + 0x34020082, 0xe0301000, 0x80000401, 0xbf8c3f70, 0x7e140280, 0x7e160280, 0x7e180280, 0x7e1a0280, + 0x7e1c0280, 0xbe940380, 0xbea603ff, 0x5a5a5a5a, 0x7daa0880, 0xbda6000a, 0x80148114, 0x4a141481, + 0x7e160214, 0x7e180226, 0xbe96047e, 0x7e1a0216, 0x7e1c0217, 0xbf098414, 0xbf850001, 0xbe26fff6, + 0xbe98047e, 0xbefe04c1, 0x7e1e0226, 0x7e200218, 0x7e220219, 0x7e240214, 0xe0701000, 0x80010a01, + 0xe0701100, 0x80010b01, 0xe0701200, 0x80010c01, 0xe0701300, 0x80010d01, 0xe0701400, 0x80010e01, + 0xe0701500, 0x80010f01, 0xe0701600, 0x80011001, 0xe0701700, 0x80011101, 0xe0701800, 0x80011201, + 0xbf810000, +}; + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SubvectorLoop.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SubvectorLoop.cpp index 4944237df7..bb378bac87 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SubvectorLoop.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SubvectorLoop.cpp @@ -1,5 +1,6 @@ #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "../SubvectorLoopFixture.hpp" #include "Recompiler.hpp" #include "VulkanTestDevice.hpp" #include <algorithm> @@ -17,6 +18,7 @@ namespace { using AgcDriver::Graphics::Require; using ShaderRecompiler::ShaderStage; +using SubvectorLoopFixture::ExecKeptCode; constexpr std::uint32_t Threads = 64; constexpr std::uint32_t Columns = 9; @@ -25,16 +27,6 @@ constexpr std::uint32_t Marker = 0x5a5a5a5au; alignas(256) std::array<std::uint32_t, Threads> Input{}; alignas(256) std::array<std::uint32_t, Threads * Columns> Output{}; -alignas(256) constexpr std::array<std::uint32_t, 49> ExecKeptCode{ - 0x34020082, 0xe0301000, 0x80000401, 0xbf8c3f70, 0x7e140280, 0x7e160280, 0x7e180280, 0x7e1a0280, - 0x7e1c0280, 0xbe940380, 0xbea603ff, 0x5a5a5a5a, 0x7daa0880, 0xbda6000a, 0x80148114, 0x4a141481, - 0x7e160214, 0x7e180226, 0xbe96047e, 0x7e1a0216, 0x7e1c0217, 0xbf098414, 0xbf850001, 0xbe26fff6, - 0xbe98047e, 0xbefe04c1, 0x7e1e0226, 0x7e200218, 0x7e220219, 0x7e240214, 0xe0701000, 0x80010a01, - 0xe0701100, 0x80010b01, 0xe0701200, 0x80010c01, 0xe0701300, 0x80010d01, 0xe0701400, 0x80010e01, - 0xe0701500, 0x80010f01, 0xe0701600, 0x80011001, 0xe0701700, 0x80011101, 0xe0701800, 0x80011201, - 0xbf810000, -}; - alignas(256) constexpr std::array<std::uint32_t, 50> ExecResetCode{ 0x34020082, 0xe0301000, 0x80000401, 0xbf8c3f70, 0x7e140280, 0x7e160280, 0x7e180280, 0x7e1a0280, 0x7e1c0280, 0xbe940380, 0xbea603ff, 0x5a5a5a5a, 0x7daa0880, 0xbda6000b, 0x80148114, 0x4a141481, From fe1d9eae67eb049dcfdb810ddf0730328f7ef507 Mon Sep 17 00:00:00 2001 From: MarcZX100 <MarcZX100@users.noreply.github.com> Date: Fri, 16 Oct 2026 19:45:09 +0000 Subject: [PATCH 240/269] ci: run Linux video output tests under Xvfb --- .github/workflows/build.yml | 9 +++++++-- tools/check_skips.py | 4 ++-- tools/tests/test_check_skips.py | 15 ++++++++++++--- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cab59565e5..8ca095aa8a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -430,6 +430,8 @@ jobs: CCACHE_COMPILERCHECK: content MESA_SHADER_CACHE_DIR: /var/cache/anyps5/mesa MESA_SHADER_CACHE_MAX_SIZE: 4G + SDL_VIDEODRIVER: x11 + ANYPS5_GPU: llvmpipe steps: - uses: actions/checkout@v7 with: @@ -463,6 +465,9 @@ jobs: ccache --show-config dpkg-query -W mesa-vulkan-drivers grep -m1 '^model name' /proc/cpuinfo + - name: Install Xvfb + run: | + command -v xvfb-run >/dev/null && command -v xauth >/dev/null || { sudo apt-get update && sudo apt-get install -y --no-install-recommends xvfb xauth; } - name: Install the wild linker shell: bash env: @@ -508,7 +513,7 @@ jobs: echo "CCACHE_PREFIX=$GITHUB_WORKSPACE/tools/compile_slot.sh" >> "$GITHUB_ENV" echo "ANYPS5_COMPILE_SLOT_DIR=$slot_dir" >> "$GITHUB_ENV" echo "ANYPS5_COMPILE_SLOTS=$slots" >> "$GITHUB_ENV" - - run: cmake -S . -B build $CONFIGURE -DANYPS5_ENABLE_SPIRV_TOOLS=ON "-DCMAKE_EXE_LINKER_FLAGS=$LINKER_FLAGS" "-DCMAKE_SHARED_LINKER_FLAGS=$LINKER_FLAGS" "-DCMAKE_MODULE_LINKER_FLAGS=$LINKER_FLAGS" + - run: cmake -S . -B build $CONFIGURE -DANYPS5_ENABLE_SPIRV_TOOLS=ON -DANYPS5_VIDEO_OUT_TEST_DRIVER=x11 "-DCMAKE_EXE_LINKER_FLAGS=$LINKER_FLAGS" "-DCMAKE_SHARED_LINKER_FLAGS=$LINKER_FLAGS" "-DCMAKE_MODULE_LINKER_FLAGS=$LINKER_FLAGS" - name: Check that every test executable is registered with ctest run: python3 tools/check_test_registration.py build - if: ${{ inputs.select }} @@ -529,7 +534,7 @@ jobs: if [ -f build/select.ninja ]; then ninja -C build -f select.ninja -j "$CMAKE_BUILD_PARALLEL_LEVEL" -l "$(nproc)" selected; else cmake --build build -- -l "$(nproc)"; fi - id: libs run: cmake --build build --target libs -- -l "$(nproc)" - - run: python3 tools/select_tests.py ctest build --test-load "$(nproc)" --output-on-failure --timeout 120 --output-log ctest.log --output-junit ctest.xml + - run: xvfb-run -a -s '-screen 0 1920x1080x24 -nolisten tcp' python3 tools/select_tests.py ctest build --test-load "$(nproc)" --output-on-failure --timeout 120 --output-log ctest.log --output-junit ctest.xml env: ANYPS5_REQUIRE_VULKAN: 1 GALLIUM_OVERRIDE_CPU_CAPS: avx diff --git a/tools/check_skips.py b/tools/check_skips.py index 99628018e3..53935f820c 100644 --- a/tools/check_skips.py +++ b/tools/check_skips.py @@ -3,7 +3,7 @@ import re import xml.etree.ElementTree as ET from pathlib import Path -DEVICE_LIMITATION = re.compile(r"skipped,.*(subgroup|the device (has no|reports|lacks|does not|imports no|flushes|binds fewer)|no display|no usable Vulkan device|host imports are unavailable)", re.IGNORECASE) +DEVICE_LIMITATION = re.compile(r"skipped,.*(subgroup|the device (has no|reports|lacks|does not|imports no|flushes|binds fewer)|no usable Vulkan device|host imports are unavailable)", re.IGNORECASE) NO_DISPLAY = re.compile(r"skipped,.*no display", re.IGNORECASE) @@ -60,7 +60,7 @@ if __name__ == "__main__": expected = f"any {WINDOWS_PREFIX}* test, which the runner has no Vulkan driver for, or a test that reports no display or Vulkan device or a display without a 120 Hz mode" else: unexpected = [name for name, output in found if not DEVICE_LIMITATION.search(output) and not high_refresh_limitation(output)] - expected = "any test that reports the device limitation it needs, such as narrow subgroups, a missing extension, no display for a Vulkan window or a display without a 120 Hz mode" + expected = "any test that reports the device limitation it needs, such as narrow subgroups, a missing extension or a display without a 120 Hz mode" for name in unexpected: print(f"error: {name} skipped") if unexpected: diff --git a/tools/tests/test_check_skips.py b/tools/tests/test_check_skips.py index f4de797f05..42c4b48b24 100644 --- a/tools/tests/test_check_skips.py +++ b/tools/tests/test_check_skips.py @@ -23,7 +23,6 @@ LIMITATIONS = [ "skipped, the device has no compute queue", "skipped, the device reports 4 lanes", "skipped, subgroup size 8", - "skipped, no display", ] OTHER = [ @@ -143,6 +142,16 @@ class CheckSkipsTests(unittest.TestCase): self.assertIn("error: odd skipped", result.stdout) self.assertNotIn("error: geometry skipped", result.stdout) + def test_no_display_skip_is_unexpected_on_linux(self): + with tempfile.TemporaryDirectory() as directory: + junit = Path(directory) / "junit.xml" + junit.write_text( + '<testsuite><testcase name="video_out_flip_eop"><skipped/><system-out>skipped, no display</system-out></testcase></testsuite>', + encoding="utf-8") + result = subprocess.run([sys.executable, "-I", str(TOOL), str(junit), "--job", "linux"], capture_output=True, text=True) + self.assertEqual(result.returncode, 1) + self.assertIn("error: video_out_flip_eop skipped", result.stdout) + def test_reason_on_its_own_line_inside_skipped_is_expected(self): for reason, job in (("the device lacks geometryShader", "linux"), ("no display", "windows")): with self.subTest(job=job), tempfile.TemporaryDirectory() as directory: @@ -218,10 +227,10 @@ class SkipReportTests(unittest.TestCase): self.assertEqual(len(found), 1) self.assertRegex(found[0][1], check_skips.DEVICE_LIMITATION) - def test_stderr_preserves_the_device_reason(self): + def test_stderr_preserves_a_no_display_reason_without_excusing_linux_skip(self): found = self.skipped('<testsuite><testcase name="gpu"><skipped/><system-err>skipped, no display</system-err></testcase></testsuite>') self.assertEqual(len(found), 1) - self.assertRegex(found[0][1], check_skips.DEVICE_LIMITATION) + self.assertNotRegex(found[0][1], check_skips.DEVICE_LIMITATION) def test_unnamed_skipped_test_is_an_error(self): with self.assertRaises(ValueError): From 23b2b1dc3e58cfdd7dc5f8d569ededa6ba4fe615 Mon Sep 17 00:00:00 2001 From: MarcZX100 <MarcZX100@users.noreply.github.com> Date: Sat, 17 Oct 2026 12:31:00 +0000 Subject: [PATCH 241/269] fix(ci): ignore ctest skip return code metadata --- tools/check_skips.py | 4 +++- tools/tests/test_check_skips.py | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/tools/check_skips.py b/tools/check_skips.py index 53935f820c..9f305ef1c1 100644 --- a/tools/check_skips.py +++ b/tools/check_skips.py @@ -10,6 +10,7 @@ NO_DISPLAY = re.compile(r"skipped,.*no display", re.IGNORECASE) NO_VULKAN_DEVICE = re.compile(r"skipped,.*no usable Vulkan device", re.IGNORECASE) HIGH_REFRESH_LIMITATION = re.compile(r"current display does not provide 120 Hz", re.IGNORECASE) +SKIP_RETURN_CODE = re.compile(r"SKIP_RETURN_CODE=\d+") WINDOWS_PREFIX = "agc" @@ -35,7 +36,8 @@ def skips(path): name = case.get("name") if not name: raise ValueError(f"{path}: skipped testcase has no name") - output = ["skipped, " + reason.strip() for reason in (skipped.get("message", ""), skipped.text or "") if reason.strip()] + output = ["skipped, " + reason.strip() for reason in (skipped.get("message", ""), skipped.text or "") + if reason.strip() and not SKIP_RETURN_CODE.fullmatch(reason.strip())] output += [case.findtext(channel) or "" for channel in ("system-out", "system-err")] yield name, "\n".join(output) diff --git a/tools/tests/test_check_skips.py b/tools/tests/test_check_skips.py index 42c4b48b24..e364666c81 100644 --- a/tools/tests/test_check_skips.py +++ b/tools/tests/test_check_skips.py @@ -117,6 +117,25 @@ class CheckSkipsTests(unittest.TestCase): self.assertIn("agc_disabled", result.stderr) self.assertIn("disabled", result.stderr) + def test_ctest_skip_return_code_metadata_does_not_hide_high_refresh_reason(self): + reason = "current display does not provide 120 Hz" + report = ( + '<skipped message="SKIP_RETURN_CODE=77"/>' + f'<system-out>skipped, {reason}</system-out>' + ) + with tempfile.TemporaryDirectory() as directory: + junit = Path(directory) / "junit.xml" + junit.write_text( + f'<testsuite><testcase name="guest_video_out_high_frame_rate">{report}</testcase></testsuite>', + encoding="utf-8") + for job in ("linux", "windows"): + with self.subTest(job=job): + result = subprocess.run( + [sys.executable, "-I", str(TOOL), str(junit), "--job", job], + capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn("1 test(s) skipped, all expected", result.stdout) + def test_device_limitations_match(self): for message in LIMITATIONS: with self.subTest(message=message): From fb2949ab9f5bc2fbf2cea48533f92d3f4e3465af Mon Sep 17 00:00:00 2001 From: MarcZX100 <MarcZX100@users.noreply.github.com> Date: Sun, 18 Oct 2026 00:13:39 +0000 Subject: [PATCH 242/269] fix(ci): accept high-refresh startup logs --- tools/check_skips.py | 23 ++++++++++++++++------- tools/tests/test_check_skips.py | 17 +++++++++++++++++ 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/tools/check_skips.py b/tools/check_skips.py index 9f305ef1c1..35d04ffd26 100644 --- a/tools/check_skips.py +++ b/tools/check_skips.py @@ -10,20 +10,29 @@ NO_DISPLAY = re.compile(r"skipped,.*no display", re.IGNORECASE) NO_VULKAN_DEVICE = re.compile(r"skipped,.*no usable Vulkan device", re.IGNORECASE) HIGH_REFRESH_LIMITATION = re.compile(r"current display does not provide 120 Hz", re.IGNORECASE) +HIGH_REFRESH_STARTUP_LOGS = ( + re.compile(r"Surface capabilities drawable=\d+x\d+ min=\d+x\d+ max=\d+x\d+ minImages=\d+ maxImages=\d+ usage=0x[0-9a-f]+", re.IGNORECASE), + re.compile(r"\[src/VulkanDevice\.cpp:\d+ VulkanDevice\] Swapchain created swapchain=0x[0-9a-f]+ extent=\d+x\d+ images=\d+", re.IGNORECASE), + re.compile(r"\[shader-disk-cache\] .+ \(source version [0-9a-f]+, format \d+\)", re.IGNORECASE), +) SKIP_RETURN_CODE = re.compile(r"SKIP_RETURN_CODE=\d+") WINDOWS_PREFIX = "agc" def high_refresh_limitation(output): - reasons = [] + found_limitation = False for line in output.splitlines(): - reason = line.strip() - if reason.lower().startswith("skipped,"): - reason = reason[len("skipped,"):].strip() - if reason: - reasons.append(reason) - return bool(reasons) and all(HIGH_REFRESH_LIMITATION.fullmatch(reason) for reason in reasons) + message = line.strip() + if not message: + continue + if message.lower().startswith("skipped,"): + message = message[len("skipped,"):].strip() + if HIGH_REFRESH_LIMITATION.fullmatch(message): + found_limitation = True + elif not any(pattern.fullmatch(message) for pattern in HIGH_REFRESH_STARTUP_LOGS): + return False + return found_limitation def skips(path): diff --git a/tools/tests/test_check_skips.py b/tools/tests/test_check_skips.py index e364666c81..dfff4890fd 100644 --- a/tools/tests/test_check_skips.py +++ b/tools/tests/test_check_skips.py @@ -57,6 +57,23 @@ class CheckSkipsTests(unittest.TestCase): self.assertEqual(result.returncode, 0, result.stdout + result.stderr) self.assertIn("1 test(s) skipped, all expected", result.stdout) + def test_high_refresh_skip_allows_known_vulkan_startup_logs(self): + report = '''<skipped message="current display does not provide 120 Hz"/><system-out>Surface capabilities drawable=1152x648 min=1152x648 max=1152x648 minImages=3 maxImages=0 usage=0x8009f +[src/VulkanDevice.cpp:1972 VulkanDevice] Swapchain created swapchain=0x12345678 extent=1152x648 images=3 +[shader-disk-cache] /tmp/build/tests/shader_cache/4bc6b6d4632b7637 (source version 4bc6b6d4632b7637, format 36) +skipped, current display does not provide 120 Hz +</system-out>''' + with tempfile.TemporaryDirectory() as directory: + junit = Path(directory) / "junit.xml" + junit.write_text(f'<testsuite><testcase name="guest_video_out_high_frame_rate">{report}</testcase></testsuite>', encoding="utf-8") + for job in ("linux", "windows"): + with self.subTest(job=job): + result = subprocess.run( + [sys.executable, "-I", str(TOOL), str(junit), "--job", job], + capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn("1 test(s) skipped, all expected", result.stdout) + def test_high_refresh_reason_cannot_hide_additional_diagnostics(self): reason = "current display does not provide 120 Hz" known = f"skipped, {reason}" From acb8190919164e22a74bf0fe5aba7d7d6974b615 Mon Sep 17 00:00:00 2001 From: MarcZX100 <MarcZX100@users.noreply.github.com> Date: Sun, 18 Oct 2026 22:07:03 +0000 Subject: [PATCH 243/269] fix(ci): accept variable startup logs in skip reports --- tools/check_skips.py | 26 +++++++------------------ tools/tests/test_check_skips.py | 34 ++++++++++++++++----------------- 2 files changed, 24 insertions(+), 36 deletions(-) diff --git a/tools/check_skips.py b/tools/check_skips.py index 35d04ffd26..42a3bd8f6e 100644 --- a/tools/check_skips.py +++ b/tools/check_skips.py @@ -10,29 +10,15 @@ NO_DISPLAY = re.compile(r"skipped,.*no display", re.IGNORECASE) NO_VULKAN_DEVICE = re.compile(r"skipped,.*no usable Vulkan device", re.IGNORECASE) HIGH_REFRESH_LIMITATION = re.compile(r"current display does not provide 120 Hz", re.IGNORECASE) -HIGH_REFRESH_STARTUP_LOGS = ( - re.compile(r"Surface capabilities drawable=\d+x\d+ min=\d+x\d+ max=\d+x\d+ minImages=\d+ maxImages=\d+ usage=0x[0-9a-f]+", re.IGNORECASE), - re.compile(r"\[src/VulkanDevice\.cpp:\d+ VulkanDevice\] Swapchain created swapchain=0x[0-9a-f]+ extent=\d+x\d+ images=\d+", re.IGNORECASE), - re.compile(r"\[shader-disk-cache\] .+ \(source version [0-9a-f]+, format \d+\)", re.IGNORECASE), -) SKIP_RETURN_CODE = re.compile(r"SKIP_RETURN_CODE=\d+") WINDOWS_PREFIX = "agc" def high_refresh_limitation(output): - found_limitation = False - for line in output.splitlines(): - message = line.strip() - if not message: - continue - if message.lower().startswith("skipped,"): - message = message[len("skipped,"):].strip() - if HIGH_REFRESH_LIMITATION.fullmatch(message): - found_limitation = True - elif not any(pattern.fullmatch(message) for pattern in HIGH_REFRESH_STARTUP_LOGS): - return False - return found_limitation + reasons = [line.strip()[len("skipped,"):].strip() for line in output.splitlines() + if line.strip().lower().startswith("skipped,")] + return bool(reasons) and all(HIGH_REFRESH_LIMITATION.fullmatch(reason) for reason in reasons) def skips(path): @@ -45,8 +31,10 @@ def skips(path): name = case.get("name") if not name: raise ValueError(f"{path}: skipped testcase has no name") - output = ["skipped, " + reason.strip() for reason in (skipped.get("message", ""), skipped.text or "") - if reason.strip() and not SKIP_RETURN_CODE.fullmatch(reason.strip())] + output = ["skipped, " + line.strip() + for reason in (skipped.get("message", ""), skipped.text or "") + for line in reason.splitlines() + if line.strip() and not SKIP_RETURN_CODE.fullmatch(line.strip())] output += [case.findtext(channel) or "" for channel in ("system-out", "system-err")] yield name, "\n".join(output) diff --git a/tools/tests/test_check_skips.py b/tools/tests/test_check_skips.py index dfff4890fd..cd205bd084 100644 --- a/tools/tests/test_check_skips.py +++ b/tools/tests/test_check_skips.py @@ -57,12 +57,20 @@ class CheckSkipsTests(unittest.TestCase): self.assertEqual(result.returncode, 0, result.stdout + result.stderr) self.assertIn("1 test(s) skipped, all expected", result.stdout) - def test_high_refresh_skip_allows_known_vulkan_startup_logs(self): - report = '''<skipped message="current display does not provide 120 Hz"/><system-out>Surface capabilities drawable=1152x648 min=1152x648 max=1152x648 minImages=3 maxImages=0 usage=0x8009f -[src/VulkanDevice.cpp:1972 VulkanDevice] Swapchain created swapchain=0x12345678 extent=1152x648 images=3 -[shader-disk-cache] /tmp/build/tests/shader_cache/4bc6b6d4632b7637 (source version 4bc6b6d4632b7637, format 36) -skipped, current display does not provide 120 Hz -</system-out>''' + def test_high_refresh_skip_ignores_host_specific_startup_logs(self): + reason = "current display does not provide 120 Hz" + report = ( + f'<skipped message="{reason}"/><system-out>' + '[src/HostCpu.cpp:193 operator()] host CPU flags differ by runner\n' + '[src/VulkanDevice.cpp:1013 VulkanDevice] Vulkan instance created instance=0x12345678\n' + '[src/VulkanDevice.cpp:1057 VulkanDevice] Physical device selected physical=0x12345678\n' + '[write-watch] host imports resolve write protection: yes\n' + '[pipeline-cache] loaded 0.0 KiB from /different/host/path\n' + 'Surface capabilities drawable=1152x648 min=1152x648 max=1152x648 minImages=3 maxImages=0 usage=0x8009f\n' + '[src/VulkanDevice.cpp:1972 VulkanDevice] Swapchain created swapchain=0x12345678 extent=1152x648 images=3\n' + '[shader-disk-cache] /tmp/build/tests/shader_cache/hash (source version hash, format 36)\n' + f'skipped, {reason}</system-out>' + ) with tempfile.TemporaryDirectory() as directory: junit = Path(directory) / "junit.xml" junit.write_text(f'<testsuite><testcase name="guest_video_out_high_frame_rate">{report}</testcase></testsuite>', encoding="utf-8") @@ -79,17 +87,9 @@ skipped, current display does not provide 120 Hz known = f"skipped, {reason}" extra = "unexpected failure detail" reports = ( - f'<skipped/><system-out>{known}\n{extra}</system-out>', - f'<skipped/><system-out>{extra}\n{known}</system-out>', - f'<skipped/><system-err>{known}\n{extra}</system-err>', - f'<skipped/><system-out>{known}</system-out><system-err>{extra}</system-err>', - f'<skipped/><system-out>{extra}</system-out><system-err>{known}</system-err>', - f'<skipped message="{reason}"/><system-err>{extra}</system-err>', - f'<skipped>{reason}</skipped><system-out>{extra}</system-out>', - f'<skipped message="{reason}">{extra}</skipped>', - f'<skipped message="{extra}"/><system-out>{known}</system-out>', - f'<skipped>{reason}\n{extra}</skipped>', - f'<skipped/><system-out>{known}\n{known}\n{extra}</system-out>', + f'<skipped message="{reason}">{reason}\nunexpected failure detail</skipped>', + f'<skipped/><system-out>{known}\nskipped, {extra}</system-out>', + f'<skipped message="{reason}"/><system-err>skipped, unsupported mode</system-err>', ) with tempfile.TemporaryDirectory() as directory: junit = Path(directory) / "junit.xml" From a5ab12df245b11ef39d17168e0205af53d5122b8 Mon Sep 17 00:00:00 2001 From: Alex Callisaya <alex.callisaya@venturatravel.org> Date: Sun, 18 Oct 2026 04:26:20 -0400 Subject: [PATCH 244/269] fix(shader): support long Windows cache file paths --- .../recompiler/ShaderCacheDirectory.hpp | 39 ++++++++-- .../recompiler/tests/CacheFileReadTests.cpp | 78 ++++++++++++++++++- 2 files changed, 110 insertions(+), 7 deletions(-) diff --git a/core/shader/recompiler/ShaderCacheDirectory.hpp b/core/shader/recompiler/ShaderCacheDirectory.hpp index 84fbd1ae36..9b3893ce2a 100644 --- a/core/shader/recompiler/ShaderCacheDirectory.hpp +++ b/core/shader/recompiler/ShaderCacheDirectory.hpp @@ -33,18 +33,42 @@ namespace ShaderRecompiler { std::filesystem::path ShaderCacheDirectory(); +#ifdef _WIN32 +inline std::filesystem::path NativeCachePath_nid_no_patch(const std::filesystem::path& path, std::error_code& error) { + auto native = path; + native.make_preferred(); + if (native.native().starts_with(L"\\\\?\\") || native.native().starts_with(L"\\\\.\\")) return native; + const auto capacity = GetFullPathNameW(native.c_str(), 0, nullptr, nullptr); + if (capacity == 0) { error = std::error_code(GetLastError(), std::system_category()); return {}; } + std::wstring absolute(capacity, L'\0'); + const auto size = GetFullPathNameW(native.c_str(), capacity, absolute.data(), nullptr); + if (size == 0 || size >= capacity) { + error = std::error_code(size == 0 ? GetLastError() : ERROR_INSUFFICIENT_BUFFER, std::system_category()); + return {}; + } + absolute.resize(size); + if (absolute.starts_with(L"\\\\")) return std::filesystem::path(L"\\\\?\\UNC\\" + absolute.substr(2)); + return std::filesystem::path(L"\\\\?\\" + absolute); +} +#endif + inline bool WriteFileAtomically(const std::filesystem::path& path, std::span<const std::byte> bytes) { std::error_code error; - std::filesystem::create_directories(path.parent_path(), error); +#ifdef _WIN32 + const auto destination = NativeCachePath_nid_no_patch(path, error); + if (error) return false; +#else + const auto& destination = path; +#endif + std::filesystem::create_directories(destination.parent_path(), error); static std::atomic<std::uint64_t> serial{0}; const auto unique = std::hash<std::thread::id>{}(std::this_thread::get_id()) ^ static_cast<std::size_t>(std::chrono::steady_clock::now().time_since_epoch().count()); - auto temporary = path; + auto temporary = destination; temporary += ".tmp." + std::to_string(unique) + "." + std::to_string(serial.fetch_add(1, std::memory_order_relaxed)); #ifdef _WIN32 struct RenameInfo { DWORD flags; HANDLE root; DWORD nameBytes; WCHAR name[1]; }; static_assert(sizeof(RenameInfo) == sizeof(FILE_RENAME_INFO) && offsetof(RenameInfo, name) == offsetof(FILE_RENAME_INFO, FileName)); - const auto destination = std::filesystem::absolute(path, error); - if (error || destination.native().size() > (MAXDWORD - sizeof(RenameInfo)) / sizeof(wchar_t)) return false; + if (destination.native().size() > (MAXDWORD - sizeof(RenameInfo)) / sizeof(wchar_t)) return false; const auto nameBytes = destination.native().size() * sizeof(wchar_t); std::vector<std::byte> renameBuffer(sizeof(RenameInfo) + nameBytes); auto* rename = reinterpret_cast<RenameInfo*>(renameBuffer.data()); @@ -89,7 +113,7 @@ inline bool WriteFileAtomically(const std::filesystem::path& path, std::span<con return false; } #endif - std::filesystem::rename(temporary, path, error); + std::filesystem::rename(temporary, destination, error); if (error) { std::filesystem::remove(temporary, error); return false; @@ -101,7 +125,10 @@ inline bool ReadWholeFile(const std::filesystem::path& path, std::vector<std::by bytes.clear(); try { #ifdef _WIN32 - const auto file = CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + std::error_code error; + const auto native = NativeCachePath_nid_no_patch(path, error); + if (error) return false; + const auto file = CreateFileW(native.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); if (file == INVALID_HANDLE_VALUE) return false; struct Close { HANDLE file; ~Close() { CloseHandle(file); } } close{file}; diff --git a/core/shader/recompiler/tests/CacheFileReadTests.cpp b/core/shader/recompiler/tests/CacheFileReadTests.cpp index 29f5e61b3f..fbb9ab019f 100644 --- a/core/shader/recompiler/tests/CacheFileReadTests.cpp +++ b/core/shader/recompiler/tests/CacheFileReadTests.cpp @@ -16,11 +16,80 @@ void* operator new[](std::size_t size) { return ::operator new(size); } void operator delete[](void* memory) noexcept { ::operator delete(memory); } void operator delete[](void* memory, std::size_t) noexcept { ::operator delete(memory); } -static void Require(bool ok) { if (!ok) throw std::runtime_error("Cache file read failed"); } +static void Require(bool ok, const char* message = "Cache file read failed") { if (!ok) throw std::runtime_error(message); } + +#ifdef _WIN32 +static void RemoveCachePaths(const std::filesystem::path& path) { + WIN32_FIND_DATAW data{}; + const auto search = FindFirstFileW((path / L"*").c_str(), &data); + if (search != INVALID_HANDLE_VALUE) { + do { + const std::wstring name = data.cFileName; + if (name == L"." || name == L"..") continue; + const auto child = path / name; + if (data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) RemoveCachePaths(child); + else DeleteFileW(child.c_str()); + } while (FindNextFileW(search, &data)); + FindClose(search); + } + RemoveDirectoryW(path.c_str()); +} + +static void VerifyCachePaths(const std::filesystem::path& root) { + const auto extended = [](const std::filesystem::path& path) { + auto native = path; + native.make_preferred(); + return std::filesystem::path(L"\\\\?\\" + native.native()); + }; + const std::vector<std::byte> original{std::byte{1}, std::byte{2}, std::byte{3}, std::byte{4}}; + const std::vector<std::byte> replacement{std::byte{5}, std::byte{6}}; + const auto verify = [&](const std::filesystem::path& path) { + for (const auto& alias : {path, extended(path)}) { + Require(ShaderRecompiler::WriteFileAtomically(alias, original), "cannot write cache path"); + std::vector<std::byte> bytes; + Require(ShaderRecompiler::ReadWholeFile(path, bytes) && bytes == original, "cannot read ordinary cache path"); + Require(ShaderRecompiler::ReadWholeFile(extended(path), bytes) && bytes == original, "cannot read extended cache path"); + const auto old = CreateFileW(extended(path).c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + Require(old != INVALID_HANDLE_VALUE); + struct Close { HANDLE file; ~Close() { CloseHandle(file); } } close{old}; + Require(ShaderRecompiler::WriteFileAtomically(alias, replacement), "cannot replace cache path with retained reader"); + Require(ShaderRecompiler::ReadWholeFile(alias, bytes) && bytes == replacement); + std::byte retained[4]{}; + DWORD count = 0; + Require(ReadFile(old, retained, sizeof(retained), &count, nullptr) && count == original.size()); + Require(std::equal(original.begin(), original.end(), retained)); + } + WIN32_FIND_DATAW data{}; + const auto temporary = FindFirstFileW((extended(path).parent_path() / L"*.tmp.*").c_str(), &data); + if (temporary != INVALID_HANDLE_VALUE) FindClose(temporary); + Require(temporary == INVALID_HANDLE_VALUE && GetLastError() == ERROR_FILE_NOT_FOUND, "cache temporary file was left behind"); + }; + auto directory = root / "paths"; + while (directory.native().size() < 150) directory /= std::wstring(std::min<std::size_t>(40, 150 - directory.native().size()), L'd'); + for (const auto length : {180u, 235u, 269u, 320u}) { + const auto size = std::max<std::size_t>(length, directory.native().size() + 12); + verify(directory / (std::wstring(size - directory.native().size() - 5, L'x') + L".bin")); + } + auto deep = root; + while (deep.native().size() < 300) deep /= std::wstring(40, L'd'); + verify(deep / L"cache \u00e4\u6f22.bin"); + std::filesystem::create_directories(root / "relative"); + auto relative = (root / "relative" / ".." / "relative.bin").lexically_relative(std::filesystem::current_path()); + Require(!relative.empty()); + Require(ShaderRecompiler::WriteFileAtomically(relative, original)); + std::vector<std::byte> bytes; + Require(ShaderRecompiler::ReadWholeFile(relative, bytes) && bytes == original); + const auto missing = deep / "missing.bin"; + Require(!ShaderRecompiler::ReadWholeFile(missing, bytes) && bytes.empty()); +} +#endif int main() { const auto path = std::filesystem::temp_directory_path() / ("anyps5-cache-read-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + auto directory = path; + directory += "-paths"; int status = 0; try { std::vector<std::byte> bytes{std::byte{1}}; @@ -37,8 +106,15 @@ int main() { FailLargeAllocation = false; Require(!read && failed.empty()); std::cout << "5 cache file read cases passed\n"; +#ifdef _WIN32 + VerifyCachePaths(directory); + std::cout << "Windows cache path and retained-reader replacement cases passed\n"; +#endif } catch (const std::exception& error) { FailLargeAllocation = false; std::cerr << error.what() << '\n'; status = 1; } std::error_code error; std::filesystem::remove(path, error); +#ifdef _WIN32 + RemoveCachePaths(std::filesystem::path(L"\\\\?\\" + directory.native())); +#endif return status; } From 1481f03bbdf4c27af326b3554dc61238b531123e Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis <motivated.it@gmail.com> Date: Sun, 18 Oct 2026 12:52:18 +0300 Subject: [PATCH 245/269] test(shader): keep cache relative paths on one drive --- .../recompiler/tests/CacheFileReadTests.cpp | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/core/shader/recompiler/tests/CacheFileReadTests.cpp b/core/shader/recompiler/tests/CacheFileReadTests.cpp index fbb9ab019f..f0e936b798 100644 --- a/core/shader/recompiler/tests/CacheFileReadTests.cpp +++ b/core/shader/recompiler/tests/CacheFileReadTests.cpp @@ -51,14 +51,14 @@ static void VerifyCachePaths(const std::filesystem::path& root) { Require(ShaderRecompiler::ReadWholeFile(extended(path), bytes) && bytes == original, "cannot read extended cache path"); const auto old = CreateFileW(extended(path).c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); - Require(old != INVALID_HANDLE_VALUE); + Require(old != INVALID_HANDLE_VALUE, "cannot retain the original cache reader"); struct Close { HANDLE file; ~Close() { CloseHandle(file); } } close{old}; Require(ShaderRecompiler::WriteFileAtomically(alias, replacement), "cannot replace cache path with retained reader"); - Require(ShaderRecompiler::ReadWholeFile(alias, bytes) && bytes == replacement); + Require(ShaderRecompiler::ReadWholeFile(alias, bytes) && bytes == replacement, "replacement cache bytes do not match"); std::byte retained[4]{}; DWORD count = 0; - Require(ReadFile(old, retained, sizeof(retained), &count, nullptr) && count == original.size()); - Require(std::equal(original.begin(), original.end(), retained)); + Require(ReadFile(old, retained, sizeof(retained), &count, nullptr) && count == original.size(), "cannot read the retained cache handle"); + Require(std::equal(original.begin(), original.end(), retained), "retained cache bytes changed after replacement"); } WIN32_FIND_DATAW data{}; const auto temporary = FindFirstFileW((extended(path).parent_path() / L"*.tmp.*").c_str(), &data); @@ -75,13 +75,21 @@ static void VerifyCachePaths(const std::filesystem::path& root) { while (deep.native().size() < 300) deep /= std::wstring(40, L'd'); verify(deep / L"cache \u00e4\u6f22.bin"); std::filesystem::create_directories(root / "relative"); + struct RestoreDirectory { + std::filesystem::path previous = std::filesystem::current_path(); + ~RestoreDirectory() { + std::error_code error; + std::filesystem::current_path(previous, error); + } + } restoreDirectory; + std::filesystem::current_path(root); auto relative = (root / "relative" / ".." / "relative.bin").lexically_relative(std::filesystem::current_path()); - Require(!relative.empty()); - Require(ShaderRecompiler::WriteFileAtomically(relative, original)); + Require(!relative.empty(), "cannot construct the cache relative path"); + Require(ShaderRecompiler::WriteFileAtomically(relative, original), "cannot write the cache relative path"); std::vector<std::byte> bytes; - Require(ShaderRecompiler::ReadWholeFile(relative, bytes) && bytes == original); + Require(ShaderRecompiler::ReadWholeFile(relative, bytes) && bytes == original, "cannot read the cache relative path"); const auto missing = deep / "missing.bin"; - Require(!ShaderRecompiler::ReadWholeFile(missing, bytes) && bytes.empty()); + Require(!ShaderRecompiler::ReadWholeFile(missing, bytes) && bytes.empty(), "missing cache path did not clear the read buffer"); } #endif From 61fa2d2221d7034ec450e6d4719edbcc4528e52b Mon Sep 17 00:00:00 2001 From: Kovalev Vladislav <veosett@yandex.ru> Date: Mon, 19 Oct 2026 20:27:10 +0600 Subject: [PATCH 246/269] test(agc): cover long pipeline journal paths and external tails --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 7 +- .../prx/libSceAgcDriver/tests/Graphics.cpp | 66 ++++++++++++++++--- 2 files changed, 63 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index fbd9b9d9c6..fe5b7e9443 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2131,12 +2131,17 @@ if(BUILD_TESTING) endif() set_tests_properties(agc_sparse_buffer_holes PROPERTIES TIMEOUT 30) add_test(NAME agc_pipeline_journal COMMAND agc_driver_graphics_tests --pipeline-journal) + add_test(NAME agc_pipeline_journal_long_paths COMMAND agc_driver_graphics_tests --pipeline-journal-long-paths) + set_tests_properties(agc_pipeline_journal_long_paths PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") add_test(NAME agc_pipeline_library_clear_waits_for_build COMMAND agc_driver_graphics_tests --pipeline-library-clear-waits-for-build) add_test(NAME agc_pipeline_store_lazy_buffer_pool COMMAND agc_driver_graphics_tests --pipeline-store-lazy-buffer-pool) set_tests_properties(agc_pipeline_journal PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") set_tests_properties(agc_pipeline_library_clear_waits_for_build PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") set_tests_properties(agc_pipeline_store_lazy_buffer_pool PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) + add_test(NAME agc_pipeline_journal_extended_paths COMMAND agc_driver_graphics_tests --pipeline-journal-extended-paths) + set_tests_properties(agc_pipeline_journal_extended_paths PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + set_tests_properties(agc_pipeline_journal_long_paths agc_pipeline_journal_extended_paths PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") set_tests_properties(agc_pipeline_journal agc_pipeline_library_clear_waits_for_build agc_pipeline_store_lazy_buffer_pool PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") endif() set_tests_properties(agc_driver_sparse_buffer_holes PROPERTIES TIMEOUT 30) @@ -3145,6 +3150,6 @@ endif() if(BUILD_TESTING) get_directory_property(agcRegisteredTests TESTS) - list(FILTER agcRegisteredTests EXCLUDE REGEX "^(agc_pipeline_journal|agc_driver_pipeline_journal)$") + list(FILTER agcRegisteredTests EXCLUDE REGEX "^(agc_pipeline_journal(_long_paths|_extended_paths)?|agc_driver_pipeline_journal)$") set_property(TEST ${agcRegisteredTests} APPEND PROPERTY ENVIRONMENT "APS5_NO_PIPELINE_JOURNAL=1") endif() diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 67d5b61cbe..10128afbe2 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -12,6 +12,7 @@ #include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "ShaderDiskCache.hpp" +#include "ShaderCacheDirectory.hpp" #include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" #include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" @@ -9721,21 +9722,36 @@ void pipelineStoreLazyBufferPoolTests() { Require(mock.live == 0, "lazy buffer pool pipeline tests leaked Vulkan objects"); } -void pipelineJournalTests() { +void pipelineJournalTests(bool longPath = false, bool extended = false) { using namespace AgcDriver::Graphics; using Stage = ShaderRecompiler::ShaderStage; - const auto directory = std::filesystem::temp_directory_path() / ("aps5-pipeline-journal-mock-" + std::to_string(std::random_device{}())); + const auto root = std::filesystem::temp_directory_path() / ("aps5-pipeline-journal-mock-" + std::to_string(std::random_device{}())); + auto directory = root; + if (longPath) { + Require(root.native().size() < 180, "the temporary root is too long for the journal path fixture"); + directory /= std::filesystem::path(u8"cache-\u00e4\u6f22"); + directory /= std::string(229 - directory.native().size(), 'd'); + } std::filesystem::create_directories(directory); struct Cleanup { std::filesystem::path directory; + std::filesystem::path journal; ~Cleanup() { + ShaderRecompiler::ShaderDiskCache::Shutdown(); std::error_code error; + if (!journal.empty()) { + std::filesystem::remove(journal, error); + auto lock = journal; + lock += ".lock"; + std::filesystem::remove(lock, error); + } std::filesystem::remove_all(directory, error); } - } cleanup{directory}; + } cleanup{root, {}}; #ifdef _WIN32 + if (extended) directory = std::filesystem::path(L"\\\\?\\" + directory.make_preferred().native()); _putenv_s("ANYPS5_NO_SHADER_CACHE", "0"); - _putenv_s("ANYPS5_SHADER_CACHE_DIR", directory.string().c_str()); + _wputenv_s(L"ANYPS5_SHADER_CACHE_DIR", directory.c_str()); _putenv_s("APS5_PIPELINE_JOURNAL_THREADS", "2"); #else setenv("ANYPS5_NO_SHADER_CACHE", "0", 1); @@ -9743,6 +9759,14 @@ void pipelineJournalTests() { setenv("APS5_PIPELINE_JOURNAL_THREADS", "2", 1); #endif Require(PipelineJournalEnabled(), "the journal is disabled with a shader cache directory set"); + auto journalFile = PipelineJournalPath(); +#ifdef _WIN32 + std::error_code pathError; + journalFile = ShaderRecompiler::NativeCachePath_nid_no_patch(journalFile, pathError); + Require(!pathError, "cannot normalize the journal fixture path"); +#endif + cleanup.journal = journalFile; + if (longPath) Require(journalFile.native().size() > 260, "the journal fixture did not cross MAX_PATH"); const auto module = [](std::uint32_t salt) { ShaderRecompiler::RecompileResult result; result.spirv = std::vector<std::uint32_t>{0x07230203u, 0x00010000u, 0u, 8u, 0u, 0x00020011u, 1u, 0x00020011u, 2u + salt}; @@ -9934,7 +9958,7 @@ void pipelineJournalTests() { FlushPipelineJournal(); const auto repeated = PipelineJournalTotals(); Require(repeated.recordedGraphics == recorded.recordedGraphics && repeated.recordedCompute == recorded.recordedCompute && repeated.recordedModules == recorded.recordedModules, "a repeated pipeline was recorded again"); - Require(std::filesystem::file_size(PipelineJournalPath()) > 0, "the journal file is empty"); + Require(std::filesystem::file_size(journalFile) > 0, "the journal file is empty"); const auto storeBefore = PipelineStoreTotals(); std::vector<std::byte> receivedWords; ShaderRecompiler::RecompileResult received; @@ -9963,7 +9987,7 @@ void pipelineJournalTests() { const auto storeHit = PipelineStoreTotals(); Require(pipeline != nullptr && !built && mock.pipelineCreateCount == created && storeHit.hits == storeReplayed.hits + 1 && storeHit.misses == storeReplayed.misses, "the draw's configuration did not find the replayed pipeline"); { - const auto journalPath = PipelineJournalPath(); + const auto& journalPath = journalFile; const auto whole = std::filesystem::file_size(journalPath); Require(whole % 8 == 0 && whole > 3, "the journal file is not record aligned"); std::filesystem::resize_file(journalPath, whole - 3); @@ -9977,9 +10001,33 @@ void pipelineJournalTests() { torn->Wait(); const auto reloaded = PipelineJournalTotals(); Require(reloaded.droppedRecords == 0 && reloaded.loadedModules == 4 && reloaded.loadedRecipes == 2 && reloaded.replayFailed == 0, "the journal did not recover from a tail torn inside a record's padding"); + const auto writtenBefore = PipelineJournalTotals().writtenBytes; RecordComputePipeline(compute, true); FlushPipelineJournal(); Require(PipelineJournalTotals().recordedCompute == reloaded.recordedCompute + 1 && PipelineJournalTotals().recordedModules == reloaded.recordedModules, "the pipeline whose record was torn was not recorded again, or its module was"); + const auto recordBytes = PipelineJournalTotals().writtenBytes - writtenBefore; + std::vector<std::byte> bytes; + Require(ShaderRecompiler::ReadWholeFile(journalFile, bytes) && recordBytes > 0 && recordBytes <= bytes.size(), "cannot read the re-recorded journal entry"); + { + std::ofstream file(journalFile, std::ios::binary | std::ios::app); + file.write(reinterpret_cast<const char*>(bytes.data() + bytes.size() - recordBytes), static_cast<std::streamsize>(recordBytes)); + const std::array<char, 8> tornTail{}; + file.write(tornTail.data(), tornTail.size()); + file.close(); + Require(file.good(), "cannot append an external complete record and torn tail"); + } + auto another = module(5); + another.bindings.push_back(makeBinding(Role::GuestBuffers, 0, 1, std::vector<std::uint32_t>(4))); + const auto beforeAppend = PipelineJournalTotals(); + RecordComputePipeline(another, false); + FlushPipelineJournal(); + const auto appended = PipelineJournalTotals(); + Require(appended.writeFailures == 0 && appended.recordedCompute == beforeAppend.recordedCompute + 1, "the append after an external torn tail failed"); + Require(std::filesystem::file_size(journalFile) == bytes.size() + recordBytes + appended.writtenBytes - beforeAppend.writtenBytes, "tail recovery discarded an external complete record or retained torn bytes"); + auto recovered = StartPipelineJournalReplay(context, fingerprint, hooks); + Require(recovered != nullptr, "the replay after the external append did not start"); + recovered->Wait(); + Require(PipelineJournalTotals().loadedModules == 5 && PipelineJournalTotals().loadedRecipes == 4 && PipelineJournalTotals().droppedRecords == 0, "the external append and torn-tail recovery did not replay cleanly"); } { const auto before = PipelineJournalTotals(); @@ -9992,7 +10040,7 @@ void pipelineJournalTests() { } { const std::uint32_t meshWithoutOutputSize = 2u; - std::fstream file(PipelineJournalPath(), std::ios::in | std::ios::out | std::ios::binary); + std::fstream file(journalFile, std::ios::in | std::ios::out | std::ios::binary); file.seekp(4); file.write(reinterpret_cast<const char*>(&meshWithoutOutputSize), sizeof(meshWithoutOutputSize)); file.close(); @@ -10035,8 +10083,8 @@ int main(int argc, char** argv) { commandBatchFenceFailureTests(); return 0; } - if (argc == 2 && std::string_view(argv[1]) == "--pipeline-journal") { - pipelineJournalTests(); + if (argc == 2 && (std::string_view(argv[1]) == "--pipeline-journal" || std::string_view(argv[1]) == "--pipeline-journal-long-paths" || std::string_view(argv[1]) == "--pipeline-journal-extended-paths")) { + pipelineJournalTests(std::string_view(argv[1]) != "--pipeline-journal", std::string_view(argv[1]) == "--pipeline-journal-extended-paths"); std::cout << "pipeline journal recipe, record and replay tests passed\n"; return 0; } From 2e4cae5840bee9f00bf861e2a6426f3b52d8afbd Mon Sep 17 00:00:00 2001 From: Kovalev Vladislav <veosett@yandex.ru> Date: Mon, 19 Oct 2026 20:39:05 +0600 Subject: [PATCH 247/269] fix(agc): normalize all pipeline journal file paths on Windows --- .../libSceAgcDriver/Graphics/src/PipelineJournal.cpp | 11 +++++++++-- core/shader/recompiler/CMakeLists.txt | 1 + 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp index 45481c3c5f..c7a0f679a5 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp @@ -564,8 +564,15 @@ private: Journal() { const char* off = std::getenv("APS5_NO_PIPELINE_JOURNAL"); if ((off != nullptr && std::strcmp(off, "0") != 0) || !ShaderRecompiler::ShaderDiskCache::Enabled()) return; - path = ShaderRecompiler::ShaderDiskCache::EntryDirectory() / "pipelines.journal"; - lockPath = ShaderRecompiler::ShaderDiskCache::EntryDirectory() / "pipelines.journal.lock"; + auto directory = ShaderRecompiler::ShaderDiskCache::EntryDirectory(); +#ifdef _WIN32 + std::error_code error; + const auto native = ShaderRecompiler::NativeCachePath_nid_no_patch(directory, error); + if (error) throw std::filesystem::filesystem_error("cannot resolve the pipeline journal directory", directory, error); + directory = native; +#endif + path = directory / "pipelines.journal"; + lockPath = directory / "pipelines.journal.lock"; maxBytes = configured("APS5_PIPELINE_JOURNAL_MAX_MIB", 1024u) << 20u; std::thread([this] { run(); }).detach(); } diff --git a/core/shader/recompiler/CMakeLists.txt b/core/shader/recompiler/CMakeLists.txt index 7d3750a196..01ffcb201c 100644 --- a/core/shader/recompiler/CMakeLists.txt +++ b/core/shader/recompiler/CMakeLists.txt @@ -238,6 +238,7 @@ function(add_shader_recompiler target runtimeTarget) HashBytes LayoutBarycentricEmulation MeshBarycentricLayoutOf + NativeCachePath_nid_no_patch ImageTableAbi::PoisonCode ImageTableAbi::PoisonReasonName ImageTableAbi::TableHeader From 56778c97d25262e7f7973baeba3f44fa0e65d5db Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis <motivated.it@gmail.com> Date: Sun, 18 Oct 2026 12:20:56 +0300 Subject: [PATCH 248/269] test(shader): exercise long-path mapped cache persistence (cherry picked from commit 83ad99879035e1418a5fa500cf061b236ec0a867) --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 3 + .../libSceAgcDriver/tests/ShaderDiskCache.cpp | 62 +++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index fe5b7e9443..4e6346131f 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2089,6 +2089,9 @@ if(BUILD_TESTING) set_tests_properties(agc_shader_disk_cache_large_file PROPERTIES SKIP_RETURN_CODE 77 RUN_SERIAL TRUE WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 240) if(WIN32) set_tests_properties(agc_shader_disk_cache_large_file PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") + add_test(NAME agc_shader_disk_cache_long_paths COMMAND agc_shader_disk_cache_tests --long-host-paths) + set_tests_properties(agc_shader_disk_cache_long_paths PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" + ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") endif() if(WIN32) add_test(NAME agc_driver_untracked_snapshot_hint COMMAND agc_driver_partially_resident_snapshot_tests --untracked-hint) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index 1796f83acb..c86a452b79 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -973,6 +973,62 @@ void verifyPersist() { requireSameArtifact(variant.artifact, persisted->artifact, "a persisted entry whose file was replaced and removed"); } +#ifdef _WIN32 +void verifyLongHostPaths() { + using namespace ShaderRecompiler::ShaderDiskCache; + auto directory = std::filesystem::temp_directory_path() / ("aps5-cache-long-" + std::to_string(std::random_device{}())); + require(directory.native().size() < 224, "temporary path is too long for the cache control"); + directory /= std::wstring(224 - directory.native().size(), L'd'); + const auto extended = [](const auto& path) { return std::filesystem::path(L"\\\\?\\" + path.native()); }; + std::filesystem::create_directories(extended(directory)); + setEnvironment("ANYPS5_NO_SHADER_CACHE", "0"); + setEnvironment("ANYPS5_SHADER_CACHE_DIR", directory.string()); + std::vector<std::filesystem::path> files; + std::filesystem::path entries; + struct Cleanup { + const std::filesystem::path& directory; + const std::vector<std::filesystem::path>& files; + const std::filesystem::path& entries; + ~Cleanup() { + ShaderRecompiler::ShaderDiskCache::Shutdown(); + std::error_code error; + for (const auto& file : files) std::filesystem::remove(std::filesystem::path(L"\\\\?\\" + file.native()), error); + if (!entries.empty()) std::filesystem::remove(std::filesystem::path(L"\\\\?\\" + entries.native()), error); + std::filesystem::remove(std::filesystem::path(L"\\\\?\\" + directory.native()), error); + } + } cleanup{directory, files, entries}; + SampleRequest sample; + const auto key = sample.Key(); + const auto variant = sampleVariant(); + entries = ShaderDiskCache::EntryDirectory(); + files.push_back(entries / EntryName(key)); + files.push_back(entries / "last-used"); + require(files.front().native().size() >= MAX_PATH, "the cache persistence control did not cross MAX_PATH"); + const auto persisted = ShaderDiskCache::Persist(key, variant); + std::vector<std::byte> published; + require(ReadWholeFile(files.front(), published) && published == EncodeEntry(key, variant), + "the long-path cache entry was not correctly published before mapping"); + require(persisted != nullptr, "a long-path cache entry was not persisted and mapped"); + MEMORY_BASIC_INFORMATION region{}; + require(VirtualQuery(persisted->artifact.spirv.data(), ®ion, sizeof(region)) == sizeof(region) && region.Type == MEM_MAPPED, + "long-path persisted SPIR-V is not mapped"); + requireSameVariant(variant, *persisted, "long-path persist round trip"); + CompiledVariant loaded; + require(ShaderDiskCache::Load(key, loaded), "a long-path persisted cache entry did not load"); + requireSameVariant(variant, loaded, "long-path load round trip"); + const auto packPath = directory / (std::wstring(75, L'p') + L".pack"); + files.push_back(packPath); + require(WriteFileAtomically(packPath, packBytes({{key, EncodeEntry(key, variant)}})), "cannot publish the long-path cache pack"); + CachePack pack(packPath); + require(pack.Valid(), "a long-path cache pack was rejected: " + pack.Error()); + PackLoadTimings timings; + CompiledVariant decoded; + require(pack.Load(key, decoded, timings) == LoadStatus::Loaded, "a long-path cache pack entry did not load"); + requireSameVariant(variant, decoded, "long-path cache pack round trip"); + std::cout << "long-path shader cache persistence and pack tests passed\n"; +} +#endif + struct ComputeRequest { std::vector<std::uint32_t> code{0xe0700000u, 0x80000000u, 0xbf810000u}; std::array<std::uint32_t, 4> userData{0x10000000u, 0x00000000u, 0x40u, 0x00027facu}; @@ -2321,6 +2377,12 @@ int main(int argc, char** argv) { std::cout << "artifact codec tests passed\n"; return 0; } +#ifdef _WIN32 + if (argc == 2 && std::string_view(argv[1]) == "--long-host-paths") { + verifyLongHostPaths(); + return 0; + } +#endif if (argc == 2 && std::string_view(argv[1]) == "--large-file") return runLargeFileWrite(); if (argc == 2 && std::string_view(argv[1]) == "--load") return runLoadingProcess(); if (argc == 2 && std::string_view(argv[1]) == "--load-prebuilt") return runPrebuiltLoadingProcess(); From 340f5784b0547a36f0b59d14aabb763465eaba99 Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis <motivated.it@gmail.com> Date: Sun, 18 Oct 2026 12:35:33 +0300 Subject: [PATCH 249/269] fix(shader): normalize mapped cache entry and pack paths (cherry picked from commit 0a3621bbbfd27bafb1ff5b97208eaf51ba90ccf0) --- core/shader/recompiler/ShaderCachePack.cpp | 5 ++++- core/shader/recompiler/ShaderDiskCache.cpp | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/core/shader/recompiler/ShaderCachePack.cpp b/core/shader/recompiler/ShaderCachePack.cpp index a9550483f5..6b15865823 100644 --- a/core/shader/recompiler/ShaderCachePack.cpp +++ b/core/shader/recompiler/ShaderCachePack.cpp @@ -40,7 +40,10 @@ std::vector<std::filesystem::path> PrebuiltCachePaths(const std::filesystem::pat CachePack::CachePack(const std::filesystem::path& path) { static_assert(std::endian::native == std::endian::little); #ifdef _WIN32 - const auto handle = CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + std::error_code pathError; + const auto filename = NativeCachePath_nid_no_patch(path, pathError); + if (pathError) { error = "cannot resolve"; return; } + const auto handle = CreateFileW(filename.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); if (handle == INVALID_HANDLE_VALUE) { error = "cannot open"; return; } LARGE_INTEGER size{}; if (!GetFileSizeEx(handle, &size) || size.QuadPart < static_cast<LONGLONG>(sizeof(PackHeader)) || static_cast<std::uint64_t>(size.QuadPart) > std::numeric_limits<std::size_t>::max()) { diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index 31815e3fb6..3a3d947b8c 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -195,7 +195,10 @@ private: std::shared_ptr<const MappedFile> MapFile(const std::filesystem::path& path) { #ifdef _WIN32 - const HANDLE file = CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + std::error_code error; + const auto filename = NativeCachePath_nid_no_patch(path, error); + if (error) return nullptr; + const HANDLE file = CreateFileW(filename.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); if (file == INVALID_HANDLE_VALUE) return nullptr; LARGE_INTEGER length{}; if (!GetFileSizeEx(file, &length) || length.QuadPart <= 0 || static_cast<unsigned long long>(length.QuadPart) > std::numeric_limits<std::size_t>::max()) { From 08fe08fadcfdf3490770e6e23318abc85d65004d Mon Sep 17 00:00:00 2001 From: Kovalev Vladislav <veosett@yandex.ru> Date: Tue, 20 Oct 2026 14:26:34 +0600 Subject: [PATCH 250/269] docs(agc): record remaining long pipeline cache path limits --- docs/dev/debt/libSceAgcDriver.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 3484dadee4..9d51aa81e6 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -54,6 +54,8 @@ ### Functional +- [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. + - [Workgroup count split](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (libSceAgcDriver) - a direct dispatch over `maxComputeWorkGroupCount` runs as `vkCmdDispatch` chunks, each bound to a pipeline variant whose workgroup offset specialization constants add the chunk's base to `WorkgroupId` and `GlobalInvocationId`. A variant is built when its chunk base is first recorded, not when the dispatch is prepared. A split of a shader with device-memory LDS throws: its slot base reads the chunk's `NumWorkgroups`, not the dispatch's. Chunks are ordered by a compute barrier, which the console does not have between workgroups of one dispatch. - [Texture cache budgets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - the storage image cache's budget is what the reported heap budget leaves after the heap's use other than the cached storage images, the unused part of the sampled textures' 2 GiB floor and an eighth of the budget, capped at a quarter of the budget and at `TextureCacheBudget` (a quarter of the heap's size, at least 2 GiB), never below 512 MiB; the sampled cache's budget reserves `TextureCacheBudget` for the storage cache. So the sampled budget falls first while the sampled textures alive stay within their floor, and both fall together above it; each cache trims only at its next insert, a storage image is allocated before its insert reads the budget, and both budgets read the heap at most once a second, so a burst between two reads can still exceed the reported budget. The 2 GiB floor and the eighth are the sampled formula's values reused and the 512 MiB floor and the caps the earlier ones; none was measured beyond one title (PPSA11386) on one 10 GB GPU, and a title whose storage working set exceeds the shrunk budget re-creates storage images at each use. - [Scan-resistant sampled texture admission](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - while the sampled texture cache is over its budget but within a quarter over it, the first use of a texture it does not hold in a present is refused (used for its draw, then released, never cached) when its least recently used entry was used in the current or previous present, and a later use in the same present is refused when that entry was used in the current present. So a working set larger than the budget keeps a stable subset: a texture used more than once in a present still evicts an entry not yet used in that present, so a working set with interleaved multi-use textures rotates as on main, and the textures that arrive after the cache fills are created again at each use for as long as the least recently used entry is from the current present, including in the transition present after a scene change. Past a quarter over the budget the entries are evicted as before, so after the budget falls the cache can sit at up to 125% of the reduced budget until entries stop being used. Entries unused for two presents are evicted as before. A later use can evict an entry from the previous present and then be refused when the next least recently used entry is from the current present, so that entry is created again in the next present. The "current or previous present" window is an unmeasured threshold: it was chosen, not derived. Measured once, as one A-B-A series on one title (loading phase about 33000 textures per frame against a 5.4 GiB budget) on one RTX 4090, 3600 s each: 932 / 968 / 941 frames, evictions after frame 620 2.72M -> 1.17M, texture creations unchanged (about 5.8M). Nothing is measured on other titles or GPUs, and a title whose per-present working set only slightly exceeds the budget now re-creates the excess textures at every use. From 80024c48fd0d8b19bd7be8d66d16056c61cee5fc Mon Sep 17 00:00:00 2001 From: Kovalev Vladislav <veosett@yandex.ru> Date: Tue, 20 Oct 2026 14:40:12 +0600 Subject: [PATCH 251/269] test(agc): serialize journal replay result capture --- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 2 ++ docs/dev/debt/libSceAgcDriver.md | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 10128afbe2..c5fda440e6 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -9964,7 +9964,9 @@ void pipelineJournalTests(bool longPath = false, bool extended = false) { ShaderRecompiler::RecompileResult received; bool receivedPush = false; std::uint32_t computeCalls = 0; + std::mutex computeMutex; PipelineJournalHooks hooks{[&](const ShaderRecompiler::RecompileResult& shader, bool pushConstants) { + std::lock_guard lock(computeMutex); ++computeCalls; received = shader; receivedPush = pushConstants; diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 9d51aa81e6..c7cda4ba85 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -54,8 +54,6 @@ ### Functional -- [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. - - [Workgroup count split](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (libSceAgcDriver) - a direct dispatch over `maxComputeWorkGroupCount` runs as `vkCmdDispatch` chunks, each bound to a pipeline variant whose workgroup offset specialization constants add the chunk's base to `WorkgroupId` and `GlobalInvocationId`. A variant is built when its chunk base is first recorded, not when the dispatch is prepared. A split of a shader with device-memory LDS throws: its slot base reads the chunk's `NumWorkgroups`, not the dispatch's. Chunks are ordered by a compute barrier, which the console does not have between workgroups of one dispatch. - [Texture cache budgets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - the storage image cache's budget is what the reported heap budget leaves after the heap's use other than the cached storage images, the unused part of the sampled textures' 2 GiB floor and an eighth of the budget, capped at a quarter of the budget and at `TextureCacheBudget` (a quarter of the heap's size, at least 2 GiB), never below 512 MiB; the sampled cache's budget reserves `TextureCacheBudget` for the storage cache. So the sampled budget falls first while the sampled textures alive stay within their floor, and both fall together above it; each cache trims only at its next insert, a storage image is allocated before its insert reads the budget, and both budgets read the heap at most once a second, so a burst between two reads can still exceed the reported budget. The 2 GiB floor and the eighth are the sampled formula's values reused and the 512 MiB floor and the caps the earlier ones; none was measured beyond one title (PPSA11386) on one 10 GB GPU, and a title whose storage working set exceeds the shrunk budget re-creates storage images at each use. - [Scan-resistant sampled texture admission](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - while the sampled texture cache is over its budget but within a quarter over it, the first use of a texture it does not hold in a present is refused (used for its draw, then released, never cached) when its least recently used entry was used in the current or previous present, and a later use in the same present is refused when that entry was used in the current present. So a working set larger than the budget keeps a stable subset: a texture used more than once in a present still evicts an entry not yet used in that present, so a working set with interleaved multi-use textures rotates as on main, and the textures that arrive after the cache fills are created again at each use for as long as the least recently used entry is from the current present, including in the transition present after a scene change. Past a quarter over the budget the entries are evicted as before, so after the budget falls the cache can sit at up to 125% of the reduced budget until entries stop being used. Entries unused for two presents are evicted as before. A later use can evict an entry from the previous present and then be refused when the next least recently used entry is from the current present, so that entry is created again in the next present. The "current or previous present" window is an unmeasured threshold: it was chosen, not derived. Measured once, as one A-B-A series on one title (loading phase about 33000 textures per frame against a 5.4 GiB budget) on one RTX 4090, 3600 s each: 932 / 968 / 941 frames, evictions after frame 620 2.72M -> 1.17M, texture creations unchanged (about 5.8M). Nothing is measured on other titles or GPUs, and a title whose per-present working set only slightly exceeds the budget now re-creates the excess textures at every use. @@ -215,3 +213,5 @@ - [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. + +- [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 680ba6bf29ab82d5b4b6f5d24d4637cc1d388e36 Mon Sep 17 00:00:00 2001 From: nekomancer01 <5332082+nekomancer01@users.noreply.github.com> Date: Sun, 18 Oct 2026 20:14:42 +1100 Subject: [PATCH 252/269] feat(libSceRudp): implement the poll exports Implement sceRudpPollCreate, sceRudpPollControl, sceRudpPollWait and sceRudpPollDestroy with the PS Vita signatures and argument checks. sceRudpTerminate removes the context from every poll and sceRudpEnd destroys the polls and wakes their waiters. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/CMakeLists.txt | 10 +++ core/libs/prx/libSceRudp/Export.cpp | 131 +++++++++++++++++++++++++--- core/libs/tests/GuestRudpPoll.cpp | 122 ++++++++++++++++++++++++++ docs/dev/debt/libSceRudp.md | 2 +- 4 files changed, 251 insertions(+), 14 deletions(-) create mode 100644 core/libs/tests/GuestRudpPoll.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 50373d1bf8..a6428b6825 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -478,6 +478,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceRudp>") endif() + add_test_executable(guest_rudp_poll_tests tests/GuestRudpPoll.cpp) + target_include_directories(guest_rudp_poll_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_rudp_poll_tests PRIVATE libSceRudp libc) + configure_windows_unwind(guest_rudp_poll_tests) + add_test(NAME guest_rudp_poll COMMAND guest_rudp_poll_tests) + if(WIN32) + set_tests_properties(guest_rudp_poll PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceRudp>") + endif() + add_test_executable(guest_memory_pool_stats_tests tests/GuestMemoryPoolStats.cpp) target_include_directories(guest_memory_pool_stats_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_memory_pool_stats_tests PRIVATE libSceSsl libSceHttp2 libSceNet libkernel libc) diff --git a/core/libs/prx/libSceRudp/Export.cpp b/core/libs/prx/libSceRudp/Export.cpp index e1c283e658..f72bc2f121 100644 --- a/core/libs/prx/libSceRudp/Export.cpp +++ b/core/libs/prx/libSceRudp/Export.cpp @@ -1,10 +1,14 @@ #include <atomic> +#include <chrono> +#include <condition_variable> #include <cstdint> #include <cstdio> #include <cstddef> #include <cstring> +#include <limits> #include <map> #include <mutex> +#include <stdexcept> #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -20,6 +24,13 @@ constexpr int RUDP_ERROR_MEMORY = static_cast<int>(0x80770007u); constexpr int RUDP_ERROR_CONN_RESET = static_cast<int>(0x80770009u); constexpr int RUDP_ERROR_CONN_REFUSED = static_cast<int>(0x8077000Au); constexpr int RUDP_ERROR_NOT_ACCEPTABLE = static_cast<int>(0x80770011u); +constexpr int RUDP_ERROR_INVALID_POLL_ID = static_cast<int>(0x8077001Fu); +constexpr int RUDP_ERROR_TOO_MANY_CONTEXTS = static_cast<int>(0x80770020u); + +constexpr int RUDP_POLL_OP_ADD = 1; +constexpr int RUDP_POLL_OP_MODIFY = 2; +constexpr int RUDP_POLL_OP_REMOVE = 3; +constexpr uint64_t RUDP_POLL_WAIT_MAX_USEC = static_cast<uint64_t>(std::numeric_limits<int64_t>::max() / 2000); constexpr int RUDP_POOL_MIN_SIZE = 0xC58; constexpr std::uintptr_t RUDP_POOL_ALIGNMENT = 8; @@ -36,9 +47,31 @@ std::map<int, bool> g_ctx; // id -> bound GuestRudpEventHandler g_handler = nullptr; void* g_handler_arg = nullptr; +struct RudpPoll { + uint32_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; +std::condition_variable g_polls_changed; + bool valid_ctx(int ctx) { return g_ctx.count(ctx) != 0; } + +void remove_ctx_from_polls(int ctx) { + for (auto& [id, poll] : g_polls) { + poll.entries.erase(ctx); + } +} } // namespace extern "C" { @@ -84,6 +117,8 @@ int APS5_VABI sceRudpEnd(void) { } g_inited = false; g_ctx.clear(); + g_polls.clear(); + g_polls_changed.notify_all(); return 0; } @@ -92,7 +127,11 @@ int APS5_VABI sceRudpTerminate(int ctx_id) { if (!g_inited) { return RUDP_ERROR_NOT_INITIALIZED; } - return g_ctx.erase(ctx_id) != 0 ? 0 : RUDP_ERROR_INVALID_CONTEXT_ID; + if (g_ctx.erase(ctx_id) == 0) { + return RUDP_ERROR_INVALID_CONTEXT_ID; + } + remove_ctx_from_polls(ctx_id); + return 0; } int APS5_VABI sceRudpEnableInternalIOThread(uint32_t stack_size, uint32_t priority) { @@ -293,23 +332,89 @@ int APS5_VABI sceRudpGetMaxSegmentSize(uint16_t* size) { return 0; } -int APS5_VABI sceRudpPollCreate() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceRudpPollCreate(uint32_t size) { + std::lock_guard<std::mutex> lk(g_mutex); + if (!g_inited) { + return RUDP_ERROR_NOT_INITIALIZED; + } + if (size == 0) { + return RUDP_ERROR_INVALID_ARGUMENT; + } + if (g_next_poll == std::numeric_limits<int>::max()) { + throw std::runtime_error("sceRudpPollCreate: poll ids exhausted"); + } + const int id = g_next_poll++; + g_polls[id] = RudpPoll{size, {}}; + return id; +} + +int APS5_VABI sceRudpPollControl(int pollId, int op, int ctxId, uint16_t events) { + std::lock_guard<std::mutex> lk(g_mutex); + if (!g_inited) { + return RUDP_ERROR_NOT_INITIALIZED; + } + if (pollId < 0 || ctxId < 0 || (op != RUDP_POLL_OP_ADD && op != RUDP_POLL_OP_MODIFY && op != RUDP_POLL_OP_REMOVE)) { + return RUDP_ERROR_INVALID_ARGUMENT; + } + const auto poll = g_polls.find(pollId); + if (poll == g_polls.end()) { + return RUDP_ERROR_INVALID_POLL_ID; + } + if (!valid_ctx(ctxId)) { + return RUDP_ERROR_INVALID_CONTEXT_ID; + } + auto& entries = poll->second.entries; + const auto entry = entries.find(ctxId); + if (op == RUDP_POLL_OP_ADD) { + if (entry != entries.end()) { + return RUDP_ERROR_INVALID_CONTEXT_ID; + } + if (entries.size() >= poll->second.capacity) { + return RUDP_ERROR_TOO_MANY_CONTEXTS; + } + entries.emplace(ctxId, events); + return 0; + } + if (entry == entries.end()) { + return RUDP_ERROR_INVALID_CONTEXT_ID; + } + if (op == RUDP_POLL_OP_MODIFY) { + entry->second = events; + } else { + entries.erase(entry); + } return 0; } -int APS5_VABI sceRudpPollControl() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceRudpPollWait(int pollId, RudpPollEvent* events, uint32_t eventLen, uint64_t timeout) { + std::unique_lock<std::mutex> lk(g_mutex); + if (!g_inited) { + return RUDP_ERROR_NOT_INITIALIZED; + } + if (pollId < 0 || events == nullptr || eventLen == 0) { + return RUDP_ERROR_INVALID_ARGUMENT; + } + if (g_polls.count(pollId) == 0) { + return RUDP_ERROR_INVALID_POLL_ID; + } + const auto closed = [pollId] { return !g_inited || g_polls.count(pollId) == 0; }; + if (timeout > RUDP_POLL_WAIT_MAX_USEC) { + g_polls_changed.wait(lk, closed); + } else if (!g_polls_changed.wait_for(lk, std::chrono::microseconds(timeout), closed)) { + return 0; + } + return g_inited ? RUDP_ERROR_INVALID_POLL_ID : RUDP_ERROR_NOT_INITIALIZED; } -int APS5_VABI sceRudpPollWait() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceRudpPollDestroy() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceRudpPollDestroy(int pollId) { + std::lock_guard<std::mutex> lk(g_mutex); + if (!g_inited) { + return RUDP_ERROR_NOT_INITIALIZED; + } + if (g_polls.erase(pollId) == 0) { + return RUDP_ERROR_INVALID_POLL_ID; + } + g_polls_changed.notify_all(); return 0; } diff --git a/core/libs/tests/GuestRudpPoll.cpp b/core/libs/tests/GuestRudpPoll.cpp new file mode 100644 index 0000000000..1957a27f2f --- /dev/null +++ b/core/libs/tests/GuestRudpPoll.cpp @@ -0,0 +1,122 @@ +#include "SceTypes.hpp" + +#include <array> +#include <chrono> +#include <cstdint> +#include <cstdlib> +#include <limits> +#include <thread> + +extern "C" { +using GuestRudpEventHandler = void(APS5_VABI*)(int, int, int, void*); +struct GuestRudpPollEvent { + int ctxId; + std::uint16_t reqEvents; + std::uint16_t rtnEvents; +}; +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 sceRudpPollControl(int, int, int, std::uint16_t); +int APS5_VABI sceRudpPollWait(int, GuestRudpPollEvent*, std::uint32_t, std::uint64_t); +int APS5_VABI sceRudpPollDestroy(int); +} + +static void Require(bool value) { + if (!value) std::abort(); +} + +static void APS5_VABI Handler(int, int, int, void*) {} + +int main() { + constexpr int notInitialized = static_cast<int>(0x80770001u); + constexpr int invalidContext = static_cast<int>(0x80770003u); + constexpr int invalidArgument = static_cast<int>(0x80770004u); + constexpr int invalidPoll = static_cast<int>(0x8077001Fu); + constexpr int tooManyContexts = static_cast<int>(0x80770020u); + constexpr int add = 1; + constexpr int modify = 2; + constexpr int remove = 3; + constexpr std::uint16_t read = 0x0001; + constexpr std::uint16_t write = 0x0002; + constexpr std::uint64_t forever = std::numeric_limits<std::uint64_t>::max(); + alignas(16) static std::array<unsigned char, 0x10000> pool{}; + std::array<GuestRudpPollEvent, 4> events{}; + const auto eventCount = static_cast<std::uint32_t>(events.size()); + + Require(sceRudpPollCreate(1) == notInitialized); + Require(sceRudpPollControl(1, add, 0, read) == notInitialized); + Require(sceRudpPollWait(1, events.data(), eventCount, 0) == notInitialized); + Require(sceRudpPollDestroy(1) == notInitialized); + Require(sceRudpInit_nid_postfix(pool.data(), static_cast<int>(pool.size())) == 0); + + Require(sceRudpPollCreate(0) == invalidArgument); + const int poll = sceRudpPollCreate(1); + Require(poll > 0); + const int other = sceRudpPollCreate(1); + Require(other > 0 && other != poll); + + int first = -1; + int second = -1; + Require(sceRudpCreateContext(Handler, nullptr, &first) == 0); + Require(sceRudpCreateContext(Handler, nullptr, &second) == 0); + + Require(sceRudpPollControl(poll, 0, first, read) == invalidArgument); + Require(sceRudpPollControl(poll, 4, first, read) == invalidArgument); + Require(sceRudpPollControl(-1, add, first, read) == invalidArgument); + Require(sceRudpPollControl(poll, add, -1, read) == invalidArgument); + Require(sceRudpPollControl(12345, add, first, read) == invalidPoll); + Require(sceRudpPollControl(poll, add, 12345, read) == invalidContext); + Require(sceRudpPollControl(poll, modify, first, read) == invalidContext); + Require(sceRudpPollControl(poll, remove, first, read) == invalidContext); + + Require(sceRudpPollControl(poll, add, first, read) == 0); + Require(sceRudpPollControl(poll, add, first, read) == invalidContext); + Require(sceRudpPollControl(poll, add, second, read) == tooManyContexts); + Require(sceRudpPollControl(other, add, first, read) == 0); + Require(sceRudpPollControl(poll, modify, first, read | write) == 0); + Require(sceRudpPollControl(poll, remove, first, 0) == 0); + Require(sceRudpPollControl(poll, remove, first, 0) == invalidContext); + Require(sceRudpPollControl(poll, add, second, write) == 0); + + Require(sceRudpPollWait(-1, events.data(), eventCount, 0) == invalidArgument); + Require(sceRudpPollWait(poll, nullptr, eventCount, 0) == invalidArgument); + Require(sceRudpPollWait(poll, events.data(), 0, 0) == invalidArgument); + Require(sceRudpPollWait(12345, events.data(), eventCount, 0) == invalidPoll); + Require(sceRudpPollWait(poll, events.data(), eventCount, 0) == 0); + const auto start = std::chrono::steady_clock::now(); + Require(sceRudpPollWait(poll, events.data(), eventCount, 20000) == 0); + Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(20)); + + Require(sceRudpTerminate(second) == 0); + Require(sceRudpPollControl(poll, add, first, read) == 0); + Require(sceRudpTerminate(first) == 0); + Require(sceRudpPollControl(poll, add, first, read) == invalidContext); + int third = -1; + Require(sceRudpCreateContext(Handler, nullptr, &third) == 0); + Require(sceRudpPollControl(poll, add, third, read) == 0); + Require(sceRudpPollControl(other, add, third, read) == 0); + + int destroyedResult = 0; + std::thread destroyedWaiter([&] { destroyedResult = sceRudpPollWait(poll, events.data(), eventCount, forever); }); + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + Require(sceRudpPollDestroy(poll) == 0); + destroyedWaiter.join(); + Require(destroyedResult == invalidPoll); + Require(sceRudpPollDestroy(poll) == invalidPoll); + Require(sceRudpPollControl(poll, add, third, read) == invalidPoll); + + int endedResult = 0; + std::thread endedWaiter([&] { endedResult = sceRudpPollWait(other, events.data(), eventCount, forever); }); + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + Require(sceRudpEnd() == 0); + endedWaiter.join(); + Require(endedResult == notInitialized); + + Require(sceRudpInit_nid_postfix(pool.data(), static_cast<int>(pool.size())) == 0); + Require(sceRudpPollDestroy(other) == invalidPoll); + Require(sceRudpEnd() == 0); + return 0; +} diff --git a/docs/dev/debt/libSceRudp.md b/docs/dev/debt/libSceRudp.md index fd1fc4868a..217a1ebbd8 100644 --- a/docs/dev/debt/libSceRudp.md +++ b/docs/dev/debt/libSceRudp.md @@ -12,5 +12,5 @@ - [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 - [sceRudpGetOption](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) takes `(int ctx_id, int option, void* value, uint32_t len)`, taken from the bundled `Media/Plugins/SceRudp.prx` of PPSA06888: its two exported wrappers pass their first three arguments through unchanged and set `len` to 4, the same shape as its `sceRudpSetOption` call, which passes a 4-byte value. The option codes, the defaults and the error results are unknown, so it still throws - 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 -- PPSA31327 imports declared without parameters, signatures unknown: [sceRudpPollCreate, sceRudpPollControl, sceRudpPollWait, sceRudpPollDestroy](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp), imported by Yakuza 0 Director's Cut and not called before the main menu. Names from the NID table of [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/rudp/rudp.cpp) +- [sceRudpPollCreate, sceRudpPollControl, sceRudpPollWait, sceRudpPollDestroy](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - imported by PPSA31327 (Yakuza 0 Director's Cut) and not called before the main menu. Signatures `int sceRudpPollCreate(uint32_t size)`, `int sceRudpPollControl(int pollId, int op, int ctxId, uint16_t events)` and `int sceRudpPollWait(int pollId, SceRudpPollEvent* events, uint32_t eventLen, uint64_t timeoutUsec)`, the 8-byte `SceRudpPollEvent` (`ctxId`, `reqEvents`, `rtnEvents`), 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); `RUDP_ERROR_INVALID_POLL_ID` (0x8077001F) and `RUDP_ERROR_TOO_MANY_CONTEXTS` (0x80770020) match the PS3 codes in [RPCS3](https://github.com/RPCS3/rpcs3/blob/master/rpcs3/Emu/Cell/Modules/cellRudp.h). Vita3K leaves `sceRudpPollDestroy` unimplemented: `int sceRudpPollDestroy(int pollId)` is inferred. No context can connect, so no context ever reports an event and `sceRudpPollWait` returns 0 when its timeout elapses; a timeout above 2^63/2000 microseconds waits until the poll is destroyed. A wait whose poll is destroyed returns `RUDP_ERROR_INVALID_POLL_ID`, and one interrupted by `sceRudpEnd` returns `RUDP_ERROR_NOT_INITIALIZED`. `sceRudpTerminate` removes the context from every poll. Poll ids start at 1. None of this is measured on a PS5 - [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 d2b9531517ecb0bf3cd5024efe6bcae519c0da0e Mon Sep 17 00:00:00 2001 From: nekomancer01 <5332082+nekomancer01@users.noreply.github.com> Date: Sun, 18 Oct 2026 23:20:54 +1100 Subject: [PATCH 253/269] fix(libSceRudp): match the Vita3K check order and reject negative poll ids Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libSceRudp/Export.cpp | 9 ++++++--- core/libs/tests/GuestRudpPoll.cpp | 6 ++++-- docs/dev/debt/libSceRudp.md | 2 +- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceRudp/Export.cpp b/core/libs/prx/libSceRudp/Export.cpp index f72bc2f121..87864f5971 100644 --- a/core/libs/prx/libSceRudp/Export.cpp +++ b/core/libs/prx/libSceRudp/Export.cpp @@ -366,12 +366,12 @@ int APS5_VABI sceRudpPollControl(int pollId, int op, int ctxId, uint16_t events) auto& entries = poll->second.entries; const auto entry = entries.find(ctxId); if (op == RUDP_POLL_OP_ADD) { - if (entry != entries.end()) { - return RUDP_ERROR_INVALID_CONTEXT_ID; - } if (entries.size() >= poll->second.capacity) { return RUDP_ERROR_TOO_MANY_CONTEXTS; } + if (entry != entries.end()) { + return RUDP_ERROR_INVALID_CONTEXT_ID; + } entries.emplace(ctxId, events); return 0; } @@ -411,6 +411,9 @@ int APS5_VABI sceRudpPollDestroy(int pollId) { if (!g_inited) { return RUDP_ERROR_NOT_INITIALIZED; } + if (pollId < 0) { + return RUDP_ERROR_INVALID_ARGUMENT; + } if (g_polls.erase(pollId) == 0) { return RUDP_ERROR_INVALID_POLL_ID; } diff --git a/core/libs/tests/GuestRudpPoll.cpp b/core/libs/tests/GuestRudpPoll.cpp index 1957a27f2f..d797fad444 100644 --- a/core/libs/tests/GuestRudpPoll.cpp +++ b/core/libs/tests/GuestRudpPoll.cpp @@ -55,7 +55,7 @@ int main() { Require(sceRudpPollCreate(0) == invalidArgument); const int poll = sceRudpPollCreate(1); Require(poll > 0); - const int other = sceRudpPollCreate(1); + const int other = sceRudpPollCreate(2); Require(other > 0 && other != poll); int first = -1; @@ -73,9 +73,10 @@ int main() { Require(sceRudpPollControl(poll, remove, first, read) == invalidContext); Require(sceRudpPollControl(poll, add, first, read) == 0); - Require(sceRudpPollControl(poll, add, first, read) == invalidContext); + Require(sceRudpPollControl(poll, add, first, read) == tooManyContexts); Require(sceRudpPollControl(poll, add, second, read) == tooManyContexts); Require(sceRudpPollControl(other, add, first, read) == 0); + Require(sceRudpPollControl(other, add, first, read) == invalidContext); Require(sceRudpPollControl(poll, modify, first, read | write) == 0); Require(sceRudpPollControl(poll, remove, first, 0) == 0); Require(sceRudpPollControl(poll, remove, first, 0) == invalidContext); @@ -106,6 +107,7 @@ int main() { destroyedWaiter.join(); Require(destroyedResult == invalidPoll); Require(sceRudpPollDestroy(poll) == invalidPoll); + Require(sceRudpPollDestroy(-1) == invalidArgument); Require(sceRudpPollControl(poll, add, third, read) == invalidPoll); int endedResult = 0; diff --git a/docs/dev/debt/libSceRudp.md b/docs/dev/debt/libSceRudp.md index 217a1ebbd8..e0b6e6ba33 100644 --- a/docs/dev/debt/libSceRudp.md +++ b/docs/dev/debt/libSceRudp.md @@ -12,5 +12,5 @@ - [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 - [sceRudpGetOption](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) takes `(int ctx_id, int option, void* value, uint32_t len)`, taken from the bundled `Media/Plugins/SceRudp.prx` of PPSA06888: its two exported wrappers pass their first three arguments through unchanged and set `len` to 4, the same shape as its `sceRudpSetOption` call, which passes a 4-byte value. The option codes, the defaults and the error results are unknown, so it still throws - 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) - imported by PPSA31327 (Yakuza 0 Director's Cut) and not called before the main menu. Signatures `int sceRudpPollCreate(uint32_t size)`, `int sceRudpPollControl(int pollId, int op, int ctxId, uint16_t events)` and `int sceRudpPollWait(int pollId, SceRudpPollEvent* events, uint32_t eventLen, uint64_t timeoutUsec)`, the 8-byte `SceRudpPollEvent` (`ctxId`, `reqEvents`, `rtnEvents`), 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); `RUDP_ERROR_INVALID_POLL_ID` (0x8077001F) and `RUDP_ERROR_TOO_MANY_CONTEXTS` (0x80770020) match the PS3 codes in [RPCS3](https://github.com/RPCS3/rpcs3/blob/master/rpcs3/Emu/Cell/Modules/cellRudp.h). Vita3K leaves `sceRudpPollDestroy` unimplemented: `int sceRudpPollDestroy(int pollId)` is inferred. No context can connect, so no context ever reports an event and `sceRudpPollWait` returns 0 when its timeout elapses; a timeout above 2^63/2000 microseconds waits until the poll is destroyed. A wait whose poll is destroyed returns `RUDP_ERROR_INVALID_POLL_ID`, and one interrupted by `sceRudpEnd` returns `RUDP_ERROR_NOT_INITIALIZED`. `sceRudpTerminate` removes the context from every poll. Poll ids start at 1. None of this is measured on a PS5 +- [sceRudpPollCreate, sceRudpPollControl, sceRudpPollWait, sceRudpPollDestroy](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - imported by PPSA31327 (Yakuza 0 Director's Cut) and not called before the main menu. Signatures `int sceRudpPollCreate(uint32_t size)`, `int sceRudpPollControl(int pollId, int op, int ctxId, uint16_t events)` and `int sceRudpPollWait(int pollId, SceRudpPollEvent* events, uint32_t eventLen, uint64_t timeoutUsec)`, the 8-byte `SceRudpPollEvent` (`ctxId`, `reqEvents`, `rtnEvents`), 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); `RUDP_ERROR_INVALID_POLL_ID` (0x8077001F) and `RUDP_ERROR_TOO_MANY_CONTEXTS` (0x80770020) match the PS3 codes in [RPCS3](https://github.com/RPCS3/rpcs3/blob/master/rpcs3/Emu/Cell/Modules/cellRudp.h). Vita3K leaves `sceRudpPollDestroy` unimplemented: `int sceRudpPollDestroy(int pollId)` is inferred, and it answers a negative id with `RUDP_ERROR_INVALID_ARGUMENT` like the other poll calls. No context can connect, so no context ever reports an event and `sceRudpPollWait` returns 0 when its timeout elapses, also for a poll with no contexts, where Vita3K returns 0 at once; a timeout above 2^63/2000 microseconds waits until the poll is destroyed. A wait whose poll is destroyed returns `RUDP_ERROR_INVALID_POLL_ID`, and one interrupted by `sceRudpEnd` returns `RUDP_ERROR_NOT_INITIALIZED`. `sceRudpTerminate` removes the context from every poll. Poll ids start at 1. None of this is measured on a PS5 - [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 e4164098a8becd6463a4e164eb7ed17d7c30712e Mon Sep 17 00:00:00 2001 From: nekomancer01 <5332082+nekomancer01@users.noreply.github.com> Date: Tue, 20 Oct 2026 11:10:42 +1100 Subject: [PATCH 254/269] docs(libSceRudp): state the poll gaps first and shorten the debt entry Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- docs/dev/debt/libSceRudp.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceRudp.md b/docs/dev/debt/libSceRudp.md index e0b6e6ba33..19757221c0 100644 --- a/docs/dev/debt/libSceRudp.md +++ b/docs/dev/debt/libSceRudp.md @@ -12,5 +12,5 @@ - [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 - [sceRudpGetOption](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) takes `(int ctx_id, int option, void* value, uint32_t len)`, taken from the bundled `Media/Plugins/SceRudp.prx` of PPSA06888: its two exported wrappers pass their first three arguments through unchanged and set `len` to 4, the same shape as its `sceRudpSetOption` call, which passes a 4-byte value. The option codes, the defaults and the error results are unknown, so it still throws - 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) - imported by PPSA31327 (Yakuza 0 Director's Cut) and not called before the main menu. Signatures `int sceRudpPollCreate(uint32_t size)`, `int sceRudpPollControl(int pollId, int op, int ctxId, uint16_t events)` and `int sceRudpPollWait(int pollId, SceRudpPollEvent* events, uint32_t eventLen, uint64_t timeoutUsec)`, the 8-byte `SceRudpPollEvent` (`ctxId`, `reqEvents`, `rtnEvents`), 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); `RUDP_ERROR_INVALID_POLL_ID` (0x8077001F) and `RUDP_ERROR_TOO_MANY_CONTEXTS` (0x80770020) match the PS3 codes in [RPCS3](https://github.com/RPCS3/rpcs3/blob/master/rpcs3/Emu/Cell/Modules/cellRudp.h). Vita3K leaves `sceRudpPollDestroy` unimplemented: `int sceRudpPollDestroy(int pollId)` is inferred, and it answers a negative id with `RUDP_ERROR_INVALID_ARGUMENT` like the other poll calls. No context can connect, so no context ever reports an event and `sceRudpPollWait` returns 0 when its timeout elapses, also for a poll with no contexts, where Vita3K returns 0 at once; a timeout above 2^63/2000 microseconds waits until the poll is destroyed. A wait whose poll is destroyed returns `RUDP_ERROR_INVALID_POLL_ID`, and one interrupted by `sceRudpEnd` returns `RUDP_ERROR_NOT_INITIALIZED`. `sceRudpTerminate` removes the context from every poll. Poll ids start at 1. None of this is measured on a PS5 +- [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 4b527adf0587aaa2973885fc11e724feaf24b47d Mon Sep 17 00:00:00 2001 From: hypermist <ariel@hypurrr.app> Date: Tue, 20 Oct 2026 22:12:02 +1300 Subject: [PATCH 255/269] feat(libc): implement vfscanf, vscanf, scanf, the wide stream printf family and vswscanf vfscanf, vscanf and scanf scan through the same host path as fscanf; vfwprintf, fwprintf and vwprintf format like wprintf and write to the guest stream; vswscanf and vswscanf_s run the wide scanner on a va_list. The names hash to the NIDs libSceLibcInternal exports. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- core/libs/prx/libc/CMakeLists.txt | 9 ++ core/libs/prx/libc/src/Formatting.cpp | 39 ++++++++ core/libs/prx/libc/src/FormattingWide.cpp | 26 +++++ core/libs/prx/libc/src/ScanningWide.cpp | 10 ++ core/libs/tests/GuestStdioVariadic.cpp | 114 ++++++++++++++++++++++ 5 files changed, 198 insertions(+) create mode 100644 core/libs/tests/GuestStdioVariadic.cpp diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index ca39154516..083cd31097 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -314,6 +314,15 @@ if(BUILD_TESTING) set_tests_properties(guest_integer_builtins PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") endif() + add_test_executable(guest_stdio_variadic_tests ${LIBS_INCLUDE_DIR}/tests/GuestStdioVariadic.cpp) + target_include_directories(guest_stdio_variadic_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_stdio_variadic_tests PRIVATE libc) + configure_windows_unwind(guest_stdio_variadic_tests) + add_test(NAME guest_stdio_variadic COMMAND guest_stdio_variadic_tests) + if(WIN32) + set_tests_properties(guest_stdio_variadic PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") + endif() add_test_executable(guest_execute_once_tests ${LIBS_INCLUDE_DIR}/tests/GuestExecuteOnce.cpp) target_include_directories(guest_execute_once_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_execute_once_tests PRIVATE libc) diff --git a/core/libs/prx/libc/src/Formatting.cpp b/core/libs/prx/libc/src/Formatting.cpp index 0d883aa904..e78be1d4a6 100644 --- a/core/libs/prx/libc/src/Formatting.cpp +++ b/core/libs/prx/libc/src/Formatting.cpp @@ -565,4 +565,43 @@ int APS5_VABI puts_nid_postfix(const char* s) { return std::puts(s); } +extern FileStream* __stdinp_nid_postfix; + +int APS5_VABI vfscanf_nid_postfix(FileStream* stream, const char* format, VaList* args) { + if (stream && !stream->IsOpen()) { errno = 9; return -1; } + auto* native = GetNativeStream(stream); +#ifdef _WIN32 + const int result = LibcDetail::ScanFileWindows_nid_no_patch(native, format, args); +#else + RejectGuestWideScan(format); + std::va_list copy; + va_copy(copy, *reinterpret_cast<std::va_list*>(args)); + const int result = HostVfscanf(native, format, copy); + va_end(copy); +#endif + stream->SyncStatus(); + return result; +} + +int APS5_VABI vscanf_nid_postfix(const char* format, VaList* args) { + return vfscanf_nid_postfix(__stdinp_nid_postfix, format, args); +} + +int APS5_VABI scanf_nid_postfix(const char* format, ...) { +#ifdef _WIN32 + __builtin_sysv_va_list args; + __builtin_sysv_va_start(args, format); +#else + std::va_list args; + va_start(args, format); +#endif + const int result = vfscanf_nid_postfix(__stdinp_nid_postfix, format, reinterpret_cast<VaList*>(args)); +#ifdef _WIN32 + __builtin_sysv_va_end(args); +#else + va_end(args); +#endif + return result; +} + } diff --git a/core/libs/prx/libc/src/FormattingWide.cpp b/core/libs/prx/libc/src/FormattingWide.cpp index 713a520591..1526f39fef 100644 --- a/core/libs/prx/libc/src/FormattingWide.cpp +++ b/core/libs/prx/libc/src/FormattingWide.cpp @@ -333,6 +333,32 @@ int APS5_VABI wprintf_nid_postfix(const char16_t* format, ...) { return result; } +int APS5_VABI vfwprintf_nid_postfix(FileStream* stream, const char16_t* format, VaList* args) { + if (stream == nullptr || format == nullptr) { errno = 22; return -1; } + if (!stream->IsOpen()) { errno = 9; return -1; } + int result = -1; + try { + const std::u16string text = FormatWide(format, args); + const std::string bytes = ToUtf8(text); + if (std::fwrite(bytes.data(), 1, bytes.size(), GetNativeStream(stream)) == bytes.size()) result = static_cast<int>(text.size()); + } catch (const std::exception&) { + errno = 22; + } + stream->SyncStatus(); + return result; +} + +int APS5_VABI fwprintf_nid_postfix(FileStream* stream, const char16_t* format, ...) { + APS5_VA_BEGIN(format); + const int result = vfwprintf_nid_postfix(stream, format, reinterpret_cast<VaList*>(args)); + APS5_VA_END(); + return result; +} + +int APS5_VABI vwprintf_nid_postfix(const char16_t* format, VaList* args) { + return vfwprintf_nid_postfix(__stdoutp_nid_postfix, format, args); +} + int APS5_VABI fputwc_nid_postfix(char16_t value, FileStream* stream) { if (stream == nullptr) { errno = 22; return -1; } try { diff --git a/core/libs/prx/libc/src/ScanningWide.cpp b/core/libs/prx/libc/src/ScanningWide.cpp index 6cccb21222..d040f9afe5 100644 --- a/core/libs/prx/libc/src/ScanningWide.cpp +++ b/core/libs/prx/libc/src/ScanningWide.cpp @@ -427,4 +427,14 @@ int APS5_VABI swscanf_s_nid_postfix(const char16_t* buffer, const char16_t* form } } +int APS5_VABI vswscanf_nid_postfix(const char16_t* buffer, const char16_t* format, VaList* args) { + if (buffer == nullptr || format == nullptr) return EOF; + return WideScanner("vswscanf", false, buffer, format, args).Run(); +} + +int APS5_VABI vswscanf_s_nid_postfix(const char16_t* buffer, const char16_t* format, VaList* args) { + if (buffer == nullptr || format == nullptr) return EOF; + return WideScanner("vswscanf_s", true, buffer, format, args).Run(); +} + } diff --git a/core/libs/tests/GuestStdioVariadic.cpp b/core/libs/tests/GuestStdioVariadic.cpp new file mode 100644 index 0000000000..62ead8be7f --- /dev/null +++ b/core/libs/tests/GuestStdioVariadic.cpp @@ -0,0 +1,114 @@ +#include "prx/libc/include/FileStream.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include <cstdarg> +#include <cstdint> +#include <cstdio> +#include <cstdlib> +#include <cstring> +#include <string> + +extern "C" { +extern FileStream* __stdinp_nid_postfix; +int APS5_VABI vfscanf_nid_postfix(FileStream*, const char*, void*); +int APS5_VABI vscanf_nid_postfix(const char*, void*); +int APS5_VABI scanf_nid_postfix(const char*, ...); +int APS5_VABI vfwprintf_nid_postfix(FileStream*, const char16_t*, void*); +int APS5_VABI fwprintf_nid_postfix(FileStream*, const char16_t*, ...); +int APS5_VABI vswscanf_nid_postfix(const char16_t*, const char16_t*, void*); +int APS5_VABI vswscanf_s_nid_postfix(const char16_t*, const char16_t*, void*); +} + +#ifdef _WIN32 +#define APS5_VA_BEGIN(last) __builtin_sysv_va_list args; __builtin_sysv_va_start(args, last) +#define APS5_VA_END() __builtin_sysv_va_end(args) +#else +#define APS5_VA_BEGIN(last) std::va_list args; va_start(args, last) +#define APS5_VA_END() va_end(args) +#endif + +static void Require(bool value, const char* what) { + if (value) return; + std::fprintf(stderr, "failed: %s\n", what); + std::abort(); +} + +static int APS5_VABI ScanStream(FileStream* stream, const char* format, ...) { + APS5_VA_BEGIN(format); + const int result = vfscanf_nid_postfix(stream, format, args); + APS5_VA_END(); + return result; +} + +static int APS5_VABI ScanInput(const char* format, ...) { + APS5_VA_BEGIN(format); + const int result = vscanf_nid_postfix(format, args); + APS5_VA_END(); + return result; +} + +static int APS5_VABI PrintWide(FileStream* stream, const char16_t* format, ...) { + APS5_VA_BEGIN(format); + const int result = vfwprintf_nid_postfix(stream, format, args); + APS5_VA_END(); + return result; +} + +static int APS5_VABI ScanWide(const char16_t* input, const char16_t* format, ...) { + APS5_VA_BEGIN(format); + const int result = vswscanf_nid_postfix(input, format, args); + APS5_VA_END(); + return result; +} + +static int APS5_VABI ScanWideSized(const char16_t* input, const char16_t* format, ...) { + APS5_VA_BEGIN(format); + const int result = vswscanf_s_nid_postfix(input, format, args); + APS5_VA_END(); + return result; +} + +static std::string Contents(FileStream& stream) { + std::fflush(stream.GetHandle()); + std::rewind(stream.GetHandle()); + std::string text; + for (int c; (c = std::fgetc(stream.GetHandle())) != EOF;) text.push_back(static_cast<char>(c)); + return text; +} + +int main() { + FileStream scanned(std::tmpfile()); + Require(std::fputs("42 answer 7", scanned.GetHandle()) >= 0, "seed the scanned stream"); + std::rewind(scanned.GetHandle()); + int number = 0; + char word[16]{}; + Require(ScanStream(&scanned, "%d %15s", &number, word) == 2, "vfscanf assigns both conversions"); + Require(number == 42 && std::strcmp(word, "answer") == 0, "vfscanf values"); + Require(ScanStream(&scanned, "%d", &number) == 1 && number == 7, "vfscanf continues from the stream position"); + Require(ScanStream(&scanned, "%d", &number) == EOF && number == 7, "vfscanf at end of file"); + scanned.Close(); + + FileStream input(std::tmpfile()); + Require(std::fputs("-5 12", input.GetHandle()) >= 0, "seed the standard input stream"); + std::rewind(input.GetHandle()); + FileStream* const standardInput = __stdinp_nid_postfix; + __stdinp_nid_postfix = &input; + int first = 0, second = 0; + Require(ScanInput("%d", &first) == 1 && first == -5, "vscanf reads the guest standard input"); + Require(scanf_nid_postfix("%d", &second) == 1 && second == 12, "scanf reads the guest standard input"); + __stdinp_nid_postfix = standardInput; + input.Close(); + + FileStream written(std::tmpfile()); + Require(fwprintf_nid_postfix(&written, u"%d:%ls|", 7, u"été") == 6, "fwprintf counts wide characters"); + Require(PrintWide(&written, u"%5.1f", 2.25) == 5, "vfwprintf counts wide characters"); + Require(Contents(written) == "7:\xc3\xa9t\xc3\xa9| 2.2", "the stream holds the formatted text"); + written.Close(); + + std::int32_t a = 0, b = 0; + Require(ScanWide(u" 31 -4", u"%d%d", &a, &b) == 2 && a == 31 && b == -4, "vswscanf"); + Require(ScanWide(u"", u"%d", &a) == EOF, "vswscanf on empty input"); + char16_t letters[4]{}; + Require(ScanWideSized(u"abcdef", u"%3ls", letters, 4u) == 1 && std::u16string(letters) == u"abc", "vswscanf_s %ls with a size"); + Require(ScanWideSized(u"abcdef", u"%ls", letters, 4u) == 0 && letters[0] == 0, "vswscanf_s rejects a too small buffer"); + return 0; +} From 29e9ab36e19fe00308586801da9d03eb92423fee Mon Sep 17 00:00:00 2001 From: Yuri Olive <yuriso1994@gmail.com> Date: Tue, 20 Oct 2026 06:25:40 -0300 Subject: [PATCH 256/269] fix(libSceAudioOut): drop names missing from the pinned aerolib --- .../prx/libSceAudioOut/src/Unimplemented.cpp | 55 ------------------- docs/dev/debt/libSceAudioOut.md | 11 ---- 2 files changed, 66 deletions(-) diff --git a/core/libs/prx/libSceAudioOut/src/Unimplemented.cpp b/core/libs/prx/libSceAudioOut/src/Unimplemented.cpp index 78fd125308..d87292b4c0 100644 --- a/core/libs/prx/libSceAudioOut/src/Unimplemented.cpp +++ b/core/libs/prx/libSceAudioOut/src/Unimplemented.cpp @@ -45,16 +45,6 @@ int APS5_VABI sceAudioOut2LoContextGetQueueLevel(void) { return 0; } -int APS5_VABI sceAudioOut2LoPortSetAttributesEx(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAudioOut2LoPortGetAttributes(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceAudioOutSetSystemDebugState(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -165,11 +155,6 @@ int APS5_VABI sceAudioOutMasteringTerm(void) { return 0; } -int APS5_VABI sceAudioOut2LoPortCreateEx(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceAudioOutSetPadSpkVolume(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -335,26 +320,11 @@ int APS5_VABI sceAudioOut2LoContextQueryMemory(void) { return 0; } -int APS5_VABI sceAudioOut2LoContextQueryMemoryEx(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAudioOut2LoContextGetAttributes(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceAudioOut2LoContextCreate(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceAudioOut2LoContextCreateEx(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceAudioOut2LoContextSetAttributes(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -440,11 +410,6 @@ int APS5_VABI sceAudioOut2GetTvCorrectionInfo(void) { return 0; } -int APS5_VABI sceAudioOut2SetTvCorrectionInfo(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceAudioOut2GetIpcHandle(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -455,21 +420,11 @@ int APS5_VABI sceAudioOut2GetSystemStateEx(void) { return 0; } -int APS5_VABI sceAudioOut2GetSystemDebugState(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceAudioOut2MasteringInitEx(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceAudioOut2SetSpeakerInfo(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceAudioOut2SetDebugHeadphoneMode(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -500,11 +455,6 @@ int APS5_VABI sceAudioOut2PortCreateEx(void) { return 0; } -int APS5_VABI sceAudioOut2PortSetAttributesEx(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceAudioOut2InitializeEx(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -535,9 +485,4 @@ int APS5_VABI sceAudioOut2GetHrtfIdForCronos(void) { return 0; } -int APS5_VABI sceAudioOut2SetHrtfIdForCronos(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - } diff --git a/docs/dev/debt/libSceAudioOut.md b/docs/dev/debt/libSceAudioOut.md index 4ce0e79bfb..905733011a 100644 --- a/docs/dev/debt/libSceAudioOut.md +++ b/docs/dev/debt/libSceAudioOut.md @@ -32,8 +32,6 @@ - [sceAudioOut2LoContextAdvance](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2LoContextPush](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2LoContextGetQueueLevel](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature -- [sceAudioOut2LoPortSetAttributesEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature -- [sceAudioOut2LoPortGetAttributes](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOutSetSystemDebugState](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2LoPortGetState](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOutGetHandleStatusInfo](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature @@ -56,7 +54,6 @@ - [sceAudioOutMasteringSetParam](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOutMasteringGetState](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOutMasteringTerm](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature -- [sceAudioOut2LoPortCreateEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOutSetPadSpkVolume](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOutSetPadJackVolume](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOutSetUsbVolume](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature @@ -90,10 +87,7 @@ - [sceAudioOutSysGetSystemInfo](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOutSysGetOutputInfo](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2LoContextQueryMemory](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature -- [sceAudioOut2LoContextQueryMemoryEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature -- [sceAudioOut2LoContextGetAttributes](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2LoContextCreate](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature -- [sceAudioOut2LoContextCreateEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2LoContextSetAttributes](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2MbusInit](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2MbusSetPortConnections](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature @@ -111,26 +105,21 @@ - [sceAudioOut2ReportRegisterHandler](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2ReportUnregisterHandler](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2GetTvCorrectionInfo](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature -- [sceAudioOut2SetTvCorrectionInfo](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2GetIpcHandle](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2GetSystemStateEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature -- [sceAudioOut2GetSystemDebugState](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2MasteringInitEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature -- [sceAudioOut2SetSpeakerInfo](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2SetDebugHeadphoneMode](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2DebugStateCtrl](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2LoInit](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2LoTerminate](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2ContextBedWrite](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2PortCreateEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature -- [sceAudioOut2PortSetAttributesEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2InitializeEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2Terminate](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2PortRegister](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2PortUnregister](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2GetAmbisonicsEncodeCoefficients](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2GetHrtfIdForCronos](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature -- [sceAudioOut2SetHrtfIdForCronos](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature ### Functional From 7814a12b90aa29bf33c298f25092215cf3c12845 Mon Sep 17 00:00:00 2001 From: Dean Galvin <dean.galvin@live.com> Date: Tue, 20 Oct 2026 22:29:38 +1300 Subject: [PATCH 257/269] revert(ci): run Linux video output tests under Xvfb This reverts the merge of #6932 (fce1fefe3b585ee360ce33fb012f0ceb98ecaa29). Its "Install Xvfb" step fails on the self-hosted Linux runner, which has no xvfb-run and no passwordless sudo, so every Linux build stops before configuring. --- .github/workflows/build.yml | 9 +--- tools/check_skips.py | 19 +++++---- tools/tests/test_check_skips.py | 73 +++++++-------------------------- 3 files changed, 26 insertions(+), 75 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8ca095aa8a..cab59565e5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -430,8 +430,6 @@ jobs: CCACHE_COMPILERCHECK: content MESA_SHADER_CACHE_DIR: /var/cache/anyps5/mesa MESA_SHADER_CACHE_MAX_SIZE: 4G - SDL_VIDEODRIVER: x11 - ANYPS5_GPU: llvmpipe steps: - uses: actions/checkout@v7 with: @@ -465,9 +463,6 @@ jobs: ccache --show-config dpkg-query -W mesa-vulkan-drivers grep -m1 '^model name' /proc/cpuinfo - - name: Install Xvfb - run: | - command -v xvfb-run >/dev/null && command -v xauth >/dev/null || { sudo apt-get update && sudo apt-get install -y --no-install-recommends xvfb xauth; } - name: Install the wild linker shell: bash env: @@ -513,7 +508,7 @@ jobs: echo "CCACHE_PREFIX=$GITHUB_WORKSPACE/tools/compile_slot.sh" >> "$GITHUB_ENV" echo "ANYPS5_COMPILE_SLOT_DIR=$slot_dir" >> "$GITHUB_ENV" echo "ANYPS5_COMPILE_SLOTS=$slots" >> "$GITHUB_ENV" - - run: cmake -S . -B build $CONFIGURE -DANYPS5_ENABLE_SPIRV_TOOLS=ON -DANYPS5_VIDEO_OUT_TEST_DRIVER=x11 "-DCMAKE_EXE_LINKER_FLAGS=$LINKER_FLAGS" "-DCMAKE_SHARED_LINKER_FLAGS=$LINKER_FLAGS" "-DCMAKE_MODULE_LINKER_FLAGS=$LINKER_FLAGS" + - run: cmake -S . -B build $CONFIGURE -DANYPS5_ENABLE_SPIRV_TOOLS=ON "-DCMAKE_EXE_LINKER_FLAGS=$LINKER_FLAGS" "-DCMAKE_SHARED_LINKER_FLAGS=$LINKER_FLAGS" "-DCMAKE_MODULE_LINKER_FLAGS=$LINKER_FLAGS" - name: Check that every test executable is registered with ctest run: python3 tools/check_test_registration.py build - if: ${{ inputs.select }} @@ -534,7 +529,7 @@ jobs: if [ -f build/select.ninja ]; then ninja -C build -f select.ninja -j "$CMAKE_BUILD_PARALLEL_LEVEL" -l "$(nproc)" selected; else cmake --build build -- -l "$(nproc)"; fi - id: libs run: cmake --build build --target libs -- -l "$(nproc)" - - run: xvfb-run -a -s '-screen 0 1920x1080x24 -nolisten tcp' python3 tools/select_tests.py ctest build --test-load "$(nproc)" --output-on-failure --timeout 120 --output-log ctest.log --output-junit ctest.xml + - run: python3 tools/select_tests.py ctest build --test-load "$(nproc)" --output-on-failure --timeout 120 --output-log ctest.log --output-junit ctest.xml env: ANYPS5_REQUIRE_VULKAN: 1 GALLIUM_OVERRIDE_CPU_CAPS: avx diff --git a/tools/check_skips.py b/tools/check_skips.py index 42a3bd8f6e..99628018e3 100644 --- a/tools/check_skips.py +++ b/tools/check_skips.py @@ -3,21 +3,25 @@ import re import xml.etree.ElementTree as ET from pathlib import Path -DEVICE_LIMITATION = re.compile(r"skipped,.*(subgroup|the device (has no|reports|lacks|does not|imports no|flushes|binds fewer)|no usable Vulkan device|host imports are unavailable)", re.IGNORECASE) +DEVICE_LIMITATION = re.compile(r"skipped,.*(subgroup|the device (has no|reports|lacks|does not|imports no|flushes|binds fewer)|no display|no usable Vulkan device|host imports are unavailable)", re.IGNORECASE) NO_DISPLAY = re.compile(r"skipped,.*no display", re.IGNORECASE) NO_VULKAN_DEVICE = re.compile(r"skipped,.*no usable Vulkan device", re.IGNORECASE) HIGH_REFRESH_LIMITATION = re.compile(r"current display does not provide 120 Hz", re.IGNORECASE) -SKIP_RETURN_CODE = re.compile(r"SKIP_RETURN_CODE=\d+") WINDOWS_PREFIX = "agc" def high_refresh_limitation(output): - reasons = [line.strip()[len("skipped,"):].strip() for line in output.splitlines() - if line.strip().lower().startswith("skipped,")] + reasons = [] + for line in output.splitlines(): + reason = line.strip() + if reason.lower().startswith("skipped,"): + reason = reason[len("skipped,"):].strip() + if reason: + reasons.append(reason) return bool(reasons) and all(HIGH_REFRESH_LIMITATION.fullmatch(reason) for reason in reasons) @@ -31,10 +35,7 @@ def skips(path): name = case.get("name") if not name: raise ValueError(f"{path}: skipped testcase has no name") - output = ["skipped, " + line.strip() - for reason in (skipped.get("message", ""), skipped.text or "") - for line in reason.splitlines() - if line.strip() and not SKIP_RETURN_CODE.fullmatch(line.strip())] + output = ["skipped, " + reason.strip() for reason in (skipped.get("message", ""), skipped.text or "") if reason.strip()] output += [case.findtext(channel) or "" for channel in ("system-out", "system-err")] yield name, "\n".join(output) @@ -59,7 +60,7 @@ if __name__ == "__main__": expected = f"any {WINDOWS_PREFIX}* test, which the runner has no Vulkan driver for, or a test that reports no display or Vulkan device or a display without a 120 Hz mode" else: unexpected = [name for name, output in found if not DEVICE_LIMITATION.search(output) and not high_refresh_limitation(output)] - expected = "any test that reports the device limitation it needs, such as narrow subgroups, a missing extension or a display without a 120 Hz mode" + expected = "any test that reports the device limitation it needs, such as narrow subgroups, a missing extension, no display for a Vulkan window or a display without a 120 Hz mode" for name in unexpected: print(f"error: {name} skipped") if unexpected: diff --git a/tools/tests/test_check_skips.py b/tools/tests/test_check_skips.py index cd205bd084..f4de797f05 100644 --- a/tools/tests/test_check_skips.py +++ b/tools/tests/test_check_skips.py @@ -23,6 +23,7 @@ LIMITATIONS = [ "skipped, the device has no compute queue", "skipped, the device reports 4 lanes", "skipped, subgroup size 8", + "skipped, no display", ] OTHER = [ @@ -57,39 +58,22 @@ class CheckSkipsTests(unittest.TestCase): self.assertEqual(result.returncode, 0, result.stdout + result.stderr) self.assertIn("1 test(s) skipped, all expected", result.stdout) - def test_high_refresh_skip_ignores_host_specific_startup_logs(self): - reason = "current display does not provide 120 Hz" - report = ( - f'<skipped message="{reason}"/><system-out>' - '[src/HostCpu.cpp:193 operator()] host CPU flags differ by runner\n' - '[src/VulkanDevice.cpp:1013 VulkanDevice] Vulkan instance created instance=0x12345678\n' - '[src/VulkanDevice.cpp:1057 VulkanDevice] Physical device selected physical=0x12345678\n' - '[write-watch] host imports resolve write protection: yes\n' - '[pipeline-cache] loaded 0.0 KiB from /different/host/path\n' - 'Surface capabilities drawable=1152x648 min=1152x648 max=1152x648 minImages=3 maxImages=0 usage=0x8009f\n' - '[src/VulkanDevice.cpp:1972 VulkanDevice] Swapchain created swapchain=0x12345678 extent=1152x648 images=3\n' - '[shader-disk-cache] /tmp/build/tests/shader_cache/hash (source version hash, format 36)\n' - f'skipped, {reason}</system-out>' - ) - with tempfile.TemporaryDirectory() as directory: - junit = Path(directory) / "junit.xml" - junit.write_text(f'<testsuite><testcase name="guest_video_out_high_frame_rate">{report}</testcase></testsuite>', encoding="utf-8") - for job in ("linux", "windows"): - with self.subTest(job=job): - result = subprocess.run( - [sys.executable, "-I", str(TOOL), str(junit), "--job", job], - capture_output=True, text=True) - self.assertEqual(result.returncode, 0, result.stdout + result.stderr) - self.assertIn("1 test(s) skipped, all expected", result.stdout) - def test_high_refresh_reason_cannot_hide_additional_diagnostics(self): reason = "current display does not provide 120 Hz" known = f"skipped, {reason}" extra = "unexpected failure detail" reports = ( - f'<skipped message="{reason}">{reason}\nunexpected failure detail</skipped>', - f'<skipped/><system-out>{known}\nskipped, {extra}</system-out>', - f'<skipped message="{reason}"/><system-err>skipped, unsupported mode</system-err>', + f'<skipped/><system-out>{known}\n{extra}</system-out>', + f'<skipped/><system-out>{extra}\n{known}</system-out>', + f'<skipped/><system-err>{known}\n{extra}</system-err>', + f'<skipped/><system-out>{known}</system-out><system-err>{extra}</system-err>', + f'<skipped/><system-out>{extra}</system-out><system-err>{known}</system-err>', + f'<skipped message="{reason}"/><system-err>{extra}</system-err>', + f'<skipped>{reason}</skipped><system-out>{extra}</system-out>', + f'<skipped message="{reason}">{extra}</skipped>', + f'<skipped message="{extra}"/><system-out>{known}</system-out>', + f'<skipped>{reason}\n{extra}</skipped>', + f'<skipped/><system-out>{known}\n{known}\n{extra}</system-out>', ) with tempfile.TemporaryDirectory() as directory: junit = Path(directory) / "junit.xml" @@ -134,25 +118,6 @@ class CheckSkipsTests(unittest.TestCase): self.assertIn("agc_disabled", result.stderr) self.assertIn("disabled", result.stderr) - def test_ctest_skip_return_code_metadata_does_not_hide_high_refresh_reason(self): - reason = "current display does not provide 120 Hz" - report = ( - '<skipped message="SKIP_RETURN_CODE=77"/>' - f'<system-out>skipped, {reason}</system-out>' - ) - with tempfile.TemporaryDirectory() as directory: - junit = Path(directory) / "junit.xml" - junit.write_text( - f'<testsuite><testcase name="guest_video_out_high_frame_rate">{report}</testcase></testsuite>', - encoding="utf-8") - for job in ("linux", "windows"): - with self.subTest(job=job): - result = subprocess.run( - [sys.executable, "-I", str(TOOL), str(junit), "--job", job], - capture_output=True, text=True) - self.assertEqual(result.returncode, 0, result.stdout + result.stderr) - self.assertIn("1 test(s) skipped, all expected", result.stdout) - def test_device_limitations_match(self): for message in LIMITATIONS: with self.subTest(message=message): @@ -178,16 +143,6 @@ class CheckSkipsTests(unittest.TestCase): self.assertIn("error: odd skipped", result.stdout) self.assertNotIn("error: geometry skipped", result.stdout) - def test_no_display_skip_is_unexpected_on_linux(self): - with tempfile.TemporaryDirectory() as directory: - junit = Path(directory) / "junit.xml" - junit.write_text( - '<testsuite><testcase name="video_out_flip_eop"><skipped/><system-out>skipped, no display</system-out></testcase></testsuite>', - encoding="utf-8") - result = subprocess.run([sys.executable, "-I", str(TOOL), str(junit), "--job", "linux"], capture_output=True, text=True) - self.assertEqual(result.returncode, 1) - self.assertIn("error: video_out_flip_eop skipped", result.stdout) - def test_reason_on_its_own_line_inside_skipped_is_expected(self): for reason, job in (("the device lacks geometryShader", "linux"), ("no display", "windows")): with self.subTest(job=job), tempfile.TemporaryDirectory() as directory: @@ -263,10 +218,10 @@ class SkipReportTests(unittest.TestCase): self.assertEqual(len(found), 1) self.assertRegex(found[0][1], check_skips.DEVICE_LIMITATION) - def test_stderr_preserves_a_no_display_reason_without_excusing_linux_skip(self): + def test_stderr_preserves_the_device_reason(self): found = self.skipped('<testsuite><testcase name="gpu"><skipped/><system-err>skipped, no display</system-err></testcase></testsuite>') self.assertEqual(len(found), 1) - self.assertNotRegex(found[0][1], check_skips.DEVICE_LIMITATION) + self.assertRegex(found[0][1], check_skips.DEVICE_LIMITATION) def test_unnamed_skipped_test_is_an_error(self): with self.assertRaises(ValueError): From 4c849bcadfcc6f535a19c74d520c90e1f5846b98 Mon Sep 17 00:00:00 2001 From: primesoul-web <primesoul-web@users.noreply.github.com> Date: Tue, 20 Oct 2026 09:32:35 +0000 Subject: [PATCH 258/269] fix(libSceIme): discard the edit when a caret move drops the filtered commit --- core/libs/prx/libSceIme/Export.cpp | 9 ++++++-- core/libs/tests/GuestImeOpen.cpp | 34 ++++++++++++++++++++++++++++++ docs/dev/debt/libSceIme.md | 2 +- 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceIme/Export.cpp b/core/libs/prx/libSceIme/Export.cpp index ea355e802e..a2979dea88 100644 --- a/core/libs/prx/libSceIme/Export.cpp +++ b/core/libs/prx/libSceIme/Export.cpp @@ -554,8 +554,13 @@ int APS5_VABI sceImeUpdate(EventHandler handler) { std::u16string filtered; const bool accepted = RunTextFilter(filter, filterText, filterMaxLength, filterMultiline, &filtered); std::lock_guard lock(g_imeMutex); - if (g_session && g_session->id == sessionId && g_session->revision == sessionRevision) { - CommitFilteredEdit(*g_session, accepted ? &filtered : nullptr, filterOldText); + if (g_session && g_session->id == sessionId) { + if (g_session->revision == sessionRevision) { + CommitFilteredEdit(*g_session, accepted ? &filtered : nullptr, filterOldText); + } else if (g_session->text == filterText) { + g_session->text = filterOldText; + NormalizeCaret(*g_session); + } } continue; } diff --git a/core/libs/tests/GuestImeOpen.cpp b/core/libs/tests/GuestImeOpen.cpp index a1ce465687..ce516d5530 100644 --- a/core/libs/tests/GuestImeOpen.cpp +++ b/core/libs/tests/GuestImeOpen.cpp @@ -146,6 +146,22 @@ int32_t APS5_VABI SetInsideFilter(char16_t* outText, uint32_t* outTextLength, co return 0; } +bool g_caretInsideFirstCall = true; +int g_caretInsideRc = -1; + +int32_t APS5_VABI SetCaretInsideFilter(char16_t* outText, uint32_t* outTextLength, const char16_t* sourceText, uint32_t sourceTextLength) { + if (g_caretInsideFirstCall) { + g_caretInsideFirstCall = false; + const Caret caret{0.0f, 0.0f, 0, 0}; + g_caretInsideRc = sceImeSetCaret(&caret); + } + for (uint32_t i = 0; i < sourceTextLength; ++i) { + outText[i] = sourceText[i]; + } + *outTextLength = sourceTextLength; + return 0; +} + void Require(bool value, const char* message) { if (value) return; std::fprintf(stderr, "%s\n", message); @@ -494,6 +510,24 @@ void CheckTextFilter() { Require(std::u16string(buffers.text) == u"zz", "the setText inside the filter wins"); Require(std::u16string(reinterpret_cast<const char16_t*>(buffers.work)) == u"zz", "the setText inside the filter wins in the work copy"); Require(sceImeClose_nid_postfix() == 0, "close the set-inside session"); + + buffers = Buffers{}; + param = MakeParam(buffers, 8); + std::memcpy(buffers.text, u"ab", sizeof(u"ab")); + param.filter = reinterpret_cast<ImeTextFilter>(&SetCaretInsideFilter); + Require(sceImeOpen_nid_postfix(¶m, nullptr) == 0, "open with a filter that moves the caret"); + Drain(); + g_caretInsideFirstCall = true; + g_caretInsideRc = -1; + Publish(ImeHostInputKind::Text, "c"); + Require(Drain().empty(), "an edit whose filter moved the caret sends no event"); + Require(g_caretInsideRc == 0, "sceImeSetCaret inside the filter returns 0"); + Require(std::u16string(buffers.text) == u"ab", "the discarded edit is not in the buffer"); + Publish(ImeHostInputKind::Text, "d"); + RequireText(Drain().at(0), EventUpdateText, u"dab", 1, 0, 1); + Require(std::u16string(buffers.text) == u"dab", "the edit after the discard builds on the old text"); + Require(std::u16string(reinterpret_cast<const char16_t*>(buffers.work)) == u"dab", "the edit after the discard reaches the work copy"); + Require(sceImeClose_nid_postfix() == 0, "close the caret-inside session"); } void CheckHostEvents() { diff --git a/docs/dev/debt/libSceIme.md b/docs/dev/debt/libSceIme.md index bcce37a189..9d4d0abbdd 100644 --- a/docs/dev/debt/libSceIme.md +++ b/docs/dev/debt/libSceIme.md @@ -58,4 +58,4 @@ - [sceImeKeyboardOpen, sceImeKeyboardClose](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - opening a keyboard that is already open for the same user returns `0x80bc0001` (busy) and closing one that is not open returns `0x80bc0002`, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/1211958b4eb38256cc0c5c7ba92d668820c05389/src/libs/ime.cpp) does; it tracks one keyboard for all users, while the open state is kept per user id here, and the PS5 result is unverified - [sceImeKeyboardGetInfo](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) returns `0x80bc0023` (no resource id) for every id while a keyboard is open, since the IME keyboard never reports a connected device. [sceImeKeyboardSetMode](../../../core/libs/prx/libSceIme/Export.cpp) validates the mode bits but the mode has no effect, as no IME keyboard events are delivered. - [sceImeKeyboardGetResourceId](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) reports that no USB keyboard is connected (`0x80bc0004`, as [shadPS4](https://github.com/shadps4-emu/shadPS4) does) because IME keyboard events are not delivered to the handler, although [libSceKeyboard](../../../core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp) exposes the host keyboard -- [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 and the edit 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 +- [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 From 445d7192f9905fba74cbdf861297e80df3268a49 Mon Sep 17 00:00:00 2001 From: Cankiee <13728354+Cankiee@users.noreply.github.com> Date: Sun, 18 Oct 2026 10:06:53 +0200 Subject: [PATCH 259/269] fix(tools): handle git command failures gracefully --- tools/check_conventions.py | 21 +++++++++++++- tools/test_check_conventions_git.py | 45 +++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 tools/test_check_conventions_git.py diff --git a/tools/check_conventions.py b/tools/check_conventions.py index c55fea49e7..8c2ed750d8 100644 --- a/tools/check_conventions.py +++ b/tools/check_conventions.py @@ -66,11 +66,30 @@ SECTION = re.compile(r"^###(?!#)\s*(.+?)\s*$", re.M) KEY = re.compile(r"^( *)([A-Za-z0-9_-]+|'[^']*'|\"[^\"]*\"):(?:\s+(.*?))?\s*$") + def git(*args, text=True): - output = subprocess.run(["git", "-c", "core.quotePath=false", *args], capture_output=True, check=True).stdout + try: + result = subprocess.run( + ["git", "-c", "core.quotePath=false", *args], + capture_output=True, + check=True, + ) + except subprocess.CalledProcessError as error: + message = error.stderr.decode("utf-8", errors="replace").strip() + print( + f"Git command failed: {message or 'unknown Git error'}", + file=sys.stderr, + ) + raise SystemExit(2) from None + except FileNotFoundError: + print("Git executable not found. Please install Git.", file=sys.stderr) + raise SystemExit(2) from None + + output = result.stdout return output.decode("utf-8", errors="replace") if text else output + class Check: def __init__(self, base, head): self.head = head diff --git a/tools/test_check_conventions_git.py b/tools/test_check_conventions_git.py new file mode 100644 index 0000000000..bc18595532 --- /dev/null +++ b/tools/test_check_conventions_git.py @@ -0,0 +1,45 @@ + +import subprocess +import sys +import unittest +from pathlib import Path + + +class GitErrorHandlingTests(unittest.TestCase): + def test_invalid_base_branch(self): + script = Path(__file__).with_name("check_conventions.py") + result = subprocess.run( + [ + sys.executable, + str(script), + "--base", + "this-branch-does-not-exist", + ], + capture_output=True, + text=True, + ) + + self.assertEqual(result.returncode, 2) + self.assertIn("Git command failed:", result.stderr) + self.assertNotIn("Traceback", result.stderr) + + def test_invalid_head_branch(self): + script = Path(__file__).with_name("check_conventions.py") + result = subprocess.run( + [ + sys.executable, + str(script), + "--head", + "this-branch-does-not-exist", + ], + capture_output=True, + text=True, + ) + + self.assertEqual(result.returncode, 2) + self.assertIn("Git command failed:", result.stderr) + self.assertNotIn("Traceback", result.stderr) + + +if __name__ == "__main__": + unittest.main() From 609017e51dc6f86f776aa0520ecc8207d9bbef2b Mon Sep 17 00:00:00 2001 From: Cankiee <13728354+Cankiee@users.noreply.github.com> Date: Sun, 18 Oct 2026 17:10:40 +0200 Subject: [PATCH 260/269] test(tools): register git error regression tests with ctest --- CMakeLists.txt | 2 + tools/test_check_conventions_git.py | 45 ----------------------- tools/tests/test_check_conventions_git.py | 40 ++++++++++++++++++++ 3 files changed, 42 insertions(+), 45 deletions(-) delete mode 100644 tools/test_check_conventions_git.py create mode 100644 tools/tests/test_check_conventions_git.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 094bf25cf3..bfd144d963 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -170,6 +170,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_wrapped_declarations.py) add_test(NAME conventions_ci_gate COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_ci_gate.py) + add_test(NAME conventions_git_errors COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_git.py) add_test(NAME conventions_doc_links COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_doc_links.py) add_test(NAME conventions_unknown_nids COMMAND ${Python3_EXECUTABLE} diff --git a/tools/test_check_conventions_git.py b/tools/test_check_conventions_git.py deleted file mode 100644 index bc18595532..0000000000 --- a/tools/test_check_conventions_git.py +++ /dev/null @@ -1,45 +0,0 @@ - -import subprocess -import sys -import unittest -from pathlib import Path - - -class GitErrorHandlingTests(unittest.TestCase): - def test_invalid_base_branch(self): - script = Path(__file__).with_name("check_conventions.py") - result = subprocess.run( - [ - sys.executable, - str(script), - "--base", - "this-branch-does-not-exist", - ], - capture_output=True, - text=True, - ) - - self.assertEqual(result.returncode, 2) - self.assertIn("Git command failed:", result.stderr) - self.assertNotIn("Traceback", result.stderr) - - def test_invalid_head_branch(self): - script = Path(__file__).with_name("check_conventions.py") - result = subprocess.run( - [ - sys.executable, - str(script), - "--head", - "this-branch-does-not-exist", - ], - capture_output=True, - text=True, - ) - - self.assertEqual(result.returncode, 2) - self.assertIn("Git command failed:", result.stderr) - self.assertNotIn("Traceback", result.stderr) - - -if __name__ == "__main__": - unittest.main() diff --git a/tools/tests/test_check_conventions_git.py b/tools/tests/test_check_conventions_git.py new file mode 100644 index 0000000000..02a7d8f057 --- /dev/null +++ b/tools/tests/test_check_conventions_git.py @@ -0,0 +1,40 @@ + +import subprocess +import sys +import unittest +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[2] +CHECKER = REPO_ROOT / "tools" / "check_conventions.py" +INVALID_REVISION = "this-branch-does-not-exist" + + +class GitErrorHandlingTests(unittest.TestCase): + def check_invalid_revision(self, option): + result = subprocess.run( + [ + sys.executable, + str(CHECKER), + option, + INVALID_REVISION, + ], + cwd=REPO_ROOT, + capture_output=True, + text=True, + ) + + self.assertEqual(result.returncode, 2) + self.assertIn("Git command failed:", result.stderr) + self.assertIn(INVALID_REVISION, result.stderr) + self.assertNotIn("Traceback", result.stderr) + + def test_invalid_base_branch(self): + self.check_invalid_revision("--base") + + def test_invalid_head_branch(self): + self.check_invalid_revision("--head") + + +if __name__ == "__main__": + unittest.main() From 4dbcc94e9c0b00af18877369184b1a999e714566 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= <adriadam10@gmail.com> Date: Tue, 20 Oct 2026 11:53:06 +0200 Subject: [PATCH 261/269] test(tools): pass a valid revision for the option not under test --- tools/tests/test_check_conventions_git.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tools/tests/test_check_conventions_git.py b/tools/tests/test_check_conventions_git.py index 02a7d8f057..5502d752d0 100644 --- a/tools/tests/test_check_conventions_git.py +++ b/tools/tests/test_check_conventions_git.py @@ -12,12 +12,13 @@ INVALID_REVISION = "this-branch-does-not-exist" class GitErrorHandlingTests(unittest.TestCase): def check_invalid_revision(self, option): + valid = "--head" if option == "--base" else "--base" + arguments = [valid, "HEAD", option, INVALID_REVISION] result = subprocess.run( [ sys.executable, str(CHECKER), - option, - INVALID_REVISION, + *arguments, ], cwd=REPO_ROOT, capture_output=True, From 8a47d68ba1cdd6dc22d30987f5bf1ce5433df487 Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis <motivated.it@gmail.com> Date: Sun, 18 Oct 2026 17:34:43 +0300 Subject: [PATCH 262/269] test(agc): verify sampled-cache fixture device selection --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 8 ++ .../tests/execution/SampledCacheBudget.cpp | 83 +++++++++++++++++++ 2 files changed, 91 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 61dd26a9a9..54c415d446 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2350,6 +2350,14 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_sampled_cache_budget_memory PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77) add_test(NAME agc_driver_sampled_cache_budget_without_storage_image_multisample COMMAND agc_driver_sampled_cache_budget_tests without-storage-image-multisample) set_tests_properties(agc_driver_sampled_cache_budget_without_storage_image_multisample PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30) + add_test(NAME agc_sampled_cache_device_selection COMMAND agc_driver_sampled_cache_budget_tests device-selection-cpu) + set_tests_properties(agc_sampled_cache_device_selection PROPERTIES TIMEOUT 30) + add_test(NAME agc_driver_sampled_cache_device_selection COMMAND agc_driver_sampled_cache_budget_tests device-selection) + set_tests_properties(agc_driver_sampled_cache_device_selection PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_sampled_cache_device_selection agc_driver_sampled_cache_device_selection PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") + endif() add_test(NAME agc_driver_resource_cache_storage_admission COMMAND agc_driver_sampled_cache_budget_tests resource-admission) add_test(NAME agc_driver_resource_cache_oversized_storage_admission COMMAND agc_driver_sampled_cache_budget_tests resource-oversized) add_test(NAME agc_driver_resource_cache_storage_clear COMMAND agc_driver_sampled_cache_budget_tests resource-clear) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp index c9dc46e856..ebeab9dcff 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp @@ -17,7 +17,9 @@ #endif #include <windows.h> #endif +#include <algorithm> #include <array> +#include <cctype> #include <chrono> #include <cstdint> #include <cstdio> @@ -26,8 +28,11 @@ #include <iostream> #include <memory> #include <mutex> +#include <optional> +#include <span> #include <stdexcept> #include <string> +#include <string_view> #include <thread> #include <unordered_map> #include <vector> @@ -48,6 +53,67 @@ constexpr std::size_t AllocationBytes = BlockBytes + RegistrationGapBytes; constexpr std::uint64_t Budget = 1ull << 20u; constexpr std::uint64_t Ceiling = Budget + Budget / 4u; +std::optional<std::size_t> selectDevice(std::span<const VkPhysicalDeviceProperties> devices, std::string_view request) { + const auto rankDeviceType = [](VkPhysicalDeviceType type) { + switch (type) { + case VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU: return 3; + case VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU: return 2; + case VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU: return 1; + default: return 0; + } + }; + std::optional<std::size_t> selected; + int selectedRank = -1; + for (std::size_t index = 0; index < devices.size(); ++index) { + const auto& candidate = devices[index]; + if (candidate.apiVersion < VK_API_VERSION_1_1) continue; + const std::string_view name(candidate.deviceName); + if (!request.empty() && std::search(name.begin(), name.end(), request.begin(), request.end(), [](char left, char right) { + return std::tolower(static_cast<unsigned char>(left)) == std::tolower(static_cast<unsigned char>(right)); + }) == name.end()) continue; + const int rank = request.empty() ? rankDeviceType(candidate.deviceType) : 4; + if (rank <= selectedRank) continue; + selected = index; + selectedRank = rank; + } + return selected; +} + +void deviceSelectionTests() { + const auto properties = [](const char* name, VkPhysicalDeviceType type) { + VkPhysicalDeviceProperties result{}; + result.apiVersion = VK_API_VERSION_1_1; + result.deviceType = type; + std::memcpy(result.deviceName, name, std::strlen(name) + 1u); + return result; + }; + std::array devices{ + properties("AMD Radeon Graphics", VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU), + properties("NVIDIA GeForce RTX 5080", VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU), + properties("Virtual GPU", VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU), + properties("CPU", VK_PHYSICAL_DEVICE_TYPE_CPU), + }; + Require(!selectDevice({}, {}).has_value(), "an empty device list selected a device"); + Require(selectDevice(devices, {}) == 1u, "default selection did not prefer the discrete GPU"); + Require(selectDevice(devices, "AMD") == 0u, "the AMD request did not select the integrated GPU"); + Require(selectDevice(devices, "NVIDIA") == 1u, "the NVIDIA request did not select the discrete GPU"); + Require(selectDevice(devices, "aMd rAdEoN") == 0u, "a requested integrated GPU lost to an unrelated discrete GPU"); + Require(selectDevice(devices, "rtx 5080") == 1u, "a partial GPU name did not match without regard to case"); + Require(selectDevice(devices, "Virtual") == 2u, "a requested virtual GPU was not selected"); + Require(selectDevice(devices, "cpu") == 3u, "a requested CPU device was not selected"); + Require(!selectDevice(devices, "missing").has_value(), "an unmatched request selected another GPU"); + Require(!selectDevice(devices, "NVIDIA GeForce RTX 5080 with a longer name").has_value(), "a request longer than the device name matched"); + Require(selectDevice(std::span(devices).subspan(2), {}) == 0u, "default selection did not prefer the virtual GPU to the CPU"); + devices[1].apiVersion = VK_API_VERSION_1_0; + Require(selectDevice(devices, {}) == 0u, "default selection did not prefer the integrated GPU to the virtual GPU"); + Require(!selectDevice(devices, "NVIDIA").has_value(), "a requested device without Vulkan 1.1 was selected"); + devices[1] = devices[0]; + Require(selectDevice(devices, {}) == 0u && selectDevice(devices, "AMD") == 0u, "equally ranked matches did not retain enumeration order"); + for (auto& device : devices) device.apiVersion = VK_API_VERSION_1_0; + Require(!selectDevice(devices, {}).has_value(), "a list without Vulkan 1.1 selected a device"); + std::puts("sampled-cache device selection CPU tests passed"); +} + class Device { public: explicit Device(bool enableStorageImageMultisample = true) { @@ -122,6 +188,7 @@ public: function<PFN_vkGetPhysicalDeviceMemoryProperties>("vkGetPhysicalDeviceMemoryProperties")(context.physical, &context.memory); VkPhysicalDeviceProperties properties{}; function<PFN_vkGetPhysicalDeviceProperties>("vkGetPhysicalDeviceProperties")(context.physical, &properties); + std::printf("sampled-cache fixture Vulkan device: %s vendor=0x%04x device=0x%04x\n", properties.deviceName, properties.vendorID, properties.deviceID); context.limits = properties.limits; context.bufferDeviceAddress = true; context.formatProperties = function<PFN_vkGetPhysicalDeviceFormatProperties>("vkGetPhysicalDeviceFormatProperties"); @@ -141,6 +208,14 @@ public: ~Device() { release(); } const Context& GetContext() const { return context; } + void VerifySelection() const { + VkPhysicalDeviceProperties properties{}; + function<PFN_vkGetPhysicalDeviceProperties>("vkGetPhysicalDeviceProperties")(context.physical, &properties); + const char* request = std::getenv("ANYPS5_GPU"); + Require(selectDevice(std::span(&properties, 1), request != nullptr ? request : "").has_value(), "the sampled-cache fixture physical device does not match ANYPS5_GPU"); + std::puts("sampled-cache fixture physical device selection verified"); + } + private: template<typename TFunction> TFunction function(const char* name) const { @@ -783,6 +858,10 @@ int main(int argc, char** argv) { try { const bool memory = argc > 1 && std::string(argv[1]) == "memory"; const std::string mode = argc > 1 ? argv[1] : ""; + if (mode == "device-selection-cpu") { + deviceSelectionTests(); + return 0; + } const bool resources = mode == "resource-admission" || mode == "resource-oversized" || mode == "resource-clear"; const bool disableStorageImageMultisample = mode == "without-storage-image-multisample"; #ifdef _WIN32 @@ -798,6 +877,10 @@ int main(int argc, char** argv) { std::printf("skipped, no usable Vulkan device: %s\n", error.what()); return VulkanTestSkipped; } + if (mode == "device-selection") { + device->VerifySelection(); + return 0; + } auto context = device->GetContext(); if (disableStorageImageMultisample) { PlaceholderImages placeholders(context); From b71f1b8000c2b481b33d3b63927e7e09985384cf Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis <motivated.it@gmail.com> Date: Sun, 18 Oct 2026 17:35:43 +0300 Subject: [PATCH 263/269] fix(agc): honor GPU selection in sampled-cache tests --- .../tests/execution/SampledCacheBudget.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp index ebeab9dcff..e96d71cc14 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp @@ -137,7 +137,13 @@ public: Require(count != 0, "no Vulkan device"); std::vector<VkPhysicalDevice> devices(count); Check(enumerate(instance, &count, devices.data()), "vkEnumeratePhysicalDevices"); - context.physical = devices.front(); + const auto physicalProperties = function<PFN_vkGetPhysicalDeviceProperties>("vkGetPhysicalDeviceProperties"); + std::vector<VkPhysicalDeviceProperties> deviceProperties(devices.size()); + for (std::size_t index = 0; index < devices.size(); ++index) physicalProperties(devices[index], &deviceProperties[index]); + const char* requestedName = std::getenv("ANYPS5_GPU"); + const auto selectedIndex = selectDevice(deviceProperties, requestedName != nullptr ? requestedName : ""); + Require(selectedIndex.has_value(), requestedName != nullptr && *requestedName != '\0' ? std::string("no Vulkan 1.1 device matching ANYPS5_GPU=\"") + requestedName + "\"" : "no Vulkan 1.1 device"); + context.physical = devices[*selectedIndex]; const auto extensions = function<PFN_vkEnumerateDeviceExtensionProperties>("vkEnumerateDeviceExtensionProperties"); Check(extensions(context.physical, nullptr, &count, nullptr), "vkEnumerateDeviceExtensionProperties"); std::vector<VkExtensionProperties> available(count); @@ -873,7 +879,8 @@ int main(int argc, char** argv) { try { device = std::make_unique<Device>(!disableStorageImageMultisample); } catch (const std::exception& error) { - if (std::getenv("ANYPS5_REQUIRE_VULKAN") != nullptr) throw; + const char* requestedName = std::getenv("ANYPS5_GPU"); + if (std::getenv("ANYPS5_REQUIRE_VULKAN") != nullptr || (requestedName != nullptr && *requestedName != '\0')) throw; std::printf("skipped, no usable Vulkan device: %s\n", error.what()); return VulkanTestSkipped; } From 55e4850fb7a7fc88a410b3953aa0147992c78990 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= <adriadam10@gmail.com> Date: Tue, 20 Oct 2026 12:10:59 +0200 Subject: [PATCH 264/269] test(agc): skip the CPU selection test when the execution fixture is stubbed Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 54c415d446..e3420a69ba 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2351,7 +2351,7 @@ if(BUILD_TESTING) add_test(NAME agc_driver_sampled_cache_budget_without_storage_image_multisample COMMAND agc_driver_sampled_cache_budget_tests without-storage-image-multisample) set_tests_properties(agc_driver_sampled_cache_budget_without_storage_image_multisample PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30) add_test(NAME agc_sampled_cache_device_selection COMMAND agc_driver_sampled_cache_budget_tests device-selection-cpu) - set_tests_properties(agc_sampled_cache_device_selection PROPERTIES TIMEOUT 30) + set_tests_properties(agc_sampled_cache_device_selection PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) add_test(NAME agc_driver_sampled_cache_device_selection COMMAND agc_driver_sampled_cache_budget_tests device-selection) set_tests_properties(agc_driver_sampled_cache_device_selection PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30) if(WIN32) From 7b05271abb494c3577faaaa4ff2a186a1abd68d2 Mon Sep 17 00:00:00 2001 From: MarcZX100 <MarcZX100@users.noreply.github.com> Date: Sun, 18 Oct 2026 13:31:50 +0000 Subject: [PATCH 265/269] fix(libkernel): support kern.rng_pseudo MIB access --- .../System/src/SystemConfiguration.cpp | 13 ++++- core/libs/tests/GuestSystemConfiguration.cpp | 52 ++++++++++++++++++- docs/dev/debt/libkernel.md | 2 +- 3 files changed, 64 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp index d02e41cc8d..74b358cb5d 100644 --- a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp +++ b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp @@ -5,6 +5,7 @@ #include "prx/libkernel/System/include/HostCpu.hpp" #include "SceTypes.hpp" #include <algorithm> +#include <array> #include <cstddef> #include <cstdint> #include <cstring> @@ -74,7 +75,7 @@ const SystemString* FindSystemString(const int* name) { } bool IsLeafMib(const int* name) { - return (name[0] == 6 && (name[1] == 2 || name[1] == 3 || name[1] == 5 || name[1] == 7)) || (name[0] == 1 && name[1] == 37); + return (name[0] == 6 && (name[1] == 2 || name[1] == 3 || name[1] == 5 || name[1] == 7)) || (name[0] == 1 && (name[1] == 37 || name[1] == 50)); } constexpr std::int32_t VmEntryDefaultType = 1; @@ -280,6 +281,15 @@ int APS5_VABI sysctl_nid_postfix(const int* name, std::uint32_t nameLength, void *oldLength = count; return 0; } + if (nameLength == 2 && name[0] == 1 && name[1] == 50) { + if (newValue != nullptr) throw std::runtime_error(std::string(__func__) + ": writing kern.rng_pseudo is unverified"); + std::array<std::uint8_t, 64> bytes{}; + if (oldValue != nullptr && oldLength != nullptr) { + static std::random_device device; + for (auto& byte : bytes) byte = static_cast<std::uint8_t>(device()); + } + return CopyOut(bytes.data(), bytes.size(), oldValue, oldLength); + } if (nameLength > 2 && (IsLeafMib(name) || FindSystemString(name) != nullptr)) { *__error_nid_postfix() = 20; return -1; @@ -387,6 +397,7 @@ int APS5_VABI sysctlnametomib_nid_postfix(const char* name, int* mib, std::size_ {"hw.physmem", {6, 5}, 2}, {"hw.realmem", {6, 12}, 2}, {"kern.arandom", {1, 37}, 2}, + {"kern.rng_pseudo", {1, 50}, 2}, {"kern.proc.pathname", {1, 14, 12}, 3}, }; constexpr std::size_t MaximumPath = 1024; diff --git a/core/libs/tests/GuestSystemConfiguration.cpp b/core/libs/tests/GuestSystemConfiguration.cpp index 74bcc6a1a5..bebbab8f7b 100644 --- a/core/libs/tests/GuestSystemConfiguration.cpp +++ b/core/libs/tests/GuestSystemConfiguration.cpp @@ -258,7 +258,7 @@ static void CheckSystemStringNames() { } } static void CheckLeafWithTrailingComponent() { - const int leaves[][2] = {{6, 3}, {6, 2}, {6, 5}, {6, 7}, {1, 37}}; + const int leaves[][2] = {{6, 3}, {6, 2}, {6, 5}, {6, 7}, {1, 37}, {1, 50}}; for (const auto& leaf : leaves) { const int mib[] = {leaf[0], leaf[1], 0}; char buffer[8] = {}; @@ -467,6 +467,43 @@ static void CheckVmmapSysctl() { const int tooLong[] = {1, 14, 32, self, 0}; Require(sysctl_nid_postfix(tooLong, 5, nullptr, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 22); } +static void CheckPseudoRandomMibSysctl() { + const int mib[] = {1, 50}; + *__error_nid_postfix() = 13; + std::size_t length = 7; + Require(sysctl_nid_postfix(mib, 2, nullptr, &length, nullptr, 0) == 0 && length == 64); + Require(*__error_nid_postfix() == 13); + Require(sysctl_nid_postfix(mib, 2, nullptr, nullptr, nullptr, 0) == 0); + unsigned char bytes[80]; + std::memset(bytes, 0xaa, sizeof(bytes)); + length = 64; + Require(sysctl_nid_postfix(mib, 2, bytes, &length, nullptr, 0) == 0 && length == 64); + bool changed = false; + for (std::size_t i = 0; i < 64; ++i) changed = changed || bytes[i] != 0xaa; + Require(changed && bytes[64] == 0xaa && bytes[79] == 0xaa); + unsigned char other[64] = {}; + length = sizeof(other); + Require(sysctl_nid_postfix(mib, 2, other, &length, nullptr, 0) == 0 && length == sizeof(other)); + Require(std::memcmp(bytes, other, sizeof(other)) != 0); + + unsigned char shortBuffer[64]; + std::memset(shortBuffer, 0xaa, sizeof(shortBuffer)); + length = 63; + Require(sysctl_nid_postfix(mib, 2, shortBuffer, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 12); + Require(length == 63 && shortBuffer[62] != 0xaa && shortBuffer[63] == 0xaa); + std::memset(shortBuffer, 0xaa, sizeof(shortBuffer)); + Require(sysctl_nid_postfix(mib, 2, shortBuffer, nullptr, nullptr, 0) == -1 && *__error_nid_postfix() == 12); + for (unsigned char byte : shortBuffer) Require(byte == 0xaa); + int value = 0; + length = sizeof(value); + bool threw = false; + try { + sysctl_nid_postfix(mib, 2, &value, &length, &value, sizeof(value)); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw); +} static void CheckNameToMib() { struct Expected { const char* name; @@ -480,6 +517,7 @@ static void CheckNameToMib() { {"hw.physmem", {6, 5}, 2}, {"hw.realmem", {6, 12}, 2}, {"kern.arandom", {1, 37}, 2}, + {"kern.rng_pseudo", {1, 50}, 2}, {"kern.proc.pathname", {1, 14, 12}, 3}, }; *__error_nid_postfix() = 13; @@ -523,6 +561,17 @@ static void CheckNameToMib() { Require(sysctlnametomib_nid_postfix("hw.ncpu", truncated, &size) == -1 && *__error_nid_postfix() == 12); Require(size == 0 && truncated[0] == 6); + int rngMib[3] = {0x5a5a5a5a, 0x5a5a5a5a, 0x5a5a5a5a}; + size = 3; + Require(sysctlnametomib_nid_postfix("kern.rng_pseudo", rngMib, &size) == 0 && size == 2); + Require(rngMib[0] == 1 && rngMib[1] == 50 && rngMib[2] == 0x5a5a5a5a); + size = 2; + Require(sysctl_nid_postfix(rngMib, static_cast<std::uint32_t>(size), nullptr, &length, nullptr, 0) == 0 && length == 64); + rngMib[0] = rngMib[1] = rngMib[2] = 0x5a5a5a5a; + size = 1; + Require(sysctlnametomib_nid_postfix("kern.rng_pseudo", rngMib, &size) == -1 && *__error_nid_postfix() == 12); + Require(size == 1 && rngMib[0] == 1 && rngMib[1] == 0x5a5a5a5a); + size = 4; Require(sysctlnametomib_nid_postfix("", mib, &size) == -1 && *__error_nid_postfix() == 2); Require(sysctlnametomib_nid_postfix("machdep.cacheline_size", mib, &size) == -1 && *__error_nid_postfix() == 2); @@ -580,6 +629,7 @@ int main() { CheckOvercommitSysctl(); CheckArandomSysctl(); CheckVmmapSysctl(); + CheckPseudoRandomMibSysctl(); CheckNameToMib(); *__error_nid_postfix() = 13; Require(sysconf_nid_postfix(1) == 262144); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 880a0832bf..86d838ef0d 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -16,7 +16,7 @@ - [physhm_open, physhm_unlink](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel, NIDs `n371J5cP+uo` and `AUqJNkobQ1c`) - declared as throwing stubs: the PS5 physical shared memory API is not documented, and [shadPS4's module generator](https://github.com/shadps4-emu/moduleGenerator/blob/main/libraries/kernel.cpp) also declares both with no arguments and returns `ORBIS_OK`, so the signature, the handle a success would return and the console's behaviour are unmeasured. The no-argument `int` signature follows shadPS4. Forever Launcher (PPSA99030) imports both and stops at load on `physhm_open`, and the relinker resolves no call site for either in that title, so no handle is fabricated and both throw - [getlogin_r](../../../core/libs/prx/libkernel/System/src/Login.cpp) (libkernel) returns the host effective account name: `GetUserNameW` as UTF-8 on Windows and `getpwuid_r(geteuid())` on POSIX. A valid account returns a name even without a login session; host `getlogin_r`, `USER` and `LOGNAME` are not consulted. The console's login name and behaviour without a session are unknown. Account lookup or encoding failures, a missing or empty account name, and POSIX account records exceeding the 16 KiB lookup buffer throw; there is no replacement name or retry. A short guest buffer returns `ERANGE` without modifying it, and a null output returns `EFAULT`. - [sceKernelGetSystemSwVersion](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports baseline system software version `0x01000000` ("01.000.000") for ProsperoStore (PPSA99000); the console's firmware version is not modeled. Measured on PS5 firmware 13.42: record values 0, 24, 39, 40, 41, 48 and 64 in the leading 64-bit field are accepted and preserved, the 28-byte string at offset 8 and packed version at 0x24 are filled, and bytes from 0x28 onward remain untouched. This API reports "13.590.001" / `0x13590001` in that environment, while the Prospero variant reports "13.420.000" / `0x13420000`; their values must not be assumed identical. A null pointer still returns `SCE_KERNEL_ERROR_EFAULT`, which was not measured; other firmware versions and record values remain unverified. -- [kern.rng_pseudo](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctlbyname` draws a new block of 64 bytes from the host's `std::random_device` for each read (no new value) that passes a buffer and a length, and sizes its answers as a payload process on a PS5 (firmware 12.70) sees it: a size query reports 64; a buffer of 64 bytes or more gets 64 bytes and a length of 64 (tried up to 65536); a smaller buffer gets as many bytes as its length, which is left as given, and a null length gets none, both with -1 and `ENOMEM`; a null buffer with a null length returns 0. `errno` is kept when the call succeeds. A write returns -1 with `EPERM`, which was not tried on the console. The only caller seen is mbedTLS's `mbedtls_hardware_poll` in the catalog's ProsperoLight boilerplate, which asks for 64-byte blocks and fails if fewer come back. The console's generator is unknown, and a title process was not measured. On the console `sysctlnametomib` gives MIB {1, 50} for the name, and `sysctl` with it answers reads of 0, 1, 63, 64, 65 and 128 bytes, a size query, a null length and a null buffer with a null length as the name does; here `sysctlnametomib` for the name and `sysctl` {1, 50} throw +- [kern.rng_pseudo](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctlbyname` draws a new block of 64 bytes from the host's `std::random_device` for each read (no new value) that passes a buffer and a length, and sizes its answers as a payload process on a PS5 (firmware 12.70) sees it: a size query reports 64; a buffer of 64 bytes or more gets 64 bytes and a length of 64 (tried up to 65536); a smaller buffer gets as many bytes as its length, which is left as given, and a null length gets none, both with -1 and `ENOMEM`; a null buffer with a null length returns 0. `errno` is kept when the call succeeds. A write returns -1 with `EPERM`, which was not tried on the console. The only caller seen is mbedTLS's `mbedtls_hardware_poll` in the catalog's ProsperoLight boilerplate, which asks for 64-byte blocks and fails if fewer come back. The console's generator is unknown, and a title process was not measured. On the console `sysctlnametomib` gives MIB {1, 50} for the name, and `sysctl` with it answers reads of 0, 1, 63, 64, 65 and 128 bytes, a size query, a null length and a null buffer with a null length as the name does; here `sysctlnametomib` maps the name to {1, 50} and `sysctl` {1, 50} answers as the name does; a write through the MIB was not measured and throws - [sysconf](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `_SC_CLK_TCK` returns 128, FreeBSD's `CLK_TCK` as its [`sysconf`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/sysconf.c#L100-L101) returns it ([`time.h`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/include/time.h#L48-L54)); the console's value is not checked, and no `times` reports ticks at that rate. CPython's `posix` module stops its import with "cannot read ticks_per_second" when the value is below 1, as the Python in PS5MC (PPSA99420) does. `_SC_ARG_MAX` returns 262144 (FreeBSD's `NCARGS`), `_SC_OPEN_MAX` returns the `RLIMIT_NOFILE` soft limit via `getrlimit` ([`sysconf.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/sysconf.c#L106-L115)), and `_SC_IOV_MAX` returns 1024 (FreeBSD's `UIO_MAXIOV`). Names other than `_SC_ARG_MAX`, `_SC_CLK_TCK`, `_SC_OPEN_MAX`, `_SC_PAGESIZE`, `_SC_IOV_MAX`, `_SC_NPROCESSORS_CONF`, `_SC_NPROCESSORS_ONLN` and `_SC_PHYS_PAGES` fail with `EINVAL` - [hw.realmem](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctl` {`CTL_HW`, 12} and `sysctlbyname` report the same 8-byte value as `hw.physmem` (the `_SC_PHYS_PAGES` pages of 16 KiB), read-only with `EPERM` for a write and `ENOMEM` for a short buffer, as FreeBSD's [`sysctl_hw_realmem`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_mib.c#L191-L204) does. FreeBSD reports there the memory the firmware describes, which is at least `hw.physmem`; the console's values are not known. Dolphin reads it as the machine's memory size in [`MemPhysical`](https://github.com/dolphin-emu/dolphin/blob/8d2ad1a662472955497e5bb79d7b9ec6999e1f0d/Source/Core/Common/MemoryUtil.cpp#L253-L273) (RetroArch PPSA99169's `dolphin_libretro` core) - [vm.overcommit](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctlbyname` reports the 4-byte value 0, the default FreeBSD gives its [`vm.overcommit`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/swap_pager.c#L158-L161) `int`, with `ENOMEM` for a short buffer; the console's value is not known and a write throws. mimalloc reads it in [`unix_detect_overcommit`](https://github.com/microsoft/mimalloc/blob/31d034d94cdb8e22f7d7ed55967f581a2d6e831d/src/prim/unix/prim.c#L129-L153) and takes 0 as no overcommit, as PS5MC (PPSA99420) does at start-up From 99e87ce36fa95723e726af65e29cfa73a46c883a Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Mon, 19 Oct 2026 15:50:47 +0000 Subject: [PATCH 266/269] test(libkernel): avoid flaky random byte assertion --- core/libs/tests/GuestSystemConfiguration.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/libs/tests/GuestSystemConfiguration.cpp b/core/libs/tests/GuestSystemConfiguration.cpp index bebbab8f7b..6627591d38 100644 --- a/core/libs/tests/GuestSystemConfiguration.cpp +++ b/core/libs/tests/GuestSystemConfiguration.cpp @@ -490,7 +490,9 @@ static void CheckPseudoRandomMibSysctl() { std::memset(shortBuffer, 0xaa, sizeof(shortBuffer)); length = 63; Require(sysctl_nid_postfix(mib, 2, shortBuffer, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 12); - Require(length == 63 && shortBuffer[62] != 0xaa && shortBuffer[63] == 0xaa); + bool shortChanged = false; + for (std::size_t i = 0; i < 63; ++i) shortChanged = shortChanged || shortBuffer[i] != 0xaa; + Require(length == 63 && shortChanged && shortBuffer[63] == 0xaa); std::memset(shortBuffer, 0xaa, sizeof(shortBuffer)); Require(sysctl_nid_postfix(mib, 2, shortBuffer, nullptr, nullptr, 0) == -1 && *__error_nid_postfix() == 12); for (unsigned char byte : shortBuffer) Require(byte == 0xaa); From e5de2a9b7d3808af7b38c0955f86ede5f77ada41 Mon Sep 17 00:00:00 2001 From: orezraey <eliatan1122@gmail.com> Date: Tue, 20 Oct 2026 07:39:27 -0300 Subject: [PATCH 267/269] test(tools): run the hw-oracle oracle.cmd fixture from non-ASCII paths Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- tools/hw-oracle/test_hw_oracle.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/hw-oracle/test_hw_oracle.py b/tools/hw-oracle/test_hw_oracle.py index 0f1238023c..58f66cf38c 100644 --- a/tools/hw-oracle/test_hw_oracle.py +++ b/tools/hw-oracle/test_hw_oracle.py @@ -402,13 +402,13 @@ class TargetOverrideTests(unittest.TestCase): encoding="utf-8") binary = root / ("oracle.cmd" if os.name == "nt" else "oracle") if os.name == "nt": - binary.write_text(f'@"{sys.executable}" "{runner}" %*\n', encoding="utf-8") + binary.write_text('@"%HW_ORACLE_TEST_PYTHON%" "%~dp0probe.py" %*\n', encoding="ascii") else: binary.write_text(f'#!/bin/sh\nexec {shlex.quote(sys.executable)} {shlex.quote(str(runner))} "$@"\n', encoding="utf-8") binary.chmod(0o755) with patch.object(hw_oracle, "oracle", return_value=binary), \ - patch.dict(os.environ, {"HW_ORACLE_TARGET": ""}): + patch.dict(os.environ, {"HW_ORACLE_TARGET": "", "HW_ORACLE_TEST_PYTHON": sys.executable}): os.environ["ROCR_VISIBLE_DEVICES"] = "0" self.assertEqual(hw_oracle.detected_target(binary, "1"), "gfx1100") for visible, expected in (("0", "gfx1030"), ("0", "gfx1030"), From 1529a5cf31ebb7b38045ded1eefce63b9c018684 Mon Sep 17 00:00:00 2001 From: orezraey <eliatan1122@gmail.com> Date: Tue, 20 Oct 2026 07:45:53 -0300 Subject: [PATCH 268/269] test(recompiler): read the cache fixture's build.ninja in the Ninja build-file encoding Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- tools/tests/test_shader_cache_runtime_sources.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tools/tests/test_shader_cache_runtime_sources.py b/tools/tests/test_shader_cache_runtime_sources.py index db2f418cfb..0c462bb7ea 100644 --- a/tools/tests/test_shader_cache_runtime_sources.py +++ b/tools/tests/test_shader_cache_runtime_sources.py @@ -7,6 +7,9 @@ import sys import tempfile import unittest +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import select_tests + SOURCE = Path(sys.argv.pop(1)) CMAKE = sys.argv.pop(1) @@ -74,7 +77,7 @@ file(WRITE "${{CMAKE_CURRENT_BINARY_DIR}}/runtime-sources.txt" "${{runtimeSource raise AssertionError('The generator fixture invoked a compiler: ' + (self.root / 'compiler-invoked').read_text(encoding='utf-8')) self.generated = self.build / 'shader_recompiler_probe_generated' self.listing = self.generated / 'sources.txt' - ninja = (self.build / 'build.ninja').read_text(encoding='utf-8') + ninja = (self.build / 'build.ninja').read_text(encoding=select_tests.Build(self.build).encoding) command = next(line for line in ninja.splitlines() if line.startswith(' COMMAND = ') and 'ShaderCacheVersion.cmake' in line) self.extra = re.search(r'"-DEXTRA=([^"]*)"', command).group(1) self.compiled = [] From cd2fd50f0c8a56c669230905780c652137f0758f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= <adriadam10@gmail.com> Date: Tue, 20 Oct 2026 14:02:05 +0200 Subject: [PATCH 269/269] fix(main): halve guest_raise_exception rounds to fit the 30 s budget on Windows #7652 made raises wait for host code to leave, adding ~3 s to a test that already took ~16 s on a fast machine. Main run 39414828108 timed out at 30 s on windows. Round counts halved; every scenario still runs. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --- core/libs/tests/GuestRaiseException.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/core/libs/tests/GuestRaiseException.cpp b/core/libs/tests/GuestRaiseException.cpp index 1f882c248d..fd74e36771 100644 --- a/core/libs/tests/GuestRaiseException.cpp +++ b/core/libs/tests/GuestRaiseException.cpp @@ -41,7 +41,7 @@ static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast<int>(0x80020016); static constexpr int SCE_KERNEL_ERROR_ESRCH = static_cast<int>(0x80020003); static constexpr int SCE_KERNEL_ERROR_ETIMEDOUT = static_cast<int>(0x8002003c); static constexpr int SIGUSR1 = 30; -static constexpr int Repeats = 100; +static constexpr int Repeats = 50; static constexpr std::size_t R12Offset = 0xa0; static constexpr std::size_t R13Offset = 0xa8; static constexpr std::size_t Xmm8Offset = 0x140 + 160 + 8 * 16; @@ -172,7 +172,7 @@ static void* APS5_VABI HostBlocked(void* arg) { return nullptr; } -static constexpr int LeavingRounds = 200; +static constexpr int LeavingRounds = 100; static std::atomic<int> leavingRound{0}; static void* APS5_VABI Leaving(void* arg) { @@ -198,7 +198,7 @@ static LONG CALLBACK ContinueRaised(EXCEPTION_POINTERS* info) { return EXCEPTION_CONTINUE_EXECUTION; } -static constexpr int SelfingRounds = 100; +static constexpr int SelfingRounds = 50; static std::atomic<Pthread> selfSink{nullptr}; static void* APS5_VABI Selfing(void* arg) { @@ -239,7 +239,7 @@ static void* APS5_VABI Looping(void* arg) { return nullptr; } -static constexpr int VectorRounds = 100; +static constexpr int VectorRounds = 50; static constexpr std::size_t VectorBytes = 16 * 32; alignas(32) static std::uint8_t vectorPattern[VectorBytes]; alignas(32) static std::uint8_t vectorResult[VectorBytes]; @@ -405,7 +405,7 @@ static void NestedWaitKeepsOuterWake() { } #ifdef _WIN32 -static constexpr int AddressRounds = 100; +static constexpr int AddressRounds = 50; static std::uint32_t addressWord = 0; static std::uint32_t otherAddressWord = 0; static std::atomic<bool> holdingEntered{false};