From 8c790274da25c0c1f9a1668555affd8b8774f8f4 Mon Sep 17 00:00:00 2001 From: Filip Michalski Date: Wed, 28 Oct 2026 08:20:12 +0100 Subject: [PATCH 01/49] perf(shader): simplify float max with a positive zero lhs --- .../tests/execution/F32MinMaxDenormalModes.cpp | 8 ++++++-- .../prx/libSceAgcDriver/tests/execution/F32MinMaxNan.cpp | 8 ++++++-- .../recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp | 4 ++++ 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/F32MinMaxDenormalModes.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/F32MinMaxDenormalModes.cpp index a9d0238d0b..c9d951d979 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/F32MinMaxDenormalModes.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/F32MinMaxDenormalModes.cpp @@ -24,9 +24,9 @@ constexpr std::uint32_t Instructions = 5; alignas(256) std::array Input{}; alignas(256) std::array Output{}; -alignas(256) constexpr std::array Code{ +alignas(256) constexpr std::array Code{ 0x34020084u, 0x34040086u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0x1e140b04u, 0x20160b04u, 0xd551000cu, - 0x041a0b04u, 0xd554000du, 0x041a0b04u, 0xd557000eu, 0x041a0b04u, 0xe0781000u, 0x80010a02u, 0xe0701010u, + 0x041a0b04u, 0xd554000du, 0x041a0b04u, 0xd557000eu, 0x041a0b04u, 0x201e0880u, 0xe0781000u, 0x80010a02u, 0xe0781010u, 0x80010e02u, 0xbf810000u, }; @@ -155,6 +155,10 @@ void Check(std::uint32_t denormals, bool ieee) { Hex(vector.operands[0]) + ", " + Hex(vector.operands[1]) + ", " + Hex(vector.operands[2]) + " is " + Hex(actual) + ", expected " + Hex(expected)); } + const auto value = vector.operands[0]; + const bool tiny = denormals != 1u && (value & 0x7f800000u) == 0u; + const auto zeroMax = value <= 0x7f800000u && !tiny ? value : 0u; + Require(Output[lane * Results + 5u] == zeroMax, "v_max_f32 v15, 0, v4 denormal result for " + Hex(value)); } } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/F32MinMaxNan.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/F32MinMaxNan.cpp index d427b01f05..29643a5996 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/F32MinMaxNan.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/F32MinMaxNan.cpp @@ -23,10 +23,10 @@ constexpr std::uint32_t Results = 16; alignas(256) std::array Input{}; alignas(256) std::array Output{}; -alignas(256) constexpr std::array Code{ +alignas(256) constexpr std::array Code{ 0x34020084, 0x34060086, 0xe0381000, 0x80000401, 0xbf8c3f70, 0x1e140b04, 0x20160b04, 0xd551000c, 0x041a0b04, 0xd554000d, 0x041a0b04, 0xd557000e, 0x041a0b04, 0xd50f000f, 0x02020b04, 0xd5100010, - 0x02020b04, 0xe0781000, 0x80010a03, 0xe0781010, 0x80010e03, 0xbf810000, + 0x02020b04, 0x20220880, 0xe0781000, 0x80010a03, 0xe0781010, 0x80010e03, 0xbf810000, }; constexpr std::uint32_t Rows[128][4] = { @@ -482,6 +482,10 @@ void Check() { for (std::uint32_t i = 0; i < 7; ++i) { Expect(Chunk * Threads + tid, out[i], (Ieee ? ExpectedIeee : Expected)[Chunk * Threads + tid][i], Names[i]); } + const auto magnitude = in[0] & 0x7fffffffu; + const bool signaling = magnitude > 0x7f800000u && (in[0] & 0x00400000u) == 0u; + const auto zeroMax = Ieee && signaling ? in[0] | 0x00400000u : in[0] <= 0x7f800000u ? in[0] : 0u; + Expect(Chunk * Threads + tid, out[7], zeroMax, "v_max_f32 v17, 0, v4"); } } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index 1f7cf2a2bd..665cecf9b7 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -1508,6 +1508,10 @@ std::uint32_t EmitClassMaskF32(SpirvEmitterState& state, std::uint32_t value, st } std::uint32_t EmitMinMaxF32Value(SpirvEmitterState& state, std::uint32_t lhs, std::uint32_t rhs, bool maxValue) { + if (maxValue && lhs == ConstantF32(state, 0u)) { + const auto positive = Binary(state, spv::OpFOrdGreaterThan, TypeBool(state), rhs, lhs); + return Select(state, TypeF32(state), positive, rhs, lhs); + } const auto lhsClass = EmitClassifyF32(state, lhs); const auto rhsClass = EmitClassifyF32(state, rhs); From c80f58dc8b3f016efa067e31db46a7c41e469de1 Mon Sep 17 00:00:00 2001 From: xdCloudy <52116030+xdCloudy@users.noreply.github.com> Date: Thu, 29 Oct 2026 06:13:55 +0000 Subject: [PATCH 02/49] fix(tools): fail closed on incomplete overlap checks --- tools/pr_overlap.py | 10 +++++ tools/tests/test_pr_overlap_fail_closed.py | 52 ++++++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 tools/tests/test_pr_overlap_fail_closed.py diff --git a/tools/pr_overlap.py b/tools/pr_overlap.py index 9ef1da8377..4ec9b7ed0c 100644 --- a/tools/pr_overlap.py +++ b/tools/pr_overlap.py @@ -503,6 +503,10 @@ def write_reports(out, rows, blob, main_exports=None): (out / f"{number}.md").write_text(report(found, blob, main_exports.get(number)), encoding="utf-8") +def unverified_overlaps(rows): + return {number: set(found["uncompared"]) for number, found in rows.items() if found.get("uncompared")} + + def main_export_findings(prs, current_exports): findings = {} for number, pr in prs.items(): @@ -563,6 +567,12 @@ def main(argv=None): raise SystemExit(describe_failure(error)) if args.branch: print(report(rows[0], blob, main_exports.get(0)) or "No overlap with open pull requests.", end="") + unresolved = unverified_overlaps(rows[0]) + if unresolved: + for number, files in sorted(unresolved.items()): + progress(f"cannot confirm overlap with PR {number}: {', '.join(sorted(files))}") + progress("overlap check failed closed; manually compare every reported PR branch before opening this one") + return 2 return 0 write_reports(args.out, rows, blob, main_exports) return 0 diff --git a/tools/tests/test_pr_overlap_fail_closed.py b/tools/tests/test_pr_overlap_fail_closed.py new file mode 100644 index 0000000000..c8e3fb69d8 --- /dev/null +++ b/tools/tests/test_pr_overlap_fail_closed.py @@ -0,0 +1,52 @@ +import sys +import unittest +from contextlib import ExitStack +from pathlib import Path +from unittest.mock import patch + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import pr_overlap + + +class FailClosedTests(unittest.TestCase): + def run_branch_check(self, found): + argv = ["pr_overlap.py", "--repo", "owner/project", "--branch", "topic"] + patches = ( + patch.object(sys, "argv", argv), + patch.object(pr_overlap, "open_prs", return_value={7: {"ref": "refs/pr/7"}}), + patch.object(pr_overlap, "fetch"), + patch.object(pr_overlap, "own", return_value={7: {"ref": "refs/pr/7"}}), + patch.object(pr_overlap, "git", return_value="a" * 40), + patch.object(pr_overlap, "scan_all"), + patch.object(pr_overlap, "base_exports", return_value={}), + patch.object(pr_overlap, "main_export_findings", return_value={}), + patch.object(pr_overlap, "overlaps", return_value=found), + patch.object(pr_overlap, "report", return_value="review result\n"), + patch.object(pr_overlap, "progress"), + patch("builtins.print"), + ) + with ExitStack() as stack: + for item in patches: + stack.enter_context(item) + return pr_overlap.main() + + def test_uncompared_shared_files_fail_the_local_branch_check(self): + result = self.run_branch_check({"added": set(), "uncompared": {"VectorAlgorithms.cpp"}}) + self.assertEqual(result, 2) + + def test_confirmed_comparisons_keep_the_local_branch_check_green(self): + result = self.run_branch_check({"added": set(), "conflicts": {}, "exports": set()}) + self.assertEqual(result, 0) + + def test_reports_every_uncompared_path(self): + self.assertEqual( + pr_overlap.unverified_overlaps({ + 7: {"uncompared": {"b.cpp", "a.cpp"}}, + 8: {"conflicts": {"c.cpp": [(1, 2)]}}, + }), + {7: {"a.cpp", "b.cpp"}}, + ) + + +if __name__ == "__main__": + unittest.main() From aed6a0be208725cfadc65d9f557003b7c7c4fc4f Mon Sep 17 00:00:00 2001 From: xdCloudy <52116030+xdCloudy@users.noreply.github.com> Date: Thu, 29 Oct 2026 06:22:01 +0000 Subject: [PATCH 03/49] test(tools): register fail-closed overlap test --- CMakeLists.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index c2e1a70c66..7aef3ae8eb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -310,6 +310,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_pages.py) add_test(NAME pr_overlap_export_cache COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_export_cache.py) + add_test(NAME pr_overlap_fail_closed COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_fail_closed.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} From 0f63ef9da0b2846817bf892df6f7bb28cb243dcc Mon Sep 17 00:00:00 2001 From: Rengundom <180709295+Rengundom@users.noreply.github.com> Date: Thu, 29 Oct 2026 05:41:02 -0500 Subject: [PATCH 04/49] refactor(shader): short-circuit terminal reachability checks --- core/shader/recompiler/ControlFlow/src/Structurizer.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/shader/recompiler/ControlFlow/src/Structurizer.cpp b/core/shader/recompiler/ControlFlow/src/Structurizer.cpp index 19da8b5235..419d0ab69a 100644 --- a/core/shader/recompiler/ControlFlow/src/Structurizer.cpp +++ b/core/shader/recompiler/ControlFlow/src/Structurizer.cpp @@ -482,6 +482,8 @@ bool canReachBefore(const ControlFlowGraph& graph, std::uint32_t start, std::uin } bool reachesWithinIteration(const ControlFlowGraph& graph, const NaturalLoop& loop, std::uint32_t start, std::uint32_t target) { + if (start == target) return true; + if (start == loop.continueBlock || start == loop.mergeBlock) return false; std::vector pending = {start}; std::vector visited(graph.blocks.size(), false); while (!pending.empty()) { From 42beeaa4686e3d5522f95e2ba602966b7af985b0 Mon Sep 17 00:00:00 2001 From: hugodearaujo Date: Thu, 29 Oct 2026 12:39:32 -0300 Subject: [PATCH 05/49] fix(libSceAudioOut): return from a blocking AudioOut2 push once the queue has room A blocking sceAudioOut2ContextPush waited only before queueing its grain, so the push that filled the queue returned at once. PRAGMATA's Wwise feeds its context from a thread that pushes and then wakes the renderer; the renderer sees a full queue, sleeps, and is woken again only by the game's next RenderAudio, so the mix advanced four grains per game frame. A blocking push now also waits after queueing until a grain finishes, bounded by the existing 200 ms limit. --- .../libSceAudioOut/src/AudioOut2Context.cpp | 34 ++++++++++++------- .../libSceAudioOut/tests/AudioOut2Timing.cpp | 28 +++++++++++++++ 2 files changed, 50 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp index 43ed70daf4..e839576383 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp @@ -278,6 +278,20 @@ static void TraceSummary(AudioOut2Context& context, Clock::time_point now) { context.summaryPeak = 0.0f; } +// Waits, with the lock released, until fewer grains than the queue depth wait to be played, or until +// FULL_WAIT_TIMEOUT. The caller holds the context lock; returns the time the wait ended. +static Clock::time_point WaitForRoom(AudioOut2Context& context, std::unique_lock& lock) { + auto now = Clock::now(); + const auto waitStart = now; + while (QueueLevel(context, now) >= context.queueDepth && now - waitStart <= FULL_WAIT_TIMEOUT) { + lock.unlock(); + PreciseSleepUs(std::chrono::microseconds(FULL_WAIT_STEP).count()); + lock.lock(); + now = Clock::now(); + } + return now; +} + extern "C" { int APS5_VABI sceAudioOut2ContextAdvance(AudioOut2ContextHandle ctx) try { @@ -368,19 +382,12 @@ int APS5_VABI sceAudioOut2ContextPush(AudioOut2ContextHandle ctx, uint32_t block std::unique_lock lock(context->lock); const QueueLevelPublisher publication{*context, lock}; auto now = Clock::now(); - const auto waitStart = now; - while (QueueLevel(*context, now) >= context->queueDepth) { - if (!blocking) { - context->fullRejects++; - if (context->fullRejects <= CALL_TRACE_FULL) AUDIOOUT2_TRACE("t=%.3f Push ctx %p non-blocking on a full queue (%u): rejected\n", AudioOut2TraceSeconds(), static_cast(context), context->queueDepth); - return SCE_AUDIO_OUT2_ERROR_QUEUE_FULL; - } - if (now - waitStart > FULL_WAIT_TIMEOUT) break; - lock.unlock(); - PreciseSleepUs(std::chrono::microseconds(FULL_WAIT_STEP).count()); - lock.lock(); - now = Clock::now(); + if (!blocking && QueueLevel(*context, now) >= context->queueDepth) { + context->fullRejects++; + if (context->fullRejects <= CALL_TRACE_FULL) AUDIOOUT2_TRACE("t=%.3f Push ctx %p non-blocking on a full queue (%u): rejected\n", AudioOut2TraceSeconds(), static_cast(context), context->queueDepth); + return SCE_AUDIO_OUT2_ERROR_QUEUE_FULL; } + if (blocking) now = WaitForRoom(*context, lock); if (context->advancedMode && context->advancedGrains.empty()) { if (!blocking) throw std::runtime_error("AudioOut2: a non-blocking push without a rendered grain is not implemented"); if (!context->grainReady.wait(lock, LibcShutdownToken_nid_postfix(), [&] { return !context->advancedGrains.empty(); })) throw ProcessShutdown{}; @@ -421,6 +428,9 @@ int APS5_VABI sceAudioOut2ContextPush(AudioOut2ContextHandle ctx, uint32_t block context->output.insert(context->output.end(), context->mix.begin(), context->mix.end()); } } + // A blocking push also returns only once the queue has room again, so a title that pushes from a + // feeder thread and wakes its renderer after each push (Wwise in PRAGMATA) is paced by playback. + if (blocking) now = WaitForRoom(*context, lock); context->pushes++; context->summaryPushes++; if (blocking) context->blockingPushes++; diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOut2Timing.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOut2Timing.cpp index d0b85c57cc..4d197d49db 100644 --- a/core/libs/prx/libSceAudioOut/tests/AudioOut2Timing.cpp +++ b/core/libs/prx/libSceAudioOut/tests/AudioOut2Timing.cpp @@ -201,6 +201,33 @@ void TestLevelWithoutADeviceUsesThePlaybackClock() { Require(level == 0 && available == queueDepth); } +// Without an SDL device (here the disk driver cannot open a directory) a blocking push returns only once +// the queue has room again, so a feeder thread that wakes its renderer after every push keeps playback +// fed. The push that fills the queue waits for the oldest grain's playback to end. +void TestBlockingPushWaitsForRoomWithoutADevice() { + SetEnvironment("SDL_DISKAUDIOFILE", std::filesystem::temp_directory_path().string()); + constexpr std::uint32_t paced = 1024; + AudioOut2ContextParam params{}; + Require(sceAudioOut2ContextResetParam(¶ms) == 0); + params.num_grains = paced; + params.queue_depth = queueDepth; + AudioOut2ContextHandle handle = 0; + Require(sceAudioOut2ContextCreate(¶ms, nullptr, 0, &handle) == 0); + Require(reinterpret_cast(handle)->device == 0); + const auto grainTime = std::chrono::duration(static_cast(paced) / frequency); + std::uint32_t level = 99; + std::uint32_t available = 99; + for (std::uint32_t round = 0; round < 4; round++) { + const auto start = std::chrono::steady_clock::now(); + Require(sceAudioOut2ContextPush(handle, 1) == 0); + const auto took = std::chrono::steady_clock::now() - start; + Require(sceAudioOut2ContextGetQueueLevel(handle, &level, &available) == 0); + Require(level < queueDepth && available > 0); + if (round + 1 >= queueDepth) Require(took >= grainTime / 2); + } + Require(sceAudioOut2ContextDestroy(handle) == 0); +} + void TestPrimingAfterRunningDry() { Session session("anyps5_audio_out2_priming.raw"); session.Fill(0.5f); @@ -359,6 +386,7 @@ int main() { TestDeviceLevelDoesNotWaitForTheContext(false); TestDeviceLevelDoesNotWaitForTheContext(true); TestLevelWithoutADeviceUsesThePlaybackClock(); + TestBlockingPushWaitsForRoomWithoutADevice(); TestPrimingAfterRunningDry(); TestLowLatencyCallsRejectBadArguments(); TestLowLatencyReadsAtNextPush(); From 7a75ea95b3fbbe72f4167ddc467bf4f685f01ed4 Mon Sep 17 00:00:00 2001 From: hugodearaujo Date: Thu, 29 Oct 2026 13:29:01 -0300 Subject: [PATCH 06/49] docs(libSceAudioOut): record the blocking push wait as inferred from PPSA02530 The wait is described in the debt file instead of in code comments. --- core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp | 4 ---- docs/dev/debt/libSceAudioOut.md | 1 + 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp index e839576383..2156ab1998 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp @@ -278,8 +278,6 @@ static void TraceSummary(AudioOut2Context& context, Clock::time_point now) { context.summaryPeak = 0.0f; } -// Waits, with the lock released, until fewer grains than the queue depth wait to be played, or until -// FULL_WAIT_TIMEOUT. The caller holds the context lock; returns the time the wait ended. static Clock::time_point WaitForRoom(AudioOut2Context& context, std::unique_lock& lock) { auto now = Clock::now(); const auto waitStart = now; @@ -428,8 +426,6 @@ int APS5_VABI sceAudioOut2ContextPush(AudioOut2ContextHandle ctx, uint32_t block context->output.insert(context->output.end(), context->mix.begin(), context->mix.end()); } } - // A blocking push also returns only once the queue has room again, so a title that pushes from a - // feeder thread and wakes its renderer after each push (Wwise in PRAGMATA) is paced by playback. if (blocking) now = WaitForRoom(*context, lock); context->pushes++; context->summaryPushes++; diff --git a/docs/dev/debt/libSceAudioOut.md b/docs/dev/debt/libSceAudioOut.md index 46aabde459..3369dc4ce2 100644 --- a/docs/dev/debt/libSceAudioOut.md +++ b/docs/dev/debt/libSceAudioOut.md @@ -130,6 +130,7 @@ ### Functional +- [sceAudioOut2ContextPush](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp) (libSceAudioOut) - a blocking Push waits for room before it queues its grain and again after, so it returns once fewer grains than the queue depth wait to be played (each wait ends after at most 200 ms). Inferred from PPSA02530, whose sound engine pushes one grain per call from a feeder thread and wakes its renderer after each push: while Push returned as soon as the grain was queued, the renderer found the queue full and slept until the next rendered frame. When the console's blocking Push returns is unmeasured. - [sceAudioOutMasteringInit, sceAudioOutMasteringTerm and sceAudioOutMasteringSetParam](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) - non-zero flags, a null pointer and a params id other than 1 throw; a repeated Init, a Term without Init and a SetParam or GetState before Init are unknown. There is no mastering stage, so Init and Term keep no state and SetParam reads only the id. The signatures and the default params id 1 follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioOut.cs#L312-L344), not a console measurement. SharpProspero documents flag bit 0 as `SwapEqCompressor`, and shadPS4's PS4 stub [rejects any non-zero Init flags](https://github.com/shadps4-emu/shadPS4/blob/8d50471b7dc141dd700b51276b8edd1f293bce26/src/core/libraries/audio/audioout.cpp#L956-L962) with 0x80260201 - [sceAudioOutMasteringGetState](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) - the states id 1 and the meter offsets 32, 128, 240, 272 and 304 in a 336-byte struct follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Audio/AudioOutMastering.cs#L97-L110), not a console measurement. With no mastering stage it zeroes the compressor input RMS and gain, and the limiter input, output and gain peaks, and leaves the other bytes, including the descriptors the AudioOut2 call writes, untouched; zero is not known to be what the console reports for an idle chain. A null pointer and a states id other than 1 throw; the console error codes are unknown - [sceAudioOut2EnableChat and sceAudioOut2DisableChat](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) - both PS5 exports take no arguments, return 0x80268006 before AudioOut2 initialization, and invoke the system chat service with selector 3. A negative service result maps to 0x80268000, otherwise they return zero. The host only records a local enabled flag, without a chat service; repeated-call and no-session behavior therefore still depends on the missing service. From 4ab6f1538e08386f7c0b7f52948352032d183853 Mon Sep 17 00:00:00 2001 From: Eduardo Ruiz Date: Thu, 29 Oct 2026 18:10:14 +0100 Subject: [PATCH 07/49] feat(libSceNpCppWebApi): bring up Matchmaking V1 offline --- .../libs/prx/libSceNpCppWebApi/CMakeLists.txt | 12 + core/libs/prx/libSceNpCppWebApi/Export.cpp | 513 ++++++++++++++++++ .../prx/libSceNpCppWebApi/include/Api.hpp | 93 ++++ .../tests/GuestNpCppWebApiMatchmaking.cpp | 190 +++++++ 4 files changed, 808 insertions(+) create mode 100644 core/libs/tests/GuestNpCppWebApiMatchmaking.cpp diff --git a/core/libs/prx/libSceNpCppWebApi/CMakeLists.txt b/core/libs/prx/libSceNpCppWebApi/CMakeLists.txt index d3ffa1ac9e..d14676369f 100644 --- a/core/libs/prx/libSceNpCppWebApi/CMakeLists.txt +++ b/core/libs/prx/libSceNpCppWebApi/CMakeLists.txt @@ -2,6 +2,18 @@ add_library(libSceNpCppWebApi SHARED EXCLUDE_FROM_ALL Export.cpp) target_include_directories(libSceNpCppWebApi PRIVATE ${LIBS_INCLUDE_DIR}) configure_windows_unwind(libSceNpCppWebApi) +if(BUILD_TESTING) + add_test_executable(guest_np_cpp_web_api_matchmaking_tests ${LIBS_INCLUDE_DIR}/tests/GuestNpCppWebApiMatchmaking.cpp) + target_include_directories(guest_np_cpp_web_api_matchmaking_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_np_cpp_web_api_matchmaking_tests PRIVATE libSceNpCppWebApi libc) + configure_windows_unwind(guest_np_cpp_web_api_matchmaking_tests) + add_test(NAME guest_np_cpp_web_api_matchmaking COMMAND guest_np_cpp_web_api_matchmaking_tests) + if(WIN32) + set_tests_properties(guest_np_cpp_web_api_matchmaking + PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() +endif() + if(MINGW) foreach(runtimeLinkFlag IN ITEMS -static-libgcc -static-libstdc++ -static) string(REPLACE "${runtimeLinkFlag}" "" CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}") diff --git a/core/libs/prx/libSceNpCppWebApi/Export.cpp b/core/libs/prx/libSceNpCppWebApi/Export.cpp index 2e1e6f0c25..ecb5a441a0 100644 --- a/core/libs/prx/libSceNpCppWebApi/Export.cpp +++ b/core/libs/prx/libSceNpCppWebApi/Export.cpp @@ -260,6 +260,519 @@ static int InitializeParameterWithBody(void* self, const std::uint64_t* context, return ParameterBodyMissing; } +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V113PlayerForRead19getPlayerAttributesEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V115LocationFactory6createEPNS1_6Common10LibContextEPNS5_12IntrusivePtrINS3_8LocationEEE(void* self) { + (void)self; + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V116AttributeFactory6createEPNS1_6Common10LibContextEPKcNS3_13AttributeTypeES9_PNS5_12IntrusivePtrINS3_9AttributeEEE(void* self) { + (void)self; + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V120GetOfferResponseBody10getPlayersEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V121GetTicketResponseBody10getPlayersEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V121GetTicketResponseBody19getTicketAttributesEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V121PlayerForTicketCreate10setNatTypeERKi(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V121PlayerForTicketCreate11setTeamNameEPKc(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V121PlayerForTicketCreate19setPlayerAttributesERKNS1_6Common6VectorINS5_12IntrusivePtrINS3_9AttributeEEEEE(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V123SubmitTicketRequestBody11setLocationERKNS1_6Common12IntrusivePtrINS3_8LocationEEE(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V123SubmitTicketRequestBody19setTicketAttributesERKNS1_6Common6VectorINS5_12IntrusivePtrINS3_9AttributeEEEEE(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V128PlayerForTicketCreateFactory6createEPNS1_6Common10LibContextERKmNS3_8PlatformEPNS5_12IntrusivePtrINS3_21PlayerForTicketCreateEEE(void* self) { + (void)self; + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V130SubmitTicketRequestBodyFactory6createEPNS1_6Common10LibContextEPKcRKNS5_6VectorINS5_12IntrusivePtrINS3_21PlayerForTicketCreateEEEEEPNSB_INS3_23SubmitTicketRequestBodyEEE(void* self) { + (void)self; + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api12cancelTicketEiRKNS4_23ParameterToCancelTicketERNS1_6Common11TransactionINS8_15DefaultResponseENS8_12IntrusivePtrINS8_18ResponseHeaderBaseEEEEE(void* self) { + (void)self; + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api12submitTicketEiRKNS4_23ParameterToSubmitTicketERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_24SubmitTicketResponseBodyEEENSA_INS8_18ResponseHeaderBaseEEEEE(void* self) { + (void)self; + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api19ParameterToGetOffer10initializeEPNS1_6Common10LibContextEPKc(void* self, const std::uint64_t* context) { + return InitializeParameter(self, context, true); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api19ParameterToGetOffer9terminateEv(void* self) { + return TerminateParameter(self); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api19ParameterToGetOfferC1Ev(void* self) { + ConstructParameter(self); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api19ParameterToGetOfferD1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api20ParameterToGetTicket10initializeEPNS1_6Common10LibContextEPKc(void* self, const std::uint64_t* context) { + return InitializeParameter(self, context, true); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api20ParameterToGetTicket7setviewEPKc(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api20ParameterToGetTicket9terminateEv(void* self) { + return TerminateParameter(self); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api20ParameterToGetTicketC1Ev(void* self) { + ConstructParameter(self); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api20ParameterToGetTicketD1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api23ParameterToCancelTicket10initializeEPNS1_6Common10LibContextEPKc(void* self, const std::uint64_t* context) { + return InitializeParameter(self, context, true); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api23ParameterToCancelTicket9terminateEv(void* self) { + return TerminateParameter(self); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api23ParameterToCancelTicketC1Ev(void* self) { + ConstructParameter(self); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api23ParameterToCancelTicketD1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api23ParameterToSubmitTicket10initializeEPNS1_6Common10LibContextENS6_12IntrusivePtrINS3_23SubmitTicketRequestBodyEEE(void* self, const std::uint64_t* context) { + return InitializeParameter(self, context, true); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api23ParameterToSubmitTicket9terminateEv(void* self) { + return TerminateParameter(self); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api23ParameterToSubmitTicketC1Ev(void* self) { + ConstructParameter(self); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api23ParameterToSubmitTicketD1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api8getOfferEiRKNS4_19ParameterToGetOfferERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_20GetOfferResponseBodyEEENSA_INS8_18ResponseHeaderBaseEEEEE(void* self) { + (void)self; + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api9getTicketEiRKNS4_20ParameterToGetTicketERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_21GetTicketResponseBodyEEENSA_INS8_18ResponseHeaderBaseEEEEE(void* self) { + (void)self; + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V18Location16setGameSessionIdEPKc(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V113PlayerForRead11getOnlineIdEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V113PlayerForRead11getPlatformEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V113PlayerForRead11getTeamNameEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V113PlayerForRead12getAccountIdEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V113PlayerForRead13onlineIdIsSetEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V113PlayerForRead13platformIsSetEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V113PlayerForRead13teamNameIsSetEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V113PlayerForRead14accountIdIsSetEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V113PlayerForRead21playerAttributesIsSetEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V118PlayerForOfferRead11getOnlineIdEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V118PlayerForOfferRead11getPlatformEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V118PlayerForOfferRead11getTeamNameEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V118PlayerForOfferRead11getTicketIdEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V118PlayerForOfferRead12getAccountIdEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V118PlayerForOfferRead13onlineIdIsSetEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V118PlayerForOfferRead13platformIsSetEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V118PlayerForOfferRead13teamNameIsSetEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V118PlayerForOfferRead13ticketIdIsSetEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V118PlayerForOfferRead14accountIdIsSetEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V120GetOfferResponseBody10getOfferIdEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V120GetOfferResponseBody11getLocationEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V120GetOfferResponseBody11statusIsSetEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V120GetOfferResponseBody12offerIdIsSetEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V120GetOfferResponseBody12playersIsSetEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V120GetOfferResponseBody13locationIsSetEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V120GetOfferResponseBody14getRulesetNameEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V120GetOfferResponseBody16rulesetNameIsSetEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V120GetOfferResponseBody18getCreatedDateTimeEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V120GetOfferResponseBody18getUpdatedDateTimeEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V120GetOfferResponseBody20createdDateTimeIsSetEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V120GetOfferResponseBody20updatedDateTimeIsSetEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V120GetOfferResponseBody9getStatusEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V121GetTicketResponseBody11getTicketIdEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V121GetTicketResponseBody11statusIsSetEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V121GetTicketResponseBody12getSubmitterEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V121GetTicketResponseBody12playersIsSetEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V121GetTicketResponseBody13ticketIdIsSetEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V121GetTicketResponseBody14getRulesetNameEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V121GetTicketResponseBody14submitterIsSetEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V121GetTicketResponseBody16rulesetNameIsSetEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V121GetTicketResponseBody18getCreatedDateTimeEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V121GetTicketResponseBody18getUpdatedDateTimeEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V121GetTicketResponseBody20createdDateTimeIsSetEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V121GetTicketResponseBody20updatedDateTimeIsSetEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V121GetTicketResponseBody21ticketAttributesIsSetEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V121GetTicketResponseBody9getStatusEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V124SubmitTicketResponseBody11getTicketIdEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V124SubmitTicketResponseBody13ticketIdIsSetEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V18Location16getGameSessionIdEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V18Location18gameSessionIdIsSetEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V19Attribute7getNameEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V19Attribute7getTypeEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V19Attribute8getValueEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V19Submitter11getPlatformEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V19Submitter12getAccountIdEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V19Submitter13platformIsSetEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V19Submitter14accountIdIsSetEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi23ParameterToDownloadData10initializeEPNS1_6Common10LibContextEPKci(void* self, const std::uint64_t* context, const char* text, int value) { (void)value; return InitializeParameter(self, context, text != nullptr); diff --git a/core/libs/prx/libSceNpCppWebApi/include/Api.hpp b/core/libs/prx/libSceNpCppWebApi/include/Api.hpp index 5554b1cb0a..dd05a46456 100644 --- a/core/libs/prx/libSceNpCppWebApi/include/Api.hpp +++ b/core/libs/prx/libSceNpCppWebApi/include/Api.hpp @@ -18,6 +18,99 @@ struct IntrusivePtr { extern "C" { int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V111UserFactory6createEPNS1_6Common10LibContextEPNS5_12IntrusivePtrINS3_4UserEEE(); + +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V113PlayerForRead19getPlayerAttributesEv(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V115LocationFactory6createEPNS1_6Common10LibContextEPNS5_12IntrusivePtrINS3_8LocationEEE(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V116AttributeFactory6createEPNS1_6Common10LibContextEPKcNS3_13AttributeTypeES9_PNS5_12IntrusivePtrINS3_9AttributeEEE(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V120GetOfferResponseBody10getPlayersEv(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V121GetTicketResponseBody10getPlayersEv(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V121GetTicketResponseBody19getTicketAttributesEv(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V121PlayerForTicketCreate10setNatTypeERKi(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V121PlayerForTicketCreate11setTeamNameEPKc(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V121PlayerForTicketCreate19setPlayerAttributesERKNS1_6Common6VectorINS5_12IntrusivePtrINS3_9AttributeEEEEE(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V123SubmitTicketRequestBody11setLocationERKNS1_6Common12IntrusivePtrINS3_8LocationEEE(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V123SubmitTicketRequestBody19setTicketAttributesERKNS1_6Common6VectorINS5_12IntrusivePtrINS3_9AttributeEEEEE(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V128PlayerForTicketCreateFactory6createEPNS1_6Common10LibContextERKmNS3_8PlatformEPNS5_12IntrusivePtrINS3_21PlayerForTicketCreateEEE(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V130SubmitTicketRequestBodyFactory6createEPNS1_6Common10LibContextEPKcRKNS5_6VectorINS5_12IntrusivePtrINS3_21PlayerForTicketCreateEEEEEPNSB_INS3_23SubmitTicketRequestBodyEEE(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api12cancelTicketEiRKNS4_23ParameterToCancelTicketERNS1_6Common11TransactionINS8_15DefaultResponseENS8_12IntrusivePtrINS8_18ResponseHeaderBaseEEEEE(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api12submitTicketEiRKNS4_23ParameterToSubmitTicketERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_24SubmitTicketResponseBodyEEENSA_INS8_18ResponseHeaderBaseEEEEE(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api19ParameterToGetOffer10initializeEPNS1_6Common10LibContextEPKc(void* self, const std::uint64_t* context); +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api19ParameterToGetOffer9terminateEv(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api19ParameterToGetOfferC1Ev(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api19ParameterToGetOfferD1Ev(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api20ParameterToGetTicket10initializeEPNS1_6Common10LibContextEPKc(void* self, const std::uint64_t* context); +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api20ParameterToGetTicket7setviewEPKc(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api20ParameterToGetTicket9terminateEv(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api20ParameterToGetTicketC1Ev(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api20ParameterToGetTicketD1Ev(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api23ParameterToCancelTicket10initializeEPNS1_6Common10LibContextEPKc(void* self, const std::uint64_t* context); +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api23ParameterToCancelTicket9terminateEv(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api23ParameterToCancelTicketC1Ev(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api23ParameterToCancelTicketD1Ev(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api23ParameterToSubmitTicket10initializeEPNS1_6Common10LibContextENS6_12IntrusivePtrINS3_23SubmitTicketRequestBodyEEE(void* self, const std::uint64_t* context); +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api23ParameterToSubmitTicket9terminateEv(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api23ParameterToSubmitTicketC1Ev(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api23ParameterToSubmitTicketD1Ev(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api8getOfferEiRKNS4_19ParameterToGetOfferERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_20GetOfferResponseBodyEEENSA_INS8_18ResponseHeaderBaseEEEEE(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api9getTicketEiRKNS4_20ParameterToGetTicketERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_21GetTicketResponseBodyEEENSA_INS8_18ResponseHeaderBaseEEEEE(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V18Location16setGameSessionIdEPKc(void* self); +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V113PlayerForRead11getOnlineIdEv(void* self); +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V113PlayerForRead11getPlatformEv(void* self); +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V113PlayerForRead11getTeamNameEv(void* self); +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V113PlayerForRead12getAccountIdEv(void* self); +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V113PlayerForRead13onlineIdIsSetEv(void* self); +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V113PlayerForRead13platformIsSetEv(void* self); +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V113PlayerForRead13teamNameIsSetEv(void* self); +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V113PlayerForRead14accountIdIsSetEv(void* self); +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V113PlayerForRead21playerAttributesIsSetEv(void* self); +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V118PlayerForOfferRead11getOnlineIdEv(void* self); +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V118PlayerForOfferRead11getPlatformEv(void* self); +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V118PlayerForOfferRead11getTeamNameEv(void* self); +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V118PlayerForOfferRead11getTicketIdEv(void* self); +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V118PlayerForOfferRead12getAccountIdEv(void* self); +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V118PlayerForOfferRead13onlineIdIsSetEv(void* self); +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V118PlayerForOfferRead13platformIsSetEv(void* self); +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V118PlayerForOfferRead13teamNameIsSetEv(void* self); +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V118PlayerForOfferRead13ticketIdIsSetEv(void* self); +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V118PlayerForOfferRead14accountIdIsSetEv(void* self); +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V120GetOfferResponseBody10getOfferIdEv(void* self); +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V120GetOfferResponseBody11getLocationEv(void* self); +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V120GetOfferResponseBody11statusIsSetEv(void* self); +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V120GetOfferResponseBody12offerIdIsSetEv(void* self); +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V120GetOfferResponseBody12playersIsSetEv(void* self); +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V120GetOfferResponseBody13locationIsSetEv(void* self); +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V120GetOfferResponseBody14getRulesetNameEv(void* self); +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V120GetOfferResponseBody16rulesetNameIsSetEv(void* self); +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V120GetOfferResponseBody18getCreatedDateTimeEv(void* self); +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V120GetOfferResponseBody18getUpdatedDateTimeEv(void* self); +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V120GetOfferResponseBody20createdDateTimeIsSetEv(void* self); +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V120GetOfferResponseBody20updatedDateTimeIsSetEv(void* self); +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V120GetOfferResponseBody9getStatusEv(void* self); +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V121GetTicketResponseBody11getTicketIdEv(void* self); +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V121GetTicketResponseBody11statusIsSetEv(void* self); +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V121GetTicketResponseBody12getSubmitterEv(void* self); +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V121GetTicketResponseBody12playersIsSetEv(void* self); +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V121GetTicketResponseBody13ticketIdIsSetEv(void* self); +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V121GetTicketResponseBody14getRulesetNameEv(void* self); +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V121GetTicketResponseBody14submitterIsSetEv(void* self); +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V121GetTicketResponseBody16rulesetNameIsSetEv(void* self); +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V121GetTicketResponseBody18getCreatedDateTimeEv(void* self); +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V121GetTicketResponseBody18getUpdatedDateTimeEv(void* self); +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V121GetTicketResponseBody20createdDateTimeIsSetEv(void* self); +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V121GetTicketResponseBody20updatedDateTimeIsSetEv(void* self); +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V121GetTicketResponseBody21ticketAttributesIsSetEv(void* self); +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V121GetTicketResponseBody9getStatusEv(void* self); +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V124SubmitTicketResponseBody11getTicketIdEv(void* self); +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V124SubmitTicketResponseBody13ticketIdIsSetEv(void* self); +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V18Location16getGameSessionIdEv(void* self); +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V18Location18gameSessionIdIsSetEv(void* self); +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V19Attribute7getNameEv(void* self); +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V19Attribute7getTypeEv(void* self); +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V19Attribute8getValueEv(void* self); +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V19Submitter11getPlatformEv(void* self); +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V19Submitter12getAccountIdEv(void* self); +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V19Submitter13platformIsSetEv(void* self); +int APS5_VABI _ZNK3sce2Np9CppWebApi11Matchmaking2V19Submitter14accountIdIsSetEv(void* self); int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V121GetRankingRequestBody18setStartSerialRankERKi(); int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V121GetRankingRequestBody8setGroupERKNS3_5GroupE(); int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V121GetRankingRequestBody8setLimitERKi(); diff --git a/core/libs/tests/GuestNpCppWebApiMatchmaking.cpp b/core/libs/tests/GuestNpCppWebApiMatchmaking.cpp new file mode 100644 index 0000000000..71eecbcaa7 --- /dev/null +++ b/core/libs/tests/GuestNpCppWebApiMatchmaking.cpp @@ -0,0 +1,190 @@ +#include "prx/libSceNpCppWebApi/include/Api.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void Require(bool value, std::source_location at = std::source_location::current()) { + if (!value) { + std::fprintf(stderr, "requirement failed at %s:%u\n", at.file_name(), at.line()); + std::abort(); + } +} + +constexpr int kNotSignedIn = static_cast(0x80552907); +constexpr int kAlreadyInitialized = static_cast(0x8055350f); +constexpr int kNotInitialized = static_cast(0x80553510); +constexpr std::size_t kParameterSize = 0x10; +constexpr std::size_t kGuardSize = 0x10; +constexpr unsigned char kFill = 0xA5; + +struct Storage { + alignas(8) unsigned char bytes[kParameterSize + kGuardSize]; +}; + +bool GuardIntact(const Storage& storage) { + for (std::size_t index = kParameterSize; index < sizeof(storage.bytes); ++index) { + if (storage.bytes[index] != kFill) return false; + } + return true; +} + +using SimpleFn = int (APS5_VABI*)(void*); +using InitFn = int (APS5_VABI*)(void*, const std::uint64_t*); + +const SimpleFn kSignedOut[] = { + _ZN3sce2Np9CppWebApi11Matchmaking2V115LocationFactory6createEPNS1_6Common10LibContextEPNS5_12IntrusivePtrINS3_8LocationEEE, + _ZN3sce2Np9CppWebApi11Matchmaking2V116AttributeFactory6createEPNS1_6Common10LibContextEPKcNS3_13AttributeTypeES9_PNS5_12IntrusivePtrINS3_9AttributeEEE, + _ZN3sce2Np9CppWebApi11Matchmaking2V128PlayerForTicketCreateFactory6createEPNS1_6Common10LibContextERKmNS3_8PlatformEPNS5_12IntrusivePtrINS3_21PlayerForTicketCreateEEE, + _ZN3sce2Np9CppWebApi11Matchmaking2V130SubmitTicketRequestBodyFactory6createEPNS1_6Common10LibContextEPKcRKNS5_6VectorINS5_12IntrusivePtrINS3_21PlayerForTicketCreateEEEEEPNSB_INS3_23SubmitTicketRequestBodyEEE, + _ZN3sce2Np9CppWebApi11Matchmaking2V13Api12cancelTicketEiRKNS4_23ParameterToCancelTicketERNS1_6Common11TransactionINS8_15DefaultResponseENS8_12IntrusivePtrINS8_18ResponseHeaderBaseEEEEE, + _ZN3sce2Np9CppWebApi11Matchmaking2V13Api12submitTicketEiRKNS4_23ParameterToSubmitTicketERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_24SubmitTicketResponseBodyEEENSA_INS8_18ResponseHeaderBaseEEEEE, + _ZN3sce2Np9CppWebApi11Matchmaking2V13Api8getOfferEiRKNS4_19ParameterToGetOfferERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_20GetOfferResponseBodyEEENSA_INS8_18ResponseHeaderBaseEEEEE, + _ZN3sce2Np9CppWebApi11Matchmaking2V13Api9getTicketEiRKNS4_20ParameterToGetTicketERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_21GetTicketResponseBodyEEENSA_INS8_18ResponseHeaderBaseEEEEE, +}; +const SimpleFn kAccessors[] = { + _ZN3sce2Np9CppWebApi11Matchmaking2V113PlayerForRead19getPlayerAttributesEv, + _ZN3sce2Np9CppWebApi11Matchmaking2V120GetOfferResponseBody10getPlayersEv, + _ZN3sce2Np9CppWebApi11Matchmaking2V121GetTicketResponseBody10getPlayersEv, + _ZN3sce2Np9CppWebApi11Matchmaking2V121GetTicketResponseBody19getTicketAttributesEv, + _ZNK3sce2Np9CppWebApi11Matchmaking2V113PlayerForRead11getOnlineIdEv, + _ZNK3sce2Np9CppWebApi11Matchmaking2V113PlayerForRead11getPlatformEv, + _ZNK3sce2Np9CppWebApi11Matchmaking2V113PlayerForRead11getTeamNameEv, + _ZNK3sce2Np9CppWebApi11Matchmaking2V113PlayerForRead12getAccountIdEv, + _ZNK3sce2Np9CppWebApi11Matchmaking2V113PlayerForRead13onlineIdIsSetEv, + _ZNK3sce2Np9CppWebApi11Matchmaking2V113PlayerForRead13platformIsSetEv, + _ZNK3sce2Np9CppWebApi11Matchmaking2V113PlayerForRead13teamNameIsSetEv, + _ZNK3sce2Np9CppWebApi11Matchmaking2V113PlayerForRead14accountIdIsSetEv, + _ZNK3sce2Np9CppWebApi11Matchmaking2V113PlayerForRead21playerAttributesIsSetEv, + _ZNK3sce2Np9CppWebApi11Matchmaking2V118PlayerForOfferRead11getOnlineIdEv, + _ZNK3sce2Np9CppWebApi11Matchmaking2V118PlayerForOfferRead11getPlatformEv, + _ZNK3sce2Np9CppWebApi11Matchmaking2V118PlayerForOfferRead11getTeamNameEv, + _ZNK3sce2Np9CppWebApi11Matchmaking2V118PlayerForOfferRead11getTicketIdEv, + _ZNK3sce2Np9CppWebApi11Matchmaking2V118PlayerForOfferRead12getAccountIdEv, + _ZNK3sce2Np9CppWebApi11Matchmaking2V118PlayerForOfferRead13onlineIdIsSetEv, + _ZNK3sce2Np9CppWebApi11Matchmaking2V118PlayerForOfferRead13platformIsSetEv, + _ZNK3sce2Np9CppWebApi11Matchmaking2V118PlayerForOfferRead13teamNameIsSetEv, + _ZNK3sce2Np9CppWebApi11Matchmaking2V118PlayerForOfferRead13ticketIdIsSetEv, + _ZNK3sce2Np9CppWebApi11Matchmaking2V118PlayerForOfferRead14accountIdIsSetEv, + _ZNK3sce2Np9CppWebApi11Matchmaking2V120GetOfferResponseBody10getOfferIdEv, + _ZNK3sce2Np9CppWebApi11Matchmaking2V120GetOfferResponseBody11getLocationEv, + _ZNK3sce2Np9CppWebApi11Matchmaking2V120GetOfferResponseBody11statusIsSetEv, + _ZNK3sce2Np9CppWebApi11Matchmaking2V120GetOfferResponseBody12offerIdIsSetEv, + _ZNK3sce2Np9CppWebApi11Matchmaking2V120GetOfferResponseBody12playersIsSetEv, + _ZNK3sce2Np9CppWebApi11Matchmaking2V120GetOfferResponseBody13locationIsSetEv, + _ZNK3sce2Np9CppWebApi11Matchmaking2V120GetOfferResponseBody14getRulesetNameEv, + _ZNK3sce2Np9CppWebApi11Matchmaking2V120GetOfferResponseBody16rulesetNameIsSetEv, + _ZNK3sce2Np9CppWebApi11Matchmaking2V120GetOfferResponseBody18getCreatedDateTimeEv, + _ZNK3sce2Np9CppWebApi11Matchmaking2V120GetOfferResponseBody18getUpdatedDateTimeEv, + _ZNK3sce2Np9CppWebApi11Matchmaking2V120GetOfferResponseBody20createdDateTimeIsSetEv, + _ZNK3sce2Np9CppWebApi11Matchmaking2V120GetOfferResponseBody20updatedDateTimeIsSetEv, + _ZNK3sce2Np9CppWebApi11Matchmaking2V120GetOfferResponseBody9getStatusEv, + _ZNK3sce2Np9CppWebApi11Matchmaking2V121GetTicketResponseBody11getTicketIdEv, + _ZNK3sce2Np9CppWebApi11Matchmaking2V121GetTicketResponseBody11statusIsSetEv, + _ZNK3sce2Np9CppWebApi11Matchmaking2V121GetTicketResponseBody12getSubmitterEv, + _ZNK3sce2Np9CppWebApi11Matchmaking2V121GetTicketResponseBody12playersIsSetEv, + _ZNK3sce2Np9CppWebApi11Matchmaking2V121GetTicketResponseBody13ticketIdIsSetEv, + _ZNK3sce2Np9CppWebApi11Matchmaking2V121GetTicketResponseBody14getRulesetNameEv, + _ZNK3sce2Np9CppWebApi11Matchmaking2V121GetTicketResponseBody14submitterIsSetEv, + _ZNK3sce2Np9CppWebApi11Matchmaking2V121GetTicketResponseBody16rulesetNameIsSetEv, + _ZNK3sce2Np9CppWebApi11Matchmaking2V121GetTicketResponseBody18getCreatedDateTimeEv, + _ZNK3sce2Np9CppWebApi11Matchmaking2V121GetTicketResponseBody18getUpdatedDateTimeEv, + _ZNK3sce2Np9CppWebApi11Matchmaking2V121GetTicketResponseBody20createdDateTimeIsSetEv, + _ZNK3sce2Np9CppWebApi11Matchmaking2V121GetTicketResponseBody20updatedDateTimeIsSetEv, + _ZNK3sce2Np9CppWebApi11Matchmaking2V121GetTicketResponseBody21ticketAttributesIsSetEv, + _ZNK3sce2Np9CppWebApi11Matchmaking2V121GetTicketResponseBody9getStatusEv, + _ZNK3sce2Np9CppWebApi11Matchmaking2V124SubmitTicketResponseBody11getTicketIdEv, + _ZNK3sce2Np9CppWebApi11Matchmaking2V124SubmitTicketResponseBody13ticketIdIsSetEv, + _ZNK3sce2Np9CppWebApi11Matchmaking2V18Location16getGameSessionIdEv, + _ZNK3sce2Np9CppWebApi11Matchmaking2V18Location18gameSessionIdIsSetEv, + _ZNK3sce2Np9CppWebApi11Matchmaking2V19Attribute7getNameEv, + _ZNK3sce2Np9CppWebApi11Matchmaking2V19Attribute7getTypeEv, + _ZNK3sce2Np9CppWebApi11Matchmaking2V19Attribute8getValueEv, + _ZNK3sce2Np9CppWebApi11Matchmaking2V19Submitter11getPlatformEv, + _ZNK3sce2Np9CppWebApi11Matchmaking2V19Submitter12getAccountIdEv, + _ZNK3sce2Np9CppWebApi11Matchmaking2V19Submitter13platformIsSetEv, + _ZNK3sce2Np9CppWebApi11Matchmaking2V19Submitter14accountIdIsSetEv, +}; +const SimpleFn kSetters[] = { + _ZN3sce2Np9CppWebApi11Matchmaking2V121PlayerForTicketCreate10setNatTypeERKi, + _ZN3sce2Np9CppWebApi11Matchmaking2V121PlayerForTicketCreate11setTeamNameEPKc, + _ZN3sce2Np9CppWebApi11Matchmaking2V121PlayerForTicketCreate19setPlayerAttributesERKNS1_6Common6VectorINS5_12IntrusivePtrINS3_9AttributeEEEEE, + _ZN3sce2Np9CppWebApi11Matchmaking2V123SubmitTicketRequestBody11setLocationERKNS1_6Common12IntrusivePtrINS3_8LocationEEE, + _ZN3sce2Np9CppWebApi11Matchmaking2V123SubmitTicketRequestBody19setTicketAttributesERKNS1_6Common6VectorINS5_12IntrusivePtrINS3_9AttributeEEEEE, + _ZN3sce2Np9CppWebApi11Matchmaking2V18Location16setGameSessionIdEPKc, +}; +const SimpleFn kConstructors[] = { + _ZN3sce2Np9CppWebApi11Matchmaking2V13Api19ParameterToGetOfferC1Ev, + _ZN3sce2Np9CppWebApi11Matchmaking2V13Api20ParameterToGetTicketC1Ev, + _ZN3sce2Np9CppWebApi11Matchmaking2V13Api23ParameterToCancelTicketC1Ev, + _ZN3sce2Np9CppWebApi11Matchmaking2V13Api23ParameterToSubmitTicketC1Ev, +}; +const SimpleFn kDestructors[] = { + _ZN3sce2Np9CppWebApi11Matchmaking2V13Api19ParameterToGetOfferD1Ev, + _ZN3sce2Np9CppWebApi11Matchmaking2V13Api20ParameterToGetTicketD1Ev, + _ZN3sce2Np9CppWebApi11Matchmaking2V13Api23ParameterToCancelTicketD1Ev, + _ZN3sce2Np9CppWebApi11Matchmaking2V13Api23ParameterToSubmitTicketD1Ev, +}; +const SimpleFn kTerminators[] = { + _ZN3sce2Np9CppWebApi11Matchmaking2V13Api19ParameterToGetOffer9terminateEv, + _ZN3sce2Np9CppWebApi11Matchmaking2V13Api20ParameterToGetTicket9terminateEv, + _ZN3sce2Np9CppWebApi11Matchmaking2V13Api23ParameterToCancelTicket9terminateEv, + _ZN3sce2Np9CppWebApi11Matchmaking2V13Api23ParameterToSubmitTicket9terminateEv, +}; +const InitFn kInitializers[] = { + _ZN3sce2Np9CppWebApi11Matchmaking2V13Api19ParameterToGetOffer10initializeEPNS1_6Common10LibContextEPKc, + _ZN3sce2Np9CppWebApi11Matchmaking2V13Api20ParameterToGetTicket10initializeEPNS1_6Common10LibContextEPKc, + _ZN3sce2Np9CppWebApi11Matchmaking2V13Api23ParameterToCancelTicket10initializeEPNS1_6Common10LibContextEPKc, + _ZN3sce2Np9CppWebApi11Matchmaking2V13Api23ParameterToSubmitTicket10initializeEPNS1_6Common10LibContextENS6_12IntrusivePtrINS3_23SubmitTicketRequestBodyEEE, +}; + +template +void RequireThrows(TCall call) { + bool threw = false; + try { call(); } catch (const std::runtime_error&) { threw = true; } + Require(threw); +} + +} + +int main() { + for (const auto fn : kSignedOut) Require(fn(nullptr) == kNotSignedIn); + for (const auto fn : kAccessors) RequireThrows([&] { (void)fn(nullptr); }); + for (const auto fn : kSetters) Require(fn(nullptr) == 0); + + for (const auto fn : kConstructors) { + Storage storage; + std::memset(storage.bytes, kFill, sizeof(storage.bytes)); + Require(fn(storage.bytes) == 0); + Require(GuardIntact(storage)); + } + for (const auto fn : kDestructors) { + Storage storage; + std::memset(storage.bytes, kFill, sizeof(storage.bytes)); + Require(fn(storage.bytes) == 0); + Require(GuardIntact(storage)); + } + for (const auto fn : kTerminators) { + Storage storage; + std::memset(storage.bytes, 0, sizeof(storage.bytes)); + Require(fn(storage.bytes) == kNotInitialized); + } + for (const auto fn : kInitializers) { + Storage storage; + std::memset(storage.bytes, kFill, sizeof(storage.bytes)); + std::memset(storage.bytes, 0, kParameterSize); + std::uint64_t context = 1; + Require(fn(storage.bytes, &context) == 0); + Require(fn(storage.bytes, &context) == kAlreadyInitialized); + Require(kTerminators[0](storage.bytes) == 0); + Require(kTerminators[0](storage.bytes) == kNotInitialized); + Require(GuardIntact(storage)); + } + return 0; +} From bb6d79e52e3d8ec72a604af30d0e4429b193cf66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0?= Date: Thu, 29 Oct 2026 18:11:39 +0100 Subject: [PATCH 08/49] test(libSceAudioOut): drop the comment above the blocking push test Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libSceAudioOut/tests/AudioOut2Timing.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOut2Timing.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOut2Timing.cpp index 4d197d49db..6a7a614f7c 100644 --- a/core/libs/prx/libSceAudioOut/tests/AudioOut2Timing.cpp +++ b/core/libs/prx/libSceAudioOut/tests/AudioOut2Timing.cpp @@ -201,9 +201,6 @@ void TestLevelWithoutADeviceUsesThePlaybackClock() { Require(level == 0 && available == queueDepth); } -// Without an SDL device (here the disk driver cannot open a directory) a blocking push returns only once -// the queue has room again, so a feeder thread that wakes its renderer after every push keeps playback -// fed. The push that fills the queue waits for the oldest grain's playback to end. void TestBlockingPushWaitsForRoomWithoutADevice() { SetEnvironment("SDL_DISKAUDIOFILE", std::filesystem::temp_directory_path().string()); constexpr std::uint32_t paced = 1024; From 1ed07de320e705708d16a5a48f0f8e2369dd88bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0?= Date: Thu, 29 Oct 2026 18:43:12 +0100 Subject: [PATCH 09/49] test(libSceAudioOut): assert the blocking push level, not elapsed time; note destroy during a push is unsupported Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libSceAudioOut/tests/AudioOut2Timing.cpp | 4 ---- docs/dev/debt/libSceAudioOut.md | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOut2Timing.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOut2Timing.cpp index 6a7a614f7c..c577b715f1 100644 --- a/core/libs/prx/libSceAudioOut/tests/AudioOut2Timing.cpp +++ b/core/libs/prx/libSceAudioOut/tests/AudioOut2Timing.cpp @@ -211,16 +211,12 @@ void TestBlockingPushWaitsForRoomWithoutADevice() { AudioOut2ContextHandle handle = 0; Require(sceAudioOut2ContextCreate(¶ms, nullptr, 0, &handle) == 0); Require(reinterpret_cast(handle)->device == 0); - const auto grainTime = std::chrono::duration(static_cast(paced) / frequency); std::uint32_t level = 99; std::uint32_t available = 99; for (std::uint32_t round = 0; round < 4; round++) { - const auto start = std::chrono::steady_clock::now(); Require(sceAudioOut2ContextPush(handle, 1) == 0); - const auto took = std::chrono::steady_clock::now() - start; Require(sceAudioOut2ContextGetQueueLevel(handle, &level, &available) == 0); Require(level < queueDepth && available > 0); - if (round + 1 >= queueDepth) Require(took >= grainTime / 2); } Require(sceAudioOut2ContextDestroy(handle) == 0); } diff --git a/docs/dev/debt/libSceAudioOut.md b/docs/dev/debt/libSceAudioOut.md index 3369dc4ce2..fa6b37143e 100644 --- a/docs/dev/debt/libSceAudioOut.md +++ b/docs/dev/debt/libSceAudioOut.md @@ -130,7 +130,7 @@ ### Functional -- [sceAudioOut2ContextPush](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp) (libSceAudioOut) - a blocking Push waits for room before it queues its grain and again after, so it returns once fewer grains than the queue depth wait to be played (each wait ends after at most 200 ms). Inferred from PPSA02530, whose sound engine pushes one grain per call from a feeder thread and wakes its renderer after each push: while Push returned as soon as the grain was queued, the renderer found the queue full and slept until the next rendered frame. When the console's blocking Push returns is unmeasured. +- [sceAudioOut2ContextPush](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp) (libSceAudioOut) - a blocking Push waits for room before it queues its grain and again after, so it returns once fewer grains than the queue depth wait to be played (each wait ends after at most 200 ms). Inferred from PPSA02530, whose sound engine pushes one grain per call from a feeder thread and wakes its renderer after each push: while Push returned as soon as the grain was queued, the renderer found the queue full and slept until the next rendered frame. When the console's blocking Push returns is unmeasured. Both waits release the context lock, so destroying a context while a push is waiting on it is not supported. - [sceAudioOutMasteringInit, sceAudioOutMasteringTerm and sceAudioOutMasteringSetParam](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) - non-zero flags, a null pointer and a params id other than 1 throw; a repeated Init, a Term without Init and a SetParam or GetState before Init are unknown. There is no mastering stage, so Init and Term keep no state and SetParam reads only the id. The signatures and the default params id 1 follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioOut.cs#L312-L344), not a console measurement. SharpProspero documents flag bit 0 as `SwapEqCompressor`, and shadPS4's PS4 stub [rejects any non-zero Init flags](https://github.com/shadps4-emu/shadPS4/blob/8d50471b7dc141dd700b51276b8edd1f293bce26/src/core/libraries/audio/audioout.cpp#L956-L962) with 0x80260201 - [sceAudioOutMasteringGetState](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) - the states id 1 and the meter offsets 32, 128, 240, 272 and 304 in a 336-byte struct follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Audio/AudioOutMastering.cs#L97-L110), not a console measurement. With no mastering stage it zeroes the compressor input RMS and gain, and the limiter input, output and gain peaks, and leaves the other bytes, including the descriptors the AudioOut2 call writes, untouched; zero is not known to be what the console reports for an idle chain. A null pointer and a states id other than 1 throw; the console error codes are unknown - [sceAudioOut2EnableChat and sceAudioOut2DisableChat](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) - both PS5 exports take no arguments, return 0x80268006 before AudioOut2 initialization, and invoke the system chat service with selector 3. A negative service result maps to 0x80268000, otherwise they return zero. The host only records a local enabled flag, without a chat service; repeated-call and no-session behavior therefore still depends on the missing service. From f828b226deb579076c168dedce5ba7deb94a0ec8 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Thu, 29 Oct 2026 16:35:08 -0400 Subject: [PATCH 10/49] fix(tools): avoid expanding unrelated helper host branches --- tools/import_audit.py | 3 +++ tools/tests/test_import_audit.py | 27 +++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/tools/import_audit.py b/tools/import_audit.py index 1c8d505787..2113f52293 100644 --- a/tools/import_audit.py +++ b/tools/import_audit.py @@ -586,7 +586,10 @@ def unconditional_calls(body): def always_stub_calls(text): helpers = {} + possible = progress.stub_calls(text) for match in progress.STUB_WRAPPER.finditer(text): + if match.group(1) not in possible: + continue body = text[match.end() - 1:progress.body_end(text, match.end() - 1) + 1] helpers.setdefault(match.group(1), []).extend(variant_calls(body)) calls = {progress.STUB} diff --git a/tools/tests/test_import_audit.py b/tools/tests/test_import_audit.py index f960029b00..75f89c4dea 100644 --- a/tools/tests/test_import_audit.py +++ b/tools/tests/test_import_audit.py @@ -1679,6 +1679,33 @@ class ImportAuditTests(unittest.TestCase): with self.assertRaisesRegex(import_audit.AuditError, "more than 256 preprocessor branch combinations"): import_audit.source_stub_classes(source) + def test_unrelated_helper_host_branches_do_not_abort_source_classification(self): + blocks = "".join(f"#ifdef HOST_{index}\n value += {index};\n#endif\n" for index in range(10)) + source = self.work.source + write_source(source, "libHosts", + f"int PollDescriptors(int value) {{\n{blocks} return value;\n}}\n" + "int PollAgain(int value) { return PollDescriptors(value); }\n" + "void FirstFail() { LastFail(); }\n" + "void LastFail() { NotImplemented_nid_no_patch(__func__); }\n" + "int APS5_VABI scePoll(int value) { return PollAgain(value); }\n" + "int APS5_VABI sceStub(int value) { FirstFail(); return value; }\n" + "int APS5_VABI scePartial(int value) { if (value) FirstFail(); return value; }\n") + stubs, partial = import_audit.source_stub_classes(source) + stub = ("libHosts.prx", nid_names.compute_nid("sceStub")) + guarded = ("libHosts.prx", nid_names.compute_nid("scePartial")) + self.assertEqual(set(stubs), {stub, guarded}) + self.assertEqual(partial, {guarded}) + + def test_relevant_helper_host_branches_keep_the_variant_limit(self): + blocks = "".join(f"#ifdef HOST_{index}\n value += {index};\n#endif\n" for index in range(9)) + source = self.work.source + write_source(source, "libHosts", + f"void LastFail(int value) {{\n{blocks} NotImplemented_nid_no_patch(__func__);\n}}\n" + "void FirstFail(int value) { LastFail(value); }\n" + "int APS5_VABI sceStub(int value) { FirstFail(value); return value; }\n") + with self.assertRaisesRegex(import_audit.AuditError, "more than 256 preprocessor branch combinations"): + import_audit.source_stub_classes(source) + def test_partial_import_is_its_own_class(self): direct, guarded = nid_names.compute_nid("sceDirect"), nid_names.compute_nid("sceGuarded") stubs = {("libA.prx", direct): "sceDirect", ("libA.prx", guarded): "sceGuarded", ("libB.prx", guarded): "sceGuarded"} From 30d5439075e33df009d3cd3cee47a9d8b8f1cedc Mon Sep 17 00:00:00 2001 From: JJenkx Date: Thu, 29 Oct 2026 18:09:10 -0400 Subject: [PATCH 11/49] fix(libc): preserve errno on stream scan input exhaustion --- core/libs/prx/libc/src/Formatting.cpp | 5 +++- core/libs/tests/FileStreams.cpp | 34 +++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libc/src/Formatting.cpp b/core/libs/prx/libc/src/Formatting.cpp index 0f5176f7fc..075928247b 100644 --- a/core/libs/prx/libc/src/Formatting.cpp +++ b/core/libs/prx/libc/src/Formatting.cpp @@ -129,7 +129,10 @@ int HostSscanf(const char* input, const char* format, ...) { #ifndef _WIN32 int HostVfscanf(std::FILE* stream, const char* format, std::va_list args) { #if defined(__GLIBC__) - return __isoc99_vfscanf(stream, format, args); + const int savedError = errno; + const int result = __isoc99_vfscanf(stream, format, args); + if (result == EOF && errno == 0) errno = savedError; + return result; #else return std::vfscanf(stream, format, args); #endif diff --git a/core/libs/tests/FileStreams.cpp b/core/libs/tests/FileStreams.cpp index 02eed2f0a3..5f2b8e40bd 100644 --- a/core/libs/tests/FileStreams.cpp +++ b/core/libs/tests/FileStreams.cpp @@ -120,6 +120,39 @@ static int APS5_VABI ScanList(FileStream* stream, const char* format, ...) { return result; } +static void CheckScanInputFailureErrno() { + const int savedError = errno; + for (const bool variadicList : {false, true}) { + for (const int seed : {0, 22, 97}) { + for (const char* input : {"", " "}) { + for (const char* format : {"%d", "%*d"}) { + auto* native = std::tmpfile(); + Require(native != nullptr); + Require(std::fwrite(input, 1, std::strlen(input), native) == std::strlen(input)); + std::rewind(native); + FileStream stream(native); + int value = 123; + errno = seed; + const int result = variadicList ? ScanList(&stream, format, &value) : fscanf_nid_postfix(&stream, format, &value); + Require(result == EOF && errno == seed, "Stream scan input exhaustion changed errno"); + Require(value == 123 && feof_nid_postfix(&stream) != 0 && ferror_nid_postfix(&stream) == 0); + Require(std::fclose(native) == 0); + } + } + } + auto* native = std::tmpfile(); + Require(native != nullptr && std::fputs("1e99999", native) >= 0); + std::rewind(native); + FileStream stream(native); + float value = 0; + errno = 97; + const int result = variadicList ? ScanList(&stream, "%f", &value) : fscanf_nid_postfix(&stream, "%f", &value); + Require(result == 1 && errno == ERANGE, "Stream scan lost its conversion range error"); + Require(std::fclose(native) == 0); + } + errno = savedError; +} + static std::string ReadWhole(const std::string& path) { auto* stream = fopen_nid_postfix(path.c_str(), "rb"); Require(stream != nullptr); @@ -142,6 +175,7 @@ static void CheckReadAfterWrite(const std::string& path, const char* mode, const } static void CheckUpdateStreamReadsAfterWrites(const std::string& base) { + CheckScanInputFailureErrno(); const std::string path = base + ".update"; constexpr char overwritten[] = "AB23456789"; CheckReadAfterWrite(path, "r+b", overwritten, [](FileStream* stream) { From ce32d855ba3817332ab44e454f95f6257836943d Mon Sep 17 00:00:00 2001 From: JJenkx Date: Thu, 29 Oct 2026 19:00:11 -0400 Subject: [PATCH 12/49] fix(libc): ignore non-wide lengths on character scans --- core/libs/prx/libc/src/Formatting.cpp | 42 +++++++++++++++++++++++++++ core/libs/tests/FileStreams.cpp | 40 +++++++++++++++++++++++++ core/libs/tests/Formatting.cpp | 38 ++++++++++++++++++++++++ docs/dev/debt/libc.md | 1 + 4 files changed, 121 insertions(+) diff --git a/core/libs/prx/libc/src/Formatting.cpp b/core/libs/prx/libc/src/Formatting.cpp index 0f5176f7fc..e0fc7bc3cf 100644 --- a/core/libs/prx/libc/src/Formatting.cpp +++ b/core/libs/prx/libc/src/Formatting.cpp @@ -102,6 +102,7 @@ const char* CutAtQuadScanDirective(const char* format, std::string& head) { #endif std::string HostLength(const std::string& length, char conversion) { + if (std::strchr("sc[", conversion) && length != "l") return {}; #ifdef _WIN32 if (length == "l" && std::strchr("diouxXn", conversion) != nullptr) return "ll"; #else @@ -110,7 +111,46 @@ std::string HostLength(const std::string& length, char conversion) { return length; } +const char* ScanLengthEnd(const char* cursor) { + if (*cursor == 'h' || *cursor == 'l') { + const char first = *cursor++; + if (*cursor == first) ++cursor; + } else if (*cursor != '\0' && std::strchr("jztL", *cursor)) ++cursor; + return cursor; +} + +const char* HostScanFormat(const char* format, std::string& normalized) { + if (format == nullptr) return format; + const char* copied = format; + const char* cursor = format; + while ((cursor = std::strchr(cursor, '%')) != nullptr) { + ++cursor; + if (*cursor == '%') { ++cursor; continue; } + while (*cursor && std::strchr("-+ #0'0123456789.*$", *cursor)) ++cursor; + const char* const length = cursor; + cursor = ScanLengthEnd(cursor); + const bool wide = cursor == length + 1 && *length == 'l'; + if (cursor != length && !wide && *cursor && std::strchr("sc[", *cursor)) { + normalized.append(copied, length); + copied = cursor; + } + if (*cursor == '[') { + ++cursor; + if (*cursor == '^') ++cursor; + if (*cursor == ']') ++cursor; + while (*cursor && *cursor != ']') ++cursor; + } + if (*cursor) ++cursor; + else break; + } + if (copied == format) return format; + normalized.append(copied); + return normalized.c_str(); +} + int HostVsscanf(const char* input, const char* format, std::va_list args) { + std::string normalized; + format = HostScanFormat(format, normalized); #if defined(__GLIBC__) return __isoc99_vsscanf(input, format, args); #else @@ -128,6 +168,8 @@ int HostSscanf(const char* input, const char* format, ...) { #ifndef _WIN32 int HostVfscanf(std::FILE* stream, const char* format, std::va_list args) { + std::string normalized; + format = HostScanFormat(format, normalized); #if defined(__GLIBC__) return __isoc99_vfscanf(stream, format, args); #else diff --git a/core/libs/tests/FileStreams.cpp b/core/libs/tests/FileStreams.cpp index 02eed2f0a3..a1986db188 100644 --- a/core/libs/tests/FileStreams.cpp +++ b/core/libs/tests/FileStreams.cpp @@ -2,6 +2,8 @@ #include "prx/libc/include/FileStream.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include +#include +#include #include #include #include @@ -129,6 +131,43 @@ static std::string ReadWhole(const std::string& path) { return std::string(buffer.data(), size); } +static int APS5_VABI ScanSafeList(FileStream* stream, const char* format, ...) { + __builtin_sysv_va_list args; + __builtin_sysv_va_start(args, format); + const int result = vfscanf_s_nid_postfix(stream, format, reinterpret_cast(args)); + __builtin_sysv_va_end(args); + return result; +} + +static void CheckCharacterScanLengths() { + using Scanner = int (APS5_VABI*)(FileStream*, const char*, ...); + const Scanner scanners[]{fscanf_nid_postfix, ScanList, fscanf_s_nid_postfix, ScanSafeList}; + for (std::size_t api = 0; api < std::size(scanners); ++api) { +#ifdef _WIN32 + if (api < 2) continue; +#endif + for (const char* length : {"hh", "h", "ll", "j", "z", "t", "L"}) { + for (const char* conversion : {"s", "c", "[a-z]"}) { + auto* native = std::tmpfile(); + Require(native != nullptr && std::fputs("ab", native) >= 0); + std::rewind(native); + FileStream stream(native); + std::array output; + output.fill('Z'); + int consumed = -1; + const std::string format = std::string("%2") + length + conversion + "%n"; + const auto scan = scanners[api]; + const int result = api >= 2 ? scan(&stream, format.c_str(), output.data(), 32u, &consumed) : scan(&stream, format.c_str(), output.data(), &consumed); + Require(result == 1 && consumed == 2, "Stream character scan ignored-length consumption"); + const bool single = conversion[0] == 'c'; + Require(output[0] == 'a' && output[1] == 'b' && (single || output[2] == 0), "Stream character scan ignored length stored wide data"); + Require(std::all_of(output.begin() + (single ? 2 : 3), output.end(), [](char value) { return value == 'Z'; })); + Require(std::fclose(native) == 0); + } + } + } +} + template static void CheckReadAfterWrite(const std::string& path, const char* mode, const char* expected, TRead read) { auto* stream = fopen_nid_postfix(path.c_str(), "wb"); @@ -210,6 +249,7 @@ static void CheckWriteAfterRead(const std::string& path, const char* mode, const } static void CheckStreamWritesAfterReads(const std::string& base) { + CheckCharacterScanLengths(); const std::string path = base + ".written"; constexpr char appended[] = "0123456789XY"; #if defined(__GLIBC__) diff --git a/core/libs/tests/Formatting.cpp b/core/libs/tests/Formatting.cpp index ed534f18d7..46e9914b45 100644 --- a/core/libs/tests/Formatting.cpp +++ b/core/libs/tests/Formatting.cpp @@ -5,6 +5,8 @@ #include "prx/libc/include/ScanQuadLength.hpp" #include #include +#include +#include #include #include #include @@ -67,6 +69,41 @@ static int APS5_VABI ScanListS(const char* buffer, const char* format, ...) { return result; } +static void CheckCharacterScanLengths() { + const auto check = [](bool condition, const char* message = "Character scan length check failed") { + if (!condition) throw std::runtime_error(message); + }; + using Scanner = int (APS5_VABI*)(const char*, const char*, ...); + const Scanner scanners[]{sscanf_nid_postfix, ScanList, sscanf_s_nid_postfix, ScanListS}; + for (std::size_t api = 0; api < std::size(scanners); ++api) { + const auto scan = scanners[api]; + const bool secure = api >= 2; +#ifdef _WIN32 + if (!secure) continue; +#endif + for (const char* length : {"hh", "h", "ll", "j", "z", "t", "L"}) { + for (const char* conversion : {"s", "c", "[a-z]"}) { + const std::string format = std::string("%2") + length + conversion + "%n"; + std::array output; + output.fill('Z'); + int consumed = -1; + const int result = secure ? scan("ab", format.c_str(), output.data(), 32u, &consumed) : scan("ab", format.c_str(), output.data(), &consumed); + check(result == 1 && consumed == 2, "Character scan ignored-length consumption"); + const bool single = conversion[0] == 'c'; + check(output[0] == 'a' && output[1] == 'b' && (single || output[2] == '\0'), "Character scan ignored length stored wide data"); + check(std::all_of(output.begin() + (single ? 2 : 3), output.end(), [](char value) { return value == 'Z'; }), "Character scan changed its destination guard"); + } + } + for (const auto& example : {std::pair{"%lls", "%5[%lls]%n"}, std::pair{"% ab", "%% %2lls%n"}}) { + std::array output{}; + int consumed = -1; + const int result = secure ? scan(example.first, example.second, output.data(), 32u, &consumed) : scan(example.first, example.second, output.data(), &consumed); + check(result == 1 && consumed == 4); + check(std::strcmp(output.data(), example.first[1] == ' ' ? "ab" : "%lls") == 0, "Character scan normalization changed a literal or scanset"); + } + } +} + static int APS5_VABI PrintList(const char* format, ...) { __builtin_sysv_va_list args; __builtin_sysv_va_start(args, format); @@ -78,6 +115,7 @@ static int APS5_VABI PrintList(const char* format, ...) { } static void CheckIgnoredLengths() { + CheckCharacterScanLengths(); char output[96]; Require(snprintf_nid_postfix(output, sizeof(output), "%Ld|%Lx|%Lu|%Lo", -1985229329, 0x89abcdefu, 0x89abcdefu, 0x89abcdefu) == 43 && std::strcmp(output, "-1985229329|89abcdef|2309737967|21152746757") == 0); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 82a66c7628..b1ed721fd4 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -107,6 +107,7 @@ - [sscanf_s and vsscanf_s](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) return `EOF` when a reached conversion has a null destination, stopping before further assignments, following C11 K.3.5.3.7 and K.3.5.3.14. Earlier assignments are retained. The constraint handler is not invoked, and console behaviour is unverified. - [memmem and memccpy](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - `memmem` searches for needle occurrences in memory buffers and `memccpy` copies buffers until a character match, following FreeBSD and POSIX specifications; behavior is unverified on console. Neither is exported by `libSceLibcInternal.sprx` on a PS5 (firmware 13.60), so a payload process cannot call them there. - [strtof and wcstof](../../../core/libs/prx/libc/src/Strings.cpp) (libc) round to the nearest float, where the console's `_Stof` in `libSceLibcInternal.sprx` (0x734a0) does not always: about a third of decimal inputs within a relative 2^-51 of a point halfway between two floats, and some hexadecimal inputs with more than 24 significant bits, come out one unit apart in the last place (`1e-40` is `0x1.16c3p-133` there and `0x1.16c2p-133` here). The console's own rounding is not reproduced +- [Short narrow character fields](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) - on glibc-backed scan paths, `%2c` with one available input byte can return `EOF` after storing that byte, while the bundled modules in PPSA03766 and PPSA07923 count one assignment. This is a CPU-only module comparison, not a console run. - [strtof and wcstof](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - `ERANGE` follows the `libc.prx` bundled with PPSA08427 (J03547016) and PPSA03671 (J03848509), both run side by side with this libc on a Linux host: a decimal input whose result is a nonzero subnormal sets it only when inexact, a hexadecimal one only when it lies below the smallest subnormal and rounds up to it, and underflow to zero and overflow set it. Both builds also take a `NAN(...)` payload in `strtof` from the sequence's leading digits (`nan(0x123)` is `0x7fc00123`, `-nan(5)` is `0xffc00005`); the sign is kept here, the payload is not. `libSceLibcInternal`'s `_Stof` differs: no `ERANGE` for hexadecimal input, a positive `nan`, and after a minus sign a failed parse is `-0.0` for a lone sign and `+0.0` before an `inf`/`nan` spelling, the reverse of `libc.prx`. Decimal `strtod` sets `ERANGE` for a nonzero subnormal in J03547016 but not in J03848509 or `libSceLibcInternal`, whose result is kept. - [operator new](../../../core/libs/prx/libc/src/StdioExtras.cpp) (libc) - the `nothrow` forms return null only for `std::bad_alloc`, where the `libc.prx` bundled with PPSA21564 (build `J03093718`) catches every exception there, so a new handler that throws anything else ends the process instead of returning null. The aligned forms throw `std::bad_alloc` for an alignment of zero or one that is not a power of two, where that binary calls `abort`. From 1f905993a3463cc9aa39460b77491bc9eb4405d7 Mon Sep 17 00:00:00 2001 From: hugodearaujo Date: Thu, 29 Oct 2026 20:40:18 -0300 Subject: [PATCH 13/49] docs(libSceAudioOut): state the blocking push's combined worst-case wait --- docs/dev/debt/libSceAudioOut.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceAudioOut.md b/docs/dev/debt/libSceAudioOut.md index fa6b37143e..2186ade6d0 100644 --- a/docs/dev/debt/libSceAudioOut.md +++ b/docs/dev/debt/libSceAudioOut.md @@ -130,7 +130,7 @@ ### Functional -- [sceAudioOut2ContextPush](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp) (libSceAudioOut) - a blocking Push waits for room before it queues its grain and again after, so it returns once fewer grains than the queue depth wait to be played (each wait ends after at most 200 ms). Inferred from PPSA02530, whose sound engine pushes one grain per call from a feeder thread and wakes its renderer after each push: while Push returned as soon as the grain was queued, the renderer found the queue full and slept until the next rendered frame. When the console's blocking Push returns is unmeasured. Both waits release the context lock, so destroying a context while a push is waiting on it is not supported. +- [sceAudioOut2ContextPush](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp) (libSceAudioOut) - a blocking Push waits for room before it queues its grain and again after, so it returns once fewer grains than the queue depth wait to be played (each wait ends after at most 200 ms, so on a queue that never drains a blocking Push returns after up to 400 ms). Inferred from PPSA02530, whose sound engine pushes one grain per call from a feeder thread and wakes its renderer after each push: while Push returned as soon as the grain was queued, the renderer found the queue full and slept until the next rendered frame. When the console's blocking Push returns is unmeasured. Both waits release the context lock, so destroying a context while a push is waiting on it is not supported. - [sceAudioOutMasteringInit, sceAudioOutMasteringTerm and sceAudioOutMasteringSetParam](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) - non-zero flags, a null pointer and a params id other than 1 throw; a repeated Init, a Term without Init and a SetParam or GetState before Init are unknown. There is no mastering stage, so Init and Term keep no state and SetParam reads only the id. The signatures and the default params id 1 follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioOut.cs#L312-L344), not a console measurement. SharpProspero documents flag bit 0 as `SwapEqCompressor`, and shadPS4's PS4 stub [rejects any non-zero Init flags](https://github.com/shadps4-emu/shadPS4/blob/8d50471b7dc141dd700b51276b8edd1f293bce26/src/core/libraries/audio/audioout.cpp#L956-L962) with 0x80260201 - [sceAudioOutMasteringGetState](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) - the states id 1 and the meter offsets 32, 128, 240, 272 and 304 in a 336-byte struct follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Audio/AudioOutMastering.cs#L97-L110), not a console measurement. With no mastering stage it zeroes the compressor input RMS and gain, and the limiter input, output and gain peaks, and leaves the other bytes, including the descriptors the AudioOut2 call writes, untouched; zero is not known to be what the console reports for an idle chain. A null pointer and a states id other than 1 throw; the console error codes are unknown - [sceAudioOut2EnableChat and sceAudioOut2DisableChat](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) - both PS5 exports take no arguments, return 0x80268006 before AudioOut2 initialization, and invoke the system chat service with selector 3. A negative service result maps to 0x80268000, otherwise they return zero. The host only records a local enabled flag, without a chat service; repeated-call and no-session behavior therefore still depends on the missing service. From f0fa366942086bad739a3d1309f3cdf31e8e34d6 Mon Sep 17 00:00:00 2001 From: Eduardo Ruiz Date: Fri, 30 Oct 2026 01:09:00 +0100 Subject: [PATCH 14/49] fix(libSceNpCppWebApi): take the Matchmaking parameter arguments Declare the string and body arguments of the four initialize exports and use InitializeParameterWithBody for SubmitTicket. Give setview the same text handling and cover it in the test. Drop the accessor throw assertions and record the namespace debt. --- core/libs/prx/libSceNpCppWebApi/Export.cpp | 21 ++- .../prx/libSceNpCppWebApi/include/Api.hpp | 10 +- .../tests/GuestNpCppWebApiMatchmaking.cpp | 123 ++++++------------ docs/dev/debt/libSceNpCppWebApi.md | 1 + 4 files changed, 59 insertions(+), 96 deletions(-) diff --git a/core/libs/prx/libSceNpCppWebApi/Export.cpp b/core/libs/prx/libSceNpCppWebApi/Export.cpp index ecb5a441a0..838e09d4b8 100644 --- a/core/libs/prx/libSceNpCppWebApi/Export.cpp +++ b/core/libs/prx/libSceNpCppWebApi/Export.cpp @@ -339,8 +339,8 @@ int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api12submitTicketEiRKNS4_23Pa return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; } -int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api19ParameterToGetOffer10initializeEPNS1_6Common10LibContextEPKc(void* self, const std::uint64_t* context) { - return InitializeParameter(self, context, true); +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api19ParameterToGetOffer10initializeEPNS1_6Common10LibContextEPKc(void* self, const std::uint64_t* context, const char* id) { + return InitializeParameter(self, context, id != nullptr); } int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api19ParameterToGetOffer9terminateEv(void* self) { @@ -357,13 +357,12 @@ int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api19ParameterToGetOfferD1Ev( return 0; } -int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api20ParameterToGetTicket10initializeEPNS1_6Common10LibContextEPKc(void* self, const std::uint64_t* context) { - return InitializeParameter(self, context, true); +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api20ParameterToGetTicket10initializeEPNS1_6Common10LibContextEPKc(void* self, const std::uint64_t* context, const char* id) { + return InitializeParameter(self, context, id != nullptr); } -int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api20ParameterToGetTicket7setviewEPKc(void* self) { - (void)self; - return 0; +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api20ParameterToGetTicket7setviewEPKc(void* self, const char* view) { + return SetParameterText(self, view); } int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api20ParameterToGetTicket9terminateEv(void* self) { @@ -380,8 +379,8 @@ int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api20ParameterToGetTicketD1Ev return 0; } -int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api23ParameterToCancelTicket10initializeEPNS1_6Common10LibContextEPKc(void* self, const std::uint64_t* context) { - return InitializeParameter(self, context, true); +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api23ParameterToCancelTicket10initializeEPNS1_6Common10LibContextEPKc(void* self, const std::uint64_t* context, const char* id) { + return InitializeParameter(self, context, id != nullptr); } int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api23ParameterToCancelTicket9terminateEv(void* self) { @@ -398,8 +397,8 @@ int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api23ParameterToCancelTicketD return 0; } -int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api23ParameterToSubmitTicket10initializeEPNS1_6Common10LibContextENS6_12IntrusivePtrINS3_23SubmitTicketRequestBodyEEE(void* self, const std::uint64_t* context) { - return InitializeParameter(self, context, true); +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api23ParameterToSubmitTicket10initializeEPNS1_6Common10LibContextENS6_12IntrusivePtrINS3_23SubmitTicketRequestBodyEEE(void* self, const std::uint64_t* context, const IntrusivePtr* body) { + return InitializeParameterWithBody(self, context, true, body); } int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api23ParameterToSubmitTicket9terminateEv(void* self) { diff --git a/core/libs/prx/libSceNpCppWebApi/include/Api.hpp b/core/libs/prx/libSceNpCppWebApi/include/Api.hpp index dd05a46456..c1ba38df59 100644 --- a/core/libs/prx/libSceNpCppWebApi/include/Api.hpp +++ b/core/libs/prx/libSceNpCppWebApi/include/Api.hpp @@ -34,20 +34,20 @@ int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V128PlayerForTicketCreateFactory int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V130SubmitTicketRequestBodyFactory6createEPNS1_6Common10LibContextEPKcRKNS5_6VectorINS5_12IntrusivePtrINS3_21PlayerForTicketCreateEEEEEPNSB_INS3_23SubmitTicketRequestBodyEEE(void* self); int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api12cancelTicketEiRKNS4_23ParameterToCancelTicketERNS1_6Common11TransactionINS8_15DefaultResponseENS8_12IntrusivePtrINS8_18ResponseHeaderBaseEEEEE(void* self); int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api12submitTicketEiRKNS4_23ParameterToSubmitTicketERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_24SubmitTicketResponseBodyEEENSA_INS8_18ResponseHeaderBaseEEEEE(void* self); -int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api19ParameterToGetOffer10initializeEPNS1_6Common10LibContextEPKc(void* self, const std::uint64_t* context); +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api19ParameterToGetOffer10initializeEPNS1_6Common10LibContextEPKc(void* self, const std::uint64_t* context, const char* id); int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api19ParameterToGetOffer9terminateEv(void* self); int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api19ParameterToGetOfferC1Ev(void* self); int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api19ParameterToGetOfferD1Ev(void* self); -int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api20ParameterToGetTicket10initializeEPNS1_6Common10LibContextEPKc(void* self, const std::uint64_t* context); -int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api20ParameterToGetTicket7setviewEPKc(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api20ParameterToGetTicket10initializeEPNS1_6Common10LibContextEPKc(void* self, const std::uint64_t* context, const char* id); +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api20ParameterToGetTicket7setviewEPKc(void* self, const char* view); int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api20ParameterToGetTicket9terminateEv(void* self); int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api20ParameterToGetTicketC1Ev(void* self); int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api20ParameterToGetTicketD1Ev(void* self); -int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api23ParameterToCancelTicket10initializeEPNS1_6Common10LibContextEPKc(void* self, const std::uint64_t* context); +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api23ParameterToCancelTicket10initializeEPNS1_6Common10LibContextEPKc(void* self, const std::uint64_t* context, const char* id); int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api23ParameterToCancelTicket9terminateEv(void* self); int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api23ParameterToCancelTicketC1Ev(void* self); int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api23ParameterToCancelTicketD1Ev(void* self); -int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api23ParameterToSubmitTicket10initializeEPNS1_6Common10LibContextENS6_12IntrusivePtrINS3_23SubmitTicketRequestBodyEEE(void* self, const std::uint64_t* context); +int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api23ParameterToSubmitTicket10initializeEPNS1_6Common10LibContextENS6_12IntrusivePtrINS3_23SubmitTicketRequestBodyEEE(void* self, const std::uint64_t* context, const IntrusivePtr* body); int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api23ParameterToSubmitTicket9terminateEv(void* self); int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api23ParameterToSubmitTicketC1Ev(void* self); int APS5_VABI _ZN3sce2Np9CppWebApi11Matchmaking2V13Api23ParameterToSubmitTicketD1Ev(void* self); diff --git a/core/libs/tests/GuestNpCppWebApiMatchmaking.cpp b/core/libs/tests/GuestNpCppWebApiMatchmaking.cpp index 71eecbcaa7..a5a32c4aa4 100644 --- a/core/libs/tests/GuestNpCppWebApiMatchmaking.cpp +++ b/core/libs/tests/GuestNpCppWebApiMatchmaking.cpp @@ -6,7 +6,6 @@ #include #include #include -#include namespace { @@ -18,8 +17,11 @@ void Require(bool value, std::source_location at = std::source_location::current } constexpr int kNotSignedIn = static_cast(0x80552907); +constexpr int kParameterNotInitialized = static_cast(0x80553502); +constexpr int kInvalidArgument = static_cast(0x80553503); constexpr int kAlreadyInitialized = static_cast(0x8055350f); constexpr int kNotInitialized = static_cast(0x80553510); +constexpr int kBodyMissing = static_cast(0x80553512); constexpr std::size_t kParameterSize = 0x10; constexpr std::size_t kGuardSize = 0x10; constexpr unsigned char kFill = 0xA5; @@ -36,7 +38,9 @@ bool GuardIntact(const Storage& storage) { } using SimpleFn = int (APS5_VABI*)(void*); -using InitFn = int (APS5_VABI*)(void*, const std::uint64_t*); +using TextFn = int (APS5_VABI*)(void*, const char*); +using StringInitFn = int (APS5_VABI*)(void*, const std::uint64_t*, const char*); +using BodyInitFn = int (APS5_VABI*)(void*, const std::uint64_t*, const IntrusivePtr*); const SimpleFn kSignedOut[] = { _ZN3sce2Np9CppWebApi11Matchmaking2V115LocationFactory6createEPNS1_6Common10LibContextEPNS5_12IntrusivePtrINS3_8LocationEEE, @@ -48,69 +52,6 @@ const SimpleFn kSignedOut[] = { _ZN3sce2Np9CppWebApi11Matchmaking2V13Api8getOfferEiRKNS4_19ParameterToGetOfferERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_20GetOfferResponseBodyEEENSA_INS8_18ResponseHeaderBaseEEEEE, _ZN3sce2Np9CppWebApi11Matchmaking2V13Api9getTicketEiRKNS4_20ParameterToGetTicketERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_21GetTicketResponseBodyEEENSA_INS8_18ResponseHeaderBaseEEEEE, }; -const SimpleFn kAccessors[] = { - _ZN3sce2Np9CppWebApi11Matchmaking2V113PlayerForRead19getPlayerAttributesEv, - _ZN3sce2Np9CppWebApi11Matchmaking2V120GetOfferResponseBody10getPlayersEv, - _ZN3sce2Np9CppWebApi11Matchmaking2V121GetTicketResponseBody10getPlayersEv, - _ZN3sce2Np9CppWebApi11Matchmaking2V121GetTicketResponseBody19getTicketAttributesEv, - _ZNK3sce2Np9CppWebApi11Matchmaking2V113PlayerForRead11getOnlineIdEv, - _ZNK3sce2Np9CppWebApi11Matchmaking2V113PlayerForRead11getPlatformEv, - _ZNK3sce2Np9CppWebApi11Matchmaking2V113PlayerForRead11getTeamNameEv, - _ZNK3sce2Np9CppWebApi11Matchmaking2V113PlayerForRead12getAccountIdEv, - _ZNK3sce2Np9CppWebApi11Matchmaking2V113PlayerForRead13onlineIdIsSetEv, - _ZNK3sce2Np9CppWebApi11Matchmaking2V113PlayerForRead13platformIsSetEv, - _ZNK3sce2Np9CppWebApi11Matchmaking2V113PlayerForRead13teamNameIsSetEv, - _ZNK3sce2Np9CppWebApi11Matchmaking2V113PlayerForRead14accountIdIsSetEv, - _ZNK3sce2Np9CppWebApi11Matchmaking2V113PlayerForRead21playerAttributesIsSetEv, - _ZNK3sce2Np9CppWebApi11Matchmaking2V118PlayerForOfferRead11getOnlineIdEv, - _ZNK3sce2Np9CppWebApi11Matchmaking2V118PlayerForOfferRead11getPlatformEv, - _ZNK3sce2Np9CppWebApi11Matchmaking2V118PlayerForOfferRead11getTeamNameEv, - _ZNK3sce2Np9CppWebApi11Matchmaking2V118PlayerForOfferRead11getTicketIdEv, - _ZNK3sce2Np9CppWebApi11Matchmaking2V118PlayerForOfferRead12getAccountIdEv, - _ZNK3sce2Np9CppWebApi11Matchmaking2V118PlayerForOfferRead13onlineIdIsSetEv, - _ZNK3sce2Np9CppWebApi11Matchmaking2V118PlayerForOfferRead13platformIsSetEv, - _ZNK3sce2Np9CppWebApi11Matchmaking2V118PlayerForOfferRead13teamNameIsSetEv, - _ZNK3sce2Np9CppWebApi11Matchmaking2V118PlayerForOfferRead13ticketIdIsSetEv, - _ZNK3sce2Np9CppWebApi11Matchmaking2V118PlayerForOfferRead14accountIdIsSetEv, - _ZNK3sce2Np9CppWebApi11Matchmaking2V120GetOfferResponseBody10getOfferIdEv, - _ZNK3sce2Np9CppWebApi11Matchmaking2V120GetOfferResponseBody11getLocationEv, - _ZNK3sce2Np9CppWebApi11Matchmaking2V120GetOfferResponseBody11statusIsSetEv, - _ZNK3sce2Np9CppWebApi11Matchmaking2V120GetOfferResponseBody12offerIdIsSetEv, - _ZNK3sce2Np9CppWebApi11Matchmaking2V120GetOfferResponseBody12playersIsSetEv, - _ZNK3sce2Np9CppWebApi11Matchmaking2V120GetOfferResponseBody13locationIsSetEv, - _ZNK3sce2Np9CppWebApi11Matchmaking2V120GetOfferResponseBody14getRulesetNameEv, - _ZNK3sce2Np9CppWebApi11Matchmaking2V120GetOfferResponseBody16rulesetNameIsSetEv, - _ZNK3sce2Np9CppWebApi11Matchmaking2V120GetOfferResponseBody18getCreatedDateTimeEv, - _ZNK3sce2Np9CppWebApi11Matchmaking2V120GetOfferResponseBody18getUpdatedDateTimeEv, - _ZNK3sce2Np9CppWebApi11Matchmaking2V120GetOfferResponseBody20createdDateTimeIsSetEv, - _ZNK3sce2Np9CppWebApi11Matchmaking2V120GetOfferResponseBody20updatedDateTimeIsSetEv, - _ZNK3sce2Np9CppWebApi11Matchmaking2V120GetOfferResponseBody9getStatusEv, - _ZNK3sce2Np9CppWebApi11Matchmaking2V121GetTicketResponseBody11getTicketIdEv, - _ZNK3sce2Np9CppWebApi11Matchmaking2V121GetTicketResponseBody11statusIsSetEv, - _ZNK3sce2Np9CppWebApi11Matchmaking2V121GetTicketResponseBody12getSubmitterEv, - _ZNK3sce2Np9CppWebApi11Matchmaking2V121GetTicketResponseBody12playersIsSetEv, - _ZNK3sce2Np9CppWebApi11Matchmaking2V121GetTicketResponseBody13ticketIdIsSetEv, - _ZNK3sce2Np9CppWebApi11Matchmaking2V121GetTicketResponseBody14getRulesetNameEv, - _ZNK3sce2Np9CppWebApi11Matchmaking2V121GetTicketResponseBody14submitterIsSetEv, - _ZNK3sce2Np9CppWebApi11Matchmaking2V121GetTicketResponseBody16rulesetNameIsSetEv, - _ZNK3sce2Np9CppWebApi11Matchmaking2V121GetTicketResponseBody18getCreatedDateTimeEv, - _ZNK3sce2Np9CppWebApi11Matchmaking2V121GetTicketResponseBody18getUpdatedDateTimeEv, - _ZNK3sce2Np9CppWebApi11Matchmaking2V121GetTicketResponseBody20createdDateTimeIsSetEv, - _ZNK3sce2Np9CppWebApi11Matchmaking2V121GetTicketResponseBody20updatedDateTimeIsSetEv, - _ZNK3sce2Np9CppWebApi11Matchmaking2V121GetTicketResponseBody21ticketAttributesIsSetEv, - _ZNK3sce2Np9CppWebApi11Matchmaking2V121GetTicketResponseBody9getStatusEv, - _ZNK3sce2Np9CppWebApi11Matchmaking2V124SubmitTicketResponseBody11getTicketIdEv, - _ZNK3sce2Np9CppWebApi11Matchmaking2V124SubmitTicketResponseBody13ticketIdIsSetEv, - _ZNK3sce2Np9CppWebApi11Matchmaking2V18Location16getGameSessionIdEv, - _ZNK3sce2Np9CppWebApi11Matchmaking2V18Location18gameSessionIdIsSetEv, - _ZNK3sce2Np9CppWebApi11Matchmaking2V19Attribute7getNameEv, - _ZNK3sce2Np9CppWebApi11Matchmaking2V19Attribute7getTypeEv, - _ZNK3sce2Np9CppWebApi11Matchmaking2V19Attribute8getValueEv, - _ZNK3sce2Np9CppWebApi11Matchmaking2V19Submitter11getPlatformEv, - _ZNK3sce2Np9CppWebApi11Matchmaking2V19Submitter12getAccountIdEv, - _ZNK3sce2Np9CppWebApi11Matchmaking2V19Submitter13platformIsSetEv, - _ZNK3sce2Np9CppWebApi11Matchmaking2V19Submitter14accountIdIsSetEv, -}; const SimpleFn kSetters[] = { _ZN3sce2Np9CppWebApi11Matchmaking2V121PlayerForTicketCreate10setNatTypeERKi, _ZN3sce2Np9CppWebApi11Matchmaking2V121PlayerForTicketCreate11setTeamNameEPKc, @@ -137,25 +78,18 @@ const SimpleFn kTerminators[] = { _ZN3sce2Np9CppWebApi11Matchmaking2V13Api23ParameterToCancelTicket9terminateEv, _ZN3sce2Np9CppWebApi11Matchmaking2V13Api23ParameterToSubmitTicket9terminateEv, }; -const InitFn kInitializers[] = { +const StringInitFn kStringInitializers[] = { _ZN3sce2Np9CppWebApi11Matchmaking2V13Api19ParameterToGetOffer10initializeEPNS1_6Common10LibContextEPKc, _ZN3sce2Np9CppWebApi11Matchmaking2V13Api20ParameterToGetTicket10initializeEPNS1_6Common10LibContextEPKc, _ZN3sce2Np9CppWebApi11Matchmaking2V13Api23ParameterToCancelTicket10initializeEPNS1_6Common10LibContextEPKc, - _ZN3sce2Np9CppWebApi11Matchmaking2V13Api23ParameterToSubmitTicket10initializeEPNS1_6Common10LibContextENS6_12IntrusivePtrINS3_23SubmitTicketRequestBodyEEE, }; - -template -void RequireThrows(TCall call) { - bool threw = false; - try { call(); } catch (const std::runtime_error&) { threw = true; } - Require(threw); -} +const BodyInitFn kBodyInitializer = _ZN3sce2Np9CppWebApi11Matchmaking2V13Api23ParameterToSubmitTicket10initializeEPNS1_6Common10LibContextENS6_12IntrusivePtrINS3_23SubmitTicketRequestBodyEEE; +const TextFn kSetView = _ZN3sce2Np9CppWebApi11Matchmaking2V13Api20ParameterToGetTicket7setviewEPKc; } int main() { for (const auto fn : kSignedOut) Require(fn(nullptr) == kNotSignedIn); - for (const auto fn : kAccessors) RequireThrows([&] { (void)fn(nullptr); }); for (const auto fn : kSetters) Require(fn(nullptr) == 0); for (const auto fn : kConstructors) { @@ -175,15 +109,44 @@ int main() { std::memset(storage.bytes, 0, sizeof(storage.bytes)); Require(fn(storage.bytes) == kNotInitialized); } - for (const auto fn : kInitializers) { + for (std::size_t index = 0; index < sizeof(kStringInitializers) / sizeof(kStringInitializers[0]); ++index) { Storage storage; std::memset(storage.bytes, kFill, sizeof(storage.bytes)); std::memset(storage.bytes, 0, kParameterSize); std::uint64_t context = 1; - Require(fn(storage.bytes, &context) == 0); - Require(fn(storage.bytes, &context) == kAlreadyInitialized); - Require(kTerminators[0](storage.bytes) == 0); - Require(kTerminators[0](storage.bytes) == kNotInitialized); + Require(kStringInitializers[index](storage.bytes, &context, nullptr) == kInvalidArgument); + Require(kStringInitializers[index](storage.bytes, &context, "id") == 0); + Require(kStringInitializers[index](storage.bytes, &context, "id") == kAlreadyInitialized); + Require(kTerminators[index](storage.bytes) == 0); + Require(kTerminators[index](storage.bytes) == kNotInitialized); + Require(GuardIntact(storage)); + } + { + Storage storage; + std::memset(storage.bytes, kFill, sizeof(storage.bytes)); + std::memset(storage.bytes, 0, kParameterSize); + std::uint64_t context = 1; + IntrusivePtr empty{}; + Require(kBodyInitializer(storage.bytes, &context, &empty) == kBodyMissing); + IntrusivePtr body{}; + body.object = storage.bytes; + Require(kBodyInitializer(storage.bytes, &context, &body) == 0); + Require(kBodyInitializer(storage.bytes, &context, &body) == kAlreadyInitialized); + Require(kTerminators[3](storage.bytes) == 0); + Require(kTerminators[3](storage.bytes) == kNotInitialized); + Require(GuardIntact(storage)); + } + { + Storage storage; + std::memset(storage.bytes, kFill, sizeof(storage.bytes)); + std::memset(storage.bytes, 0, kParameterSize); + std::uint64_t context = 1; + Require(kSetView(storage.bytes, "view") == kParameterNotInitialized); + Require(kStringInitializers[1](storage.bytes, &context, "id") == 0); + Require(kSetView(storage.bytes, "view") == 0); + Require(kSetView(storage.bytes, nullptr) == kInvalidArgument); + Require(kTerminators[1](storage.bytes) == 0); + Require(kSetView(storage.bytes, "view") == kParameterNotInitialized); Require(GuardIntact(storage)); } return 0; diff --git a/docs/dev/debt/libSceNpCppWebApi.md b/docs/dev/debt/libSceNpCppWebApi.md index 2b3559a12b..1e07ad891b 100644 --- a/docs/dev/debt/libSceNpCppWebApi.md +++ b/docs/dev/debt/libSceNpCppWebApi.md @@ -15,6 +15,7 @@ - [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 `copyFrom` throw: the only `Vector`s the title reads back come from failed transactions, which never produce one. `empty`, `size` and `operator[]` read the host-created body honestly and throw on title-provided bodies - [sce::Np::CppWebApi::SessionManager2V1](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - imported by PPSA03671. Offline, the factories, `PlayerSessionsApi` calls and transaction `start`/`getResponse`/`readData` fail with `SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN`; parameter setters, `initialize`, `terminate`, constructors and destructors succeed without keeping state, as no request is ever sent. Response-body accessors (`getSessionId`, `getPlayerSessions`, `getInvitations`, the `IsSet` queries) throw: a failed request never produces a response object +- [sce::Np::CppWebApi::Matchmaking::V1](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - imported by PPSA21564 (86 of the 92 exports) and the other six by PPSA05510, PPSA25256, PPSA17226 and PPSA08395. Offline, the four `Api` calls and four factories fail with `SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN`. `ParameterTo{CancelTicket,GetOffer,GetTicket,SubmitTicket}` keep only whether they are initialized: `initialize` returns 0x80553503 for a null `LibContext` or string, 0x80553502 for an unset `LibContext`, 0x8055350f for a second `initialize` and 0x80553512 for an empty `SubmitTicketRequestBody`; `terminate` returns 0x80553510. `ParameterToGetTicket::setview` returns 0x80553502 uninitialized and 0x80553503 for a null string. The strings and setters are not kept. Response accessors throw: a failed request produces no response. Source: the module bundled with PPSA21564/PPSA23732 (builds J03093737/J03341606), `initialize` 0x1769a0, `SubmitTicket::initialize` 0x17a270, `setview` 0x177ed0; none measured on a console. 0x80553504 is not returned: the stub stores a static pointer and does not allocate - [sce::Np::CppWebApi::UserProfile::V1, ProfanityFilter::V2, CommunicationRestrictionStatus::V3](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - imported by an Epic Online Services module whose dump ships no `libSceNpCppWebApi.prx`. The reference-count offset 0x00 of `ResponseHeaderBase` and `WebApiFilterRequest` is assumed from the other objects, not measured. Offline, the five API calls, `WebApiFilterRequestFactory::create` and the transactions' `start`/`getResponseHeaders` fail with `SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN`. Response accessors, `ResponseHeaderBase::getHeaderValue` and the `Vector`/`Iterator` members over `IntrusivePtr` and `unsigned long` throw: a failed request never produces a response - [libSceNpCppWebApi](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - unimplemented stubs have their parameter lists, const qualifiers and return types aligned with the demangled symbols confirmed by NID matching in [#8143](https://github.com/boykopovar/AnyPS5/issues/8143). They cover response-body accessors, iterators, and uninstantiated transactions in Leaderboards::V1, SessionManager::V1, InGameCatalog::V5, TitleCloudStorage::V1, and Common templates: each mangled name gives the method and parameter types, but the runtime layout of response objects and errors are unverified on console, so each call continues to throw NotImplemented_nid_no_patch. No call site is recorded for them - [sce::Np::CppWebApi::SessionManager2V1 and sce::Np::CppWebApi::InGameCatalog2V5](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - 198 further exports imported by PPSA20397, named from aerolib. API calls, factories and transaction `start`/`getResponse*` fail `SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN`; parameter bookkeeping succeeds without state; `IntrusivePtr`/`Vector` helpers follow the layouts above, with `Vector::empty`/`size`/`operator[]` reading host-created bodies and throwing on title-provided ones. Response accessors, iterators, `Vector::begin`/`end`/`copyFrom` and transaction `operator=` throw: a failed request produces no response object. As `start` never succeeds here, `TransactionBase::finish` and `abort` return 0x80553508 and `getId` returns 0, which is what the module bundled with PPSA21564 returns while the two words its `Transaction` constructor zeroes are still unset From 2aca989548fe6428d582047032a429bc5ec1c95b Mon Sep 17 00:00:00 2001 From: hugodearaujo Date: Thu, 29 Oct 2026 22:14:23 -0300 Subject: [PATCH 15/49] test(libSceAudioOut): hold a blocking push on a test clock until playback frees a slot A device-less context can run its queue model on a clock the test sets. With the clock frozen, a blocking push that fills the queue is seen still waiting with its grain queued and not counted, and returns once the test plays one grain; a blocking push that meets a queue filled by non-blocking pushes does not queue past the depth. --- .../libSceAudioOut/src/AudioOut2Context.cpp | 14 ++-- .../libSceAudioOut/src/AudioOut2Internal.hpp | 2 + .../libSceAudioOut/tests/AudioOut2Timing.cpp | 81 +++++++++++++++++++ 3 files changed, 92 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp index 2156ab1998..fa53ce361f 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp @@ -56,6 +56,10 @@ double AudioOut2TraceSeconds() { return std::chrono::duration(Clock::now() - start).count(); } +static Clock::time_point Now(const AudioOut2Context& context) { + return context.clockForTest ? *context.clockForTest : Clock::now(); +} + static AudioOut2Context* FromHandle(AudioOut2ContextHandle ctx) { return reinterpret_cast(ctx); } @@ -279,13 +283,13 @@ static void TraceSummary(AudioOut2Context& context, Clock::time_point now) { } static Clock::time_point WaitForRoom(AudioOut2Context& context, std::unique_lock& lock) { - auto now = Clock::now(); + auto now = Now(context); const auto waitStart = now; while (QueueLevel(context, now) >= context.queueDepth && now - waitStart <= FULL_WAIT_TIMEOUT) { lock.unlock(); PreciseSleepUs(std::chrono::microseconds(FULL_WAIT_STEP).count()); lock.lock(); - now = Clock::now(); + now = Now(context); } return now; } @@ -364,7 +368,7 @@ int APS5_VABI sceAudioOut2ContextGetQueueLevel(AudioOut2ContextHandle ctx, uint3 return 0; } std::lock_guard lock(context->lock); - const auto level = std::min(context->queueDepth, QueueLevel(*context, Clock::now()) + static_cast(context->advancedGrains.size())); + const auto level = std::min(context->queueDepth, QueueLevel(*context, Now(*context)) + static_cast(context->advancedGrains.size())); if (queue_level) *queue_level = level; if (available_queue) *available_queue = context->queueDepth - level; context->queueLevelPolls++; @@ -379,7 +383,7 @@ int APS5_VABI sceAudioOut2ContextPush(AudioOut2ContextHandle ctx, uint32_t block if (!context) return SCE_AUDIO_OUT2_ERROR_INVALID_HANDLE; std::unique_lock lock(context->lock); const QueueLevelPublisher publication{*context, lock}; - auto now = Clock::now(); + auto now = Now(*context); if (!blocking && QueueLevel(*context, now) >= context->queueDepth) { context->fullRejects++; if (context->fullRejects <= CALL_TRACE_FULL) AUDIOOUT2_TRACE("t=%.3f Push ctx %p non-blocking on a full queue (%u): rejected\n", AudioOut2TraceSeconds(), static_cast(context), context->queueDepth); @@ -389,7 +393,7 @@ int APS5_VABI sceAudioOut2ContextPush(AudioOut2ContextHandle ctx, uint32_t block if (context->advancedMode && context->advancedGrains.empty()) { if (!blocking) throw std::runtime_error("AudioOut2: a non-blocking push without a rendered grain is not implemented"); if (!context->grainReady.wait(lock, LibcShutdownToken_nid_postfix(), [&] { return !context->advancedGrains.empty(); })) throw ProcessShutdown{}; - now = Clock::now(); + now = Now(*context); Drain(*context, now); } context->queued++; diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp b/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp index 2df977b0ee..5f07962692 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -128,6 +129,7 @@ struct AudioOut2Context { std::chrono::steady_clock::time_point summaryStart; bool deviceBacked = false; std::atomic publishedQueueLevel{0}; + std::optional clockForTest; }; // Mixes every port of the context that carries PCM data into out (stereo float, frames frames), summing diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOut2Timing.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOut2Timing.cpp index c577b715f1..f1c6a93307 100644 --- a/core/libs/prx/libSceAudioOut/tests/AudioOut2Timing.cpp +++ b/core/libs/prx/libSceAudioOut/tests/AudioOut2Timing.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -221,6 +222,84 @@ void TestBlockingPushWaitsForRoomWithoutADevice() { Require(sceAudioOut2ContextDestroy(handle) == 0); } +AudioOut2ContextHandle CreateContextOnATestClock() { + SetEnvironment("SDL_DISKAUDIOFILE", std::filesystem::temp_directory_path().string()); + AudioOut2ContextParam params{}; + Require(sceAudioOut2ContextResetParam(¶ms) == 0); + params.num_grains = grain; + params.queue_depth = queueDepth; + AudioOut2ContextHandle handle = 0; + Require(sceAudioOut2ContextCreate(¶ms, nullptr, 0, &handle) == 0); + auto& context = *reinterpret_cast(handle); + std::lock_guard lock(context.lock); + Require(context.device == 0); + const auto start = std::chrono::steady_clock::now(); + context.clockForTest = start; + context.playHead = start; + return handle; +} + +void PlayOneGrainOnTheTestClock(AudioOut2ContextHandle handle) { + auto& context = *reinterpret_cast(handle); + std::lock_guard lock(context.lock); + *context.clockForTest += std::chrono::duration_cast(std::chrono::duration(static_cast(grain) / frequency)) + std::chrono::microseconds(1); +} + +template +bool ContextReaches(AudioOut2ContextHandle handle, std::chrono::milliseconds limit, Predicate predicate) { + auto& context = *reinterpret_cast(handle); + const auto deadline = std::chrono::steady_clock::now() + limit; + for (;;) { + { + std::lock_guard lock(context.lock); + if (predicate(context)) return true; + } + if (std::chrono::steady_clock::now() >= deadline) return false; + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } +} + +std::uint32_t QueueLevelNow(AudioOut2ContextHandle handle) { + std::uint32_t level = 99; + std::uint32_t available = 99; + Require(sceAudioOut2ContextGetQueueLevel(handle, &level, &available) == 0); + Require(level + available == queueDepth); + return level; +} + +void TestBlockingPushStaysBlockedUntilPlaybackFreesASlot() { + const auto handle = CreateContextOnATestClock(); + for (std::uint32_t push = 0; push + 1 < queueDepth; push++) Require(sceAudioOut2ContextPush(handle, 1) == 0); + Require(QueueLevelNow(handle) == queueDepth - 1); + auto push = std::async(std::launch::async, [handle] { return sceAudioOut2ContextPush(handle, 1); }); + std::uint64_t pushesOnceQueued = 0; + Require(ContextReaches(handle, std::chrono::seconds(10), [&](const AudioOut2Context& context) { + pushesOnceQueued = context.pushes; + return context.queued == queueDepth; + })); + Require(pushesOnceQueued == queueDepth - 1); + Require(push.wait_for(std::chrono::seconds(0)) == std::future_status::timeout); + Require(QueueLevelNow(handle) == queueDepth); + PlayOneGrainOnTheTestClock(handle); + Require(push.get() == 0); + Require(QueueLevelNow(handle) == queueDepth - 1); + Require(sceAudioOut2ContextDestroy(handle) == 0); +} + +void TestBlockingPushOnAFullQueueQueuesOnlyOnceASlotIsFree() { + const auto handle = CreateContextOnATestClock(); + for (std::uint32_t push = 0; push < queueDepth; push++) Require(sceAudioOut2ContextPush(handle, 0) == 0); + Require(sceAudioOut2ContextPush(handle, 0) == queueFull); + auto push = std::async(std::launch::async, [handle] { return sceAudioOut2ContextPush(handle, 1); }); + Require(!ContextReaches(handle, std::chrono::seconds(1), [](const AudioOut2Context& context) { return context.queued > queueDepth; })); + Require(push.wait_for(std::chrono::seconds(0)) == std::future_status::timeout); + PlayOneGrainOnTheTestClock(handle); + PlayOneGrainOnTheTestClock(handle); + Require(push.get() == 0); + Require(QueueLevelNow(handle) == queueDepth - 1); + Require(sceAudioOut2ContextDestroy(handle) == 0); +} + void TestPrimingAfterRunningDry() { Session session("anyps5_audio_out2_priming.raw"); session.Fill(0.5f); @@ -380,6 +459,8 @@ int main() { TestDeviceLevelDoesNotWaitForTheContext(true); TestLevelWithoutADeviceUsesThePlaybackClock(); TestBlockingPushWaitsForRoomWithoutADevice(); + TestBlockingPushStaysBlockedUntilPlaybackFreesASlot(); + TestBlockingPushOnAFullQueueQueuesOnlyOnceASlotIsFree(); TestPrimingAfterRunningDry(); TestLowLatencyCallsRejectBadArguments(); TestLowLatencyReadsAtNextPush(); From 107e3c5f768f4705ee74032f10061d68b2c17142 Mon Sep 17 00:00:00 2001 From: hugodearaujo Date: Thu, 29 Oct 2026 22:15:42 -0300 Subject: [PATCH 16/49] test(libSceAudioOut): run the test-clock push tests before the live-clock one --- core/libs/prx/libSceAudioOut/tests/AudioOut2Timing.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOut2Timing.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOut2Timing.cpp index f1c6a93307..5f8f11cea1 100644 --- a/core/libs/prx/libSceAudioOut/tests/AudioOut2Timing.cpp +++ b/core/libs/prx/libSceAudioOut/tests/AudioOut2Timing.cpp @@ -458,9 +458,9 @@ int main() { TestDeviceLevelDoesNotWaitForTheContext(false); TestDeviceLevelDoesNotWaitForTheContext(true); TestLevelWithoutADeviceUsesThePlaybackClock(); - TestBlockingPushWaitsForRoomWithoutADevice(); TestBlockingPushStaysBlockedUntilPlaybackFreesASlot(); TestBlockingPushOnAFullQueueQueuesOnlyOnceASlotIsFree(); + TestBlockingPushWaitsForRoomWithoutADevice(); TestPrimingAfterRunningDry(); TestLowLatencyCallsRejectBadArguments(); TestLowLatencyReadsAtNextPush(); From 3fc5485e6c6cc0b25e27d9db9e1b7c3504f56695 Mon Sep 17 00:00:00 2001 From: hugodearaujo Date: Thu, 29 Oct 2026 22:17:42 -0300 Subject: [PATCH 17/49] test(libSceAudioOut): keep the timing test includes sorted --- core/libs/prx/libSceAudioOut/tests/AudioOut2Timing.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOut2Timing.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOut2Timing.cpp index 5f8f11cea1..a44097499f 100644 --- a/core/libs/prx/libSceAudioOut/tests/AudioOut2Timing.cpp +++ b/core/libs/prx/libSceAudioOut/tests/AudioOut2Timing.cpp @@ -11,8 +11,8 @@ #include #include #include -#include #include +#include #include #include #include From e51063a12795db252903b7234188d0e9511e84cf Mon Sep 17 00:00:00 2001 From: Queuze <58829992+lucaslima430@users.noreply.github.com> Date: Thu, 29 Oct 2026 21:39:56 -0300 Subject: [PATCH 18/49] test(libc): guard memccpy stop boundaries --- core/libs/prx/libc/CMakeLists.txt | 10 +++ core/libs/tests/GuestMemccpyBounds.cpp | 85 ++++++++++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 core/libs/tests/GuestMemccpyBounds.cpp diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index 3dd688d709..f1bccca33e 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -243,6 +243,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test_executable(guest_memccpy_bounds_tests ${LIBS_INCLUDE_DIR}/tests/GuestMemccpyBounds.cpp) + target_include_directories(guest_memccpy_bounds_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_memccpy_bounds_tests PRIVATE libc) + configure_windows_unwind(guest_memccpy_bounds_tests) + add_test(NAME guest_memccpy_bounds COMMAND guest_memccpy_bounds_tests) + set_tests_properties(guest_memccpy_bounds PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_memccpy_bounds PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_allocating_strings_tests ${LIBS_INCLUDE_DIR}/tests/GuestAllocatingStrings.cpp) target_include_directories(guest_allocating_strings_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_allocating_strings_tests PRIVATE libc) diff --git a/core/libs/tests/GuestMemccpyBounds.cpp b/core/libs/tests/GuestMemccpyBounds.cpp new file mode 100644 index 0000000000..cfb89a8be1 --- /dev/null +++ b/core/libs/tests/GuestMemccpyBounds.cpp @@ -0,0 +1,85 @@ +#include "prx/libc/include/Api.hpp" +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#else +#include +#include +#endif + +static void Check(bool condition, int line) { + if (!condition) { + std::fprintf(stderr, "memccpy check failed at line %d\n", line); + std::abort(); + } +} +#define Require(condition) Check((condition), __LINE__) + +class GuardedPage { +public: + std::size_t size; + unsigned char* data; + + GuardedPage() { +#ifdef _WIN32 + SYSTEM_INFO info{}; + GetSystemInfo(&info); + size = info.dwPageSize; + data = static_cast(VirtualAlloc(nullptr, size * 2, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + Require(data != nullptr); + DWORD previous = 0; + Require(VirtualProtect(data + size, size, PAGE_NOACCESS, &previous) != 0); +#else + const long pageSize = sysconf(_SC_PAGESIZE); + Require(pageSize > 0); + size = static_cast(pageSize); + data = static_cast(mmap(nullptr, size * 2, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)); + Require(data != MAP_FAILED); + Require(mprotect(data + size, size, PROT_NONE) == 0); +#endif + } + + ~GuardedPage() { +#ifdef _WIN32 + VirtualFree(data, 0, MEM_RELEASE); +#else + munmap(data, size * 2); +#endif + } +}; + +int main() { + GuardedPage source; + GuardedPage destination; + for (std::size_t count = 0; count <= 256; ++count) { + for (const std::size_t stop : {count, count / 2, std::size_t{0}}) { + std::memset(source.data, 0x35, source.size); + std::memset(destination.data, 0xA7, destination.size); + auto* input = source.data + source.size - std::max(count, std::size_t{1}); + auto* output = destination.data + destination.size - std::max(count, std::size_t{1}); + if (stop < count) input[stop] = 0xFF; + const std::vector before(source.data, source.data + source.size); + std::vector expected(destination.data, destination.data + destination.size); + const std::size_t copied = stop < count ? stop + 1 : count; + std::copy_n(input, copied, expected.data() + (output - destination.data)); + void* result = memccpy_nid_postfix(output, input, 0x1FF, count); + Require(result == (stop < count ? output + copied : nullptr)); + Require(std::memcmp(destination.data, expected.data(), destination.size) == 0); + Require(std::memcmp(source.data, before.data(), source.size) == 0); + } + } + for (const int character : {-1, 0xFF, 0x1FF}) { + source.data[source.size - 1] = 0xFF; + destination.data[destination.size - 1] = 0; + Require(memccpy_nid_postfix(destination.data + destination.size - 1, source.data + source.size - 1, + character, 256) == destination.data + destination.size); + Require(destination.data[destination.size - 1] == 0xFF); + } + std::puts("memccpy return values, stop bytes, guards and unsigned conversion passed"); +} From 7e659e7eab0ca26d272e600b81af1371592d9e89 Mon Sep 17 00:00:00 2001 From: Himansh Raj Date: Fri, 30 Oct 2026 08:35:04 +0530 Subject: [PATCH 19/49] test(agc): catch the descriptor pool reset terminate on macOS agc_descriptor_pool_reset_failure makes vkResetDescriptorPool return VK_ERROR_DEVICE_LOST and expects DescriptorCache::Free to end in std::terminate, caught by a handler set through the guest _ZSt13set_terminatePFvvE_nid_postfix. On Linux libc routes the driver's std::terminate to that guest handler; on macOS it reaches the host libc++abi handler instead, so the test aborted. On macOS the test now also installs the same exit handler with std::set_terminate around the Free call and restores it afterwards. The driver is unchanged. --- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 7fcf7a0e90..86cc762a06 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -6245,7 +6245,15 @@ void descriptorPoolResetFailureTest() { const auto previous = _ZSt13set_terminatePFvvE_nid_postfix([] { std::_Exit(mock.descriptorResetResult == VK_SUCCESS ? 0 : 1); }); +#ifdef __APPLE__ + const auto previousHost = std::set_terminate([] { + std::_Exit(mock.descriptorResetResult == VK_SUCCESS ? 0 : 1); + }); +#endif cache.Free(allocation); +#ifdef __APPLE__ + std::set_terminate(previousHost); +#endif _ZSt13set_terminatePFvvE_nid_postfix(previous); } From ff736bd24ae73dfa54f1f27fca96419cba3b0195 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Thu, 29 Oct 2026 23:34:52 -0400 Subject: [PATCH 20/49] test(libc): retain real stream scan read errors --- core/libs/tests/FileStreams.cpp | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/core/libs/tests/FileStreams.cpp b/core/libs/tests/FileStreams.cpp index 5f2b8e40bd..dd43e09f2e 100644 --- a/core/libs/tests/FileStreams.cpp +++ b/core/libs/tests/FileStreams.cpp @@ -120,7 +120,7 @@ static int APS5_VABI ScanList(FileStream* stream, const char* format, ...) { return result; } -static void CheckScanInputFailureErrno() { +static void CheckScanInputFailureErrno(const std::string& path) { const int savedError = errno; for (const bool variadicList : {false, true}) { for (const int seed : {0, 22, 97}) { @@ -140,6 +140,20 @@ static void CheckScanInputFailureErrno() { } } } +#if defined(__GLIBC__) + auto* errorNative = std::tmpfile(); + Require(errorNative != nullptr); + const int writeOnly = ::open(path.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0600); + Require(writeOnly >= 0); + Require(::dup2(writeOnly, ::fileno(errorNative)) >= 0 && ::close(writeOnly) == 0); + FileStream readError(errorNative); + int unchanged = 123; + errno = 97; + const int failed = variadicList ? ScanList(&readError, "%d", &unchanged) : fscanf_nid_postfix(&readError, "%d", &unchanged); + Require(failed == EOF && errno != 97, "Stream scan lost its read error"); + Require(unchanged == 123 && ferror_nid_postfix(&readError) != 0, "Stream scan did not keep its read error indicator"); + Require(std::fclose(errorNative) == 0 && std::remove(path.c_str()) == 0); +#endif auto* native = std::tmpfile(); Require(native != nullptr && std::fputs("1e99999", native) >= 0); std::rewind(native); @@ -175,7 +189,7 @@ static void CheckReadAfterWrite(const std::string& path, const char* mode, const } static void CheckUpdateStreamReadsAfterWrites(const std::string& base) { - CheckScanInputFailureErrno(); + CheckScanInputFailureErrno(base + ".scan-error"); const std::string path = base + ".update"; constexpr char overwritten[] = "AB23456789"; CheckReadAfterWrite(path, "r+b", overwritten, [](FileStream* stream) { From 9bc853e0b92f719fa614df91c9d17ffff6e6e6d1 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Thu, 29 Oct 2026 23:38:58 -0400 Subject: [PATCH 21/49] test(tools): cover relevant helpers at the branch limit --- tools/tests/test_import_audit.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tools/tests/test_import_audit.py b/tools/tests/test_import_audit.py index 75f89c4dea..fe45755433 100644 --- a/tools/tests/test_import_audit.py +++ b/tools/tests/test_import_audit.py @@ -1696,6 +1696,15 @@ class ImportAuditTests(unittest.TestCase): self.assertEqual(set(stubs), {stub, guarded}) self.assertEqual(partial, {guarded}) + def test_relevant_helper_at_the_variant_limit_remains_a_stub(self): + blocks = "".join(f"#ifdef HOST_{index}\n value += {index};\n#endif\n" for index in range(8)) + source = self.work.source + write_source(source, "libHosts", + f"void Fail(int value) {{\n{blocks} NotImplemented_nid_no_patch(__func__);\n}}\n" + "int APS5_VABI sceStub(int value) { Fail(value); return value; }\n") + stub = ("libHosts.prx", nid_names.compute_nid("sceStub")) + self.assertEqual(import_audit.source_stub_classes(source), ({stub: "sceStub"}, set())) + def test_relevant_helper_host_branches_keep_the_variant_limit(self): blocks = "".join(f"#ifdef HOST_{index}\n value += {index};\n#endif\n" for index in range(9)) source = self.work.source From 62d9f110ab8f3fb1da6969822c496deb94840a5b Mon Sep 17 00:00:00 2001 From: Eduardo Ruiz Date: Fri, 30 Oct 2026 07:25:57 +0100 Subject: [PATCH 22/49] docs(libSceNpCppWebApi): shorten the Matchmaking debt entry --- docs/dev/debt/libSceNpCppWebApi.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceNpCppWebApi.md b/docs/dev/debt/libSceNpCppWebApi.md index 1e07ad891b..f2b4091239 100644 --- a/docs/dev/debt/libSceNpCppWebApi.md +++ b/docs/dev/debt/libSceNpCppWebApi.md @@ -15,7 +15,7 @@ - [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 `copyFrom` throw: the only `Vector`s the title reads back come from failed transactions, which never produce one. `empty`, `size` and `operator[]` read the host-created body honestly and throw on title-provided bodies - [sce::Np::CppWebApi::SessionManager2V1](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - imported by PPSA03671. Offline, the factories, `PlayerSessionsApi` calls and transaction `start`/`getResponse`/`readData` fail with `SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN`; parameter setters, `initialize`, `terminate`, constructors and destructors succeed without keeping state, as no request is ever sent. Response-body accessors (`getSessionId`, `getPlayerSessions`, `getInvitations`, the `IsSet` queries) throw: a failed request never produces a response object -- [sce::Np::CppWebApi::Matchmaking::V1](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - imported by PPSA21564 (86 of the 92 exports) and the other six by PPSA05510, PPSA25256, PPSA17226 and PPSA08395. Offline, the four `Api` calls and four factories fail with `SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN`. `ParameterTo{CancelTicket,GetOffer,GetTicket,SubmitTicket}` keep only whether they are initialized: `initialize` returns 0x80553503 for a null `LibContext` or string, 0x80553502 for an unset `LibContext`, 0x8055350f for a second `initialize` and 0x80553512 for an empty `SubmitTicketRequestBody`; `terminate` returns 0x80553510. `ParameterToGetTicket::setview` returns 0x80553502 uninitialized and 0x80553503 for a null string. The strings and setters are not kept. Response accessors throw: a failed request produces no response. Source: the module bundled with PPSA21564/PPSA23732 (builds J03093737/J03341606), `initialize` 0x1769a0, `SubmitTicket::initialize` 0x17a270, `setview` 0x177ed0; none measured on a console. 0x80553504 is not returned: the stub stores a static pointer and does not allocate +- [sce::Np::CppWebApi::Matchmaking::V1](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - imported by PPSA21564 (86 of 92) and PPSA05510, PPSA25256, PPSA17226 and PPSA08395 for the other six; behaviour comes from the module bundled with PPSA21564/PPSA23732 (builds J03093737/J03341606), not from a console. Offline, the four `Api` calls and four factories return `SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN`. The four `ParameterTo*` objects keep only an initialized flag: `initialize` returns 0x80553503 for a null context or string, 0x80553502 for an unset context, 0x8055350f when already initialized and 0x80553512 for an empty `SubmitTicketRequestBody`; `terminate` returns 0x80553510. `ParameterToGetTicket::setview` returns 0x80553502 uninitialized and 0x80553503 for a null string. Other setters succeed without keeping state. Response accessors throw: a failed request produces no response. 0x80553504 is never returned; the stub stores a static pointer. - [sce::Np::CppWebApi::UserProfile::V1, ProfanityFilter::V2, CommunicationRestrictionStatus::V3](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - imported by an Epic Online Services module whose dump ships no `libSceNpCppWebApi.prx`. The reference-count offset 0x00 of `ResponseHeaderBase` and `WebApiFilterRequest` is assumed from the other objects, not measured. Offline, the five API calls, `WebApiFilterRequestFactory::create` and the transactions' `start`/`getResponseHeaders` fail with `SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN`. Response accessors, `ResponseHeaderBase::getHeaderValue` and the `Vector`/`Iterator` members over `IntrusivePtr` and `unsigned long` throw: a failed request never produces a response - [libSceNpCppWebApi](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - unimplemented stubs have their parameter lists, const qualifiers and return types aligned with the demangled symbols confirmed by NID matching in [#8143](https://github.com/boykopovar/AnyPS5/issues/8143). They cover response-body accessors, iterators, and uninstantiated transactions in Leaderboards::V1, SessionManager::V1, InGameCatalog::V5, TitleCloudStorage::V1, and Common templates: each mangled name gives the method and parameter types, but the runtime layout of response objects and errors are unverified on console, so each call continues to throw NotImplemented_nid_no_patch. No call site is recorded for them - [sce::Np::CppWebApi::SessionManager2V1 and sce::Np::CppWebApi::InGameCatalog2V5](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - 198 further exports imported by PPSA20397, named from aerolib. API calls, factories and transaction `start`/`getResponse*` fail `SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN`; parameter bookkeeping succeeds without state; `IntrusivePtr`/`Vector` helpers follow the layouts above, with `Vector::empty`/`size`/`operator[]` reading host-created bodies and throwing on title-provided ones. Response accessors, iterators, `Vector::begin`/`end`/`copyFrom` and transaction `operator=` throw: a failed request produces no response object. As `start` never succeeds here, `TransactionBase::finish` and `abort` return 0x80553508 and `getId` returns 0, which is what the module bundled with PPSA21564 returns while the two words its `Transaction` constructor zeroes are still unset From 4189ea4f026c140fc18322a6e79dd871ca017532 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Thu, 15 Oct 2026 23:42:12 +0000 Subject: [PATCH 23/49] fix(agc): exclude native MSAA spans from BDA tables --- .../Graphics/include/MultisampleTarget.hpp | 2 + .../Graphics/src/BdaResources.cpp | 10 +- .../Graphics/src/MultisampleTarget.cpp | 51 ++++++++ .../execution/SampleMultisampleTarget.cpp | 110 ++++++++++++++++++ 4 files changed, 170 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp index 43e8248442..efa12835ed 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp @@ -4,6 +4,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Context.hpp" #include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" #include "prx/libSceAgcDriver/Graphics/include/SampleOrder.hpp" +#include "BdaAbi.hpp" #include "prx/libSceAgcDriver/Graphics/include/State.hpp" #include #include @@ -81,6 +82,7 @@ bool MultisampledColorFillAbsorbs(std::uint64_t address, std::size_t bytes, std: void NoteColorMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32_t pattern); std::shared_ptr MultisampleTargetFmaskTexture_nid_no_patch(const Context& context, const GuestTextureResource& resource, VkComponentMapping components); void ClearMultisampleTargets(VkDevice device); +std::optional> ExcludeMultisampleTargetRanges(VkDevice device, std::span ranges); void InstallResidentImageReadGuard_nid_no_patch(); void UpdateResidentImageUnmapObserver_nid_no_patch(); void GuardOneFragmentFmask(const Context& context, const ColorTarget& target); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp index a8440fb7f0..346872780a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp @@ -1,5 +1,6 @@ #include "prx/libSceAgcDriver/Graphics/include/BdaResources.hpp" #include "prx/libSceAgcDriver/Execution/include/Mutex.hpp" +#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" #include "ImageTableAbi.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include @@ -135,8 +136,10 @@ BdaResources::BdaResources(const Context& context, const GuestBufferMemory& memo const auto cached = memory.CachedAddressTable(); std::vector built; if (!cached.has_value()) built = memory.AddressRanges(); - const auto& ranges = cached.has_value() ? *cached->ranges : built; - const auto serial = cached.has_value() ? cached->serial : 0; + const auto& addressRanges = cached.has_value() ? *cached->ranges : built; + auto filtered = ExcludeMultisampleTargetRanges(context.device, addressRanges); + const auto& ranges = filtered.has_value() ? *filtered : addressRanges; + const auto serial = cached.has_value() && !filtered.has_value() ? cached->serial : 0; Require(ranges.size() <= std::numeric_limits::max(), "BDA table range count overflow"); Require(ranges.size() <= (std::numeric_limits::max() - sizeof(ShaderRecompiler::BdaAbi::Header)) / sizeof(ShaderRecompiler::BdaAbi::Range), "BDA table size overflow"); tableBytes = sizeof(ShaderRecompiler::BdaAbi::Header) + ranges.size() * sizeof(ShaderRecompiler::BdaAbi::Range); @@ -209,7 +212,8 @@ BdaResources::BdaResources(const Context& context, const GuestBufferMemory& memo cache.entries.clear(); cache.device = context.device; } - cache.entries.push_front({hash, cached.has_value() ? ranges : std::move(built), table, serial}); + auto cachedRanges = filtered.has_value() ? std::move(*filtered) : cached.has_value() ? *cached->ranges : std::move(built); + cache.entries.push_front({hash, std::move(cachedRanges), table, serial}); while (cache.entries.size() > tableCacheEntries()) cache.entries.pop_back(); } } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp index 051b7149df..15f47cc376 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -1463,6 +1463,57 @@ void GuardOneFragmentFmask(const Context& context, const ColorTarget& target) { InstallResidentImageReadGuard_nid_no_patch(); } +std::optional> ExcludeMultisampleTargetRanges(VkDevice device, std::span ranges) { + if (ranges.empty() || !anyGuarded().load(std::memory_order_acquire)) return std::nullopt; + std::vector> excluded; + { + std::shared_lock lock(guardedMutex()); + for (const auto& range : guarded()) { + if (range.device != device || !range.active->load(std::memory_order_acquire)) continue; + excluded.emplace_back(range.address, range.address + range.bytes); + } + } + if (excluded.empty()) return std::nullopt; + std::sort(excluded.begin(), excluded.end()); + std::vector> merged; + merged.reserve(excluded.size()); + for (const auto& range : excluded) { + if (!merged.empty() && range.first <= merged.back().second) merged.back().second = std::max(merged.back().second, range.second); + else merged.push_back(range); + } + std::vector filtered; + filtered.reserve(ranges.size() + merged.size()); + bool changed = false; + for (const auto& range : ranges) { + auto cursor = range.begin; + for (const auto& [begin, end] : merged) { + if (end <= cursor) continue; + if (begin >= range.end) break; + const auto cutBegin = std::max(cursor, begin); + const auto cutEnd = std::min(range.end, end); + if (cutBegin > cursor) { + auto part = range; + part.begin = cursor; + part.end = cutBegin; + part.deviceAddress += cursor - range.begin; + filtered.push_back(part); + } + if (cutBegin < cutEnd) { + changed = true; + cursor = cutEnd; + } + if (cursor == range.end) break; + } + if (cursor < range.end) { + auto part = range; + part.begin = cursor; + part.deviceAddress += cursor - range.begin; + filtered.push_back(part); + } + } + return changed ? std::optional(std::move(filtered)) : std::nullopt; +} + void RequireNoMultisampleTarget(std::uint64_t address, std::size_t bytes, const char* access) { RequireNoResidentDepthRead_nid_no_patch(address, bytes); RequireNoMultisampleColorTarget(address, bytes, access); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp index e4cc459aaf..b97e3414ee 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp @@ -41,6 +41,11 @@ #include #include #include +#ifdef _WIN32 +#include +#else +#include +#endif namespace AgcDriver { @@ -82,6 +87,9 @@ constexpr std::uint32_t TileD64KBX = 0x1a; constexpr std::uint32_t Threads = 32; constexpr std::uint32_t Words = 32; constexpr std::uint32_t LoadResult = 12; +constexpr std::uint32_t BdaThreads = 64; +constexpr std::uint32_t BdaLoadBase = 0x100; +constexpr std::uint32_t BdaOutputBase = 0x8000; constexpr std::array Drawn{0.25f, 0.5f, 0.125f, 1.0f}; constexpr double EdgeMargin = 1.0 / 32.0; constexpr std::uint32_t FillOnes = 0x3c003c00u; @@ -143,6 +151,11 @@ struct SampleWrite { std::uint32_t sample; }; +alignas(256) constexpr std::array BdaSpanCode{ + 0x34040084, 0x4a0404ff, BdaLoadBase, 0x34080081, 0x36080882, 0x4a040504, 0x34060083, 0x4a0606ff, + BdaOutputBase, 0xdc388ff8, 0x04000002, 0xbf8c3f70, 0xdc708000, 0x00000503, 0xdc708004, 0x00000703, 0xbf810000, +}; + constexpr std::array, 3> Triangle{{ {-1.0f, -1.0f, 0.5f, 1.0f}, {0.83f, -1.0f, 0.5f, 1.0f}, {-1.0f, 0.71f, 0.5f, 1.0f} }}; @@ -151,6 +164,43 @@ alignas(256) std::array Buffer{}; alignas(256) constexpr std::array BufferLoadCode{0x34020082u, 0xe8001000u | (20u << 19u), 0x80000201u, 0xbf8c0000u, 0xe0701000u, 0x80010201u, 0xbf810000u}; +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; +}; + struct Block { explicit Block(std::size_t bytes, bool watched = false) : bytes(bytes), watched(watched) { #ifdef _WIN32 @@ -680,6 +730,55 @@ void StoreWords(const Block& block, std::uint64_t bytes, std::uint32_t word) { for (std::uint64_t offset = 0; offset < bytes; offset += sizeof(word)) std::memcpy(block.data + offset, &word, sizeof(word)); } +ShaderRecompiler::RecompileResult CompileBdaSpan(AgcDriver::VulkanDevice& device, std::uint64_t address) { + const std::span code(BdaSpanCode); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const std::array userData{static_cast(address), static_cast(address >> 32u)}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{BdaThreads, 1, 1}, 0, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + Require(std::ranges::any_of(result.bindings, [](const auto& binding) { return binding.role == ShaderRecompiler::DescriptorRole::BdaPagetable; }), "the native multisample BDA test has no address-table binding"); + return result; +} + +void CheckBdaSpan(AgcDriver::VulkanDevice& device, const Block& block, std::size_t offset, bool refused) { + std::vector before(block.bytes); + for (std::size_t index = 0; index < before.size(); ++index) before[index] = static_cast(index * 37u + 11u); + const auto output = offset + 0x8000u; + std::fill(before.begin() + static_cast(output), before.begin() + static_cast(output + BdaThreads * 8u), 0xcdu); + std::memcpy(block.data, before.data(), before.size()); + auto expected = before; + for (std::uint32_t tid = 0; tid < BdaThreads; ++tid) { + const auto load = offset + BdaLoadBase + tid * 16u + (tid & 1u) * 2u - 8u; + std::memcpy(expected.data() + output + tid * 8u, before.data() + load + 4u, sizeof(std::uint32_t)); + std::memcpy(expected.data() + output + tid * 8u + 4u, before.data() + load + 12u, sizeof(std::uint32_t)); + } + std::string failure; + StderrCapture log; + try { + const auto address = block.Address() + offset; + device.Dispatch(CompileBdaSpan(device, address), 1, 1, 1, {}, reinterpret_cast(BdaSpanCode.data())); + device.WaitIdle(); + } catch (const std::exception& error) { + failure = error.what(); + } + const auto deferred = log.Finish(); + if (failure.empty()) failure = deferred; + if (refused) { + Require(failure.find("BDA access failed:") != std::string::npos, "BDA access to a resident native multisample range was not refused: " + failure); + Require(std::equal(before.begin(), before.end(), block.data), "a refused BDA access changed guest memory"); + } else { + Require(failure.empty(), "BDA access to an ordinary neighboring range failed: " + failure); + Require(std::equal(expected.begin(), expected.end(), block.data), "the ordinary BDA span did not preserve its reads, writes or boundaries"); + } +} + void RequireRefused(AgcDriver::VulkanDevice& device, const std::array& texture, const std::string& expected, const std::string& what) { std::string refusal; try { @@ -1115,6 +1214,14 @@ int main(int argc, char** argv) { return VulkanTestSkipped; } const auto colorBytes = AgcDriver::Graphics::MultisampledColorBytes(color); + Require(colorBytes == BlockBytes, "the native color target does not occupy exactly one BDA block"); + const auto state = TargetState(color); + CheckBdaSpan(*device, block, 0, false); + CheckBdaSpan(*device, block, BlockBytes, false); + Draw(*device, state); + CheckBdaSpan(*device, block, 0, true); + CheckBdaSpan(*device, block, BlockBytes, false); + CheckSamples(state, LoadSamples(*device, block), {}); Require(colorBytes != 0 && colorBytes <= block.bytes, "the 4x target's color memory does not fit its block"); AgcDriver::Graphics::NoteColorMetadataFill(block.Address(), static_cast(colorBytes), FillOnes); CheckUniform(LoadSamples(*device, block), Ones, "after a fill of the target's color memory with half-float ones"); @@ -1240,6 +1347,9 @@ int main(int argc, char** argv) { Block standard(BlockBytes); StoreWords(standard, colorBytes, FillOnes); RequireRefused(*device, TargetDescriptor(standard, SampleShift, Format16161616Float, TileS64KBX), "has no modeled multisampled color footprint", "loading an S swizzle multisampled texture no draw rendered to"); + device->WaitIdle(); + AgcDriver::Graphics::ClearMultisampleTargets(device->Device()); + CheckBdaSpan(*device, block, 0, false); std::puts("every sample of a 4x color target loaded through its multisampled texture descriptor matches the triangle drawn into it, then the fill of its color memory; a surface no draw rendered to loads the value its color memory repeats and keeps it under a later draw"); struct ReleaseIndexDevice { std::unique_ptr& device; From b82506224f536415d5a76eed02c74f9a80e10def Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Fri, 16 Oct 2026 00:22:44 +0000 Subject: [PATCH 24/49] fix(agc): retain BDA table fast path with MSAA guards --- .../Graphics/include/BdaResources.hpp | 4 +- .../Graphics/include/MultisampleTarget.hpp | 4 +- .../Graphics/src/BdaResources.cpp | 47 ++++++++++++------- .../Graphics/src/MultisampleTarget.cpp | 32 +++++++++++-- .../libSceAgcDriver/tests/BdaResources.cpp | 2 + 5 files changed, 63 insertions(+), 26 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/BdaResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/BdaResources.hpp index 3f75b2a804..e8f90365c8 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/BdaResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/BdaResources.hpp @@ -37,8 +37,8 @@ public: std::uint64_t hits = 0; std::uint64_t misses = 0; std::size_t held = 0; - // Hits served by the cached address space's serial alone (no hash, no compare; see - // GuestBufferMemory::CachedAddressTable), counted in `hits` too. + // Hits served by the cached address space serial and MSAA guard generation (no hash or + // range compare; see GuestBufferMemory::CachedAddressTable), counted in `hits` too. std::uint64_t spaceTables = 0; // Why the most recently used table did not serve a build ([bda-table] first-differing entry // class): its owners were gone (expired weak reference), or its word hash differed, split by diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp index efa12835ed..4f41564c88 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp @@ -82,7 +82,9 @@ bool MultisampledColorFillAbsorbs(std::uint64_t address, std::size_t bytes, std: void NoteColorMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32_t pattern); std::shared_ptr MultisampleTargetFmaskTexture_nid_no_patch(const Context& context, const GuestTextureResource& resource, VkComponentMapping components); void ClearMultisampleTargets(VkDevice device); -std::optional> ExcludeMultisampleTargetRanges(VkDevice device, std::span ranges); +std::uint64_t MultisampleTargetRangeGeneration(); +std::optional> ExcludeMultisampleTargetRanges( + VkDevice device, std::span ranges, std::uint64_t& generation); void InstallResidentImageReadGuard_nid_no_patch(); void UpdateResidentImageUnmapObserver_nid_no_patch(); void GuardOneFragmentFmask(const Context& context, const ColorTarget& target); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp index 346872780a..a26fa825ed 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp @@ -42,6 +42,7 @@ struct TableEntry { std::vector ranges; std::weak_ptr buffer; std::uint64_t spaceSerial = 0; + std::uint64_t guardSerial = 0; }; struct TableCache { @@ -137,9 +138,30 @@ BdaResources::BdaResources(const Context& context, const GuestBufferMemory& memo std::vector built; if (!cached.has_value()) built = memory.AddressRanges(); const auto& addressRanges = cached.has_value() ? *cached->ranges : built; - auto filtered = ExcludeMultisampleTargetRanges(context.device, addressRanges); + const auto serial = cached.has_value() ? cached->serial : 0; + auto guardSerial = MultisampleTargetRangeGeneration(); + if (tableCacheEnabled() && serial != 0) { + auto& cache = Tables(); + std::lock_guard lock(cache.mutex); + if (cache.device != context.device) { + cache.entries.clear(); + cache.device = context.device; + } + for (auto it = cache.entries.begin(); it != cache.entries.end(); ++it) { + if (it->spaceSerial != serial || it->guardSerial != guardSerial) continue; + if (auto shared = it->buffer.lock(); shared != nullptr) { + ++cache.hits; + ++cache.classes.spaceTables; + table = std::move(shared); + cache.entries.splice(cache.entries.begin(), cache.entries, it); + return; + } + cache.entries.erase(it); + break; + } + } + auto filtered = ExcludeMultisampleTargetRanges(context.device, addressRanges, guardSerial); const auto& ranges = filtered.has_value() ? *filtered : addressRanges; - const auto serial = cached.has_value() && !filtered.has_value() ? cached->serial : 0; Require(ranges.size() <= std::numeric_limits::max(), "BDA table range count overflow"); Require(ranges.size() <= (std::numeric_limits::max() - sizeof(ShaderRecompiler::BdaAbi::Header)) / sizeof(ShaderRecompiler::BdaAbi::Range), "BDA table size overflow"); tableBytes = sizeof(ShaderRecompiler::BdaAbi::Header) + ranges.size() * sizeof(ShaderRecompiler::BdaAbi::Range); @@ -153,20 +175,6 @@ BdaResources::BdaResources(const Context& context, const GuestBufferMemory& memo cache.entries.clear(); cache.device = context.device; } - if (serial != 0) { - for (auto it = cache.entries.begin(); it != cache.entries.end(); ++it) { - if (it->spaceSerial != serial) continue; - if (auto shared = it->buffer.lock(); shared != nullptr) { - ++cache.hits; - ++cache.classes.spaceTables; - table = std::move(shared); - cache.entries.splice(cache.entries.begin(), cache.entries, it); - return; - } - cache.entries.erase(it); - break; - } - } hash = hashRanges(ranges); classifyFirstEntry(cache, hash, ranges); for (auto it = cache.entries.begin(); it != cache.entries.end();) { @@ -191,7 +199,10 @@ BdaResources::BdaResources(const Context& context, const GuestBufferMemory& memo ++cache.hits; // An equal table of an earlier space (a rebuild that mapped the same ranges the // same way): the next build of this space binds it by the serial. - if (serial != 0) it->spaceSerial = serial; + if (serial != 0) { + it->spaceSerial = serial; + it->guardSerial = guardSerial; + } table = std::move(shared); cache.entries.splice(cache.entries.begin(), cache.entries, it); return; @@ -213,7 +224,7 @@ BdaResources::BdaResources(const Context& context, const GuestBufferMemory& memo cache.device = context.device; } auto cachedRanges = filtered.has_value() ? std::move(*filtered) : cached.has_value() ? *cached->ranges : std::move(built); - cache.entries.push_front({hash, std::move(cachedRanges), table, serial}); + cache.entries.push_front({hash, std::move(cachedRanges), table, serial, guardSerial}); while (cache.entries.size() > tableCacheEntries()) cache.entries.pop_back(); } } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp index 15f47cc376..7b45c22866 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -790,6 +790,11 @@ std::atomic& anyGuarded() { return any; } +std::atomic& guardedGeneration() { + static std::atomic generation{0}; + return generation; +} + void guardReads(std::uint64_t address, std::size_t bytes) { RequireNoMultisampleTarget(address, bytes, "a CPU read"); } @@ -828,13 +833,14 @@ void retireUnmapped(std::uintptr_t address, std::size_t bytes) { forgetSingleClears(address, bytes); RetireUnmappedDepth_nid_no_patch(address, bytes); std::unique_lock lock(guardedMutex()); - std::erase_if(guarded(), [&](const auto& range) { + const auto invalidated = std::erase_if(guarded(), [&](const auto& range) { if (range.metadataValid && address < range.address + range.bytes && range.address < address + bytes) range.metadataValid->store(false, std::memory_order_release); if (range.address < address || range.address - address > bytes || range.bytes > bytes - (range.address - address)) return false; if (!range.metadataValid) range.active->store(false, std::memory_order_release); return true; }); - std::erase_if(guarded(), [](const auto& range) { return range.metadataValid && !range.active->load(std::memory_order_acquire); }); + const auto staleMetadata = std::erase_if(guarded(), [](const auto& range) { return range.metadataValid && !range.active->load(std::memory_order_acquire); }); + if (invalidated != 0 || staleMetadata != 0) guardedGeneration().fetch_add(1, std::memory_order_release); anyGuarded().store(!guarded().empty(), std::memory_order_release); } @@ -856,6 +862,7 @@ void guard(const Context& context, const ColorTarget& target, const std::shared_ if (target.cmaskAddress != 0) guarded().push_back({context.device, target.cmaskAddress, target.cmaskBytes, target.samples, active, metadataValid, hostOnly}); if (target.samples == 8u && target.fmaskAddress != 0u && target.cmaskAddress != 0u) guarded().push_back({context.device, target.fmaskAddress, DescribeSurface(fmaskResource(target)).guestBytes, target.samples, active, metadataValid, hostOnly}); } + guardedGeneration().fetch_add(1, std::memory_order_release); anyGuarded().store(true, std::memory_order_release); InstallResidentImageReadGuard_nid_no_patch(); } @@ -1447,7 +1454,8 @@ void ClearMultisampleTargets(VkDevice device) { std::lock_guard lock(targetsMutex()); std::erase_if(targets(), [&](const auto& target) { return target->context.device == device; }); std::unique_lock guardLock(guardedMutex()); - std::erase_if(guarded(), [&](const auto& range) { return range.device == device; }); + const auto removed = std::erase_if(guarded(), [&](const auto& range) { return range.device == device; }); + if (removed != 0) guardedGeneration().fetch_add(1, std::memory_order_release); anyGuarded().store(!guarded().empty(), std::memory_order_release); UpdateResidentImageUnmapObserver_nid_no_patch(); } @@ -1463,11 +1471,25 @@ void GuardOneFragmentFmask(const Context& context, const ColorTarget& target) { InstallResidentImageReadGuard_nid_no_patch(); } -std::optional> ExcludeMultisampleTargetRanges(VkDevice device, std::span ranges) { - if (ranges.empty() || !anyGuarded().load(std::memory_order_acquire)) return std::nullopt; +std::uint64_t MultisampleTargetRangeGeneration() { + std::shared_lock lock(guardedMutex()); + return guardedGeneration().load(std::memory_order_acquire); +} + +std::optional> ExcludeMultisampleTargetRanges( + VkDevice device, std::span ranges, std::uint64_t& generation) { + if (ranges.empty()) { + generation = MultisampleTargetRangeGeneration(); + return std::nullopt; + } + if (!anyGuarded().load(std::memory_order_acquire)) { + generation = MultisampleTargetRangeGeneration(); + return std::nullopt; + } std::vector> excluded; { std::shared_lock lock(guardedMutex()); + generation = guardedGeneration().load(std::memory_order_relaxed); for (const auto& range : guarded()) { if (range.device != device || !range.active->load(std::memory_order_acquire)) continue; excluded.emplace_back(range.address, range.address + range.bytes); diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index 2cafbf2f1f..2dc323d6b8 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -116,6 +116,7 @@ void snapshotTableTests(const Context& context, const BdaTestAccess& access) { auto a = build(first, false, true, snapshotAddress); auto plain = build(first, false, false, snapshotAddress); auto d = build(first, true, true, snapshotAddress); + const auto spaceTableHitsBefore = BdaResources::TableCacheCounters().spaceTables; auto b = build(first, false, true, snapshotAddress); auto c = build(second, false, true, snapshotAddress); Require(a.snapshot == b.snapshot && a.view == b.view, "two builds of one snapshot with the same bytes do not share its device copy"); @@ -124,6 +125,7 @@ void snapshotTableTests(const Context& context, const BdaTestAccess& access) { if (spaces) { Require(a.memory->CachedAddressTable().has_value() && c.memory->CachedAddressTable().has_value() && d.memory->CachedAddressTable().has_value(), "a build with its own regions over the address space has no cached BDA table"); Require(a.table == b.table, "two builds of the same space and snapshot do not share their BDA table"); + Require(BdaResources::TableCacheCounters().spaceTables == spaceTableHitsBefore + 1, "a repeated build with a stable address-space and MSAA guard generation missed the serial-keyed BDA table"); Require(c.table != a.table, "a build with other snapshot bytes took another build's BDA table"); Require(a.memory->CachedAddressTable()->serial == b.memory->CachedAddressTable()->serial && c.memory->CachedAddressTable()->serial != a.memory->CachedAddressTable()->serial, "cached BDA table serials do not follow the build's ranges"); } From 572dfb6f19a4d9818503704ac0f1ada466cd7aff Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Fri, 16 Oct 2026 00:27:30 +0000 Subject: [PATCH 25/49] style(agc): remove non-debt cache comment --- core/libs/prx/libSceAgcDriver/Graphics/include/BdaResources.hpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/BdaResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/BdaResources.hpp index e8f90365c8..2a796be046 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/BdaResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/BdaResources.hpp @@ -37,8 +37,6 @@ public: std::uint64_t hits = 0; std::uint64_t misses = 0; std::size_t held = 0; - // Hits served by the cached address space serial and MSAA guard generation (no hash or - // range compare; see GuestBufferMemory::CachedAddressTable), counted in `hits` too. std::uint64_t spaceTables = 0; // Why the most recently used table did not serve a build ([bda-table] first-differing entry // class): its owners were gone (expired weak reference), or its word hash differed, split by From ce55f5f672dc94cae7b5a2611c8495ed1ec723bc Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Fri, 16 Oct 2026 00:38:46 +0000 Subject: [PATCH 26/49] fix(agc): retain equivalent BDA space cache keys --- .../Graphics/src/BdaResources.cpp | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp index a26fa825ed..59a869f22f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp @@ -35,14 +35,13 @@ namespace { // copies). Hence several recent tables, most recently used first, matched by a word hash before // the byte compare. APS5_BDA_TABLE_CACHE_ENTRIES sets how many (default 8; 1 is the previous // behaviour); APS5_NO_BDA_TABLE_CACHE=1 builds every table. -// A table built for (or found equal to) the cached address space's ranges carries the space's -// serial: a build served by that space alone binds it by the serial, with no hash and no compare. +// A table built for (or found equal to) cached address-space ranges carries their serial and +// multisample generation, allowing equivalent spaces to bind the table without hashing or comparing. struct TableEntry { std::uint64_t hash; std::vector ranges; std::weak_ptr buffer; - std::uint64_t spaceSerial = 0; - std::uint64_t guardSerial = 0; + std::vector> spaceKeys; }; struct TableCache { @@ -148,7 +147,8 @@ BdaResources::BdaResources(const Context& context, const GuestBufferMemory& memo cache.device = context.device; } for (auto it = cache.entries.begin(); it != cache.entries.end(); ++it) { - if (it->spaceSerial != serial || it->guardSerial != guardSerial) continue; + const auto key = std::pair{serial, guardSerial}; + if (std::find(it->spaceKeys.begin(), it->spaceKeys.end(), key) == it->spaceKeys.end()) continue; if (auto shared = it->buffer.lock(); shared != nullptr) { ++cache.hits; ++cache.classes.spaceTables; @@ -197,11 +197,12 @@ BdaResources::BdaResources(const Context& context, const GuestBufferMemory& memo } if (sameRanges(it->ranges, ranges)) { ++cache.hits; - // An equal table of an earlier space (a rebuild that mapped the same ranges the - // same way): the next build of this space binds it by the serial. if (serial != 0) { - it->spaceSerial = serial; - it->guardSerial = guardSerial; + const auto key = std::pair{serial, guardSerial}; + auto found = std::find(it->spaceKeys.begin(), it->spaceKeys.end(), key); + if (found != it->spaceKeys.end()) it->spaceKeys.erase(found); + it->spaceKeys.push_back(key); + if (it->spaceKeys.size() > tableCacheEntries()) it->spaceKeys.erase(it->spaceKeys.begin()); } table = std::move(shared); cache.entries.splice(cache.entries.begin(), cache.entries, it); @@ -224,7 +225,8 @@ BdaResources::BdaResources(const Context& context, const GuestBufferMemory& memo cache.device = context.device; } auto cachedRanges = filtered.has_value() ? std::move(*filtered) : cached.has_value() ? *cached->ranges : std::move(built); - cache.entries.push_front({hash, std::move(cachedRanges), table, serial, guardSerial}); + cache.entries.push_front({hash, std::move(cachedRanges), table, {}}); + if (serial != 0) cache.entries.front().spaceKeys.emplace_back(serial, guardSerial); while (cache.entries.size() > tableCacheEntries()) cache.entries.pop_back(); } } From 50e0836bdbf70a3554fa14aa211fdec11139f004 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Fri, 16 Oct 2026 00:42:47 +0000 Subject: [PATCH 27/49] style(agc): remove cache key comments --- .../Graphics/src/BdaResources.cpp | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp index 59a869f22f..662e9c5ded 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp @@ -19,24 +19,6 @@ namespace AgcDriver::Graphics { namespace { -// The page tables built per device, reused by a build whose ranges (guest ranges, device -// addresses, permissions) equal one of them: consecutive address-based builds map the same registered -// set through the same imports and mirrors, so the ~1200-entry table repeats. The shader only reads -// the table (its lookup keeps its state in function variables), and a batch in flight keeps its -// BdaResources, so sharing the buffer is safe. The cache holds the tables weakly: they never outlive -// the builds that own them (a static owner would destroy the buffer after its device, at exit or on -// device replacement, and would fail the tests' leak check), and with deferred lease release the -// previous builds are still in flight when the next one asks, which is what makes the hit rate. -// One table was enough while every build repeated the last one exactly; now the misaligned -// descriptor sub-ranges the GPU copies out of imports sit in pool staging buffers whose device -// addresses cycle through the few buffers the batches in flight leave free, and the three -// address-based programs of one queue interleave with the lease draws, so a table differs from the -// previous one and comes back a few builds later (69% misses in the wave-8 run, 4% before the GPU -// copies). Hence several recent tables, most recently used first, matched by a word hash before -// the byte compare. APS5_BDA_TABLE_CACHE_ENTRIES sets how many (default 8; 1 is the previous -// behaviour); APS5_NO_BDA_TABLE_CACHE=1 builds every table. -// A table built for (or found equal to) cached address-space ranges carries their serial and -// multisample generation, allowing equivalent spaces to bind the table without hashing or comparing. struct TableEntry { std::uint64_t hash; std::vector ranges; @@ -55,8 +37,6 @@ struct TableCache { bool sameRanges(const std::vector& left, const std::vector& right); -// APS5_PROFILE_DRAW: why the most recently used entry did not serve this build (see -// TableCacheStats). Under the cache mutex. void classifyFirstEntry(TableCache& cache, std::uint64_t hash, const std::vector& ranges) { static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; if (!profile) return; From 23bfa5787d0acbc2fedfd9c0dfe0d3f5696df83c Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Fri, 16 Oct 2026 05:18:37 +0000 Subject: [PATCH 28/49] fix(agc): restore cached BDA table range --- core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp | 1 + core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp | 1 + 2 files changed, 2 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp index 662e9c5ded..3b0498e7b8 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp @@ -130,6 +130,7 @@ BdaResources::BdaResources(const Context& context, const GuestBufferMemory& memo const auto key = std::pair{serial, guardSerial}; if (std::find(it->spaceKeys.begin(), it->spaceKeys.end(), key) == it->spaceKeys.end()) continue; if (auto shared = it->buffer.lock(); shared != nullptr) { + tableBytes = shared->Bytes().size(); ++cache.hits; ++cache.classes.spaceTables; table = std::move(shared); diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index 2dc323d6b8..a00cc6e740 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -126,6 +126,7 @@ void snapshotTableTests(const Context& context, const BdaTestAccess& access) { Require(a.memory->CachedAddressTable().has_value() && c.memory->CachedAddressTable().has_value() && d.memory->CachedAddressTable().has_value(), "a build with its own regions over the address space has no cached BDA table"); Require(a.table == b.table, "two builds of the same space and snapshot do not share their BDA table"); Require(BdaResources::TableCacheCounters().spaceTables == spaceTableHitsBefore + 1, "a repeated build with a stable address-space and MSAA guard generation missed the serial-keyed BDA table"); + Require(b.bda->Table().range == a.bda->Table().range && b.bda->Table().range != 0, "a cached BDA descriptor lost its byte range: " + std::to_string(b.bda->Table().range) + " instead of " + std::to_string(a.bda->Table().range)); Require(c.table != a.table, "a build with other snapshot bytes took another build's BDA table"); Require(a.memory->CachedAddressTable()->serial == b.memory->CachedAddressTable()->serial && c.memory->CachedAddressTable()->serial != a.memory->CachedAddressTable()->serial, "cached BDA table serials do not follow the build's ranges"); } From fb1260b85577358cab3dc15b8f1843d7fb15fb53 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Sat, 17 Oct 2026 00:41:07 +0000 Subject: [PATCH 29/49] fix(agc): invalidate cached BDA tables on metadata retirement --- .../libSceAgcDriver/Graphics/src/MultisampleTarget.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp index 7b45c22866..dd41a2ddd9 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -833,14 +833,18 @@ void retireUnmapped(std::uintptr_t address, std::size_t bytes) { forgetSingleClears(address, bytes); RetireUnmappedDepth_nid_no_patch(address, bytes); std::unique_lock lock(guardedMutex()); + bool metadataChanged = false; const auto invalidated = std::erase_if(guarded(), [&](const auto& range) { - if (range.metadataValid && address < range.address + range.bytes && range.address < address + bytes) range.metadataValid->store(false, std::memory_order_release); + if (range.metadataValid && address < range.address + range.bytes && range.address < address + bytes) { + range.metadataValid->store(false, std::memory_order_release); + metadataChanged = true; + } if (range.address < address || range.address - address > bytes || range.bytes > bytes - (range.address - address)) return false; if (!range.metadataValid) range.active->store(false, std::memory_order_release); return true; }); const auto staleMetadata = std::erase_if(guarded(), [](const auto& range) { return range.metadataValid && !range.active->load(std::memory_order_acquire); }); - if (invalidated != 0 || staleMetadata != 0) guardedGeneration().fetch_add(1, std::memory_order_release); + if (metadataChanged || invalidated != 0 || staleMetadata != 0) guardedGeneration().fetch_add(1, std::memory_order_release); anyGuarded().store(!guarded().empty(), std::memory_order_release); } From 618e3614d9025f0aaafc6caa5c05a36361606f0c Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Sat, 17 Oct 2026 20:49:23 +0000 Subject: [PATCH 30/49] test(agc): restore native target guard after cleanup --- .../libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp index b97e3414ee..2fdc4cb04f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp @@ -1350,6 +1350,7 @@ int main(int argc, char** argv) { device->WaitIdle(); AgcDriver::Graphics::ClearMultisampleTargets(device->Device()); CheckBdaSpan(*device, block, 0, false); + Draw(*device, TargetState(DecodeTarget(block))); std::puts("every sample of a 4x color target loaded through its multisampled texture descriptor matches the triangle drawn into it, then the fill of its color memory; a surface no draw rendered to loads the value its color memory repeats and keeps it under a later draw"); struct ReleaseIndexDevice { std::unique_ptr& device; From eda087f6b48be5cf2d9a0fb03cb3b6a0a3331d9f Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Sun, 18 Oct 2026 01:50:22 +0000 Subject: [PATCH 31/49] fix(agc): remove duplicate multisample test state --- .../libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp index 2fdc4cb04f..d3a88efaf9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp @@ -1215,7 +1215,6 @@ int main(int argc, char** argv) { } const auto colorBytes = AgcDriver::Graphics::MultisampledColorBytes(color); Require(colorBytes == BlockBytes, "the native color target does not occupy exactly one BDA block"); - const auto state = TargetState(color); CheckBdaSpan(*device, block, 0, false); CheckBdaSpan(*device, block, BlockBytes, false); Draw(*device, state); From 51ab4d79e6803d78707880052b441ec8279963a6 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 27 Oct 2026 05:08:16 +0000 Subject: [PATCH 32/49] test(agc): check the BDA span on a native target that no earlier draw made resident A multisampled target that a first-sample read has sampled stays guarded on the host now, so its memory is already refused through BDA before the test's own draw. The ordinary-neighbour and refused-after-draw checks run on a fresh block, and the block of the earlier draws keeps its refused check and its neighbour. --- .../tests/execution/SampleMultisampleTarget.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp index d3a88efaf9..c78691aa76 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp @@ -1215,8 +1215,13 @@ int main(int argc, char** argv) { } const auto colorBytes = AgcDriver::Graphics::MultisampledColorBytes(color); Require(colorBytes == BlockBytes, "the native color target does not occupy exactly one BDA block"); - CheckBdaSpan(*device, block, 0, false); - CheckBdaSpan(*device, block, BlockBytes, false); + Block bda(4u * BlockBytes); + const auto bdaState = TargetState(DecodeTarget(bda)); + CheckBdaSpan(*device, bda, 0, false); + CheckBdaSpan(*device, bda, BlockBytes, false); + Draw(*device, bdaState); + CheckBdaSpan(*device, bda, 0, true); + CheckBdaSpan(*device, bda, BlockBytes, false); Draw(*device, state); CheckBdaSpan(*device, block, 0, true); CheckBdaSpan(*device, block, BlockBytes, false); From ae615aeb7153cfd6bafe76f7b90c60efc1a50d26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0?= Date: Fri, 30 Oct 2026 07:46:10 +0100 Subject: [PATCH 33/49] test(agc): clear the adjacent block before the typed buffer control --- .../libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp index c78691aa76..9b2977385d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp @@ -1255,6 +1255,7 @@ int main(int argc, char** argv) { device->WaitIdle(); }, "recipe dispatch", missing); } + std::memset(block.data + colorBytes, 0, BlockBytes); CheckBufferLoad(*device, CompileBufferLoad(*device, block.Address() + colorBytes, output.Address()), output); CheckUniform(LoadSamples(*device, block), Ones, "after the native buffer access refusals and adjacent buffer control"); std::cout << "native buffer controls: adjacent=passed deferred=" << (prepared != nullptr) << " recipe=" << recipeReused << std::endl; From a5dc1ebf2b7e1ff10cea3ba61d526bd6ff344853 Mon Sep 17 00:00:00 2001 From: frankea Date: Thu, 29 Oct 2026 12:45:08 -0400 Subject: [PATCH 34/49] feat(libSceAcm): encode sceAcmBatchJobPriority jobs --- core/libs/prx/libSceAcm/Export.cpp | 7 ++++++- core/libs/tests/GuestAcm.cpp | 16 ++++++++++++++++ docs/dev/debt/libSceAcm.md | 2 +- 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAcm/Export.cpp b/core/libs/prx/libSceAcm/Export.cpp index 14c227487b..dbc75f58bd 100644 --- a/core/libs/prx/libSceAcm/Export.cpp +++ b/core/libs/prx/libSceAcm/Export.cpp @@ -354,7 +354,12 @@ int APS5_VABI sceAcm_Panner(AcmBatchInfo* batch, std::uint32_t sourceCount, cons } int APS5_VABI sceAcmBatchJobPriority(AcmBatchInfo* batch, std::uint32_t priority) { - NotImplemented_nid_no_patch(__func__); + if (!batch || priority >= 4) return SCE_ACM_ERROR_BAD_POINTER; + const auto offset = batch->offset; + if (offset > batch->buffer_size || batch->buffer_size - offset < 8) return static_cast(0x81940009); + const std::uint64_t job = 0x10 | (static_cast(priority) << 8); + std::memcpy(static_cast(batch->buffer) + offset, &job, sizeof(job)); + batch->offset = offset + sizeof(job); return 0; } diff --git a/core/libs/tests/GuestAcm.cpp b/core/libs/tests/GuestAcm.cpp index bce7fb869e..d5be6cee44 100644 --- a/core/libs/tests/GuestAcm.cpp +++ b/core/libs/tests/GuestAcm.cpp @@ -288,6 +288,22 @@ int main() { outputA.channelCount = 3; Require(reverb(1, reverbFilters, reverbOutputs) == BadPointer); Require(reverbBatch.offset == 0 && reverbBatch.buffer_size == sizeof(reverbCommands)); + commandBatch = {commands, 0, sizeof(commands)}; + std::memset(commands, 0xCC, sizeof(commands)); + Require(sceAcmBatchJobPriority(nullptr, 0) == BadPointer); + Require(sceAcmBatchJobPriority(&commandBatch, 4) == BadPointer && commandBatch.offset == 0); + Require(sceAcmBatchJobPriority(&commandBatch, 0xFFFFFFFFu) == BadPointer && commandBatch.offset == 0); + Require(sceAcmBatchJobPriority(&commandBatch, 3) == 0 && commandBatch.offset == 8); + Require(sceAcmBatchJobPriority(&commandBatch, 0) == 0 && commandBatch.offset == 16 && commandBatch.buffer_size == sizeof(commands)); + std::uint64_t priorityWords[3]; + std::memcpy(priorityWords, commands, sizeof(priorityWords)); + Require(priorityWords[0] == 0x310 && priorityWords[1] == 0x10 && priorityWords[2] == 0xCCCCCCCCCCCCCCCCull); + commandBatch = {commands, sizeof(commands) - 7, sizeof(commands)}; + Require(sceAcmBatchJobPriority(&commandBatch, 1) == static_cast(0x81940009) && commandBatch.offset == sizeof(commands) - 7); + commandBatch = {commands, sizeof(commands) - 8, sizeof(commands)}; + Require(sceAcmBatchJobPriority(&commandBatch, 1) == 0 && commandBatch.offset == sizeof(commands)); + std::memcpy(priorityWords, commands + sizeof(commands) - 8, sizeof(std::uint64_t)); + Require(priorityWords[0] == 0x110); Require(sceAcmContextDestroy(context1) == 0); Require(sceAcmContextDestroy(context1) == BadContext); diff --git a/docs/dev/debt/libSceAcm.md b/docs/dev/debt/libSceAcm.md index 73aaab1619..63fe35d04f 100644 --- a/docs/dev/debt/libSceAcm.md +++ b/docs/dev/debt/libSceAcm.md @@ -2,7 +2,7 @@ ### Silent stubs -- [libSceAcm](../../../core/libs/prx/libSceAcm/Export.cpp) batches execute as silent no-ops, and do not execute encoded panner, FFT and reverb commands. +- [libSceAcm](../../../core/libs/prx/libSceAcm/Export.cpp) batches execute as silent no-ops, and do not execute encoded panner, FFT, reverb and priority commands. ### Unknown function info From f6bf44e2d1b48a69a6afd582ef185d0f7ace5d0f Mon Sep 17 00:00:00 2001 From: frankea Date: Thu, 29 Oct 2026 16:04:48 -0400 Subject: [PATCH 35/49] test(libSceAcm): check priority before room and offsets past the batch --- core/libs/tests/GuestAcm.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/libs/tests/GuestAcm.cpp b/core/libs/tests/GuestAcm.cpp index d5be6cee44..366cf3764b 100644 --- a/core/libs/tests/GuestAcm.cpp +++ b/core/libs/tests/GuestAcm.cpp @@ -300,6 +300,9 @@ int main() { Require(priorityWords[0] == 0x310 && priorityWords[1] == 0x10 && priorityWords[2] == 0xCCCCCCCCCCCCCCCCull); commandBatch = {commands, sizeof(commands) - 7, sizeof(commands)}; Require(sceAcmBatchJobPriority(&commandBatch, 1) == static_cast(0x81940009) && commandBatch.offset == sizeof(commands) - 7); + Require(sceAcmBatchJobPriority(&commandBatch, 4) == BadPointer && commandBatch.offset == sizeof(commands) - 7); + commandBatch = {commands, sizeof(commands) + 8, sizeof(commands)}; + Require(sceAcmBatchJobPriority(&commandBatch, 1) == static_cast(0x81940009) && commandBatch.offset == sizeof(commands) + 8); commandBatch = {commands, sizeof(commands) - 8, sizeof(commands)}; Require(sceAcmBatchJobPriority(&commandBatch, 1) == 0 && commandBatch.offset == sizeof(commands)); std::memcpy(priorityWords, commands + sizeof(commands) - 8, sizeof(std::uint64_t)); From 0be3e0c3407e9ef26e6f7191f5fda47124f50e4c Mon Sep 17 00:00:00 2001 From: frankea Date: Thu, 29 Oct 2026 16:04:48 -0400 Subject: [PATCH 36/49] docs(libSceAcm): cite the firmware source for priority jobs --- docs/dev/debt/libSceAcm.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceAcm.md b/docs/dev/debt/libSceAcm.md index 63fe35d04f..340bd20f62 100644 --- a/docs/dev/debt/libSceAcm.md +++ b/docs/dev/debt/libSceAcm.md @@ -2,7 +2,7 @@ ### Silent stubs -- [libSceAcm](../../../core/libs/prx/libSceAcm/Export.cpp) batches execute as silent no-ops, and do not execute encoded panner, FFT, reverb and priority commands. +- [libSceAcm](../../../core/libs/prx/libSceAcm/Export.cpp) batches execute as silent no-ops, and do not execute encoded panner, FFT, reverb and priority commands (the priority job layout and checks are read from `libSceAcm.sprx`, firmware 13.40, not measured on a console). ### Unknown function info From aee6ce2fd999edc0fa2d75f12ab56c6d5d142062 Mon Sep 17 00:00:00 2001 From: hugodearaujo Date: Thu, 29 Oct 2026 22:22:28 -0300 Subject: [PATCH 37/49] perf(agc): prune and gate the label take records A queue's passes on label dwords and the takes its own stores make of them move to LabelTakes. A wait only considers stores recorded after its submission entered the driver, so a pass or take at a serial no newer than that entry can no longer hide a store: each queue's passes are kept in serial order and dropped when its worker starts a submission received after them (for a rewind tail, the oldest submission of the queue still pending bounds it). noteLabelIssue reads a per-(queue, dword) count of live passes without the lock and takes labelStoresMutex only when a pass may exist. Passes and takes of a queue, and its stores that take them, all run on that queue's worker. --- .../Execution/include/Driver/Driver.hpp | 5 +- .../Driver/Synchronization/LabelTakes.hpp | 88 +++++++++++++++++++ .../src/Driver/Queues/QueueWorker.cpp | 1 + .../src/Driver/Queues/Submission.cpp | 4 + .../Driver/Synchronization/LabelHistory.cpp | 32 ++++--- .../libs/prx/libSceAgcDriver/tests/Submit.cpp | 64 ++++++++++++++ 6 files changed, 175 insertions(+), 19 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/Execution/include/Driver/Synchronization/LabelTakes.hpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp index dfc0e43630..6059fa4ac9 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp @@ -13,6 +13,7 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Dispatch/IndirectDispatch.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Memory/WriteEvidence.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Packets/PacketTiming.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/LabelTakes.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/WaitMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" @@ -220,6 +221,7 @@ private: void noteLabelStore(std::uint64_t address, std::span bytes, std::uint64_t stamp, std::uint32_t queue); void noteLabelPass(std::span packet, std::uint32_t queue); void noteLabelIssue(std::span packet, std::uint32_t queue); + void retireLabelTakes(std::uint32_t queue, std::uint64_t received); bool storedSince(std::span packet, std::uint64_t address, std::size_t bytes, std::uint64_t received, std::uint32_t queue, std::uint32_t* writer = nullptr); static void traceLabel(std::span packet, std::uint32_t queue); static std::array& writeHistory(); @@ -461,8 +463,7 @@ private: AgcDriver::Mutex labelStoresMutex; std::unordered_map> labelStores; - std::unordered_map labelPasses; - std::unordered_map labelTaken; + LabelTakes labelTakes; std::vector> replacedDevices; std::stop_token shutdownToken = LibcShutdownToken_nid_postfix(); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Synchronization/LabelTakes.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Synchronization/LabelTakes.hpp new file mode 100644 index 0000000000..eff8338f78 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Synchronization/LabelTakes.hpp @@ -0,0 +1,88 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_SYNCHRONIZATION_LABELTAKES_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_SYNCHRONIZATION_LABELTAKES_HPP + +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver::DriverDetail { + +class LabelTakes { +public: + void Pass(std::uint32_t queue, std::uint64_t dword, std::uint64_t serial) { + const auto key = queueDword(queue, dword); + auto& entry = entries[key]; + if (entry.passed == 0) passCounts[slot(key)].fetch_add(1, std::memory_order_relaxed); + entry.passed = serial; + passes[queue].emplace_back(serial, key); + } + + bool MayHavePass(std::uint32_t queue, std::uint64_t dword) const { + return passCounts[slot(queueDword(queue, dword))].load(std::memory_order_relaxed) != 0; + } + + void Take(std::uint32_t queue, std::uint64_t dword) { + const auto key = queueDword(queue, dword); + const auto found = entries.find(key); + if (found == entries.end() || found->second.passed == 0) return; + found->second.taken = found->second.passed; + found->second.passed = 0; + passCounts[slot(key)].fetch_sub(1, std::memory_order_relaxed); + } + + std::uint64_t Taken(std::uint32_t queue, std::uint64_t dword) const { + const auto found = entries.find(queueDword(queue, dword)); + return found == entries.end() ? 0 : found->second.taken; + } + + void Retire(std::uint32_t queue, std::uint64_t received) { + const auto queued = passes.find(queue); + if (queued == passes.end()) return; + auto& order = queued->second; + while (!order.empty() && order.front().first <= received) { + const auto [serial, key] = order.front(); + order.pop_front(); + const auto found = entries.find(key); + if (found == entries.end()) continue; + if (found->second.passed == serial) { + found->second.passed = 0; + passCounts[slot(key)].fetch_sub(1, std::memory_order_relaxed); + } + if (found->second.taken == serial) found->second.taken = 0; + if (found->second.passed == 0 && found->second.taken == 0) entries.erase(found); + } + if (order.empty()) passes.erase(queued); + } + + bool Empty() const { + return entries.empty() && passes.empty(); + } + +private: + struct Entry { + std::uint64_t passed = 0; + std::uint64_t taken = 0; + }; + + static constexpr std::size_t PassSlotBits = 12; + + static std::uint64_t queueDword(std::uint32_t queue, std::uint64_t dword) { + return dword | (static_cast(queue) << 56u); + } + + static std::size_t slot(std::uint64_t key) { + return static_cast(((key >> 2u) * 0x9e3779b97f4a7c15ull) >> (64u - PassSlotBits)); + } + + std::unordered_map entries; + std::unordered_map>> passes; + std::array, std::size_t{1} << PassSlotBits> passCounts{}; +}; + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp index a620cb1849..aaf8455ca5 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp @@ -278,6 +278,7 @@ void Driver::run(std::uint32_t id) noexcept { running.Start(); if (profile && submission.enqueuedAt != std::chrono::steady_clock::time_point{}) costs.dequeueNs += static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now() - submission.enqueuedAt).count()); } + retireLabelTakes(id, submission.received); execute(submission); if (submission.progress && submission.rewindTail == nullptr && !completionDeferred()) submitOpenWork(submission.progress); if (traceGpu) std::fprintf(stderr, "[gpu] %.1f done serial=%llu queue=0x%x\n", TraceMs(), static_cast(submission.serial), id); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp index 7b452b32f3..ed5af8d7e8 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp @@ -362,6 +362,7 @@ void Driver::executeRewindTail(const Submission& stalled) { if (APS5_ENABLE_TIMING_LOG) tail.roomReadyAt = std::chrono::steady_clock::now(); auto* const capture = SubmissionCapture::Active(); auto captured = capture != nullptr ? SubmissionCapture::Prepare(tail.queue, SubmissionKind::Rewind, tail.commands, tail.conditionalEnds) : CapturedSubmission{}; + std::uint64_t retiredTakes = 0; { std::lock_guard lock(mutex); rethrowFailure(); @@ -370,12 +371,15 @@ void Driver::executeRewindTail(const Submission& stalled) { tail.shaders = shaders; tail.serial = stalled.serial; tail.received = ++eventSerial; + const auto& waiting = workers[tail.queue].pending; + retiredTakes = waiting.empty() ? tail.received : std::min(tail.received, waiting.front().received); if (capture != nullptr) capture->Take(captured, tail.serial, captureTime(), tail.heldAtSubmit); if (ring) RecordSubmission(tail.queue, tail.serial, tail.commands, stalled.rewindTail, true); } if (capture != nullptr) writeCaptured(*this, *capture, captured); if (APS5_ENABLE_TIMING_LOG) tail.enqueuedAt = tail.dequeuedAt = tail.orderedAt = std::chrono::steady_clock::now(); timing.Finish(); + retireLabelTakes(tail.queue, retiredTakes); execute(tail); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/LabelHistory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/LabelHistory.cpp index db078dae9d..8b4141c7eb 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/LabelHistory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/LabelHistory.cpp @@ -20,14 +20,6 @@ void Driver::noteLabelStore(std::uint64_t address, std::span by } } -namespace { - -std::uint64_t queueDword(std::uint32_t queue, std::uint64_t dword) { - return dword | (static_cast(queue) << 56u); -} - -} - void Driver::noteLabelPass(std::span packet, std::uint32_t queue) { const auto opcode = (packet[0] >> 8u) & 0xffu; if ((opcode != 0x3c && opcode != 0x93) || packet.size() < 7 || ((packet[1] >> 4u) & 3u) != 1u) return; @@ -35,20 +27,28 @@ void Driver::noteLabelPass(std::span packet, std::uint32_t const std::uint64_t address = (packet[2] & (bytes == 8 ? ~7u : ~3u)) | (static_cast(packet[3]) << 32u); const auto passed = eventSerial.load(std::memory_order_acquire); std::lock_guard lock(labelStoresMutex); - for (std::size_t offset = 0; offset < bytes; offset += 4) labelPasses[queueDword(queue, address + offset)] = passed; + for (std::size_t offset = 0; offset < bytes; offset += 4) labelTakes.Pass(queue, address + offset, passed); +} + +void Driver::retireLabelTakes(std::uint32_t queue, std::uint64_t received) { + std::lock_guard lock(labelStoresMutex); + labelTakes.Retire(queue, received); } void Driver::noteLabelIssue(std::span packet, std::uint32_t queue) { - const auto take = [&](std::uint64_t address, std::uint64_t bytes) { + const auto mayTake = [&](std::uint64_t address, std::uint64_t bytes) { for (std::uint64_t offset = 0; offset < bytes; offset += 4) { - const auto key = queueDword(queue, address + offset); - if (const auto pass = labelPasses.find(key); pass != labelPasses.end()) labelTaken[key] = pass->second; + if (labelTakes.MayHavePass(queue, address + offset)) return true; } + return false; + }; + const auto take = [&](std::uint64_t address, std::uint64_t bytes) { + for (std::uint64_t offset = 0; offset < bytes; offset += 4) labelTakes.Take(queue, address + offset); }; switch ((packet[0] >> 8u) & 0xffu) { case 0x37: case 0x49: { const auto label = Pm4::DecodeLabelWrite(packet); - if (!label.has_value() || !KeepsLabelStore(label->address, label->Bytes().size())) return; + if (!label.has_value() || !KeepsLabelStore(label->address, label->Bytes().size()) || !mayTake(label->address, label->Bytes().size())) return; std::lock_guard lock(labelStoresMutex); take(label->address, label->Bytes().size()); return; @@ -57,7 +57,7 @@ void Driver::noteLabelIssue(std::span packet, std::uint32_t default: return; } auto stores = Pm4::DecodeStores(packet); - std::erase_if(stores, [](const Pm4::PacketStore& store) { return store.conditional || !KeepsLabelStore(store.address, static_cast(store.bytes)); }); + std::erase_if(stores, [&](const Pm4::PacketStore& store) { return store.conditional || !KeepsLabelStore(store.address, static_cast(store.bytes)) || !mayTake(store.address, store.bytes); }); if (stores.empty()) return; std::lock_guard lock(labelStoresMutex); for (const auto& store : stores) take(store.address, store.bytes); @@ -70,9 +70,7 @@ bool Driver::storedSince(std::span packet, std::uint64_t ad const auto high = bytes == 8 ? labelStores.find(address + 4) : labelStores.end(); if (bytes == 8 && high == labelStores.end()) return false; std::uint64_t taken = 0; - for (std::size_t offset = 0; offset < bytes; offset += 4) { - if (const auto found = labelTaken.find(queueDword(queue, address + offset)); found != labelTaken.end()) taken = std::max(taken, found->second); - } + for (std::size_t offset = 0; offset < bytes; offset += 4) taken = std::max(taken, labelTakes.Taken(queue, address + offset)); for (const auto& store : low->second) { if (store.stamp <= received || (store.queue != queue && store.stamp <= taken)) continue; std::uint64_t value = store.value; diff --git a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp index 1d226750d1..482c6aad1b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp @@ -39,6 +39,18 @@ static_assert(offsetof(Packet, addr) == 0); static_assert(offsetof(Packet, dw_num) == 8); static_assert(offsetof(Packet, flags) == 12); +namespace AgcDriver::DriverDetail { + +struct DriverTestAccess { + static std::uint64_t LabelTaken(std::uint32_t queue, const volatile std::uint32_t* address) { + auto& driver = Driver::Get(); + std::lock_guard lock(driver.labelStoresMutex); + return driver.labelTakes.Taken(queue, reinterpret_cast(address)); + } +}; + +} + namespace { void check(bool condition, const char* message) { @@ -451,6 +463,56 @@ void testLabelTakenAndResetByEarlierSubmission() { AgcDriverWaitIdle_nid_postfix(); } +void testLabelTakesRetire() { + using AgcDriver::DriverDetail::LabelTakes; + auto takes = std::make_unique(); + constexpr std::uint64_t label = 0x1000, other = 0x2000; + takes->Pass(0x20, label, 10); + check(takes->MayHavePass(0x20, label) && takes->Taken(0x20, label) == 0, "a passed wait was not kept for the store that takes it"); + takes->Take(0x20, label); + check(takes->Taken(0x20, label) == 10 && !takes->MayHavePass(0x20, label), "the queue's own store did not turn its pass into a take"); + takes->Retire(0x20, 9); + check(takes->Taken(0x20, label) == 10, "a take was dropped while a submission of the queue entered before it could still wait"); + takes->Retire(0x28, 20); + check(takes->Taken(0x20, label) == 10, "another queue's submission retired a take"); + takes->Retire(0x20, 10); + check(takes->Taken(0x20, label) == 0 && takes->Empty(), "a take was kept after every submission of the queue that could wait entered after it"); + takes->Pass(0x20, label, 30); + takes->Pass(0x20, label, 40); + takes->Retire(0x20, 30); + check(takes->MayHavePass(0x20, label), "an older pass on the dword retired its newer pass"); + takes->Take(0x20, label); + takes->Retire(0x20, 39); + check(takes->Taken(0x20, label) == 40, "a take was retired by a submission entered before it"); + takes->Pass(0x20, other, 50); + takes->Pass(0x28, label, 50); + takes->Retire(0x20, 50); + check(takes->Taken(0x20, label) == 0 && !takes->MayHavePass(0x20, other), "a submission of the queue entered after its passes and take did not retire them"); + check(takes->MayHavePass(0x28, label) && !takes->Empty(), "a submission of one queue retired another queue's pass"); + takes->Retire(0x28, 50); + check(takes->Empty(), "a pass was kept after every submission of its queue entered after it"); +} + +void testLabelTakeRetiredByLaterSubmission() { + using AgcDriver::DriverDetail::DriverTestAccess; + alignas(64) static volatile std::uint32_t gate = 0, label = 0, first = 0, second = 0, third = 0; + submit(0x20, commands(waitEqual(&gate, 1), waitEqual(&label, 1), writeData(&label, 0), writeData(&first, 1))); + submit(0x20, commands(waitEqual(&label, 1), writeData(&second, 1))); + submit(0, commands(writeData(&label, 1))); + waitFor(&label, 1, "producer label never landed"); + gate = 1; + waitFor(&first, 1, "consumer never passed its first label wait"); + std::this_thread::sleep_for(std::chrono::milliseconds(1200)); + check(second == 0, "a label store an earlier submission of the queue had taken and reset satisfied a later submission's wait, or the wait expired"); + check(DriverTestAccess::LabelTaken(0x20, &label) != 0, "a take was dropped while a submission of the queue entered before it was waiting"); + submit(0, commands(writeData(&label, 1))); + waitFor(&second, 1, "the later submission did not resume after a new producer label"); + submit(0x20, commands(writeData(&third, 1))); + waitFor(&third, 1, "a submission entered after the take never ran"); + check(DriverTestAccess::LabelTaken(0x20, &label) == 0, "a take was kept after a submission of the queue entered after it started"); + AgcDriverWaitIdle_nid_postfix(); +} + void testWaitFreeSubmissionAfterEarlierQueue0Work() { alignas(64) static volatile std::uint32_t first = 0, second = 0; constexpr std::uint32_t writes = 20000; @@ -1103,6 +1165,8 @@ int main(int argc, char** argv) { testLabelHeldAtSubmission(); testLabelTakenAndResetInOneSubmission(); testLabelTakenAndResetByEarlierSubmission(); + testLabelTakesRetire(); + testLabelTakeRetiredByLaterSubmission(); testWideLabelStoredSinceSubmission(); testWaitFreeSubmissionAfterEarlierQueue0Work(); testWaitFreeSubmissionQueue0WaitsOn(); From 96581cb7d6462261de71b6a41376b4f2399c8dbd Mon Sep 17 00:00:00 2001 From: atinseau Date: Thu, 22 Oct 2026 10:39:56 +0200 Subject: [PATCH 38/49] fix(relinker): keep moved instructions off a function entry after padding An island or relocation window could start in the int3 or nop padding after a jump or return and run into the next function. A function that only a pointer reaches, such as a virtual method, is not a known branch target, so its first bytes became the middle of a jump to a stub. An instruction that follows a jump, a return, a trap or padding after one of them can now only start a window. An int3 inside a window does not count as such an instruction, so a window that lies entirely in int3 padding is still used, as before; nop padding stays excluded, since a function built with -fpatchable-function-entry can begin with a nop. --- .../codegen/x86/X64OpcodeConstants.hpp | 3 ++ .../codegen/src/Amd64OnlyConverter.cpp | 39 ++++++++++++++++++- .../codegen/tests/Amd64OnlyConverterTests.cpp | 35 +++++++++++++++++ 3 files changed, 75 insertions(+), 2 deletions(-) diff --git a/core/relinker/codegen/include/codegen/x86/X64OpcodeConstants.hpp b/core/relinker/codegen/include/codegen/x86/X64OpcodeConstants.hpp index 951a99b18e..648b8cbd79 100644 --- a/core/relinker/codegen/include/codegen/x86/X64OpcodeConstants.hpp +++ b/core/relinker/codegen/include/codegen/x86/X64OpcodeConstants.hpp @@ -20,6 +20,7 @@ inline constexpr std::uint8_t PrefixAddressSize = 0x67; inline constexpr std::uint8_t RexMin = 0x40; inline constexpr std::uint8_t RexMax = 0x4F; inline constexpr std::uint8_t RexWBit = 0x08; +inline constexpr std::uint8_t RexBBit = 0x01; inline constexpr std::uint8_t TwoByteOpcodeEscape = 0x0F; @@ -108,6 +109,8 @@ inline constexpr std::uint8_t OneByteLoopMax = 0xE2; inline constexpr std::uint8_t OneByteInOutImm8Min = 0xE4; inline constexpr std::uint8_t OneByteInOutImm8Max = 0xE7; inline constexpr std::uint8_t OneByteInt = 0xCD; +inline constexpr std::uint8_t OneByteInt3 = 0xCC; +inline constexpr std::uint8_t OneByteNop = 0x90; inline constexpr std::uint8_t OneByteRetImm16 = 0xC2; inline constexpr std::uint8_t OneByteRetFarImm16 = 0xCA; inline constexpr std::uint8_t OneByteEnter = 0xC8; diff --git a/core/relinker/codegen/src/Amd64OnlyConverter.cpp b/core/relinker/codegen/src/Amd64OnlyConverter.cpp index d8d1be1731..f6817ce47e 100644 --- a/core/relinker/codegen/src/Amd64OnlyConverter.cpp +++ b/core/relinker/codegen/src/Amd64OnlyConverter.cpp @@ -112,6 +112,41 @@ private: return hit != _branchTargets.end() && *hit < end; } + [[nodiscard]] bool _int3(const std::size_t index) const { + const auto bytes = _bytes(index); + return bytes.size() == 1 && bytes[0] == X64OpcodeConstants::OneByteInt3; + } + + [[nodiscard]] bool _padding(const std::size_t index) const { + const auto info = _info(index); + if (info.IsTwoByteOpcode) + return info.Opcode == X64OpcodeConstants::TwoByteNopModRm; + if (info.Opcode != X64OpcodeConstants::OneByteNop) + return _int3(index); + const auto prefixes = _bytes(index).first(info.OpcodeOffset); + return (info.RexPrefix & X64OpcodeConstants::RexBBit) == 0 && std::find(prefixes.begin(), prefixes.end(), X64OpcodeConstants::PrefixRep) == prefixes.end(); + } + + [[nodiscard]] bool _mayBeEntered(const std::size_t index) const { + for (auto previous = index; previous > 0; --previous) { + if (_end(previous - 1) != _address(previous) || _blocksFollowing(previous - 1)) + return true; + if (!_padding(previous - 1)) + return false; + } + return true; + } + + [[nodiscard]] bool _enteredWithin(const std::size_t first, const std::size_t last) const { + if (_entered(_address(first), _end(last))) + return true; + for (auto index = first + 1; index <= last; ++index) { + if (!_int3(index) && _mayBeEntered(index)) + return true; + } + return false; + } + [[nodiscard]] bool _blocksFollowing(const std::size_t index) const { const auto kind = _info(index).FlowKind; return kind != ControlFlowKind::Sequential && kind != ControlFlowKind::ConditionalBranch; @@ -135,7 +170,7 @@ private: } [[nodiscard]] std::optional _relocate(const std::size_t index, const std::size_t first, const std::size_t last, const bool allowInPlace = false) const { - if (_entered(_address(first), _end(last))) + if (_enteredWithin(first, last)) return std::nullopt; StubBodyBuilder body; Placement placement; @@ -162,7 +197,7 @@ private: using Amd64OnlySubstitutionTable::kJmpRel32; std::size_t head = 0; for (auto last = start; last < _matches.size(); ++last) { - if (!_movable(last, true) || _entered(_address(start), _end(last))) + if (!_movable(last, true) || _enteredWithin(start, last)) return std::nullopt; if (head == 0 && _end(last) - _address(start) >= kJmpRel32.Size) head = static_cast(_end(last) - _address(start)); diff --git a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp index d5823779fc..451340b35f 100644 --- a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp +++ b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp @@ -1058,6 +1058,39 @@ void converterWideStoreWindows() { requireFailure([&] { (void)convert(withStore(far, tail)); }, "A 256-bit store whose movable neighbours are all beyond a short jump was kept"); } +void converterIslandIndirectEntries() { + const auto converter = Codegen::MakeAmd64OnlyConverter(Codegen::Amd64OnlyTarget::Rosetta); + const Bytes prologue = {0x55, 0x48, 0x89, 0xE5, 0x41, 0x57, 0x41, 0x56, 0x41, 0x55, 0x41, 0x54, 0x53, 0xC3}; + for (const auto& padding : {Bytes{0xCC, 0xCC}, Bytes{0x66, 0x90}, Bytes{0x0F, 0x1F, 0x00}, Bytes{}}) { + Bytes text = {0xE8, 0x40, 0x00, 0x00, 0x00, 0xC5, 0xFC, 0x11, 0x07, 0xC3, 0xEB, 0xFD}; + text.insert(text.end(), padding.begin(), padding.end()); + const auto entry = static_cast(0x200 + text.size()); + text.insert(text.end(), prologue.begin(), prologue.end()); + Bytes file(0x300 + text.size(), 0xCC); + std::copy(text.begin(), text.end(), file.begin() + 0x200); + const auto result = converter->Convert(file, {segmentHeader(text.size())}); + require(result.Trampolines.size() == 2 && result.KeptCount == 0, "A 256-bit store before a function that only a pointer reaches was not lowered through an island"); + for (const auto& site : result.Trampolines) + require(site.Offset >= entry || site.Offset + site.Length <= entry, "An island covered the entry of a function that follows a jump and its padding"); + require(result.Trampolines[0].Offset == entry, "An island next to a function that follows a jump and its padding did not start at its entry"); + } +} + +void converterIslandInTrapPadding() { + const auto converter = Codegen::MakeAmd64OnlyConverter(Codegen::Amd64OnlyTarget::Rosetta); + Bytes text = {0xE8, 0x40, 0x00, 0x00, 0x00, 0xC5, 0xFC, 0x11, 0x07, 0xC3, 0xEB, 0xFD}; + const Bytes padding(15, 0xCC); + text.insert(text.end(), padding.begin(), padding.end()); + const auto entry = static_cast(0x200 + text.size()); + text.insert(text.end(), {0xE9, 0x00, 0x01, 0x00, 0x00}); + Bytes file(0x300 + text.size(), 0xCC); + std::copy(text.begin(), text.end(), file.begin() + 0x200); + const auto result = converter->Convert(file, {segmentHeader(text.size())}); + require(result.Trampolines.size() == 2 && result.KeptCount == 0, "A 256-bit store before int3 padding and a function that cannot move was not lowered through an island"); + for (const auto& site : result.Trampolines) + require(site.Offset + site.Length <= entry, "An island in the int3 padding after a jump ran into the next function"); +} + void converterClaimedFollower() { const auto converter = Codegen::MakeAmd64OnlyConverter(Codegen::Amd64OnlyTarget::Rosetta); for (const auto& [shortInstruction, name] : {std::pair{Bytes{0x0F, 0x53, 0xCA}, "RCPPS"}, std::pair{Bytes{0xF3, 0x0F, 0xC7, 0xF8}, "RDPID"}}) { @@ -2413,6 +2446,8 @@ int main() { wideStoreSplit(); converterWideStoreWindows(); converterWideStoreIslandAcrossInt64Max(); + converterIslandIndirectEntries(); + converterIslandInTrapPadding(); converterClaimedFollower(); converterJumpTableTargets(); converterReciprocal(); From 38cba3eb5da24132f3af6164c9159392496481f5 Mon Sep 17 00:00:00 2001 From: Omar Elmahy Date: Fri, 30 Oct 2026 09:09:39 +0000 Subject: [PATCH 39/49] test(libSceNpSessionSignaling): cover post-terminate CreateContext lifecycle In GuestNpSessionSignaling.cpp, assert that sceNpSessionSignalingCreateContext with SessionSignalingContextParam1 returns NotInitialized after termination. --- core/libs/tests/GuestNpSessionSignaling.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/tests/GuestNpSessionSignaling.cpp b/core/libs/tests/GuestNpSessionSignaling.cpp index d69593f7e5..cd5d5c0943 100644 --- a/core/libs/tests/GuestNpSessionSignaling.cpp +++ b/core/libs/tests/GuestNpSessionSignaling.cpp @@ -160,4 +160,5 @@ int main() { Require(sceNpSessionSignalingInitialize(reinterpret_cast(initializeParam)) == 0, "reinitialize"); Require(sceNpSessionSignalingRequestPrepare(contextId, &requestId) == ContextNotFound, "reinitialize retained a context"); Require(sceNpSessionSignalingTerminate() == 0, "final terminate"); + Require(sceNpSessionSignalingCreateContext(reinterpret_cast(1), &contextId) == NotInitialized, "create context post-terminate"); } From c55a6c60636b7d98e6f6c0cc7fc19f3138d60214 Mon Sep 17 00:00:00 2001 From: Hedi Trigui Date: Fri, 30 Oct 2026 00:38:54 +0100 Subject: [PATCH 40/49] test(libSceVdecsw): cover two-picture output rejection in GuestVdecsw --- core/libs/tests/GuestVdecsw.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/core/libs/tests/GuestVdecsw.cpp b/core/libs/tests/GuestVdecsw.cpp index 0990141df2..9b2bc63990 100644 --- a/core/libs/tests/GuestVdecsw.cpp +++ b/core/libs/tests/GuestVdecsw.cpp @@ -102,6 +102,12 @@ void checkGenericPictureInfo(const OutputInfo& output, const TInfo& typed) { shorter.thisSize = sizeof(OutputInfo) - 8; auto actual = typed; check(sceVdecswGetPictureInfo(&shorter, &actual, nullptr) == 0 && std::memcmp(&actual, &typed, sizeof(actual)) == 0, "generic picture info rejected the shorter output descriptor"); + auto doubled = output; + doubled.pictureCount = 2; + auto doubledFirst = typed; + const auto doubledBefore = doubledFirst; + auto doubledSecond = typed; + check(throws([&] { sceVdecswGetPictureInfo(&doubled, &doubledFirst, &doubledSecond); }) && std::memcmp(&doubledFirst, &doubledBefore, sizeof(doubledFirst)) == 0 && std::memcmp(&doubledSecond, &doubledBefore, sizeof(doubledSecond)) == 0, "generic picture info accepted two pictures"); } void testGenericPictureInfoValidation() { From cd9f6033d7f760a26d4d9c82b7fba899a0814320 Mon Sep 17 00:00:00 2001 From: Giannis Date: Thu, 29 Oct 2026 19:45:08 +0200 Subject: [PATCH 41/49] test(agc): skip the unclamped normalized blend test without shaderStorageImageReadWithoutFormat The pixel shader merge behind the unclamped blend reads the color target through a storage image without a format, so on a device with fragmentShaderPixelInterlock but without shaderStorageImageReadWithoutFormat (Intel HD Graphics 630, Mesa ANV) every draw fails with "storage image reads without a format are unavailable on the target device". Skip the test there with the device reason, as the 4_4_4_4 partial write test does for the same merge path. Co-Authored-By: Claude Opus 5.5 --- .../tests/execution/UnclampedNormalizedBlend.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/UnclampedNormalizedBlend.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/UnclampedNormalizedBlend.cpp index fe4ba8a092..a8c47fc2c4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/UnclampedNormalizedBlend.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/UnclampedNormalizedBlend.cpp @@ -512,6 +512,10 @@ int main() { std::puts("skipped, the device lacks fragmentShaderPixelInterlock"); return VulkanTestSkipped; } + if (!TargetHasCapability(device->Target(), spv::CapabilityStorageImageReadWithoutFormat)) { + std::puts("skipped, the device lacks shaderStorageImageReadWithoutFormat"); + return VulkanTestSkipped; + } const ColorTargetLayout layout(Width, Height, ColorTileMode::RenderTarget, 4); Block block(layout.Bytes()); const auto unorm = DecodeTarget(block, Unorm8888Info); From a98d4415b34c27bed6958598022b85e5ed654c6d Mon Sep 17 00:00:00 2001 From: Giannis Date: Thu, 29 Oct 2026 20:20:49 +0200 Subject: [PATCH 42/49] docs(agc): record that the unclamped normalized blend needs shaderStorageImageReadWithoutFormat The pixel shader reads the color target through a storage image without a format, so the recompiler refuses the shader on a device without shaderStorageImageReadWithoutFormat. The sentence on unblended targets goes, to keep the entry within the 1000-character limit. Asked for by oneandonlydean in review. Co-Authored-By: Claude Opus 5.5 --- docs/dev/debt/libSceAgcDriver.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 44f58733d3..42d9a55019 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -286,7 +286,7 @@ - [Host import refresh](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): after a change to the guest allocation registry, an import is checked again only when its import window, or its own range when larger, touches a range that changed since the last check, or when it was made since then; every import is checked when the window is 0 or the window or the budget changed. An unchanged import whose run could now be larger, because a remembered refusal no longer holds, is kept until its window changes or a request for the larger run retires it; before, any registry change anywhere retired it. Remembered failures and refusals are dropped the same way and stay checked where they are used. - [Sampled MSAA surface geometry](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp) supplies the fragment-aware linear size and one mip for a tiled 2D surface at two, four or eight fragments. Unsupported arrays, mip chains, extents and block sizes throw. Linear descriptors retain the existing geometry path, including its use when sampling an existing native target; a linear MSAA guest layout is not modeled. The range calculation follows AMD Addrlib's thin-block dimensions. Console layout equivalence and title effects of the additional mip metadata remain unmeasured. This does not add sample upload or readback. -- [Unclamped normalized color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - CB_COLOR_INFO targets with BLEND_CLAMP clear (bit 15) on UNORM and SNORM formats are accepted. Without blending Vulkan's conversion clamps the stored value. With blending the pixel shader reads the target through its storage image under pixel interlock, evaluates the blend equation on the unclamped export, and stores the clamped result, because Vulkan clamps blend inputs to the normalized range. The result is not measured on a console. The final float to normalized rounding is Vulkan's. The path needs fragmentShaderPixelInterlock, recorded draws and a single-sample resident target, and refuses dual-source blending, multisampled, packed, reversed and alternate component order targets, a signed target whose write mask skips a component (the shader rewrites it through a float, so the code -128 would become -127), and a packed target's partial write in the same draw. +- [Unclamped normalized color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - CB_COLOR_INFO targets with BLEND_CLAMP clear (bit 15) on UNORM and SNORM formats are accepted. With blending the pixel shader reads the target through its storage image under pixel interlock, evaluates the blend equation on the unclamped export, and stores the clamped result, because Vulkan clamps blend inputs to the normalized range. The result is not measured on a console. The final float to normalized rounding is Vulkan's. The path needs fragmentShaderPixelInterlock and shaderStorageImageReadWithoutFormat (refused without either), recorded draws and a single-sample resident target, and refuses dual-source blending, multisampled, packed, reversed and alternate component order targets, a signed target whose write mask skips a component (the shader rewrites it through a float, so the code -128 would become -127), and a packed target's partial write in the same draw. - [Multisample image readback](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleReadback.cpp) fetches unsigned R8/R16/R32/R32G32/R32G32B32A32 or D32 depth samples through a 2D array view in GENERAL, in layer/y/x/fragment order. The caller owns producer completion, image transitions and resource lifetimes. It implements Vulkan per-sample fetches, not guest-memory publication, metadata expansion or PS5 layout equivalence; D32 exceptional-value preservation has not been measured. Other formats, sample counts and invalid ranges throw. - [Multisample image upload](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleUpload.cpp) does not model guest layouts, native ownership or CMASK/FMASK/DCC publication; console equivalence and large-title initialization costs remain unmeasured. It stores 1/2/4/8/16-byte elements from a storage buffer into an unsigned 2D multisample array view in layer/y/x/fragment order. It requires enabled multisample storage images and formatless image writes, 2/4/8 fragments and valid aligned buffer and dispatch ranges. The caller supplies the matching storage view in GENERAL and owns barriers, completion and resource lifetimes. From 3ecb625ae1cf6cdb7bc4e68871e842f3ae529ca0 Mon Sep 17 00:00:00 2001 From: bunbeans2-arch <344598290+bunbeans2-arch@users.noreply.github.com> Date: Fri, 30 Oct 2026 07:19:25 -0400 Subject: [PATCH 43/49] fix(tests): decode configure_seed output as UTF-8 configure_seed reconfigures stdout and stderr to UTF-8, but the tests decoded them with the locale encoding. Under a TEMP path with a non-ASCII character, the unreadable-cache and unwritable-seed tests read the path back in cp1252 and failed on Windows. Co-Authored-By: Claude Opus 5.5 --- tools/tests/test_configure_seed.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tools/tests/test_configure_seed.py b/tools/tests/test_configure_seed.py index 781bafce5c..dba19da350 100644 --- a/tools/tests/test_configure_seed.py +++ b/tools/tests/test_configure_seed.py @@ -47,7 +47,7 @@ class ConfigureSeedPathTests(unittest.TestCase): "resource.setrlimit(resource.RLIMIT_FSIZE, (64, 64)); " "sys.path.insert(0, sys.argv[1]); import configure_seed; configure_seed.main(sys.argv[2:])") result = subprocess.run([sys.executable, "-c", program, str(TOOL.parent), str(self.cache), str(seed)], - capture_output=True, text=True, timeout=5) + capture_output=True, encoding="utf-8", timeout=5) self.assertEqual(result.returncode, 2, result.stderr) self.assertNotIn("Traceback", result.stderr) self.assertEqual(result.stdout, "") @@ -62,11 +62,11 @@ class ConfigureSeedPathTests(unittest.TestCase): seed = self.root / "seed.cmake" program = "import os,sys; os.umask(0o027); sys.path.insert(0,sys.argv[1]); import configure_seed; configure_seed.main(sys.argv[2:])" arguments = [sys.executable, "-c", program, str(TOOL.parent), str(self.cache), str(seed)] - result = subprocess.run(arguments, capture_output=True, text=True, timeout=5) + result = subprocess.run(arguments, capture_output=True, encoding="utf-8", timeout=5) self.assertEqual(result.returncode, 0, result.stderr) self.assertEqual(seed.stat().st_mode & 0o777, 0o640) seed.chmod(0o600) - result = subprocess.run(arguments, capture_output=True, text=True, timeout=5) + result = subprocess.run(arguments, capture_output=True, encoding="utf-8", timeout=5) self.assertEqual(result.returncode, 0, result.stderr) self.assertEqual(seed.stat().st_mode & 0o777, 0o600) @@ -110,7 +110,7 @@ class ConfigureSeedPathTests(unittest.TestCase): def run_seed(self, seed, *options): return subprocess.run([sys.executable, str(TOOL), str(self.cache), str(seed), *options], - capture_output=True, text=True) + capture_output=True, encoding="utf-8") def assert_rejected(self, seed, *options): result = self.run_seed(seed, *options) @@ -124,7 +124,7 @@ class ConfigureSeedPathTests(unittest.TestCase): def test_unreadable_cache_is_reported_without_a_traceback(self): missing = self.root / "no cache dir" / "CMakeCache.txt" result = subprocess.run([sys.executable, str(TOOL), str(missing), str(self.root / "seed.cmake")], - capture_output=True, text=True) + capture_output=True, encoding="utf-8") self.assertEqual(result.returncode, 2, result.stderr) self.assertIn(str(missing), result.stderr) self.assertNotIn("Traceback", result.stderr) @@ -373,7 +373,7 @@ class ChecksParsingTests(unittest.TestCase): ("--include", "ARCH_SIZE"), ("--exclude", "ARCH_SIZE")): with self.subTest(options=options): result = subprocess.run([sys.executable, str(TOOL), str(cache), str(seed), *options], - capture_output=True, text=True) + capture_output=True, encoding="utf-8") self.assertEqual(result.returncode, 0, result.stderr) text = seed.read_text(encoding="utf-8") self.assertNotIn("ARCH_SIZE", text) From 728d7aa34cabcd2ec56d94ccd86783325446e0d0 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Fri, 30 Oct 2026 05:12:00 -0400 Subject: [PATCH 44/49] fix(pad): let harness input answer system dialogs APS5_PAD_INPUT_FILE input was merged only into guest pad reads. While a modal dialog or the IME held the pad, the title saw the intercepted state and the dialog saw only host SDL input, and without a controller video out did not publish slot 0 at all. A message dialog opened by the title (Void Terrarium++ "System data not found" on a fresh profile) could then never be answered in a scripted run. Merge the injected input into the host state handed to the capturing dialog, publish slot 0 every frame while the pad is captured, and once more when the capture ends so the title stops reading the intercepted state. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libScePad/include/PadState.hpp | 1 + core/libs/prx/libScePad/src/PadState.cpp | 8 +++ .../prx/libSceVideoOut/include/PadInput.hpp | 1 + core/libs/prx/libSceVideoOut/src/PadInput.cpp | 5 +- core/libs/prx/libSceVideoOut/tests/PadSdl.cpp | 51 +++++++++++++++++++ 5 files changed, 65 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libScePad/include/PadState.hpp b/core/libs/prx/libScePad/include/PadState.hpp index 365627eb3a..1dfc108fcc 100644 --- a/core/libs/prx/libScePad/include/PadState.hpp +++ b/core/libs/prx/libScePad/include/PadState.hpp @@ -80,6 +80,7 @@ bool IsConnected(int port); extern "C" void PadPublishInput_nid_postfix(const PadInputState& input); extern "C" void PadPublishInputForPort_nid_postfix(int port, const PadInputState& input); +extern "C" void PadApplyInjectedInput_nid_postfix(PadInputState* state); extern "C" void PadReportInputFailure_nid_postfix(std::exception_ptr error); extern "C" bool PadFetchOutput_nid_postfix(std::uint32_t* seenSequence, PadOutputState* out); extern "C" bool PadFetchOutputForPort_nid_postfix(int port, std::uint32_t* seenSequence, PadOutputState* out); diff --git a/core/libs/prx/libScePad/src/PadState.cpp b/core/libs/prx/libScePad/src/PadState.cpp index 2b57276eed..65618584ee 100644 --- a/core/libs/prx/libScePad/src/PadState.cpp +++ b/core/libs/prx/libScePad/src/PadState.cpp @@ -505,6 +505,14 @@ extern "C" void PadPublishInput_nid_postfix(const PadInputState& input) { PadPublishInputForPort_nid_postfix(0, input); } +extern "C" void PadApplyInjectedInput_nid_postfix(PadInputState* state) { + if (state == nullptr) return; + std::lock_guard lock(stateMutex); + const auto injected = InjectedInput(sceKernelGetProcessTime()); + state->buttons |= injected.buttons; + if (injected.hasSticks) state->sticks = injected.sticks; +} + extern "C" void PadReportInputFailure_nid_postfix(std::exception_ptr error) { if (!error) throw std::invalid_argument("Pad: missing input failure"); std::lock_guard lock(stateMutex); diff --git a/core/libs/prx/libSceVideoOut/include/PadInput.hpp b/core/libs/prx/libSceVideoOut/include/PadInput.hpp index 6e7441138f..027eee925e 100644 --- a/core/libs/prx/libSceVideoOut/include/PadInput.hpp +++ b/core/libs/prx/libSceVideoOut/include/PadInput.hpp @@ -62,6 +62,7 @@ private: std::array slots{}; VideoOutTelemetry::PadChangeFilter telemetryFilter; AgcDriver::PadStatusProvider statusProvider; + bool padCaptured = false; }; #endif diff --git a/core/libs/prx/libSceVideoOut/src/PadInput.cpp b/core/libs/prx/libSceVideoOut/src/PadInput.cpp index 4a3c1b8185..922b4ced42 100644 --- a/core/libs/prx/libSceVideoOut/src/PadInput.cpp +++ b/core/libs/prx/libSceVideoOut/src/PadInput.cpp @@ -357,7 +357,9 @@ void PadInput::Update(const DisplayWindow& window) { return; } const bool changed = keyboardMouse.Advance(std::chrono::steady_clock::now()); - if (slots[0].controller != nullptr || keyboardMouse.MouseMode() || changed) { + const bool captured = ImeCapturesPad_nid_postfix() || ModalDialogCapturesInput_nid_postfix(); + const bool captureChanged = std::exchange(padCaptured, captured) != captured; + if (slots[0].controller != nullptr || keyboardMouse.MouseMode() || changed || captured || captureChanged) { publish(window.FrameNumber(), 0); } for (int slot = 1; slot < PAD_MAX_CONTROLLERS; ++slot) { @@ -377,6 +379,7 @@ PadInputState PadInput::handOff(int slot, PadInputState state) { auto& held = slots[slot].heldThroughCapture; const bool imeCaptures = ImeCapturesPad_nid_postfix(); if (imeCaptures || ModalDialogCapturesInput_nid_postfix()) { + if (slot == 0) PadApplyInjectedInput_nid_postfix(&state); if (slot == 0 && imeCaptures) ImePublishPad_nid_postfix({state.buttons, state.sticks[0], state.sticks[1], state.analogButtonsL2}); else if (slot == 0) ModalDialogPublishPad_nid_postfix({state.buttons, state.sticks[0], state.sticks[1], state.analogButtonsL2}); held = state.buttons; diff --git a/core/libs/prx/libSceVideoOut/tests/PadSdl.cpp b/core/libs/prx/libSceVideoOut/tests/PadSdl.cpp index 894b928bf8..489eb45c5b 100644 --- a/core/libs/prx/libSceVideoOut/tests/PadSdl.cpp +++ b/core/libs/prx/libSceVideoOut/tests/PadSdl.cpp @@ -7,8 +7,13 @@ #include "prx/libScePad/include/PadState.hpp" #include "prx/libSceIme/include/ImeInput.hpp" #include "prx/libSceIme/include/ModalDialog.hpp" +#include #include #include +#include +#include +#include +#include #include #undef main @@ -113,6 +118,40 @@ void HandOff(PadInput& padInput, const DisplayWindow& window, int device) { ModalDialogSetPresenter_nid_postfix(false); } +void InjectedAnswer(PadInput& padInput, const DisplayWindow& window, const std::filesystem::path& commandPath) { + constexpr std::uint32_t Cross = 0x4000; + constexpr std::uint32_t Intercepted = 0x80000000; + ModalDialogSetPresenter_nid_postfix(true); + ModalDialogRequest request; + request.message = u"Create new data?"; + request.buttons = {u"OK"}; + Require(ModalDialogOpen_nid_postfix(request)); + padInput.Update(window); + Require(Pad::ReadState(0).buttons == Intercepted); + std::ofstream(commandPath) << "1 16384 400\n"; + std::size_t chosen = 9; + ModalDialogState state = ModalDialogState::Showing; + for (int frame = 0; frame < 200 && state == ModalDialogState::Showing; ++frame) { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + padInput.Update(window); + Require(Pad::ReadState(0).buttons == Intercepted); + state = ModalDialogPoll_nid_postfix(chosen); + } + Require(state == ModalDialogState::Answered && chosen == 0); + padInput.Update(window); + Require((Pad::ReadState(0).buttons & Intercepted) == 0); + std::ofstream(commandPath) << "2 16384 400\n"; + bool reached = false; + for (int frame = 0; frame < 200 && !reached; ++frame) { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + padInput.Update(window); + reached = Pad::ReadState(0).buttons == Cross; + } + Require(reached); + ModalDialogReset_nid_postfix(); + ModalDialogSetPresenter_nid_postfix(false); +} + } int main() { @@ -123,6 +162,17 @@ int main() { return 1; #endif + const auto commandPath = std::filesystem::temp_directory_path() / ("anyps5-pad-sdl-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + struct Cleanup { + std::filesystem::path path; + ~Cleanup() { std::error_code error; std::filesystem::remove(path, error); } + } cleanup{commandPath}; +#ifdef _WIN32 + Require(_wputenv_s(L"APS5_PAD_INPUT_FILE", commandPath.c_str()) == 0); +#else + Require(setenv("APS5_PAD_INPUT_FILE", commandPath.string().c_str(), 1) == 0); +#endif + Require(SDL_Init(SDL_INIT_GAMECONTROLLER | SDL_INIT_JOYSTICK) == 0); const int hostJoysticks = SDL_NumJoysticks(); @@ -199,6 +249,7 @@ int main() { padInput.HandleEvent(rem0, window); Require(!padInput.SlotOccupied(0)); Require(!padInput.SlotOccupied(1)); + InjectedAnswer(padInput, window, commandPath); Require(SDL_JoystickDetachVirtual(dev1) == 0); Require(SDL_JoystickDetachVirtual(dev0) == 0); From 9624b77bc6a284816aa7fed750c3e7be08d4234f Mon Sep 17 00:00:00 2001 From: edubox Date: Wed, 28 Oct 2026 09:35:38 +0100 Subject: [PATCH 45/49] feat(http2): inflate gzip response bodies --- core/libs/prx/libSceHttp2/CMakeLists.txt | 9 ++-- core/libs/prx/libSceHttp2/Export.cpp | 2 +- core/libs/prx/libSceHttp2/Transport.cpp | 55 ++++++++++++++++++++++++ core/libs/prx/libSceHttp2/Transport.hpp | 1 + core/libs/tests/GuestHttp2Transport.cpp | 35 ++++++++++++++- docs/dev/debt/libSceHttp2.md | 1 + 6 files changed, 98 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceHttp2/CMakeLists.txt b/core/libs/prx/libSceHttp2/CMakeLists.txt index 15d6efc930..45e3b07081 100644 --- a/core/libs/prx/libSceHttp2/CMakeLists.txt +++ b/core/libs/prx/libSceHttp2/CMakeLists.txt @@ -22,10 +22,13 @@ if(MINGW OR CMAKE_SYSTEM_NAME STREQUAL "Linux") endif() install(FILES ${CMAKE_SOURCE_DIR}/3rdparty/nghttp2/COPYING DESTINATION licenses/nghttp2) -add_library(anyps5_http2_transport STATIC EXCLUDE_FROM_ALL Transport.cpp) -target_include_directories(anyps5_http2_transport PRIVATE ${LIBS_INCLUDE_DIR}) +set(http2ZlibDir ${CMAKE_SOURCE_DIR}/3rdparty/libjpeg-turbo/src/spng/zlib) +add_library(anyps5_http2_transport STATIC EXCLUDE_FROM_ALL Transport.cpp + ${http2ZlibDir}/adler32.c ${http2ZlibDir}/crc32.c ${http2ZlibDir}/inffast.c + ${http2ZlibDir}/inflate.c ${http2ZlibDir}/inftrees.c ${http2ZlibDir}/zutil.c) +target_include_directories(anyps5_http2_transport PRIVATE ${LIBS_INCLUDE_DIR} ${http2ZlibDir}) target_link_libraries(anyps5_http2_transport PRIVATE libSceNet libSceSsl libkernel libc nghttp2_static) -set_target_properties(anyps5_http2_transport PROPERTIES POSITION_INDEPENDENT_CODE ON CXX_VISIBILITY_PRESET hidden) +set_target_properties(anyps5_http2_transport PROPERTIES POSITION_INDEPENDENT_CODE ON CXX_VISIBILITY_PRESET hidden C_VISIBILITY_PRESET hidden) if(MINGW OR CMAKE_SYSTEM_NAME STREQUAL "Linux") target_link_options(anyps5_http2_transport INTERFACE LINKER:--exclude-libs,libanyps5_http2_transport.a) endif() diff --git a/core/libs/prx/libSceHttp2/Export.cpp b/core/libs/prx/libSceHttp2/Export.cpp index a87518e163..da8ca45a89 100644 --- a/core/libs/prx/libSceHttp2/Export.cpp +++ b/core/libs/prx/libSceHttp2/Export.cpp @@ -254,7 +254,7 @@ int APS5_VABI sceHttp2SetConnectTimeOut(int id, uint32_t usec) { } int APS5_VABI sceHttp2SetInflateGZIPEnabled(int id, int enable) { - return Update(id, __func__, [&](auto&) { if (enable != 0) NotImplemented_nid_no_patch("sceHttp2SetInflateGZIPEnabled"); }); + return Update(id, __func__, [&](auto& settings) { settings.inflateGzip = enable != 0; }); } int APS5_VABI sceHttp2SetRecvTimeOut(int id, uint32_t usec) { diff --git a/core/libs/prx/libSceHttp2/Transport.cpp b/core/libs/prx/libSceHttp2/Transport.cpp index 52aab59d91..086c2e2055 100644 --- a/core/libs/prx/libSceHttp2/Transport.cpp +++ b/core/libs/prx/libSceHttp2/Transport.cpp @@ -8,6 +8,8 @@ #endif #include "Transport.hpp" + +#include "zlib.h" #include "prx/libSceNet/include/Net.hpp" #include "prx/libSceSsl/include/Ssl.hpp" #include "prx/libc/include/General.hpp" @@ -254,6 +256,8 @@ struct Request::State { bool ready = false; bool ended = false; int status = 0; + std::string contentEncoding; + bool inflatedGzip = false; int blockStatus = 0; int networkResult = 0; int socket = -1; @@ -318,6 +322,55 @@ struct Request::State { } } + void InflateGzip() { + if (inflatedGzip || !settings.inflateGzip || contentEncoding != "gzip" || body.empty()) return; + inflatedGzip = true; + const std::uint8_t* data = body.data(); + const std::size_t size = body.size(); + Require(size >= 18 && data[0] == 0x1f && data[1] == 0x8b && data[2] == 8, "sceHttp2: malformed gzip header"); + const std::uint8_t flags = data[3]; + Require((flags & 0xe0) == 0, "sceHttp2: unknown gzip header flags"); + std::size_t offset = 10; + const auto skip = [&](std::size_t count) { Require(offset + count <= size - 8, "sceHttp2: truncated gzip header"); offset += count; }; + if (flags & 4) { + Require(offset + 2 <= size - 8, "sceHttp2: truncated gzip extra field"); + const std::size_t extra = static_cast(data[offset]) | (static_cast(data[offset + 1]) << 8); + skip(2 + extra); + } + if (flags & 8) while (skip(1), data[offset - 1] != 0) {} + if (flags & 16) while (skip(1), data[offset - 1] != 0) {} + if (flags & 2) skip(2); + z_stream inflater{}; + Require(inflateInit2(&inflater, -15) == Z_OK, "sceHttp2: gzip inflate initialization failed"); + inflater.next_in = const_cast(data + offset); + inflater.avail_in = static_cast(size - offset - 8); + std::vector output; + std::uint8_t chunk[16384]; + int result = Z_OK; + while (result != Z_STREAM_END) { + inflater.next_out = chunk; + inflater.avail_out = sizeof(chunk); + result = inflate(&inflater, Z_NO_FLUSH); + Require(result == Z_OK || result == Z_STREAM_END, "sceHttp2: gzip inflate failed"); + output.insert(output.end(), chunk, chunk + (sizeof(chunk) - inflater.avail_out)); + Require(result == Z_STREAM_END || inflater.avail_in != 0 || inflater.avail_out == 0, "sceHttp2: truncated gzip stream"); + Require(output.size() <= 64 * 1024 * 1024, "sceHttp2: inflated response exceeds the host limit"); + } + Require(inflater.avail_in == 0, "sceHttp2: concatenated gzip members are not implemented"); + inflateEnd(&inflater); + const auto readLe32 = [&](std::size_t position) { + return static_cast(data[position]) | (static_cast(data[position + 1]) << 8) | + (static_cast(data[position + 2]) << 16) | (static_cast(data[position + 3]) << 24); + }; + Require(crc32(0, output.data(), static_cast(output.size())) == readLe32(size - 8), "sceHttp2: gzip checksum mismatch"); + Require(static_cast(output.size()) == readLe32(size - 4), "sceHttp2: gzip size mismatch"); + body = std::move(output); + bodyOffset = 0; + contentLength = body.size(); + hasLength = true; + contentEncoding.clear(); + } + int Connect() { #ifdef _WIN32 const int startup = sceNetSocket("http2-resolver", 2, 1, 0); @@ -412,6 +465,7 @@ struct Request::State { state.contentLength = Number(text); state.hasLength = true; } + if (key == "content-encoding") state.contentEncoding = Lower(std::string(text)); }); } @@ -433,6 +487,7 @@ struct Request::State { (frame->hd.flags & NGHTTP2_FLAG_END_STREAM)) { Require(state.ready, "sceHttp2: response ended without final headers"); state.ended = true; + state.InflateGzip(); } }); } diff --git a/core/libs/prx/libSceHttp2/Transport.hpp b/core/libs/prx/libSceHttp2/Transport.hpp index 5131206d6c..1595ab1048 100644 --- a/core/libs/prx/libSceHttp2/Transport.hpp +++ b/core/libs/prx/libSceHttp2/Transport.hpp @@ -22,6 +22,7 @@ struct Settings { std::vector> headers; std::uint64_t contentLength = 0; bool noContentLength = false; + bool inflateGzip = false; bool auth = true; int redirect = 1; std::uint32_t verifyFlags = 0x3d; diff --git a/core/libs/tests/GuestHttp2Transport.cpp b/core/libs/tests/GuestHttp2Transport.cpp index 1c25112829..091fef1c13 100644 --- a/core/libs/tests/GuestHttp2Transport.cpp +++ b/core/libs/tests/GuestHttp2Transport.cpp @@ -52,7 +52,7 @@ bool Throws(TCall call) { return false; } -enum class Mode { Normal, Large, NoLength, Head, NotFound, Redirect, Auth, Reset, Truncate, StallHandshake, StallHeaders, StallBody, NoAlpn, Informational, StallUpload, EarlyFinal, EarlyReset, EarlyEmpty, EarlyLarge, EarlyResetIncomplete }; +enum class Mode { Normal, Large, NoLength, Head, NotFound, Redirect, Auth, Reset, Truncate, Gzip, StallHandshake, StallHeaders, StallBody, NoAlpn, Informational, StallUpload, EarlyFinal, EarlyReset, EarlyEmpty, EarlyLarge, EarlyResetIncomplete }; const char* Protocols[] = {"h2", nullptr}; class Server { @@ -194,6 +194,14 @@ private: auto length = std::to_string(body.size()); if (mode == Mode::Head) body.clear(); if (mode == Mode::Truncate) body = "short"; + if (mode == Mode::Gzip) { + static const unsigned char compressed[] = { + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x2b, 0x4a, 0x2d, 0x2e, 0xc8, 0xcf, + 0x2b, 0x4e, 0x55, 0x48, 0xca, 0x4f, 0xa9, 0x04, 0x00, 0x36, 0x76, 0x8b, 0x0c, 0x0d, 0x00, 0x00, 0x00, + }; + body.assign(reinterpret_cast(compressed), sizeof(compressed)); + length = std::to_string(body.size()); + } if (mode == Mode::Informational) { nghttp2_nv early{reinterpret_cast(const_cast(":status")), reinterpret_cast(const_cast("103")), 7, 3, 0}; Require(nghttp2_submit_headers(session, 0, stream, nullptr, &early, 1, nullptr) == 0, "server informational response"); @@ -201,6 +209,7 @@ private: std::vector> fields{{":status", status}, {"x-loopback", "yes"}}; if (mode != Mode::NoLength) fields.emplace_back("content-length", length); if (mode == Mode::Redirect) fields.emplace_back("location", "/another"); + if (mode == Mode::Gzip) fields.emplace_back("content-encoding", "gzip"); std::vector response; for (auto& [name, value] : fields) response.push_back({reinterpret_cast(name.data()), reinterpret_cast(value.data()), name.size(), value.size(), 0}); nghttp2_data_provider2 provider{}; @@ -355,6 +364,29 @@ void Success(int ssl, Mode mode, bool disableCa = false) { Require(Throws([&] { sceHttp2SendRequest(client.request, nullptr, 0); }), "unsupported reuse throws"); } +void Gzip(int ssl) { + std::fprintf(stderr, "HTTP2 gzip scenario\n"); + for (const int enabled : {1, 0}) { + Server server(Mode::Gzip); + Client client(ssl); + Require(sceHttp2SetInflateGZIPEnabled(client.tmpl, enabled) == 0, "gzip option"); + client.Create(server.Url()); + Require(sceHttp2SendRequest(client.request, nullptr, 0) == 0, "gzip send"); + int kind = -1; + std::uint64_t length = 0; + Require(sceHttp2GetResponseContentLength(client.request, &kind, &length) == 0, "gzip length"); + std::array buffer{}; + const int count = sceHttp2ReadData(client.request, buffer.data(), buffer.size()); + if (enabled) { + Require(kind == 0 && length == 13 && count == 13 && std::string(buffer.data(), 13) == "response body", "gzip inflated body"); + } else { + Require(kind == 0 && length == 33 && count == 33 && static_cast(buffer[0]) == 0x1f && + static_cast(buffer[1]) == 0x8b, "gzip passthrough body"); + } + server.Join(); + } +} + void Failure(int ssl, Mode mode, bool verifyName = false) { std::fprintf(stderr, "HTTP2 failure scenario %d\n", static_cast(mode)); Server server(mode); @@ -586,6 +618,7 @@ int main(int argc, char** argv) { } else { Require(argc == 1, "test arguments"); for (const auto mode : {Mode::Normal, Mode::Large, Mode::NoLength, Mode::Head, Mode::NotFound, Mode::Redirect, Mode::Auth, Mode::Informational}) Success(ssl, mode); + Gzip(ssl); for (const auto mode : {Mode::NoAlpn, Mode::Reset, Mode::Truncate, Mode::Redirect, Mode::Auth}) Failure(ssl, mode); Failure(ssl, Mode::Normal, true); for (const auto mode : {Mode::StallHandshake, Mode::StallHeaders, Mode::StallBody, Mode::StallUpload}) { diff --git a/docs/dev/debt/libSceHttp2.md b/docs/dev/debt/libSceHttp2.md index 600830fa6e..36ee776c61 100644 --- a/docs/dev/debt/libSceHttp2.md +++ b/docs/dev/debt/libSceHttp2.md @@ -33,6 +33,7 @@ - [libSceHttp2 TLS settings](../../../core/libs/prx/libSceHttp2/Export.cpp) - minimum SSL versions outside 2 to 5, nonnull SSL callbacks and flags outside `0x3d` throw. Supported certificate-check bits follow SharpProspero and use libSceSsl's TLS 1.2 backend and CA context; native defaults and error translation are unverified. Settings on object kinds other than templates/requests throw, except for the request options below; native object-kind restrictions are otherwise unverified. Settings become immutable when transport is created. Zero timeout values, negative resolve retries, gzip inflation and connection-slot wait timeouts throw; a failed lookup is retried up to the set count. Host HTTP allocations are not charged to the context pool, so `sceHttp2GetMemoryPoolStats` reports a live context's pool size with zero peak and current use. PPSA01494, PPSA30839 and PPSA31162 read the three sizes for memory logging without checking the result, and stopped at startup while it threw. - [sceHttp2SetMinSslVersion](../../../core/libs/prx/libSceHttp2/Export.cpp) - libSceHttp2's own range and validation order are not measured: versions 2 (SSL 3.0) to 5 (TLS 1.2) are accepted, the range `sceSslSetMinSslVersion` takes on a 13.40 console ([libSceSsl](libSceSsl.md)). The floor is not kept: libSceSsl negotiates TLS 1.2 only, which meets every accepted floor, so a server limited to versions older than TLS 1.2 fails the handshake whatever the floor; what a lower floor does with such a server on a console is not measured. - [libSceHttp2 request options](../../../core/libs/prx/libSceHttp2/Export.cpp) - extends the TLS settings entry: the auth and redirect flags, headers and the auth-info and pre-send callbacks live in `Http2::Settings`, are copied into each new request and become immutable once transport exists; the getters still read them then. Accepted kinds, defaults, copying, header modes, error codes and their order follow the 13.42 module run in an emulator ([#10536](https://github.com/boykopovar/AnyPS5/pull/10536)), not a console. A null out-pointer to `sceHttp2GetAuthEnabled` is written through on that module; here it throws. The pre-send callback type is unknown. The transport never calls either callback, and the auth cache flush only validates the context +- [libSceHttp2 gzip responses](../../../core/libs/prx/libSceHttp2/Transport.cpp) - `sceHttp2SetInflateGZIPEnabled` stores the flag. When it is set and the response carries `content-encoding: gzip`, the transport inflates the body with the vendored zlib before `sceHttp2ReadData` and `sceHttp2GetResponseContentLength` see it, and reports the inflated length; both calls wait for END_STREAM while the enabled response is gzip. The transport neither adds nor inspects `Accept-Encoding`, so the guest supplies that request header and an unsolicited gzip response is inflated too. Whether a console rewrites the header block (Content-Encoding and Content-Length) is unverified, so the serialized headers keep the fields the server sent; other encodings are not inflated, a malformed or truncated stream, a bad checksum or size and concatenated members throw, and the optional gzip header CRC is not verified. - [sceHttp2GetAllResponseHeaders](../../../core/libs/prx/libSceHttp2/Export.cpp) throws: the serialization, size and lifetime of a decoded HTTP/2 header block have not been established. Status and Content-Length fields are decoded; absent Content-Length reports `NotFound`, following SharpProspero. - [libSceHttp2 asynchronous requests](../../../core/libs/prx/libSceHttp2/Export.cpp) - send/read submission and `sceHttp2WaitAsync` throw. The previous offline implementation queued a fabricated network failure and used event type zero; public sources do not establish the completion event types, wait timeout updates, cancellation results or ownership of async buffers. PPSA17221's kqueue layout and PPSA15508's 24-byte wait result establish caller ABI only. No completion or equeue event is manufactured. From bd68b64dd1f753080108c47bfbc69d1a8f9d4763 Mon Sep 17 00:00:00 2001 From: Eduardo Ruiz Date: Wed, 28 Oct 2026 11:06:36 +0100 Subject: [PATCH 46/49] fix(http2): inflate gzip responses after END_STREAM --- core/libs/prx/libSceHttp2/Transport.cpp | 17 +++++-- core/libs/tests/GuestHttp2Transport.cpp | 59 +++++++++++++++++++++++-- docs/dev/debt/libSceHttp2.md | 7 ++- 3 files changed, 73 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libSceHttp2/Transport.cpp b/core/libs/prx/libSceHttp2/Transport.cpp index 086c2e2055..444d789167 100644 --- a/core/libs/prx/libSceHttp2/Transport.cpp +++ b/core/libs/prx/libSceHttp2/Transport.cpp @@ -322,8 +322,10 @@ struct Request::State { } } + bool Inflating() const { return settings.inflateGzip && contentEncoding == "gzip"; } + void InflateGzip() { - if (inflatedGzip || !settings.inflateGzip || contentEncoding != "gzip" || body.empty()) return; + if (inflatedGzip || !Inflating() || body.empty()) return; inflatedGzip = true; const std::uint8_t* data = body.data(); const std::size_t size = body.size(); @@ -342,6 +344,10 @@ struct Request::State { if (flags & 2) skip(2); z_stream inflater{}; Require(inflateInit2(&inflater, -15) == Z_OK, "sceHttp2: gzip inflate initialization failed"); + struct End { + z_stream* stream; + ~End() { inflateEnd(stream); } + } end{&inflater}; inflater.next_in = const_cast(data + offset); inflater.avail_in = static_cast(size - offset - 8); std::vector output; @@ -357,7 +363,6 @@ struct Request::State { Require(output.size() <= 64 * 1024 * 1024, "sceHttp2: inflated response exceeds the host limit"); } Require(inflater.avail_in == 0, "sceHttp2: concatenated gzip members are not implemented"); - inflateEnd(&inflater); const auto readLe32 = [&](std::size_t position) { return static_cast(data[position]) | (static_cast(data[position + 1]) << 8) | (static_cast(data[position + 2]) << 16) | (static_cast(data[position + 3]) << 24); @@ -653,7 +658,7 @@ int Request::Read(void* data, std::size_t size) { try { const auto deadline = state->Deadline(state->settings.recvTimeout); state->Check(deadline); - while (state->bodyOffset == state->body.size() && !state->ended) state->Pump(deadline); + while (!state->ended && (state->bodyOffset == state->body.size() || state->Inflating())) state->Pump(deadline); const auto count = std::min({size, state->body.size() - state->bodyOffset, static_cast(std::numeric_limits::max())}); state->Check(deadline); if (count) std::memcpy(data, state->body.data() + state->bodyOffset, count); @@ -677,6 +682,12 @@ int Request::ContentLength(int* kind, std::uint64_t* length) { auto lock = state->Lock(); if (state->networkResult) return state->networkResult; Require(kind && length && state->ready, "sceHttp2GetResponseContentLength: null output or response unavailable"); + if (state->Inflating()) { + try { + const auto deadline = state->Deadline(state->settings.recvTimeout); + while (!state->ended) state->Pump(deadline); + } catch (...) { state->Fail(); throw; } + } *kind = state->hasLength ? 0 : 1; *length = state->hasLength ? state->contentLength : 0; return 0; diff --git a/core/libs/tests/GuestHttp2Transport.cpp b/core/libs/tests/GuestHttp2Transport.cpp index 091fef1c13..ca6ff1d9be 100644 --- a/core/libs/tests/GuestHttp2Transport.cpp +++ b/core/libs/tests/GuestHttp2Transport.cpp @@ -52,7 +52,7 @@ bool Throws(TCall call) { return false; } -enum class Mode { Normal, Large, NoLength, Head, NotFound, Redirect, Auth, Reset, Truncate, Gzip, StallHandshake, StallHeaders, StallBody, NoAlpn, Informational, StallUpload, EarlyFinal, EarlyReset, EarlyEmpty, EarlyLarge, EarlyResetIncomplete }; +enum class Mode { Normal, Large, NoLength, Head, NotFound, Redirect, Auth, Reset, Truncate, Gzip, GzipSplit, GzipTruncated, StallHandshake, StallHeaders, StallBody, NoAlpn, Informational, StallUpload, EarlyFinal, EarlyReset, EarlyEmpty, EarlyLarge, EarlyResetIncomplete }; const char* Protocols[] = {"h2", nullptr}; class Server { @@ -137,6 +137,8 @@ private: std::size_t offset = 0; bool responded = false; bool ended = false; + bool splitSent = false; + bool splitResumed = false; void Stage(int value) { { std::lock_guard lock(mutex); stage = value; } @@ -176,7 +178,12 @@ private: std::uint32_t* flags, nghttp2_data_source*, void* data) { auto& server = *static_cast(data); if (server.mode == Mode::StallBody || server.mode == Mode::EarlyResetIncomplete) return NGHTTP2_ERR_DEFERRED; + if (server.mode == Mode::GzipSplit && server.splitSent && !server.splitResumed) return NGHTTP2_ERR_DEFERRED; size = std::min(size, server.body.size() - server.offset); + if (server.mode == Mode::GzipSplit && !server.splitSent) { + size = std::min(size, 8); + server.splitSent = true; + } if (size) std::memcpy(bytes, server.body.data() + server.offset, size); server.offset += size; if (server.offset == server.body.size()) *flags |= NGHTTP2_DATA_FLAG_EOF; @@ -194,12 +201,13 @@ private: auto length = std::to_string(body.size()); if (mode == Mode::Head) body.clear(); if (mode == Mode::Truncate) body = "short"; - if (mode == Mode::Gzip) { + if (mode == Mode::Gzip || mode == Mode::GzipSplit || mode == Mode::GzipTruncated) { static const unsigned char compressed[] = { 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x2b, 0x4a, 0x2d, 0x2e, 0xc8, 0xcf, 0x2b, 0x4e, 0x55, 0x48, 0xca, 0x4f, 0xa9, 0x04, 0x00, 0x36, 0x76, 0x8b, 0x0c, 0x0d, 0x00, 0x00, 0x00, }; body.assign(reinterpret_cast(compressed), sizeof(compressed)); + if (mode == Mode::GzipTruncated) body.resize(body.size() - 4); length = std::to_string(body.size()); } if (mode == Mode::Informational) { @@ -209,7 +217,7 @@ private: std::vector> fields{{":status", status}, {"x-loopback", "yes"}}; if (mode != Mode::NoLength) fields.emplace_back("content-length", length); if (mode == Mode::Redirect) fields.emplace_back("location", "/another"); - if (mode == Mode::Gzip) fields.emplace_back("content-encoding", "gzip"); + if (mode == Mode::Gzip || mode == Mode::GzipSplit || mode == Mode::GzipTruncated) fields.emplace_back("content-encoding", "gzip"); std::vector response; for (auto& [name, value] : fields) response.push_back({reinterpret_cast(name.data()), reinterpret_cast(value.data()), name.size(), value.size(), 0}); nghttp2_data_provider2 provider{}; @@ -268,6 +276,12 @@ private: } if (!sent) break; } + if (mode == Mode::GzipSplit && splitSent && !splitResumed) { + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + Require(nghttp2_session_resume_data(session, 1) == 0, "resume split gzip body"); + splitResumed = true; + continue; + } if (IsEarly() && responded && sent) { Stage(2); if (!resetSent && (mode == Mode::EarlyReset || mode == Mode::EarlyResetIncomplete)) { @@ -387,6 +401,43 @@ void Gzip(int ssl) { } } +void GzipSplit(int ssl) { + std::fprintf(stderr, "HTTP2 gzip split scenario\n"); + Server server(Mode::GzipSplit); + Client client(ssl); + Require(sceHttp2SetInflateGZIPEnabled(client.tmpl, 1) == 0, "gzip split option"); + client.Create(server.Url()); + Require(sceHttp2SendRequest(client.request, nullptr, 0) == 0, "gzip split send"); + int kind = -1; + std::uint64_t length = 0; + Require(sceHttp2GetResponseContentLength(client.request, &kind, &length) == 0, "gzip split length"); + Require(kind == 0 && length == 13, "gzip split inflated length"); + std::string body; + std::array buffer; + for (;;) { + const int count = sceHttp2ReadData(client.request, buffer.data(), buffer.size()); + Require(count >= 0 && count <= static_cast(buffer.size()), "gzip split read count"); + if (!count) break; + body.append(buffer.data(), static_cast(count)); + } + Require(body == "response body", "gzip split inflated body"); + server.Join(); +} + +void GzipFailure(int ssl) { + std::fprintf(stderr, "HTTP2 gzip truncated scenario\n"); + Server server(Mode::GzipTruncated); + Client client(ssl); + Require(sceHttp2SetInflateGZIPEnabled(client.tmpl, 1) == 0, "gzip truncated option"); + client.Create(server.Url()); + Require(Throws([&] { + sceHttp2SendRequest(client.request, nullptr, 0); + std::array buffer; + sceHttp2ReadData(client.request, buffer.data(), buffer.size()); + }), "truncated gzip throws"); + server.Join(); +} + void Failure(int ssl, Mode mode, bool verifyName = false) { std::fprintf(stderr, "HTTP2 failure scenario %d\n", static_cast(mode)); Server server(mode); @@ -619,6 +670,8 @@ int main(int argc, char** argv) { Require(argc == 1, "test arguments"); for (const auto mode : {Mode::Normal, Mode::Large, Mode::NoLength, Mode::Head, Mode::NotFound, Mode::Redirect, Mode::Auth, Mode::Informational}) Success(ssl, mode); Gzip(ssl); + GzipSplit(ssl); + GzipFailure(ssl); for (const auto mode : {Mode::NoAlpn, Mode::Reset, Mode::Truncate, Mode::Redirect, Mode::Auth}) Failure(ssl, mode); Failure(ssl, Mode::Normal, true); for (const auto mode : {Mode::StallHandshake, Mode::StallHeaders, Mode::StallBody, Mode::StallUpload}) { diff --git a/docs/dev/debt/libSceHttp2.md b/docs/dev/debt/libSceHttp2.md index 36ee776c61..9f5bf3d611 100644 --- a/docs/dev/debt/libSceHttp2.md +++ b/docs/dev/debt/libSceHttp2.md @@ -29,11 +29,10 @@ - [Default TLS trust](../../../core/libs/prx/libSceHttp2/Transport.cpp) (libSceHttp2) - HTTPS connections also trust the system CA list, `//common/cert/CA_LIST.cer`, served by libkernel from the host trust store, after the title's own libSceSsl certificates. RetroArch (PPSA99169, [updater](https://github.com/mihawk-99/PS5_RetroArch/blob/7268510b12fe7461342e0cc4497c87a0910c1055/src/webui_update.cpp#L302-L312)), PS5LM (PPSA99575) and Porpoise (PPSA99764) send version-3 requests to public `https://` servers with no loaded certificate, verify callback or verification option, which pass the known-CA check only if the console trusts public roots by default. That the console's roots are exactly this file is not measured - [libSceHttp2 transport](../../../core/libs/prx/libSceHttp2/Transport.cpp) - only synchronous HTTPS requests with version 3 and ALPN `h2` are supported. HTTP/1.x, cleartext HTTP/2, negotiation fallback, connection reuse, multiplexing across requests, CONNECT, response trailers, automatic redirects and authentication challenges throw. Version values, method strings, header overwrite/append modes, status/read/content-length signatures and timeout units follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Http2.cs); wire framing and negotiation follow [RFC 9113 sections 3, 5, 6 and 8](https://www.rfc-editor.org/rfc/rfc9113.html). [prosper](https://github.com/mattias800/prosper/blob/b1bec57186a0/prosper/src/hle/net/hle_http2.cpp) instead records the method argument as an integer; the string signature follows SharpProspero. These are public compatibility contracts, not console measurements. -- [libSceHttp2 error paths](../../../core/libs/prx/libSceHttp2/Transport.cpp) - cancellation, deadlines, DNS/TLS/protocol failures, malformed inputs, concurrent calls on one request and exhausted connection slots throw because their console error mappings are unknown. Socket connect errors propagate the libSceNet result, following the module-analysis report in [prosper #2894](https://github.com/mattias800/prosper/issues/2894); the report does not establish every exported error path. A request uses one connection; a context's concurrency cap counts connections until EOF, failure or deletion. Native pooling, scheduling, validation order and teardown semantics remain unverified. DNS uses two host workers and a queue of eight; cancellation/deadlines abandon the caller's wait, not libc's lookup. Library unload can wait for a host lookup to return. A resolve retry resubmits a failed lookup with a new timeout; a timed-out lookup isn't retried. The console's retry semantics are unverified. -- [libSceHttp2 TLS settings](../../../core/libs/prx/libSceHttp2/Export.cpp) - minimum SSL versions outside 2 to 5, nonnull SSL callbacks and flags outside `0x3d` throw. Supported certificate-check bits follow SharpProspero and use libSceSsl's TLS 1.2 backend and CA context; native defaults and error translation are unverified. Settings on object kinds other than templates/requests throw, except for the request options below; native object-kind restrictions are otherwise unverified. Settings become immutable when transport is created. Zero timeout values, negative resolve retries, gzip inflation and connection-slot wait timeouts throw; a failed lookup is retried up to the set count. Host HTTP allocations are not charged to the context pool, so `sceHttp2GetMemoryPoolStats` reports a live context's pool size with zero peak and current use. PPSA01494, PPSA30839 and PPSA31162 read the three sizes for memory logging without checking the result, and stopped at startup while it threw. -- [sceHttp2SetMinSslVersion](../../../core/libs/prx/libSceHttp2/Export.cpp) - libSceHttp2's own range and validation order are not measured: versions 2 (SSL 3.0) to 5 (TLS 1.2) are accepted, the range `sceSslSetMinSslVersion` takes on a 13.40 console ([libSceSsl](libSceSsl.md)). The floor is not kept: libSceSsl negotiates TLS 1.2 only, which meets every accepted floor, so a server limited to versions older than TLS 1.2 fails the handshake whatever the floor; what a lower floor does with such a server on a console is not measured. +- [libSceHttp2 error paths](../../../core/libs/prx/libSceHttp2/Transport.cpp) - cancellation, deadlines, DNS/TLS/protocol failures, malformed inputs, concurrent calls on one request and exhausted connection slots throw because their console error mappings are unknown. Socket connect errors propagate the libSceNet result, following the module-analysis report in [prosper #2894](https://github.com/mattias800/prosper/issues/2894); the report does not establish every exported error path. A request uses one connection; a context's concurrency cap counts connections until EOF, failure or deletion. Native pooling, scheduling, validation order and teardown semantics remain unverified. DNS uses two host workers and a queue of eight; cancellation/deadlines abandon the caller's wait, not libc's lookup. Library unload can wait for a host lookup to return. +- [libSceHttp2 TLS settings](../../../core/libs/prx/libSceHttp2/Export.cpp) - minimum-version selection, nonnull SSL callbacks and flags outside `0x3d` throw. Supported certificate-check bits follow SharpProspero and use libSceSsl's TLS 1.2 backend and CA context; native defaults and error translation are unverified. Settings on object kinds other than templates/requests throw, except for the request options below; native object-kind restrictions are otherwise unverified. Settings become immutable when transport is created. Zero timeout values, nonzero resolve retries, gzip inflation and connection-slot wait timeouts throw. Host HTTP allocations are not charged to the context pool, so `sceHttp2GetMemoryPoolStats` reports a live context's pool size with zero peak and current use. PPSA01494, PPSA30839 and PPSA31162 read the three sizes for memory logging without checking the result, and stopped at startup while it threw. - [libSceHttp2 request options](../../../core/libs/prx/libSceHttp2/Export.cpp) - extends the TLS settings entry: the auth and redirect flags, headers and the auth-info and pre-send callbacks live in `Http2::Settings`, are copied into each new request and become immutable once transport exists; the getters still read them then. Accepted kinds, defaults, copying, header modes, error codes and their order follow the 13.42 module run in an emulator ([#10536](https://github.com/boykopovar/AnyPS5/pull/10536)), not a console. A null out-pointer to `sceHttp2GetAuthEnabled` is written through on that module; here it throws. The pre-send callback type is unknown. The transport never calls either callback, and the auth cache flush only validates the context -- [libSceHttp2 gzip responses](../../../core/libs/prx/libSceHttp2/Transport.cpp) - `sceHttp2SetInflateGZIPEnabled` stores the flag. When it is set and the response carries `content-encoding: gzip`, the transport inflates the body with the vendored zlib before `sceHttp2ReadData` and `sceHttp2GetResponseContentLength` see it, and reports the inflated length; both calls wait for END_STREAM while the enabled response is gzip. The transport neither adds nor inspects `Accept-Encoding`, so the guest supplies that request header and an unsolicited gzip response is inflated too. Whether a console rewrites the header block (Content-Encoding and Content-Length) is unverified, so the serialized headers keep the fields the server sent; other encodings are not inflated, a malformed or truncated stream, a bad checksum or size and concatenated members throw, and the optional gzip header CRC is not verified. +- [libSceHttp2 gzip responses](../../../core/libs/prx/libSceHttp2/Transport.cpp) - `sceHttp2SetInflateGZIPEnabled` stores the flag. When it is set and the response carries `content-encoding: gzip`, the transport inflates the body with the vendored zlib before `sceHttp2ReadData` and `sceHttp2GetResponseContentLength` see it, and reports the inflated length; both calls wait for END_STREAM while the enabled response is gzip. The transport neither adds nor inspects `Accept-Encoding`, so the guest supplies that request header and an unsolicited gzip response is inflated too. Whether a console rewrites the header block (Content-Encoding and Content-Length) is unverified, so the serialized headers keep the fields the server sent; other encodings are not inflated, a malformed or truncated stream and concatenated members throw, the optional gzip header CRC is not verified, and the inflated body is capped at 64 MiB. - [sceHttp2GetAllResponseHeaders](../../../core/libs/prx/libSceHttp2/Export.cpp) throws: the serialization, size and lifetime of a decoded HTTP/2 header block have not been established. Status and Content-Length fields are decoded; absent Content-Length reports `NotFound`, following SharpProspero. - [libSceHttp2 asynchronous requests](../../../core/libs/prx/libSceHttp2/Export.cpp) - send/read submission and `sceHttp2WaitAsync` throw. The previous offline implementation queued a fabricated network failure and used event type zero; public sources do not establish the completion event types, wait timeout updates, cancellation results or ownership of async buffers. PPSA17221's kqueue layout and PPSA15508's 24-byte wait result establish caller ABI only. No completion or equeue event is manufactured. From 58f325624e7e616afbb06fbe68c6d86d4682ff2b Mon Sep 17 00:00:00 2001 From: Eduardo Ruiz Date: Wed, 28 Oct 2026 12:06:45 +0100 Subject: [PATCH 47/49] fix(http2): consume flow control while buffering gzip bodies Reads already wait for END_STREAM when inflation applies, so keep the receive window open while the encoded body is buffered. This lets a gzip response larger than 65,535 bytes inflate instead of failing, and the split test now reads before the content length so it exercises the read path. --- core/libs/prx/libSceHttp2/Transport.cpp | 12 +++- core/libs/tests/GuestHttp2Transport.cpp | 73 +++++++++++++++++++++++-- docs/dev/debt/libSceHttp2.md | 2 +- 3 files changed, 79 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceHttp2/Transport.cpp b/core/libs/prx/libSceHttp2/Transport.cpp index 444d789167..60ba971b06 100644 --- a/core/libs/prx/libSceHttp2/Transport.cpp +++ b/core/libs/prx/libSceHttp2/Transport.cpp @@ -38,6 +38,7 @@ using Clock = std::chrono::steady_clock; constexpr int WantRead = static_cast(0x8095f013); constexpr int WantWrite = static_cast(0x8095f014); constexpr int InvalidValue = static_cast(0x817B11FE); +constexpr std::size_t MaxGzipBytes = 64 * 1024 * 1024; [[noreturn]] void Unknown(const char* message) { NotImplemented_nid_no_patch(message); @@ -360,7 +361,7 @@ struct Request::State { Require(result == Z_OK || result == Z_STREAM_END, "sceHttp2: gzip inflate failed"); output.insert(output.end(), chunk, chunk + (sizeof(chunk) - inflater.avail_out)); Require(result == Z_STREAM_END || inflater.avail_in != 0 || inflater.avail_out == 0, "sceHttp2: truncated gzip stream"); - Require(output.size() <= 64 * 1024 * 1024, "sceHttp2: inflated response exceeds the host limit"); + Require(output.size() <= MaxGzipBytes, "sceHttp2: inflated response exceeds the host limit"); } Require(inflater.avail_in == 0, "sceHttp2: concatenated gzip members are not implemented"); const auto readLe32 = [&](std::size_t position) { @@ -497,11 +498,16 @@ struct Request::State { }); } - static int Data(nghttp2_session*, std::uint8_t, std::int32_t streamId, const std::uint8_t* bytes, std::size_t size, void* data) { + static int Data(nghttp2_session* session, std::uint8_t, std::int32_t streamId, const std::uint8_t* bytes, std::size_t size, void* data) { auto& state = *static_cast(data); return Callback(state, [&] { if (streamId != state.stream) return; - Require(state.body.size() + size <= 65535, "sceHttp2: receive flow-control limit exceeded"); + if (state.Inflating()) { + Require(state.body.size() + size <= MaxGzipBytes, "sceHttp2: encoded gzip response exceeds the host limit"); + CheckNg(nghttp2_session_consume(session, streamId, size)); + } else { + Require(state.body.size() + size <= 65535, "sceHttp2: receive flow-control limit exceeded"); + } state.body.insert(state.body.end(), bytes, bytes + size); }); } diff --git a/core/libs/tests/GuestHttp2Transport.cpp b/core/libs/tests/GuestHttp2Transport.cpp index ca6ff1d9be..ebe87cd057 100644 --- a/core/libs/tests/GuestHttp2Transport.cpp +++ b/core/libs/tests/GuestHttp2Transport.cpp @@ -52,9 +52,41 @@ bool Throws(TCall call) { return false; } -enum class Mode { Normal, Large, NoLength, Head, NotFound, Redirect, Auth, Reset, Truncate, Gzip, GzipSplit, GzipTruncated, StallHandshake, StallHeaders, StallBody, NoAlpn, Informational, StallUpload, EarlyFinal, EarlyReset, EarlyEmpty, EarlyLarge, EarlyResetIncomplete }; +enum class Mode { Normal, Large, NoLength, Head, NotFound, Redirect, Auth, Reset, Truncate, Gzip, GzipSplit, GzipTruncated, GzipLarge, StallHandshake, StallHeaders, StallBody, NoAlpn, Informational, StallUpload, EarlyFinal, EarlyReset, EarlyEmpty, EarlyLarge, EarlyResetIncomplete }; const char* Protocols[] = {"h2", nullptr}; +std::string GzipPayload() { + std::string payload(70000, '\0'); + for (std::size_t i = 0; i < payload.size(); ++i) payload[i] = static_cast((i * 31 + 7) & 0xff); + return payload; +} + +std::string StoredGzip(const std::string& payload) { + static const unsigned char header[] = {0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff}; + std::string body(reinterpret_cast(header), sizeof(header)); + std::size_t offset = 0; + while (offset < payload.size()) { + const std::size_t count = std::min(payload.size() - offset, 65535); + body.push_back(static_cast(offset + count == payload.size() ? 1 : 0)); + body.push_back(static_cast(count & 0xff)); + body.push_back(static_cast((count >> 8) & 0xff)); + body.push_back(static_cast(~count & 0xff)); + body.push_back(static_cast((~count >> 8) & 0xff)); + body.append(payload, offset, count); + offset += count; + } + std::uint32_t crc = 0xffffffffu; + for (const unsigned char byte : payload) { + crc ^= byte; + for (int bit = 0; bit < 8; ++bit) crc = (crc >> 1) ^ (0xedb88320u & (0u - (crc & 1u))); + } + crc = ~crc; + const std::uint32_t size = static_cast(payload.size()); + for (int shift = 0; shift < 32; shift += 8) body.push_back(static_cast((crc >> shift) & 0xff)); + for (int shift = 0; shift < 32; shift += 8) body.push_back(static_cast((size >> shift) & 0xff)); + return body; +} + class Server { public: explicit Server(Mode mode) : mode(mode) { @@ -210,6 +242,10 @@ private: if (mode == Mode::GzipTruncated) body.resize(body.size() - 4); length = std::to_string(body.size()); } + if (mode == Mode::GzipLarge) { + body = StoredGzip(GzipPayload()); + length = std::to_string(body.size()); + } if (mode == Mode::Informational) { nghttp2_nv early{reinterpret_cast(const_cast(":status")), reinterpret_cast(const_cast("103")), 7, 3, 0}; Require(nghttp2_submit_headers(session, 0, stream, nullptr, &early, 1, nullptr) == 0, "server informational response"); @@ -217,7 +253,7 @@ private: std::vector> fields{{":status", status}, {"x-loopback", "yes"}}; if (mode != Mode::NoLength) fields.emplace_back("content-length", length); if (mode == Mode::Redirect) fields.emplace_back("location", "/another"); - if (mode == Mode::Gzip || mode == Mode::GzipSplit || mode == Mode::GzipTruncated) fields.emplace_back("content-encoding", "gzip"); + if (mode == Mode::Gzip || mode == Mode::GzipSplit || mode == Mode::GzipTruncated || mode == Mode::GzipLarge) fields.emplace_back("content-encoding", "gzip"); std::vector response; for (auto& [name, value] : fields) response.push_back({reinterpret_cast(name.data()), reinterpret_cast(value.data()), name.size(), value.size(), 0}); nghttp2_data_provider2 provider{}; @@ -408,12 +444,17 @@ void GzipSplit(int ssl) { Require(sceHttp2SetInflateGZIPEnabled(client.tmpl, 1) == 0, "gzip split option"); client.Create(server.Url()); Require(sceHttp2SendRequest(client.request, nullptr, 0) == 0, "gzip split send"); + std::string body; + std::array buffer; + while (body.size() < 12) { + const int count = sceHttp2ReadData(client.request, buffer.data(), buffer.size()); + Require(count > 0 && count <= static_cast(buffer.size()), "gzip split read count"); + body.append(buffer.data(), static_cast(count)); + } int kind = -1; std::uint64_t length = 0; Require(sceHttp2GetResponseContentLength(client.request, &kind, &length) == 0, "gzip split length"); Require(kind == 0 && length == 13, "gzip split inflated length"); - std::string body; - std::array buffer; for (;;) { const int count = sceHttp2ReadData(client.request, buffer.data(), buffer.size()); Require(count >= 0 && count <= static_cast(buffer.size()), "gzip split read count"); @@ -424,6 +465,29 @@ void GzipSplit(int ssl) { server.Join(); } +void GzipLarge(int ssl) { + std::fprintf(stderr, "HTTP2 gzip large scenario\n"); + Server server(Mode::GzipLarge); + Client client(ssl); + Require(sceHttp2SetInflateGZIPEnabled(client.tmpl, 1) == 0, "gzip large option"); + client.Create(server.Url()); + Require(sceHttp2SendRequest(client.request, nullptr, 0) == 0, "gzip large send"); + int kind = -1; + std::uint64_t length = 0; + Require(sceHttp2GetResponseContentLength(client.request, &kind, &length) == 0, "gzip large length"); + Require(kind == 0 && length == 70000, "gzip large inflated length"); + std::string body; + std::array buffer; + for (;;) { + const int count = sceHttp2ReadData(client.request, buffer.data(), buffer.size()); + Require(count >= 0 && count <= static_cast(buffer.size()), "gzip large read count"); + if (!count) break; + body.append(buffer.data(), static_cast(count)); + } + Require(body == GzipPayload(), "gzip large inflated body"); + server.Join(); +} + void GzipFailure(int ssl) { std::fprintf(stderr, "HTTP2 gzip truncated scenario\n"); Server server(Mode::GzipTruncated); @@ -671,6 +735,7 @@ int main(int argc, char** argv) { for (const auto mode : {Mode::Normal, Mode::Large, Mode::NoLength, Mode::Head, Mode::NotFound, Mode::Redirect, Mode::Auth, Mode::Informational}) Success(ssl, mode); Gzip(ssl); GzipSplit(ssl); + GzipLarge(ssl); GzipFailure(ssl); for (const auto mode : {Mode::NoAlpn, Mode::Reset, Mode::Truncate, Mode::Redirect, Mode::Auth}) Failure(ssl, mode); Failure(ssl, Mode::Normal, true); diff --git a/docs/dev/debt/libSceHttp2.md b/docs/dev/debt/libSceHttp2.md index 9f5bf3d611..9728dbc03d 100644 --- a/docs/dev/debt/libSceHttp2.md +++ b/docs/dev/debt/libSceHttp2.md @@ -32,7 +32,7 @@ - [libSceHttp2 error paths](../../../core/libs/prx/libSceHttp2/Transport.cpp) - cancellation, deadlines, DNS/TLS/protocol failures, malformed inputs, concurrent calls on one request and exhausted connection slots throw because their console error mappings are unknown. Socket connect errors propagate the libSceNet result, following the module-analysis report in [prosper #2894](https://github.com/mattias800/prosper/issues/2894); the report does not establish every exported error path. A request uses one connection; a context's concurrency cap counts connections until EOF, failure or deletion. Native pooling, scheduling, validation order and teardown semantics remain unverified. DNS uses two host workers and a queue of eight; cancellation/deadlines abandon the caller's wait, not libc's lookup. Library unload can wait for a host lookup to return. - [libSceHttp2 TLS settings](../../../core/libs/prx/libSceHttp2/Export.cpp) - minimum-version selection, nonnull SSL callbacks and flags outside `0x3d` throw. Supported certificate-check bits follow SharpProspero and use libSceSsl's TLS 1.2 backend and CA context; native defaults and error translation are unverified. Settings on object kinds other than templates/requests throw, except for the request options below; native object-kind restrictions are otherwise unverified. Settings become immutable when transport is created. Zero timeout values, nonzero resolve retries, gzip inflation and connection-slot wait timeouts throw. Host HTTP allocations are not charged to the context pool, so `sceHttp2GetMemoryPoolStats` reports a live context's pool size with zero peak and current use. PPSA01494, PPSA30839 and PPSA31162 read the three sizes for memory logging without checking the result, and stopped at startup while it threw. - [libSceHttp2 request options](../../../core/libs/prx/libSceHttp2/Export.cpp) - extends the TLS settings entry: the auth and redirect flags, headers and the auth-info and pre-send callbacks live in `Http2::Settings`, are copied into each new request and become immutable once transport exists; the getters still read them then. Accepted kinds, defaults, copying, header modes, error codes and their order follow the 13.42 module run in an emulator ([#10536](https://github.com/boykopovar/AnyPS5/pull/10536)), not a console. A null out-pointer to `sceHttp2GetAuthEnabled` is written through on that module; here it throws. The pre-send callback type is unknown. The transport never calls either callback, and the auth cache flush only validates the context -- [libSceHttp2 gzip responses](../../../core/libs/prx/libSceHttp2/Transport.cpp) - `sceHttp2SetInflateGZIPEnabled` stores the flag. When it is set and the response carries `content-encoding: gzip`, the transport inflates the body with the vendored zlib before `sceHttp2ReadData` and `sceHttp2GetResponseContentLength` see it, and reports the inflated length; both calls wait for END_STREAM while the enabled response is gzip. The transport neither adds nor inspects `Accept-Encoding`, so the guest supplies that request header and an unsolicited gzip response is inflated too. Whether a console rewrites the header block (Content-Encoding and Content-Length) is unverified, so the serialized headers keep the fields the server sent; other encodings are not inflated, a malformed or truncated stream and concatenated members throw, the optional gzip header CRC is not verified, and the inflated body is capped at 64 MiB. +- [libSceHttp2 gzip responses](../../../core/libs/prx/libSceHttp2/Transport.cpp) - `sceHttp2SetInflateGZIPEnabled` stores the flag. When it is set and the response carries `content-encoding: gzip`, the transport inflates the body with the vendored zlib before `sceHttp2ReadData` and `sceHttp2GetResponseContentLength` see it, and reports the inflated length; both calls wait for END_STREAM while the enabled response is gzip. The transport neither adds nor inspects `Accept-Encoding`, so the guest supplies that request header and an unsolicited gzip response is inflated too. Whether a console rewrites the header block (Content-Encoding and Content-Length) is unverified, so the serialized headers keep the fields the server sent; other encodings are not inflated, a malformed or truncated stream and concatenated members throw, the optional gzip header CRC is not verified, and the encoded and inflated bodies are capped at 64 MiB. - [sceHttp2GetAllResponseHeaders](../../../core/libs/prx/libSceHttp2/Export.cpp) throws: the serialization, size and lifetime of a decoded HTTP/2 header block have not been established. Status and Content-Length fields are decoded; absent Content-Length reports `NotFound`, following SharpProspero. - [libSceHttp2 asynchronous requests](../../../core/libs/prx/libSceHttp2/Export.cpp) - send/read submission and `sceHttp2WaitAsync` throw. The previous offline implementation queued a fabricated network failure and used event type zero; public sources do not establish the completion event types, wait timeout updates, cancellation results or ownership of async buffers. PPSA17221's kqueue layout and PPSA15508's 24-byte wait result establish caller ABI only. No completion or equeue event is manufactured. From 72c2df44a4b026ae5cd3a92e20f06afccd81b5d0 Mon Sep 17 00:00:00 2001 From: Eduardo Ruiz Date: Wed, 28 Oct 2026 13:05:58 +0100 Subject: [PATCH 48/49] test(http2): cover gzip checksum and concatenated members --- core/libs/tests/GuestHttp2Transport.cpp | 22 +++++++++++++--------- docs/dev/debt/libSceHttp2.md | 2 +- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/core/libs/tests/GuestHttp2Transport.cpp b/core/libs/tests/GuestHttp2Transport.cpp index ebe87cd057..4aab31e278 100644 --- a/core/libs/tests/GuestHttp2Transport.cpp +++ b/core/libs/tests/GuestHttp2Transport.cpp @@ -52,7 +52,7 @@ bool Throws(TCall call) { return false; } -enum class Mode { Normal, Large, NoLength, Head, NotFound, Redirect, Auth, Reset, Truncate, Gzip, GzipSplit, GzipTruncated, GzipLarge, StallHandshake, StallHeaders, StallBody, NoAlpn, Informational, StallUpload, EarlyFinal, EarlyReset, EarlyEmpty, EarlyLarge, EarlyResetIncomplete }; +enum class Mode { Normal, Large, NoLength, Head, NotFound, Redirect, Auth, Reset, Truncate, Gzip, GzipSplit, GzipTruncated, GzipChecksum, GzipConcat, GzipLarge, StallHandshake, StallHeaders, StallBody, NoAlpn, Informational, StallUpload, EarlyFinal, EarlyReset, EarlyEmpty, EarlyLarge, EarlyResetIncomplete }; const char* Protocols[] = {"h2", nullptr}; std::string GzipPayload() { @@ -233,13 +233,15 @@ private: auto length = std::to_string(body.size()); if (mode == Mode::Head) body.clear(); if (mode == Mode::Truncate) body = "short"; - if (mode == Mode::Gzip || mode == Mode::GzipSplit || mode == Mode::GzipTruncated) { + if (mode == Mode::Gzip || mode == Mode::GzipSplit || mode == Mode::GzipTruncated || mode == Mode::GzipChecksum || mode == Mode::GzipConcat) { static const unsigned char compressed[] = { 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x2b, 0x4a, 0x2d, 0x2e, 0xc8, 0xcf, 0x2b, 0x4e, 0x55, 0x48, 0xca, 0x4f, 0xa9, 0x04, 0x00, 0x36, 0x76, 0x8b, 0x0c, 0x0d, 0x00, 0x00, 0x00, }; body.assign(reinterpret_cast(compressed), sizeof(compressed)); if (mode == Mode::GzipTruncated) body.resize(body.size() - 4); + if (mode == Mode::GzipChecksum) body[body.size() - 8] ^= 0xff; + if (mode == Mode::GzipConcat) { const std::string member = body; body += member; } length = std::to_string(body.size()); } if (mode == Mode::GzipLarge) { @@ -253,7 +255,7 @@ private: std::vector> fields{{":status", status}, {"x-loopback", "yes"}}; if (mode != Mode::NoLength) fields.emplace_back("content-length", length); if (mode == Mode::Redirect) fields.emplace_back("location", "/another"); - if (mode == Mode::Gzip || mode == Mode::GzipSplit || mode == Mode::GzipTruncated || mode == Mode::GzipLarge) fields.emplace_back("content-encoding", "gzip"); + if (mode == Mode::Gzip || mode == Mode::GzipSplit || mode == Mode::GzipTruncated || mode == Mode::GzipChecksum || mode == Mode::GzipConcat || mode == Mode::GzipLarge) fields.emplace_back("content-encoding", "gzip"); std::vector response; for (auto& [name, value] : fields) response.push_back({reinterpret_cast(name.data()), reinterpret_cast(value.data()), name.size(), value.size(), 0}); nghttp2_data_provider2 provider{}; @@ -488,17 +490,17 @@ void GzipLarge(int ssl) { server.Join(); } -void GzipFailure(int ssl) { - std::fprintf(stderr, "HTTP2 gzip truncated scenario\n"); - Server server(Mode::GzipTruncated); +void GzipFailure(int ssl, Mode mode, const char* message) { + std::fprintf(stderr, "HTTP2 gzip failure scenario %d\n", static_cast(mode)); + Server server(mode); Client client(ssl); - Require(sceHttp2SetInflateGZIPEnabled(client.tmpl, 1) == 0, "gzip truncated option"); + Require(sceHttp2SetInflateGZIPEnabled(client.tmpl, 1) == 0, "gzip failure option"); client.Create(server.Url()); Require(Throws([&] { sceHttp2SendRequest(client.request, nullptr, 0); std::array buffer; sceHttp2ReadData(client.request, buffer.data(), buffer.size()); - }), "truncated gzip throws"); + }), message); server.Join(); } @@ -736,7 +738,9 @@ int main(int argc, char** argv) { Gzip(ssl); GzipSplit(ssl); GzipLarge(ssl); - GzipFailure(ssl); + GzipFailure(ssl, Mode::GzipTruncated, "truncated gzip throws"); + GzipFailure(ssl, Mode::GzipChecksum, "gzip checksum mismatch throws"); + GzipFailure(ssl, Mode::GzipConcat, "concatenated gzip members throw"); for (const auto mode : {Mode::NoAlpn, Mode::Reset, Mode::Truncate, Mode::Redirect, Mode::Auth}) Failure(ssl, mode); Failure(ssl, Mode::Normal, true); for (const auto mode : {Mode::StallHandshake, Mode::StallHeaders, Mode::StallBody, Mode::StallUpload}) { diff --git a/docs/dev/debt/libSceHttp2.md b/docs/dev/debt/libSceHttp2.md index 9728dbc03d..4e0b8f728d 100644 --- a/docs/dev/debt/libSceHttp2.md +++ b/docs/dev/debt/libSceHttp2.md @@ -32,7 +32,7 @@ - [libSceHttp2 error paths](../../../core/libs/prx/libSceHttp2/Transport.cpp) - cancellation, deadlines, DNS/TLS/protocol failures, malformed inputs, concurrent calls on one request and exhausted connection slots throw because their console error mappings are unknown. Socket connect errors propagate the libSceNet result, following the module-analysis report in [prosper #2894](https://github.com/mattias800/prosper/issues/2894); the report does not establish every exported error path. A request uses one connection; a context's concurrency cap counts connections until EOF, failure or deletion. Native pooling, scheduling, validation order and teardown semantics remain unverified. DNS uses two host workers and a queue of eight; cancellation/deadlines abandon the caller's wait, not libc's lookup. Library unload can wait for a host lookup to return. - [libSceHttp2 TLS settings](../../../core/libs/prx/libSceHttp2/Export.cpp) - minimum-version selection, nonnull SSL callbacks and flags outside `0x3d` throw. Supported certificate-check bits follow SharpProspero and use libSceSsl's TLS 1.2 backend and CA context; native defaults and error translation are unverified. Settings on object kinds other than templates/requests throw, except for the request options below; native object-kind restrictions are otherwise unverified. Settings become immutable when transport is created. Zero timeout values, nonzero resolve retries, gzip inflation and connection-slot wait timeouts throw. Host HTTP allocations are not charged to the context pool, so `sceHttp2GetMemoryPoolStats` reports a live context's pool size with zero peak and current use. PPSA01494, PPSA30839 and PPSA31162 read the three sizes for memory logging without checking the result, and stopped at startup while it threw. - [libSceHttp2 request options](../../../core/libs/prx/libSceHttp2/Export.cpp) - extends the TLS settings entry: the auth and redirect flags, headers and the auth-info and pre-send callbacks live in `Http2::Settings`, are copied into each new request and become immutable once transport exists; the getters still read them then. Accepted kinds, defaults, copying, header modes, error codes and their order follow the 13.42 module run in an emulator ([#10536](https://github.com/boykopovar/AnyPS5/pull/10536)), not a console. A null out-pointer to `sceHttp2GetAuthEnabled` is written through on that module; here it throws. The pre-send callback type is unknown. The transport never calls either callback, and the auth cache flush only validates the context -- [libSceHttp2 gzip responses](../../../core/libs/prx/libSceHttp2/Transport.cpp) - `sceHttp2SetInflateGZIPEnabled` stores the flag. When it is set and the response carries `content-encoding: gzip`, the transport inflates the body with the vendored zlib before `sceHttp2ReadData` and `sceHttp2GetResponseContentLength` see it, and reports the inflated length; both calls wait for END_STREAM while the enabled response is gzip. The transport neither adds nor inspects `Accept-Encoding`, so the guest supplies that request header and an unsolicited gzip response is inflated too. Whether a console rewrites the header block (Content-Encoding and Content-Length) is unverified, so the serialized headers keep the fields the server sent; other encodings are not inflated, a malformed or truncated stream and concatenated members throw, the optional gzip header CRC is not verified, and the encoded and inflated bodies are capped at 64 MiB. +- [libSceHttp2 gzip responses](../../../core/libs/prx/libSceHttp2/Transport.cpp) - `sceHttp2SetInflateGZIPEnabled` stores the flag. When it is set and the response carries `content-encoding: gzip`, the transport inflates the body with the vendored zlib before `sceHttp2ReadData` and `sceHttp2GetResponseContentLength` see it, and reports the inflated length; both calls wait for END_STREAM while the enabled response is gzip. The transport neither adds nor inspects `Accept-Encoding`, so the guest supplies that request header and an unsolicited gzip response is inflated too. Whether a console rewrites the header block (Content-Encoding and Content-Length) is unverified, so the serialized headers keep the fields the server sent; other encodings are not inflated, a malformed or truncated stream, a bad checksum or size and concatenated members throw, and the optional gzip header CRC is not verified. - [sceHttp2GetAllResponseHeaders](../../../core/libs/prx/libSceHttp2/Export.cpp) throws: the serialization, size and lifetime of a decoded HTTP/2 header block have not been established. Status and Content-Length fields are decoded; absent Content-Length reports `NotFound`, following SharpProspero. - [libSceHttp2 asynchronous requests](../../../core/libs/prx/libSceHttp2/Export.cpp) - send/read submission and `sceHttp2WaitAsync` throw. The previous offline implementation queued a fabricated network failure and used event type zero; public sources do not establish the completion event types, wait timeout updates, cancellation results or ownership of async buffers. PPSA17221's kqueue layout and PPSA15508's 24-byte wait result establish caller ABI only. No completion or equeue event is manufactured. From ea63edc9ce213fb410e54ad1621a3de9a745fd19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0?= Date: Fri, 30 Oct 2026 13:31:16 +0100 Subject: [PATCH 49/49] docs(http2): restore main's debt entries and record gzip inflation Co-Authored-By: Claude Sonnet 5.5 --- docs/dev/debt/libSceHttp2.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/dev/debt/libSceHttp2.md b/docs/dev/debt/libSceHttp2.md index 4e0b8f728d..f0f949af7c 100644 --- a/docs/dev/debt/libSceHttp2.md +++ b/docs/dev/debt/libSceHttp2.md @@ -29,8 +29,9 @@ - [Default TLS trust](../../../core/libs/prx/libSceHttp2/Transport.cpp) (libSceHttp2) - HTTPS connections also trust the system CA list, `//common/cert/CA_LIST.cer`, served by libkernel from the host trust store, after the title's own libSceSsl certificates. RetroArch (PPSA99169, [updater](https://github.com/mihawk-99/PS5_RetroArch/blob/7268510b12fe7461342e0cc4497c87a0910c1055/src/webui_update.cpp#L302-L312)), PS5LM (PPSA99575) and Porpoise (PPSA99764) send version-3 requests to public `https://` servers with no loaded certificate, verify callback or verification option, which pass the known-CA check only if the console trusts public roots by default. That the console's roots are exactly this file is not measured - [libSceHttp2 transport](../../../core/libs/prx/libSceHttp2/Transport.cpp) - only synchronous HTTPS requests with version 3 and ALPN `h2` are supported. HTTP/1.x, cleartext HTTP/2, negotiation fallback, connection reuse, multiplexing across requests, CONNECT, response trailers, automatic redirects and authentication challenges throw. Version values, method strings, header overwrite/append modes, status/read/content-length signatures and timeout units follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Http2.cs); wire framing and negotiation follow [RFC 9113 sections 3, 5, 6 and 8](https://www.rfc-editor.org/rfc/rfc9113.html). [prosper](https://github.com/mattias800/prosper/blob/b1bec57186a0/prosper/src/hle/net/hle_http2.cpp) instead records the method argument as an integer; the string signature follows SharpProspero. These are public compatibility contracts, not console measurements. -- [libSceHttp2 error paths](../../../core/libs/prx/libSceHttp2/Transport.cpp) - cancellation, deadlines, DNS/TLS/protocol failures, malformed inputs, concurrent calls on one request and exhausted connection slots throw because their console error mappings are unknown. Socket connect errors propagate the libSceNet result, following the module-analysis report in [prosper #2894](https://github.com/mattias800/prosper/issues/2894); the report does not establish every exported error path. A request uses one connection; a context's concurrency cap counts connections until EOF, failure or deletion. Native pooling, scheduling, validation order and teardown semantics remain unverified. DNS uses two host workers and a queue of eight; cancellation/deadlines abandon the caller's wait, not libc's lookup. Library unload can wait for a host lookup to return. -- [libSceHttp2 TLS settings](../../../core/libs/prx/libSceHttp2/Export.cpp) - minimum-version selection, nonnull SSL callbacks and flags outside `0x3d` throw. Supported certificate-check bits follow SharpProspero and use libSceSsl's TLS 1.2 backend and CA context; native defaults and error translation are unverified. Settings on object kinds other than templates/requests throw, except for the request options below; native object-kind restrictions are otherwise unverified. Settings become immutable when transport is created. Zero timeout values, nonzero resolve retries, gzip inflation and connection-slot wait timeouts throw. Host HTTP allocations are not charged to the context pool, so `sceHttp2GetMemoryPoolStats` reports a live context's pool size with zero peak and current use. PPSA01494, PPSA30839 and PPSA31162 read the three sizes for memory logging without checking the result, and stopped at startup while it threw. +- [libSceHttp2 error paths](../../../core/libs/prx/libSceHttp2/Transport.cpp) - cancellation, deadlines, DNS/TLS/protocol failures, malformed inputs, concurrent calls on one request and exhausted connection slots throw because their console error mappings are unknown. Socket connect errors propagate the libSceNet result, following the module-analysis report in [prosper #2894](https://github.com/mattias800/prosper/issues/2894); the report does not establish every exported error path. A request uses one connection; a context's concurrency cap counts connections until EOF, failure or deletion. Native pooling, scheduling, validation order and teardown semantics remain unverified. DNS uses two host workers and a queue of eight; cancellation/deadlines abandon the caller's wait, not libc's lookup. Library unload can wait for a host lookup to return. A resolve retry resubmits a failed lookup with a new timeout; a timed-out lookup isn't retried. The console's retry semantics are unverified. +- [libSceHttp2 TLS settings](../../../core/libs/prx/libSceHttp2/Export.cpp) - minimum SSL versions outside 2 to 5, nonnull SSL callbacks and flags outside `0x3d` throw. Supported certificate-check bits follow SharpProspero and use libSceSsl's TLS 1.2 backend and CA context; native defaults and error translation are unverified. Settings on object kinds other than templates/requests throw, except for the request options below; native object-kind restrictions are otherwise unverified. Settings become immutable when transport is created. Zero timeout values, negative resolve retries and connection-slot wait timeouts throw; a failed lookup is retried up to the set count. Host HTTP allocations are not charged to the context pool, so `sceHttp2GetMemoryPoolStats` reports a live context's pool size with zero peak and current use. PPSA01494, PPSA30839 and PPSA31162 read the three sizes for memory logging without checking the result, and stopped at startup while it threw. +- [sceHttp2SetMinSslVersion](../../../core/libs/prx/libSceHttp2/Export.cpp) - libSceHttp2's own range and validation order are not measured: versions 2 (SSL 3.0) to 5 (TLS 1.2) are accepted, the range `sceSslSetMinSslVersion` takes on a 13.40 console ([libSceSsl](libSceSsl.md)). The floor is not kept: libSceSsl negotiates TLS 1.2 only, which meets every accepted floor, so a server limited to versions older than TLS 1.2 fails the handshake whatever the floor; what a lower floor does with such a server on a console is not measured. - [libSceHttp2 request options](../../../core/libs/prx/libSceHttp2/Export.cpp) - extends the TLS settings entry: the auth and redirect flags, headers and the auth-info and pre-send callbacks live in `Http2::Settings`, are copied into each new request and become immutable once transport exists; the getters still read them then. Accepted kinds, defaults, copying, header modes, error codes and their order follow the 13.42 module run in an emulator ([#10536](https://github.com/boykopovar/AnyPS5/pull/10536)), not a console. A null out-pointer to `sceHttp2GetAuthEnabled` is written through on that module; here it throws. The pre-send callback type is unknown. The transport never calls either callback, and the auth cache flush only validates the context - [libSceHttp2 gzip responses](../../../core/libs/prx/libSceHttp2/Transport.cpp) - `sceHttp2SetInflateGZIPEnabled` stores the flag. When it is set and the response carries `content-encoding: gzip`, the transport inflates the body with the vendored zlib before `sceHttp2ReadData` and `sceHttp2GetResponseContentLength` see it, and reports the inflated length; both calls wait for END_STREAM while the enabled response is gzip. The transport neither adds nor inspects `Accept-Encoding`, so the guest supplies that request header and an unsolicited gzip response is inflated too. Whether a console rewrites the header block (Content-Encoding and Content-Length) is unverified, so the serialized headers keep the fields the server sent; other encodings are not inflated, a malformed or truncated stream, a bad checksum or size and concatenated members throw, and the optional gzip header CRC is not verified. - [sceHttp2GetAllResponseHeaders](../../../core/libs/prx/libSceHttp2/Export.cpp) throws: the serialization, size and lifetime of a decoded HTTP/2 header block have not been established. Status and Content-Length fields are decoded; absent Content-Length reports `NotFound`, following SharpProspero.