From 8c790274da25c0c1f9a1668555affd8b8774f8f4 Mon Sep 17 00:00:00 2001 From: Filip Michalski Date: Wed, 28 Oct 2026 08:20:12 +0100 Subject: [PATCH 01/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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 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 07/51] 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 08/51] 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 a661d4367fcf730609ba5e57f7d9c57e29a39c3a Mon Sep 17 00:00:00 2001 From: milescoviello Date: Tue, 27 Oct 2026 16:54:27 +0000 Subject: [PATCH 09/51] perf(shader): allocate a program's use lists from an arena Every IR value keeps a vector of the instructions that read it, and each one grows by reallocating on the heap. A program's values all die with the program, so their use lists are now drawn from a bump arena the program owns: growth is a pointer bump and freeing is dropped. Values built outside a program keep the heap allocator, and copies of a use list go back to the heap. Co-Authored-By: Claude Opus 5.5 --- .../prx/libSceAgcDriver/tests/IrReclaim.cpp | 20 +++++++ .../IntermediateRepresentation/IrProgram.hpp | 1 + .../IntermediateRepresentation/IrValue.hpp | 53 +++++++++++++++++-- .../src/IrProgram.cpp | 2 +- .../src/IrValue.cpp | 4 +- .../tests/UseRewrite.cpp | 8 +-- .../Optimization/src/ConstantFolder.cpp | 4 +- 7 files changed, 80 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/IrReclaim.cpp b/core/libs/prx/libSceAgcDriver/tests/IrReclaim.cpp index 8befc9e582..9c8a8e5769 100644 --- a/core/libs/prx/libSceAgcDriver/tests/IrReclaim.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/IrReclaim.cpp @@ -76,6 +76,26 @@ int main() { program.Metadata().dynamicReads.clear(); if (program.ReclaimUnusedValues() != 2u) throw std::runtime_error("reclamation did not recover after an external-owner rejection"); if (builder.Constant(999u).Id() <= pinned.back()->Id()) throw std::runtime_error("reclamation reused a value ID"); + IrProgram replaced; + auto& shared = replaced.CreateValue(IrOpcode::IAdd32, IrType::U32); + std::vector readers; + for (std::uint32_t number = 0; number < 40u; ++number) { + auto& reader = replaced.CreateValue(IrOpcode::IAdd32, IrType::U32); + reader.AddArgument(&shared); + readers.push_back(&reader); + } + for (std::size_t index = 0; index < readers.size(); ++index) { + if (shared.OperandUses()[index].user != readers[index]) throw std::runtime_error("a use list lost an entry while growing"); + } + auto& target = replaced.CreateValue(IrOpcode::IAdd32, IrType::U32); + shared.ReplaceAllUsesWith(&target); + if (shared.HasUses() || target.UseCount() != readers.size() || readers.front()->Argument(0) != &target || readers.back()->Argument(0) != &target) throw std::runtime_error("moving a grown use list to another value lost a use"); + IrProgram replacement; + auto& kept = replacement.CreateValue(IrOpcode::IAdd32, IrType::U32); + auto& keptReader = replacement.CreateValue(IrOpcode::IAdd32, IrType::U32); + keptReader.AddArgument(&kept); + replaced = std::move(replacement); + if (kept.UseCount() != 1u || kept.OperandUses().front().user != &keptReader) throw std::runtime_error("move assignment invalidated the assigned program's use lists"); IrProgram moved = std::move(program); if (value.Id() != id || value.ImmediateU32() != 42u || moved.ReclaimUnusedValues() != 1u) throw std::runtime_error("moving a program invalidated its allocation owner"); IrProgram assigned; diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrProgram.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrProgram.hpp index f724becd4b..a93cd962c5 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrProgram.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrProgram.hpp @@ -38,6 +38,7 @@ public: private: std::shared_ptr instructionNodes; std::vector> blocks; + IrUseArena useArena; std::vector blockOrder; std::vector> values; IrResourcePlan resourcePlan; diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrValue.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrValue.hpp index d37a3156af..a452593474 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrValue.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrValue.hpp @@ -7,6 +7,8 @@ #include #include #include +#include +#include #include #include #include @@ -16,6 +18,51 @@ namespace ShaderRecompiler { class IrBlock; class IrValue; +struct IrUse; + +template +class IrUseAllocator { +public: + using value_type = T; + using propagate_on_container_move_assignment = std::true_type; + using propagate_on_container_swap = std::true_type; + IrUseAllocator() noexcept = default; + explicit IrUseAllocator(std::pmr::memory_resource* arena) noexcept : arena(arena) {} + template + IrUseAllocator(const IrUseAllocator& other) noexcept : arena(other.Arena()) {} + T* allocate(std::size_t count) { + if (arena == nullptr) return std::allocator{}.allocate(count); + return static_cast(arena->allocate(count * sizeof(T), alignof(T))); + } + void deallocate(T* pointer, std::size_t count) noexcept { + if (arena == nullptr) std::allocator{}.deallocate(pointer, count); + } + [[nodiscard]] std::pmr::memory_resource* Arena() const noexcept { return arena; } + IrUseAllocator select_on_container_copy_construction() const noexcept { return {}; } + friend bool operator==(const IrUseAllocator& left, const IrUseAllocator& right) noexcept { return left.arena == right.arena; } + +private: + std::pmr::memory_resource* arena = nullptr; +}; + +using IrUseList = std::vector>; + +class IrUseArena { +public: + IrUseArena() = default; + IrUseArena(IrUseArena&& other) noexcept = default; + IrUseArena& operator=(IrUseArena&& other) noexcept { + resource.swap(other.resource); + return *this; + } + std::pmr::memory_resource* Resource() { + if (resource == nullptr) resource = std::make_unique(); + return resource.get(); + } + +private: + std::unique_ptr resource; +}; struct IrUse { IrValue* user = nullptr; @@ -91,7 +138,7 @@ private: class IrValue { public: - IrValue(IrOpcode opcode, IrType type, std::uint32_t id); + IrValue(IrOpcode opcode, IrType type, std::uint32_t id, std::pmr::memory_resource* useArena = nullptr); IrValue(const IrValue& other) = delete; IrValue& operator=(const IrValue& other) = delete; IrValue(IrValue&& other) = delete; @@ -136,7 +183,7 @@ public: [[nodiscard]] std::size_t PhiBlockCount() const; [[nodiscard]] IrValue* Argument(std::size_t index) const; [[nodiscard]] IrBlock* PhiBlock(std::size_t index) const; - [[nodiscard]] const std::vector& OperandUses() const; + [[nodiscard]] const IrUseList& OperandUses() const; [[nodiscard]] std::uint64_t ImmediateU64() const; [[nodiscard]] std::uint16_t ImmediateF16Bits() const; [[nodiscard]] float ImmediateF32() const; @@ -166,7 +213,7 @@ private: GuestRegister registerValue{}; std::uint64_t flags = 0; std::vector phiBlocks; - std::vector operandUses; + IrUseList operandUses; bool hasImmediate; IrOperands arguments; IrBlock* parent; diff --git a/core/shader/recompiler/IntermediateRepresentation/src/IrProgram.cpp b/core/shader/recompiler/IntermediateRepresentation/src/IrProgram.cpp index 4c5e1fc57c..8d134e86f1 100644 --- a/core/shader/recompiler/IntermediateRepresentation/src/IrProgram.cpp +++ b/core/shader/recompiler/IntermediateRepresentation/src/IrProgram.cpp @@ -315,7 +315,7 @@ IrBlock& IrProgram::CreateBlock() { } IrValue& IrProgram::CreateValue(IrOpcode opcode, IrType type) { - auto value = std::make_unique(opcode, type, nextValueId); + auto value = std::make_unique(opcode, type, nextValueId, useArena.Resource()); nextValueId++; IrValue& created = *value; values.push_back(std::move(value)); diff --git a/core/shader/recompiler/IntermediateRepresentation/src/IrValue.cpp b/core/shader/recompiler/IntermediateRepresentation/src/IrValue.cpp index 2bbfc39eda..b910a7bd6a 100644 --- a/core/shader/recompiler/IntermediateRepresentation/src/IrValue.cpp +++ b/core/shader/recompiler/IntermediateRepresentation/src/IrValue.cpp @@ -5,7 +5,7 @@ namespace ShaderRecompiler { -IrValue::IrValue(IrOpcode opcode, IrType type, std::uint32_t id) : opcode(opcode), type(type), id(id), hasImmediate(false), parent(nullptr) { +IrValue::IrValue(IrOpcode opcode, IrType type, std::uint32_t id, std::pmr::memory_resource* useArena) : opcode(opcode), type(type), id(id), hasImmediate(false), parent(nullptr), operandUses(IrUseAllocator(useArena)) { arguments.reserve(IrOpcodeOperandCount(opcode)); } @@ -121,7 +121,7 @@ IrBlock* IrValue::PhiBlock(std::size_t index) const { return phiBlocks[index]; } -const std::vector& IrValue::OperandUses() const { +const IrUseList& IrValue::OperandUses() const { return operandUses; } diff --git a/core/shader/recompiler/IntermediateRepresentation/tests/UseRewrite.cpp b/core/shader/recompiler/IntermediateRepresentation/tests/UseRewrite.cpp index f5a711b969..0f17c87cd6 100644 --- a/core/shader/recompiler/IntermediateRepresentation/tests/UseRewrite.cpp +++ b/core/shader/recompiler/IntermediateRepresentation/tests/UseRewrite.cpp @@ -91,7 +91,7 @@ void CheckSimpleReplacement() { Require(!source->HasUses() && source->OperandUses().empty()); Require(first->Argument(0) == target && first->Argument(1) == target && first->Argument(2) == target); Require(second->Argument(0) == target); - const std::vector expected{{existing, 0}, {first, 2}, {first, 1}, {second, 0}, {first, 0}}; + const IrUseList expected{{existing, 0}, {first, 2}, {first, 1}, {second, 0}, {first, 0}}; Require(target->OperandUses() == expected); Require(std::ranges::equal(target->Uses(), std::vector({existing, first, first, second, first}))); first->ReplaceArgument(1, source); @@ -110,10 +110,10 @@ void CheckTailArgumentRemoval() { last->AddArgument(source); last->AddArgument(source); last->ReplaceArgument(0, nullptr); - Require(source->OperandUses() == std::vector({{first, 0}, {last, 1}})); + Require(source->OperandUses() == IrUseList({{first, 0}, {last, 1}})); last->ReplaceArgument(1, graph.Value(3)); - Require(source->OperandUses() == std::vector({{first, 0}})); - Require(graph.Value(3)->OperandUses() == std::vector({{last, 1}})); + Require(source->OperandUses() == IrUseList({{first, 0}})); + Require(graph.Value(3)->OperandUses() == IrUseList({{last, 1}})); first->Invalidate(); Require(!source->HasUses() && first->Argument(0) == nullptr); } diff --git a/core/shader/recompiler/Optimization/src/ConstantFolder.cpp b/core/shader/recompiler/Optimization/src/ConstantFolder.cpp index 43db702ad8..e31e25adb8 100644 --- a/core/shader/recompiler/Optimization/src/ConstantFolder.cpp +++ b/core/shader/recompiler/Optimization/src/ConstantFolder.cpp @@ -765,7 +765,7 @@ bool forwardsWord(const IrUse& use) { void collectReaders(IrValue& value, std::vector& readers) { std::unordered_set visited{&value}; - std::vector, std::size_t>> pending; + std::vector> pending; pending.emplace_back(value.OperandUses(), 0u); while (!pending.empty()) { auto& [uses, next] = pending.back(); @@ -819,7 +819,7 @@ void lowerPackedAncillary(IrProgram& program, IrBuilder& builder, IrValue& ancil if (!isPackedAncillary(ancillary)) { return; } - const std::vector uses = ancillary.OperandUses(); + const IrUseList uses = ancillary.OperandUses(); std::vector direct; std::vector forwarded; for (const IrUse& use : uses) { From c01d0bb24f9ad66ea08470496b60a0dd39b79635 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Wed, 28 Oct 2026 10:12:08 +0000 Subject: [PATCH 10/51] fix(shader): move a cached program's use lists to the heap and drop its arena A program kept in a SourceEntry held its use-list arena until the entry was destroyed, so the buffers of values ReclaimUnusedValues dropped, and the buffers every use list outgrew, stayed allocated for as long as the program was cached. Before a program is cached, ReleaseUseArena now copies each value's use list into a heap vector and frees the arena. Values created after that draw from a new arena. Co-Authored-By: Claude Opus 5.5 --- .../prx/libSceAgcDriver/tests/IrReclaim.cpp | 17 +++++++++++++++++ .../IntermediateRepresentation/IrProgram.hpp | 1 + .../IntermediateRepresentation/IrValue.hpp | 1 + .../src/IrProgram.cpp | 5 +++++ .../IntermediateRepresentation/src/IrValue.cpp | 5 +++++ core/shader/recompiler/Recompiler.cpp | 1 + 6 files changed, 30 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/IrReclaim.cpp b/core/libs/prx/libSceAgcDriver/tests/IrReclaim.cpp index 9c8a8e5769..4256def9d9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/IrReclaim.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/IrReclaim.cpp @@ -96,6 +96,23 @@ int main() { keptReader.AddArgument(&kept); replaced = std::move(replacement); if (kept.UseCount() != 1u || kept.OperandUses().front().user != &keptReader) throw std::runtime_error("move assignment invalidated the assigned program's use lists"); + IrProgram cached; + auto& source = cached.CreateValue(IrOpcode::IAdd32, IrType::U32); + std::vector users; + for (std::uint32_t number = 0; number < 40u; ++number) { + auto& user = cached.CreateValue(IrOpcode::IAdd32, IrType::U32); + user.AddArgument(&source); + users.push_back(&user); + } + cached.ReleaseUseArena(); + if (source.OperandUses().get_allocator().Arena() != nullptr || users.front()->OperandUses().get_allocator().Arena() != nullptr) throw std::runtime_error("releasing the use arena left a use list in it"); + if (source.UseCount() != users.size()) throw std::runtime_error("releasing the use arena lost a use"); + for (std::size_t index = 0; index < users.size(); ++index) { + if (source.OperandUses()[index].user != users[index] || users[index]->Argument(0) != &source) throw std::runtime_error("releasing the use arena reordered a use list"); + } + auto& late = cached.CreateValue(IrOpcode::IAdd32, IrType::U32); + late.AddArgument(&source); + if (late.OperandUses().get_allocator().Arena() == nullptr || source.UseCount() != users.size() + 1u || source.OperandUses().back().user != &late) throw std::runtime_error("a value created after releasing the use arena did not get a new one"); IrProgram moved = std::move(program); if (value.Id() != id || value.ImmediateU32() != 42u || moved.ReclaimUnusedValues() != 1u) throw std::runtime_error("moving a program invalidated its allocation owner"); IrProgram assigned; diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrProgram.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrProgram.hpp index a93cd962c5..bb66d00d95 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrProgram.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrProgram.hpp @@ -32,6 +32,7 @@ public: IrValue& CreateValue(IrOpcode opcode, IrType type, std::uint64_t flags); void SetEntryBlock(IrBlock& block); std::size_t ReclaimUnusedValues(); + void ReleaseUseArena(); [[nodiscard]] std::vector& BlockOrder(); [[nodiscard]] const std::vector& BlockOrder() const; diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrValue.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrValue.hpp index a452593474..699436a0e3 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrValue.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrValue.hpp @@ -184,6 +184,7 @@ public: [[nodiscard]] IrValue* Argument(std::size_t index) const; [[nodiscard]] IrBlock* PhiBlock(std::size_t index) const; [[nodiscard]] const IrUseList& OperandUses() const; + void MoveUsesToHeap(); [[nodiscard]] std::uint64_t ImmediateU64() const; [[nodiscard]] std::uint16_t ImmediateF16Bits() const; [[nodiscard]] float ImmediateF32() const; diff --git a/core/shader/recompiler/IntermediateRepresentation/src/IrProgram.cpp b/core/shader/recompiler/IntermediateRepresentation/src/IrProgram.cpp index 8d134e86f1..34a251932b 100644 --- a/core/shader/recompiler/IntermediateRepresentation/src/IrProgram.cpp +++ b/core/shader/recompiler/IntermediateRepresentation/src/IrProgram.cpp @@ -274,6 +274,11 @@ std::size_t IrProgram::ReclaimUnusedValues() { return removed; } +void IrProgram::ReleaseUseArena() { + for (const auto& value : values) value->MoveUsesToHeap(); + useArena = IrUseArena{}; +} + std::vector>& IrProgram::Blocks() { return blocks; } diff --git a/core/shader/recompiler/IntermediateRepresentation/src/IrValue.cpp b/core/shader/recompiler/IntermediateRepresentation/src/IrValue.cpp index b910a7bd6a..b549aa7393 100644 --- a/core/shader/recompiler/IntermediateRepresentation/src/IrValue.cpp +++ b/core/shader/recompiler/IntermediateRepresentation/src/IrValue.cpp @@ -125,6 +125,11 @@ const IrUseList& IrValue::OperandUses() const { return operandUses; } +void IrValue::MoveUsesToHeap() { + if (operandUses.get_allocator().Arena() == nullptr) return; + operandUses = IrUseList(operandUses.begin(), operandUses.end(), IrUseAllocator{}); +} + std::uint64_t IrValue::ImmediateU64() const { return immediateBits; } diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 73da42c9aa..2dced2f200 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -536,6 +536,7 @@ std::shared_ptr getSource(const RecompileRequest& request, ShaderPr ResourceProgram resource(request, preparation, reclaim); source->plan = std::move(resource.plan); if (!hasPreparedArtifact && std::ranges::none_of(source->variants, [&](const auto& variant) { return sameLayout(variant->layout, request.layout); })) { + resource.program->ReleaseUseArena(); source->program = std::move(resource.program); } } catch (...) { From 6a98793e3b32994bd9f5481f37e925fc6af18a1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0?= Date: Thu, 29 Oct 2026 21:17:50 +0100 Subject: [PATCH 11/51] fix(shader): IrValue is 144 bytes with the use allocator Co-Authored-By: Claude Sonnet 5.5 --- core/shader/recompiler/ShaderDiskCache.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index 9be24e6ccb..04845ac349 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -106,7 +106,7 @@ static_assert(sizeof(BindingAllocationResult) == 184, "BindingAllocationResult c static_assert(sizeof(CompiledBindingLayout) == 88, "CompiledBindingLayout changed: update the allocation encoder"); static_assert(sizeof(BindingLayout) == 16, "BindingLayout changed: update BuildKey"); static_assert(sizeof(IrResourcePlan) == 728, "IrResourcePlan changed: update EncodePlan and DecodePlan"); -static_assert(sizeof(IrValue) == 136, "IrValue changed: update the plan value encoder"); +static_assert(sizeof(IrValue) == 144, "IrValue changed: update the plan value encoder"); static_assert(sizeof(GuestRegister) == 8, "GuestRegister changed: update the plan value encoder"); static_assert(sizeof(MemoryInfo) == 80, "MemoryInfo changed: update the plan encoder"); static_assert(sizeof(DescriptorSource) == 192, "DescriptorSource changed: update the plan encoder"); From ce32d855ba3817332ab44e454f95f6257836943d Mon Sep 17 00:00:00 2001 From: JJenkx Date: Thu, 29 Oct 2026 19:00:11 -0400 Subject: [PATCH 12/51] 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/51] 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 2aca989548fe6428d582047032a429bc5ec1c95b Mon Sep 17 00:00:00 2001 From: hugodearaujo Date: Thu, 29 Oct 2026 22:14:23 -0300 Subject: [PATCH 14/51] 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 15/51] 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 16/51] 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 17/51] 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 18/51] 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 4189ea4f026c140fc18322a6e79dd871ca017532 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Thu, 15 Oct 2026 23:42:12 +0000 Subject: [PATCH 19/51] 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 20/51] 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 21/51] 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 22/51] 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 23/51] 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 24/51] 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 25/51] 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 26/51] 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 27/51] 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 28/51] 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 29/51] 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 db7421c55a6180ebbc3b979d3d0ea837f7c57cdb Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 29 Oct 2026 07:48:54 +0000 Subject: [PATCH 30/51] feat(libSceFreeTypeOt): implement the ft_mem memory helpers Eight libSceFreeTypeOt exports carry the NIDs of FreeType's ft_mem_alloc, ft_mem_qalloc, ft_mem_realloc, ft_mem_qrealloc, ft_mem_free, ft_mem_dup, ft_mem_strdup and ft_mem_strcpyn. Implement them in src/Memory.cpp as bundled FreeType 2.13.3 ftutil.c does on LP64: allocation, resizing and freeing go through the guest FT_MemoryRec callbacks, the cleared variants zero the new bytes, and the error codes are Invalid_Argument, Array_Too_Large and Out_Of_Memory. Add the guest FT_MemoryRec layout and a test that checks every sourced path with a recording guest allocator. --- .../libSceFreeType/include/GuestFreeType.hpp | 8 + core/libs/prx/libSceFreeTypeOt/CMakeLists.txt | 17 ++ .../prx/libSceFreeTypeOt/include/Memory.hpp | 18 ++ core/libs/prx/libSceFreeTypeOt/src/Memory.cpp | 105 +++++++ core/libs/tests/GuestFreeTypeOtMemory.cpp | 258 ++++++++++++++++++ docs/dev/debt/libSceFreeTypeOt.md | 4 +- 6 files changed, 409 insertions(+), 1 deletion(-) create mode 100644 core/libs/prx/libSceFreeTypeOt/include/Memory.hpp create mode 100644 core/libs/prx/libSceFreeTypeOt/src/Memory.cpp create mode 100644 core/libs/tests/GuestFreeTypeOtMemory.cpp diff --git a/core/libs/prx/libSceFreeType/include/GuestFreeType.hpp b/core/libs/prx/libSceFreeType/include/GuestFreeType.hpp index 67af76996e..6e18b7ce0d 100644 --- a/core/libs/prx/libSceFreeType/include/GuestFreeType.hpp +++ b/core/libs/prx/libSceFreeType/include/GuestFreeType.hpp @@ -15,6 +15,13 @@ struct GuestGeneric { void (APS5_VABI *finalizer)(void*); }; +struct GuestMemory { + void* user; + void* (APS5_VABI *alloc)(GuestMemory*, std::int64_t); + void (APS5_VABI *free)(GuestMemory*, void*); + void* (APS5_VABI *realloc)(GuestMemory*, std::int64_t, std::int64_t, void*); +}; + struct GuestVector { std::int64_t x; std::int64_t y; @@ -191,6 +198,7 @@ struct GuestPropIncreaseXHeight { static_assert(sizeof(GuestMatrix) == 32 && offsetof(GuestMatrix, xy) == 8 && offsetof(GuestMatrix, yx) == 16 && offsetof(GuestMatrix, yy) == 24); static_assert(sizeof(GuestGeneric) == 16 && offsetof(GuestGeneric, finalizer) == 8); +static_assert(sizeof(GuestMemory) == 32 && offsetof(GuestMemory, alloc) == 8 && offsetof(GuestMemory, free) == 16 && offsetof(GuestMemory, realloc) == 24); static_assert(sizeof(GuestBitmapSize) == 32 && offsetof(GuestBitmapSize, size) == 8); static_assert(sizeof(GuestCharMapRec) == 16 && offsetof(GuestCharMapRec, platform_id) == 12); static_assert(sizeof(GuestSfntName) == 24 && offsetof(GuestSfntName, string) == 8 && offsetof(GuestSfntName, string_len) == 16); diff --git a/core/libs/prx/libSceFreeTypeOt/CMakeLists.txt b/core/libs/prx/libSceFreeTypeOt/CMakeLists.txt index 4d34176d1f..5fe4b84169 100644 --- a/core/libs/prx/libSceFreeTypeOt/CMakeLists.txt +++ b/core/libs/prx/libSceFreeTypeOt/CMakeLists.txt @@ -18,3 +18,20 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endif() + +target_sources(libSceFreeTypeOt PRIVATE src/Memory.cpp) +if(BUILD_TESTING) + add_test_executable(guest_freetype_ot_memory_tests ${LIBS_INCLUDE_DIR}/tests/GuestFreeTypeOtMemory.cpp) + target_include_directories(guest_freetype_ot_memory_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_freetype_ot_memory_tests PRIVATE libSceFreeTypeOt) + if(WIN32) + target_link_libraries(guest_freetype_ot_memory_tests PRIVATE libc) + endif() + configure_windows_unwind(guest_freetype_ot_memory_tests) + add_test(NAME guest_freetype_ot_memory COMMAND guest_freetype_ot_memory_tests) + set_tests_properties(guest_freetype_ot_memory PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_freetype_ot_memory PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceFreeTypeOt/include/Memory.hpp b/core/libs/prx/libSceFreeTypeOt/include/Memory.hpp new file mode 100644 index 0000000000..6f3aa42892 --- /dev/null +++ b/core/libs/prx/libSceFreeTypeOt/include/Memory.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include "prx/libSceFreeType/include/GuestFreeType.hpp" + +#include + +extern "C" { + +void* APS5_VABI ft_mem_alloc_nid_postfix(FreeType::GuestMemory* memory, std::int64_t size, std::int32_t* error); +void* APS5_VABI ft_mem_qalloc_nid_postfix(FreeType::GuestMemory* memory, std::int64_t size, std::int32_t* error); +void* APS5_VABI ft_mem_realloc_nid_postfix(FreeType::GuestMemory* memory, std::int64_t itemSize, std::int64_t currentCount, std::int64_t newCount, void* block, std::int32_t* error); +void* APS5_VABI ft_mem_qrealloc_nid_postfix(FreeType::GuestMemory* memory, std::int64_t itemSize, std::int64_t currentCount, std::int64_t newCount, void* block, std::int32_t* error); +void APS5_VABI ft_mem_free_nid_postfix(FreeType::GuestMemory* memory, const void* block); +void* APS5_VABI ft_mem_dup_nid_postfix(FreeType::GuestMemory* memory, const void* address, std::uint64_t size, std::int32_t* error); +void* APS5_VABI ft_mem_strdup_nid_postfix(FreeType::GuestMemory* memory, const char* string, std::int32_t* error); +std::int32_t APS5_VABI ft_mem_strcpyn_nid_postfix(char* destination, const char* source, std::uint64_t size); + +} diff --git a/core/libs/prx/libSceFreeTypeOt/src/Memory.cpp b/core/libs/prx/libSceFreeTypeOt/src/Memory.cpp new file mode 100644 index 0000000000..832fe71cba --- /dev/null +++ b/core/libs/prx/libSceFreeTypeOt/src/Memory.cpp @@ -0,0 +1,105 @@ +#include "prx/libSceFreeTypeOt/include/Memory.hpp" + +#include +#include +#include + +namespace { + +constexpr std::int32_t InvalidArgument = 0x06; +constexpr std::int32_t ArrayTooLarge = 0x0A; +constexpr std::int32_t OutOfMemory = 0x40; +constexpr std::int64_t IntMax = 0x7FFFFFFF; + +std::int64_t Bytes(std::int64_t count, std::int64_t itemSize) { + return static_cast(static_cast(count) * static_cast(itemSize)); +} + +} + +extern "C" { + +void* APS5_VABI ft_mem_qalloc_nid_postfix(FreeType::GuestMemory* memory, std::int64_t size, std::int32_t* error) { + std::int32_t result = 0; + void* block = nullptr; + if (size > 0) { + block = memory->alloc(memory, size); + if (!block) result = OutOfMemory; + } else if (size < 0) { + result = InvalidArgument; + } + *error = result; + return block; +} + +void* APS5_VABI ft_mem_alloc_nid_postfix(FreeType::GuestMemory* memory, std::int64_t size, std::int32_t* error) { + std::int32_t result = 0; + void* block = ft_mem_qalloc_nid_postfix(memory, size, &result); + if (!result && block && size > 0) std::memset(block, 0, static_cast(size)); + *error = result; + return block; +} + +void APS5_VABI ft_mem_free_nid_postfix(FreeType::GuestMemory* memory, const void* block) { + if (block) memory->free(memory, const_cast(block)); +} + +void* APS5_VABI ft_mem_qrealloc_nid_postfix(FreeType::GuestMemory* memory, std::int64_t itemSize, std::int64_t currentCount, + std::int64_t newCount, void* block, std::int32_t* error) { + std::int32_t result = 0; + if (currentCount < 0 || newCount < 0 || itemSize < 0) { + result = InvalidArgument; + } else if (newCount == 0 || itemSize == 0) { + ft_mem_free_nid_postfix(memory, block); + block = nullptr; + } else if (newCount > IntMax / itemSize) { + result = ArrayTooLarge; + } else if (currentCount == 0) { + block = memory->alloc(memory, newCount * itemSize); + if (!block) result = OutOfMemory; + } else { + void* resized = memory->realloc(memory, Bytes(currentCount, itemSize), newCount * itemSize, block); + if (resized) { + block = resized; + } else { + result = OutOfMemory; + } + } + *error = result; + return block; +} + +void* APS5_VABI ft_mem_realloc_nid_postfix(FreeType::GuestMemory* memory, std::int64_t itemSize, std::int64_t currentCount, + std::int64_t newCount, void* block, std::int32_t* error) { + std::int32_t result = 0; + block = ft_mem_qrealloc_nid_postfix(memory, itemSize, currentCount, newCount, block, &result); + if (!result && block && newCount > currentCount) { + std::memset(static_cast(block) + currentCount * itemSize, 0, static_cast((newCount - currentCount) * itemSize)); + } + *error = result; + return block; +} + +void* APS5_VABI ft_mem_dup_nid_postfix(FreeType::GuestMemory* memory, const void* address, std::uint64_t size, std::int32_t* error) { + std::int32_t result = 0; + void* copy = ft_mem_qalloc_nid_postfix(memory, static_cast(size), &result); + if (!result && address && size > 0) std::memcpy(copy, address, static_cast(size)); + *error = result; + return copy; +} + +void* APS5_VABI ft_mem_strdup_nid_postfix(FreeType::GuestMemory* memory, const char* string, std::int32_t* error) { + const std::uint64_t length = string ? std::strlen(string) + 1 : 0; + return ft_mem_dup_nid_postfix(memory, string, length, error); +} + +std::int32_t APS5_VABI ft_mem_strcpyn_nid_postfix(char* destination, const char* source, std::uint64_t size) { + while (size > 1 && *source != 0) { + *destination++ = *source++; + --size; + } + *destination = 0; + return *source != 0; +} + +} diff --git a/core/libs/tests/GuestFreeTypeOtMemory.cpp b/core/libs/tests/GuestFreeTypeOtMemory.cpp new file mode 100644 index 0000000000..eee4f331a8 --- /dev/null +++ b/core/libs/tests/GuestFreeTypeOtMemory.cpp @@ -0,0 +1,258 @@ +#include "prx/libSceFreeTypeOt/include/Memory.hpp" +#include +#include +#include +#include + +namespace { + +using FreeType::GuestMemory; + +struct Recorder { + GuestMemory memory{}; + int allocs = 0; + int frees = 0; + int reallocs = 0; + std::int64_t lastSize = 0; + std::int64_t lastCurrent = 0; + std::int64_t lastNew = 0; + void* lastBlock = nullptr; + GuestMemory* lastMemory = nullptr; + bool fail = false; +}; + +Recorder* Owner(GuestMemory* memory) { + return static_cast(memory->user); +} + +void* APS5_VABI RecordAlloc(GuestMemory* memory, std::int64_t size) { + auto* recorder = Owner(memory); + ++recorder->allocs; + recorder->lastMemory = memory; + recorder->lastSize = size; + if (recorder->fail) return nullptr; + void* block = std::malloc(static_cast(size)); + std::memset(block, 0xAA, static_cast(size)); + return block; +} + +void APS5_VABI RecordFree(GuestMemory* memory, void* block) { + auto* recorder = Owner(memory); + ++recorder->frees; + recorder->lastMemory = memory; + recorder->lastBlock = block; + std::free(block); +} + +void* APS5_VABI RecordRealloc(GuestMemory* memory, std::int64_t current, std::int64_t size, void* block) { + auto* recorder = Owner(memory); + ++recorder->reallocs; + recorder->lastMemory = memory; + recorder->lastCurrent = current; + recorder->lastNew = size; + recorder->lastBlock = block; + if (recorder->fail) return nullptr; + auto* resized = static_cast(std::realloc(block, static_cast(size))); + if (size > current) std::memset(resized + current, 0xAA, static_cast(size - current)); + return resized; +} + +void Reset(Recorder& recorder) { + recorder = Recorder{}; + recorder.memory = {&recorder, RecordAlloc, RecordFree, RecordRealloc}; +} + +void Require(bool condition, const char* message) { + if (condition) return; + std::fprintf(stderr, "FreeType Ot memory: %s\n", message); + std::exit(1); +} + +bool Filled(const void* block, std::size_t size, unsigned char value) { + const auto* bytes = static_cast(block); + for (std::size_t i = 0; i < size; ++i) { + if (bytes[i] != value) return false; + } + return true; +} + +void QuickAllocation() { + Recorder recorder; + Reset(recorder); + std::int32_t error = -1; + void* block = ft_mem_qalloc_nid_postfix(&recorder.memory, 24, &error); + Require(block && error == 0, "qalloc of a positive size returns a block and no error"); + Require(recorder.allocs == 1 && recorder.lastSize == 24 && recorder.lastMemory == &recorder.memory, "qalloc passes the memory record and size to alloc"); + Require(Filled(block, 24, 0xAA), "qalloc leaves the block uncleared"); + ft_mem_free_nid_postfix(&recorder.memory, block); + Require(recorder.frees == 1 && recorder.lastBlock == block && recorder.lastMemory == &recorder.memory, "free passes the block to memory->free"); + + Reset(recorder); + error = -1; + Require(!ft_mem_qalloc_nid_postfix(&recorder.memory, 0, &error) && error == 0 && recorder.allocs == 0, "qalloc of size 0 returns null without calling alloc"); + error = -1; + Require(!ft_mem_qalloc_nid_postfix(&recorder.memory, -1, &error) && error == 0x06 && recorder.allocs == 0, "qalloc of a negative size is Invalid_Argument"); + recorder.fail = true; + error = -1; + Require(!ft_mem_qalloc_nid_postfix(&recorder.memory, 8, &error) && error == 0x40 && recorder.allocs == 1, "qalloc reports Out_Of_Memory when alloc fails"); +} + +void ClearedAllocation() { + Recorder recorder; + Reset(recorder); + std::int32_t error = -1; + void* block = ft_mem_alloc_nid_postfix(&recorder.memory, 16, &error); + Require(block && error == 0 && recorder.allocs == 1 && recorder.lastSize == 16, "alloc allocates through memory->alloc"); + Require(Filled(block, 16, 0), "alloc clears the block"); + ft_mem_free_nid_postfix(&recorder.memory, block); + + error = -1; + Require(!ft_mem_alloc_nid_postfix(&recorder.memory, 0, &error) && error == 0, "alloc of size 0 returns null"); + error = -1; + Require(!ft_mem_alloc_nid_postfix(&recorder.memory, -5, &error) && error == 0x06, "alloc of a negative size is Invalid_Argument"); + recorder.fail = true; + error = -1; + Require(!ft_mem_alloc_nid_postfix(&recorder.memory, 4, &error) && error == 0x40, "alloc reports Out_Of_Memory when alloc fails"); + + Reset(recorder); + ft_mem_free_nid_postfix(&recorder.memory, nullptr); + Require(recorder.frees == 0, "free of null does not call memory->free"); +} + +void QuickReallocation() { + Recorder recorder; + Reset(recorder); + std::int32_t error = -1; + void* block = ft_mem_qrealloc_nid_postfix(&recorder.memory, 4, 0, 3, nullptr, &error); + Require(block && error == 0 && recorder.allocs == 1 && recorder.lastSize == 12 && recorder.reallocs == 0, "qrealloc from zero items allocates new_count * item_size"); + Require(Filled(block, 12, 0xAA), "qrealloc from zero items leaves the block uncleared"); + std::memset(block, 0x11, 12); + + error = -1; + void* grown = ft_mem_qrealloc_nid_postfix(&recorder.memory, 4, 3, 5, block, &error); + Require(grown && error == 0 && recorder.reallocs == 1, "qrealloc of a live block calls memory->realloc"); + Require(recorder.lastCurrent == 12 && recorder.lastNew == 20 && recorder.lastBlock == block, "qrealloc passes byte sizes and the old block"); + Require(Filled(grown, 12, 0x11) && Filled(static_cast(grown) + 12, 8, 0xAA), "qrealloc does not clear the grown tail"); + + recorder.fail = true; + error = -1; + Require(ft_mem_qrealloc_nid_postfix(&recorder.memory, 4, 5, 9, grown, &error) == grown && error == 0x40, "qrealloc keeps the old block when realloc fails"); + recorder.fail = false; + + const int callbacks = recorder.allocs + recorder.frees + recorder.reallocs; + error = -1; + Require(ft_mem_qrealloc_nid_postfix(&recorder.memory, -1, 5, 6, grown, &error) == grown && error == 0x06, "negative item_size is Invalid_Argument"); + error = -1; + Require(ft_mem_qrealloc_nid_postfix(&recorder.memory, 4, -1, 6, grown, &error) == grown && error == 0x06, "negative cur_count is Invalid_Argument"); + error = -1; + Require(ft_mem_qrealloc_nid_postfix(&recorder.memory, 4, 5, -1, grown, &error) == grown && error == 0x06, "negative new_count is Invalid_Argument"); + error = -1; + Require(ft_mem_qrealloc_nid_postfix(&recorder.memory, 2, 5, 0x40000000, grown, &error) == grown && error == 0x0A, "new_count above FT_INT_MAX / item_size is Array_Too_Large"); + Require(recorder.allocs + recorder.frees + recorder.reallocs == callbacks, "rejected qrealloc calls make no memory callback"); + + recorder.fail = true; + error = -1; + Require(!ft_mem_qrealloc_nid_postfix(&recorder.memory, 2, 0, 0x3FFFFFFF, nullptr, &error) && error == 0x40, "new_count at FT_INT_MAX / item_size is allocated"); + Require(recorder.lastSize == 0x7FFFFFFE, "the boundary allocation asks for new_count * item_size bytes"); + recorder.fail = false; + + error = -1; + Require(!ft_mem_qrealloc_nid_postfix(&recorder.memory, 4, 5, 0, grown, &error) && error == 0, "qrealloc to zero items frees and returns null"); + Require(recorder.lastBlock == grown && recorder.frees == 1, "qrealloc to zero items passes the block to memory->free"); + + block = ft_mem_qalloc_nid_postfix(&recorder.memory, 8, &error); + error = -1; + Require(!ft_mem_qrealloc_nid_postfix(&recorder.memory, 0, 2, 4, block, &error) && error == 0, "qrealloc with item_size 0 frees and returns null"); + Require(recorder.lastBlock == block && recorder.frees == 2, "qrealloc with item_size 0 passes the block to memory->free"); +} + +void ClearedReallocation() { + Recorder recorder; + Reset(recorder); + std::int32_t error = -1; + void* block = ft_mem_realloc_nid_postfix(&recorder.memory, 4, 0, 3, nullptr, &error); + Require(block && error == 0 && recorder.lastSize == 12 && Filled(block, 12, 0), "realloc from zero items allocates and clears"); + auto* bytes = static_cast(block); + for (int i = 0; i < 12; ++i) bytes[i] = static_cast(i + 1); + + error = -1; + void* grown = ft_mem_realloc_nid_postfix(&recorder.memory, 4, 3, 5, block, &error); + Require(grown && error == 0 && recorder.reallocs == 1 && recorder.lastCurrent == 12 && recorder.lastNew == 20, "realloc resizes through memory->realloc"); + bytes = static_cast(grown); + bool kept = true; + for (int i = 0; i < 12; ++i) kept = kept && bytes[i] == i + 1; + Require(kept && Filled(bytes + 12, 8, 0), "realloc keeps the old items and clears the new ones"); + + std::memset(bytes, 0x22, 20); + error = -1; + void* shrunk = ft_mem_realloc_nid_postfix(&recorder.memory, 4, 5, 2, grown, &error); + Require(shrunk && error == 0 && recorder.lastCurrent == 20 && recorder.lastNew == 8 && Filled(shrunk, 8, 0x22), "realloc shrinks without clearing"); + + error = -1; + Require(ft_mem_realloc_nid_postfix(&recorder.memory, 4, 2, -3, shrunk, &error) == shrunk && error == 0x06 && Filled(shrunk, 8, 0x22), "realloc errors keep the block unchanged"); + recorder.fail = true; + error = -1; + Require(ft_mem_realloc_nid_postfix(&recorder.memory, 4, 2, 6, shrunk, &error) == shrunk && error == 0x40 && Filled(shrunk, 8, 0x22), "realloc keeps the block when realloc fails"); + recorder.fail = false; + ft_mem_free_nid_postfix(&recorder.memory, shrunk); +} + +void Duplication() { + Recorder recorder; + Reset(recorder); + const char source[7] = {'a', 'b', 'c', 0, 'd', 'e', 'f'}; + std::int32_t error = -1; + void* copy = ft_mem_dup_nid_postfix(&recorder.memory, source, sizeof(source), &error); + Require(copy && error == 0 && recorder.allocs == 1 && recorder.lastSize == 7, "dup allocates size bytes"); + Require(std::memcmp(copy, source, sizeof(source)) == 0, "dup copies every byte, including embedded zeros"); + ft_mem_free_nid_postfix(&recorder.memory, copy); + + error = -1; + Require(!ft_mem_dup_nid_postfix(&recorder.memory, source, 0, &error) && error == 0 && recorder.allocs == 1, "dup of size 0 returns null"); + error = -1; + void* uncopied = ft_mem_dup_nid_postfix(&recorder.memory, nullptr, 4, &error); + Require(uncopied && error == 0 && recorder.allocs == 2 && Filled(uncopied, 4, 0xAA), "dup of a null address allocates without copying"); + ft_mem_free_nid_postfix(&recorder.memory, uncopied); + error = -1; + Require(!ft_mem_dup_nid_postfix(&recorder.memory, source, 0x8000000000000000ull, &error) && error == 0x06 && recorder.allocs == 2, "dup of a size above FT_Long is Invalid_Argument"); + + error = -1; + char* text = static_cast(ft_mem_strdup_nid_postfix(&recorder.memory, "hello", &error)); + Require(text && error == 0 && recorder.lastSize == 6 && std::strcmp(text, "hello") == 0, "strdup copies strlen + 1 bytes"); + ft_mem_free_nid_postfix(&recorder.memory, text); + const int allocs = recorder.allocs; + error = -1; + Require(!ft_mem_strdup_nid_postfix(&recorder.memory, nullptr, &error) && error == 0 && recorder.allocs == allocs, "strdup of null returns null without allocating"); + recorder.fail = true; + error = -1; + Require(!ft_mem_strdup_nid_postfix(&recorder.memory, "x", &error) && error == 0x40, "strdup reports Out_Of_Memory when alloc fails"); +} + +void BoundedCopy() { + char buffer[8]; + std::memset(buffer, 'x', sizeof(buffer)); + Require(ft_mem_strcpyn_nid_postfix(buffer, "abc", sizeof(buffer)) == 0 && std::strcmp(buffer, "abc") == 0, "strcpyn copies a string that fits and returns 0"); + std::memset(buffer, 'x', sizeof(buffer)); + Require(ft_mem_strcpyn_nid_postfix(buffer, "abc", 4) == 0 && std::strcmp(buffer, "abc") == 0, "strcpyn fits size - 1 characters"); + std::memset(buffer, 'x', sizeof(buffer)); + Require(ft_mem_strcpyn_nid_postfix(buffer, "abcdef", 4) == 1 && std::strcmp(buffer, "abc") == 0 && buffer[4] == 'x', "strcpyn truncates to size - 1 characters and returns 1"); + std::memset(buffer, 'x', sizeof(buffer)); + Require(ft_mem_strcpyn_nid_postfix(buffer, "a", 1) == 1 && buffer[0] == 0 && buffer[1] == 'x', "strcpyn of size 1 writes only the terminator"); + std::memset(buffer, 'x', sizeof(buffer)); + Require(ft_mem_strcpyn_nid_postfix(buffer, "a", 0) == 1 && buffer[0] == 0 && buffer[1] == 'x', "strcpyn of size 0 still writes the terminator"); + std::memset(buffer, 'x', sizeof(buffer)); + Require(ft_mem_strcpyn_nid_postfix(buffer, "", 5) == 0 && buffer[0] == 0 && buffer[1] == 'x', "strcpyn of an empty string returns 0"); +} + +} + +int main() { + QuickAllocation(); + ClearedAllocation(); + QuickReallocation(); + ClearedReallocation(); + Duplication(); + BoundedCopy(); + std::puts("Guest FreeType Ot memory tests passed"); + return 0; +} diff --git a/docs/dev/debt/libSceFreeTypeOt.md b/docs/dev/debt/libSceFreeTypeOt.md index 7013351fc5..39cdf41974 100644 --- a/docs/dev/debt/libSceFreeTypeOt.md +++ b/docs/dev/debt/libSceFreeTypeOt.md @@ -1,7 +1,7 @@ # libSceFreeTypeOt technical debt - The FT_Outline_Get_Orientation, FT_Request_Size, FT_Sin, FT_Tan, FT_Vector_Unit, FT_New_Size, FT_Outline_Copy, FT_Outline_Decompose, FT_Stroker_New, FT_Stroker_Done, FT_Stroker_GetCounts, FT_Stroker_BeginSubPath, FT_Stroker_LineTo, FT_Stroker_EndSubPath, FT_Stroker_Set, FT_Stroker_Rewind, FT_Stroker_GetBorderCounts, FT_Stroker_ConicTo and FT_Stroker_CubicTo NIDs in [Export.cpp](../../../core/libs/prx/libSceFreeTypeOt/Export.cpp) are identified, but their provider-specific prototypes and behavior remain unverified. Their named helpers retain the explicit inventory aliases and throw; the separate libSceFreeType implementations do not supply this provider. -- The 120 `sceFreeTypeOtUnknown_*_nid_no_patch` exports in [Export.cpp](../../../core/libs/prx/libSceFreeTypeOt/Export.cpp) are present in the PS5 3.20 export inventory from [PS5-3.20_Libs](https://github.com/DNNDHH/PS5-3.20_Libs). Their prototypes and behavior are unverified. Each uses the no-parameter import-resolution convention and throws through `NotImplemented_nid_no_patch`; these declarations make no ABI or behavior claim. +- The 83 `sceFreeTypeOtUnknown_*_nid_no_patch` exports in [Export.cpp](../../../core/libs/prx/libSceFreeTypeOt/Export.cpp) are present in the PS5 3.20 export inventory from [PS5-3.20_Libs](https://github.com/DNNDHH/PS5-3.20_Libs). Their prototypes and behavior are unverified. Each uses the no-parameter import-resolution convention and throws through `NotImplemented_nid_no_patch`; these declarations make no ABI or behavior claim. ### Unknown function info - [FT_Outline_GetInsideBorder, FT_Outline_GetOutsideBorder](../../../core/libs/prx/libSceFreeTypeOt/CMakeLists.txt) (libSceFreeTypeOl, libSceFreeTypeOt) - console prototypes, enum/layout equivalence, FreeType configuration and module/library versions are unverified. The existing LP64 outline layout and unsigned 16-bit counts follow bundled FreeType 2.13.3; border selection follows [ftstroke.c](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftstroke.c), with the shifted polygon area from [ftoutln.c](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftoutln.c) accumulated in 64 bits. Null positive-count arrays and out-of-range or non-increasing contour endpoints throw when reached; their console behavior is unknown. Tags and flags are not read, and input arrays remain caller-owned. @@ -13,3 +13,5 @@ - [ft_corner_is_flat, ft_corner_orientation](../../../core/libs/prx/libSceFreeTypeOt/Export.cpp) (libSceFreeTypeOt) - console prototypes and FreeType version equivalence are unverified. The LP64 signatures and 64-bit coordinate calculations follow bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftcalc.c): orientation returns +1 for counter-clockwise turns, -1 for clockwise turns and 0 for collinear inputs using 64-bit coordinate cross-products; flatness compares hypotenuse approximations via the alpha-max-plus-beta-min metric and returns 1 when the corner angle deviation does not exceed the threshold, 0 otherwise. - [FT_MulDiv_No_Round, FT_Matrix_Multiply_Scaled, FT_Vector_Transform_Scaled, ft_highpow2](../../../core/libs/prx/libSceFreeTypeOt/Export.cpp) (libSceFreeTypeOt) - console prototypes and the FT_LONG64 build option are unverified. FT_Property_Set and ft_highpow2 date this FreeType to 2.4.11-2.5.3, whose stock [ftconfig.h](https://github.com/freetype/freetype/blob/cecf93ef90c660a8b0b45e5adbbfb5ea443fb6b9/include/freetype/config/ftconfig.h#L289-L306) drops FT_LONG64 under `__STDC__` and compiles the 32-bit [FT_MulDiv](https://github.com/freetype/freetype/blob/cecf93ef90c660a8b0b45e5adbbfb5ea443fb6b9/src/base/ftcalc.c#L411-L479). Inputs where that build, the FT_LONG64 build and bundled 2.13.3 can differ throw without writing: an operand above 0x7FFFFFFF in magnitude, a zero divisor with a zero factor, or a quotient above 0xFFFFFFFF. The scaled calls divide each term by 0x10000 * scaling, so |scaling| > 32767 throws; a null matrix or vector is a no-op unless that product overflows. + +- [ft_mem_alloc, ft_mem_qalloc, ft_mem_realloc, ft_mem_qrealloc, ft_mem_free, ft_mem_dup, ft_mem_strdup, ft_mem_strcpyn](../../../core/libs/prx/libSceFreeTypeOt/src/Memory.cpp) (libSceFreeTypeOt) - the exports carry the NIDs of these FreeType names; console prototypes and FreeType version equivalence are unverified. The LP64 signatures, the guest `FT_MemoryRec` callbacks and the error codes (Invalid_Argument 0x06, Array_Too_Large 0x0A, Out_Of_Memory 0x40) follow bundled FreeType 2.13.3 [ftutil.c](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftutil.c#L48). As in a release build, `ft_mem_qrealloc` does not check the `FT_ASSERT` that a block with no current items is null. The current byte size passed to `realloc` wraps where the source's signed product would overflow. From 2354d1d59fcd9907ee20e5ae8990f37c778d4ff3 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 29 Oct 2026 09:19:44 +0000 Subject: [PATCH 31/51] refactor(libSceFreeTypeOt): reuse FreeType::GuestMemoryRec for the ft_mem helpers --- .../prx/libSceFreeType/include/GuestFreeType.hpp | 8 -------- .../libs/prx/libSceFreeTypeOt/include/Memory.hpp | 16 ++++++++-------- core/libs/prx/libSceFreeTypeOt/src/Memory.cpp | 14 +++++++------- core/libs/tests/GuestFreeTypeOtMemory.cpp | 14 +++++++------- 4 files changed, 22 insertions(+), 30 deletions(-) diff --git a/core/libs/prx/libSceFreeType/include/GuestFreeType.hpp b/core/libs/prx/libSceFreeType/include/GuestFreeType.hpp index 6e18b7ce0d..67af76996e 100644 --- a/core/libs/prx/libSceFreeType/include/GuestFreeType.hpp +++ b/core/libs/prx/libSceFreeType/include/GuestFreeType.hpp @@ -15,13 +15,6 @@ struct GuestGeneric { void (APS5_VABI *finalizer)(void*); }; -struct GuestMemory { - void* user; - void* (APS5_VABI *alloc)(GuestMemory*, std::int64_t); - void (APS5_VABI *free)(GuestMemory*, void*); - void* (APS5_VABI *realloc)(GuestMemory*, std::int64_t, std::int64_t, void*); -}; - struct GuestVector { std::int64_t x; std::int64_t y; @@ -198,7 +191,6 @@ struct GuestPropIncreaseXHeight { static_assert(sizeof(GuestMatrix) == 32 && offsetof(GuestMatrix, xy) == 8 && offsetof(GuestMatrix, yx) == 16 && offsetof(GuestMatrix, yy) == 24); static_assert(sizeof(GuestGeneric) == 16 && offsetof(GuestGeneric, finalizer) == 8); -static_assert(sizeof(GuestMemory) == 32 && offsetof(GuestMemory, alloc) == 8 && offsetof(GuestMemory, free) == 16 && offsetof(GuestMemory, realloc) == 24); static_assert(sizeof(GuestBitmapSize) == 32 && offsetof(GuestBitmapSize, size) == 8); static_assert(sizeof(GuestCharMapRec) == 16 && offsetof(GuestCharMapRec, platform_id) == 12); static_assert(sizeof(GuestSfntName) == 24 && offsetof(GuestSfntName, string) == 8 && offsetof(GuestSfntName, string_len) == 16); diff --git a/core/libs/prx/libSceFreeTypeOt/include/Memory.hpp b/core/libs/prx/libSceFreeTypeOt/include/Memory.hpp index 6f3aa42892..b0453f8844 100644 --- a/core/libs/prx/libSceFreeTypeOt/include/Memory.hpp +++ b/core/libs/prx/libSceFreeTypeOt/include/Memory.hpp @@ -1,18 +1,18 @@ #pragma once -#include "prx/libSceFreeType/include/GuestFreeType.hpp" +#include "prx/libSceFreeType/include/GuestList.hpp" #include extern "C" { -void* APS5_VABI ft_mem_alloc_nid_postfix(FreeType::GuestMemory* memory, std::int64_t size, std::int32_t* error); -void* APS5_VABI ft_mem_qalloc_nid_postfix(FreeType::GuestMemory* memory, std::int64_t size, std::int32_t* error); -void* APS5_VABI ft_mem_realloc_nid_postfix(FreeType::GuestMemory* memory, std::int64_t itemSize, std::int64_t currentCount, std::int64_t newCount, void* block, std::int32_t* error); -void* APS5_VABI ft_mem_qrealloc_nid_postfix(FreeType::GuestMemory* memory, std::int64_t itemSize, std::int64_t currentCount, std::int64_t newCount, void* block, std::int32_t* error); -void APS5_VABI ft_mem_free_nid_postfix(FreeType::GuestMemory* memory, const void* block); -void* APS5_VABI ft_mem_dup_nid_postfix(FreeType::GuestMemory* memory, const void* address, std::uint64_t size, std::int32_t* error); -void* APS5_VABI ft_mem_strdup_nid_postfix(FreeType::GuestMemory* memory, const char* string, std::int32_t* error); +void* APS5_VABI ft_mem_alloc_nid_postfix(FreeType::GuestMemoryRec* memory, std::int64_t size, std::int32_t* error); +void* APS5_VABI ft_mem_qalloc_nid_postfix(FreeType::GuestMemoryRec* memory, std::int64_t size, std::int32_t* error); +void* APS5_VABI ft_mem_realloc_nid_postfix(FreeType::GuestMemoryRec* memory, std::int64_t itemSize, std::int64_t currentCount, std::int64_t newCount, void* block, std::int32_t* error); +void* APS5_VABI ft_mem_qrealloc_nid_postfix(FreeType::GuestMemoryRec* memory, std::int64_t itemSize, std::int64_t currentCount, std::int64_t newCount, void* block, std::int32_t* error); +void APS5_VABI ft_mem_free_nid_postfix(FreeType::GuestMemoryRec* memory, const void* block); +void* APS5_VABI ft_mem_dup_nid_postfix(FreeType::GuestMemoryRec* memory, const void* address, std::uint64_t size, std::int32_t* error); +void* APS5_VABI ft_mem_strdup_nid_postfix(FreeType::GuestMemoryRec* memory, const char* string, std::int32_t* error); std::int32_t APS5_VABI ft_mem_strcpyn_nid_postfix(char* destination, const char* source, std::uint64_t size); } diff --git a/core/libs/prx/libSceFreeTypeOt/src/Memory.cpp b/core/libs/prx/libSceFreeTypeOt/src/Memory.cpp index 832fe71cba..7b7d93d289 100644 --- a/core/libs/prx/libSceFreeTypeOt/src/Memory.cpp +++ b/core/libs/prx/libSceFreeTypeOt/src/Memory.cpp @@ -19,7 +19,7 @@ std::int64_t Bytes(std::int64_t count, std::int64_t itemSize) { extern "C" { -void* APS5_VABI ft_mem_qalloc_nid_postfix(FreeType::GuestMemory* memory, std::int64_t size, std::int32_t* error) { +void* APS5_VABI ft_mem_qalloc_nid_postfix(FreeType::GuestMemoryRec* memory, std::int64_t size, std::int32_t* error) { std::int32_t result = 0; void* block = nullptr; if (size > 0) { @@ -32,7 +32,7 @@ void* APS5_VABI ft_mem_qalloc_nid_postfix(FreeType::GuestMemory* memory, std::in return block; } -void* APS5_VABI ft_mem_alloc_nid_postfix(FreeType::GuestMemory* memory, std::int64_t size, std::int32_t* error) { +void* APS5_VABI ft_mem_alloc_nid_postfix(FreeType::GuestMemoryRec* memory, std::int64_t size, std::int32_t* error) { std::int32_t result = 0; void* block = ft_mem_qalloc_nid_postfix(memory, size, &result); if (!result && block && size > 0) std::memset(block, 0, static_cast(size)); @@ -40,11 +40,11 @@ void* APS5_VABI ft_mem_alloc_nid_postfix(FreeType::GuestMemory* memory, std::int return block; } -void APS5_VABI ft_mem_free_nid_postfix(FreeType::GuestMemory* memory, const void* block) { +void APS5_VABI ft_mem_free_nid_postfix(FreeType::GuestMemoryRec* memory, const void* block) { if (block) memory->free(memory, const_cast(block)); } -void* APS5_VABI ft_mem_qrealloc_nid_postfix(FreeType::GuestMemory* memory, std::int64_t itemSize, std::int64_t currentCount, +void* APS5_VABI ft_mem_qrealloc_nid_postfix(FreeType::GuestMemoryRec* memory, std::int64_t itemSize, std::int64_t currentCount, std::int64_t newCount, void* block, std::int32_t* error) { std::int32_t result = 0; if (currentCount < 0 || newCount < 0 || itemSize < 0) { @@ -69,7 +69,7 @@ void* APS5_VABI ft_mem_qrealloc_nid_postfix(FreeType::GuestMemory* memory, std:: return block; } -void* APS5_VABI ft_mem_realloc_nid_postfix(FreeType::GuestMemory* memory, std::int64_t itemSize, std::int64_t currentCount, +void* APS5_VABI ft_mem_realloc_nid_postfix(FreeType::GuestMemoryRec* memory, std::int64_t itemSize, std::int64_t currentCount, std::int64_t newCount, void* block, std::int32_t* error) { std::int32_t result = 0; block = ft_mem_qrealloc_nid_postfix(memory, itemSize, currentCount, newCount, block, &result); @@ -80,7 +80,7 @@ void* APS5_VABI ft_mem_realloc_nid_postfix(FreeType::GuestMemory* memory, std::i return block; } -void* APS5_VABI ft_mem_dup_nid_postfix(FreeType::GuestMemory* memory, const void* address, std::uint64_t size, std::int32_t* error) { +void* APS5_VABI ft_mem_dup_nid_postfix(FreeType::GuestMemoryRec* memory, const void* address, std::uint64_t size, std::int32_t* error) { std::int32_t result = 0; void* copy = ft_mem_qalloc_nid_postfix(memory, static_cast(size), &result); if (!result && address && size > 0) std::memcpy(copy, address, static_cast(size)); @@ -88,7 +88,7 @@ void* APS5_VABI ft_mem_dup_nid_postfix(FreeType::GuestMemory* memory, const void return copy; } -void* APS5_VABI ft_mem_strdup_nid_postfix(FreeType::GuestMemory* memory, const char* string, std::int32_t* error) { +void* APS5_VABI ft_mem_strdup_nid_postfix(FreeType::GuestMemoryRec* memory, const char* string, std::int32_t* error) { const std::uint64_t length = string ? std::strlen(string) + 1 : 0; return ft_mem_dup_nid_postfix(memory, string, length, error); } diff --git a/core/libs/tests/GuestFreeTypeOtMemory.cpp b/core/libs/tests/GuestFreeTypeOtMemory.cpp index eee4f331a8..39dac619fc 100644 --- a/core/libs/tests/GuestFreeTypeOtMemory.cpp +++ b/core/libs/tests/GuestFreeTypeOtMemory.cpp @@ -6,10 +6,10 @@ namespace { -using FreeType::GuestMemory; +using FreeType::GuestMemoryRec; struct Recorder { - GuestMemory memory{}; + GuestMemoryRec memory{}; int allocs = 0; int frees = 0; int reallocs = 0; @@ -17,15 +17,15 @@ struct Recorder { std::int64_t lastCurrent = 0; std::int64_t lastNew = 0; void* lastBlock = nullptr; - GuestMemory* lastMemory = nullptr; + GuestMemoryRec* lastMemory = nullptr; bool fail = false; }; -Recorder* Owner(GuestMemory* memory) { +Recorder* Owner(GuestMemoryRec* memory) { return static_cast(memory->user); } -void* APS5_VABI RecordAlloc(GuestMemory* memory, std::int64_t size) { +void* APS5_VABI RecordAlloc(GuestMemoryRec* memory, std::int64_t size) { auto* recorder = Owner(memory); ++recorder->allocs; recorder->lastMemory = memory; @@ -36,7 +36,7 @@ void* APS5_VABI RecordAlloc(GuestMemory* memory, std::int64_t size) { return block; } -void APS5_VABI RecordFree(GuestMemory* memory, void* block) { +void APS5_VABI RecordFree(GuestMemoryRec* memory, void* block) { auto* recorder = Owner(memory); ++recorder->frees; recorder->lastMemory = memory; @@ -44,7 +44,7 @@ void APS5_VABI RecordFree(GuestMemory* memory, void* block) { std::free(block); } -void* APS5_VABI RecordRealloc(GuestMemory* memory, std::int64_t current, std::int64_t size, void* block) { +void* APS5_VABI RecordRealloc(GuestMemoryRec* memory, std::int64_t current, std::int64_t size, void* block) { auto* recorder = Owner(memory); ++recorder->reallocs; recorder->lastMemory = memory; From 86fcd7b59e8334f5c1dc120cff5c1e5ed2502463 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 29 Oct 2026 12:16:55 +0000 Subject: [PATCH 32/51] fix(libSceFreeTypeOt): zero-fill ft_mem_qrealloc from a count of 0, as FreeType 2.4.12 does --- core/libs/prx/libSceFreeTypeOt/src/Memory.cpp | 3 +-- core/libs/tests/GuestFreeTypeOtMemory.cpp | 2 +- docs/dev/debt/libSceFreeTypeOt.md | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceFreeTypeOt/src/Memory.cpp b/core/libs/prx/libSceFreeTypeOt/src/Memory.cpp index 7b7d93d289..95a2712409 100644 --- a/core/libs/prx/libSceFreeTypeOt/src/Memory.cpp +++ b/core/libs/prx/libSceFreeTypeOt/src/Memory.cpp @@ -55,8 +55,7 @@ void* APS5_VABI ft_mem_qrealloc_nid_postfix(FreeType::GuestMemoryRec* memory, st } else if (newCount > IntMax / itemSize) { result = ArrayTooLarge; } else if (currentCount == 0) { - block = memory->alloc(memory, newCount * itemSize); - if (!block) result = OutOfMemory; + block = ft_mem_alloc_nid_postfix(memory, newCount * itemSize, &result); } else { void* resized = memory->realloc(memory, Bytes(currentCount, itemSize), newCount * itemSize, block); if (resized) { diff --git a/core/libs/tests/GuestFreeTypeOtMemory.cpp b/core/libs/tests/GuestFreeTypeOtMemory.cpp index 39dac619fc..1a1d9691a8 100644 --- a/core/libs/tests/GuestFreeTypeOtMemory.cpp +++ b/core/libs/tests/GuestFreeTypeOtMemory.cpp @@ -125,7 +125,7 @@ void QuickReallocation() { std::int32_t error = -1; void* block = ft_mem_qrealloc_nid_postfix(&recorder.memory, 4, 0, 3, nullptr, &error); Require(block && error == 0 && recorder.allocs == 1 && recorder.lastSize == 12 && recorder.reallocs == 0, "qrealloc from zero items allocates new_count * item_size"); - Require(Filled(block, 12, 0xAA), "qrealloc from zero items leaves the block uncleared"); + Require(Filled(block, 12, 0), "qrealloc from zero items clears the new block, as ft_mem_alloc does"); std::memset(block, 0x11, 12); error = -1; diff --git a/docs/dev/debt/libSceFreeTypeOt.md b/docs/dev/debt/libSceFreeTypeOt.md index 39cdf41974..0994235958 100644 --- a/docs/dev/debt/libSceFreeTypeOt.md +++ b/docs/dev/debt/libSceFreeTypeOt.md @@ -14,4 +14,4 @@ - [FT_MulDiv_No_Round, FT_Matrix_Multiply_Scaled, FT_Vector_Transform_Scaled, ft_highpow2](../../../core/libs/prx/libSceFreeTypeOt/Export.cpp) (libSceFreeTypeOt) - console prototypes and the FT_LONG64 build option are unverified. FT_Property_Set and ft_highpow2 date this FreeType to 2.4.11-2.5.3, whose stock [ftconfig.h](https://github.com/freetype/freetype/blob/cecf93ef90c660a8b0b45e5adbbfb5ea443fb6b9/include/freetype/config/ftconfig.h#L289-L306) drops FT_LONG64 under `__STDC__` and compiles the 32-bit [FT_MulDiv](https://github.com/freetype/freetype/blob/cecf93ef90c660a8b0b45e5adbbfb5ea443fb6b9/src/base/ftcalc.c#L411-L479). Inputs where that build, the FT_LONG64 build and bundled 2.13.3 can differ throw without writing: an operand above 0x7FFFFFFF in magnitude, a zero divisor with a zero factor, or a quotient above 0xFFFFFFFF. The scaled calls divide each term by 0x10000 * scaling, so |scaling| > 32767 throws; a null matrix or vector is a no-op unless that product overflows. -- [ft_mem_alloc, ft_mem_qalloc, ft_mem_realloc, ft_mem_qrealloc, ft_mem_free, ft_mem_dup, ft_mem_strdup, ft_mem_strcpyn](../../../core/libs/prx/libSceFreeTypeOt/src/Memory.cpp) (libSceFreeTypeOt) - the exports carry the NIDs of these FreeType names; console prototypes and FreeType version equivalence are unverified. The LP64 signatures, the guest `FT_MemoryRec` callbacks and the error codes (Invalid_Argument 0x06, Array_Too_Large 0x0A, Out_Of_Memory 0x40) follow bundled FreeType 2.13.3 [ftutil.c](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftutil.c#L48). As in a release build, `ft_mem_qrealloc` does not check the `FT_ASSERT` that a block with no current items is null. The current byte size passed to `realloc` wraps where the source's signed product would overflow. +- [ft_mem_alloc, ft_mem_qalloc, ft_mem_realloc, ft_mem_qrealloc, ft_mem_free, ft_mem_dup, ft_mem_strdup, ft_mem_strcpyn](../../../core/libs/prx/libSceFreeTypeOt/src/Memory.cpp) (libSceFreeTypeOt) - the exports carry the NIDs of these FreeType names; console prototypes and FreeType version equivalence are unverified. The LP64 signatures, the guest `FT_MemoryRec` callbacks and the error codes (Invalid_Argument 0x06, Array_Too_Large 0x0A, Out_Of_Memory 0x40) follow FreeType 2.4.12 [ftutil.c](https://github.com/freetype/freetype/blob/cecf93ef90c660a8b0b45e5adbbfb5ea443fb6b9/src/base/ftutil.c#L49-L221). Bundled FreeType 2.13.3 differs only in `ft_mem_qrealloc` from a count of 0, which it does not zero. As in a release build, `ft_mem_qrealloc` does not check the `FT_ASSERT` that a block with no current items is null. The current byte size passed to `realloc` wraps where the source's signed product would overflow. From db82fffab34cb7cdf75fd3df70ba0b176cde4ec1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0?= Date: Fri, 30 Oct 2026 08:20:15 +0100 Subject: [PATCH 33/51] fix(libSceFreeTypeOt): include Memory.hpp from the library umbrella header Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libSceFreeTypeOt/libSceFreeTypeOt.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/prx/libSceFreeTypeOt/libSceFreeTypeOt.hpp b/core/libs/prx/libSceFreeTypeOt/libSceFreeTypeOt.hpp index 00e2da7277..1155682fa1 100644 --- a/core/libs/prx/libSceFreeTypeOt/libSceFreeTypeOt.hpp +++ b/core/libs/prx/libSceFreeTypeOt/libSceFreeTypeOt.hpp @@ -3,3 +3,4 @@ #include "prx/libSceFreeTypeOt/include/Stream.hpp" #include "prx/libSceFreeType/include/Api.hpp" #include "prx/libSceFreeTypeOt/include/Calc.hpp" +#include "prx/libSceFreeTypeOt/include/Memory.hpp" From 7cc20a0d0dedaf5625e1919aa8ec3e9e3332ba2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0?= Date: Fri, 30 Oct 2026 08:21:09 +0100 Subject: [PATCH 34/51] fix(libSceFreeTypeOt): drop the Unknown stubs that carry the ft_mem NIDs Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libSceFreeTypeOt/Export.cpp | 40 ----------------------- docs/dev/debt/libSceFreeTypeOt.md | 2 +- 2 files changed, 1 insertion(+), 41 deletions(-) diff --git a/core/libs/prx/libSceFreeTypeOt/Export.cpp b/core/libs/prx/libSceFreeTypeOt/Export.cpp index 8669bd9fe2..da2d170fba 100644 --- a/core/libs/prx/libSceFreeTypeOt/Export.cpp +++ b/core/libs/prx/libSceFreeTypeOt/Export.cpp @@ -590,46 +590,6 @@ int APS5_VABI sceFreeTypeOtUnknown_AEU2tkjdZ2w_nid_no_patch() { } APS5_EXPORT("AEU2tkjdZ2w", sceFreeTypeOtUnknown_AEU2tkjdZ2w_nid_no_patch); -int APS5_VABI sceFreeTypeOtUnknown_fPrLu1YGOaw_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("fPrLu1YGOaw", sceFreeTypeOtUnknown_fPrLu1YGOaw_nid_no_patch); - -int APS5_VABI sceFreeTypeOtUnknown_t3vCgnT5YL0_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("t3vCgnT5YL0", sceFreeTypeOtUnknown_t3vCgnT5YL0_nid_no_patch); - -int APS5_VABI sceFreeTypeOtUnknown_8bkQ_YRnpOE_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("8bkQ+YRnpOE", sceFreeTypeOtUnknown_8bkQ_YRnpOE_nid_no_patch); - -int APS5_VABI sceFreeTypeOtUnknown_q4qhGebYuGI_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("q4qhGebYuGI", sceFreeTypeOtUnknown_q4qhGebYuGI_nid_no_patch); - -int APS5_VABI sceFreeTypeOtUnknown_ni_K_leoQNg_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("ni-K-leoQNg", sceFreeTypeOtUnknown_ni_K_leoQNg_nid_no_patch); - -int APS5_VABI sceFreeTypeOtUnknown_XfcgJB6V2a0_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("XfcgJB6V2a0", sceFreeTypeOtUnknown_XfcgJB6V2a0_nid_no_patch); - -int APS5_VABI sceFreeTypeOtUnknown_TWzf5iykiGw_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("TWzf5iykiGw", sceFreeTypeOtUnknown_TWzf5iykiGw_nid_no_patch); - -int APS5_VABI sceFreeTypeOtUnknown__b4iVKArt94_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("-b4iVKArt94", sceFreeTypeOtUnknown__b4iVKArt94_nid_no_patch); - int APS5_VABI sceFreeTypeOtUnknown_uSNqxrARpEQ_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } diff --git a/docs/dev/debt/libSceFreeTypeOt.md b/docs/dev/debt/libSceFreeTypeOt.md index 0994235958..880f53c69e 100644 --- a/docs/dev/debt/libSceFreeTypeOt.md +++ b/docs/dev/debt/libSceFreeTypeOt.md @@ -1,7 +1,7 @@ # libSceFreeTypeOt technical debt - The FT_Outline_Get_Orientation, FT_Request_Size, FT_Sin, FT_Tan, FT_Vector_Unit, FT_New_Size, FT_Outline_Copy, FT_Outline_Decompose, FT_Stroker_New, FT_Stroker_Done, FT_Stroker_GetCounts, FT_Stroker_BeginSubPath, FT_Stroker_LineTo, FT_Stroker_EndSubPath, FT_Stroker_Set, FT_Stroker_Rewind, FT_Stroker_GetBorderCounts, FT_Stroker_ConicTo and FT_Stroker_CubicTo NIDs in [Export.cpp](../../../core/libs/prx/libSceFreeTypeOt/Export.cpp) are identified, but their provider-specific prototypes and behavior remain unverified. Their named helpers retain the explicit inventory aliases and throw; the separate libSceFreeType implementations do not supply this provider. -- The 83 `sceFreeTypeOtUnknown_*_nid_no_patch` exports in [Export.cpp](../../../core/libs/prx/libSceFreeTypeOt/Export.cpp) are present in the PS5 3.20 export inventory from [PS5-3.20_Libs](https://github.com/DNNDHH/PS5-3.20_Libs). Their prototypes and behavior are unverified. Each uses the no-parameter import-resolution convention and throws through `NotImplemented_nid_no_patch`; these declarations make no ABI or behavior claim. +- The 75 `sceFreeTypeOtUnknown_*_nid_no_patch` exports in [Export.cpp](../../../core/libs/prx/libSceFreeTypeOt/Export.cpp) are present in the PS5 3.20 export inventory from [PS5-3.20_Libs](https://github.com/DNNDHH/PS5-3.20_Libs). Their prototypes and behavior are unverified. Each uses the no-parameter import-resolution convention and throws through `NotImplemented_nid_no_patch`; these declarations make no ABI or behavior claim. ### Unknown function info - [FT_Outline_GetInsideBorder, FT_Outline_GetOutsideBorder](../../../core/libs/prx/libSceFreeTypeOt/CMakeLists.txt) (libSceFreeTypeOl, libSceFreeTypeOt) - console prototypes, enum/layout equivalence, FreeType configuration and module/library versions are unverified. The existing LP64 outline layout and unsigned 16-bit counts follow bundled FreeType 2.13.3; border selection follows [ftstroke.c](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftstroke.c), with the shifted polygon area from [ftoutln.c](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftoutln.c) accumulated in 64 bits. Null positive-count arrays and out-of-range or non-increasing contour endpoints throw when reached; their console behavior is unknown. Tags and flags are not read, and input arrays remain caller-owned. From a5dc1ebf2b7e1ff10cea3ba61d526bd6ff344853 Mon Sep 17 00:00:00 2001 From: frankea Date: Thu, 29 Oct 2026 12:45:08 -0400 Subject: [PATCH 35/51] 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 36/51] 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 37/51] 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 38/51] 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 39/51] 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 40/51] 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 41/51] 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 42/51] 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 43/51] 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 44/51] 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 45/51] 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 46/51] 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 47/51] 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 48/51] 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 49/51] 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 50/51] 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. From a3b667b92e0692791cce09449a886553ec30a300 Mon Sep 17 00:00:00 2001 From: lakhantheboss <61951686+lakhantheboss@users.noreply.github.com> Date: Fri, 30 Oct 2026 15:12:56 +0530 Subject: [PATCH 51/51] docs(libSceRtc): document measured leap-second parsing behavior --- docs/dev/debt/libSceRtc.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libSceRtc.md b/docs/dev/debt/libSceRtc.md index 32629a0137..62b5d6b939 100644 --- a/docs/dev/debt/libSceRtc.md +++ b/docs/dev/debt/libSceRtc.md @@ -21,4 +21,5 @@ ### Functional +- [sceRtcParseRFC3339, sceRtcParseDateTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - when `second == 60`, both functions return 0 (success) but do not write the tick for the leap second; they still subtract the timezone offset from the existing output tick. `sceRtcParseDateTime` has the same behavior for digit-led (RFC 3339 style) input. Measured on PS5 firmware 13.60; see PR #1397. - The process-local clocks set by [sceRtcSetCurrentTick, sceRtcSetCurrentNetworkTick, sceRtcSetCurrentAdNetworkTick and sceRtcSetCurrentDebugNetworkTick](../../../core/libs/prx/libSceRtc/Export.cpp) are visible only through libSceRtc. The system clock and libkernel clock APIs keep reporting host time; cross-library clock consistency is not modelled. The console's clock interactions are not measured