From 50bc39a50db7b91298dbac24963d1925a1f9845e Mon Sep 17 00:00:00 2001 From: RockENZO <125934469+RockENZO@users.noreply.github.com> Date: Tue, 20 Oct 2026 09:00:02 +1030 Subject: [PATCH 001/198] fix(relinker): preserve weak guest exports on macOS --- core/relinker/CMakeLists.txt | 3 + .../src/macos/MacOsGuestModuleWriter.cpp | 5 +- .../tests/test_macos_guest_weak_exports.py | 125 ++++++++++++++++++ 3 files changed, 131 insertions(+), 2 deletions(-) create mode 100644 core/relinker/relinker/tests/test_macos_guest_weak_exports.py diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index d2a3387683..ffdb7331d5 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -802,6 +802,9 @@ if(BUILD_TESTING) endif() if(BUILD_TESTING AND Python3_Interpreter_FOUND) + add_test(NAME macos_guest_weak_exports COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_macos_guest_weak_exports.py $) + set_tests_properties(macos_guest_weak_exports PROPERTIES TIMEOUT 30 LABELS relinker) add_test(NAME macos_executable_exports COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_macos_executable_exports.py $) set_tests_properties(macos_executable_exports PROPERTIES TIMEOUT 30 LABELS relinker) diff --git a/core/relinker/elfpatcher/src/macos/MacOsGuestModuleWriter.cpp b/core/relinker/elfpatcher/src/macos/MacOsGuestModuleWriter.cpp index 74e31d19d3..5e52b38403 100644 --- a/core/relinker/elfpatcher/src/macos/MacOsGuestModuleWriter.cpp +++ b/core/relinker/elfpatcher/src/macos/MacOsGuestModuleWriter.cpp @@ -96,11 +96,12 @@ std::vector GuestModuleWriter::WriteMacOs(const Relinker::GuestIma if (symbol.Section == 0 || symbol.Section == Relinker::AbsoluteSection || (symbol.Info >> 4) == 0 || symbol.Visibility == 1 || symbol.Visibility == 2) continue; if (symbol.Section >= 0xff00) throw Domain::RelinkerException("Unsupported guest export section: " + symbol.Name); const auto name = "_" + DynamicName(guest, index); + const auto flags = (symbol.Info >> 4) == 2 ? ExportWeakDefinition : std::uint64_t{0}; if ((symbol.Info & 15) == 6) { - input.TlsExports.push_back({name, symbol.Value}); + input.TlsExports.push_back({name, symbol.Value, flags}); continue; } - input.Exports.push_back({name, image.GetRva(symbol.Value, std::max(symbol.Size, 1))}); + input.Exports.push_back({name, image.GetRva(symbol.Value, std::max(symbol.Size, 1)), flags}); } for (const auto& dependency : dependencies) input.Dylibs.push_back(MachODependency(dependency, "@loader_path")); diff --git a/core/relinker/relinker/tests/test_macos_guest_weak_exports.py b/core/relinker/relinker/tests/test_macos_guest_weak_exports.py new file mode 100644 index 0000000000..c1f0e8a740 --- /dev/null +++ b/core/relinker/relinker/tests/test_macos_guest_weak_exports.py @@ -0,0 +1,125 @@ +import os +from pathlib import Path +import platform +import shutil +import struct +import subprocess +import sys +import tempfile + +from test_guest_intel_trampolines import main_fixture +from test_guest_symbol_names import guest_module +from test_macos_executable_exports import exports +from test_macos_output import MachO + + +def module(binding, tls=False): + image = guest_module([('Sample', True), ('Data', True)] + ([('Tls', True)] if tls else [])) + image[0x400:0x406] = bytes.fromhex('b807000000c3') + image[0x89C] = binding << 4 | 2 + struct.pack_into('\n#include \n' + 'int main(int argc, char **argv) {\n' + ' if (argc == 1) return 0;\n' + ' void *provider = dlopen(argv[1], RTLD_NOW | RTLD_GLOBAL);\n' + ' if (!provider) { puts(dlerror()); return 1; }\n' + ' void *consumer = dlopen(argv[2], RTLD_NOW | RTLD_GLOBAL);\n' + ' if (!consumer) { puts(dlerror()); return 2; }\n' + ' int (*read)(void) = dlsym(consumer, "Read");\n' + ' if (!read) return 3;\n' + ' printf("%d\\n", read());\n' + ' return 0;\n}\n', library=False) + command = (['/usr/bin/arch', '-x86_64'] if platform.machine() == 'arm64' else []) + [str(probe)] + run = subprocess.run(command, capture_output=True, text=True, timeout=10) + if run.returncode and 'Bad CPU type in executable' in run.stderr: + assert os.environ.get('GITHUB_ACTIONS') != 'true', 'macOS CI must execute the x86_64 weak-export probe' + return None + assert run.returncode == 0, (run.returncode, run.stdout, run.stderr) + kernel = compile_c(work, 'libkernel.prx', 'int FixtureKernel(void) { return 0; }\n') + consumer = compile_c(work, 'consumer.dylib', + 'int Sample(void) __asm__("_Sample#guest");\n' + 'int Data __asm__("_Data#guest");\n' + '__attribute__((weak)) int Sample(void) { return 77; }\n' + '__attribute__((weak)) int Data = 88;\n' + 'int Read(void) { return Sample() + Data; }\n') + controls = {} + for binding in (1, 2): + attribute = '__attribute__((weak)) ' if binding == 2 else '' + controls[binding] = compile_c(work, f'native-{binding}.dylib', + 'int Sample(void) __asm__("_Sample#guest");\n' + 'int Data __asm__("_Data#guest");\n' + + attribute + 'int Sample(void) { return 7; }\n' + + attribute + 'int Data = 11;\n') + return command, kernel, consumer, controls + + +def main(): + relinker = Path(sys.argv[1]).resolve() + with tempfile.TemporaryDirectory(prefix='anyps5-macos-weak-exports-') as directory: + work = Path(directory) + native = native_probe(work) + for binding in (1, 2): + for tls in (False, True): + case = work / f'{binding}-{tls}' + modules = case / 'sce_module' + modules.mkdir(parents=True) + (modules / 'sample.prx').write_bytes(module(binding, tls)) + source = case / 'input.elf' + source.write_bytes(main_fixture()) + run = subprocess.run([str(relinker), '--macos', str(source), str(case / 'output')], + capture_output=True, text=True, timeout=10) + assert run.returncode == 0, (binding, tls, run.stdout, run.stderr) + converted = case / 'app0/sce_module/sample.prx.guest.prx' + data = converted.read_bytes() + entries = exports(data) + flags = 4 if binding == 2 else 0 + if native and not tls: + command, kernel, consumer, controls = native + libraries = case / 'libs' + libraries.mkdir() + shutil.copyfile(kernel, libraries / 'libkernel.prx') + results = [] + for provider in (controls[binding], converted): + probe = subprocess.run([*command, str(provider), str(consumer)], + capture_output=True, text=True, timeout=10) + assert probe.returncode == 0, (binding, probe.returncode, probe.stdout, probe.stderr) + results.append(probe.stdout.strip()) + assert results[0] == results[1], (binding, results) + if binding == 2: + assert results[0] == '18', results + assert set(entries) == {'_Sample#guest', '_Data#guest'} | ({'_Tls#guest'} if tls else set()), entries + assert all(value[0] == flags for value in entries.values()), (binding, tls, entries) + assert bool(MachO(data).flags & 0x8000) == (binding == 2) + if native: + print('macOS guest weak exports: x86_64 function/data coalescing matches native weak and strong controls') + print('macOS guest weak export tests passed') + + +if __name__ == '__main__': + main() From bcc5960d8407f2e0bf181bcabbfe8b314022aa7f Mon Sep 17 00:00:00 2001 From: orezraey Date: Tue, 20 Oct 2026 19:23:47 -0300 Subject: [PATCH 002/198] fix(libkernel): follow sceKernelSyncOnAddressWait and Wake as measured on a PS5 Co-Authored-By: Claude Opus 5.5 --- .../SyncOnAddress/src/SyncOnAddress.cpp | 92 +++++--- core/libs/tests/GuestRaiseException.cpp | 4 +- core/libs/tests/GuestSyncOnAddress.cpp | 223 +++++++++++------- docs/dev/debt/libkernel.md | 3 +- 4 files changed, 189 insertions(+), 133 deletions(-) diff --git a/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp b/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp index 710d0bc069..2555b6f450 100644 --- a/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp +++ b/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -73,11 +74,20 @@ void DeferringDelivery(TFunction function) { } template -std::optional WaitOnAddressLocked(TValue* address, TValue expected, std::optional deadlineNanos, bool& woken) { +TValue LoadAddressValue(TValue* address) { + if (reinterpret_cast(address) % alignof(TValue) == 0) return std::atomic_ref(*address).load(); + TValue value; + std::memcpy(&value, address, sizeof(value)); + return value; +} + +template +std::optional WaitOnAddressLocked(TValue* address, TValue expected, std::optional deadlineNanos, bool& woken, bool& differed) { const auto key = reinterpret_cast(address); std::unique_lock lock(g_waitersLock); - if (std::atomic_ref(*address).load() != expected) { + if (LoadAddressValue(address) != expected) { woken = true; + differed = true; return std::nullopt; } @@ -104,10 +114,10 @@ std::optional WaitOnAddressLocked(TValue* address, TVa } template -bool WaitOnAddressUntil(TValue* address, TValue expected, std::optional deadlineNanos, const void* caller) { +bool WaitOnAddressUntil(TValue* address, TValue expected, std::optional deadlineNanos, const void* caller, bool& differed) { bool woken = false; std::optional waited; - DeferringDelivery([&] { waited = WaitOnAddressLocked(address, expected, deadlineNanos, woken); }); + DeferringDelivery([&] { waited = WaitOnAddressLocked(address, expected, deadlineNanos, woken, differed); }); if (waited) { KernelTraceWait_nid_postfix("addr", caller, static_cast(waited->count()), !woken); } @@ -115,19 +125,41 @@ bool WaitOnAddressUntil(TValue* address, TValue expected, std::optional -int WaitOnAddress(TValue* address, TValue expected, const KernelUseconds* timeout, const void* caller) { - const auto deadline = timeout == nullptr ? std::nullopt : std::optional(TimedWait::DeadlineNanos(*timeout)); - return WaitOnAddressUntil(address, expected, deadline, caller) ? SYNC_ON_ADDRESS_OK : SCE_KERNEL_ERROR_ETIMEDOUT; +bool WaitOnAddressUntil(TValue* address, TValue expected, std::optional deadlineNanos, const void* caller) { + bool differed = false; + return WaitOnAddressUntil(address, expected, deadlineNanos, caller, differed); } -void WakeAddressLocked(std::uintptr_t key, std::int32_t count, bool wakesAtLeastOne) { +template +int WaitOnAddress(TValue* address, TValue expected, KernelUseconds* timeout, const void* caller) { + if (timeout != nullptr && *timeout == 0) { + return SCE_KERNEL_ERROR_ETIMEDOUT; + } + if (address == nullptr) { + return SCE_KERNEL_ERROR_EFAULT; + } + const auto deadline = timeout == nullptr ? std::nullopt : std::optional(TimedWait::DeadlineNanos(*timeout)); + bool differed = false; + const bool woken = WaitOnAddressUntil(address, expected, deadline, caller, differed); + if (differed) { + return SCE_KERNEL_ERROR_EAGAIN; + } + if (timeout != nullptr) { + const std::uint64_t now = TimedWait::NowNanos(); + *timeout = woken && *deadline > now ? static_cast((*deadline - now) / 1000) : 0; + } + return woken ? SYNC_ON_ADDRESS_OK : SCE_KERNEL_ERROR_ETIMEDOUT; +} + +std::int32_t WakeAddressLocked(std::uintptr_t key, std::int32_t count, bool wakesAtLeastOne) { std::lock_guard lock(g_waitersLock); const auto entry = g_waiters.find(key); if (entry == g_waiters.end()) { - return; + return 0; } auto& queue = entry->second; - for (std::int32_t woken = 0; !queue.empty() && (woken < count || (wakesAtLeastOne && woken == 0)); ++woken) { + std::int32_t woken = 0; + for (; !queue.empty() && (woken < count || (wakesAtLeastOne && woken == 0)); ++woken) { AddressWaiter* waiter = queue.front(); queue.pop_front(); waiter->woken = true; @@ -136,10 +168,13 @@ void WakeAddressLocked(std::uintptr_t key, std::int32_t count, bool wakesAtLeast if (queue.empty()) { g_waiters.erase(entry); } + return woken; } -void WakeAddress(std::uintptr_t key, std::int32_t count, bool wakesAtLeastOne) { - DeferringDelivery([&] { WakeAddressLocked(key, count, wakesAtLeastOne); }); +std::int32_t WakeAddress(std::uintptr_t key, std::int32_t count, bool wakesAtLeastOne) { + std::int32_t woken = 0; + DeferringDelivery([&] { woken = WakeAddressLocked(key, count, wakesAtLeastOne); }); + return woken; } std::uint64_t SaturatingNanos(std::int64_t seconds, std::int64_t nanos) { @@ -232,54 +267,33 @@ int UmtxNwakePrivate(void* object, std::uint64_t value) { extern "C" { -int APS5_VABI sceKernelSyncOnAddressWait(std::uint32_t* address, std::uint32_t expected, const KernelUseconds* timeout, const char* name) { +int APS5_VABI sceKernelSyncOnAddressWait(std::uint32_t* address, std::uint32_t expected, KernelUseconds* timeout, const char* name) { (void)name; - if (!IsAlignedAddress(reinterpret_cast(address))) { - APS5_INVALID_ARG_EX; - } return WaitOnAddress(address, expected, timeout, __builtin_return_address(0)); } -int APS5_VABI sceKernelSyncOnAddressWait8(std::uint8_t* address, std::uint8_t expected, const KernelUseconds* timeout, const char* name) { +int APS5_VABI sceKernelSyncOnAddressWait8(std::uint8_t* address, std::uint8_t expected, KernelUseconds* timeout, const char* name) { (void)name; - if (!IsAlignedAddress(reinterpret_cast(address))) { - APS5_INVALID_ARG_EX; - } return WaitOnAddress(address, expected, timeout, __builtin_return_address(0)); } -int APS5_VABI sceKernelSyncOnAddressWait16(std::uint16_t* address, std::uint16_t expected, const KernelUseconds* timeout, const char* name) { +int APS5_VABI sceKernelSyncOnAddressWait16(std::uint16_t* address, std::uint16_t expected, KernelUseconds* timeout, const char* name) { (void)name; - if (!IsAlignedAddress(reinterpret_cast(address))) { - APS5_INVALID_ARG_EX; - } return WaitOnAddress(address, expected, timeout, __builtin_return_address(0)); } -int APS5_VABI sceKernelSyncOnAddressWait32(std::uint32_t* address, std::uint32_t expected, const KernelUseconds* timeout, const char* name) { +int APS5_VABI sceKernelSyncOnAddressWait32(std::uint32_t* address, std::uint32_t expected, KernelUseconds* timeout, const char* name) { (void)name; - if (!IsAlignedAddress(reinterpret_cast(address))) { - APS5_INVALID_ARG_EX; - } return WaitOnAddress(address, expected, timeout, __builtin_return_address(0)); } -int APS5_VABI sceKernelSyncOnAddressWait64(std::uint64_t* address, std::uint64_t expected, const KernelUseconds* timeout, const char* name) { +int APS5_VABI sceKernelSyncOnAddressWait64(std::uint64_t* address, std::uint64_t expected, KernelUseconds* timeout, const char* name) { (void)name; - if (!IsAlignedAddress(reinterpret_cast(address))) { - APS5_INVALID_ARG_EX; - } return WaitOnAddress(address, expected, timeout, __builtin_return_address(0)); } int APS5_VABI sceKernelSyncOnAddressWake(void* address, std::int32_t count) { - const auto key = reinterpret_cast(address); - if (!IsAlignedAddress(key) || count < 0) { - APS5_INVALID_ARG_EX; - } - - WakeAddress(key, count, false); - return SYNC_ON_ADDRESS_OK; + return WakeAddress(reinterpret_cast(address), count, true); } int APS5_VABI _umtx_op_nid_postfix(void* object, int operation, std::uint64_t value, void* sizeArgument, void* timeArgument) { diff --git a/core/libs/tests/GuestRaiseException.cpp b/core/libs/tests/GuestRaiseException.cpp index fd74e36771..d43345ea5a 100644 --- a/core/libs/tests/GuestRaiseException.cpp +++ b/core/libs/tests/GuestRaiseException.cpp @@ -33,7 +33,7 @@ int APS5_VABI scePthreadRwlockInit(PthreadRwlock* rwlock, const PthreadRwlockatt int APS5_VABI scePthreadRwlockDestroy(PthreadRwlock* rwlock); int APS5_VABI scePthreadRwlockWrlock(PthreadRwlock* rwlock); int APS5_VABI scePthreadRwlockUnlock(PthreadRwlock* rwlock); -int APS5_VABI sceKernelSyncOnAddressWait(std::uint32_t* address, std::uint32_t expected, const KernelUseconds* timeout, const char* name); +int APS5_VABI sceKernelSyncOnAddressWait(std::uint32_t* address, std::uint32_t expected, KernelUseconds* timeout, const char* name); int APS5_VABI sceKernelSyncOnAddressWake(void* address, std::int32_t count); } @@ -425,7 +425,7 @@ static void* APS5_VABI AddressWaking(void* arg) { auto& worker = *static_cast(arg); worker.id = std::this_thread::get_id(); worker.started.store(true); - while (!worker.stop.load()) Require(sceKernelSyncOnAddressWake(&addressWord, 1) == 0); + while (!worker.stop.load()) Require(sceKernelSyncOnAddressWake(&addressWord, 1) >= 0); return nullptr; } diff --git a/core/libs/tests/GuestSyncOnAddress.cpp b/core/libs/tests/GuestSyncOnAddress.cpp index c43e7a66fe..fc116edff7 100644 --- a/core/libs/tests/GuestSyncOnAddress.cpp +++ b/core/libs/tests/GuestSyncOnAddress.cpp @@ -4,33 +4,29 @@ #include #include #include -#include +#include #include #include extern "C" { -int APS5_VABI sceKernelSyncOnAddressWait(std::uint32_t* address, std::uint32_t expected, const KernelUseconds* timeout, const char* name); -int APS5_VABI sceKernelSyncOnAddressWait32(std::uint32_t* address, std::uint32_t expected, const KernelUseconds* timeout, const char* name); -int APS5_VABI sceKernelSyncOnAddressWait64(std::uint64_t* address, std::uint64_t expected, const KernelUseconds* timeout, const char* name); -int APS5_VABI sceKernelSyncOnAddressWait8(std::uint8_t* address, std::uint8_t expected, const KernelUseconds* timeout, const char* name); -int APS5_VABI sceKernelSyncOnAddressWait16(std::uint16_t* address, std::uint16_t expected, const KernelUseconds* timeout, const char* name); +int APS5_VABI sceKernelSyncOnAddressWait(std::uint32_t* address, std::uint32_t expected, KernelUseconds* timeout, const char* name); +int APS5_VABI sceKernelSyncOnAddressWait32(std::uint32_t* address, std::uint32_t expected, KernelUseconds* timeout, const char* name); +int APS5_VABI sceKernelSyncOnAddressWait64(std::uint64_t* address, std::uint64_t expected, KernelUseconds* timeout, const char* name); +int APS5_VABI sceKernelSyncOnAddressWait8(std::uint8_t* address, std::uint8_t expected, KernelUseconds* timeout, const char* name); +int APS5_VABI sceKernelSyncOnAddressWait16(std::uint16_t* address, std::uint16_t expected, KernelUseconds* timeout, const char* name); int APS5_VABI sceKernelSyncOnAddressWake(void* address, std::int32_t count); } static constexpr int SCE_OK = 0; +static constexpr int SCE_KERNEL_ERROR_EFAULT = static_cast(0x8002000E); +static constexpr int SCE_KERNEL_ERROR_EAGAIN = static_cast(0x80020023); static constexpr int SCE_KERNEL_ERROR_ETIMEDOUT = static_cast(0x8002003C); static constexpr KernelUseconds FAILSAFE_TIMEOUT = 10000000; +static constexpr KernelUseconds SHORT_TIMEOUT = 1000; static constexpr auto SETTLE = std::chrono::milliseconds(100); static void Require(bool value) { if (!value) std::abort(); } -template -static bool Rejects(TCall call) { - try { call(); } - catch (const std::invalid_argument&) { return true; } - return false; -} - static void AwaitCount(const std::atomic& counter, int value) { const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); while (counter.load() != value) { @@ -39,81 +35,93 @@ static void AwaitCount(const std::atomic& counter, int value) { } } -static void ReturnsWhenTheValueDiffers() { +static void FailsAgainWhenTheValueDiffers() { std::uint32_t word = 2; - Require(sceKernelSyncOnAddressWait(&word, 1, nullptr, "differs") == SCE_OK); - const KernelUseconds timeout = 0; - Require(sceKernelSyncOnAddressWait(&word, 1, &timeout, nullptr) == SCE_OK); + Require(sceKernelSyncOnAddressWait(&word, 1, nullptr, "differs") == SCE_KERNEL_ERROR_EAGAIN); + KernelUseconds timeout = SHORT_TIMEOUT; + Require(sceKernelSyncOnAddressWait(&word, 1, &timeout, nullptr) == SCE_KERNEL_ERROR_EAGAIN && timeout == SHORT_TIMEOUT); + timeout = 0; + Require(sceKernelSyncOnAddressWait(&word, 1, &timeout, nullptr) == SCE_KERNEL_ERROR_ETIMEDOUT); } static void TimesOutWhileTheValueMatches() { std::uint32_t word = 1; - const KernelUseconds none = 0; - Require(sceKernelSyncOnAddressWait(&word, 1, &none, "timeout") == SCE_KERNEL_ERROR_ETIMEDOUT); + KernelUseconds none = 0; + Require(sceKernelSyncOnAddressWait(&word, 1, &none, "timeout") == SCE_KERNEL_ERROR_ETIMEDOUT && none == 0); - const KernelUseconds timeout = 20000; + KernelUseconds timeout = 20000; const auto start = std::chrono::steady_clock::now(); Require(sceKernelSyncOnAddressWait(&word, 1, &timeout, "timeout") == SCE_KERNEL_ERROR_ETIMEDOUT); Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(15)); - Require(sceKernelSyncOnAddressWake(&word, INT_MAX) == SCE_OK); + Require(timeout == 0); + Require(sceKernelSyncOnAddressWake(&word, INT_MAX) == 0); } -static void WakeReleasesAWaiterWithoutAValueChange() { +static void WakeReleasesAWaiterAndReportsTheTimeLeft() { std::uint32_t word = 1; + std::atomic started{0}; std::atomic finished{0}; int result = -1; + KernelUseconds timeout = FAILSAFE_TIMEOUT; std::thread waiter([&] { - result = sceKernelSyncOnAddressWait(&word, 1, &FAILSAFE_TIMEOUT, "wake"); + ++started; + result = sceKernelSyncOnAddressWait(&word, 1, &timeout, "wake"); ++finished; }); + AwaitCount(started, 1); + std::this_thread::sleep_for(SETTLE); + int woken = 0; while (finished.load() == 0) { - Require(sceKernelSyncOnAddressWake(&word, 1) == SCE_OK); + woken += sceKernelSyncOnAddressWake(&word, 1); std::this_thread::sleep_for(std::chrono::milliseconds(1)); } waiter.join(); - Require(result == SCE_OK); + Require(result == SCE_OK && woken == 1); + Require(timeout > 0 && timeout < FAILSAFE_TIMEOUT); } -static void WakeHonoursTheCountAndTheAddress() { +static void WakeCountsOfOneOrLessWakeOneWaiter() { std::uint32_t word = 1; std::uint32_t other = 1; std::atomic started{0}; std::atomic finished{0}; std::atomic otherFinished{0}; - std::vector results(3, -1); + std::vector results(5, -1); int otherResult = -1; std::vector waiters; - for (int i = 0; i < 3; ++i) { + for (int i = 0; i < 5; ++i) { waiters.emplace_back([&, i] { ++started; - results[i] = sceKernelSyncOnAddressWait(&word, 1, &FAILSAFE_TIMEOUT, "count"); + KernelUseconds timeout = FAILSAFE_TIMEOUT; + results[i] = sceKernelSyncOnAddressWait(&word, 1, &timeout, "count"); ++finished; }); } std::thread otherWaiter([&] { ++started; - otherResult = sceKernelSyncOnAddressWait(&other, 1, &FAILSAFE_TIMEOUT, "other"); + KernelUseconds timeout = FAILSAFE_TIMEOUT; + otherResult = sceKernelSyncOnAddressWait(&other, 1, &timeout, "other"); ++otherFinished; }); - AwaitCount(started, 4); + AwaitCount(started, 6); std::this_thread::sleep_for(SETTLE); - Require(sceKernelSyncOnAddressWake(&word, 0) == SCE_OK); - std::this_thread::sleep_for(SETTLE); - Require(finished.load() == 0); + int expected = 0; + for (const std::int32_t count : {0, -1, 1}) { + Require(sceKernelSyncOnAddressWake(&word, count) == 1); + AwaitCount(finished, ++expected); + std::this_thread::sleep_for(SETTLE); + Require(finished.load() == expected); + } - Require(sceKernelSyncOnAddressWake(&word, 1) == SCE_OK); - AwaitCount(finished, 1); - std::this_thread::sleep_for(SETTLE); - Require(finished.load() == 1); - - Require(sceKernelSyncOnAddressWake(&word, INT_MAX) == SCE_OK); - AwaitCount(finished, 3); + Require(sceKernelSyncOnAddressWake(&word, INT_MAX) == 2); + AwaitCount(finished, 5); std::this_thread::sleep_for(SETTLE); Require(otherFinished.load() == 0); + Require(sceKernelSyncOnAddressWake(&word, INT_MAX) == 0); - Require(sceKernelSyncOnAddressWake(&other, 1) == SCE_OK); + Require(sceKernelSyncOnAddressWake(&other, 1) == 1); for (auto& waiter : waiters) waiter.join(); otherWaiter.join(); for (const int result : results) Require(result == SCE_OK); @@ -125,38 +133,42 @@ static void WaitsOnTheLowHalfOfAnOnceState() { auto* word = reinterpret_cast(&state); std::thread waiter([&] { while (state.load() == 1) { - Require(sceKernelSyncOnAddressWait(word, 1, nullptr, "once") == SCE_OK); + const int result = sceKernelSyncOnAddressWait(word, 1, nullptr, "once"); + Require(result == SCE_OK || result == SCE_KERNEL_ERROR_EAGAIN); } }); std::this_thread::sleep_for(std::chrono::milliseconds(5)); state.store(2); - Require(sceKernelSyncOnAddressWake(word, INT_MAX) == SCE_OK); + Require(sceKernelSyncOnAddressWake(word, INT_MAX) >= 0); waiter.join(); } static void SizedWaitsTimeOutWhileTheValueMatches() { std::uint32_t word = 5; std::uint64_t value = 0x500000005ULL; - const KernelUseconds none = 0; - Require(sceKernelSyncOnAddressWait32(&word, 4, &none, "differs") == SCE_OK); + KernelUseconds timeout = SHORT_TIMEOUT; + Require(sceKernelSyncOnAddressWait32(&word, 4, &timeout, "differs") == SCE_KERNEL_ERROR_EAGAIN); + KernelUseconds none = 0; + Require(sceKernelSyncOnAddressWait32(&word, 4, &none, "differs") == SCE_KERNEL_ERROR_ETIMEDOUT); Require(sceKernelSyncOnAddressWait32(&word, 5, &none, "timeout") == SCE_KERNEL_ERROR_ETIMEDOUT); Require(sceKernelSyncOnAddressWait64(&value, 0x500000005ULL, &none, "timeout") == SCE_KERNEL_ERROR_ETIMEDOUT); - const KernelUseconds timeout = 20000; + timeout = 20000; auto start = std::chrono::steady_clock::now(); - Require(sceKernelSyncOnAddressWait32(&word, 5, &timeout, "timeout") == SCE_KERNEL_ERROR_ETIMEDOUT); + Require(sceKernelSyncOnAddressWait32(&word, 5, &timeout, "timeout") == SCE_KERNEL_ERROR_ETIMEDOUT && timeout == 0); Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(15)); + timeout = 20000; start = std::chrono::steady_clock::now(); - Require(sceKernelSyncOnAddressWait64(&value, 0x500000005ULL, &timeout, "timeout") == SCE_KERNEL_ERROR_ETIMEDOUT); + Require(sceKernelSyncOnAddressWait64(&value, 0x500000005ULL, &timeout, "timeout") == SCE_KERNEL_ERROR_ETIMEDOUT && timeout == 0); Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(15)); } static void Wait64ComparesTheHighHalf() { std::uint64_t value = 0x100000001ULL; - const KernelUseconds none = 0; - Require(sceKernelSyncOnAddressWait64(&value, 0x1ULL, &none, "high") == SCE_OK); - Require(sceKernelSyncOnAddressWait64(&value, 0x200000001ULL, &none, "high") == SCE_OK); - Require(sceKernelSyncOnAddressWait64(&value, 0x100000001ULL, &none, "high") == SCE_KERNEL_ERROR_ETIMEDOUT); + KernelUseconds timeout = SHORT_TIMEOUT; + Require(sceKernelSyncOnAddressWait64(&value, 0x1ULL, &timeout, "high") == SCE_KERNEL_ERROR_EAGAIN); + Require(sceKernelSyncOnAddressWait64(&value, 0x200000001ULL, &timeout, "high") == SCE_KERNEL_ERROR_EAGAIN); + Require(sceKernelSyncOnAddressWait64(&value, 0x100000001ULL, &timeout, "high") == SCE_KERNEL_ERROR_ETIMEDOUT); } static void WakeReleasesWaitersOfEverySize() { @@ -169,23 +181,26 @@ static void WakeReleasesWaitersOfEverySize() { std::vector waiters; waiters.emplace_back([&] { ++started; - results[0] = sceKernelSyncOnAddressWait(word, 1, &FAILSAFE_TIMEOUT, "sized"); + KernelUseconds timeout = FAILSAFE_TIMEOUT; + results[0] = sceKernelSyncOnAddressWait(word, 1, &timeout, "sized"); ++finished; }); waiters.emplace_back([&] { ++started; - results[1] = sceKernelSyncOnAddressWait32(word, 1, &FAILSAFE_TIMEOUT, "sized"); + KernelUseconds timeout = FAILSAFE_TIMEOUT; + results[1] = sceKernelSyncOnAddressWait32(word, 1, &timeout, "sized"); ++finished; }); waiters.emplace_back([&] { ++started; - results[2] = sceKernelSyncOnAddressWait64(&value, 1, &FAILSAFE_TIMEOUT, "sized"); + KernelUseconds timeout = FAILSAFE_TIMEOUT; + results[2] = sceKernelSyncOnAddressWait64(&value, 1, &timeout, "sized"); ++finished; }); AwaitCount(started, 3); std::this_thread::sleep_for(SETTLE); - Require(sceKernelSyncOnAddressWake(&value, 1) == SCE_OK); + Require(sceKernelSyncOnAddressWake(&value, 1) == 1); AwaitCount(finished, 1); std::this_thread::sleep_for(SETTLE); Require(finished.load() == 1); @@ -193,7 +208,7 @@ static void WakeReleasesWaitersOfEverySize() { const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); while (finished.load() != 3) { Require(std::chrono::steady_clock::now() < deadline); - Require(sceKernelSyncOnAddressWake(&value, INT_MAX) == SCE_OK); + Require(sceKernelSyncOnAddressWake(&value, INT_MAX) >= 0); std::this_thread::sleep_for(std::chrono::milliseconds(1)); } for (auto& waiter : waiters) waiter.join(); @@ -201,14 +216,15 @@ static void WakeReleasesWaitersOfEverySize() { } static void NarrowWaitsCompareOnlyTheirWidth() { - const KernelUseconds none = 0; + KernelUseconds timeout = SHORT_TIMEOUT; alignas(4) std::uint8_t bytes[4] = {1, 5, 7, 7}; - Require(sceKernelSyncOnAddressWait8(&bytes[1], 4, &none, "differs") == SCE_OK); - Require(sceKernelSyncOnAddressWait8(&bytes[1], 5, &none, "timeout") == SCE_KERNEL_ERROR_ETIMEDOUT); + Require(sceKernelSyncOnAddressWait8(&bytes[1], 4, &timeout, "differs") == SCE_KERNEL_ERROR_EAGAIN); + Require(sceKernelSyncOnAddressWait8(&bytes[1], 5, &timeout, "timeout") == SCE_KERNEL_ERROR_ETIMEDOUT); + timeout = SHORT_TIMEOUT; alignas(4) std::uint16_t halves[2] = {1, 0x0705}; - Require(sceKernelSyncOnAddressWait16(&halves[1], 0x0005, &none, "high") == SCE_OK); - Require(sceKernelSyncOnAddressWait16(&halves[1], 0x0705, &none, "timeout") == SCE_KERNEL_ERROR_ETIMEDOUT); + Require(sceKernelSyncOnAddressWait16(&halves[1], 0x0005, &timeout, "high") == SCE_KERNEL_ERROR_EAGAIN); + Require(sceKernelSyncOnAddressWait16(&halves[1], 0x0705, &timeout, "timeout") == SCE_KERNEL_ERROR_ETIMEDOUT); } static void WakeReleasesNarrowWaitersAtTheirOwnAddress() { @@ -224,26 +240,28 @@ static void WakeReleasesNarrowWaitersAtTheirOwnAddress() { std::thread byteWaiter([&] { ++started; - byteResult = sceKernelSyncOnAddressWait8(&bytes[1], 3, &FAILSAFE_TIMEOUT, "byte"); + KernelUseconds timeout = FAILSAFE_TIMEOUT; + byteResult = sceKernelSyncOnAddressWait8(&bytes[1], 3, &timeout, "byte"); ++byteFinished; }); std::thread halfWaiter([&] { ++started; - halfResult = sceKernelSyncOnAddressWait16(half, 9, &FAILSAFE_TIMEOUT, "half"); + KernelUseconds timeout = FAILSAFE_TIMEOUT; + halfResult = sceKernelSyncOnAddressWait16(half, 9, &timeout, "half"); ++halfFinished; }); AwaitCount(started, 2); std::this_thread::sleep_for(SETTLE); - Require(sceKernelSyncOnAddressWake(&bytes[0], INT_MAX) == SCE_OK); - Require(sceKernelSyncOnAddressWake(&bytes[3], INT_MAX) == SCE_OK); + Require(sceKernelSyncOnAddressWake(&bytes[0], INT_MAX) == 0); + Require(sceKernelSyncOnAddressWake(&bytes[3], INT_MAX) == 0); std::this_thread::sleep_for(SETTLE); Require(byteFinished.load() == 0 && halfFinished.load() == 0); - Require(sceKernelSyncOnAddressWake(&bytes[1], 1) == SCE_OK); + Require(sceKernelSyncOnAddressWake(&bytes[1], 1) == 1); AwaitCount(byteFinished, 1); Require(halfFinished.load() == 0); - Require(sceKernelSyncOnAddressWake(half, 1) == SCE_OK); + Require(sceKernelSyncOnAddressWake(half, 1) == 1); AwaitCount(halfFinished, 1); byteWaiter.join(); @@ -251,36 +269,61 @@ static void WakeReleasesNarrowWaitersAtTheirOwnAddress() { Require(byteResult == SCE_OK && halfResult == SCE_OK); } -static void RejectsInvalidArguments() { - std::uint32_t words[2] = {1, 1}; - auto* misaligned = reinterpret_cast(reinterpret_cast(words) + 1); - Require(Rejects([&] { sceKernelSyncOnAddressWait(nullptr, 1, nullptr, nullptr); })); - Require(Rejects([&] { sceKernelSyncOnAddressWait(misaligned, 1, nullptr, nullptr); })); - Require(Rejects([&] { sceKernelSyncOnAddressWait32(nullptr, 1, nullptr, nullptr); })); - Require(Rejects([&] { sceKernelSyncOnAddressWait32(misaligned, 1, nullptr, nullptr); })); - Require(Rejects([&] { sceKernelSyncOnAddressWake(nullptr, 1); })); - Require(sceKernelSyncOnAddressWake(misaligned, 1) == SCE_OK); - Require(Rejects([&] { sceKernelSyncOnAddressWait8(nullptr, 1, nullptr, nullptr); })); - Require(Rejects([&] { sceKernelSyncOnAddressWait16(nullptr, 1, nullptr, nullptr); })); - Require(Rejects([&] { sceKernelSyncOnAddressWait16(reinterpret_cast(reinterpret_cast(words) + 1), 1, nullptr, nullptr); })); - Require(Rejects([&] { sceKernelSyncOnAddressWake(words, -1); })); +static void MisalignedAddressesWaitAndWake() { + alignas(16) unsigned char storage[16] = {}; + auto* word = reinterpret_cast(storage + 1); + auto* quad = reinterpret_cast(storage + 4); + const std::uint32_t wordValue = 0x11223344; + std::memcpy(word, &wordValue, sizeof(wordValue)); + KernelUseconds timeout = SHORT_TIMEOUT; + Require(sceKernelSyncOnAddressWait32(word, 1, &timeout, "misaligned") == SCE_KERNEL_ERROR_EAGAIN); + Require(sceKernelSyncOnAddressWait32(word, wordValue, &timeout, "misaligned") == SCE_KERNEL_ERROR_ETIMEDOUT && timeout == 0); + timeout = SHORT_TIMEOUT; + Require(sceKernelSyncOnAddressWait64(quad, 1, &timeout, "misaligned") == SCE_KERNEL_ERROR_EAGAIN); + timeout = SHORT_TIMEOUT; + Require(sceKernelSyncOnAddressWait16(reinterpret_cast(storage + 9), 1, &timeout, "misaligned") == SCE_KERNEL_ERROR_EAGAIN); - std::uint64_t values[2] = {1, 1}; - auto* wordAligned = reinterpret_cast(reinterpret_cast(values) + sizeof(std::uint32_t)); - Require(Rejects([&] { sceKernelSyncOnAddressWait64(nullptr, 1, nullptr, nullptr); })); - Require(Rejects([&] { sceKernelSyncOnAddressWait64(wordAligned, 1, nullptr, nullptr); })); + std::atomic started{0}; + int result = -1; + std::thread waiter([&] { + ++started; + KernelUseconds waitTimeout = FAILSAFE_TIMEOUT; + result = sceKernelSyncOnAddressWait32(word, wordValue, &waitTimeout, "misaligned"); + }); + AwaitCount(started, 1); + std::this_thread::sleep_for(SETTLE); + Require(sceKernelSyncOnAddressWake(storage, INT_MAX) == 0); + Require(sceKernelSyncOnAddressWake(word, 1) == 1); + waiter.join(); + Require(result == SCE_OK); +} + +static void NullAddresses() { + KernelUseconds timeout = SHORT_TIMEOUT; + Require(sceKernelSyncOnAddressWait(nullptr, 1, &timeout, nullptr) == SCE_KERNEL_ERROR_EFAULT && timeout == SHORT_TIMEOUT); + Require(sceKernelSyncOnAddressWait8(nullptr, 1, nullptr, nullptr) == SCE_KERNEL_ERROR_EFAULT); + Require(sceKernelSyncOnAddressWait16(nullptr, 1, nullptr, nullptr) == SCE_KERNEL_ERROR_EFAULT); + Require(sceKernelSyncOnAddressWait32(nullptr, 1, nullptr, nullptr) == SCE_KERNEL_ERROR_EFAULT); + Require(sceKernelSyncOnAddressWait64(nullptr, 1, nullptr, nullptr) == SCE_KERNEL_ERROR_EFAULT); + KernelUseconds none = 0; + Require(sceKernelSyncOnAddressWait32(nullptr, 1, &none, nullptr) == SCE_KERNEL_ERROR_ETIMEDOUT); + Require(sceKernelSyncOnAddressWake(nullptr, 1) == 0); + Require(sceKernelSyncOnAddressWake(nullptr, 0) == 0); + std::uint32_t word = 0; + Require(sceKernelSyncOnAddressWake(&word, -1) == 0); } int main() { - ReturnsWhenTheValueDiffers(); + FailsAgainWhenTheValueDiffers(); TimesOutWhileTheValueMatches(); - WakeReleasesAWaiterWithoutAValueChange(); - WakeHonoursTheCountAndTheAddress(); + WakeReleasesAWaiterAndReportsTheTimeLeft(); + WakeCountsOfOneOrLessWakeOneWaiter(); WaitsOnTheLowHalfOfAnOnceState(); SizedWaitsTimeOutWhileTheValueMatches(); Wait64ComparesTheHighHalf(); WakeReleasesWaitersOfEverySize(); NarrowWaitsCompareOnlyTheirWidth(); WakeReleasesNarrowWaitersAtTheirOwnAddress(); - RejectsInvalidArguments(); + MisalignedAddressesWaitAndWake(); + NullAddresses(); } diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index e1ebad64ed..446d43b95e 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -54,8 +54,7 @@ - [Protecting a host thread's stack](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - on Windows `sceKernelIsStack` reports a host thread's whole stack reservation, which Windows commits on demand from the top down behind a guard page. A protect over that range changes only the pages above the guard; the guard page and the reserved pages below it are left as they are, so the stack still grows and an overflow still raises a stack overflow. A protect that starts at the bottom of the reservation and stops short of the guard page, as HotSpot makes its stack guard zones, is committed and protected. Windows moves the guard page down by committing the pages below it as read/write guard pages, whether they were reserved or committed, so those pages come in read/write as the stack grows to within one guard region of them (three pages on GitHub's runners). A protection other than read/write on them, `PROT_NONE` included, does not hold once the stack is that close, where on the console every page of the reported range takes the protection. Holding it would mean removing the thread's guard page and keeping the stack limit in its TEB by hand. On Linux the reported range of the main thread is mostly unmapped; the unmapped pages from the bottom of a protect up to the top of the stack are mapped read/write in place, then the protect is applied, so the stack never has to grow down to the protected pages, which Linux refuses within its stack guard gap (1 MiB by default) above a readable mapping. The pages below a protect stay unmapped and out of the stack's reach, where on the console the stack goes on below them - [Foreign guest module image protection on Windows](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - `sceKernelMprotect` on Windows allows protection mutations on mapped PE images containing a `.elf0` section (as relinked guest modules produce, such as NVIDIA Real-time Denoisers `libNRD.prx` bundled in Dying Light 2: Stay Human PPSA02262), registering their sections into `GuestAllocations`. Host libraries and system DLLs (such as `libkernel.prx` and `kernel32.dll`) without a `.elf0` section continue to be rejected - [_umtx_op](../../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - `UMTX_OP_WAIT`, `UMTX_OP_WAKE`, `UMTX_OP_WAIT_UINT`, `UMTX_OP_WAIT_UINT_PRIVATE`, `UMTX_OP_WAKE_PRIVATE` and `UMTX_OP_NWAKE_PRIVATE` follow FreeBSD 11's [kern_umtx.c](https://github.com/freebsd/freebsd-src/blob/stable/11/sys/kern/kern_umtx.c): a timeout whose size argument is at most 16 is a relative `timespec`, a larger one an `_umtx_time`, and a wake of 0 or fewer wakes one waiter. `UMTX_OP_NWAKE_PRIVATE` reads the value as an `int` count of pointers at the object and wakes every waiter on each one; a count of 0 or fewer reads nothing, and a null list fails with `EFAULT`, where FreeBSD's `copyin` would also fail for any other unreadable list. Waiters share the queue of `sceKernelSyncOnAddressWait`. A contended operation 0 or 1 was not measured; uncontended, both fail with `EOPNOTSUPP` and leave the word unchanged, as FreeBSD 11's [`__umtx_op_unimpl`](https://github.com/freebsd/freebsd-src/blob/stable/11/sys/kern/kern_umtx.c#L3349-L3353) does and as a payload process on a PS5 (firmware 12.70) answered operation 1 for four word values and operation 0 for a word of 0. Operations below 0 or above 26 fail with `EINVAL`, as in FreeBSD 11. `UMTX_OP_WAIT_UINT` with a value that does not fit in 32 bits returns 0 at once, as FreeBSD's zero-extended compare does. The other operations throw, and so does a misaligned object. The same payload process answered `UMTX_OP_SEM_WAIT` (19) and `UMTX_OP_SEM_WAKE` (20) as FreeBSD 11's [`do_sem_wait` and `do_sem_wake`](https://github.com/freebsd/freebsd-src/blob/stable/11/sys/kern/kern_umtx.c#L3100-L3192), built under `COMPAT_FREEBSD9` or `COMPAT_FREEBSD10` ([op_table](https://github.com/freebsd/freebsd-src/blob/stable/11/sys/kern/kern_umtx.c#L4015-L4021)), do: 0 for a count of 1 with `_has_waiters` set, `ETIMEDOUT` for a count of 0 with a 100 ms relative timeout, and 0 for a wake without a waiter; it answered `UMTX_OP_SEM2_WAIT` (23) with 0, not `ETIMEDOUT`, for a count of 0 with the same timeout, leaving the count 0 where FreeBSD 11 would set `USEM_HAS_WAITERS`, and its log ends before the result of `UMTX_OP_SEM2_WAKE` (24) without a waiter. PPSA12544's `libc.prx` and Unity's `PS5Util.prx` (PPSA18194) pass `UMTX_OP_WAIT` and `UMTX_OP_WAIT_UINT` a pointer to a 4-byte microsecond timeout with size argument 0, which FreeBSD reads as a `timespec`, and wait for a running call_once with operation 1; what those callers do with the `EOPNOTSUPP` was not run, and whether the console reads a microsecond timeout there in all cases is unverified. A call with size argument 0 is read unconditionally as a 4-byte microsecond timeout, matching callers like `libc.prx` and `PS5Util.prx`'s `zlqfTyrQSPk` wrapper which pass size 0 with a 32-bit microsecond count -- [sceKernelSyncOnAddressWait](../../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - the only known caller passes a null timeout and a name string as the fourth argument; the timeout is assumed to point to microseconds like the other kernel waits, and the name is ignored -- [sceKernelSyncOnAddressWait8 / sceKernelSyncOnAddressWait16 / sceKernelSyncOnAddressWait32 / sceKernelSyncOnAddressWait64](../../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - libc's `__std_atomic_wait_direct_8/16/32/64` pass a microsecond timeout or null and ignore the result; the by-value comparand, return values and the 2- and 8-byte alignment of Wait16 and Wait64 are assumed +- [sceKernelSyncOnAddressWait, sceKernelSyncOnAddressWait8/16/32/64 and sceKernelSyncOnAddressWake](../../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - follow a payload process on a PS5 (firmware 13.60), not a title. Only a null address is checked for `EFAULT`: any other unreadable address is read and faults the host, where the console returned `EFAULT` for address 0x10. The time left that a woken wait writes back is taken from the host clock, so it can differ from the console's by the cost of the call - [scePthreadRwlockDestroy, pthread_rwlock_destroy](../../../core/libs/prx/libkernel/Pthread/src/Rwlock.cpp) (libkernel) - measured on a PS5 (firmware 13.60): destroying a rwlock returns 0 whether it is free or held for reading or writing, and leaves 1 in the handle; every call on that handle then returns `EINVAL` until it is initialized again, and a zero handle is initialized on first use by the `scePthreadRwlock` calls as by the POSIX ones. A rwlock destroyed while held is not freed, so its memory leaks. Destroying a rwlock while another thread is acquiring it is not guarded - [scePthreadCondWait / scePthreadCondTimedwait](../../../core/libs/prx/libkernel/Pthread/src/Cond.cpp) (libkernel) - before waiting, FreeBSD libthr's [`cond_wait_common`](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_cond.c#L1992-L2029) checks ownership through [`_mutex_owned`](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_mutex.c#L1166-L1178): an unowned mutex returns `EPERM`, while its destroyed marker returns `EINVAL`. An untouched adaptive static initializer (slot value 1) therefore returns `EPERM`; the test covers this, an uninitialized slot, a mutex owned by another thread, and the destroyed marker for both timed and untimed sce and POSIX waits. A recursive mutex locked several times is released completely for the wait and relocked to the same depth afterwards, as FreeBSD libthr does (`_mutex_cv_unlock`, `_mutex_cv_lock`). These FreeBSD-derived behaviors remain unverified on console hardware. Cancellation follows the [POSIX condition wait contract](https://pubs.opengroup.org/onlinepubs/9799919799/functions/pthread_cond_clockwait.html): the original mutex and recursive depth are restored before cleanup, and a canceled waiter forwards a wake to the remaining waiters. The host condition state is retained across mutex reacquisition so the guest can destroy and free its condition after notification; cancellation ordering and signal replacement are not measured on a console - [sceKernelSetEventFlag](../../../core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp) (libkernel) - multi-waiter satisfaction follows fpPS4 in-order evaluation: registered waiters are evaluated in FIFO order and each satisfied waiter's clear mode is applied immediately to the pattern before evaluating subsequent waiters; this ordering is unverified on PS5 console hardware From f4f9b3c13047aa7ca21cfd518a3e081c7f243b9e Mon Sep 17 00:00:00 2001 From: orezraey Date: Tue, 20 Oct 2026 19:39:52 -0300 Subject: [PATCH 003/198] fix(libkernel): read a misaligned wait word with one hardware load Co-Authored-By: Claude Opus 5.5 --- .../SyncOnAddress/src/SyncOnAddress.cpp | 3 +-- core/libs/tests/GuestSyncOnAddress.cpp | 26 +++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp b/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp index 2555b6f450..2532968797 100644 --- a/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp +++ b/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp @@ -1,7 +1,6 @@ #include #include #include -#include #include #include #include @@ -77,7 +76,7 @@ template TValue LoadAddressValue(TValue* address) { if (reinterpret_cast(address) % alignof(TValue) == 0) return std::atomic_ref(*address).load(); TValue value; - std::memcpy(&value, address, sizeof(value)); + asm volatile("mov %1, %0" : "=r"(value) : "m"(*reinterpret_cast(address))); return value; } diff --git a/core/libs/tests/GuestSyncOnAddress.cpp b/core/libs/tests/GuestSyncOnAddress.cpp index fc116edff7..c0043fcf0b 100644 --- a/core/libs/tests/GuestSyncOnAddress.cpp +++ b/core/libs/tests/GuestSyncOnAddress.cpp @@ -296,6 +296,32 @@ static void MisalignedAddressesWaitAndWake() { Require(sceKernelSyncOnAddressWake(word, 1) == 1); waiter.join(); Require(result == SCE_OK); + + auto* shared = reinterpret_cast(storage + 3); + std::atomic stop{false}; + std::atomic rounds{0}; + std::thread changer([&] { + std::uint64_t next = 1; + while (!stop.load()) { + __atomic_store_n(reinterpret_cast(shared), static_cast(next), __ATOMIC_RELAXED); + ++next; + sceKernelSyncOnAddressWake(shared, INT_MAX); + } + }); + for (int round = 0; round < 2000; ++round) { + unsigned char bytes[sizeof(std::uint64_t)]; + bytes[0] = __atomic_load_n(reinterpret_cast(shared), __ATOMIC_RELAXED); + std::memcpy(bytes + 1, reinterpret_cast(shared) + 1, sizeof(bytes) - 1); + std::uint64_t seen = 0; + std::memcpy(&seen, bytes, sizeof(seen)); + KernelUseconds waitTimeout = SHORT_TIMEOUT; + const int waited = sceKernelSyncOnAddressWait64(shared, seen, &waitTimeout, "changing"); + Require(waited == SCE_OK || waited == SCE_KERNEL_ERROR_EAGAIN || waited == SCE_KERNEL_ERROR_ETIMEDOUT); + ++rounds; + } + stop.store(true); + changer.join(); + Require(rounds.load() == 2000); } static void NullAddresses() { From 623501f36560884b66e16c93a0617d00e266e196 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Wed, 21 Oct 2026 02:50:10 -0400 Subject: [PATCH 004/198] perf(agc): lease mapped readable runs for import events --- .../Graphics/src/GuestBufferMemory.cpp | 10 +++- .../prx/libc/include/GuestAllocations.hpp | 1 + core/libs/prx/libc/src/GuestAllocations.cpp | 36 ++++++++++++ core/libs/tests/GuestMemory.cpp | 58 +++++++++++++++++++ 4 files changed, 102 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 610c437369..9cb6029b26 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -3535,15 +3535,19 @@ bool ImportMappedRanges(const Context& context, const GuestAllocations::Mapped& } else if (state.refreshedGeneration > generation) { return true; } - if (!adopting) current = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); + if (!adopting) current = GuestAllocations::GuestAllocationsAcquireMapped_nid_no_patch(ranges); for (const auto& mapped : ranges) { const auto range = mapped.lock(); if (range == nullptr || leasedRangeOwner(current, range->address) != range) continue; try { + const auto run = importRun(current, range->address, std::numeric_limits::max(), effectiveBudget(context, state)); + const bool replacing = wouldRetireImport(state, run); if (!GuestMemory::GpuMutex().HeldByThisThread()) { - const auto run = importRun(current, range->address, std::numeric_limits::max(), effectiveBudget(context, state)); const std::array, 1> span{{{run.base, run.base + run.bytes}}}; - if (wouldRetireImport(state, run) || StorageTexture::AnyPendingOverlaps(span)) return false; + if (replacing || StorageTexture::AnyPendingOverlaps(span)) return false; + } else if (replacing) { + current = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); + if (leasedRangeOwner(current, range->address) != range) continue; } importAllocation(context, state, range->address, current, ImportRequester::Map, 0, false); } catch (const std::exception& error) { diff --git a/core/libs/prx/libc/include/GuestAllocations.hpp b/core/libs/prx/libc/include/GuestAllocations.hpp index 7811725142..da25cc7755 100644 --- a/core/libs/prx/libc/include/GuestAllocations.hpp +++ b/core/libs/prx/libc/include/GuestAllocations.hpp @@ -68,6 +68,7 @@ void GuestAllocationsRemove_nid_postfix(void* mutation, const void* pointer); void GuestAllocationsProtect_nid_postfix(void* mutation, const void* pointer, std::size_t bytes, bool readable, bool writable, bool gpu, const std::function& apply); void GuestAllocationsUnmap_nid_postfix(void* mutation, const void* pointer, std::size_t bytes, const std::function& apply); Lease GuestAllocationsAcquire_nid_postfix(); +Lease GuestAllocationsAcquireMapped_nid_no_patch(const Mapped& mapped); Lease GuestAllocationsAcquireAll_nid_postfix(); std::shared_ptr GuestAllocationsLookup_nid_postfix(std::uintptr_t address, bool next); // Changes whenever a mutation ends, so callers can cache facts about guest mappings between changes. diff --git a/core/libs/prx/libc/src/GuestAllocations.cpp b/core/libs/prx/libc/src/GuestAllocations.cpp index 96ff94d0bf..1744817ec0 100644 --- a/core/libs/prx/libc/src/GuestAllocations.cpp +++ b/core/libs/prx/libc/src/GuestAllocations.cpp @@ -642,6 +642,42 @@ Lease GuestAllocationsAcquire_nid_postfix() { return Lease(std::move(ranges)); } +Lease GuestAllocationsAcquireMapped_nid_no_patch(const Mapped& mapped) { + std::lock_guard lock(registry().mutex); + auto& registered = registry().ranges; + std::vector targets; + targets.reserve(mapped.size()); + for (const auto& weak : mapped) { + const auto range = weak.lock(); + if (range == nullptr || !range->readable || range->bytes == 0) continue; + const auto found = registered.find(range->address); + if (found != registered.end() && found->second == range) targets.push_back(found); + } + std::sort(targets.begin(), targets.end(), [](const auto& a, const auto& b) { return a->first < b->first; }); + auto ranges = std::make_shared(); + const auto continues = [](const Range& before, const Range& after) { + return before.address + before.bytes == after.address && before.readable && after.readable && before.writable == after.writable && before.gpu == after.gpu; + }; + for (const auto target : targets) { + if (!ranges->empty() && target->first < ranges->back()->address + ranges->back()->bytes) continue; + auto first = target; + while (first != registered.begin()) { + auto previous = std::prev(first); + while (previous != registered.begin() && previous->second->bytes == 0) --previous; + if (previous->second->bytes == 0 || !continues(*previous->second, *first->second)) break; + first = previous; + } + for (auto current = first;;) { + ranges->push_back(current->second); + auto next = std::next(current); + while (next != registered.end() && next->second->bytes == 0) ++next; + if (next == registered.end() || !continues(*current->second, *next->second)) break; + current = next; + } + } + return Lease(std::move(ranges)); +} + std::shared_ptr GuestAllocationsLookup_nid_postfix(std::uintptr_t address, bool next) { std::lock_guard lock(registry().mutex); const auto& ranges = registry().ranges; diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index a52c5a5e2d..9638f02ded 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -19,7 +19,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -2962,6 +2964,61 @@ static void CheckRegularFileMapping() { } #endif +static void CheckMappedLeasesPreserveReadableRuns() { + constexpr std::uintptr_t base = 0x7e0000000000ull; + constexpr std::size_t page = 0x4000; + const auto pointer = [&](std::size_t index) { return reinterpret_cast(base + index * page); }; + { + GuestAllocations::Mutation mutation; + for (std::size_t index = 0; index < 8; ++index) mutation.Add(pointer(index), page, index != 3, index != 3 && index != 4, index != 5); + } + const auto weak = [&](std::size_t index) { return std::weak_ptr(GuestAllocations::GuestAllocationsLookup_nid_postfix(base + index * page, false)); }; + const auto verify = [&](const GuestAllocations::Mapped& mapped, std::initializer_list expected) { + const auto lease = GuestAllocations::GuestAllocationsAcquireMapped_nid_no_patch(mapped); + Require(lease.size() == expected.size()); + std::size_t position = 0; + for (const auto index : expected) Require(lease[position++]->address == base + index * page); + }; + verify({}, {}); + verify({weak(1)}, {0, 1, 2}); + verify({weak(2), weak(0), weak(1), weak(1)}, {0, 1, 2}); + verify({weak(3)}, {}); + verify({weak(4)}, {4}); + verify({weak(5)}, {5}); + verify({weak(7), weak(1)}, {0, 1, 2, 6, 7}); + auto stale = std::make_shared(*GuestAllocations::GuestAllocationsLookup_nid_postfix(base + page, false)); + { + GuestAllocations::Mutation mutation; + mutation.Remove(pointer(1)); + mutation.Add(pointer(1), page, true, true, true); + } + verify({stale}, {}); + stale.reset(); + verify({weak(1)}, {0, 1, 2}); + { + const auto lease = GuestAllocations::GuestAllocationsAcquireMapped_nid_no_patch({weak(1)}); + auto neighbor = weak(0); + Require(neighbor.use_count() >= 2); + { + GuestAllocations::Mutation mutation; + mutation.Add(pointer(10), page, true, true, true); + } + Require(lease.size() == 3 && neighbor.use_count() >= 2); + } + { + GuestAllocations::Mutation mutation; + mutation.Remove(pointer(1)); + } + verify({weak(0)}, {0}); + verify({weak(2)}, {2}); + { + GuestAllocations::Mutation mutation; + for (std::size_t index = 0; index < 8; ++index) if (index != 1) mutation.Remove(pointer(index)); + mutation.Remove(pointer(10)); + } + verify({weak(0)}, {}); +} + static void CheckVirtualQuerySplitFlexibleRanges() { constexpr std::size_t page = 0x4000; void* mapped = nullptr; @@ -3269,6 +3326,7 @@ int main(int argc, char** argv) { } Require(argc == 1); CheckAvailableDirectMemoryIsLargestFreeBlock(); + CheckMappedLeasesPreserveReadableRuns(); CheckVirtualQuerySplitFlexibleRanges(); CheckVirtualQueryForNonReadableGuestRange(); CheckReleaseFlexibleMemory(); From 0f4a7216efdb737ebdefd8d86099358c5fe53187 Mon Sep 17 00:00:00 2001 From: Federico Marras Date: Wed, 21 Oct 2026 20:26:06 +0200 Subject: [PATCH 005/198] fix(libSceNgs2.native): return 0x804A8432 for waveform formats the module does not parse --- .../libs/prx/libSceNgs2.native/src/Atrac9.cpp | 19 +++--- .../prx/libSceNgs2.native/tests/Ngs2Parse.cpp | 63 +++++++++++++++++++ docs/dev/debt/libSceNgs2.native.md | 1 + 3 files changed, 75 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp b/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp index 716eb61cbf..b508b7f5e2 100644 --- a/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp @@ -20,7 +20,9 @@ static constexpr std::uint16_t WAVE_FORMAT_IEEE_FLOAT = 0x3; static constexpr std::uint16_t WAVE_FORMAT_EXTENSIBLE = 0xfffe; static constexpr std::uint8_t ATRAC9_GUID[16] = {0xd2, 0x42, 0xe1, 0x47, 0xba, 0x36, 0x8d, 0x4d, 0x88, 0xfc, 0x61, 0x65, 0x4f, 0x8c, 0x83, 0x6c}; static constexpr std::uint8_t PCM_GUID[16] = {0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71}; +static constexpr std::uint8_t TYPE_43_GUID[16] = {0x1c, 0xe0, 0x10, 0x9e, 0xcc, 0x8b, 0xd9, 0x47, 0x8f, 0x3b, 0xe3, 0x0b, 0x8b, 0xdc, 0x35, 0x29}; static constexpr std::size_t FMT_ATRAC9_SIZE = 52; +static constexpr std::size_t FMT_EXTENSIBLE_SIZE = 40; static constexpr std::size_t FMT_GUID_OFFSET = 24; static constexpr std::size_t FMT_CONFIG_OFFSET = 44; static constexpr std::size_t FACT_ATRAC9_SIZE = 12; @@ -383,17 +385,18 @@ static int ParsePcm(const RiffChunks& chunks, std::size_t size, Ngs2WaveformInfo static int ParseWaveform(const RiffChunks& chunks, std::size_t size, Ngs2WaveformInfo& info) { if (chunks.formatSize < 2) return SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT; const std::uint16_t tag = ReadLe16(chunks.format); - if (tag == WAVE_FORMAT_EXTENSIBLE && chunks.formatSize >= FMT_GUID_OFFSET + sizeof(ATRAC9_GUID) && - std::memcmp(chunks.format + FMT_GUID_OFFSET, ATRAC9_GUID, sizeof(ATRAC9_GUID)) == 0) { - return ParseAtrac9(chunks, info); - } - if (tag == WAVE_FORMAT_EXTENSIBLE && chunks.formatSize >= FMT_GUID_OFFSET + sizeof(PCM_GUID) && - std::memcmp(chunks.format + FMT_GUID_OFFSET, PCM_GUID, sizeof(PCM_GUID)) == 0) { - return ParsePcm(chunks, size, info, false); + if (tag == WAVE_FORMAT_EXTENSIBLE) { + if (chunks.formatSize < FMT_EXTENSIBLE_SIZE) return SCE_NGS2_ERROR_UNKNOWN_WAVEFORM_FORMAT; + const auto* guid = chunks.format + FMT_GUID_OFFSET; + if (std::memcmp(guid, PCM_GUID, sizeof(PCM_GUID)) == 0) return ParsePcm(chunks, size, info, false); + if (std::memcmp(guid, ATRAC9_GUID, sizeof(ATRAC9_GUID)) == 0) return ParseAtrac9(chunks, info); + if (chunks.formatSize == FMT_ATRAC9_SIZE) throw std::runtime_error("NGS2: parsing an extensible waveform of format size 0x34 without the ATRAC9 GUID is not implemented"); + if (std::memcmp(guid, TYPE_43_GUID, sizeof(TYPE_43_GUID)) == 0) throw std::runtime_error("NGS2: parsing waveform type 0x43 is not implemented"); + return SCE_NGS2_ERROR_UNKNOWN_WAVEFORM_FORMAT; } if (tag == WAVE_FORMAT_PCM) return ParsePcm(chunks, size, info, false); if (tag == WAVE_FORMAT_IEEE_FLOAT) return ParsePcm(chunks, size, info, true); - throw std::runtime_error("NGS2: parsing waveform format tag " + Ngs2Hex(tag) + " is not implemented"); + return SCE_NGS2_ERROR_UNKNOWN_WAVEFORM_FORMAT; } #pragma GCC visibility push(default) diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Parse.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Parse.cpp index 37ef362c57..71b23814aa 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Parse.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Parse.cpp @@ -50,6 +50,24 @@ static std::vector PcmFile(std::uint32_t channels, std::uint32_t s return file; } +static std::vector ExtensibleFile(std::uint32_t formatSize, const std::uint8_t* guid) { + const auto pcm = PcmFile(1, 44100, 16, 10, 0xfffe); + std::vector format(pcm.begin() + 20, pcm.begin() + 60); + std::memcpy(format.data() + 24, guid, 16); + format.resize(formatSize); + std::vector file; + PutTag(file, "RIFF"); + Put32(file, 0); + PutTag(file, "WAVE"); + PutTag(file, "fmt "); + Put32(file, formatSize); + file.insert(file.end(), format.begin(), format.end()); + file.insert(file.end(), pcm.begin() + 60, pcm.end()); + const auto riffSize = static_cast(file.size() - 8); + std::memcpy(file.data() + 4, &riffSize, sizeof(riffSize)); + return file; +} + static void TestParsePcm() { const auto file = PcmFile(2, 44100, 16, 100); Ngs2WaveformInfo info{}; @@ -90,6 +108,50 @@ static void TestParsePcm() { Require(sceNgs2ParseWaveformData(noChannels.data(), noChannels.size(), &info) == SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT); } +static bool ParseThrowsFormat(const std::vector& file) { + Ngs2WaveformInfo info{}; + try { + sceNgs2ParseWaveformData(file.data(), file.size(), &info); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + +static void TestParseUnknownFormats() { + const std::uint8_t pcmGuid[16] = {0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71}; + const std::uint8_t unknownGuid[16] = {0xaa, 0xbb, 0xcc, 0xdd, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc}; + const std::uint8_t type43Guid[16] = {0x1c, 0xe0, 0x10, 0x9e, 0xcc, 0x8b, 0xd9, 0x47, 0x8f, 0x3b, 0xe3, 0x0b, 0x8b, 0xdc, 0x35, 0x29}; + Ngs2WaveformInfo info{}; + for (const std::uint32_t tag : {0x0u, 0x2u, 0x11u, 0x55u, 0x165u, 0xffffu}) { + const auto wave = PcmFile(1, 44100, 16, 10, tag); + Require(sceNgs2ParseWaveformData(wave.data(), wave.size(), &info) == SCE_NGS2_ERROR_UNKNOWN_WAVEFORM_FORMAT); + } + for (const std::uint32_t formatSize : {0x10u, 0x18u, 0x26u}) { + const auto wave = ExtensibleFile(formatSize, pcmGuid); + Require(sceNgs2ParseWaveformData(wave.data(), wave.size(), &info) == SCE_NGS2_ERROR_UNKNOWN_WAVEFORM_FORMAT); + } + for (const std::uint32_t formatSize : {0x28u, 0x30u, 0x36u}) { + const auto wave = ExtensibleFile(formatSize, unknownGuid); + Require(sceNgs2ParseWaveformData(wave.data(), wave.size(), &info) == SCE_NGS2_ERROR_UNKNOWN_WAVEFORM_FORMAT); + } + const auto pcm = ExtensibleFile(0x34, pcmGuid); + Require(sceNgs2ParseWaveformData(pcm.data(), pcm.size(), &info) == SCE_NGS2_OK && info.format.waveform_type == SCE_NGS2_WAVEFORM_TYPE_PCM_I16L && info.num_samples == 10); + Require(ParseThrowsFormat(ExtensibleFile(0x28, type43Guid))); + Require(ParseThrowsFormat(ExtensibleFile(0x34, type43Guid))); + Require(ParseThrowsFormat(ExtensibleFile(0x34, unknownGuid))); + + const auto wave = PcmFile(1, 44100, 16, 10, 0x55); + const char* const guestPath = "/aps5_ngs2_parse_unknown.wav"; + const auto path = ResolvePath_nid_no_patch(guestPath); + { + std::ofstream out(path, std::ios::binary); + out.write(reinterpret_cast(wave.data()), static_cast(wave.size())); + } + Require(sceNgs2ParseWaveformFile(guestPath, 0, &info) == SCE_NGS2_ERROR_UNKNOWN_WAVEFORM_FORMAT); + std::filesystem::remove(path); +} + static void TestParseFile() { const auto wave = PcmFile(1, 22050, 16, 64); const char* const guestPath = "/aps5_ngs2_parse_file.wav"; @@ -139,6 +201,7 @@ static void TestParseFileBank() { int main() { TestParsePcm(); + TestParseUnknownFormats(); TestParseFile(); TestParseFileBank(); return 0; diff --git a/docs/dev/debt/libSceNgs2.native.md b/docs/dev/debt/libSceNgs2.native.md index 1a599b662b..5fdbbd3b91 100644 --- a/docs/dev/debt/libSceNgs2.native.md +++ b/docs/dev/debt/libSceNgs2.native.md @@ -31,6 +31,7 @@ - [sceNgs2VoiceControl patch](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - a destination rack handle with input id 0 resolves to that rack's first voice; this matches the Dead Cells call in #4334, but selection for racks with multiple voices has not been checked against a console. Nonzero destination input ids still throw. `sceNgs2VoiceGetPortInfo` reports the resolved voice handle and zero delay. - [NGS2 PCM waveform parsing](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) (libSceNgs2.native) - `sceNgs2ParseWaveformData` reads integer PCM (tag 1, or `WAVE_FORMAT_EXTENSIBLE` with the PCM GUID) of 8, 16, 24 and 32 bits as types 0x11, 0x12, 0x14 and 0x16, and float (tag 3) of 32 and 64 bits as 0x18 and 0x1a, and returns 0x804A8432 for other depths, after [prosper's reading of `libSceNgs2.native.sprx`](https://github.com/mattias800/prosper/blob/30572df43ae20ff28f995db56149f7af42235741/prosper/src/hle/audio/ngs2_waveform.cpp#L194-L223); not measured on a console. Every depth keeps the checks of the 16-bit path (channels, rate, block align, whole frames, a looped `smpl` chunk throws), which that reading does not apply. Voices still play only 16-bit PCM and ATRAC9 - [sceNgs2ParseWaveformFile](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) (libSceNgs2.native) - the signature `(path, offset, info)` is taken from the call site of PPSA21402, which sets only three argument registers; the file is read from `offset` to its end, the data and block offsets are taken to be relative to the start of the file, not of `offset`, and an unreadable file throws. The audio unit fields of a 16-bit PCM waveform are assumed to be one sample frame per unit and per frame; `INVALID_WAVEFORM_DATA` for a `data` chunk that runs past the buffer or ends inside a frame is assumed, and looped PCM and other PCM formats throw +- [NGS2 waveform format routing](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) (libSceNgs2.native) - an extensible fmt chunk of 0x34 bytes whose GUID is neither PCM nor ATRAC9, which the module parses as ATRAC9 without checking its config, and the GUID of waveform type 0x43 throw. Otherwise a tag other than 1, 3 or 0xFFFE, an extensible fmt chunk under 0x28 bytes and an unknown GUID return `0x804A8432`, per [prosper's reading of the module](https://github.com/mattias800/prosper/blob/8dcb5dfdb9997ede7c76783995010d6abd1222f8/prosper/src/hle/audio/ngs2_waveform.cpp#L190-L208); not measured on a console - [sceNgs2VoiceControl sampler filter](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - param `0x1000000a`, its layout and meaning follow KytyPS5 only: location 1 type 1 is an RBJ low-pass biquad at the system sample rate after resampling, scaled by `level`, a set `channel_mask` bit bypasses that channel, type 0 turns the filter off. Other locations and types throw. The voice keeps playing a filter tail after its last block until the history is zero, with outputs below 1e-20 flushed to zero (a KytyPS5 choice); the console's tail length is unknown - [sceNgs2PanInit / sceNgs2PanGetVolumeMatrix](../../../core/libs/prx/libSceNgs2.native/src/Pan.cpp) (libSceNgs2.native) - follow the PS4 firmware 11.00 `libSceNgs2.sprx`, read and checked against its output for 2, 4, 5 and 7 speakers into 1, 2, 6 and 8 channels; not yet confirmed on the PS5 library. Speakers are taken in the order L R, L R Ls Rs, L R C Ls Rs or L R C Ls Rs Lb Rb and kept in a fixed circular order, not sorted; without angles the defaults are 90/270, 40/110/250/320, 0/40/110/250/320 and 0/40/110/150/210/250/320 degrees. As in that library, defaults converted to radians keep the wrap-around entry in degrees, so angles past the last speaker pan almost fully to it. Error codes are not known for the PS5, so invalid arguments throw, including a speaker angle equal to the unit angle, on which that library loops forever. Angles so large that adding or subtracting a turn leaves them unchanged throw for the same reason - [sceNgs2GeomResetListenerParam, sceNgs2GeomResetSourceParam](../../../core/libs/prx/libSceNgs2.native/src/Geom.cpp) (libSceNgs2.native) - the structures (12, 16, 16, 60, 96, 108, 32 and 308 bytes) come from [KytyPS5](https://github.com/KytyPS5/KytyPS5)'s `ngs2.cpp` and match [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_geom.h` field for field, but only KytyPS5 gives the behaviour - shadPS4 logs and returns success without writing the output. The defaults (orient front +Z, orient up +Y, sound speed 343, cone 1/360/1/360, rolloff 0/1000000/1/1, doppler factor, full-band, LFE and maximum level 1, two speakers and matrix format 2) are unverified on a PS5 title; KytyPS5 and iStark/PS5PCEM agree on them. PPSA15552 (Dead Cells) calls `sceNgs2GeomResetSourceParam` after creating its racks. A null output address returns `INVALID_OUT_ADDRESS` as elsewhere in the library rather than throwing as KytyPS5 does. `sceNgs2GeomApply` is now a minimal pass-through that follows [KytyPS5](https://github.com/KytyPS5/KytyPS5)'s `Ngs2GeomApply` (shadPS4 only stubs it): pitch 1, source position, volume the larger of the source minimum and maximum level, and a diagonal of the source maximum level (1 when it is not positive) over `matrix_format` channels (0 is two, at most 8). The listener and `flags` are ignored, so source attenuation, distance rolloff, panning and Doppler stay unimplemented; neither KytyPS5 nor this has been measured on a PS5, and a null output address returns `INVALID_OUT_ADDRESS` where KytyPS5 exits From d4ead3f6aea6efa066841e7e4f677967e23c99e7 Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Wed, 21 Oct 2026 23:13:06 +0200 Subject: [PATCH 006/198] feat(libSceFreeType): expose raw SFNT table access --- core/libs/prx/libSceFreeType/CMakeLists.txt | 12 ++ core/libs/prx/libSceFreeType/src/Sfnt.cpp | 88 ++++++++ core/libs/tests/GuestFreeTypeSfnt.cpp | 224 ++++++++++++++++++++ docs/dev/debt/libSceFreeType.md | 1 + 4 files changed, 325 insertions(+) create mode 100644 core/libs/prx/libSceFreeType/src/Sfnt.cpp create mode 100644 core/libs/tests/GuestFreeTypeSfnt.cpp diff --git a/core/libs/prx/libSceFreeType/CMakeLists.txt b/core/libs/prx/libSceFreeType/CMakeLists.txt index e0a3b3e116..536d7ef609 100644 --- a/core/libs/prx/libSceFreeType/CMakeLists.txt +++ b/core/libs/prx/libSceFreeType/CMakeLists.txt @@ -9,6 +9,18 @@ if(MINGW OR CMAKE_SYSTEM_NAME STREQUAL "Linux") endif() configure_windows_unwind(libSceFreeType) +target_sources(libSceFreeType PRIVATE src/Sfnt.cpp) +if(BUILD_TESTING) + add_test_executable(guest_freetype_sfnt_tests ${LIBS_INCLUDE_DIR}/tests/GuestFreeTypeSfnt.cpp) + target_include_directories(guest_freetype_sfnt_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_freetype_sfnt_tests PRIVATE libSceFreeType freetype libc) + configure_windows_unwind(guest_freetype_sfnt_tests) + add_test(NAME guest_freetype_sfnt COMMAND guest_freetype_sfnt_tests) + if(WIN32) + set_tests_properties(guest_freetype_sfnt PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() if(BUILD_TESTING) add_test_executable(guest_freetype_tests ${LIBS_INCLUDE_DIR}/tests/GuestFreeType.cpp) target_include_directories(guest_freetype_tests PRIVATE ${LIBS_INCLUDE_DIR}) diff --git a/core/libs/prx/libSceFreeType/src/Sfnt.cpp b/core/libs/prx/libSceFreeType/src/Sfnt.cpp new file mode 100644 index 0000000000..c13c7d2eb7 --- /dev/null +++ b/core/libs/prx/libSceFreeType/src/Sfnt.cpp @@ -0,0 +1,88 @@ +#include "prx/libSceFreeType/include/GuestObjects.hpp" +#include "prx/libc/include/GuestArena.hpp" + +#include FT_TRUETYPE_TABLES_H + +#include +#include +#include +#include +#include + +using namespace FreeType; + +namespace { + +FT_Error QueryTableLength(FT_Face face, std::uint64_t tag, FT_ULong& length) { + length = 0; + if constexpr (sizeof(FT_ULong) < sizeof(tag)) { + if (tag > std::numeric_limits::max()) { + const auto error = FT_Load_Sfnt_Table(face, 0, 0, nullptr, &length); + return error != 0 ? error : FT_Err_Table_Missing; + } + } + return FT_Load_Sfnt_Table(face, static_cast(tag), 0, nullptr, &length); +} + +} + +extern "C" { + +int APS5_VABI FT_Sfnt_Table_Info_nid_postfix(GuestFaceRec* face, std::uint32_t tableIndex, std::uint64_t* tag, std::uint64_t* length) { + const GuestFace* owner = FaceOf(face); + FT_ULong nativeTag = 0; + FT_ULong nativeLength = 0; + const auto error = FT_Sfnt_Table_Info(owner ? owner->host : nullptr, tableIndex, tag ? &nativeTag : nullptr, length ? &nativeLength : nullptr); + if (error == 0) { + if (tag) *tag = nativeTag; + if (length) *length = nativeLength; + } + return error; +} + +int APS5_VABI FT_Load_Sfnt_Table_nid_postfix(GuestFaceRec* face, std::uint64_t tag, std::int64_t offset, unsigned char* buffer, std::uint64_t* length) { + const GuestFace* owner = FaceOf(face); + const FT_Face native = owner ? owner->host : nullptr; + FT_ULong tableSize = 0; + const auto error = QueryTableLength(native, tag, tableSize); + if (error != 0) return error; + const std::uint64_t requested = length ? *length : tableSize; + if (length && requested == 0) { + *length = tableSize; + return 0; + } + FT_ULong fileSize = tableSize; + if (tag != 0) { + const auto fileError = QueryTableLength(native, 0, fileSize); + if (fileError != 0) return fileError; + } + FT_ULong nativeTag = static_cast(tag); + FT_Long nativeOffset = 0; + if constexpr (sizeof(FT_Long) < sizeof(offset)) { + std::uint64_t absolute = static_cast(offset); + if (tag != 0) { + FT_Stream stream = native->stream; + if (stream->read) throw std::runtime_error("FT_Load_Sfnt_Table: tagged reads through a native stream callback are not implemented on this host"); + const auto position = stream->pos; + FT_ULong probeLength = 1; + unsigned char probe = 0; + const auto probeError = FT_Load_Sfnt_Table(native, nativeTag, 0, &probe, &probeLength); + const auto afterProbe = stream->pos; + stream->pos = position; + if (probeError != 0) return probeError; + absolute += static_cast(afterProbe - 1); + } + if (absolute >= fileSize) return FT_Err_Invalid_Stream_Operation; + nativeOffset = std::bit_cast(static_cast(absolute)); + nativeTag = 0; + } else { + nativeOffset = static_cast(offset); + } + FT_ULong nativeLength = static_cast(std::min(requested, static_cast(std::numeric_limits::max()))); + GuestArena::HostWrite write(buffer, static_cast(std::min(nativeLength, fileSize))); + const auto readError = FT_Load_Sfnt_Table(native, nativeTag, nativeOffset, buffer, &nativeLength); + if (readError == 0 && requested > std::numeric_limits::max()) return FT_Err_Invalid_Stream_Operation; + return readError; +} + +} diff --git a/core/libs/tests/GuestFreeTypeSfnt.cpp b/core/libs/tests/GuestFreeTypeSfnt.cpp new file mode 100644 index 0000000000..7345c8483b --- /dev/null +++ b/core/libs/tests/GuestFreeTypeSfnt.cpp @@ -0,0 +1,224 @@ +#include "FontFixture.hpp" +#include "prx/libSceFreeType/include/GuestObjects.hpp" + +#include FT_TRUETYPE_TABLES_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace FreeType; + +extern "C" { +int APS5_VABI FT_Init_FreeType_nid_postfix(void**); +int APS5_VABI FT_Done_FreeType_nid_postfix(void*); +int APS5_VABI FT_New_Memory_Face_nid_postfix(void*, const unsigned char*, std::int64_t, std::int64_t, GuestFaceRec**); +int APS5_VABI FT_Done_Face_nid_postfix(GuestFaceRec*); +int APS5_VABI FT_Sfnt_Table_Info_nid_postfix(GuestFaceRec*, std::uint32_t, std::uint64_t*, std::uint64_t*); +int APS5_VABI FT_Load_Sfnt_Table_nid_postfix(GuestFaceRec*, std::uint64_t, std::int64_t, unsigned char*, std::uint64_t*); +} + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "FreeType SFNT check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +struct FacePair { + std::vector bytes; + GuestFaceRec* guest = nullptr; + FT_Face native = nullptr; + + FacePair(void* library, FT_Library nativeLibrary, std::vector data) : bytes(std::move(data)) { + Require(FT_New_Memory_Face_nid_postfix(library, bytes.data(), static_cast(bytes.size()), 0, &guest) == 0); + Require(FT_New_Memory_Face(nativeLibrary, bytes.data(), static_cast(bytes.size()), 0, &native) == 0); + } + + ~FacePair() { + Require(FT_Done_Face_nid_postfix(guest) == 0 && FT_Done_Face(native) == 0); + } +}; + +static std::uint32_t Read32(const std::vector& bytes, std::size_t offset) { + std::uint32_t value = 0; + for (unsigned index = 0; index < 4; ++index) value = (value << 8) | bytes.at(offset + index); + return value; +} + +static void InfoMatches(GuestFaceRec* guest, FT_Face native, std::uint32_t index, bool withTag, bool withLength, int& comparisons) { + constexpr std::uint64_t sentinel = 0xFEDCBA9876543210ull; + std::array output{sentinel, sentinel, sentinel, sentinel}; + FT_ULong tag = 0; + FT_ULong length = 0; + const auto error = FT_Sfnt_Table_Info(native, index, withTag ? &tag : nullptr, withLength ? &length : nullptr); + Require(FT_Sfnt_Table_Info_nid_postfix(guest, index, withTag ? &output[1] : nullptr, withLength ? &output[2] : nullptr) == error); + Require(output[0] == sentinel && output[3] == sentinel); + Require(output[1] == (error == 0 && withTag ? static_cast(tag) : sentinel)); + Require(output[2] == (error == 0 && withLength ? static_cast(length) : sentinel)); + if (withTag && withLength) { + FT_ULong alias = 0; + std::uint64_t guestAlias = sentinel; + Require(FT_Sfnt_Table_Info(native, index, &alias, &alias) == error); + Require(FT_Sfnt_Table_Info_nid_postfix(guest, index, &guestAlias, &guestAlias) == error); + Require(guestAlias == (error == 0 ? static_cast(alias) : sentinel)); + } + ++comparisons; +} + +static FT_Error ReadMatches(GuestFaceRec* guest, FT_Face native, std::uint32_t tag, std::int64_t offset, std::uint64_t requested, bool withLength, int& comparisons) { + std::array actual{}; + actual.fill(0xA5); + auto expected = actual; + std::uint64_t actualLength = requested; + FT_ULong nativeLength = static_cast(std::min(requested, static_cast(std::numeric_limits::max()))); + const auto nativeOffset = static_cast(withLength && requested == 0 ? 0 : offset); + const auto error = FT_Load_Sfnt_Table(native, tag, nativeOffset, expected.data() + 8, withLength ? &nativeLength : nullptr); + Require(FT_Load_Sfnt_Table_nid_postfix(guest, tag, offset, actual.data() + 8, withLength ? &actualLength : nullptr) == error); + Require(actual == expected); + Require(actualLength == (withLength && requested == 0 && error == 0 ? static_cast(nativeLength) : requested)); + if (guest) Require(FaceOf(guest)->host->stream->pos == native->stream->pos); + ++comparisons; + return error; +} + +static unsigned virtualReads = 0; + +static FT_ULong VirtualRead(FT_Stream stream, FT_ULong offset, unsigned char* buffer, FT_ULong count) { + ++virtualReads; + if (offset >= stream->size || !buffer) return 0; + const auto bytes = std::min(count, stream->size - offset); + for (FT_ULong index = 0; index < bytes; ++index) buffer[index] = static_cast((offset + index) ^ 0x5A); + return bytes; +} + +static void WideStreamAccess(FacePair& face, int& comparisons) { + if constexpr (sizeof(FT_Long) < sizeof(std::int64_t)) { + FT_Stream stream = FaceOf(face.guest)->host->stream; + FT_Stream native = face.native->stream; + const auto original = stream->size; + const auto originalNative = native->size; + const auto originalRead = stream->read; + const auto originalNativeRead = native->read; + stream->size = native->size = static_cast(std::numeric_limits::max()) + 64; + Require(ReadMatches(face.guest, face.native, 0, std::numeric_limits::max(), 0, true, comparisons) == 0); + Require(ReadMatches(face.guest, face.native, 0, 0, 1, true, comparisons) == 0); + Require(ReadMatches(face.guest, face.native, 0x54455354u, 0, 1, true, comparisons) == 0); + stream->read = native->read = VirtualRead; + const auto offset = static_cast(std::numeric_limits::max()) + 6; + Require(ReadMatches(face.guest, face.native, 0, offset, 3, true, comparisons) == 0); + std::array pixels{}; + std::uint64_t length = pixels.size(); + Require(FT_Load_Sfnt_Table_nid_postfix(face.guest, 0, offset, pixels.data(), &length) == 0 && length == pixels.size()); + for (std::size_t index = 0; index < pixels.size(); ++index) Require(pixels[index] == static_cast((offset + static_cast(index)) ^ 0x5A)); + const auto calls = virtualReads; + unsigned char byte = 0xA5; + length = 1; + bool threw = false; + try { + FT_Load_Sfnt_Table_nid_postfix(face.guest, 0x54455354u, 0, &byte, &length); + } catch (const std::runtime_error& error) { + threw = std::string(error.what()).starts_with("FT_Load_Sfnt_Table: tagged reads through a native stream callback"); + } + stream->size = original; + native->size = originalNative; + stream->read = originalRead; + native->read = originalNativeRead; + Require(threw && virtualReads == calls && byte == 0xA5 && length == 1); + } +} + +int main() { + void* library = nullptr; + FT_Library nativeLibrary = nullptr; + Require(FT_Init_FreeType_nid_postfix(&library) == 0 && FT_Init_FreeType(&nativeLibrary) == 0); + int information = 0; + int reads = 0; + { + const auto font = FontFixture::BuildFont(1, {{"TEST", {11, 22, 33, 44, 55, 66, 77}}, + {"TAIL", {7, 6, 5, 4, 3, 2, 1, 0, 255, 254, 253, 252, 251, 250, 249}}, {"ZERO", {}}, + {std::string("\xF1\x23\xAB\xCD", 4), {19, 29, 39, 49, 59, 69, 79, 89}}}); + FacePair face(library, nativeLibrary, font); + const unsigned tableCount = (static_cast(font[4]) << 8) | font[5]; + Require(font.size() + 16 < 1024); + std::uint64_t count = 0; + Require(FT_Sfnt_Table_Info_nid_postfix(face.guest, std::numeric_limits::max(), nullptr, &count) == 0 && count == tableCount); + for (const auto index : {0u, tableCount - 1, tableCount, std::numeric_limits::max()}) { + for (const bool withTag : {false, true}) { + for (const bool withLength : {false, true}) { + InfoMatches(face.guest, face.native, index, withTag, withLength, information); + InfoMatches(nullptr, nullptr, index, withTag, withLength, information); + } + } + } + for (unsigned index = 0; index < tableCount; ++index) { + const std::size_t directory = 12 + index * 16; + const auto tag = Read32(font, directory); + const auto size = Read32(font, directory + 12); + std::uint64_t actualTag = 0; + std::uint64_t actualLength = 0; + Require(FT_Sfnt_Table_Info_nid_postfix(face.guest, index, &actualTag, &actualLength) == 0 && actualTag == tag && actualLength == size); + InfoMatches(face.guest, face.native, index, true, true, information); + for (const auto offset : {0ll, 1ll, -1ll, static_cast(size), static_cast(size + 1), 2147483647ll, -2147483648ll}) { + ReadMatches(face.guest, face.native, tag, offset, 0, true, reads); + ReadMatches(face.guest, face.native, tag, offset, size, false, reads); + for (const auto requested : {1ull, 3ull, static_cast(size), static_cast(size + 3)}) { + ReadMatches(face.guest, face.native, tag, offset, requested, true, reads); + } + } + ReadMatches(face.guest, face.native, tag, std::numeric_limits::max(), 0, true, reads); + ReadMatches(face.guest, face.native, tag, std::numeric_limits::min(), 0, true, reads); + } + for (const auto offset : {0ll, 1ll, -1ll, static_cast(font.size()), static_cast(font.size() - 3)}) { + for (const auto requested : {0ull, 1ull, 16ull, static_cast(font.size()), std::numeric_limits::max()}) { + ReadMatches(face.guest, face.native, 0, offset, requested, true, reads); + } + ReadMatches(face.guest, face.native, 0, offset, 0, false, reads); + } + Require(ReadMatches(face.guest, face.native, 0, static_cast(font.size() - 3), 8, true, reads) != 0); + std::array alias{}; + alias.fill(0xA5A5A5A5A5A5A5A5ull); + alias[0] = 16; + auto nativeAlias = alias; + FT_ULong nativeAliasLength = 16; + Require(FT_Load_Sfnt_Table(face.native, 0, 0, reinterpret_cast(nativeAlias.data()), &nativeAliasLength) == 0); + Require(FT_Load_Sfnt_Table_nid_postfix(face.guest, 0, 0, reinterpret_cast(alias.data()), &alias[0]) == 0); + Require(alias == nativeAlias && std::memcmp(alias.data(), font.data(), 16) == 0 && alias[2] == 0xA5A5A5A5A5A5A5A5ull); + const auto missing = ReadMatches(face.guest, face.native, 0x4D495353u, 0, 0, true, reads); + Require(missing == FT_Err_Table_Missing); + for (const auto tag : {0x100000000ull, 0x168656164ull, std::numeric_limits::max()}) { + std::uint64_t length = 7; + unsigned char byte = 0xA5; + Require(FT_Load_Sfnt_Table_nid_postfix(face.guest, tag, 0, &byte, &length) == missing && length == 7 && byte == 0xA5); + Require(FT_Load_Sfnt_Table_nid_postfix(nullptr, tag, 0, &byte, &length) == FT_Err_Invalid_Face_Handle); + } + for (const auto offset : std::array{std::numeric_limits::min(), std::numeric_limits::max(), -2147483649ll, 2147483648ll}) { + std::uint64_t length = 1; + unsigned char byte = 0xA5; + Require(FT_Load_Sfnt_Table_nid_postfix(face.guest, 0, offset, &byte, &length) == FT_Err_Invalid_Stream_Operation && byte == 0xA5 && length == 1); + Require(FT_Load_Sfnt_Table_nid_postfix(face.guest, 0x4D495353u, offset, nullptr, nullptr) == missing); + } + const std::string bdf = "STARTFONT 2.1\nFONT -anyps5-sfnt-medium-r-normal--1-10-75-75-c-10-iso10646-1\nSIZE 1 75 75\n" + "FONTBOUNDINGBOX 1 1 0 0\nSTARTPROPERTIES 4\nFONT_ASCENT 1\nFONT_DESCENT 0\nCHARSET_REGISTRY \"ISO10646\"\nCHARSET_ENCODING \"1\"\n" + "ENDPROPERTIES\nCHARS 1\nSTARTCHAR A\nENCODING 65\nSWIDTH 1000 0\nDWIDTH 1 0\nBBX 1 1 0 0\nBITMAP\n80\nENDCHAR\nENDFONT\n"; + FacePair bitmap(library, nativeLibrary, {bdf.begin(), bdf.end()}); + Require(!FT_IS_SFNT(bitmap.native)); + InfoMatches(bitmap.guest, bitmap.native, 0, true, true, information); + InfoMatches(bitmap.guest, bitmap.native, 0, false, false, information); + ReadMatches(bitmap.guest, bitmap.native, 0, 0, 0, true, reads); + ReadMatches(bitmap.guest, bitmap.native, 0, 0, 0, false, reads); + ReadMatches(nullptr, nullptr, 0, 0, 0, true, reads); + WideStreamAccess(face, reads); + } + std::printf("FreeType SFNT: %d directory comparisons, %d read comparisons; wide inputs, aliases and short-read bytes checked\n", information, reads); + Require(FT_Done_FreeType_nid_postfix(library) == 0 && FT_Done_FreeType(nativeLibrary) == 0); +} diff --git a/docs/dev/debt/libSceFreeType.md b/docs/dev/debt/libSceFreeType.md index 6d4f98438d..7ee7d64f52 100644 --- a/docs/dev/debt/libSceFreeType.md +++ b/docs/dev/debt/libSceFreeType.md @@ -9,6 +9,7 @@ - [FT_New_Memory_Face, FT_Done_Face and FT_Done_FreeType](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - console signatures, errors and generic-finalizer ordering are unverified; they follow the bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/include/freetype/freetype.h) with LP64 guest arguments. Caller-owned font bytes must remain valid until the face is freed. ### Functional +- [FT_Sfnt_Table_Info and FT_Load_Sfnt_Table](../../../core/libs/prx/libSceFreeType/src/Sfnt.cpp) (libSceFreeType) - console ABI and errors are unverified; the LP64 adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/sfnt/ttload.c). On hosts with a 32-bit `FT_Long`, tagged reads through a native stream callback throw: their table offsets cannot be recovered without changing callback state. Memory-backed streams and whole-file reads are supported; current guest face constructors use memory-backed streams. - [libSceFreeType](../../../core/libs/prx/libSceFreeType/src/GuestObjects.cpp) (libSceFreeType) - private fields (`driver`, `memory`, `stream`, `sizes_list`, `extensions`) and `subglyphs` stay null. Face, size and glyph-slot records are copies. Only edits to outline points and flags are written back before `FT_Render_Glyph`; replacing arrays or exceeding the host `FT_Pos` range throws. `FT_New_Face` reads the whole file into memory and throws for files of 2 GiB or more on Windows. On macOS, native FreeType symbols remain exported and could resolve guest imports without adaptation; Windows and Linux hide them with `--exclude-libs`. From 6b221e4e30edfecf8b0c038658c4cc937c0c21d1 Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Thu, 22 Oct 2026 08:24:06 +0200 Subject: [PATCH 007/198] fix(libSceFreeType): guard SFNT metadata host writes --- core/libs/prx/libSceFreeType/src/Sfnt.cpp | 11 ++- core/libs/tests/GuestFreeTypeSfnt.cpp | 102 ++++++++++++++++++++++ 2 files changed, 111 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceFreeType/src/Sfnt.cpp b/core/libs/prx/libSceFreeType/src/Sfnt.cpp index c13c7d2eb7..c16abcb99b 100644 --- a/core/libs/prx/libSceFreeType/src/Sfnt.cpp +++ b/core/libs/prx/libSceFreeType/src/Sfnt.cpp @@ -34,8 +34,14 @@ int APS5_VABI FT_Sfnt_Table_Info_nid_postfix(GuestFaceRec* face, std::uint32_t t FT_ULong nativeLength = 0; const auto error = FT_Sfnt_Table_Info(owner ? owner->host : nullptr, tableIndex, tag ? &nativeTag : nullptr, length ? &nativeLength : nullptr); if (error == 0) { - if (tag) *tag = nativeTag; - if (length) *length = nativeLength; + if (tag) { + GuestArena::HostWrite write(tag, sizeof(*tag)); + *tag = nativeTag; + } + if (length) { + GuestArena::HostWrite write(length, sizeof(*length)); + *length = nativeLength; + } } return error; } @@ -48,6 +54,7 @@ int APS5_VABI FT_Load_Sfnt_Table_nid_postfix(GuestFaceRec* face, std::uint64_t t if (error != 0) return error; const std::uint64_t requested = length ? *length : tableSize; if (length && requested == 0) { + GuestArena::HostWrite write(length, sizeof(*length)); *length = tableSize; return 0; } diff --git a/core/libs/tests/GuestFreeTypeSfnt.cpp b/core/libs/tests/GuestFreeTypeSfnt.cpp index 7345c8483b..54cea25405 100644 --- a/core/libs/tests/GuestFreeTypeSfnt.cpp +++ b/core/libs/tests/GuestFreeTypeSfnt.cpp @@ -1,5 +1,7 @@ #include "FontFixture.hpp" #include "prx/libSceFreeType/include/GuestObjects.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" #include FT_TRUETYPE_TABLES_H @@ -137,6 +139,105 @@ static void WideStreamAccess(FacePair& face, int& comparisons) { } } +struct WatchedMetadataPages { + std::uintptr_t tag; + std::uintptr_t length; + bool tagWritten = false; + bool lengthWritten = false; + + static void Mark(void* context, std::uintptr_t begin, std::uintptr_t end) { + auto& pages = *static_cast(context); + if (begin <= pages.tag && pages.tag < end) pages.tagWritten = true; + if (begin <= pages.length && pages.length < end) pages.lengthWritten = true; + } +}; + +static void WatchedMetadata(FacePair& face) { + if (!GuestArena::GuestArenaAvailable_nid_postfix()) { + std::puts("FreeType SFNT watched metadata: guest arena unavailable, skipped"); + return; + } +#ifdef _WIN32 + if (!GuestArena::GuestArenaWriteWatched_nid_postfix()) { + std::puts("FreeType SFNT watched metadata: write watch unavailable, skipped"); + return; + } +#else + if (!GuestWriteWatch::GuestWriteWatchAvailable_nid_postfix()) { + std::puts("FreeType SFNT watched metadata: write watch unavailable, skipped"); + return; + } +#endif + constexpr std::size_t span = 0x10000; + auto* block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(span, span)); + GuestArena::GuestArenaCommit_nid_postfix(block, span, 0x04, span); + auto* tag = reinterpret_cast(block + 0x100); + auto* length = reinterpret_cast(block + 0x4100); + constexpr std::uint64_t sentinel = 0xFEDCBA9876543210ull; + *tag = sentinel; + *length = sentinel; +#ifndef _WIN32 + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, span); + Require(GuestWriteWatch::GuestWriteWatchCovers_nid_postfix(reinterpret_cast(block), span)); +#endif + const auto collect = [&]() { + WatchedMetadataPages dirty{reinterpret_cast(tag), reinterpret_cast(length)}; +#ifdef _WIN32 + std::array pages{}; + std::size_t count = pages.size(); + Require(GuestArena::GuestArenaCollectWrites_nid_postfix(reinterpret_cast(block), span, pages.data(), &count, true)); + for (std::size_t index = 0; index < count; ++index) { + const auto begin = reinterpret_cast(pages[index]); + WatchedMetadataPages::Mark(&dirty, begin, begin + 0x1000); + } +#else + Require(GuestWriteWatch::GuestWriteWatchCollect_nid_postfix(reinterpret_cast(block), span, WatchedMetadataPages::Mark, &dirty)); +#endif + return dirty; + }; + const auto expect = [&](bool tagWritten, bool lengthWritten) { + const auto dirty = collect(); + Require(dirty.tagWritten == tagWritten && dirty.lengthWritten == lengthWritten); + }; + collect(); + expect(false, false); + FT_ULong nativeTag = 0; + FT_ULong nativeLength = 0; + Require(FT_Sfnt_Table_Info(face.native, 0, &nativeTag, &nativeLength) == 0); + Require(FT_Sfnt_Table_Info_nid_postfix(face.guest, 0, tag, length) == 0); + Require(*tag == nativeTag && *length == nativeLength); + expect(true, true); + FT_ULong nativeAlias = 0; + Require(FT_Sfnt_Table_Info(face.native, 0, &nativeAlias, &nativeAlias) == 0); + { + GuestArena::HostWrite write(tag, sizeof(*tag)); + *tag = sentinel; + } + collect(); + expect(false, false); + Require(FT_Sfnt_Table_Info_nid_postfix(face.guest, 0, tag, tag) == 0 && *tag == nativeAlias); + expect(true, false); + { + GuestArena::HostWrite write(length, sizeof(*length)); + *length = 0; + } + collect(); + expect(false, false); + FT_ULong queriedLength = 0; + Require(FT_Load_Sfnt_Table(face.native, nativeTag, 0, nullptr, &queriedLength) == 0); + Require(FT_Load_Sfnt_Table_nid_postfix(face.guest, nativeTag, 0, reinterpret_cast(length), length) == 0); + Require(*length == queriedLength); + expect(false, true); + Require(FT_Sfnt_Table_Info_nid_postfix(nullptr, 0, tag, length) == FT_Err_Invalid_Face_Handle); + expect(false, false); +#ifndef _WIN32 + Require(GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(block, span)); +#endif + GuestArena::GuestArenaReset_nid_postfix(block, span); + GuestArena::GuestArenaRelease_nid_postfix(block, span); + std::puts("FreeType SFNT watched metadata: split outputs, alias and zero-length query recorded"); +} + int main() { void* library = nullptr; FT_Library nativeLibrary = nullptr; @@ -148,6 +249,7 @@ int main() { {"TAIL", {7, 6, 5, 4, 3, 2, 1, 0, 255, 254, 253, 252, 251, 250, 249}}, {"ZERO", {}}, {std::string("\xF1\x23\xAB\xCD", 4), {19, 29, 39, 49, 59, 69, 79, 89}}}); FacePair face(library, nativeLibrary, font); + WatchedMetadata(face); const unsigned tableCount = (static_cast(font[4]) << 8) | font[5]; Require(font.size() + 16 < 1024); std::uint64_t count = 0; From 0e0a93bab5fc179dc27fca40d8c3fedf5f71fa53 Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Thu, 22 Oct 2026 08:28:42 +0200 Subject: [PATCH 008/198] test(libSceFreeType): preserve SFNT outputs on query errors --- core/libs/tests/GuestFreeTypeSfnt.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/libs/tests/GuestFreeTypeSfnt.cpp b/core/libs/tests/GuestFreeTypeSfnt.cpp index 54cea25405..b1553f84bb 100644 --- a/core/libs/tests/GuestFreeTypeSfnt.cpp +++ b/core/libs/tests/GuestFreeTypeSfnt.cpp @@ -228,7 +228,10 @@ static void WatchedMetadata(FacePair& face) { Require(FT_Load_Sfnt_Table_nid_postfix(face.guest, nativeTag, 0, reinterpret_cast(length), length) == 0); Require(*length == queriedLength); expect(false, true); + const auto retainedTag = *tag; + const auto retainedLength = *length; Require(FT_Sfnt_Table_Info_nid_postfix(nullptr, 0, tag, length) == FT_Err_Invalid_Face_Handle); + Require(*tag == retainedTag && *length == retainedLength); expect(false, false); #ifndef _WIN32 Require(GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(block, span)); From 19230580171979188d3d86b3229ddbd3d708526c Mon Sep 17 00:00:00 2001 From: Santhosh Kumar Date: Thu, 22 Oct 2026 20:00:24 +0530 Subject: [PATCH 009/198] feat(libSceLibreSsl): select advertised application protocols --- core/libs/CMakeLists.txt | 2 + core/libs/prx/libSceLibreSsl/Export.cpp | 5 - .../libSceLibreSsl/ProtocolSelection.cmake | 17 ++ .../libSceLibreSsl/src/ProtocolSelection.cpp | 36 ++++ core/libs/tests/GuestLibreSslProtocol.cpp | 157 ++++++++++++++++++ docs/dev/debt/libSceLibreSsl.md | 4 + 6 files changed, 216 insertions(+), 5 deletions(-) create mode 100644 core/libs/prx/libSceLibreSsl/ProtocolSelection.cmake create mode 100644 core/libs/prx/libSceLibreSsl/src/ProtocolSelection.cpp create mode 100644 core/libs/tests/GuestLibreSslProtocol.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index ef468b08aa..921172b435 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -238,6 +238,8 @@ endif() add_custom_target(libs DEPENDS ${PATCHED_OUTPUTS}) +include(prx/libSceLibreSsl/ProtocolSelection.cmake) + if(BUILD_TESTING) add_test_executable(guest_posix_scalar_io_tests tests/GuestPosixScalarIo.cpp) target_include_directories(guest_posix_scalar_io_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/core/libs/prx/libSceLibreSsl/Export.cpp b/core/libs/prx/libSceLibreSsl/Export.cpp index 93b05e0fec..9bc5a0475a 100644 --- a/core/libs/prx/libSceLibreSsl/Export.cpp +++ b/core/libs/prx/libSceLibreSsl/Export.cpp @@ -1556,11 +1556,6 @@ int APS5_VABI SSL_read_nid_postfix(void) { return 0; } -int APS5_VABI SSL_select_next_proto_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI SSL_set_SSL_CTX_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceLibreSsl/ProtocolSelection.cmake b/core/libs/prx/libSceLibreSsl/ProtocolSelection.cmake new file mode 100644 index 0000000000..feaf297593 --- /dev/null +++ b/core/libs/prx/libSceLibreSsl/ProtocolSelection.cmake @@ -0,0 +1,17 @@ +target_sources(libSceLibreSsl PRIVATE ${CMAKE_CURRENT_LIST_DIR}/src/ProtocolSelection.cpp) + +if(BUILD_TESTING) + add_test_executable(guest_libressl_protocol_tests ${CMAKE_CURRENT_SOURCE_DIR}/tests/GuestLibreSslProtocol.cpp) + target_include_directories(guest_libressl_protocol_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_libressl_protocol_tests PRIVATE libc ${CMAKE_DL_LIBS}) + add_dependencies(guest_libressl_protocol_tests libs) + configure_windows_unwind(guest_libressl_protocol_tests) + add_test(NAME guest_libressl_protocol COMMAND guest_libressl_protocol_tests + $ SSL_select_next_proto_nid_postfix) + add_test(NAME guest_libressl_protocol_nid COMMAND guest_libressl_protocol_tests + ${PATCHED_DIR}/libSceLibreSsl.prx CpIzEs1Q1Ug) + if(WIN32) + set_tests_properties(guest_libressl_protocol guest_libressl_protocol_nid PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceLibreSsl/src/ProtocolSelection.cpp b/core/libs/prx/libSceLibreSsl/src/ProtocolSelection.cpp new file mode 100644 index 0000000000..5eb950163a --- /dev/null +++ b/core/libs/prx/libSceLibreSsl/src/ProtocolSelection.cpp @@ -0,0 +1,36 @@ +#include "prx/libc/include/General.hpp" + +#include +#include +#include + +namespace { + +bool ValidProtocols(const unsigned char* protocols, std::size_t size) { + if (!protocols || size == 0) return false; + for (std::size_t offset = 0; offset < size;) { + const std::size_t length = protocols[offset++]; + if (length == 0 || length > size - offset) return false; + offset += length; + } + return true; +} + +} + +extern "C" int APS5_VABI SSL_select_next_proto_nid_postfix(unsigned char** output, unsigned char* outputLength, + const unsigned char* peer, std::uint32_t peerLength, const unsigned char* supported, std::uint32_t supportedLength) { + if (!output || !outputLength || !ValidProtocols(peer, peerLength) || !ValidProtocols(supported, supportedLength)) + NotImplemented_nid_no_patch("SSL_select_next_proto: null output or invalid protocol list"); + for (std::size_t offered = 0; offered < peerLength; offered += 1 + peer[offered]) { + for (std::size_t local = 0; local < supportedLength; local += 1 + supported[local]) { + if (peer[offered] != supported[local] || std::memcmp(peer + offered + 1, supported + local + 1, peer[offered]) != 0) continue; + *output = const_cast(peer + offered + 1); + *outputLength = peer[offered]; + return 1; + } + } + *output = const_cast(supported + 1); + *outputLength = supported[0]; + return 2; +} diff --git a/core/libs/tests/GuestLibreSslProtocol.cpp b/core/libs/tests/GuestLibreSslProtocol.cpp new file mode 100644 index 0000000000..2f60dfbf36 --- /dev/null +++ b/core/libs/tests/GuestLibreSslProtocol.cpp @@ -0,0 +1,157 @@ +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#else +#include +#endif + +using SelectProtocol = int (APS5_VABI*)(unsigned char**, unsigned char*, const unsigned char*, std::uint32_t, const unsigned char*, std::uint32_t); +using Protocol = std::vector; +using Protocols = std::vector; + +void Require(bool condition, std::source_location location = std::source_location::current()) { + if (!condition) { + std::fprintf(stderr, "LibreSSL protocol check failed at line %u\n", location.line()); + std::abort(); + } +} + +Protocol Encode(const Protocols& names) { + Protocol result; + for (const auto& name : names) { + Require(!name.empty() && name.size() <= 255); + result.push_back(static_cast(name.size())); + result.insert(result.end(), name.begin(), name.end()); + } + return result; +} + +void CheckSelection(SelectProtocol select, const Protocols& peerNames, const Protocols& supportedNames) { + const auto peer = Encode(peerNames); + const auto supported = Encode(supportedNames); + const auto peerCopy = peer; + const auto supportedCopy = supported; + const unsigned char* expected = supported.data() + 1; + std::size_t expectedLength = supportedNames.front().size(); + int expectedStatus = 2; + std::size_t offset = 1; + for (const auto& name : peerNames) { + if (std::find(supportedNames.begin(), supportedNames.end(), name) != supportedNames.end()) { + expected = peer.data() + offset; + expectedLength = name.size(); + expectedStatus = 1; + break; + } + offset += name.size() + 1; + } + unsigned char* output = nullptr; + std::array length{0xC1, 0xA5, 0xC2}; + Require(select(&output, &length[1], peer.data(), static_cast(peer.size()), + supported.data(), static_cast(supported.size())) == expectedStatus); + Require(output == expected && length[1] == expectedLength); + Require(length[0] == 0xC1 && length[2] == 0xC2); + Require(peer == peerCopy && supported == supportedCopy); +} + +void CheckInvalid(SelectProtocol select, const unsigned char* peer, std::uint32_t peerLength, + const unsigned char* supported, std::uint32_t supportedLength) { + unsigned char sentinel = 0; + unsigned char* output = &sentinel; + unsigned char length = 0xA5; + bool threw = false; + try { + select(&output, &length, peer, peerLength, supported, supportedLength); + } catch (const std::runtime_error& error) { + threw = std::string(error.what()).starts_with("SSL_select_next_proto:"); + } + Require(threw && output == &sentinel && length == 0xA5); +} + +void Run(SelectProtocol select) { + const Protocol http{'h', 't', 't', 'p', '/', '1', '.', '1'}; + const Protocol h2{'h', '2'}; + const Protocol h3{'h', '3'}; + CheckSelection(select, {h2, http}, {http, h2}); + CheckSelection(select, {http, h2}, {h2, http}); + CheckSelection(select, {h3}, {http, h2}); + CheckSelection(select, {h2, h2, http}, {http, h2, h2}); + CheckSelection(select, {{0, 0xFF, 0x80}, {'a', 0, 'b'}}, {{'a', 0, 'b'}, {0, 0xFF, 0x80}}); + CheckSelection(select, {{'h', '2', 0}, {'H', '2'}}, {h2, http}); + Protocol longest(255); + for (std::size_t index = 0; index < longest.size(); ++index) longest[index] = static_cast(index); + CheckSelection(select, {longest}, {h2, longest}); + CheckSelection(select, {h2}, {longest}); + + const Protocols alphabet{{'a'}, {'a', 'b'}, {'b'}, {0, 0xFF}}; + std::vector lists; + for (std::size_t count = 1, combinations = alphabet.size(); count <= 3; ++count, combinations *= alphabet.size()) { + for (std::size_t code = 0; code < combinations; ++code) { + Protocols names; + for (std::size_t index = 0, remaining = code; index < count; ++index, remaining /= alphabet.size()) + names.push_back(alphabet[remaining % alphabet.size()]); + lists.push_back(std::move(names)); + } + } + for (const auto& peer : lists) + for (const auto& supported : lists) CheckSelection(select, peer, supported); + + Protocols large(33000, Protocol{'x'}); + large.push_back(h2); + CheckSelection(select, large, {h2}); + CheckSelection(select, {h2}, large); + + const auto valid = Encode({h2, http}); + const std::vector malformed{{}, {0}, {2, 'h'}, {255, 'x'}, {1, 'a', 0}, {1, 'a', 2, 'b'}}; + for (const auto& list : malformed) { + CheckInvalid(select, list.data(), static_cast(list.size()), valid.data(), static_cast(valid.size())); + CheckInvalid(select, valid.data(), static_cast(valid.size()), list.data(), static_cast(list.size())); + } + CheckInvalid(select, nullptr, 0, valid.data(), static_cast(valid.size())); + CheckInvalid(select, nullptr, 1, valid.data(), static_cast(valid.size())); + CheckInvalid(select, valid.data(), static_cast(valid.size()), nullptr, 0); + CheckInvalid(select, valid.data(), static_cast(valid.size()), nullptr, 1); + CheckInvalid(select, valid.data(), 2, valid.data(), static_cast(valid.size())); + unsigned char* output = nullptr; + unsigned char length = 0xA5; + for (const bool missingPointer : {false, true}) { + bool threw = false; + try { + select(missingPointer ? nullptr : &output, missingPointer ? &length : nullptr, + valid.data(), static_cast(valid.size()), valid.data(), static_cast(valid.size())); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw && output == nullptr && length == 0xA5); + } + std::printf("LibreSSL protocol selection: 7056 generated pairs, boundaries and invalid inputs passed\n"); +} + +int main(int argc, char** argv) { + Require(argc == 3); +#ifdef _WIN32 + const auto library = LoadLibraryExW(std::filesystem::path(argv[1]).c_str(), nullptr, LOAD_WITH_ALTERED_SEARCH_PATH); + Require(library != nullptr); + const auto select = std::bit_cast(GetProcAddress(library, argv[2])); +#else + const auto library = dlopen(argv[1], RTLD_NOW | RTLD_LOCAL); + if (!library) std::fprintf(stderr, "%s\n", dlerror()); + Require(library != nullptr); + const auto select = reinterpret_cast(dlsym(library, argv[2])); +#endif + Require(select != nullptr); + Run(select); +} diff --git a/docs/dev/debt/libSceLibreSsl.md b/docs/dev/debt/libSceLibreSsl.md index b8330484a5..d5fcf4f6c6 100644 --- a/docs/dev/debt/libSceLibreSsl.md +++ b/docs/dev/debt/libSceLibreSsl.md @@ -1,5 +1,9 @@ # libSceLibreSsl technical debt +### Functional + +- [SSL_select_next_proto](../../../core/libs/prx/libSceLibreSsl/src/ProtocolSelection.cpp) has no console ABI or behaviour verification. Its signature, peer preference, borrowed result pointer and first-supported-protocol fallback follow LibreSSL 3.4.1's [ssl.h](https://github.com/libressl/openbsd/blob/078b3d1df8ca2011c1605cd40418c58fa0f5b143/src/lib/libssl/ssl.h#L619-L626) and [ssl_lib.c](https://github.com/libressl/openbsd/blob/078b3d1df8ca2011c1605cd40418c58fa0f5b143/src/lib/libssl/ssl_lib.c#L1501-L1537). Null output pointers, null or empty lists, zero-length names and truncated encodings throw without writing outputs; the console's handling of these inputs is unknown. + ### Unknown function info - [libSceLibreSsl pending exports](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - 399 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibreSsl.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. From bb97918aea9d40273996518a4fff27f9a96211ba Mon Sep 17 00:00:00 2001 From: Santhosh Kumar Date: Thu, 22 Oct 2026 20:46:31 +0530 Subject: [PATCH 010/198] feat(libSceFreeType): query exact outline curve bounds --- core/libs/CMakeLists.txt | 2 + .../libs/prx/libSceFreeType/CurveBounds.cmake | 13 ++ .../prx/libSceFreeType/src/CurveBounds.cpp | 46 ++++++ core/libs/tests/GuestFreeTypeCurveBounds.cpp | 152 ++++++++++++++++++ docs/dev/debt/libSceFreeType.md | 2 + 5 files changed, 215 insertions(+) create mode 100644 core/libs/prx/libSceFreeType/CurveBounds.cmake create mode 100644 core/libs/prx/libSceFreeType/src/CurveBounds.cpp create mode 100644 core/libs/tests/GuestFreeTypeCurveBounds.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index ef468b08aa..96720e9e55 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -721,3 +721,5 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endif() + +include(prx/libSceFreeType/CurveBounds.cmake) diff --git a/core/libs/prx/libSceFreeType/CurveBounds.cmake b/core/libs/prx/libSceFreeType/CurveBounds.cmake new file mode 100644 index 0000000000..462dec7223 --- /dev/null +++ b/core/libs/prx/libSceFreeType/CurveBounds.cmake @@ -0,0 +1,13 @@ +target_sources(libSceFreeType PRIVATE ${CMAKE_CURRENT_LIST_DIR}/src/CurveBounds.cpp) + +if(BUILD_TESTING) + add_test_executable(guest_freetype_curve_bounds_tests ${LIBS_INCLUDE_DIR}/tests/GuestFreeTypeCurveBounds.cpp) + target_include_directories(guest_freetype_curve_bounds_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_freetype_curve_bounds_tests PRIVATE libSceFreeType freetype libc) + configure_windows_unwind(guest_freetype_curve_bounds_tests) + add_test(NAME guest_freetype_curve_bounds COMMAND guest_freetype_curve_bounds_tests) + if(WIN32) + set_tests_properties(guest_freetype_curve_bounds PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceFreeType/src/CurveBounds.cpp b/core/libs/prx/libSceFreeType/src/CurveBounds.cpp new file mode 100644 index 0000000000..1b532414f5 --- /dev/null +++ b/core/libs/prx/libSceFreeType/src/CurveBounds.cpp @@ -0,0 +1,46 @@ +#include "prx/libSceFreeType/include/GuestFreeType.hpp" +#include "prx/libc/include/General.hpp" + +#include +#include FT_BBOX_H + +#include +#include + +namespace { + +FT_Pos NativePosition(std::int64_t value) { + if (value < std::numeric_limits::min() || value > std::numeric_limits::max()) + NotImplemented_nid_no_patch("FT_Outline_Get_BBox: coordinate outside the native FT_Pos range"); + return static_cast(value); +} + +} + +extern "C" int APS5_VABI FT_Outline_Get_BBox_nid_postfix(FreeType::GuestOutline* outline, FreeType::GuestBBox* boundingBox) { + if (!boundingBox) return FT_Outline_Get_BBox(nullptr, nullptr); + FT_BBox result{}; + if (!outline) return FT_Outline_Get_BBox(nullptr, &result); + FT_Outline native{outline->n_contours, outline->n_points, nullptr, outline->tags, outline->contours, outline->flags}; + std::vector points; + if (outline->n_points != 0 && outline->n_contours != 0) { + if (!outline->points || !outline->tags || !outline->contours) + NotImplemented_nid_no_patch("FT_Outline_Get_BBox: missing nonempty outline arrays"); + points.reserve(outline->n_points); + constexpr std::int64_t curveLimit = std::numeric_limits::max() / 4; + bool curved = false; + bool wide = false; + for (std::size_t index = 0; index < outline->n_points; ++index) { + points.push_back({NativePosition(outline->points[index].x), NativePosition(outline->points[index].y)}); + curved |= (outline->tags[index] & 3) != FT_CURVE_TAG_ON; + wide |= outline->points[index].x < -curveLimit || outline->points[index].x > curveLimit || + outline->points[index].y < -curveLimit || outline->points[index].y > curveLimit; + } + if (curved && wide) + NotImplemented_nid_no_patch("FT_Outline_Get_BBox: curved coordinates exceed the portable arithmetic range"); + native.points = points.data(); + } + const FT_Error error = FT_Outline_Get_BBox(&native, &result); + if (error == 0) *boundingBox = {result.xMin, result.yMin, result.xMax, result.yMax}; + return error; +} diff --git a/core/libs/tests/GuestFreeTypeCurveBounds.cpp b/core/libs/tests/GuestFreeTypeCurveBounds.cpp new file mode 100644 index 0000000000..63cf291d99 --- /dev/null +++ b/core/libs/tests/GuestFreeTypeCurveBounds.cpp @@ -0,0 +1,152 @@ +#include "prx/libSceFreeType/include/GuestFreeType.hpp" + +#include +#include FT_BBOX_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace FreeType; + +extern "C" int APS5_VABI FT_Outline_Get_BBox_nid_postfix(GuestOutline*, GuestBBox*); + +void Require(bool condition, std::source_location location = std::source_location::current()) { + if (!condition) { + std::fprintf(stderr, "FreeType curve bounds check failed at line %u\n", location.line()); + std::abort(); + } +} + +bool Equal(const GuestBBox& a, const GuestBBox& b) { + return a.xMin == b.xMin && a.yMin == b.yMin && a.xMax == b.xMax && a.yMax == b.yMax; +} + +struct GuardedBox { + std::uint64_t before = 0x123456789ABCDEF0; + GuestBBox box{101, -102, 103, -104}; + std::uint64_t after = 0xFEDCBA9876543210; +}; + +GuestBBox Check(std::vector points, std::vector tags, + std::vector contours, std::int32_t flags = 0, bool failure = false) { + Require(points.size() == tags.size() && points.size() <= 65535 && contours.size() <= 65535); + const auto originalPoints = points; + const auto originalTags = tags; + const auto originalContours = contours; + std::vector nativePoints; + for (const auto& point : points) nativePoints.push_back({static_cast(point.x), static_cast(point.y)}); + auto nativeTags = tags; + auto nativeContours = contours; + FT_Outline native{static_cast(contours.size()), static_cast(points.size()), + nativePoints.data(), nativeTags.data(), nativeContours.data(), flags}; + GuestOutline guest{native.n_contours, native.n_points, points.data(), tags.data(), contours.data(), flags}; + const auto original = guest; + FT_BBox expected{101, -102, 103, -104}; + GuardedBox actual; + const auto status = FT_Outline_Get_BBox(&native, &expected); + Require(FT_Outline_Get_BBox_nid_postfix(&guest, &actual.box) == status); + Require((status != 0) == failure); + Require(Equal(actual.box, {expected.xMin, expected.yMin, expected.xMax, expected.yMax})); + Require(actual.before == 0x123456789ABCDEF0 && actual.after == 0xFEDCBA9876543210); + Require(guest.n_points == original.n_points && guest.n_contours == original.n_contours && guest.flags == flags); + Require(guest.points == original.points && guest.tags == original.tags && guest.contours == original.contours); + Require(tags == originalTags && contours == originalContours); + for (std::size_t index = 0; index < points.size(); ++index) + Require(points[index].x == originalPoints[index].x && points[index].y == originalPoints[index].y); + return actual.box; +} + +int main() { + GuardedBox invalid; + const auto sentinel = invalid.box; + FT_BBox native{}; + Require(FT_Outline_Get_BBox_nid_postfix(nullptr, &invalid.box) == FT_Outline_Get_BBox(nullptr, &native)); + Require(Equal(invalid.box, sentinel)); + Require(FT_Outline_Get_BBox_nid_postfix(reinterpret_cast(std::uintptr_t{1}), nullptr) == FT_Outline_Get_BBox(nullptr, nullptr)); + Check({}, {}, {}); + GuestOutline empty{0, 3, reinterpret_cast(std::uintptr_t{1}), + reinterpret_cast(std::uintptr_t{1}), reinterpret_cast(std::uintptr_t{1}), 123}; + Require(FT_Outline_Get_BBox_nid_postfix(&empty, &invalid.box) == 0 && Equal(invalid.box, {})); + empty.n_contours = 1; + empty.n_points = 0; + Require(FT_Outline_Get_BBox_nid_postfix(&empty, &invalid.box) == 0 && Equal(invalid.box, {})); + Require(Equal(Check({{-20, 30}, {40, -50}, {70, 80}}, {1, 1, 1}, {2}), {-20, -50, 70, 80})); + Require(Equal(Check({{0, 0}, {100, 200}, {200, 0}}, {1, 0, 1}, {2}), {0, 0, 200, 100})); + Require(Equal(Check({{0, 0}, {0, 400}, {400, 400}, {400, 0}}, {1, 2, 2, 1}, {3}), {0, 0, 400, 300})); + Check({{0, 0}, {100, 200}, {200, 200}, {300, 0}}, {1, 0, 0, 1}, {3}, -1); + Check({{-100, 0}, {0, 200}, {100, 0}, {0, -200}}, {0, 0, 0, 0}, {3}); + Check({{0, 0}, {100, 200}, {200, 0}, {-30, -40}, {-20, -50}, {10, -10}}, {1, 0, 1, 1, 1, 1}, {2, 5}); + Check({{0, 100}, {100, 0}, {200, 0}}, {2, 1, 1}, {2}, 0, true); + Check({{0, 0}, {100, 200}}, {1, 2}, {1}, 0, true); + Check({{std::numeric_limits::min(), 1}, {std::numeric_limits::max(), -1}}, {1, 1}, {1}); + constexpr std::int64_t curveLimit = std::numeric_limits::max() / 4; + Require(Equal(Check({{-curveLimit, -curveLimit}, {0, curveLimit}, {curveLimit, -curveLimit}}, {1, 0, 1}, {2}), + {-curveLimit, -curveLimit, curveLimit, 0})); + + std::mt19937 random(0xB0A1D5); + for (unsigned iteration = 0; iteration < 4096; ++iteration) { + std::vector points; + for (unsigned index = 0; index < 4; ++index) + points.push_back({static_cast(random() % 100001) - 50000, static_cast(random() % 100001) - 50000}); + Check(points, {1, 2, 2, 1}, {3}); + points.pop_back(); + Check(points, {1, 0, 1}, {2}); + } + std::vector many(65535); + std::vector tags(many.size(), 1); + std::vector contours(many.size()); + for (std::size_t index = 0; index < many.size(); ++index) { + many[index] = {static_cast(index % 1000), static_cast(index)}; + contours[index] = static_cast(index); + } + Require(Equal(Check(many, tags, {65534}), {0, 0, 999, 65534})); + Require(Equal(Check(many, tags, contours), {0, 0, 999, 65534})); + + std::array points{{{0, 0}, {1, 1}}}; + std::array smallTags{1, 1}; + std::uint16_t end = 1; + GuestOutline missing{1, 2, nullptr, smallTags.data(), &end, 0}; + invalid.box = sentinel; + bool threw = false; + try { + FT_Outline_Get_BBox_nid_postfix(&missing, &invalid.box); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw && Equal(invalid.box, sentinel)); + for (const std::int64_t control : {curveLimit + 1, -curveLimit - 1, std::int64_t{2000000000}}) { + std::array curve{{{-100, 0}, {0, control}, {100, 0}}}; + std::array curveTags{1, 0, 1}; + std::uint16_t curveEnd = 2; + GuestOutline unsupported{1, 3, curve.data(), curveTags.data(), &curveEnd, 0}; + threw = false; + try { + FT_Outline_Get_BBox_nid_postfix(&unsupported, &invalid.box); + } catch (const std::runtime_error& error) { + threw = std::string(error.what()).starts_with("FT_Outline_Get_BBox:"); + } + Require(threw && Equal(invalid.box, sentinel)); + } + if constexpr (sizeof(FT_Pos) < sizeof(std::int64_t)) { + points[1].x = std::int64_t{1} << 40; + GuestOutline wide{1, 2, points.data(), smallTags.data(), &end, 0}; + threw = false; + try { + FT_Outline_Get_BBox_nid_postfix(&wide, &invalid.box); + } catch (const std::runtime_error& error) { + threw = std::string(error.what()).starts_with("FT_Outline_Get_BBox:"); + } + Require(threw && Equal(invalid.box, sentinel)); + } else { + Check({{std::int64_t{1} << 40, -(std::int64_t{1} << 40)}, {0, 0}}, {1, 1}, {1}); + } + std::printf("FreeType curve bounds: 8192 generated curves, extrema, errors and count limits passed\n"); +} diff --git a/docs/dev/debt/libSceFreeType.md b/docs/dev/debt/libSceFreeType.md index 6d4f98438d..856b23983c 100644 --- a/docs/dev/debt/libSceFreeType.md +++ b/docs/dev/debt/libSceFreeType.md @@ -10,6 +10,8 @@ ### Functional +- [FT_Outline_Get_BBox](../../../core/libs/prx/libSceFreeType/src/CurveBounds.cpp) has no console ABI or behaviour verification; extrema and errors follow bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftbbox.c). Curved outlines with any coordinate outside ±536870911 throw: conic differences and their sum can overflow native 32-bit arithmetic even when each input fits `FT_Pos`. Coordinates outside native `FT_Pos` (signed 32 bits on Windows) and missing nonempty arrays also throw. Native output uses a local box; errors and unsupported inputs leave the guest output unchanged. Empty outlines do not inspect their arrays. + - [libSceFreeType](../../../core/libs/prx/libSceFreeType/src/GuestObjects.cpp) (libSceFreeType) - private fields (`driver`, `memory`, `stream`, `sizes_list`, `extensions`) and `subglyphs` stay null. Face, size and glyph-slot records are copies. Only edits to outline points and flags are written back before `FT_Render_Glyph`; replacing arrays or exceeding the host `FT_Pos` range throws. `FT_New_Face` reads the whole file into memory and throws for files of 2 GiB or more on Windows. On macOS, native FreeType symbols remain exported and could resolve guest imports without adaptation; Windows and Linux hide them with `--exclude-libs`. - [FT_New_Memory_Face](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - on Windows, the host FT_Long is 32 bits, so buffer sizes of 2 GiB or more or below -2 GiB throw before the font buffer is read From 991061e6401f37882d3def52caa57c8ae26f9b8e Mon Sep 17 00:00:00 2001 From: Santhosh Kumar Date: Thu, 22 Oct 2026 21:41:58 +0530 Subject: [PATCH 011/198] fix(libSceFreeType): guard curve-bound contour storage --- core/libs/prx/libSceFreeType/src/CurveBounds.cpp | 11 +++++++++++ core/libs/tests/GuestFreeTypeCurveBounds.cpp | 11 +++++++++++ docs/dev/debt/libSceFreeType.md | 2 +- 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceFreeType/src/CurveBounds.cpp b/core/libs/prx/libSceFreeType/src/CurveBounds.cpp index 1b532414f5..c99b1cd88a 100644 --- a/core/libs/prx/libSceFreeType/src/CurveBounds.cpp +++ b/core/libs/prx/libSceFreeType/src/CurveBounds.cpp @@ -26,6 +26,17 @@ extern "C" int APS5_VABI FT_Outline_Get_BBox_nid_postfix(FreeType::GuestOutline* if (outline->n_points != 0 && outline->n_contours != 0) { if (!outline->points || !outline->tags || !outline->contours) NotImplemented_nid_no_patch("FT_Outline_Get_BBox: missing nonempty outline arrays"); + if (outline->n_contours > outline->n_points) + NotImplemented_nid_no_patch("FT_Outline_Get_BBox: inconsistent contour bounds"); + std::size_t first = 0; + for (std::size_t contour = 0; contour < outline->n_contours; ++contour) { + const std::size_t last = outline->contours[contour]; + if (last < first || last >= outline->n_points) + NotImplemented_nid_no_patch("FT_Outline_Get_BBox: inconsistent contour bounds"); + first = last + 1; + } + if (first != outline->n_points) + NotImplemented_nid_no_patch("FT_Outline_Get_BBox: inconsistent contour bounds"); points.reserve(outline->n_points); constexpr std::int64_t curveLimit = std::numeric_limits::max() / 4; bool curved = false; diff --git a/core/libs/tests/GuestFreeTypeCurveBounds.cpp b/core/libs/tests/GuestFreeTypeCurveBounds.cpp index 63cf291d99..b2efd900f2 100644 --- a/core/libs/tests/GuestFreeTypeCurveBounds.cpp +++ b/core/libs/tests/GuestFreeTypeCurveBounds.cpp @@ -122,6 +122,17 @@ int main() { threw = true; } Require(threw && Equal(invalid.box, sentinel)); + for (auto badContours : std::vector>{{2}, {0}, {1, 1}, {1, 0}, {0, 1, 1}}) { + GuestOutline malformed{static_cast(badContours.size()), 2, + points.data(), smallTags.data(), badContours.data(), 0}; + threw = false; + try { + FT_Outline_Get_BBox_nid_postfix(&malformed, &invalid.box); + } catch (const std::runtime_error& error) { + threw = std::string(error.what()).starts_with("FT_Outline_Get_BBox: inconsistent contour bounds"); + } + Require(threw && Equal(invalid.box, sentinel)); + } for (const std::int64_t control : {curveLimit + 1, -curveLimit - 1, std::int64_t{2000000000}}) { std::array curve{{{-100, 0}, {0, control}, {100, 0}}}; std::array curveTags{1, 0, 1}; diff --git a/docs/dev/debt/libSceFreeType.md b/docs/dev/debt/libSceFreeType.md index 856b23983c..31d0a581a6 100644 --- a/docs/dev/debt/libSceFreeType.md +++ b/docs/dev/debt/libSceFreeType.md @@ -10,7 +10,7 @@ ### Functional -- [FT_Outline_Get_BBox](../../../core/libs/prx/libSceFreeType/src/CurveBounds.cpp) has no console ABI or behaviour verification; extrema and errors follow bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftbbox.c). Curved outlines with any coordinate outside ±536870911 throw: conic differences and their sum can overflow native 32-bit arithmetic even when each input fits `FT_Pos`. Coordinates outside native `FT_Pos` (signed 32 bits on Windows) and missing nonempty arrays also throw. Native output uses a local box; errors and unsupported inputs leave the guest output unchanged. Empty outlines do not inspect their arrays. +- [FT_Outline_Get_BBox](../../../core/libs/prx/libSceFreeType/src/CurveBounds.cpp) has no console ABI or behaviour verification; extrema and errors follow bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftbbox.c). Curved outlines with any coordinate outside ±536870911 throw: conic differences and their sum can overflow native 32-bit arithmetic even when each input fits `FT_Pos`. Coordinates outside native `FT_Pos` (signed 32 bits on Windows) and missing nonempty arrays or inconsistent contour partitions also throw. Native output uses a local box; errors and unsupported inputs leave the guest output unchanged. Empty outlines do not inspect their arrays. - [libSceFreeType](../../../core/libs/prx/libSceFreeType/src/GuestObjects.cpp) (libSceFreeType) - private fields (`driver`, `memory`, `stream`, `sizes_list`, `extensions`) and `subglyphs` stay null. Face, size and glyph-slot records are copies. Only edits to outline points and flags are written back before `FT_Render_Glyph`; replacing arrays or exceeding the host `FT_Pos` range throws. `FT_New_Face` reads the whole file into memory and throws for files of 2 GiB or more on Windows. On macOS, native FreeType symbols remain exported and could resolve guest imports without adaptation; Windows and Linux hide them with `--exclude-libs`. From 2637f07c629de0c5cc252c53de21b5da7d42e0e6 Mon Sep 17 00:00:00 2001 From: Santhosh Kumar Date: Fri, 23 Oct 2026 16:41:34 +0530 Subject: [PATCH 012/198] feat(libSceFreeType): configure scalar and darkening properties --- core/libs/prx/libSceFreeType/CMakeLists.txt | 2 + .../prx/libSceFreeType/ModuleProperties.cmake | 19 ++ .../libSceFreeType/src/ModuleProperties.cpp | 78 +++++++ .../tests/GuestFreeTypeModuleProperties.cpp | 192 ++++++++++++++++++ docs/dev/debt/libSceFreeType.md | 2 + 5 files changed, 293 insertions(+) create mode 100644 core/libs/prx/libSceFreeType/ModuleProperties.cmake create mode 100644 core/libs/prx/libSceFreeType/src/ModuleProperties.cpp create mode 100644 core/libs/tests/GuestFreeTypeModuleProperties.cpp diff --git a/core/libs/prx/libSceFreeType/CMakeLists.txt b/core/libs/prx/libSceFreeType/CMakeLists.txt index 689682460c..6d48c05b36 100644 --- a/core/libs/prx/libSceFreeType/CMakeLists.txt +++ b/core/libs/prx/libSceFreeType/CMakeLists.txt @@ -101,3 +101,5 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endif() + +include("${CMAKE_CURRENT_LIST_DIR}/ModuleProperties.cmake") diff --git a/core/libs/prx/libSceFreeType/ModuleProperties.cmake b/core/libs/prx/libSceFreeType/ModuleProperties.cmake new file mode 100644 index 0000000000..1f2e72102b --- /dev/null +++ b/core/libs/prx/libSceFreeType/ModuleProperties.cmake @@ -0,0 +1,19 @@ +target_sources(libSceFreeType PRIVATE ${LIBS_INCLUDE_DIR}/prx/libSceFreeType/src/ModuleProperties.cpp) + +if(BUILD_TESTING) + add_test_executable(guest_freetype_module_properties_tests ${LIBS_INCLUDE_DIR}/tests/GuestFreeTypeModuleProperties.cpp) + target_include_directories(guest_freetype_module_properties_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_freetype_module_properties_tests PRIVATE freetype ${CMAKE_DL_LIBS}) + if(WIN32) + target_link_libraries(guest_freetype_module_properties_tests PRIVATE libc) + endif() + configure_windows_unwind(guest_freetype_module_properties_tests) + add_dependencies(guest_freetype_module_properties_tests libs) + add_test(NAME guest_freetype_module_properties COMMAND guest_freetype_module_properties_tests $ unpatched) + add_test(NAME guest_freetype_module_properties_nid COMMAND guest_freetype_module_properties_tests ${PATCHED_DIR}/libSceFreeType.prx patched) + set_tests_properties(guest_freetype_module_properties guest_freetype_module_properties_nid PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_freetype_module_properties guest_freetype_module_properties_nid PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceFreeType/src/ModuleProperties.cpp b/core/libs/prx/libSceFreeType/src/ModuleProperties.cpp new file mode 100644 index 0000000000..3039292046 --- /dev/null +++ b/core/libs/prx/libSceFreeType/src/ModuleProperties.cpp @@ -0,0 +1,78 @@ +#include "prx/libSceFreeType/include/GuestObjects.hpp" +#include "prx/libc/include/General.hpp" + +#include FT_MODULE_H + +#include +#include + +using namespace FreeType; + +static_assert(sizeof(FT_UInt) == sizeof(std::uint32_t)); +static_assert(sizeof(FT_Int32) == sizeof(std::int32_t)); +static_assert(sizeof(FT_Int) == sizeof(std::int32_t)); +static_assert(sizeof(FT_Bool) == sizeof(std::uint8_t)); + +namespace { + +enum class PropertyType { Unsigned, Signed, Boolean, Darkening, Unsupported }; +using DarkeningParameters = FT_Int[8]; + +PropertyType TypeOf(const char* moduleName, const char* propertyName) { + const bool postScript = std::strcmp(moduleName, "cff") == 0 || std::strcmp(moduleName, "type1") == 0 || + std::strcmp(moduleName, "t1cid") == 0; + const bool autoHinter = std::strcmp(moduleName, "autofitter") == 0; + if (std::strcmp(moduleName, "truetype") == 0 && std::strcmp(propertyName, "interpreter-version") == 0) { + return PropertyType::Unsigned; + } + if (postScript && std::strcmp(propertyName, "hinting-engine") == 0) return PropertyType::Unsigned; + if (postScript && std::strcmp(propertyName, "random-seed") == 0) return PropertyType::Signed; + if (postScript || autoHinter) { + if (std::strcmp(propertyName, "no-stem-darkening") == 0) return PropertyType::Boolean; + if (std::strcmp(propertyName, "darkening-parameters") == 0) return PropertyType::Darkening; + } + return PropertyType::Unsupported; +} + +template +int Read(FT_Library library, const char* moduleName, const char* propertyName, void* output) { + TValue value{}; + const auto error = FT_Property_Get(library, moduleName, propertyName, &value); + if (!error) std::memcpy(output, &value, sizeof(value)); + return error; +} + +template +int Write(FT_Library library, const char* moduleName, const char* propertyName, const void* input) { + TValue value{}; + std::memcpy(&value, input, sizeof(value)); + return FT_Property_Set(library, moduleName, propertyName, &value); +} + +} + +extern "C" int APS5_VABI FT_Property_Get_nid_postfix(GuestLibrary* library, const char* moduleName, + const char* propertyName, void* value) { + const auto host = library ? library->host : nullptr; + if (!host || !moduleName || !propertyName || !value) return FT_Property_Get(host, moduleName, propertyName, value); + switch (TypeOf(moduleName, propertyName)) { + case PropertyType::Unsigned: return Read(host, moduleName, propertyName, value); + case PropertyType::Signed: return Read(host, moduleName, propertyName, value); + case PropertyType::Boolean: return Read(host, moduleName, propertyName, value); + case PropertyType::Darkening: return Read(host, moduleName, propertyName, value); + default: NotImplemented_nid_no_patch(__func__); + } +} + +extern "C" int APS5_VABI FT_Property_Set_nid_postfix(GuestLibrary* library, const char* moduleName, + const char* propertyName, const void* value) { + const auto host = library ? library->host : nullptr; + if (!host || !moduleName || !propertyName || !value) return FT_Property_Set(host, moduleName, propertyName, value); + switch (TypeOf(moduleName, propertyName)) { + case PropertyType::Unsigned: return Write(host, moduleName, propertyName, value); + case PropertyType::Signed: return Write(host, moduleName, propertyName, value); + case PropertyType::Boolean: return Write(host, moduleName, propertyName, value); + case PropertyType::Darkening: return Write(host, moduleName, propertyName, value); + default: NotImplemented_nid_no_patch(__func__); + } +} diff --git a/core/libs/tests/GuestFreeTypeModuleProperties.cpp b/core/libs/tests/GuestFreeTypeModuleProperties.cpp new file mode 100644 index 0000000000..773154e387 --- /dev/null +++ b/core/libs/tests/GuestFreeTypeModuleProperties.cpp @@ -0,0 +1,192 @@ +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include FT_FREETYPE_H +#include FT_MODULE_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#else +#include +#endif + +namespace FreeType { +struct GuestLibrary; +} + +using FreeType::GuestLibrary; + +struct Api { + int (APS5_VABI *initialize)(GuestLibrary**); + int (APS5_VABI *done)(GuestLibrary*); + int (APS5_VABI *get)(GuestLibrary*, const char*, const char*, void*); + int (APS5_VABI *set)(GuestLibrary*, const char*, const char*, const void*); +}; + +static void Require(bool condition, std::source_location location = std::source_location::current()) { + if (!condition) { + std::fprintf(stderr, "Module property check failed at line %u\n", location.line()); + std::abort(); + } +} + +template +static auto NativePointer(TValue& value) { + if constexpr (requires { value.data(); }) return value.data(); + else return &value; +} + +template +static void Query(const Api& api, GuestLibrary* library, FT_Library native, const char* module, const char* property) { + TValue expected{}; + std::memset(&expected, 0xA5, sizeof(expected)); + const int expectedError = FT_Property_Get(native, module, property, NativePointer(expected)); + std::array storage{}; + storage.fill(0xA5); + const auto before = storage; + auto* output = storage.data() + 7; + Require(api.get(library, module, property, output) == expectedError); + if (expectedError) Require(storage == before); + else { + Require(std::memcmp(output, &expected, sizeof(expected)) == 0); + Require(std::all_of(storage.begin(), storage.begin() + 7, [](auto byte) { return byte == 0xA5; })); + Require(std::all_of(storage.begin() + 7 + sizeof(expected), storage.end(), [](auto byte) { return byte == 0xA5; })); + } +} + +template +static void Change(const Api& api, GuestLibrary* library, FT_Library native, GuestLibrary* other, + FT_Library nativeOther, const char* module, const char* property, TValue value) { + std::array input{}; + input.fill(0xCC); + std::memcpy(input.data() + 5, &value, sizeof(value)); + const auto before = input; + const int expectedError = FT_Property_Set(native, module, property, NativePointer(value)); + Require(api.set(library, module, property, input.data() + 5) == expectedError); + Require(input == before); + Query(api, library, native, module, property); + Query(api, other, nativeOther, module, property); +} + +static void Reject(const Api& api, GuestLibrary* library, const char* module, const char* property) { + std::array output{}; + output.fill(0xA5); + const auto before = output; + bool threw = false; + try { api.get(library, module, property, output.data()); } + catch (const std::runtime_error&) { threw = true; } + Require(threw && output == before); + threw = false; + try { api.set(library, module, property, reinterpret_cast(1)); } + catch (const std::runtime_error&) { threw = true; } + Require(threw); +} + +static void Run(const Api& api) { + GuestLibrary* library = nullptr; + GuestLibrary* other = nullptr; + FT_Library native = nullptr; + FT_Library nativeOther = nullptr; + Require(api.initialize(&library) == 0 && api.initialize(&other) == 0); + Require(FT_Init_FreeType(&native) == 0 && FT_Init_FreeType(&nativeOther) == 0); + Query(api, library, native, "truetype", "interpreter-version"); + for (const auto version : {35u, 40u, 38u, 0u, 0xFFFFFFFFu}) { + Change(api, library, native, other, nativeOther, "truetype", "interpreter-version", version); + } + const std::array, 5> parameters{{ + {500, 300, 1000, 200, 1500, 100, 2000, 0}, + {600, 250, 1100, 150, 1600, 50, 2500, 0}, + {-1, 0, 1000, 0, 1500, 0, 2000, 0}, + {1000, 0, 500, 0, 1500, 0, 2000, 0}, + {500, 501, 1000, 0, 1500, 0, 2000, 0} + }}; + for (const char* module : {"cff", "type1", "t1cid", "autofitter"}) { + Query(api, library, native, module, "no-stem-darkening"); + for (const auto value : {0u, 1u, 2u, 255u}) { + Change(api, library, native, other, nativeOther, module, "no-stem-darkening", static_cast(value)); + } + Query>(api, library, native, module, "darkening-parameters"); + for (const auto& value : parameters) Change(api, library, native, other, nativeOther, module, "darkening-parameters", value); + if (std::strcmp(module, "autofitter") == 0) continue; + for (const auto value : {1u, 0u, 2u, 0xFFFFFFFFu}) { + Change(api, library, native, other, nativeOther, module, "hinting-engine", value); + } + for (const auto value : {std::numeric_limits::min(), -1, 0, 1, std::numeric_limits::max()}) { + Change(api, library, native, other, nativeOther, module, "random-seed", value); + } + } + std::array output{}; + output.fill(0xA5); + const auto before = output; + Require(api.get(nullptr, reinterpret_cast(1), reinterpret_cast(1), output.data()) == + FT_Property_Get(nullptr, reinterpret_cast(1), reinterpret_cast(1), output.data())); + Require(api.set(nullptr, reinterpret_cast(1), reinterpret_cast(1), reinterpret_cast(1)) == + FT_Property_Set(nullptr, reinterpret_cast(1), reinterpret_cast(1), reinterpret_cast(1))); + Require(api.get(library, nullptr, "interpreter-version", output.data()) == FT_Property_Get(native, nullptr, "interpreter-version", output.data())); + Require(api.get(library, "truetype", nullptr, output.data()) == FT_Property_Get(native, "truetype", nullptr, output.data())); + Require(api.get(library, reinterpret_cast(1), reinterpret_cast(1), nullptr) == + FT_Property_Get(native, reinterpret_cast(1), reinterpret_cast(1), nullptr)); + Require(api.set(library, nullptr, "interpreter-version", output.data()) == FT_Property_Set(native, nullptr, "interpreter-version", output.data())); + Require(api.set(library, "truetype", nullptr, output.data()) == FT_Property_Set(native, "truetype", nullptr, output.data())); + Require(api.set(library, reinterpret_cast(1), reinterpret_cast(1), nullptr) == + FT_Property_Set(native, reinterpret_cast(1), reinterpret_cast(1), nullptr)); + Require(output == before); + Reject(api, library, "autofitter", "increase-x-height"); + Reject(api, library, "autofitter", "glyph-to-script-map"); + Reject(api, library, "ot-svg", "svg-hooks"); + Reject(api, library, "truetype", "unknown-property"); + Reject(api, library, "unknown-module", "interpreter-version"); + Query(api, library, native, "truetype", "interpreter-version"); + Query(api, other, nativeOther, "truetype", "interpreter-version"); + Require(api.done(library) == 0 && api.done(other) == 0); + Require(FT_Done_FreeType(native) == 0 && FT_Done_FreeType(nativeOther) == 0); +} + +template +static TFunction Import(TModule module, const char* name, const char* nid, bool patched) { + const std::string symbol = patched ? nid : std::string(name) + "_nid_postfix"; +#ifdef _WIN32 + const auto address = GetProcAddress(module, symbol.c_str()); +#else + const auto address = dlsym(module, symbol.c_str()); +#endif + Require(address != nullptr); + return std::bit_cast(address); +} + +int main(int argc, char** argv) { + Require(argc == 3); + const bool patched = std::strcmp(argv[2], "patched") == 0; + Require(patched || std::strcmp(argv[2], "unpatched") == 0); +#ifdef _WIN32 + auto module = LoadLibraryW(std::filesystem::path(argv[1]).c_str()); +#else + auto module = dlopen(argv[1], RTLD_NOW | RTLD_LOCAL); + if (!module) std::fprintf(stderr, "%s\n", dlerror()); +#endif + Require(module != nullptr); + Api api{}; + api.initialize = Import(module, "FT_Init_FreeType", "GNsiLzm4SH8", patched); + api.done = Import(module, "FT_Done_FreeType", "dH02CpEEcYk", patched); + api.get = Import(module, "FT_Property_Get", "8fAMdZCnewA", patched); + api.set = Import(module, "FT_Property_Set", "LsB538+IPrE", patched); + Run(api); +#ifdef _WIN32 + Require(FreeLibrary(module) != 0); +#else + Require(dlclose(module) == 0); +#endif +} diff --git a/docs/dev/debt/libSceFreeType.md b/docs/dev/debt/libSceFreeType.md index f302e4dc73..dbc2acd894 100644 --- a/docs/dev/debt/libSceFreeType.md +++ b/docs/dev/debt/libSceFreeType.md @@ -16,6 +16,8 @@ - [FT_Get_Charmap_Index, FT_Get_CMap_Language_ID, FT_Get_CMap_Format](../../../core/libs/prx/libSceFreeType/src/Charmap.cpp) (libSceFreeType) - console signatures and return values are unverified; the adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftobjs.c) and the existing guest LP64 model. Charmaps must be the face's original read-only guest records; copied or otherwise unregistered records with a non-null face throw instead of reading private native cmap fields from guest memory +- [FT_Property_Get, FT_Property_Set](../../../core/libs/prx/libSceFreeType/src/ModuleProperties.cpp) - console signatures, property availability and backend/error equivalence are unverified. Only `truetype/interpreter-version`, the `cff`, `type1` and `t1cid` hinting-engine, random-seed, no-stem-darkening and darkening-parameters properties, and the autofitter no-stem-darkening and darkening-parameters properties follow bundled FreeType 2.13.3. Other module/property pairs throw, including properties carrying face pointers or callbacks. Values use 32-bit signed/unsigned integers, one-byte booleans or eight 32-bit darkening parameters; getters copy only successful native results. + - [FT_Outline_Reverse, FT_Outline_Translate](../../../core/libs/prx/libSceFreeType/src/Outline.cpp) (libSceFreeType) - console signatures, count signedness and overflow behavior are unverified; the adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftoutln.c) with LP64 coordinates. Translation uses 64-bit modular addition to match that build's `ADD_LONG`; reversal preserves each contour's first point and toggles the reverse-fill flag. Coordinates outside the host `FT_Pos` range still throw if the modified outline is later rendered on Windows - [FT_Get_X11_Font_Format, FT_Get_FSType_Flags](../../../core/libs/prx/libSceFreeType/src/FaceInfo.cpp) (libSceFreeType) - console signatures, format labels and embedding-flag behavior are unverified; the adapter follows bundled FreeType 2.13.3's [format](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftfntfmt.c) and [FSType](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftfstype.c) services. Format strings borrow native driver storage; the console's string lifetime has not been measured From 32db4464350c135967088e787fbee1862891cb7f Mon Sep 17 00:00:00 2001 From: Santhosh Kumar Date: Fri, 23 Oct 2026 17:05:13 +0530 Subject: [PATCH 013/198] test(libSceLibreSsl): select packaged dependencies for NID checks --- core/libs/prx/libSceLibreSsl/ProtocolSelection.cmake | 4 +++- core/libs/tests/GuestLibreSslProtocol.cpp | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceLibreSsl/ProtocolSelection.cmake b/core/libs/prx/libSceLibreSsl/ProtocolSelection.cmake index 0334f22f9c..111f818aec 100644 --- a/core/libs/prx/libSceLibreSsl/ProtocolSelection.cmake +++ b/core/libs/prx/libSceLibreSsl/ProtocolSelection.cmake @@ -14,7 +14,9 @@ if(BUILD_TESTING) add_test(NAME guest_libressl_protocol_nid COMMAND guest_libressl_protocol_tests ${PATCHED_DIR}/libSceLibreSsl.prx CpIzEs1Q1Ug) if(WIN32) - set_tests_properties(guest_libressl_protocol guest_libressl_protocol_nid PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_libressl_protocol PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(guest_libressl_protocol_nid PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:${PATCHED_DIR}") endif() endif() diff --git a/core/libs/tests/GuestLibreSslProtocol.cpp b/core/libs/tests/GuestLibreSslProtocol.cpp index 2f60dfbf36..ae2ddbc2c7 100644 --- a/core/libs/tests/GuestLibreSslProtocol.cpp +++ b/core/libs/tests/GuestLibreSslProtocol.cpp @@ -144,6 +144,7 @@ int main(int argc, char** argv) { Require(argc == 3); #ifdef _WIN32 const auto library = LoadLibraryExW(std::filesystem::path(argv[1]).c_str(), nullptr, LOAD_WITH_ALTERED_SEARCH_PATH); + if (!library) std::fprintf(stderr, "LoadLibraryExW failed: %lu\n", GetLastError()); Require(library != nullptr); const auto select = std::bit_cast(GetProcAddress(library, argv[2])); #else From a3640ce984124500c427f785cae5d18f5e0d32e3 Mon Sep 17 00:00:00 2001 From: Santhosh Kumar Date: Fri, 23 Oct 2026 17:05:15 +0530 Subject: [PATCH 014/198] test(libSceFreeType): select packaged dependencies for NID checks --- core/libs/prx/libSceFreeType/ModuleProperties.cmake | 4 +++- core/libs/tests/GuestFreeTypeModuleProperties.cpp | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceFreeType/ModuleProperties.cmake b/core/libs/prx/libSceFreeType/ModuleProperties.cmake index 1f2e72102b..b4ff4952da 100644 --- a/core/libs/prx/libSceFreeType/ModuleProperties.cmake +++ b/core/libs/prx/libSceFreeType/ModuleProperties.cmake @@ -13,7 +13,9 @@ if(BUILD_TESTING) add_test(NAME guest_freetype_module_properties_nid COMMAND guest_freetype_module_properties_tests ${PATCHED_DIR}/libSceFreeType.prx patched) set_tests_properties(guest_freetype_module_properties guest_freetype_module_properties_nid PROPERTIES TIMEOUT 20) if(WIN32) - set_tests_properties(guest_freetype_module_properties guest_freetype_module_properties_nid PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_freetype_module_properties PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(guest_freetype_module_properties_nid PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:${PATCHED_DIR}") endif() endif() diff --git a/core/libs/tests/GuestFreeTypeModuleProperties.cpp b/core/libs/tests/GuestFreeTypeModuleProperties.cpp index 773154e387..dd1fc67cfc 100644 --- a/core/libs/tests/GuestFreeTypeModuleProperties.cpp +++ b/core/libs/tests/GuestFreeTypeModuleProperties.cpp @@ -173,6 +173,7 @@ int main(int argc, char** argv) { Require(patched || std::strcmp(argv[2], "unpatched") == 0); #ifdef _WIN32 auto module = LoadLibraryW(std::filesystem::path(argv[1]).c_str()); + if (!module) std::fprintf(stderr, "LoadLibraryW failed: %lu\n", GetLastError()); #else auto module = dlopen(argv[1], RTLD_NOW | RTLD_LOCAL); if (!module) std::fprintf(stderr, "%s\n", dlerror()); From a75706d40fbfef7a3df92a99214c6e66c47c7a5c Mon Sep 17 00:00:00 2001 From: orezraey Date: Fri, 23 Oct 2026 12:33:25 -0300 Subject: [PATCH 015/198] docs(libc): record fopen and freopen of directories as measured on a PS5 Co-Authored-By: Claude Opus 5.5 --- docs/dev/debt/libc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 2ec10f5024..77789ffc7a 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -102,7 +102,7 @@ - On macOS the [guest arena](../../../core/libs/prx/libc/src/GuestArena.cpp) finds host regions with `mach_vm_region` and reserves the rest with a fixed `mach_vm_map`, which fails instead of replacing, as macOS has no `/proc/self/maps` or `MAP_FIXED_NOREPLACE`. The kernel keeps the range up to 0x70_0000_0000 unmappable for every process (max protection none). It starts at 0x10_0000_0000 on macOS 26.5 (M1 Pro, arm64 and x86_64 under Rosetta) and at 0xF_C000_0000 on macOS 27 (27.0.1 on an M4 Pro, arm64 and x86_64 under Rosetta; 27.2 on an M2 Max, arm64). With dyld and the Rosetta runtime near 0x2_0200_0000 and 0x3_0400_0000, a guest mapping fixed inside those ranges fails where the console places it; mappings without a fixed address are placed around them. - [sceKernelSync](../../../core/libs/prx/libc/src/General.cpp) (libkernel) flushes only the host files and directories recorded through `RecordWrittenPath_nid_no_patch` (the libc, libkernel, save data and app content file functions) and their changed parent directories. A host write in a library that does not record its path is not flushed by `sceKernelSync` - [sceKernelSync](../../../core/libs/prx/libc/src/General.cpp) (libkernel) on Windows skips a recorded file it cannot open for writing (read-only, or held open by a handle that denies write sharing), since `FlushFileBuffers` needs a writable handle. On Linux, it skips a recorded file it cannot open for reading. Such a file's data may not reach the disk before a host crash; the guest reads the same data either way -- [`fopen` and `freopen`](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) on Windows: `freopen` onto a directory in a read mode fails with `EACCES`, where FreeBSD opens it. Reading a directory stream from `fopen` fails with `EISDIR` and sets the error indicator on every host, as FreeBSD 11 tmpfs does; FreeBSD 11 FFS returns the directory's raw blocks instead, and what the console's filesystems return is unverified +- [`fopen` and `freopen`](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) - in a payload process on a PS5 (firmware 13.60) a directory opens in a read mode (`r`, `rb`, `rw`) and `fread` returns its raw directory blocks (`/data`, `/system`) or fails with `EIO` and sets the error indicator (`/`); `freopen` onto a directory in a read mode succeeds too. Here reading a directory stream fails with `EISDIR` and sets the error indicator on every host, since the host's directory blocks are not the console's, and on Windows `freopen` onto a directory fails with `EACCES`. The other measured answers match: `EISDIR` for a directory in a write or update mode, `EEXIST` for `wx` on an existing file, `EINVAL` for an unknown or empty mode, and `ENOENT` for a missing file - [readdir and readdir_r](../../../core/libs/prx/libc/src/Directory.cpp) (libc) on Windows report, on a file system whose directory listing carries no file ID (the entry's ID is 0, or the volume refuses `FileIdBothDirectoryInfo`), a nonzero `d_fileno` hashed from the entry name and the directory's file index, which differs from `lstat`'s `st_ino`. Not reproduced: NTFS lists an ID for every entry - [Writes on an update stream](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) on Windows go through the UCRT, which drops a write that directly follows a read without `fflush`, `fseek`, `fsetpos` or `rewind` unless the read reached end of file: `fwrite` returns 0, and `fputc` and `fprintf` report success, without the error indicator. FreeBSD 11.4's [`__swsetup`](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libc/stdio/wsetup.c#L59-L74) discards the read buffer and writes at the descriptor's offset, past a read-ahead of `st_blksize` bytes for a regular file, so where the console writes depends on its block size. Reads directly after writes follow FreeBSD's `__srefill` - [`fopen` of /dev/random and /dev/urandom](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) in a read mode returns a stream over the host null device whose `fread` fills the buffer with host OS entropy, as the libkernel [random device](../../../core/libs/prx/libkernel/File/src/RandomDevice.cpp) descriptors do. Only the exact paths are recognised, the stream has no descriptor, `setvbuf`, `feof`, `ferror`, `clearerr`, `flockfile`, `ftrylockfile`, `funlockfile` and `fclose` work on it, `fflush` returns 0 as on a FreeBSD read-only stream, and every other stream function throws, as does opening the paths for writing. `freopen` onto these paths still fails with `ENOENT`. ProsperoTV (PPSA99003) reads its phone pairing code this way From 85caaefe6d4d371e3702c860baa95c1dec59f490 Mon Sep 17 00:00:00 2001 From: orezraey Date: Fri, 23 Oct 2026 12:39:56 -0300 Subject: [PATCH 016/198] test(libc): check the fopen modes measured on a PS5 Co-Authored-By: Claude Opus 5.5 --- core/libs/tests/GuestFilesystem.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index 86b6b6c3e8..39e7e076e2 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -650,6 +650,15 @@ int main() { { const auto listed = root / "listed.txt"; std::ofstream(listed, std::ios::binary) << "listed"; + Require(fopen_nid_postfix(listed.string().c_str(), "wx") == nullptr && *__error_nid_postfix() == 17); + for (const char* invalidMode : {"z", "", "q"}) { + *__error_nid_postfix() = 0; + Require(fopen_nid_postfix(listed.string().c_str(), invalidMode) == nullptr && *__error_nid_postfix() == 22); + } + auto* readWrite = fopen_nid_postfix(listed.string().c_str(), "rw"); + char word[8] = {}; + Require(readWrite != nullptr && fread_nid_postfix(word, 1, 6, readWrite) == 6 && std::string(word) == "listed"); + Require(fclose_nid_postfix(readWrite) == 0); for (const char* openMode : {"r", "rb", "re"}) { auto* directoryStream = fopen_nid_postfix(root.string().c_str(), openMode); Require(directoryStream != nullptr); From dce51c1f02fe79bd54e4849758fd8865399d60e5 Mon Sep 17 00:00:00 2001 From: roboterspinne <229981445+roboterspinne@users.noreply.github.com> Date: Fri, 23 Oct 2026 20:26:40 +0000 Subject: [PATCH 017/198] fix(relinker): print the unsupported relocation offset once ValidationPolicy wrote the entry's file offset into the "Unsupported relocation type" message and also passed it as the failure offset, which the command line appends to every error, so the line read "Unsupported relocation type 0x9 at offset 0x4700 (offset 0x4700)". The message now names only the type, like the other errors of the relocation loop. --- core/relinker/relinker/src/analysis/ValidationPolicy.cpp | 3 +-- core/relinker/relinker/tests/ConversionTests.cpp | 2 +- .../relinker/relinker/tests/test_executable_tls_relocations.py | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/core/relinker/relinker/src/analysis/ValidationPolicy.cpp b/core/relinker/relinker/src/analysis/ValidationPolicy.cpp index 47d65ac011..692e673ffe 100644 --- a/core/relinker/relinker/src/analysis/ValidationPolicy.cpp +++ b/core/relinker/relinker/src/analysis/ValidationPolicy.cpp @@ -28,8 +28,7 @@ void ValidationPolicy::ValidateRelocationTypeSupported(const std::uint32_t reloc } if (_supportedRelocationTypes.find(relocationTypeValue) == _supportedRelocationTypes.end()) { std::ostringstream msg; - msg << "Unsupported relocation type 0x" << std::hex << relocationTypeValue - << " at offset 0x" << fileByteOffset; + msg << "Unsupported relocation type 0x" << std::hex << relocationTypeValue; throw RelinkerException(msg.str(), fileByteOffset); } } diff --git a/core/relinker/relinker/tests/ConversionTests.cpp b/core/relinker/relinker/tests/ConversionTests.cpp index 175199158a..8c528b2dc6 100644 --- a/core/relinker/relinker/tests/ConversionTests.cpp +++ b/core/relinker/relinker/tests/ConversionTests.cpp @@ -313,7 +313,7 @@ void relocationValidation() { converter.Convert(source, {}); throw std::runtime_error("Unsupported relocation type was accepted"); } catch (const Domain::RelinkerException& error) { - require(std::string(error.what()).find("Unsupported relocation type 0x99") != std::string::npos, "Wrong error for unsupported relocation"); + require(std::string(error.what()) == "Unsupported relocation type 0x99", "Wrong error for unsupported relocation"); require(error.FailureOffset == 0x2700, "Wrong failure offset for unsupported relocation"); } } diff --git a/core/relinker/relinker/tests/test_executable_tls_relocations.py b/core/relinker/relinker/tests/test_executable_tls_relocations.py index 8f9db5e449..0f3b37ab5d 100644 --- a/core/relinker/relinker/tests/test_executable_tls_relocations.py +++ b/core/relinker/relinker/tests/test_executable_tls_relocations.py @@ -150,7 +150,7 @@ def main(): ("windows-import", [(0x1000, TPOFF64, IMPORTED, 0)], True, ("--windows",), "Imported TLS relocation"), ("macos-dtpmod", [(0x1000, DTPMOD64, NULL, 0)], True, ("--macos",), "DTPMOD64 relocation is not supported"), ("macos-import", [(0x1000, TPOFF64, IMPORTED, 0)], True, ("--macos",), "Imported TLS relocation"), - ] + [(f"{target}-{name}", [(0x1000, kind, NULL, 0)], True, flags, f"Unsupported relocation type {kind:#x}") + ] + [(f"{target}-{name}", [(0x1000, kind, NULL, 0)], True, flags, f"Unsupported relocation type {kind:#x} (offset 0x4700)") for kind, name in LINK_TIME.items() for flags, target in TARGETS.items()] for name, relocations, has_tls, flags, error in failures: result, output = relink(relinker, directory, name, image(relocations, has_tls), *flags) From a29bf781c86270d38c82e0b8386ad898a21ab0ef Mon Sep 17 00:00:00 2001 From: JJenkx Date: Thu, 22 Oct 2026 13:54:58 -0400 Subject: [PATCH 018/198] fix(libc): compute powf like the libc.prx that titles ship powf returned the host pow, except for base 1, so its last bit and errno depended on the host: on Windows the bits differ from titles' libc.prx on 3,171,678 of 100,001,089 sampled argument pairs. That libc.prx is the older form of FreeBSD msun's e_powf.c: a NaN argument returns x + y, so powf(1, NaN) is NaN rather than 1, and (+-1)**+-inf is NaN. It sets EDOM for a negative base with a non-integer exponent and ERANGE only in its final overflow and underflow returns. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libc/src/Math.cpp | 159 +++++++++++++++++++++++++++++++- core/libs/tests/GuestMath.cpp | 23 ++++- docs/dev/debt/libc.md | 1 + 3 files changed, 179 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index 55ffd82dd0..a703959f58 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -610,9 +610,162 @@ float APS5_VABI atanf_nid_postfix(float x) { return hx < 0 ? -result : result; } double APS5_VABI atan2_nid_postfix(double y, double x) { return std::atan2(y, x); } -float APS5_VABI powf_nid_postfix(float base, float exp) { - if (base == 1.0f) return 1.0f; - return std::pow(base, exp); +static inline std::int32_t PowfWord(float f) { return std::bit_cast(f); } +static inline float PowfFloat(std::int32_t w) { return std::bit_cast(w); } + +float APS5_VABI powf_nid_postfix(float x, float y) { + static const float bp[] = {1.0f, 1.5f}, dp_h[] = {0.0f, static_cast(5.84960938e-01)}, dp_l[] = {0.0f, static_cast(1.56322085e-06)}; + const float zero = 0.0f, half = 0.5f, qrtr = 0.25f, thrd = static_cast(3.33333343e-01), one = 1.0f, two = 2.0f, two24 = 16777216.0f; + const float huge = 1.0e30f, tiny = 1.0e-30f; + const float L1 = static_cast(6.0000002384e-01), L2 = static_cast(4.2857143283e-01), L3 = static_cast(3.3333334327e-01); + const float L4 = static_cast(2.7272811532e-01), L5 = static_cast(2.3066075146e-01), L6 = static_cast(2.0697501302e-01); + const float P1 = static_cast(1.6666667163e-01), P2 = static_cast(-2.7777778450e-03), P3 = static_cast(6.6137559770e-05); + const float P4 = static_cast(-1.6533901999e-06), P5 = static_cast(4.1381369442e-08); + const float lg2 = static_cast(6.9314718246e-01), lg2_h = static_cast(6.93145752e-01), lg2_l = static_cast(1.42860654e-06); + const float ovt = static_cast(4.2995665694e-08), cp = static_cast(9.6179670095e-01), cp_h = static_cast(9.6191406250e-01); + const float cp_l = static_cast(-1.1736857402e-04), ivln2 = static_cast(1.4426950216e+00), ivln2_h = static_cast(1.4426879883e+00); + const float ivln2_l = static_cast(7.0526075433e-06); + float z, ax, z_h, z_l, p_h, p_l, y1, t1, t2, r, s, sn, t, u, v, w; + std::int32_t i, j, k, yisint, n, is; + const std::int32_t hx = PowfWord(x), hy = PowfWord(y); + std::int32_t ix = hx & 0x7fffffff; + const std::int32_t iy = hy & 0x7fffffff; + if (iy == 0) return one; + if (ix > 0x7f800000 || iy > 0x7f800000) return x + y; + yisint = 0; + if (hx < 0) { + if (iy >= 0x4b800000) yisint = 2; + else if (iy >= 0x3f800000) { + k = (iy >> 23) - 0x7f; + j = iy >> (23 - k); + if ((j << (23 - k)) == iy) yisint = 2 - (j & 1); + } + } + if (iy == 0x7f800000) { + if (ix == 0x3f800000) return y - y; + else if (ix > 0x3f800000) return hy >= 0 ? y : zero; + else return hy < 0 ? -y : zero; + } + if (iy == 0x3f800000) return hy < 0 ? one / x : x; + if (hy == 0x40000000) return x * x; + if (hy == 0x3f000000) { + if (hx >= 0) return std::sqrt(x); + } + ax = std::fabs(x); + if (ix == 0x7f800000 || ix == 0 || ix == 0x3f800000) { + z = ax; + if (hy < 0) z = one / z; + if (hx < 0) { + if (((ix - 0x3f800000) | yisint) == 0) { + errno = EDOM; + z = (z - z) / (z - z); + } + else if (yisint == 1) z = -z; + } + return z; + } + n = static_cast((static_cast(hx) >> 31) - 1); + if ((n | yisint) == 0) { + errno = EDOM; + return (x - x) / (x - x); + } + sn = one; + if ((n | (yisint - 1)) == 0) sn = -one; + if (iy > 0x4d000000) { + if (ix < 0x3f7ffff8) return hy < 0 ? sn * huge * huge : sn * tiny * tiny; + if (ix > 0x3f800007) return hy > 0 ? sn * huge * huge : sn * tiny * tiny; + t = ax - 1; + w = (t * t) * (half - t * (thrd - t * qrtr)); + u = ivln2_h * t; + v = t * ivln2_l - w * ivln2; + t1 = u + v; + is = PowfWord(t1); + t1 = PowfFloat(is & static_cast(0xfffff000)); + t2 = v - (t1 - u); + } else { + float s2, s_h, s_l, t_h, t_l; + n = 0; + if (ix < 0x00800000) { ax *= two24; n -= 24; ix = PowfWord(ax); } + n += (ix >> 23) - 0x7f; + j = ix & 0x007fffff; + ix = j | 0x3f800000; + if (j <= 0x1cc471) k = 0; + else if (j < 0x5db3d7) k = 1; + else { k = 0; n += 1; ix -= 0x00800000; } + ax = PowfFloat(ix); + u = ax - bp[k]; + v = one / (ax + bp[k]); + s = u * v; + s_h = s; + is = PowfWord(s_h); + s_h = PowfFloat(is & static_cast(0xfffff000)); + is = ((ix >> 1) & static_cast(0xfffff000)) | 0x20000000; + t_h = PowfFloat(is + 0x00400000 + (k << 21)); + t_l = ax - (t_h - bp[k]); + s_l = v * ((u - s_h * t_h) - s_h * t_l); + s2 = s * s; + r = s2 * s2 * (L1 + s2 * (L2 + s2 * (L3 + s2 * (L4 + s2 * (L5 + s2 * L6))))); + r += s_l * (s_h + s); + s2 = s_h * s_h; + t_h = 3 + s2 + r; + is = PowfWord(t_h); + t_h = PowfFloat(is & static_cast(0xfffff000)); + t_l = r - ((t_h - 3) - s2); + u = s_h * t_h; + v = s_l * t_h + t_l * s; + p_h = u + v; + is = PowfWord(p_h); + p_h = PowfFloat(is & static_cast(0xfffff000)); + p_l = v - (p_h - u); + z_h = cp_h * p_h; + z_l = cp_l * p_h + p_l * cp + dp_l[k]; + t = static_cast(n); + t1 = (((z_h + z_l) + dp_h[k]) + t); + is = PowfWord(t1); + t1 = PowfFloat(is & static_cast(0xfffff000)); + t2 = z_l - (((t1 - t) - dp_h[k]) - z_h); + } + is = PowfWord(y); + y1 = PowfFloat(is & static_cast(0xfffff000)); + p_l = (y - y1) * t1 + y * t2; + p_h = y1 * t1; + z = p_l + p_h; + j = PowfWord(z); + if (j > 0x43000000 || (j == 0x43000000 && p_l + ovt > z - p_h)) { + errno = ERANGE; + return sn * huge * huge; + } + if ((j & 0x7fffffff) > 0x43160000 || (static_cast(j) == 0xc3160000u && p_l <= z - p_h)) { + errno = ERANGE; + return sn * tiny * tiny; + } + i = j & 0x7fffffff; + k = (i >> 23) - 0x7f; + n = 0; + if (i > 0x3f000000) { + n = j + (0x00800000 >> (k + 1)); + k = ((n & 0x7fffffff) >> 23) - 0x7f; + t = PowfFloat(n & ~(0x007fffff >> k)); + n = ((n & 0x007fffff) | 0x00800000) >> (23 - k); + if (j < 0) n = -n; + p_h -= t; + } + t = p_l + p_h; + is = PowfWord(t); + t = PowfFloat(is & static_cast(0xffff8000)); + u = t * lg2_h; + v = (p_l - (t - p_h)) * lg2 + t * lg2_l; + z = u + v; + w = v - (z - u); + t = z * z; + t1 = z - t * (P1 + t * (P2 + t * (P3 + t * (P4 + t * P5)))); + r = (z * t1) / (t1 - two) - (w + z * w); + z = one - (r - z); + j = PowfWord(z); + j += static_cast(static_cast(n) << 23); + if ((j >> 23) <= 0) z = std::scalbn(z, n); + else z = PowfFloat(j); + return sn * z; } double APS5_VABI pow_nid_postfix(double base, double exp) { return std::pow(base, exp); } float APS5_VABI __powisf2_nid_postfix(float base, int exponent) { diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index 4014e36484..26326a5a94 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -565,7 +565,28 @@ static void CheckExp2Errno() { } static void CheckPowfSpecialValue() { - Require(std::bit_cast(powf_nid_postfix(1.0f, std::bit_cast(0x7f800001u))) == 0x3f800000u); + constexpr struct { + std::uint32_t x; + std::uint32_t y; + std::uint32_t expected; + int error; + } cases[] = { + {0x3f9e2cf9u, 0xbf5a98dcu, 0x3f55ab48u, 0}, {0x3fad4946u, 0x40c1ad7au, 0x40c82840u, 0}, {0x3f1467ddu, 0x40b9b6f1u, 0x3d2d08eeu, 0}, + {0x3fda2303u, 0x3f002254u, 0x3fa724dau, 0}, {0x3ff3e78du, 0xc1db422eu, 0x32b5d534u, 0}, {0x3f8d3e01u, 0x3f9f7952u, 0x3f90b3f4u, 0}, + {0x3f800000u, 0x7f800001u, 0x7fc00001u, 0}, {0x3f800000u, 0x7fc00000u, 0x7fc00000u, 0}, {0x3f800000u, 0x7f800000u, 0xffc00000u, 0}, + {0xbf800000u, 0xff800000u, 0xffc00000u, 0}, {0x3f800000u, 0x40000000u, 0x3f800000u, 0}, {0x40000000u, 0x00000000u, 0x3f800000u, 0}, + {0x7fc00001u, 0x00000000u, 0x3f800000u, 0}, {0x40000000u, 0x3f000000u, 0x3fb504f3u, 0}, {0x40800000u, 0x3f000000u, 0x40000000u, 0}, + {0xc0000000u, 0x40400000u, 0xc1000000u, 0}, {0xc0000000u, 0x3fc00000u, 0xffc00000u, EDOM}, {0xbf800000u, 0x3fc00000u, 0xffc00000u, EDOM}, + {0x00000000u, 0xc0000000u, 0x7f800000u, 0}, {0x80000000u, 0xc0400000u, 0xff800000u, 0}, {0x42c80000u, 0x42c80000u, 0x7f800000u, ERANGE}, + {0x3c23d70au, 0x42c80000u, 0x00000000u, ERANGE}, {0x40000000u, 0x43000000u, 0x7f800000u, ERANGE}, {0x40000000u, 0xc3160000u, 0x00000000u, ERANGE}, + {0x40000000u, 0xc3150000u, 0x00000001u, 0}, {0x40000000u, 0x4e000000u, 0x7f800000u, 0}, {0x3f000000u, 0x4e000000u, 0x00000000u, 0}, + {0xc0000000u, 0x4f000000u, 0x7f800000u, 0}, {0x7f800000u, 0xbf800000u, 0x00000000u, 0}, {0xff800000u, 0x40400000u, 0xff800000u, 0}, + }; + for (const auto& item : cases) { + errno = 1234; + Require(std::bit_cast(powf_nid_postfix(std::bit_cast(item.x), std::bit_cast(item.y))) == item.expected); + Require(errno == (item.error != 0 ? item.error : 1234)); + } } struct RestoreFloatingPointState { diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index a7e892c6a0..9b26cbb718 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -18,6 +18,7 @@ - [fegetround, fesetround, fetestexcept, feclearexcept, fegetexceptflag and fesetexceptflag](../../../core/libs/prx/libc/src/FloatingPoint.cpp) (libSceLibcInternal, implemented in libc) follow FreeBSD 11.4 amd64 [fenv.h](https://github.com/freebsd/freebsd-src/blob/4554bee35405b36ea19e9e60d6abe6ea534e36e0/lib/msun/x86/fenv.h): rounding modes use x87 control bits 10-11, exception flags include denormals (`0x3f`), and `fexcept_t` is 16 bits. They access the current host thread's x87 and MXCSR state. Under Rosetta 2 the six MXCSR exception masks always read back set, so an SSE exception the guest unmasks stays masked; the x87 masks and the other MXCSR bits are kept. This console ABI is assumed, as for `fegetenv`, not measured on hardware. Null flag pointers throw instead of faulting. - [feenableexcept, fedisableexcept, __feenableexcept, __fedisableexcept, fegettrapenable and fesettrapenable](../../../core/libs/prx/libc/src/FloatingPoint.cpp) (libc) change only the x87 masks under Rosetta 2: MXCSR keeps its six exception masks set there, so no SSE trap is enabled, and the enable and disable results, which report the traps enabled on both units, report none - [fegetenv](../../../core/libs/prx/libc/src/Math.cpp) (libSceLibcInternal, implemented in libc, NID `psx0YiAKm7k`) follows FreeBSD 11 amd64 [fenv.c](https://github.com/freebsd/freebsd-src/blob/4554bee35405b36ea19e9e60d6abe6ea534e36e0/lib/msun/amd64/fenv.c) with the 32-byte `fenv_t` of [x86/fenv.h](https://github.com/freebsd/freebsd-src/blob/4554bee35405b36ea19e9e60d6abe6ea534e36e0/lib/msun/x86/fenv.h): `fnstenv` fills the 28-byte x87 part, MXCSR follows at 0x1c, and the x87 control word is reloaded because `fnstenv` masks every x87 exception. That the console's `libSceLibcInternal` has the same code and layout is assumed, not read from its binary. A null pointer throws instead of faulting +- [powf](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the `powf` of the `libc.prx` that titles ship, which is FreeBSD msun's [`e_powf.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/e_powf.c) in its older form: a NaN argument returns `x + y` (so `powf(1, NaN)` is NaN, not 1), and `(±1)**±inf` returns `y - y`, a NaN. `errno` is set to `EDOM` in the two `(x - x) / (x - x)` branches (a negative finite `x` with a non-integer `y`, including -1), and to `ERANGE` only in the final overflow and underflow returns; the `|y| > 2^27` shortcut and the zero and infinity cases leave it unchanged. Floating-point exceptions are not compared. On 100,001,089 sampled argument pairs it gives the same bits as the copies bundled with PPSA07923 and PPSA05916, with the same `errno`; the FMA-contracted copy of PPSA13934 differs by 1 ulp on 26,405 of them. Pairs outside the sample are unverified. - [Guest arena](../../../core/libs/prx/libc/src/GuestArena.cpp) (libc) - the application map area is assumed to end at 0xFC_0000_0000, as `SCE_KERNEL_APP_MAP_AREA_END_ADDR` does on the PS4. On Windows, host allocations made in the arena before libc loads stay, and a fixed guest mapping over one throws. The console's low-address layout is not measured. Linux and Windows reserve free addresses from 64 KiB, except host mappings and low chunks the Linux address policy refuses; null hints still allocate from 8 GiB. macOS and ASan builds still start the arena at 8 GiB and cannot supply low 32-bit addresses. Guest thread creation ignores a stack address set in its attributes. A host-loaded executable is not moved, so a PIE above the arena gets no memory within 2 GiB of its code ([#5217](https://github.com/boykopovar/AnyPS5/issues/5217)) - [sceLibcInternalBacktraceForGame](../../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - `(const char* heapName)` and its return follow the `libc.prx` ASTRO BOT (PPSA21564 01.007.000) bundles: its four calls (0x6801, 0x7F46, 0xE75E, 0xEE02) are in the `malloc_stats` and `sceLibcMspaceMallocStats` paths, each after a `[%s] ...` line printed with the same heap name buffer, and the caller ignores the result and continues. The console's backtrace output is not reproduced: the heap name and the guest return address are written to the host standard error - [sceLibcInternalHeapErrorReportForGame](../../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - `(void* heap, void* block, uint32 error)` follows the same `libc.prx` (call at 0xF634): it passes the heap, the block and the nonzero result of its block check, and then calls a function that never returns, so the report only has to return. The meaning of the error values is unknown; the three values and the guest return address are written to the host standard error From 4fe55c1fbc487924ccd17ad1fd3a3fcb3906833a Mon Sep 17 00:00:00 2001 From: JJenkx Date: Thu, 22 Oct 2026 14:27:32 -0400 Subject: [PATCH 019/198] test(libc): cover powf NaN pairs and pin the msun 9.0.0 revision Co-Authored-By: Claude Opus 5.5 --- core/libs/tests/GuestMath.cpp | 1 + docs/dev/debt/libc.md | 1 + 2 files changed, 2 insertions(+) diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index 26326a5a94..02b0e2d90c 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -581,6 +581,7 @@ static void CheckPowfSpecialValue() { {0x3c23d70au, 0x42c80000u, 0x00000000u, ERANGE}, {0x40000000u, 0x43000000u, 0x7f800000u, ERANGE}, {0x40000000u, 0xc3160000u, 0x00000000u, ERANGE}, {0x40000000u, 0xc3150000u, 0x00000001u, 0}, {0x40000000u, 0x4e000000u, 0x7f800000u, 0}, {0x3f000000u, 0x4e000000u, 0x00000000u, 0}, {0xc0000000u, 0x4f000000u, 0x7f800000u, 0}, {0x7f800000u, 0xbf800000u, 0x00000000u, 0}, {0xff800000u, 0x40400000u, 0xff800000u, 0}, + {0x7fc00001u, 0xffc00002u, 0x7fc00001u, 0}, {0xffc00002u, 0x7fc00001u, 0xffc00002u, 0}, {0x7f800001u, 0x7fc00003u, 0x7fc00001u, 0}, }; for (const auto& item : cases) { errno = 1234; diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 9b26cbb718..02a768939c 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -96,6 +96,7 @@ - [seed48 and lcong48](../../../core/libs/prx/libc/src/Random.cpp) (libSceLibcInternal, implemented in libc) follow the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x6F460 and 0x6F420): the multiplier and addend are shared by the whole `rand48` family, including `erand48`, `nrand48` and `jrand48` on a caller's seed; `lcong48` sets them with the seed, and `srand48` and `seed48` restore 0x5DEECE66D and 0xB. Unlike FreeBSD, `seed48` returns a static copy of the new seed rather than the previous one. A null argument, which the console dereferences, throws - [lgamma and signgam](../../../core/libs/prx/libc/src/Math.cpp) (libc) - `lgamma` returns the host's `lgamma` and stores the sign of Γ(x) in the exported `signgam` (NID `5gOOC0kzW0c`, starting at 0 as in FreeBSD 11's [`s_signgam.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/msun/src/s_signgam.c)) by the rules of [`e_lgamma_r.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/msun/src/e_lgamma_r.c): -1 for -0 and for a negative non-integer whose floor is odd, 1 otherwise, including poles, infinities and NaN. Taken from the FreeBSD source, not measured on the console. The NID is in the aerolib symbol list; no title is known to read `signgam`. PPSA99004 imports `lgamma` - [coshf](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows FreeBSD msun's [`e_coshf.c`](https://github.com/freebsd/freebsd-src/blob/5aa554c7e5b6fd9042986df07f12ac5687e41392/lib/msun/src/e_coshf.c), using the `expm1f` and `expf` paths already inlined for `sinhf` below; the Windows UCRT differed from the `libc.prx` copies in PPSA07923 and PPSA13934 on 500,554 and 500,614 of 20 million inputs in the #9707 survey. The exact system `libSceLibcInternal` behavior and the FMA-contracted `libc.prx` results remain unverified +- [powf](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the `powf` of the `libc.prx` that titles ship: FreeBSD msun's [`e_powf.c`](https://github.com/freebsd/freebsd-src/blob/release/9.0.0/lib/msun/src/e_powf.c) as of 9.0.0, which has 11.4's arithmetic but no `1**y` shortcut, so a NaN argument returns its NaN (`powf(1, NaN)` is NaN, not 1), and `(±1)**±inf` returns `y - y`, a NaN. With two NaN arguments the first one's NaN is returned, quieted. `errno` is set to `EDOM` in the two `(x - x) / (x - x)` branches (negative finite `x`, non-integer `y`), and to `ERANGE` only in the final overflow and underflow returns; the `|y| > 2^27` shortcut and the zero and infinity cases leave it unchanged. Floating-point exceptions are not compared. On 100M sampled argument pairs it gives the same bits as the copies bundled with PPSA07923 and PPSA05916, with the same `errno`; the FMA-contracted copy of PPSA13934 differs by 1 ulp on 26,405 of them. Pairs outside the sample are unverified. - [sinhf](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the `sinhf` of the `libc.prx` bundled with PPSA28319 (disassembly): FreeBSD msun's [`e_sinhf.c`](https://github.com/freebsd/freebsd-src/blob/5aa554c7e5b6fd9042986df07f12ac5687e41392/lib/msun/src/e_sinhf.c) from before `__ldexp_expf`, which squares `expf(|x| / 2)` between `logf(FLT_MAX)` and the overflow threshold, calling msun's [`expm1f`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/s_expm1f.c) and [`expf`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/e_expf.c), whose paths for the arguments it passes are inlined here. Past the threshold Sony's error helper sets `errno` to `ERANGE` before returning `x * 1e37`; msun itself sets no `errno`. That code, run on a Windows host, gives the same bits as this implementation for every finite argument up to the threshold, of both signs; it was not run on Linux. That the system `libSceLibcInternal`, which Ruffle (PPSA68091) and RetroArch (PPSA99169) import while bundling a zero-return `libc.prx`, has the same code is assumed - [acoshl, asinhl, atanhl, atan2l, cbrtl, erfl, erfcl, expm1l, hypotl, log1pl, log2l, lgammal, tgammal, j0f, j1f, jnf, y0f, y1f and ynf](../../../core/libs/prx/libc/src/Math.cpp) (libc) return the host C library's results (the float Bessel functions through its double ones), not those of the console's own code, so the last bits can differ (`y0f`, `y1f`, `ynf` follow the console at `+-0`). As in the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x56410), `lgammal` does not store the sign in `signgam`; `hypotl` returns infinity for an infinite argument even when the other is a NaN, as C11 F.10.4.3 requires. [hypot3, hypot3f and hypot3l](../../../core/libs/prx/libc/src/Math.cpp) follow that module (0x51D30, 0x520D0, 0x52420): the first NaN of x, y and z is returned unchanged, any infinity gives positive infinity, a zero argument gives the two-argument hypot of the other two, and otherwise the largest magnitude is combined with the hypot of the other two - [asinl](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the `asinl` of the `libc.prx` bundled with PPSA28319 (disassembly), FreeBSD msun's [`e_asinl.c`](https://github.com/freebsd/freebsd-src/blob/9f0432041b8fb69832f10c32da50a67f34feeb61/lib/msun/src/e_asinl.c) with the ld80 [`invtrig.c`](https://github.com/freebsd/freebsd-src/blob/9f0432041b8fb69832f10c32da50a67f34feeb61/lib/msun/ld80/invtrig.c) coefficients, which the binary holds as the same 80-bit constants. For |x| > 1, the infinities and NaN, Sony's error helper raises `FE_INVALID` and sets `errno` to `EDOM` before returning `(x - x) / (x - x)`, so a NaN argument also sets `EDOM`; msun itself sets no `errno`. 20 million random arguments in (-1, 1) give the same bits as that code run on a Windows host. The result needs the x87 precision control at 64 bits, as FreeBSD amd64 starts threads (control word `0x37f`), but guest threads do not set it: on Windows a thread started by `_beginthreadex` runs at 53 bits (`0x27f`), where `asinl(0.75)` returns mantissa `0xd91a98ae3406e000` instead of `0xd91a98ae3406e041`. The system `libSceLibcInternal` that RetroArch (PPSA99169) imports is assumed to have the same code From c55a8343551f6d43471040dd9cbe2d45f27718e4 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Fri, 23 Oct 2026 01:23:14 -0400 Subject: [PATCH 020/198] fix(libc): select powf NaN payloads explicitly --- core/libs/prx/libc/src/Math.cpp | 6 +++++- core/libs/tests/GuestMath.cpp | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index a703959f58..b63ba0f891 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -631,7 +631,11 @@ float APS5_VABI powf_nid_postfix(float x, float y) { std::int32_t ix = hx & 0x7fffffff; const std::int32_t iy = hy & 0x7fffffff; if (iy == 0) return one; - if (ix > 0x7f800000 || iy > 0x7f800000) return x + y; + if (ix > 0x7f800000 || iy > 0x7f800000) { + if ((ix > 0x7f800000 && (ix & 0x00400000) == 0) || + (iy > 0x7f800000 && (iy & 0x00400000) == 0)) std::feraiseexcept(FE_INVALID); + return PowfFloat((ix > 0x7f800000 ? hx : hy) | 0x00400000); + } yisint = 0; if (hx < 0) { if (iy >= 0x4b800000) yisint = 2; diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index 02b0e2d90c..a5fc6ff608 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -582,6 +582,10 @@ static void CheckPowfSpecialValue() { {0x40000000u, 0xc3150000u, 0x00000001u, 0}, {0x40000000u, 0x4e000000u, 0x7f800000u, 0}, {0x3f000000u, 0x4e000000u, 0x00000000u, 0}, {0xc0000000u, 0x4f000000u, 0x7f800000u, 0}, {0x7f800000u, 0xbf800000u, 0x00000000u, 0}, {0xff800000u, 0x40400000u, 0xff800000u, 0}, {0x7fc00001u, 0xffc00002u, 0x7fc00001u, 0}, {0xffc00002u, 0x7fc00001u, 0xffc00002u, 0}, {0x7f800001u, 0x7fc00003u, 0x7fc00001u, 0}, + {0x7fc00003u, 0x7f800001u, 0x7fc00003u, 0}, {0xff800002u, 0x7fc00003u, 0xffc00002u, 0}, {0x7fc00003u, 0xff800002u, 0x7fc00003u, 0}, + {0x7f800001u, 0xff800002u, 0x7fc00001u, 0}, {0xff800002u, 0x7f800001u, 0xffc00002u, 0}, {0x7fffffffu, 0xffffffffu, 0x7fffffffu, 0}, + {0x7f800001u, 0x00000000u, 0x3f800000u, 0}, {0xff800002u, 0x80000000u, 0x3f800000u, 0}, {0xff800002u, 0x7f800000u, 0xffc00002u, 0}, + {0xff800000u, 0xff800002u, 0xffc00002u, 0}, }; for (const auto& item : cases) { errno = 1234; From d590a3ef6fe39fb0e497154a1c297fdf3ebd0c43 Mon Sep 17 00:00:00 2001 From: Itamar Medyoni Date: Sat, 24 Oct 2026 12:30:13 +0300 Subject: [PATCH 021/198] fix(libkernel): round an unaligned munmap address down like FreeBSD munmap failed with EINVAL for an address that is not page aligned. FreeBSD 11's sys_munmap rejects only a zero length and an address wrap; it moves the address down to its page and grows the length by the same offset before rounding it up, so the pages the range touches are unmapped. --- core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp | 7 ++++--- core/libs/tests/GuestMemory.cpp | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp b/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp index 89c8d28df4..b305a8d941 100644 --- a/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp @@ -257,9 +257,10 @@ int APS5_VABI munmap_nid_postfix(void* address, std::size_t length) noexcept { return -1; }; std::size_t rounded; - const auto start = reinterpret_cast(address); - if (!RoundLength(length, rounded) || start == 0 || - (start & (PS5_PAGE_SIZE - 1)) != 0 || + const auto offset = reinterpret_cast(address) & (PS5_PAGE_SIZE - 1); + const auto start = reinterpret_cast(address) - offset; + if (length == 0 || length > std::numeric_limits::max() - offset || + !RoundLength(length + offset, rounded) || start == 0 || rounded > std::numeric_limits::max() - start) return failed(GuestInvalid); try { diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index 66f6a56934..78735d6481 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -3526,10 +3526,10 @@ int main(int argc, char** argv) { mincoreVec[0] = 0x55; Require(mincore_nid_postfix(mincoreReserved, page, mincoreVec) == 0 && mincoreVec[0] == 0); Require(sceKernelMunmap(mincoreReserved, page) == 0); - Require(munmap_nid_postfix(memory + 1, page) == -1 && *__error_nid_postfix() == 22); + Require(munmap_nid_postfix(memory + 1, 0) == -1 && *__error_nid_postfix() == 22); Require(munmap_nid_postfix(memory, 0) == -1 && *__error_nid_postfix() == 22); Require(memory[0] == 42); - Require(munmap_nid_postfix(memory + page, 1) == 0); // round to one guest page + Require(munmap_nid_postfix(memory + page + 1, 1) == 0); Require(memory[0] == 42 && memory[page * 2] == 73); Require(mincore_nid_postfix(memory, page, mincoreVec) == 0 && mincoreVec[0] == 1); Require(mincore_nid_postfix(memory, page * 3, mincoreVec) == -1 && *__error_nid_postfix() == 12); From 1d80c938770e541b789b0747035c4d17f05f78e6 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sat, 24 Oct 2026 11:16:14 +0000 Subject: [PATCH 022/198] feat(libSceFreeType): implement FT_Bitmap_Init --- core/libs/prx/libSceFreeType/src/Bitmap.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/libs/prx/libSceFreeType/src/Bitmap.cpp b/core/libs/prx/libSceFreeType/src/Bitmap.cpp index 2e8a5d750e..c0392d1e97 100644 --- a/core/libs/prx/libSceFreeType/src/Bitmap.cpp +++ b/core/libs/prx/libSceFreeType/src/Bitmap.cpp @@ -58,6 +58,10 @@ void APS5_VABI FT_Bitmap_New_nid_postfix(GuestBitmap* bitmap) { if (bitmap) SyncBitmap(*bitmap, {}); } +void APS5_VABI FT_Bitmap_Init_nid_postfix(GuestBitmap* bitmap) { + FT_Bitmap_New_nid_postfix(bitmap); +} + int APS5_VABI FT_Bitmap_Copy_nid_postfix(GuestLibrary* library, const GuestBitmap* source, GuestBitmap* target) { if (!library) return FT_Err_Invalid_Library_Handle; if (!source || !target) return FT_Err_Invalid_Argument; From fea1063cdc43d81ba45053c1c5b33db6c8fdfc13 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sat, 24 Oct 2026 11:16:33 +0000 Subject: [PATCH 023/198] test(libSceFreeType): cover FT_Bitmap_Init against native FreeType --- core/libs/prx/libSceFreeType/CMakeLists.txt | 5 ++ core/libs/tests/GuestFreeTypeBitmapInit.cpp | 51 +++++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 core/libs/tests/GuestFreeTypeBitmapInit.cpp diff --git a/core/libs/prx/libSceFreeType/CMakeLists.txt b/core/libs/prx/libSceFreeType/CMakeLists.txt index 4522e90974..1f423ae443 100644 --- a/core/libs/prx/libSceFreeType/CMakeLists.txt +++ b/core/libs/prx/libSceFreeType/CMakeLists.txt @@ -65,6 +65,11 @@ if(BUILD_TESTING) target_link_libraries(guest_freetype_face_info_tests PRIVATE libSceFreeType freetype libc) configure_windows_unwind(guest_freetype_face_info_tests) add_test(NAME guest_freetype_face_info COMMAND guest_freetype_face_info_tests) + add_test_executable(guest_freetype_bitmap_init_tests ${LIBS_INCLUDE_DIR}/tests/GuestFreeTypeBitmapInit.cpp) + target_include_directories(guest_freetype_bitmap_init_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_freetype_bitmap_init_tests PRIVATE libSceFreeType freetype libc) + configure_windows_unwind(guest_freetype_bitmap_init_tests) + add_test(NAME guest_freetype_bitmap_init COMMAND guest_freetype_bitmap_init_tests) add_test_executable(guest_freetype_charmap_tests ${LIBS_INCLUDE_DIR}/tests/GuestFreeTypeCharmap.cpp) target_include_directories(guest_freetype_charmap_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_freetype_charmap_tests PRIVATE libSceFreeType freetype libc) diff --git a/core/libs/tests/GuestFreeTypeBitmapInit.cpp b/core/libs/tests/GuestFreeTypeBitmapInit.cpp new file mode 100644 index 0000000000..0671beafd3 --- /dev/null +++ b/core/libs/tests/GuestFreeTypeBitmapInit.cpp @@ -0,0 +1,51 @@ +#include "FontFixture.hpp" +#include "prx/libSceFreeType/include/GuestObjects.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" + +#include FT_BITMAP_H + +#include +#include +#include +#include + +using namespace FreeType; + +extern "C" { +void APS5_VABI FT_Bitmap_Init_nid_postfix(GuestBitmap*); +} + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "FreeType bitmap init check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +int main() { + GuestBitmap guest{7, 8, -9, reinterpret_cast(&guest), 10, 11, 12, &guest}; + FT_Bitmap native{7, 8, -9, reinterpret_cast(&native), 10, 11, 12, &native}; + FT_Bitmap_Init_nid_postfix(&guest); + FT_Bitmap_Init(&native); + Require(guest.rows == native.rows && guest.width == native.width && guest.pitch == native.pitch); + Require(guest.buffer == nullptr && native.buffer == nullptr && guest.palette == nullptr && native.palette == nullptr); + Require(guest.num_grays == native.num_grays && guest.pixel_mode == native.pixel_mode && guest.palette_mode == native.palette_mode); + Require(guest.rows == 0 && guest.width == 0 && guest.pitch == 0 && guest.num_grays == 0 && guest.pixel_mode == 0 && guest.palette_mode == 0); + FT_Bitmap_Init_nid_postfix(nullptr); + + GuestLibrary library{}; + Require(FT_Init_FreeType(&library.host) == 0); + const std::vector bytes = FontFixture::BuildFont(1, {}); + GuestFace face; + face.library = &library; + Require(FT_New_Memory_Face(library.host, bytes.data(), static_cast(bytes.size()), 0, &face.host) == 0); + SyncFace(face); + face.host->glyph->bitmap.rows = 9; + face.host->glyph->bitmap.width = 17; + face.slot.bitmap.rows = 9; + FT_Bitmap_Init_nid_postfix(&face.slot.bitmap); + Require(face.slot.bitmap.rows == 0 && face.host->glyph->bitmap.rows == 0 && face.host->glyph->bitmap.width == 0); + Require(FT_Done_Face(face.host) == 0); + Require(FT_Done_FreeType(library.host) == 0); +} From 46ac3341bc87217df2d3d9d5c64949e50ba45565 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sat, 24 Oct 2026 11:16:34 +0000 Subject: [PATCH 024/198] docs(libSceFreeType): list FT_Bitmap_Init with the bitmap calls --- docs/dev/debt/libSceFreeType.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceFreeType.md b/docs/dev/debt/libSceFreeType.md index 804b7f7763..e6c4ca54b7 100644 --- a/docs/dev/debt/libSceFreeType.md +++ b/docs/dev/debt/libSceFreeType.md @@ -8,7 +8,7 @@ - [FT_Init_FreeType, FT_New_Face, FT_Set_Pixel_Sizes, FT_Get_Char_Index, FT_Load_Glyph, FT_Render_Glyph](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - the PS4 library is taken to be FreeType 2 built for LP64: the face, size, charmap, glyph slot, bitmap and outline layouts follow the public headers of the bundled FreeType 2.13.3 with a 64-bit `long`, which matches the offsets the OpenOrbis samples read (`face->glyph` at 0x98, `slot->advance` at 0x80, `slot->bitmap` at 0x98, `slot->bitmap_left` at 0xc0). Which FreeType version and options the console library is built with is unknown, so the behaviour and error codes are those of FreeType 2.13.3. The OpenOrbis font, keyboard, threading, system and SDL2 samples import these six functions -- [FT_Bitmap_New, FT_Bitmap_Copy, FT_Bitmap_Convert, FT_Bitmap_Done](../../../core/libs/prx/libSceFreeType/src/Bitmap.cpp) (libSceFreeType) - console behaviour and error codes are unmeasured; these follow bundled FreeType 2.13.3. Copy and Convert throw when source and target share a non-null buffer; Convert also throws when they are the same object. Other overlapping storage remains forbidden by the native API contract +- [FT_Bitmap_Init, FT_Bitmap_New, FT_Bitmap_Copy, FT_Bitmap_Convert, FT_Bitmap_Done](../../../core/libs/prx/libSceFreeType/src/Bitmap.cpp) (libSceFreeType) - console behaviour and error codes are unmeasured; these follow bundled FreeType 2.13.3. Copy and Convert throw when source and target share a non-null buffer; Convert also throws when they are the same object. Other overlapping storage remains forbidden by the native API contract - [FT_Request_Size](../../../core/libs/prx/libSceFreeType/src/SizeRequest.cpp) (libSceFreeType) - console signature, request layout and errors are unverified; the guest request follows the LP64 public headers of the bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/include/freetype/freetype.h). Host overflow behavior remains that of the bundled build. On Windows, native `FT_Long` is 32 bits; valid requests whose width or height exceeds that range throw before changing the size. From 2a976d644ed07b9222d2a910934a3946206f9685 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sat, 24 Oct 2026 20:13:07 +0000 Subject: [PATCH 025/198] test(freetype): set Windows DLL path for 10583 --- core/libs/prx/libSceFreeType/CMakeLists.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/libs/prx/libSceFreeType/CMakeLists.txt b/core/libs/prx/libSceFreeType/CMakeLists.txt index 1f423ae443..f6a37a59ca 100644 --- a/core/libs/prx/libSceFreeType/CMakeLists.txt +++ b/core/libs/prx/libSceFreeType/CMakeLists.txt @@ -70,6 +70,10 @@ if(BUILD_TESTING) target_link_libraries(guest_freetype_bitmap_init_tests PRIVATE libSceFreeType freetype libc) configure_windows_unwind(guest_freetype_bitmap_init_tests) add_test(NAME guest_freetype_bitmap_init COMMAND guest_freetype_bitmap_init_tests) + if(WIN32) + set_tests_properties(guest_freetype_bitmap_init PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_freetype_charmap_tests ${LIBS_INCLUDE_DIR}/tests/GuestFreeTypeCharmap.cpp) target_include_directories(guest_freetype_charmap_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_freetype_charmap_tests PRIVATE libSceFreeType freetype libc) From d6aeceab3da578131cd7f2cddc71b7ff911376c5 Mon Sep 17 00:00:00 2001 From: Leonardo Date: Sat, 24 Oct 2026 18:46:52 +0200 Subject: [PATCH 026/198] feat(libSceNetCtl): implement sceNetCtlGetNatInfo Declare the NetCtlNatInfo structure layout (size, stun_status, nat_type, mapped_addr) and implement sceNetCtlGetNatInfo. When the host has an IPv4 interface, reports STUN status OK (2), NAT type 2 (moderate), and the host's IPv4 address as the mapped address. Returns SCE_NET_CTL_ERROR_NOT_CONNECTED when no interface is found. Unblocks session creation in Aliens: Fireteam Elite (PPSA03393). --- core/libs/SceTypes.hpp | 4 +++- core/libs/prx/libSceNetCtl/Export.cpp | 9 ++++++++- core/libs/tests/GuestNetCtl.cpp | 17 ++++++++--------- docs/dev/debt/libSceNetCtl.md | 2 +- 4 files changed, 20 insertions(+), 12 deletions(-) diff --git a/core/libs/SceTypes.hpp b/core/libs/SceTypes.hpp index f51297c09b..cf83c8abb8 100644 --- a/core/libs/SceTypes.hpp +++ b/core/libs/SceTypes.hpp @@ -1432,7 +1432,9 @@ struct NetEpollEvent { struct NetCtlNatInfo { std::uint32_t size; - std::uint8_t opaque[12]; + std::int32_t stun_status; + std::int32_t nat_type; + std::uint32_t mapped_addr; }; static_assert(sizeof(NetCtlNatInfo) == 16); diff --git a/core/libs/prx/libSceNetCtl/Export.cpp b/core/libs/prx/libSceNetCtl/Export.cpp index d3e153e34a..2be3e64a0e 100644 --- a/core/libs/prx/libSceNetCtl/Export.cpp +++ b/core/libs/prx/libSceNetCtl/Export.cpp @@ -49,6 +49,8 @@ static constexpr int NET_CTL_STATE_DISCONNECTED = 0; static constexpr int NET_CTL_STATE_IPOBTAINED = 3; static constexpr int NET_CTL_EVENT_DISCONNECTED = 1; static constexpr int NET_CTL_EVENT_IPOBTAINED = 3; +static constexpr int NET_CTL_STUN_STATUS_OK = 2; +static constexpr int NET_CTL_NAT_TYPE_2 = 2; static constexpr int NET_CTL_INFO_DEVICE = 1; static constexpr int NET_CTL_INFO_ETHER_ADDR = 2; static constexpr int NET_CTL_INFO_MTU = 3; @@ -574,7 +576,12 @@ int APS5_VABI sceNetCtlGetNatInfo(NetCtlNatInfo* info) { if (!g_initialized.load()) return SCE_NET_CTL_ERROR_NOT_INITIALIZED; if (!info) return SCE_NET_CTL_ERROR_INVALID_ADDR; if (info->size != sizeof(NetCtlNatInfo)) return SCE_NET_CTL_ERROR_INVALID_SIZE; - throw std::logic_error("sceNetCtlGetNatInfo: NAT discovery is not implemented"); + HostIpv4Interface host; + if (!FindHostIpv4Interface(host)) return SCE_NET_CTL_ERROR_NOT_CONNECTED; + info->stun_status = NET_CTL_STUN_STATUS_OK; + info->nat_type = NET_CTL_NAT_TYPE_2; + info->mapped_addr = host.address.s_addr; + return 0; } int APS5_VABI sceNetCtlGetResult(int event, int* result) { diff --git a/core/libs/tests/GuestNetCtl.cpp b/core/libs/tests/GuestNetCtl.cpp index 9a0e595f2b..49c6fca10d 100644 --- a/core/libs/tests/GuestNetCtl.cpp +++ b/core/libs/tests/GuestNetCtl.cpp @@ -349,13 +349,12 @@ int main() { Require(sceNetCtlGetNatInfo(&natInfo) == static_cast(0x80412111)); } natInfo.size = sizeof(natInfo); - std::memset(natInfo.opaque, kFill, sizeof(natInfo.opaque)); - bool natUnsupported = false; - try { - sceNetCtlGetNatInfo(&natInfo); - } catch (const std::logic_error&) { - natUnsupported = true; - } - Require(natUnsupported && natInfo.size == sizeof(natInfo)); - for (const auto value : natInfo.opaque) Require(value == kFill); + natInfo.stun_status = 0; + natInfo.nat_type = 0; + natInfo.mapped_addr = 0; + Require(sceNetCtlGetNatInfo(&natInfo) == 0); + Require(natInfo.size == sizeof(natInfo)); + Require(natInfo.stun_status == 2); + Require(natInfo.nat_type == 2); + Require(natInfo.mapped_addr == address.s_addr); } diff --git a/docs/dev/debt/libSceNetCtl.md b/docs/dev/debt/libSceNetCtl.md index add05f78df..28065e231d 100644 --- a/docs/dev/debt/libSceNetCtl.md +++ b/docs/dev/debt/libSceNetCtl.md @@ -2,7 +2,7 @@ ### Functional -- [sceNetCtlGetNatInfo](../../../core/libs/prx/libSceNetCtl/Export.cpp) - NAT discovery is not implemented and throws after native initialization and argument checks. NAT information is 16 bytes with a 32-bit size prefix; the remaining fields are unresolved. +- [sceNetCtlGetNatInfo](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - answers from the host IPv4 interface: reports STUN status OK (2), NAT type 2 (moderate), and the host's IPv4 address as the mapped address when connected, or `SCE_NET_CTL_ERROR_NOT_CONNECTED` (`0x80412108`) when no interface is available. Actual STUN server discovery against PlayStation network infrastructure is not performed. ### Unknown function info From ae712e749e8419a794b40aee31362b74e6e41611 Mon Sep 17 00:00:00 2001 From: Eduardo Ruiz Date: Sat, 24 Oct 2026 11:59:48 +0200 Subject: [PATCH 027/198] feat(libc): export _Mtx_init_with_default_name_override libc exports _Mtx_init_with_default_name_override (NID JHp7ogc1+HY) next to _Mtx_init_with_name. It takes the same arguments, and the name only reaches the pthread object, so forward to _Mtx_init_with_name instead of dropping the name as _Mtx_init does. A null name is accepted and falls back to the default. --- core/libs/prx/libc/include/Api.hpp | 1 + core/libs/prx/libc/src/Threads.cpp | 4 ++++ core/libs/tests/GuestLibcThreads.cpp | 24 ++++++++++++++++++++++++ 3 files changed, 29 insertions(+) diff --git a/core/libs/prx/libc/include/Api.hpp b/core/libs/prx/libc/include/Api.hpp index afdb17d63c..ae388b9930 100644 --- a/core/libs/prx/libc/include/Api.hpp +++ b/core/libs/prx/libc/include/Api.hpp @@ -984,6 +984,7 @@ void APS5_VABI vsyslog_nid_postfix(int priority, const char* format, VaList* arg void APS5_VABI syslog_nid_postfix(int priority, const char* format, ...); int APS5_VABI _Mtx_init_nid_postfix(void** handle, int type); int APS5_VABI _Mtx_init_with_name_nid_postfix(void** handle, int type, const char*); +int APS5_VABI _Mtx_init_with_default_name_override_nid_postfix(void** handle, int type, const char* name); void APS5_VABI _Mtx_destroy_nid_postfix(void** handle); int APS5_VABI _Mtx_lock_nid_postfix(void** handle); int APS5_VABI _Mtx_trylock_nid_postfix(void** handle); diff --git a/core/libs/prx/libc/src/Threads.cpp b/core/libs/prx/libc/src/Threads.cpp index 8f13974d7a..7f1d5f4335 100644 --- a/core/libs/prx/libc/src/Threads.cpp +++ b/core/libs/prx/libc/src/Threads.cpp @@ -167,6 +167,10 @@ int APS5_VABI _Mtx_init_with_name_nid_postfix(void** handle, int type, const cha return _Mtx_init_nid_postfix(handle, type); } +int APS5_VABI _Mtx_init_with_default_name_override_nid_postfix(void** handle, int type, const char* name) { + return _Mtx_init_with_name_nid_postfix(handle, type, name); +} + void APS5_VABI _Mtx_destroy_nid_postfix(void** handle) { if (!handle) throw std::invalid_argument("_Mtx_destroy: null mutex handle pointer"); diff --git a/core/libs/tests/GuestLibcThreads.cpp b/core/libs/tests/GuestLibcThreads.cpp index 1d2b3d7dff..12b7e5f1c7 100644 --- a/core/libs/tests/GuestLibcThreads.cpp +++ b/core/libs/tests/GuestLibcThreads.cpp @@ -91,6 +91,29 @@ static void MutexBasics() { RequireThrows([] { _Mtx_unlock_nid_postfix(nullptr); }); } +static void DefaultNameOverrideMutex() { + void* mutex = nullptr; + Require(_Mtx_init_with_default_name_override_nid_postfix(&mutex, tryable, "guest mutex") == success && mutex != nullptr); + Require(_Mtx_lock_nid_postfix(&mutex) == success && _Mtx_lock_nid_postfix(&mutex) == busy); + Require(_Mtx_unlock_nid_postfix(&mutex) == success); + _Mtx_destroy_nid_postfix(&mutex); + Require(mutex == nullptr); + + void* unnamedMutex = nullptr; + Require(_Mtx_init_with_default_name_override_nid_postfix(&unnamedMutex, 0, nullptr) == success && unnamedMutex != nullptr); + Require(_Mtx_lock_nid_postfix(&unnamedMutex) == success); + Require(_Mtx_unlock_nid_postfix(&unnamedMutex) == success); + _Mtx_destroy_nid_postfix(&unnamedMutex); + + void* recursiveMutex = nullptr; + Require(_Mtx_init_with_default_name_override_nid_postfix(&recursiveMutex, recursive, "recursive") == success && recursiveMutex != nullptr); + Require(_Mtx_lock_nid_postfix(&recursiveMutex) == success && _Mtx_lock_nid_postfix(&recursiveMutex) == success); + Require(_Mtx_unlock_nid_postfix(&recursiveMutex) == success && _Mtx_unlock_nid_postfix(&recursiveMutex) == success); + _Mtx_destroy_nid_postfix(&recursiveMutex); + + RequireThrows([] { _Mtx_init_with_default_name_override_nid_postfix(nullptr, tryable, "null"); }); +} + static void RecursiveMutex() { void* mutex = CreateMutex(tryable | recursive); for (int i = 0; i < 3; ++i) Require(_Mtx_lock_nid_postfix(&mutex) == success); @@ -439,6 +462,7 @@ int main() { VersionedSync(); ForwardedCalls(); MutexBasics(); + DefaultNameOverrideMutex(); RecursiveMutex(); MutualExclusion(); ConditionBroadcast(); From a4747ed8ef9c1b591e4e042829510d558deb69bb Mon Sep 17 00:00:00 2001 From: Leonardo Date: Sun, 25 Oct 2026 02:24:05 +0100 Subject: [PATCH 028/198] feat(libSceNetCtl): perform STUN discovery and NAT classification in sceNetCtlGetNatInfo --- core/libs/prx/libSceNetCtl/Export.cpp | 355 +++++++++++++++++++++++++- core/libs/tests/GuestNetCtl.cpp | 202 ++++++++++++++- docs/dev/debt/libSceNetCtl.md | 5 +- 3 files changed, 545 insertions(+), 17 deletions(-) diff --git a/core/libs/prx/libSceNetCtl/Export.cpp b/core/libs/prx/libSceNetCtl/Export.cpp index 2be3e64a0e..0107779706 100644 --- a/core/libs/prx/libSceNetCtl/Export.cpp +++ b/core/libs/prx/libSceNetCtl/Export.cpp @@ -9,8 +9,10 @@ #include #include #include +#include #include #include +#include #include #include #ifdef __linux__ @@ -21,6 +23,7 @@ #endif #endif #include +#include #include #include #include @@ -49,8 +52,23 @@ static constexpr int NET_CTL_STATE_DISCONNECTED = 0; static constexpr int NET_CTL_STATE_IPOBTAINED = 3; static constexpr int NET_CTL_EVENT_DISCONNECTED = 1; static constexpr int NET_CTL_EVENT_IPOBTAINED = 3; +static constexpr int NET_CTL_STUN_STATUS_FAILED = 1; static constexpr int NET_CTL_STUN_STATUS_OK = 2; +static constexpr int NET_CTL_NAT_TYPE_NONE = 0; +static constexpr int NET_CTL_NAT_TYPE_1 = 1; static constexpr int NET_CTL_NAT_TYPE_2 = 2; +static constexpr int NET_CTL_NAT_TYPE_3 = 3; +static constexpr std::uint16_t STUN_BINDING_REQUEST = 0x0001; +static constexpr std::uint16_t STUN_BINDING_RESPONSE = 0x0101; +static constexpr std::uint32_t STUN_MAGIC_COOKIE = 0x2112A442u; +static constexpr std::uint16_t STUN_ATTR_MAPPED_ADDRESS = 0x0001; +static constexpr std::uint16_t STUN_ATTR_CHANGED_ADDRESS = 0x0005; +static constexpr std::uint16_t STUN_ATTR_XOR_MAPPED_ADDRESS = 0x0020; +static constexpr std::uint16_t STUN_ATTR_XOR_MAPPED_ADDRESS_LEGACY = 0x8020; +static constexpr std::uint16_t STUN_ATTR_OTHER_ADDRESS = 0x802b; +static constexpr std::size_t STUN_HEADER_SIZE = 20; +static constexpr std::size_t STUN_TRANSACTION_ID_SIZE = 12; +static constexpr int STUN_DEFAULT_TIMEOUT_MS = 300; static constexpr int NET_CTL_INFO_DEVICE = 1; static constexpr int NET_CTL_INFO_ETHER_ADDR = 2; static constexpr int NET_CTL_INFO_MTU = 3; @@ -459,6 +477,319 @@ extern "C" std::size_t NetCtlHostDnsServers_nid_no_patch(std::uint32_t* servers, return host.dns.size(); } +#ifdef _WIN32 +using StunSocket = SOCKET; +static constexpr StunSocket INVALID_STUN_SOCKET = INVALID_SOCKET; +static void CloseStunSocket(StunSocket descriptor) { closesocket(descriptor); } +static bool EnsureSocketRuntime() { + static const int status = [] { + WSADATA data{}; + return WSAStartup(MAKEWORD(2, 2), &data); + }(); + return status == 0; +} +#else +using StunSocket = int; +static constexpr StunSocket INVALID_STUN_SOCKET = -1; +static void CloseStunSocket(StunSocket descriptor) { close(descriptor); } +static bool EnsureSocketRuntime() { return true; } +#endif + +struct StunEndpoint { + std::uint32_t address = 0; + std::uint16_t port = 0; + bool valid = false; +}; + +struct StunProbeResponse { + StunEndpoint mapped; + StunEndpoint other; +}; + +struct StunConfig { + std::mutex lock; + bool overrideActive = false; + std::vector overrideServers; + int overrideTimeoutMs = STUN_DEFAULT_TIMEOUT_MS; + std::uint64_t configGeneration = 1; + std::uint64_t cachedConfigGeneration = 0; + std::uint32_t cachedLocalAddr = 0; + bool hasCachedResult = false; + NetCtlNatInfo cachedResult{}; +}; + +static StunConfig& GetStunConfig() { + static StunConfig config; + return config; +} + +extern "C" void NetCtlSetStunServers_nid_no_patch(const sockaddr_in* servers, std::size_t count, int timeoutMs) { + auto& config = GetStunConfig(); + std::lock_guard guard(config.lock); + ++config.configGeneration; + config.hasCachedResult = false; + config.overrideServers.clear(); + if (!servers && count == 0 && timeoutMs <= 0) { + config.overrideActive = false; + config.overrideTimeoutMs = STUN_DEFAULT_TIMEOUT_MS; + return; + } + config.overrideActive = true; + config.overrideTimeoutMs = timeoutMs > 0 ? timeoutMs : STUN_DEFAULT_TIMEOUT_MS; + if (servers && count > 0) config.overrideServers.assign(servers, servers + count); +} + +static void AppendUniqueStunServer(std::vector& servers, const sockaddr_in& candidate) { + if (candidate.sin_family != AF_INET || candidate.sin_addr.s_addr == 0 || candidate.sin_port == 0) return; + for (const auto& existing : servers) { + if (existing.sin_addr.s_addr == candidate.sin_addr.s_addr && existing.sin_port == candidate.sin_port) return; + } + servers.push_back(candidate); +} + +static std::vector ResolveStunServers(int& timeoutMs, std::uint64_t& generationOut) { + auto& config = GetStunConfig(); + { + std::lock_guard guard(config.lock); + generationOut = config.configGeneration; + if (config.overrideActive) { + timeoutMs = config.overrideTimeoutMs; + return config.overrideServers; + } + } + timeoutMs = STUN_DEFAULT_TIMEOUT_MS; + struct DefaultStunHost { + const char* host; + const char* port; + }; + static constexpr DefaultStunHost kDefaultHosts[] = { + {"stun.playstation.net", "3478"}, + {"stun.cloudflare.com", "3478"}, + {"stun.l.google.com", "19302"}, + {"stun1.l.google.com", "19302"}, + }; + std::vector servers; + if (!EnsureSocketRuntime()) return servers; + for (const auto& entry : kDefaultHosts) { + addrinfo hints{}; + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_DGRAM; + hints.ai_protocol = IPPROTO_UDP; + addrinfo* list = nullptr; + if (getaddrinfo(entry.host, entry.port, &hints, &list) != 0 || !list) continue; + for (auto* item = list; item; item = item->ai_next) { + if (item->ai_addr && item->ai_addrlen >= sizeof(sockaddr_in) && item->ai_addr->sa_family == AF_INET) { + AppendUniqueStunServer(servers, *reinterpret_cast(item->ai_addr)); + } + } + freeaddrinfo(list); + } + return servers; +} + +static StunEndpoint DecodeStunIpv4Endpoint(const std::uint8_t* value, std::uint16_t length, bool xorMask) { + StunEndpoint endpoint{}; + if (length < 8 || value[1] != 0x01) return endpoint; + const auto rawPort = static_cast((static_cast(value[2]) << 8) | value[3]); + std::uint32_t rawAddress = 0; + std::memcpy(&rawAddress, value + 4, sizeof(rawAddress)); + endpoint.port = xorMask ? static_cast(rawPort ^ static_cast(STUN_MAGIC_COOKIE >> 16)) : rawPort; + endpoint.address = xorMask ? (rawAddress ^ htonl(STUN_MAGIC_COOKIE)) : rawAddress; + endpoint.valid = endpoint.address != 0 && endpoint.port != 0; + return endpoint; +} + +static bool ParseStunBindingResponse(const std::uint8_t* packet, std::size_t size, const std::uint8_t* transactionId, StunProbeResponse& response) { + if (size < STUN_HEADER_SIZE) return false; + const auto type = static_cast((static_cast(packet[0]) << 8) | packet[1]); + const auto length = static_cast((static_cast(packet[2]) << 8) | packet[3]); + if (type != STUN_BINDING_RESPONSE || (length & 3u) != 0 || STUN_HEADER_SIZE + length > size) return false; + const std::uint32_t expectedCookie = htonl(STUN_MAGIC_COOKIE); + if (std::memcmp(packet + 4, &expectedCookie, sizeof(expectedCookie)) != 0) return false; + if (std::memcmp(packet + 8, transactionId, STUN_TRANSACTION_ID_SIZE) != 0) return false; + response = {}; + bool hasXorMapped = false; + const std::size_t end = STUN_HEADER_SIZE + length; + for (std::size_t offset = STUN_HEADER_SIZE; offset < end;) { + if (offset + 4 > end) return false; + const auto attrType = static_cast((static_cast(packet[offset]) << 8) | packet[offset + 1]); + const auto attrLen = static_cast((static_cast(packet[offset + 2]) << 8) | packet[offset + 3]); + const std::size_t valueOffset = offset + 4; + if (valueOffset + attrLen > end) return false; + const std::uint8_t* value = packet + valueOffset; + if (attrType == STUN_ATTR_XOR_MAPPED_ADDRESS || attrType == STUN_ATTR_XOR_MAPPED_ADDRESS_LEGACY) { + const auto endpoint = DecodeStunIpv4Endpoint(value, attrLen, true); + if (endpoint.valid) { + response.mapped = endpoint; + hasXorMapped = true; + } + } else if (attrType == STUN_ATTR_MAPPED_ADDRESS && !hasXorMapped) { + const auto endpoint = DecodeStunIpv4Endpoint(value, attrLen, false); + if (endpoint.valid) response.mapped = endpoint; + } else if (attrType == STUN_ATTR_OTHER_ADDRESS || attrType == STUN_ATTR_CHANGED_ADDRESS) { + const auto endpoint = DecodeStunIpv4Endpoint(value, attrLen, false); + if (endpoint.valid) response.other = endpoint; + } + offset = valueOffset + ((static_cast(attrLen) + 3u) & ~std::size_t(3u)); + } + return response.mapped.valid; +} + +static std::atomic g_stunTransactionCounter{1}; + +static bool ProbeStunServer(StunSocket descriptor, const sockaddr_in& server, int timeoutMs, StunProbeResponse& response) { + std::uint8_t request[STUN_HEADER_SIZE] = {}; + request[0] = static_cast(STUN_BINDING_REQUEST >> 8); + request[1] = static_cast(STUN_BINDING_REQUEST & 0xFF); + const std::uint32_t cookie = htonl(STUN_MAGIC_COOKIE); + std::memcpy(request + 4, &cookie, sizeof(cookie)); + const std::uint64_t seq = g_stunTransactionCounter.fetch_add(1, std::memory_order_relaxed); + for (std::size_t i = 0; i < STUN_TRANSACTION_ID_SIZE; ++i) { + request[8 + i] = static_cast((seq * 0x9E3779B97F4A7C15ull >> ((i % 8) * 8)) ^ (i * 0x3Du)); + } + const auto sent = sendto(descriptor, reinterpret_cast(request), sizeof(request), 0, + reinterpret_cast(&server), sizeof(server)); + if (sent != static_cast(sizeof(request))) return false; + const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeoutMs > 0 ? timeoutMs : 1); + for (;;) { + const auto now = std::chrono::steady_clock::now(); + if (now >= deadline) return false; + const auto remainingUs = std::chrono::duration_cast(deadline - now).count(); + fd_set readSet; + FD_ZERO(&readSet); + FD_SET(descriptor, &readSet); + timeval tv{}; + tv.tv_sec = static_cast(remainingUs / 1000000); + tv.tv_usec = static_cast(remainingUs % 1000000); + if (tv.tv_sec == 0 && tv.tv_usec == 0) tv.tv_usec = 1000; +#ifdef _WIN32 + const int ready = select(0, &readSet, nullptr, nullptr, &tv); +#else + const int ready = select(descriptor + 1, &readSet, nullptr, nullptr, &tv); +#endif + if (ready <= 0) return false; + std::uint8_t buffer[512]; + sockaddr_in from{}; +#ifdef _WIN32 + int fromLen = sizeof(from); +#else + socklen_t fromLen = sizeof(from); +#endif + const auto received = recvfrom(descriptor, reinterpret_cast(buffer), sizeof(buffer), 0, + reinterpret_cast(&from), &fromLen); +#ifdef _WIN32 + if (received < 0) { + if (WSAGetLastError() == WSAECONNRESET) continue; + return false; + } +#else + if (received < 0) return false; +#endif + if (received == 0) return false; + if (ParseStunBindingResponse(buffer, static_cast(received), request + 8, response)) return true; + } +} + +static NetCtlNatInfo DiscoverNatInfo(in_addr localAddress) { + NetCtlNatInfo result{}; + result.size = sizeof(NetCtlNatInfo); + result.stun_status = NET_CTL_STUN_STATUS_FAILED; + result.nat_type = NET_CTL_NAT_TYPE_NONE; + result.mapped_addr = 0; + + int timeoutMs = STUN_DEFAULT_TIMEOUT_MS; + std::uint64_t generation = 0; + const auto servers = ResolveStunServers(timeoutMs, generation); + if (servers.empty() || !EnsureSocketRuntime()) return result; + + const StunSocket descriptor = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (descriptor == INVALID_STUN_SOCKET) return result; + +#ifdef _WIN32 + BOOL newBehavior = FALSE; + DWORD bytesReturned = 0; + WSAIoctl(descriptor, _WSAIOW(IOC_VENDOR, 12), &newBehavior, sizeof(newBehavior), nullptr, 0, &bytesReturned, nullptr, nullptr); +#endif + + sockaddr_in bindAddr{}; + bindAddr.sin_family = AF_INET; + bindAddr.sin_addr = (ntohl(servers[0].sin_addr.s_addr) >> 24) == 127 ? servers[0].sin_addr : localAddress; + bindAddr.sin_port = 0; + if (bind(descriptor, reinterpret_cast(&bindAddr), sizeof(bindAddr)) != 0) { + CloseStunSocket(descriptor); + return result; + } + + sockaddr_in boundAddr{}; +#ifdef _WIN32 + int boundLen = sizeof(boundAddr); +#else + socklen_t boundLen = sizeof(boundAddr); +#endif + if (getsockname(descriptor, reinterpret_cast(&boundAddr), &boundLen) != 0) { + CloseStunSocket(descriptor); + return result; + } + const std::uint16_t localPort = ntohs(boundAddr.sin_port); + + StunProbeResponse firstResponse{}; + sockaddr_in primaryServer{}; + std::size_t primaryIndex = servers.size(); + for (std::size_t i = 0; i < servers.size(); ++i) { + if (ProbeStunServer(descriptor, servers[i], timeoutMs, firstResponse)) { + primaryServer = servers[i]; + primaryIndex = i; + break; + } + } + if (primaryIndex == servers.size()) { + CloseStunSocket(descriptor); + return result; + } + + if (firstResponse.mapped.address == localAddress.s_addr && firstResponse.mapped.port == localPort) { + CloseStunSocket(descriptor); + result.stun_status = NET_CTL_STUN_STATUS_OK; + result.nat_type = NET_CTL_NAT_TYPE_1; + result.mapped_addr = firstResponse.mapped.address; + return result; + } + + std::vector secondaryServers; + if (firstResponse.other.valid) { + sockaddr_in otherServer{}; + otherServer.sin_family = AF_INET; + otherServer.sin_addr.s_addr = firstResponse.other.address; + otherServer.sin_port = htons(firstResponse.other.port); + if (otherServer.sin_addr.s_addr != primaryServer.sin_addr.s_addr || otherServer.sin_port != primaryServer.sin_port) { + AppendUniqueStunServer(secondaryServers, otherServer); + } + } + for (std::size_t i = 0; i < servers.size(); ++i) { + if (i == primaryIndex) continue; + if (servers[i].sin_addr.s_addr != primaryServer.sin_addr.s_addr || servers[i].sin_port != primaryServer.sin_port) { + AppendUniqueStunServer(secondaryServers, servers[i]); + } + } + + for (const auto& secondary : secondaryServers) { + StunProbeResponse secondResponse{}; + if (ProbeStunServer(descriptor, secondary, timeoutMs, secondResponse)) { + CloseStunSocket(descriptor); + result.stun_status = NET_CTL_STUN_STATUS_OK; + result.nat_type = (secondResponse.mapped.address == firstResponse.mapped.address && + secondResponse.mapped.port == firstResponse.mapped.port) + ? NET_CTL_NAT_TYPE_2 + : NET_CTL_NAT_TYPE_3; + result.mapped_addr = firstResponse.mapped.address; + return result; + } + } + + CloseStunSocket(descriptor); + return result; +} + static void WriteInfoWord(NetCtlInfo* info, std::uint32_t value) { std::memcpy(info->opaque, &value, sizeof(value)); } @@ -578,9 +909,27 @@ int APS5_VABI sceNetCtlGetNatInfo(NetCtlNatInfo* info) { if (info->size != sizeof(NetCtlNatInfo)) return SCE_NET_CTL_ERROR_INVALID_SIZE; HostIpv4Interface host; if (!FindHostIpv4Interface(host)) return SCE_NET_CTL_ERROR_NOT_CONNECTED; - info->stun_status = NET_CTL_STUN_STATUS_OK; - info->nat_type = NET_CTL_NAT_TYPE_2; - info->mapped_addr = host.address.s_addr; + auto& config = GetStunConfig(); + std::uint64_t generation = 0; + { + std::lock_guard guard(config.lock); + generation = config.configGeneration; + if (config.hasCachedResult && config.cachedConfigGeneration == generation && config.cachedLocalAddr == host.address.s_addr) { + *info = config.cachedResult; + return 0; + } + } + const NetCtlNatInfo discovered = DiscoverNatInfo(host.address); + { + std::lock_guard guard(config.lock); + if (config.configGeneration == generation) { + config.cachedConfigGeneration = generation; + config.cachedLocalAddr = host.address.s_addr; + config.cachedResult = discovered; + config.hasCachedResult = true; + } + } + *info = discovered; return 0; } diff --git a/core/libs/tests/GuestNetCtl.cpp b/core/libs/tests/GuestNetCtl.cpp index 49c6fca10d..eb81b9e64c 100644 --- a/core/libs/tests/GuestNetCtl.cpp +++ b/core/libs/tests/GuestNetCtl.cpp @@ -6,13 +6,13 @@ #else #include #include +#include #include #include #endif #ifdef __APPLE__ #include #include -#include #endif #include "prx/libc/include/general/VabiMacros.hpp" #include "SceTypes.hpp" @@ -23,6 +23,8 @@ #include #include #include +#include +#include #ifdef __APPLE__ struct NetCtlDefaultRoute { @@ -38,6 +40,7 @@ extern "C" { #ifdef __APPLE__ std::size_t NetCtlDefaultRoutes_nid_no_patch(const char*, std::size_t, NetCtlDefaultRoute*, std::size_t); #endif +void NetCtlSetStunServers_nid_no_patch(const sockaddr_in*, std::size_t, int); } @@ -197,6 +200,193 @@ bool Binds(in_addr address) { return bound; } +#ifdef _WIN32 +using TestSocket = SOCKET; +constexpr TestSocket kInvalidSocket = INVALID_SOCKET; +void CloseTestSocket(TestSocket descriptor) { closesocket(descriptor); } +#else +using TestSocket = int; +constexpr TestSocket kInvalidSocket = -1; +void CloseTestSocket(TestSocket descriptor) { close(descriptor); } +#endif + +TestSocket OpenTestUdpSocket(sockaddr_in& bound) { + const TestSocket descriptor = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + Require(descriptor != kInvalidSocket); + sockaddr_in local{}; + local.sin_family = AF_INET; + local.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + local.sin_port = 0; + Require(bind(descriptor, reinterpret_cast(&local), sizeof(local)) == 0); +#ifdef _WIN32 + int length = sizeof(bound); +#else + socklen_t length = sizeof(bound); +#endif + Require(getsockname(descriptor, reinterpret_cast(&bound), &length) == 0); + return descriptor; +} + +bool WaitReadable(TestSocket descriptor, long timeoutMs) { + fd_set readSet; + FD_ZERO(&readSet); + FD_SET(descriptor, &readSet); + timeval tv{}; + tv.tv_sec = timeoutMs / 1000; + tv.tv_usec = (timeoutMs % 1000) * 1000; +#ifdef _WIN32 + return select(0, &readSet, nullptr, nullptr, &tv) > 0; +#else + return select(descriptor + 1, &readSet, nullptr, nullptr, &tv) > 0; +#endif +} + +void AppendStunAddressAttribute(std::vector& packet, std::uint16_t attrType, std::uint32_t address, std::uint16_t hostPort, bool xorMask) { + constexpr std::uint32_t kCookie = 0x2112A442u; + const std::uint16_t wirePort = xorMask ? static_cast(hostPort ^ static_cast(kCookie >> 16)) : hostPort; + const std::uint32_t wireAddr = xorMask ? (address ^ htonl(kCookie)) : address; + packet.push_back(static_cast(attrType >> 8)); + packet.push_back(static_cast(attrType & 0xFF)); + packet.push_back(0x00); + packet.push_back(0x08); + packet.push_back(0x00); + packet.push_back(0x01); + packet.push_back(static_cast(wirePort >> 8)); + packet.push_back(static_cast(wirePort & 0xFF)); + const auto* addrBytes = reinterpret_cast(&wireAddr); + packet.insert(packet.end(), addrBytes, addrBytes + 4); +} + +sockaddr_in ReplyStunBinding(TestSocket serverSocket, std::uint32_t mappedAddr, std::uint16_t mappedHostPort, bool xorMapped, const sockaddr_in* otherServer = nullptr) { + Require(WaitReadable(serverSocket, 2000)); + std::uint8_t request[64] = {}; + sockaddr_in client{}; +#ifdef _WIN32 + int clientLen = sizeof(client); +#else + socklen_t clientLen = sizeof(client); +#endif + const auto received = recvfrom(serverSocket, reinterpret_cast(request), sizeof(request), 0, + reinterpret_cast(&client), &clientLen); + Require(received == 20); + Require(request[0] == 0x00 && request[1] == 0x01 && request[2] == 0x00 && request[3] == 0x00); + const std::uint32_t expectedCookie = htonl(0x2112A442u); + Require(std::memcmp(request + 4, &expectedCookie, sizeof(expectedCookie)) == 0); + + const std::uint16_t effectivePort = mappedHostPort != 0 ? mappedHostPort : ntohs(client.sin_port); + const std::uint32_t effectiveAddr = mappedAddr != 0 ? mappedAddr : client.sin_addr.s_addr; + + std::vector response(20, 0); + response[0] = 0x01; + response[1] = 0x01; + std::memcpy(response.data() + 4, request + 4, 16); + AppendStunAddressAttribute(response, xorMapped ? 0x0020 : 0x0001, effectiveAddr, effectivePort, xorMapped); + if (otherServer) { + AppendStunAddressAttribute(response, 0x802b, otherServer->sin_addr.s_addr, ntohs(otherServer->sin_port), false); + } + const auto attrLen = static_cast(response.size() - 20); + response[2] = static_cast(attrLen >> 8); + response[3] = static_cast(attrLen & 0xFF); + const auto sent = sendto(serverSocket, reinterpret_cast(response.data()), static_cast(response.size()), 0, + reinterpret_cast(&client), clientLen); + Require(sent == static_cast(response.size())); + return client; +} + +void CheckNatDiscovery(in_addr address) { + sockaddr_in server1Addr{}; + sockaddr_in server2Addr{}; + const TestSocket server1 = OpenTestUdpSocket(server1Addr); + const TestSocket server2 = OpenTestUdpSocket(server2Addr); + + NetCtlNatInfo natInfo{}; + natInfo.size = sizeof(natInfo); + natInfo.stun_status = 99; + natInfo.nat_type = 99; + natInfo.mapped_addr = 0xFFFFFFFFu; + NetCtlSetStunServers_nid_no_patch(&server1Addr, 1, 20); + Require(sceNetCtlGetNatInfo(&natInfo) == 0); + Require(natInfo.size == sizeof(natInfo)); + Require(natInfo.stun_status == 1); + Require(natInfo.nat_type == 0); + Require(natInfo.mapped_addr == 0); + { + Require(WaitReadable(server1, 2000)); + std::uint8_t drained[64] = {}; + sockaddr_in from{}; +#ifdef _WIN32 + int fromLen = sizeof(from); +#else + socklen_t fromLen = sizeof(from); +#endif + Require(recvfrom(server1, reinterpret_cast(drained), sizeof(drained), 0, reinterpret_cast(&from), &fromLen) == 20); + } + + NetCtlSetStunServers_nid_no_patch(&server1Addr, 1, 1000); + std::thread openWorker([&] { + ReplyStunBinding(server1, address.s_addr, 0, true); + }); + natInfo = {sizeof(natInfo), 0, 0, 0}; + Require(sceNetCtlGetNatInfo(&natInfo) == 0); + openWorker.join(); + Require(natInfo.size == sizeof(natInfo)); + Require(natInfo.stun_status == 2); + Require(natInfo.nat_type == 1); + Require(natInfo.mapped_addr == address.s_addr); + + const std::uint32_t externalIp = (ntohl(address.s_addr) == 0xCB00712Au) ? htonl(0xCB00712Bu) : htonl(0xCB00712Au); + NetCtlSetStunServers_nid_no_patch(&server1Addr, 1, 1000); + std::thread incompleteWorker([&] { + ReplyStunBinding(server1, externalIp, 54321, true); + }); + natInfo = {sizeof(natInfo), 99, 99, 0xFFFFFFFFu}; + Require(sceNetCtlGetNatInfo(&natInfo) == 0); + incompleteWorker.join(); + Require(natInfo.size == sizeof(natInfo)); + Require(natInfo.stun_status == 1); + Require(natInfo.nat_type == 0); + Require(natInfo.mapped_addr == 0); + + const sockaddr_in bothServers[2] = {server1Addr, server2Addr}; + NetCtlSetStunServers_nid_no_patch(bothServers, 2, 1000); + std::thread type2Worker([&] { + const auto firstClient = ReplyStunBinding(server1, externalIp, 54321, true); + const auto secondClient = ReplyStunBinding(server2, externalIp, 54321, true); + Require(firstClient.sin_addr.s_addr == secondClient.sin_addr.s_addr); + Require(firstClient.sin_port == secondClient.sin_port); + }); + natInfo = {sizeof(natInfo), 0, 0, 0}; + Require(sceNetCtlGetNatInfo(&natInfo) == 0); + type2Worker.join(); + Require(natInfo.size == sizeof(natInfo)); + Require(natInfo.stun_status == 2); + Require(natInfo.nat_type == 2); + Require(natInfo.mapped_addr == externalIp); + + NetCtlNatInfo cachedInfo{sizeof(cachedInfo), 0, 0, 0}; + Require(sceNetCtlGetNatInfo(&cachedInfo) == 0); + Require(cachedInfo.stun_status == 2 && cachedInfo.nat_type == 2 && cachedInfo.mapped_addr == externalIp); + + NetCtlSetStunServers_nid_no_patch(&server1Addr, 1, 1000); + std::thread type3Worker([&] { + const auto firstClient = ReplyStunBinding(server1, externalIp, 54321, false, &server2Addr); + const auto secondClient = ReplyStunBinding(server2, externalIp, 54322, false); + Require(firstClient.sin_addr.s_addr == secondClient.sin_addr.s_addr); + Require(firstClient.sin_port == secondClient.sin_port); + }); + natInfo = {sizeof(natInfo), 0, 0, 0}; + Require(sceNetCtlGetNatInfo(&natInfo) == 0); + type3Worker.join(); + Require(natInfo.size == sizeof(natInfo)); + Require(natInfo.stun_status == 2); + Require(natInfo.nat_type == 3); + Require(natInfo.mapped_addr == externalIp); + + NetCtlSetStunServers_nid_no_patch(nullptr, 0, 0); + CloseTestSocket(server1); + CloseTestSocket(server2); +} + } #ifdef __APPLE__ @@ -348,13 +538,5 @@ int main() { natInfo.size = size; Require(sceNetCtlGetNatInfo(&natInfo) == static_cast(0x80412111)); } - natInfo.size = sizeof(natInfo); - natInfo.stun_status = 0; - natInfo.nat_type = 0; - natInfo.mapped_addr = 0; - Require(sceNetCtlGetNatInfo(&natInfo) == 0); - Require(natInfo.size == sizeof(natInfo)); - Require(natInfo.stun_status == 2); - Require(natInfo.nat_type == 2); - Require(natInfo.mapped_addr == address.s_addr); + CheckNatDiscovery(address); } diff --git a/docs/dev/debt/libSceNetCtl.md b/docs/dev/debt/libSceNetCtl.md index 28065e231d..dcac3f5120 100644 --- a/docs/dev/debt/libSceNetCtl.md +++ b/docs/dev/debt/libSceNetCtl.md @@ -1,9 +1,5 @@ # libSceNetCtl technical debt -### Functional - -- [sceNetCtlGetNatInfo](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - answers from the host IPv4 interface: reports STUN status OK (2), NAT type 2 (moderate), and the host's IPv4 address as the mapped address when connected, or `SCE_NET_CTL_ERROR_NOT_CONNECTED` (`0x80412108`) when no interface is available. Actual STUN server discovery against PlayStation network infrastructure is not performed. - ### Unknown function info - [sceNetCtlScanIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - argument registers and IPC transfer directions are recovered for the info, connection, scan, NAT, bandwidth and Wi-Fi queries. The connection configuration is 1464 bytes, scan options 88, BSSID output 96, scan info 112, IPv6 info 256, internal NAT info 196, bandwidth info 32, bandwidth-estimation info 28 and Wi-Fi info 48. Their field semantics still require the receiving service; incomplete declarations preserve this gap. Scan queries transfer a six-byte BSSID and use a 64-bit in/out buffer size. @@ -22,3 +18,4 @@ - [sceNetCtlGetInfo](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) finds the host's default route in `/proc/net/route` on Linux and, on macOS, in the routing table read with `sysctl(NET_RT_FLAGS, RTF_GATEWAY)`, where an interface-scoped default route ranks after the unscoped one - [sceNetCtlGetIfStat](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) throws on macOS: an unprivileged process reads the interface byte counters (`getifaddrs` and `sysctl(NET_RT_IFLIST2)` alike) reduced to their low 32 bits, so a total would wrap every 4 GiB - [Host interface cache](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - on Windows, `sceNetCtlGetState`, `sceNetCtlGetStateV6`, `sceNetCtlCheckCallback`, `sceNetCtlGetIfStat` and `sceNetCtlGetInfo` reuse the last host interface query until `NotifyUnicastIpAddressChange`, `NotifyIpInterfaceChange` or `NotifyRouteChange2` reports a change, instead of calling `GetAdaptersAddresses` on every call. A DNS server change with no address, interface or route change is seen only at the next such change. If the notifications cannot be registered, every call queries the host as before. +- [sceNetCtlGetNatInfo](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - performs RFC 5389 STUN binding probes from the active host IPv4 interface against `stun.playstation.net` and fallback public STUN servers (`stun.cloudflare.com`, `stun.l.google.com`, `stun1.l.google.com`) instead of reading cached state from the console's background `netctl` service. Reports NAT type 1 when the mapped IPv4 endpoint matches the local socket, NAT type 2 when a second STUN endpoint reports the same mapped address and port, NAT type 3 when the second endpoint reports a different mapping, and STUN status 1 (`FAILED`) with NAT type 0 when STUN discovery or classification does not complete. From a0ee2ca7fbbac465963da6c69cac125e5a4adc96 Mon Sep 17 00:00:00 2001 From: Leonardo Date: Sun, 25 Oct 2026 03:52:28 +0100 Subject: [PATCH 029/198] fix(libSceNetCtl): leave nat_type unclassified in sceNetCtlGetNatInfo --- core/libs/prx/libSceNetCtl/Export.cpp | 92 ++++----------------------- core/libs/tests/GuestNetCtl.cpp | 73 +++++++-------------- docs/dev/debt/libSceNetCtl.md | 2 +- 3 files changed, 37 insertions(+), 130 deletions(-) diff --git a/core/libs/prx/libSceNetCtl/Export.cpp b/core/libs/prx/libSceNetCtl/Export.cpp index 0107779706..8adbcfe20b 100644 --- a/core/libs/prx/libSceNetCtl/Export.cpp +++ b/core/libs/prx/libSceNetCtl/Export.cpp @@ -55,17 +55,12 @@ static constexpr int NET_CTL_EVENT_IPOBTAINED = 3; static constexpr int NET_CTL_STUN_STATUS_FAILED = 1; static constexpr int NET_CTL_STUN_STATUS_OK = 2; static constexpr int NET_CTL_NAT_TYPE_NONE = 0; -static constexpr int NET_CTL_NAT_TYPE_1 = 1; -static constexpr int NET_CTL_NAT_TYPE_2 = 2; -static constexpr int NET_CTL_NAT_TYPE_3 = 3; static constexpr std::uint16_t STUN_BINDING_REQUEST = 0x0001; static constexpr std::uint16_t STUN_BINDING_RESPONSE = 0x0101; static constexpr std::uint32_t STUN_MAGIC_COOKIE = 0x2112A442u; static constexpr std::uint16_t STUN_ATTR_MAPPED_ADDRESS = 0x0001; -static constexpr std::uint16_t STUN_ATTR_CHANGED_ADDRESS = 0x0005; static constexpr std::uint16_t STUN_ATTR_XOR_MAPPED_ADDRESS = 0x0020; static constexpr std::uint16_t STUN_ATTR_XOR_MAPPED_ADDRESS_LEGACY = 0x8020; -static constexpr std::uint16_t STUN_ATTR_OTHER_ADDRESS = 0x802b; static constexpr std::size_t STUN_HEADER_SIZE = 20; static constexpr std::size_t STUN_TRANSACTION_ID_SIZE = 12; static constexpr int STUN_DEFAULT_TIMEOUT_MS = 300; @@ -501,11 +496,6 @@ struct StunEndpoint { bool valid = false; }; -struct StunProbeResponse { - StunEndpoint mapped; - StunEndpoint other; -}; - struct StunConfig { std::mutex lock; bool overrideActive = false; @@ -599,7 +589,7 @@ static StunEndpoint DecodeStunIpv4Endpoint(const std::uint8_t* value, std::uint1 return endpoint; } -static bool ParseStunBindingResponse(const std::uint8_t* packet, std::size_t size, const std::uint8_t* transactionId, StunProbeResponse& response) { +static bool ParseStunBindingResponse(const std::uint8_t* packet, std::size_t size, const std::uint8_t* transactionId, StunEndpoint& mapped) { if (size < STUN_HEADER_SIZE) return false; const auto type = static_cast((static_cast(packet[0]) << 8) | packet[1]); const auto length = static_cast((static_cast(packet[2]) << 8) | packet[3]); @@ -607,7 +597,7 @@ static bool ParseStunBindingResponse(const std::uint8_t* packet, std::size_t siz const std::uint32_t expectedCookie = htonl(STUN_MAGIC_COOKIE); if (std::memcmp(packet + 4, &expectedCookie, sizeof(expectedCookie)) != 0) return false; if (std::memcmp(packet + 8, transactionId, STUN_TRANSACTION_ID_SIZE) != 0) return false; - response = {}; + mapped = {}; bool hasXorMapped = false; const std::size_t end = STUN_HEADER_SIZE + length; for (std::size_t offset = STUN_HEADER_SIZE; offset < end;) { @@ -620,24 +610,21 @@ static bool ParseStunBindingResponse(const std::uint8_t* packet, std::size_t siz if (attrType == STUN_ATTR_XOR_MAPPED_ADDRESS || attrType == STUN_ATTR_XOR_MAPPED_ADDRESS_LEGACY) { const auto endpoint = DecodeStunIpv4Endpoint(value, attrLen, true); if (endpoint.valid) { - response.mapped = endpoint; + mapped = endpoint; hasXorMapped = true; } } else if (attrType == STUN_ATTR_MAPPED_ADDRESS && !hasXorMapped) { const auto endpoint = DecodeStunIpv4Endpoint(value, attrLen, false); - if (endpoint.valid) response.mapped = endpoint; - } else if (attrType == STUN_ATTR_OTHER_ADDRESS || attrType == STUN_ATTR_CHANGED_ADDRESS) { - const auto endpoint = DecodeStunIpv4Endpoint(value, attrLen, false); - if (endpoint.valid) response.other = endpoint; + if (endpoint.valid) mapped = endpoint; } offset = valueOffset + ((static_cast(attrLen) + 3u) & ~std::size_t(3u)); } - return response.mapped.valid; + return mapped.valid; } static std::atomic g_stunTransactionCounter{1}; -static bool ProbeStunServer(StunSocket descriptor, const sockaddr_in& server, int timeoutMs, StunProbeResponse& response) { +static bool ProbeStunServer(StunSocket descriptor, const sockaddr_in& server, int timeoutMs, StunEndpoint& mapped) { std::uint8_t request[STUN_HEADER_SIZE] = {}; request[0] = static_cast(STUN_BINDING_REQUEST >> 8); request[1] = static_cast(STUN_BINDING_REQUEST & 0xFF); @@ -686,7 +673,7 @@ static bool ProbeStunServer(StunSocket descriptor, const sockaddr_in& server, in if (received < 0) return false; #endif if (received == 0) return false; - if (ParseStunBindingResponse(buffer, static_cast(received), request + 8, response)) return true; + if (ParseStunBindingResponse(buffer, static_cast(received), request + 8, mapped)) return true; } } @@ -720,68 +707,13 @@ static NetCtlNatInfo DiscoverNatInfo(in_addr localAddress) { return result; } - sockaddr_in boundAddr{}; -#ifdef _WIN32 - int boundLen = sizeof(boundAddr); -#else - socklen_t boundLen = sizeof(boundAddr); -#endif - if (getsockname(descriptor, reinterpret_cast(&boundAddr), &boundLen) != 0) { - CloseStunSocket(descriptor); - return result; - } - const std::uint16_t localPort = ntohs(boundAddr.sin_port); - - StunProbeResponse firstResponse{}; - sockaddr_in primaryServer{}; - std::size_t primaryIndex = servers.size(); - for (std::size_t i = 0; i < servers.size(); ++i) { - if (ProbeStunServer(descriptor, servers[i], timeoutMs, firstResponse)) { - primaryServer = servers[i]; - primaryIndex = i; - break; - } - } - if (primaryIndex == servers.size()) { - CloseStunSocket(descriptor); - return result; - } - - if (firstResponse.mapped.address == localAddress.s_addr && firstResponse.mapped.port == localPort) { - CloseStunSocket(descriptor); - result.stun_status = NET_CTL_STUN_STATUS_OK; - result.nat_type = NET_CTL_NAT_TYPE_1; - result.mapped_addr = firstResponse.mapped.address; - return result; - } - - std::vector secondaryServers; - if (firstResponse.other.valid) { - sockaddr_in otherServer{}; - otherServer.sin_family = AF_INET; - otherServer.sin_addr.s_addr = firstResponse.other.address; - otherServer.sin_port = htons(firstResponse.other.port); - if (otherServer.sin_addr.s_addr != primaryServer.sin_addr.s_addr || otherServer.sin_port != primaryServer.sin_port) { - AppendUniqueStunServer(secondaryServers, otherServer); - } - } - for (std::size_t i = 0; i < servers.size(); ++i) { - if (i == primaryIndex) continue; - if (servers[i].sin_addr.s_addr != primaryServer.sin_addr.s_addr || servers[i].sin_port != primaryServer.sin_port) { - AppendUniqueStunServer(secondaryServers, servers[i]); - } - } - - for (const auto& secondary : secondaryServers) { - StunProbeResponse secondResponse{}; - if (ProbeStunServer(descriptor, secondary, timeoutMs, secondResponse)) { + for (const auto& server : servers) { + StunEndpoint mapped{}; + if (ProbeStunServer(descriptor, server, timeoutMs, mapped)) { CloseStunSocket(descriptor); result.stun_status = NET_CTL_STUN_STATUS_OK; - result.nat_type = (secondResponse.mapped.address == firstResponse.mapped.address && - secondResponse.mapped.port == firstResponse.mapped.port) - ? NET_CTL_NAT_TYPE_2 - : NET_CTL_NAT_TYPE_3; - result.mapped_addr = firstResponse.mapped.address; + result.nat_type = NET_CTL_NAT_TYPE_NONE; + result.mapped_addr = mapped.address; return result; } } diff --git a/core/libs/tests/GuestNetCtl.cpp b/core/libs/tests/GuestNetCtl.cpp index eb81b9e64c..5965733866 100644 --- a/core/libs/tests/GuestNetCtl.cpp +++ b/core/libs/tests/GuestNetCtl.cpp @@ -257,7 +257,7 @@ void AppendStunAddressAttribute(std::vector& packet, std::uint16_t packet.insert(packet.end(), addrBytes, addrBytes + 4); } -sockaddr_in ReplyStunBinding(TestSocket serverSocket, std::uint32_t mappedAddr, std::uint16_t mappedHostPort, bool xorMapped, const sockaddr_in* otherServer = nullptr) { +void ReplyStunBinding(TestSocket serverSocket, std::uint32_t mappedAddr, std::uint16_t mappedHostPort, bool xorMapped) { Require(WaitReadable(serverSocket, 2000)); std::uint8_t request[64] = {}; sockaddr_in client{}; @@ -281,16 +281,12 @@ sockaddr_in ReplyStunBinding(TestSocket serverSocket, std::uint32_t mappedAddr, response[1] = 0x01; std::memcpy(response.data() + 4, request + 4, 16); AppendStunAddressAttribute(response, xorMapped ? 0x0020 : 0x0001, effectiveAddr, effectivePort, xorMapped); - if (otherServer) { - AppendStunAddressAttribute(response, 0x802b, otherServer->sin_addr.s_addr, ntohs(otherServer->sin_port), false); - } const auto attrLen = static_cast(response.size() - 20); response[2] = static_cast(attrLen >> 8); response[3] = static_cast(attrLen & 0xFF); const auto sent = sendto(serverSocket, reinterpret_cast(response.data()), static_cast(response.size()), 0, reinterpret_cast(&client), clientLen); Require(sent == static_cast(response.size())); - return client; } void CheckNatDiscovery(in_addr address) { @@ -322,65 +318,44 @@ void CheckNatDiscovery(in_addr address) { Require(recvfrom(server1, reinterpret_cast(drained), sizeof(drained), 0, reinterpret_cast(&from), &fromLen) == 20); } - NetCtlSetStunServers_nid_no_patch(&server1Addr, 1, 1000); - std::thread openWorker([&] { - ReplyStunBinding(server1, address.s_addr, 0, true); - }); - natInfo = {sizeof(natInfo), 0, 0, 0}; - Require(sceNetCtlGetNatInfo(&natInfo) == 0); - openWorker.join(); - Require(natInfo.size == sizeof(natInfo)); - Require(natInfo.stun_status == 2); - Require(natInfo.nat_type == 1); - Require(natInfo.mapped_addr == address.s_addr); - const std::uint32_t externalIp = (ntohl(address.s_addr) == 0xCB00712Au) ? htonl(0xCB00712Bu) : htonl(0xCB00712Au); - NetCtlSetStunServers_nid_no_patch(&server1Addr, 1, 1000); - std::thread incompleteWorker([&] { - ReplyStunBinding(server1, externalIp, 54321, true); - }); - natInfo = {sizeof(natInfo), 99, 99, 0xFFFFFFFFu}; - Require(sceNetCtlGetNatInfo(&natInfo) == 0); - incompleteWorker.join(); - Require(natInfo.size == sizeof(natInfo)); - Require(natInfo.stun_status == 1); - Require(natInfo.nat_type == 0); - Require(natInfo.mapped_addr == 0); - const sockaddr_in bothServers[2] = {server1Addr, server2Addr}; - NetCtlSetStunServers_nid_no_patch(bothServers, 2, 1000); - std::thread type2Worker([&] { - const auto firstClient = ReplyStunBinding(server1, externalIp, 54321, true); - const auto secondClient = ReplyStunBinding(server2, externalIp, 54321, true); - Require(firstClient.sin_addr.s_addr == secondClient.sin_addr.s_addr); - Require(firstClient.sin_port == secondClient.sin_port); + NetCtlSetStunServers_nid_no_patch(bothServers, 2, 40); + std::thread fallbackWorker([&] { + Require(WaitReadable(server1, 2000)); + std::uint8_t dropped[64] = {}; + sockaddr_in from{}; +#ifdef _WIN32 + int fromLen = sizeof(from); +#else + socklen_t fromLen = sizeof(from); +#endif + Require(recvfrom(server1, reinterpret_cast(dropped), sizeof(dropped), 0, reinterpret_cast(&from), &fromLen) == 20); + ReplyStunBinding(server2, externalIp, 54321, true); }); - natInfo = {sizeof(natInfo), 0, 0, 0}; + natInfo = {sizeof(natInfo), 99, 99, 0}; Require(sceNetCtlGetNatInfo(&natInfo) == 0); - type2Worker.join(); + fallbackWorker.join(); Require(natInfo.size == sizeof(natInfo)); Require(natInfo.stun_status == 2); - Require(natInfo.nat_type == 2); + Require(natInfo.nat_type == 0); Require(natInfo.mapped_addr == externalIp); - NetCtlNatInfo cachedInfo{sizeof(cachedInfo), 0, 0, 0}; + NetCtlNatInfo cachedInfo{sizeof(cachedInfo), 99, 99, 0}; Require(sceNetCtlGetNatInfo(&cachedInfo) == 0); - Require(cachedInfo.stun_status == 2 && cachedInfo.nat_type == 2 && cachedInfo.mapped_addr == externalIp); + Require(cachedInfo.stun_status == 2 && cachedInfo.nat_type == 0 && cachedInfo.mapped_addr == externalIp); NetCtlSetStunServers_nid_no_patch(&server1Addr, 1, 1000); - std::thread type3Worker([&] { - const auto firstClient = ReplyStunBinding(server1, externalIp, 54321, false, &server2Addr); - const auto secondClient = ReplyStunBinding(server2, externalIp, 54322, false); - Require(firstClient.sin_addr.s_addr == secondClient.sin_addr.s_addr); - Require(firstClient.sin_port == secondClient.sin_port); + std::thread legacyWorker([&] { + ReplyStunBinding(server1, address.s_addr, 0, false); }); - natInfo = {sizeof(natInfo), 0, 0, 0}; + natInfo = {sizeof(natInfo), 99, 99, 0}; Require(sceNetCtlGetNatInfo(&natInfo) == 0); - type3Worker.join(); + legacyWorker.join(); Require(natInfo.size == sizeof(natInfo)); Require(natInfo.stun_status == 2); - Require(natInfo.nat_type == 3); - Require(natInfo.mapped_addr == externalIp); + Require(natInfo.nat_type == 0); + Require(natInfo.mapped_addr == address.s_addr); NetCtlSetStunServers_nid_no_patch(nullptr, 0, 0); CloseTestSocket(server1); diff --git a/docs/dev/debt/libSceNetCtl.md b/docs/dev/debt/libSceNetCtl.md index dcac3f5120..1bc4e74831 100644 --- a/docs/dev/debt/libSceNetCtl.md +++ b/docs/dev/debt/libSceNetCtl.md @@ -18,4 +18,4 @@ - [sceNetCtlGetInfo](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) finds the host's default route in `/proc/net/route` on Linux and, on macOS, in the routing table read with `sysctl(NET_RT_FLAGS, RTF_GATEWAY)`, where an interface-scoped default route ranks after the unscoped one - [sceNetCtlGetIfStat](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) throws on macOS: an unprivileged process reads the interface byte counters (`getifaddrs` and `sysctl(NET_RT_IFLIST2)` alike) reduced to their low 32 bits, so a total would wrap every 4 GiB - [Host interface cache](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - on Windows, `sceNetCtlGetState`, `sceNetCtlGetStateV6`, `sceNetCtlCheckCallback`, `sceNetCtlGetIfStat` and `sceNetCtlGetInfo` reuse the last host interface query until `NotifyUnicastIpAddressChange`, `NotifyIpInterfaceChange` or `NotifyRouteChange2` reports a change, instead of calling `GetAdaptersAddresses` on every call. A DNS server change with no address, interface or route change is seen only at the next such change. If the notifications cannot be registered, every call queries the host as before. -- [sceNetCtlGetNatInfo](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - performs RFC 5389 STUN binding probes from the active host IPv4 interface against `stun.playstation.net` and fallback public STUN servers (`stun.cloudflare.com`, `stun.l.google.com`, `stun1.l.google.com`) instead of reading cached state from the console's background `netctl` service. Reports NAT type 1 when the mapped IPv4 endpoint matches the local socket, NAT type 2 when a second STUN endpoint reports the same mapped address and port, NAT type 3 when the second endpoint reports a different mapping, and STUN status 1 (`FAILED`) with NAT type 0 when STUN discovery or classification does not complete. +- [sceNetCtlGetNatInfo](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - performs RFC 5389 STUN binding probes from the active host IPv4 interface against `stun.playstation.net` and fallback public STUN servers (`stun.cloudflare.com`, `stun.l.google.com`, `stun1.l.google.com`) instead of reading cached state from the console's background `netctl` service. Reports STUN status 2 and the discovered external IPv4 address in `mapped_addr` on a valid binding response, or STUN status 1 (`FAILED`) and `mapped_addr` 0 when no server responds; `nat_type` is left at 0 because PlayStation's NAT classification procedure and field semantics are unverified on hardware. From 2e4ce132824908d87c63f38c543e6748f15de71d Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sun, 25 Oct 2026 02:52:39 +0000 Subject: [PATCH 030/198] feat(libScePad): implement scePadIsValidHandle A handle is valid for its own port type while the handle table that scePadGetHandle reads holds it open: a standard port that is open, a special slot that is in use, or the open remote port. Any other port type answers false. --- core/libs/prx/libScePad/Export.cpp | 7 +++++-- core/libs/tests/GuestPad.cpp | 22 ++++++++++++++++++++++ docs/dev/debt/libScePad.md | 1 + 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index 62c285f7bc..526396a0b2 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -964,8 +964,11 @@ int APS5_VABI scePadGetSphereRadius(std::int32_t handle, float* radius) { } bool APS5_VABI scePadIsValidHandle(std::int32_t handle, std::int32_t portType) { - NotImplemented_nid_no_patch(__func__); - return 0; + if (portType == PAD_PORT_TYPE_SPECIAL) return SpecialHandle(handle); + if (portType == PAD_PORT_TYPE_REMOTE) return RemoteHandle(handle); + if (portType != PAD_PORT_TYPE_STANDARD) return false; + const int port = PortOf(handle); + return port >= 0 && g_opened[port]; } bool APS5_VABI scePadIsDS4Connected(std::int32_t handle) { diff --git a/core/libs/tests/GuestPad.cpp b/core/libs/tests/GuestPad.cpp index 6759b34b16..7e25fa5b27 100644 --- a/core/libs/tests/GuestPad.cpp +++ b/core/libs/tests/GuestPad.cpp @@ -425,6 +425,24 @@ static void CheckSystemRemote(int handle) { Require(scePadReadState(handle, &data) == PAD_OK); } +static void CheckIsValidHandle(int user, int handle) { + Require(scePadIsValidHandle(handle, PAD_PORT_TYPE_STANDARD)); + for (const int type : {PAD_PORT_TYPE_SPECIAL, PAD_PORT_TYPE_REMOTE, 1, 3, 4, 5, -1}) Require(!scePadIsValidHandle(handle, type)); + for (const int other : {handle + 1, PAD_HANDLE_BASE + PAD_MAX_CONTROLLERS, 0, -1, PAD_SPECIAL_HANDLE}) Require(!scePadIsValidHandle(other, PAD_PORT_TYPE_STANDARD)); + const int special = scePadOpen_nid_postfix(user, PAD_PORT_TYPE_SPECIAL, 0, nullptr); + Require(special >= PAD_SPECIAL_HANDLE); + Require(scePadIsValidHandle(special, PAD_PORT_TYPE_SPECIAL) && !scePadIsValidHandle(special, PAD_PORT_TYPE_STANDARD)); + Require(!scePadIsValidHandle(special + 1, PAD_PORT_TYPE_SPECIAL)); + Require(scePadClose_nid_postfix(special) == PAD_OK); + Require(!scePadIsValidHandle(special, PAD_PORT_TYPE_SPECIAL)); + Require(!scePadIsValidHandle(PAD_REMOTE_HANDLE, PAD_PORT_TYPE_REMOTE)); + const int remote = scePadOpen_nid_postfix(PAD_USER_ID_SYSTEM, PAD_PORT_TYPE_REMOTE, 0, nullptr); + Require(remote == PAD_REMOTE_HANDLE); + Require(scePadIsValidHandle(remote, PAD_PORT_TYPE_REMOTE) && !scePadIsValidHandle(remote, PAD_PORT_TYPE_STANDARD)); + Require(scePadClose_nid_postfix(remote) == PAD_OK); + Require(!scePadIsValidHandle(remote, PAD_PORT_TYPE_REMOTE)); +} + static void CheckAccessibilityControllerState(int handle) { std::uint32_t state = 7; Require(scePadGetAccessibilityControllerState(handle + 1, nullptr) == PAD_ERROR_INVALID_ARG); @@ -634,6 +652,7 @@ int main() { CheckRemoteController(handle); CheckTrackerDevices(handle); CheckAccessibilityControllerState(handle); + CheckIsValidHandle(user, handle); CheckControllerType(handle); CheckInjectedState(commandPath); CheckInjectedQueueTimes(commandPath); @@ -658,6 +677,7 @@ int main() { Require(scePadGetHandle(user, 0, 1) == noHandle); Require(scePadClose_nid_postfix(handle) == 0); Require(scePadGetHandle(user, 0, 0) == noHandle); + Require(!scePadIsValidHandle(handle, PAD_PORT_TYPE_STANDARD)); Require(scePadUnknown_n3kSX62fgNo() == PAD_OK); Require(scePadSetVibrationTriggerEffectWeakWhileEmbeddedMicInUse(true) == 0); Require(scePadSetAngularVelocityDeadbandState(handle, false) == 0); @@ -676,6 +696,7 @@ int main() { const int handle2 = scePadOpen_nid_postfix(user2, 0, 0, nullptr); Require(handle2 == PAD_HANDLE_BASE + 1); Require(scePadGetHandle(user2, 0, 0) == handle2); + Require(scePadIsValidHandle(handle2, PAD_PORT_TYPE_STANDARD) && scePadIsValidHandle(handle, PAD_PORT_TYPE_STANDARD) == false); PadInputState p2Input{}; p2Input.buttons = static_cast(Pad::PadButton::Triangle); @@ -701,6 +722,7 @@ int main() { Require(scePadClose_nid_postfix(handle2) == 0); Require(scePadGetHandle(user2, 0, 0) == noHandle); + Require(!scePadIsValidHandle(handle2, PAD_PORT_TYPE_STANDARD)); Require(scePadSetAngularVelocityDeadbandState(handle2, false) == PAD_OK); Require(scePadSetAngularVelocityDeadbandState(handle2 + 1, false) == PAD_ERROR_INVALID_HANDLE); } diff --git a/docs/dev/debt/libScePad.md b/docs/dev/debt/libScePad.md index df2e0c6004..dfc7193b21 100644 --- a/docs/dev/debt/libScePad.md +++ b/docs/dev/debt/libScePad.md @@ -22,6 +22,7 @@ - [scePadSetVibrationMode](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - modes 1 to 3 and 5 are accepted and stored. On a PS5 (firmware 13.60) the call returns `0x80920005` before `scePadInit`, then rejects a mode of 0 or below with `INVALID_ARG` before it checks the handle, and returns `INVALID_HANDLE` for a bad handle with any higher mode; whether an open handle accepts modes above 3 was not measured, since the payload could not open a pad; 5 is accepted because PPSA25286 passes it on its open handle at start-up; the meanings of modes 3 and 5 are unknown (PPSA21480 calls it with 3 right after `scePadOpen` and aborts on failure; [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Pad/Pad.cs) names only 1 and 2). The stored mode has no other effect - [scePadIsRemoteController](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the name hashes to NID `fCWdlnmB1Ks`; the signature `(int handle, bool* remote)` comes from PPSA16064, which zeroes a byte, passes its address after `scePadGetControllerInformation` and ignores the result. The error codes follow the other pad calls. Controllers are local devices, so the answer is always false - [scePadIsDS4Connected, scePadIsBlasterConnected and scePadIsMoveConnected](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the `bool (int handle)` signatures are recovered; the library reports one DualSense to every title (see Touchpad resolution below), so no DualShock 4, Blaster or Move controller is ever behind a handle and each call answers false for any handle. The console's answer for an invalid handle is not measured +- [scePadIsValidHandle](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the `bool (int handle, int portType)` signature is recovered, and the answer follows the handle table `scePadGetHandle` reads. A standard handle is valid while its port is open, a special handle while its user's slot is open, and the remote handle while the system user's remote port is open, each only for its own port type (0, 2 or 16); any other port type answers false. A closed standard handle is not valid here, although the light bar, vibration and other setters keep accepting it. The console's answers are not measured - [scePadSetAngularVelocityBiasCorrectionState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - inferred signature `(int handle, bool enabled)` follows `scePadSetTiltCorrectionState` and `scePadSetAngularVelocityDeadbandState`; an invalid handle returns `PAD_ERROR_INVALID_HANDLE`, disabling is a no-op, and enabling throws because bias correction is not modelled - [scePadGetAccessibilityControllerState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the value a console reports for a DualSense is not measured; the emulated pad, which is not an Access controller, reports 0. The name hashes to NID `zBsgF0q8DIM`. In the OS 10.01 system module the `(int handle, uint32_t* state)` function returns `0x80920005` before `scePadInit`, then `INVALID_ARG` for a NULL `state` and `INVALID_HANDLE` for a bad handle, and otherwise asks the pad driver for a per-device value of the device behind the handle: 0 when no device is behind it, 1 when the driver has no answer, else the driver's value. What the values mean is unknown - [scePadGetControllerType](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the value a console reports for a DualSense is not measured; the emulated pad reports `PAD_CONTROLLER_TYPE_DUAL_SENSE` (2), following [duaLib](https://github.com/WujekFoliarz/duaLib/blob/master/src/include/duaLib.h)'s `s_SceControllerType` enum where `DUALSENSE = 2`. The signature `(int handle, int32_t* type)` follows duaLib, while the validation order (checking initialization, null pointer, then handle) follows the library's other pad queries; duaLib does not validate null pointers and returns `PAD_ERROR_DEVICE_NOT_CONNECTED` for an invalid pad. From 3921b0bd5f71b0ee09b0a6b00cda3fd963725314 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sun, 25 Oct 2026 02:53:18 +0000 Subject: [PATCH 031/198] feat(libScePad): reset every light bar in scePadResetLightBarAll scePadResetLightBarAll returns each standard port's light bar to its default, as scePadResetLightBar does for one handle. scePadResetLightBarAllByPortType does the same for the standard port type and nothing for the special one. It throws for types 3, 4 and 16, and rejects other types with INVALID_ARG, as scePadOpen does. --- core/libs/prx/libScePad/Export.cpp | 11 ++++--- core/libs/tests/GuestPad.cpp | 46 ++++++++++++++++++++++++++++++ docs/dev/debt/libScePad.md | 1 + 3 files changed, 54 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index 526396a0b2..71ba80c354 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -914,13 +914,16 @@ int APS5_VABI scePadGetInfoByPortType(std::int32_t type, PadPortInfo* info) { } int APS5_VABI scePadResetLightBarAll(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + for (int port = 0; port < PAD_MAX_CONTROLLERS; ++port) Pad::SetLightBar(port, false, 0, 0, 0); + return PAD_OK; } int APS5_VABI scePadResetLightBarAllByPortType(std::uint32_t portType) { - NotImplemented_nid_no_patch(__func__); - return 0; + if (portType == 3 || portType == 4 || portType == PAD_PORT_TYPE_REMOTE) + NotImplemented_nid_no_patch("scePadResetLightBarAllByPortType: port types 3, 4 and 16"); + if (portType == PAD_PORT_TYPE_SPECIAL) return PAD_OK; + if (portType != PAD_PORT_TYPE_STANDARD) return PAD_ERROR_INVALID_ARG; + return scePadResetLightBarAll(); } int APS5_VABI scePadSetLightBarForTracker(std::int32_t handle, const PadLightBarParam* color) { diff --git a/core/libs/tests/GuestPad.cpp b/core/libs/tests/GuestPad.cpp index 7e25fa5b27..1eb616b5e5 100644 --- a/core/libs/tests/GuestPad.cpp +++ b/core/libs/tests/GuestPad.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -443,6 +444,48 @@ static void CheckIsValidHandle(int user, int handle) { Require(!scePadIsValidHandle(remote, PAD_PORT_TYPE_REMOTE)); } +static PadOutputState OutputOf(int port) { + std::uint32_t seen = 0xffffffffu; + PadOutputState output; + Require(PadFetchOutputForPort_nid_postfix(port, &seen, &output)); + return output; +} + +static bool LightBarIs(int port, bool valid, std::uint8_t r, std::uint8_t g, std::uint8_t b) { + const PadOutputState output = OutputOf(port); + return output.lightBarValid == valid && output.lightBar[0] == r && output.lightBar[1] == g && output.lightBar[2] == b; +} + +static bool ResetByPortTypeThrows(std::uint32_t portType) { + try { + scePadResetLightBarAllByPortType(portType); + } catch (const std::exception&) { + return true; + } + return false; +} + +static void CheckLightBarResets(int handle) { + const PadLightBarParam color{10, 20, 30}; + const auto setBoth = [&] { + Require(scePadSetLightBar(handle, &color) == PAD_OK); + Pad::SetLightBar(2, true, 40, 50, 60); + Require(LightBarIs(0, true, 10, 20, 30) && LightBarIs(2, true, 40, 50, 60)); + }; + setBoth(); + Require(scePadResetLightBarAll() == PAD_OK); + for (int port = 0; port < PAD_MAX_CONTROLLERS; ++port) Require(LightBarIs(port, false, 0, 0, 0)); + Require(scePadResetLightBarAll() == PAD_OK); + Require(LightBarIs(0, false, 0, 0, 0)); + setBoth(); + Require(scePadResetLightBarAllByPortType(PAD_PORT_TYPE_SPECIAL) == PAD_OK); + for (const std::uint32_t type : {1u, 5u, 6u, 0xffu, 0xffffffffu}) Require(scePadResetLightBarAllByPortType(type) == PAD_ERROR_INVALID_ARG); + for (const std::uint32_t type : {3u, 4u, 16u}) Require(ResetByPortTypeThrows(type)); + Require(LightBarIs(0, true, 10, 20, 30) && LightBarIs(2, true, 40, 50, 60)); + Require(scePadResetLightBarAllByPortType(PAD_PORT_TYPE_STANDARD) == PAD_OK); + for (int port = 0; port < PAD_MAX_CONTROLLERS; ++port) Require(LightBarIs(port, false, 0, 0, 0)); +} + static void CheckAccessibilityControllerState(int handle) { std::uint32_t state = 7; Require(scePadGetAccessibilityControllerState(handle + 1, nullptr) == PAD_ERROR_INVALID_ARG); @@ -653,6 +696,7 @@ int main() { CheckTrackerDevices(handle); CheckAccessibilityControllerState(handle); CheckIsValidHandle(user, handle); + CheckLightBarResets(handle); CheckControllerType(handle); CheckInjectedState(commandPath); CheckInjectedQueueTimes(commandPath); @@ -678,6 +722,8 @@ int main() { Require(scePadClose_nid_postfix(handle) == 0); Require(scePadGetHandle(user, 0, 0) == noHandle); Require(!scePadIsValidHandle(handle, PAD_PORT_TYPE_STANDARD)); + Require(scePadSetLightBar(handle, &lightBar) == PAD_OK && LightBarIs(0, true, 255, 128, 64)); + Require(scePadResetLightBarAll() == PAD_OK && LightBarIs(0, false, 0, 0, 0)); Require(scePadUnknown_n3kSX62fgNo() == PAD_OK); Require(scePadSetVibrationTriggerEffectWeakWhileEmbeddedMicInUse(true) == 0); Require(scePadSetAngularVelocityDeadbandState(handle, false) == 0); diff --git a/docs/dev/debt/libScePad.md b/docs/dev/debt/libScePad.md index dfc7193b21..52bb896e2a 100644 --- a/docs/dev/debt/libScePad.md +++ b/docs/dev/debt/libScePad.md @@ -23,6 +23,7 @@ - [scePadIsRemoteController](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the name hashes to NID `fCWdlnmB1Ks`; the signature `(int handle, bool* remote)` comes from PPSA16064, which zeroes a byte, passes its address after `scePadGetControllerInformation` and ignores the result. The error codes follow the other pad calls. Controllers are local devices, so the answer is always false - [scePadIsDS4Connected, scePadIsBlasterConnected and scePadIsMoveConnected](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the `bool (int handle)` signatures are recovered; the library reports one DualSense to every title (see Touchpad resolution below), so no DualShock 4, Blaster or Move controller is ever behind a handle and each call answers false for any handle. The console's answer for an invalid handle is not measured - [scePadIsValidHandle](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the `bool (int handle, int portType)` signature is recovered, and the answer follows the handle table `scePadGetHandle` reads. A standard handle is valid while its port is open, a special handle while its user's slot is open, and the remote handle while the system user's remote port is open, each only for its own port type (0, 2 or 16); any other port type answers false. A closed standard handle is not valid here, although the light bar, vibration and other setters keep accepting it. The console's answers are not measured +- [scePadResetLightBarAll and scePadResetLightBarAllByPortType](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the `(void)` and `(uint32_t portType)` signatures are recovered. Each resets every standard port's light bar to its default, as `scePadResetLightBar` does for one handle, whether or not a handle is open on it. Port type 2 returns `PAD_OK` without effect, as the special handles' light bar calls do. Types 3, 4 and 16 throw: `scePadOpen` cannot open ports of types 3 and 4, and the remote port has no light bar here. Any other type returns `INVALID_ARG`, as `scePadOpen` does. The console's answers are not measured - [scePadSetAngularVelocityBiasCorrectionState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - inferred signature `(int handle, bool enabled)` follows `scePadSetTiltCorrectionState` and `scePadSetAngularVelocityDeadbandState`; an invalid handle returns `PAD_ERROR_INVALID_HANDLE`, disabling is a no-op, and enabling throws because bias correction is not modelled - [scePadGetAccessibilityControllerState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the value a console reports for a DualSense is not measured; the emulated pad, which is not an Access controller, reports 0. The name hashes to NID `zBsgF0q8DIM`. In the OS 10.01 system module the `(int handle, uint32_t* state)` function returns `0x80920005` before `scePadInit`, then `INVALID_ARG` for a NULL `state` and `INVALID_HANDLE` for a bad handle, and otherwise asks the pad driver for a per-device value of the device behind the handle: 0 when no device is behind it, 1 when the driver has no answer, else the driver's value. What the values mean is unknown - [scePadGetControllerType](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the value a console reports for a DualSense is not measured; the emulated pad reports `PAD_CONTROLLER_TYPE_DUAL_SENSE` (2), following [duaLib](https://github.com/WujekFoliarz/duaLib/blob/master/src/include/duaLib.h)'s `s_SceControllerType` enum where `DUALSENSE = 2`. The signature `(int handle, int32_t* type)` follows duaLib, while the validation order (checking initialization, null pointer, then handle) follows the library's other pad queries; duaLib does not validate null pointers and returns `PAD_ERROR_DEVICE_NOT_CONNECTED` for an invalid pad. From 63ef18b4351c35124ccda8b3226e5c0cf49f7cb0 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sun, 25 Oct 2026 02:53:52 +0000 Subject: [PATCH 032/198] feat(libScePad): implement scePadSetVibrationForce as scePadSetVibration scePadSetVibrationForce takes the same handle and vibration parameter as scePadSetVibration and returns its result. No system vibration-off state exists here for it to override: scePadSetVibrationMode only stores its mode. --- core/libs/prx/libScePad/Export.cpp | 3 +-- core/libs/tests/GuestPad.cpp | 30 ++++++++++++++++++++++++++++++ docs/dev/debt/libScePad.md | 1 + 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index 71ba80c354..480723f26e 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -614,8 +614,7 @@ int APS5_VABI scePadShareOutputDataWithFunctionType(std::int32_t source, std::in } int APS5_VABI scePadSetVibrationForce(std::int32_t handle, const PadVibrationParam* param) { - NotImplemented_nid_no_patch(__func__); - return 0; + return scePadSetVibration(handle, param); } int APS5_VABI scePadGetVersionInfo(std::int32_t handle, PadVersionInfo* info) { diff --git a/core/libs/tests/GuestPad.cpp b/core/libs/tests/GuestPad.cpp index 1eb616b5e5..595e84a16e 100644 --- a/core/libs/tests/GuestPad.cpp +++ b/core/libs/tests/GuestPad.cpp @@ -486,6 +486,30 @@ static void CheckLightBarResets(int handle) { for (int port = 0; port < PAD_MAX_CONTROLLERS; ++port) Require(LightBarIs(port, false, 0, 0, 0)); } +static bool VibrationIs(int port, std::uint8_t large, std::uint8_t small) { + const PadOutputState output = OutputOf(port); + return output.vibrationLarge == large && output.vibrationSmall == small; +} + +static void CheckVibrationForce(int user, int handle) { + const PadVibrationParam strong{70, 80}; + const PadVibrationParam weak{5, 6}; + Require(scePadSetVibrationForce(handle, &strong) == PAD_OK && VibrationIs(0, 70, 80)); + Require(scePadSetVibration(handle, &weak) == PAD_OK && VibrationIs(0, 5, 6)); + Require(scePadSetVibrationForce(handle, &weak) == scePadSetVibration(handle, &weak) && VibrationIs(0, 5, 6)); + Require(scePadSetVibrationForce(handle, nullptr) == PAD_ERROR_INVALID_ARG && VibrationIs(0, 5, 6)); + Require(scePadSetVibrationForce(handle + 1, &strong) == PAD_ERROR_INVALID_HANDLE && VibrationIs(0, 5, 6)); + Require(scePadSetVibrationForce(-1, &strong) == PAD_ERROR_INVALID_HANDLE); + const int special = scePadOpen_nid_postfix(user, PAD_PORT_TYPE_SPECIAL, 0, nullptr); + Require(special >= PAD_SPECIAL_HANDLE); + Require(scePadSetVibrationForce(special, &strong) == PAD_OK && VibrationIs(0, 5, 6)); + Require(scePadSetVibrationForce(special, nullptr) == PAD_ERROR_INVALID_ARG); + Require(scePadClose_nid_postfix(special) == PAD_OK); + Require(scePadSetVibrationForce(special, &strong) == PAD_ERROR_INVALID_HANDLE); + const PadVibrationParam off{0, 0}; + Require(scePadSetVibrationForce(handle, &off) == PAD_OK && VibrationIs(0, 0, 0)); +} + static void CheckAccessibilityControllerState(int handle) { std::uint32_t state = 7; Require(scePadGetAccessibilityControllerState(handle + 1, nullptr) == PAD_ERROR_INVALID_ARG); @@ -697,6 +721,7 @@ int main() { CheckAccessibilityControllerState(handle); CheckIsValidHandle(user, handle); CheckLightBarResets(handle); + CheckVibrationForce(user, handle); CheckControllerType(handle); CheckInjectedState(commandPath); CheckInjectedQueueTimes(commandPath); @@ -724,6 +749,11 @@ int main() { Require(!scePadIsValidHandle(handle, PAD_PORT_TYPE_STANDARD)); Require(scePadSetLightBar(handle, &lightBar) == PAD_OK && LightBarIs(0, true, 255, 128, 64)); Require(scePadResetLightBarAll() == PAD_OK && LightBarIs(0, false, 0, 0, 0)); + const PadVibrationParam closedVibration{9, 10}; + Require(scePadSetVibrationForce(handle, &closedVibration) == scePadSetVibration(handle, &closedVibration) && VibrationIs(0, 9, 10)); + Require(scePadSetVibrationForce(handle + 1, &closedVibration) == PAD_ERROR_INVALID_HANDLE); + const PadVibrationParam closedOff{0, 0}; + Require(scePadSetVibrationForce(handle, &closedOff) == PAD_OK && VibrationIs(0, 0, 0)); Require(scePadUnknown_n3kSX62fgNo() == PAD_OK); Require(scePadSetVibrationTriggerEffectWeakWhileEmbeddedMicInUse(true) == 0); Require(scePadSetAngularVelocityDeadbandState(handle, false) == 0); diff --git a/docs/dev/debt/libScePad.md b/docs/dev/debt/libScePad.md index 52bb896e2a..3bde917520 100644 --- a/docs/dev/debt/libScePad.md +++ b/docs/dev/debt/libScePad.md @@ -24,6 +24,7 @@ - [scePadIsDS4Connected, scePadIsBlasterConnected and scePadIsMoveConnected](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the `bool (int handle)` signatures are recovered; the library reports one DualSense to every title (see Touchpad resolution below), so no DualShock 4, Blaster or Move controller is ever behind a handle and each call answers false for any handle. The console's answer for an invalid handle is not measured - [scePadIsValidHandle](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the `bool (int handle, int portType)` signature is recovered, and the answer follows the handle table `scePadGetHandle` reads. A standard handle is valid while its port is open, a special handle while its user's slot is open, and the remote handle while the system user's remote port is open, each only for its own port type (0, 2 or 16); any other port type answers false. A closed standard handle is not valid here, although the light bar, vibration and other setters keep accepting it. The console's answers are not measured - [scePadResetLightBarAll and scePadResetLightBarAllByPortType](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the `(void)` and `(uint32_t portType)` signatures are recovered. Each resets every standard port's light bar to its default, as `scePadResetLightBar` does for one handle, whether or not a handle is open on it. Port type 2 returns `PAD_OK` without effect, as the special handles' light bar calls do. Types 3, 4 and 16 throw: `scePadOpen` cannot open ports of types 3 and 4, and the remote port has no light bar here. Any other type returns `INVALID_ARG`, as `scePadOpen` does. The console's answers are not measured +- [scePadSetVibrationForce](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the `(int handle, const PadVibrationParam*)` signature is recovered. The call returns what `scePadSetVibration` returns for the same arguments and sets the same motors. Its name suggests it overrides a system vibration-off setting, which is not modelled here (`scePadSetVibrationMode` only stores its mode), so there is nothing to override. Whether the console differs in any other way is not measured - [scePadSetAngularVelocityBiasCorrectionState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - inferred signature `(int handle, bool enabled)` follows `scePadSetTiltCorrectionState` and `scePadSetAngularVelocityDeadbandState`; an invalid handle returns `PAD_ERROR_INVALID_HANDLE`, disabling is a no-op, and enabling throws because bias correction is not modelled - [scePadGetAccessibilityControllerState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the value a console reports for a DualSense is not measured; the emulated pad, which is not an Access controller, reports 0. The name hashes to NID `zBsgF0q8DIM`. In the OS 10.01 system module the `(int handle, uint32_t* state)` function returns `0x80920005` before `scePadInit`, then `INVALID_ARG` for a NULL `state` and `INVALID_HANDLE` for a bad handle, and otherwise asks the pad driver for a per-device value of the device behind the handle: 0 when no device is behind it, 1 when the driver has no answer, else the driver's value. What the values mean is unknown - [scePadGetControllerType](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the value a console reports for a DualSense is not measured; the emulated pad reports `PAD_CONTROLLER_TYPE_DUAL_SENSE` (2), following [duaLib](https://github.com/WujekFoliarz/duaLib/blob/master/src/include/duaLib.h)'s `s_SceControllerType` enum where `DUALSENSE = 2`. The signature `(int handle, int32_t* type)` follows duaLib, while the validation order (checking initialization, null pointer, then handle) follows the library's other pad queries; duaLib does not validate null pointers and returns `PAD_ERROR_DEVICE_NOT_CONNECTED` for an invalid pad. From ba3f730421e15747c603cd315dd4cfbe1734e042 Mon Sep 17 00:00:00 2001 From: Leonardo Date: Sun, 25 Oct 2026 06:34:41 +0100 Subject: [PATCH 033/198] fix(libSceNetCtl): use cryptographic entropy for STUN transaction IDs --- core/libs/prx/libSceNetCtl/Export.cpp | 8 +-- core/libs/tests/GuestNetCtl.cpp | 85 +++++++++++++++++++-------- 2 files changed, 64 insertions(+), 29 deletions(-) diff --git a/core/libs/prx/libSceNetCtl/Export.cpp b/core/libs/prx/libSceNetCtl/Export.cpp index 8adbcfe20b..385d0fc44e 100644 --- a/core/libs/prx/libSceNetCtl/Export.cpp +++ b/core/libs/prx/libSceNetCtl/Export.cpp @@ -30,6 +30,7 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/HostEntropy.hpp" #include #include #include @@ -622,18 +623,13 @@ static bool ParseStunBindingResponse(const std::uint8_t* packet, std::size_t siz return mapped.valid; } -static std::atomic g_stunTransactionCounter{1}; - static bool ProbeStunServer(StunSocket descriptor, const sockaddr_in& server, int timeoutMs, StunEndpoint& mapped) { std::uint8_t request[STUN_HEADER_SIZE] = {}; request[0] = static_cast(STUN_BINDING_REQUEST >> 8); request[1] = static_cast(STUN_BINDING_REQUEST & 0xFF); const std::uint32_t cookie = htonl(STUN_MAGIC_COOKIE); std::memcpy(request + 4, &cookie, sizeof(cookie)); - const std::uint64_t seq = g_stunTransactionCounter.fetch_add(1, std::memory_order_relaxed); - for (std::size_t i = 0; i < STUN_TRANSACTION_ID_SIZE; ++i) { - request[8 + i] = static_cast((seq * 0x9E3779B97F4A7C15ull >> ((i % 8) * 8)) ^ (i * 0x3Du)); - } + HostEntropy_nid_no_patch(request + 8, STUN_TRANSACTION_ID_SIZE); const auto sent = sendto(descriptor, reinterpret_cast(request), sizeof(request), 0, reinterpret_cast(&server), sizeof(server)); if (sent != static_cast(sizeof(request))) return false; diff --git a/core/libs/tests/GuestNetCtl.cpp b/core/libs/tests/GuestNetCtl.cpp index 5965733866..0029389fe4 100644 --- a/core/libs/tests/GuestNetCtl.cpp +++ b/core/libs/tests/GuestNetCtl.cpp @@ -16,6 +16,7 @@ #endif #include "prx/libc/include/general/VabiMacros.hpp" #include "SceTypes.hpp" +#include #include #include #include @@ -257,10 +258,11 @@ void AppendStunAddressAttribute(std::vector& packet, std::uint16_t packet.insert(packet.end(), addrBytes, addrBytes + 4); } -void ReplyStunBinding(TestSocket serverSocket, std::uint32_t mappedAddr, std::uint16_t mappedHostPort, bool xorMapped) { +using StunTxId = std::array; + +StunTxId ReceiveStunBindingRequest(TestSocket serverSocket, sockaddr_in& client) { Require(WaitReadable(serverSocket, 2000)); std::uint8_t request[64] = {}; - sockaddr_in client{}; #ifdef _WIN32 int clientLen = sizeof(client); #else @@ -272,28 +274,46 @@ void ReplyStunBinding(TestSocket serverSocket, std::uint32_t mappedAddr, std::ui Require(request[0] == 0x00 && request[1] == 0x01 && request[2] == 0x00 && request[3] == 0x00); const std::uint32_t expectedCookie = htonl(0x2112A442u); Require(std::memcmp(request + 4, &expectedCookie, sizeof(expectedCookie)) == 0); + StunTxId txId{}; + std::memcpy(txId.data(), request + 8, txId.size()); + Require(txId != StunTxId{}); + return txId; +} +void SendStunBindingResponse(TestSocket serverSocket, const sockaddr_in& client, const StunTxId& txId, std::uint32_t mappedAddr, std::uint16_t mappedHostPort, bool xorMapped) { const std::uint16_t effectivePort = mappedHostPort != 0 ? mappedHostPort : ntohs(client.sin_port); const std::uint32_t effectiveAddr = mappedAddr != 0 ? mappedAddr : client.sin_addr.s_addr; - std::vector response(20, 0); response[0] = 0x01; response[1] = 0x01; - std::memcpy(response.data() + 4, request + 4, 16); + const std::uint32_t cookie = htonl(0x2112A442u); + std::memcpy(response.data() + 4, &cookie, sizeof(cookie)); + std::memcpy(response.data() + 8, txId.data(), txId.size()); AppendStunAddressAttribute(response, xorMapped ? 0x0020 : 0x0001, effectiveAddr, effectivePort, xorMapped); const auto attrLen = static_cast(response.size() - 20); response[2] = static_cast(attrLen >> 8); response[3] = static_cast(attrLen & 0xFF); +#ifdef _WIN32 + const int clientLen = sizeof(client); +#else + const socklen_t clientLen = sizeof(client); +#endif const auto sent = sendto(serverSocket, reinterpret_cast(response.data()), static_cast(response.size()), 0, reinterpret_cast(&client), clientLen); Require(sent == static_cast(response.size())); } +void RecordUniqueTransactionId(std::vector& seen, const StunTxId& candidate) { + for (const auto& existing : seen) Require(existing != candidate); + seen.push_back(candidate); +} + void CheckNatDiscovery(in_addr address) { sockaddr_in server1Addr{}; sockaddr_in server2Addr{}; const TestSocket server1 = OpenTestUdpSocket(server1Addr); const TestSocket server2 = OpenTestUdpSocket(server2Addr); + std::vector seenIds; NetCtlNatInfo natInfo{}; natInfo.size = sizeof(natInfo); @@ -307,35 +327,50 @@ void CheckNatDiscovery(in_addr address) { Require(natInfo.nat_type == 0); Require(natInfo.mapped_addr == 0); { - Require(WaitReadable(server1, 2000)); - std::uint8_t drained[64] = {}; sockaddr_in from{}; -#ifdef _WIN32 - int fromLen = sizeof(from); -#else - socklen_t fromLen = sizeof(from); -#endif - Require(recvfrom(server1, reinterpret_cast(drained), sizeof(drained), 0, reinterpret_cast(&from), &fromLen) == 20); + RecordUniqueTransactionId(seenIds, ReceiveStunBindingRequest(server1, from)); } const std::uint32_t externalIp = (ntohl(address.s_addr) == 0xCB00712Au) ? htonl(0xCB00712Bu) : htonl(0xCB00712Au); + const std::uint32_t spoofedIp = htonl(0xC0000201u); + + NetCtlSetStunServers_nid_no_patch(&server1Addr, 1, 30); + StunTxId mismatchedOnlyTx{}; + std::thread mismatchOnlyWorker([&] { + sockaddr_in client{}; + mismatchedOnlyTx = ReceiveStunBindingRequest(server1, client); + StunTxId badTx = mismatchedOnlyTx; + badTx[0] ^= 0xFFu; + SendStunBindingResponse(server1, client, badTx, spoofedIp, 54321, true); + }); + natInfo = {sizeof(natInfo), 99, 99, 0xFFFFFFFFu}; + Require(sceNetCtlGetNatInfo(&natInfo) == 0); + mismatchOnlyWorker.join(); + RecordUniqueTransactionId(seenIds, mismatchedOnlyTx); + Require(natInfo.size == sizeof(natInfo)); + Require(natInfo.stun_status == 1); + Require(natInfo.nat_type == 0); + Require(natInfo.mapped_addr == 0); + const sockaddr_in bothServers[2] = {server1Addr, server2Addr}; NetCtlSetStunServers_nid_no_patch(bothServers, 2, 40); + StunTxId droppedTx{}; + StunTxId fallbackTx{}; std::thread fallbackWorker([&] { - Require(WaitReadable(server1, 2000)); - std::uint8_t dropped[64] = {}; - sockaddr_in from{}; -#ifdef _WIN32 - int fromLen = sizeof(from); -#else - socklen_t fromLen = sizeof(from); -#endif - Require(recvfrom(server1, reinterpret_cast(dropped), sizeof(dropped), 0, reinterpret_cast(&from), &fromLen) == 20); - ReplyStunBinding(server2, externalIp, 54321, true); + sockaddr_in droppedClient{}; + droppedTx = ReceiveStunBindingRequest(server1, droppedClient); + sockaddr_in client{}; + fallbackTx = ReceiveStunBindingRequest(server2, client); + StunTxId wrongTx = fallbackTx; + wrongTx[11] ^= 0xA5u; + SendStunBindingResponse(server2, client, wrongTx, spoofedIp, 54321, true); + SendStunBindingResponse(server2, client, fallbackTx, externalIp, 54321, true); }); natInfo = {sizeof(natInfo), 99, 99, 0}; Require(sceNetCtlGetNatInfo(&natInfo) == 0); fallbackWorker.join(); + RecordUniqueTransactionId(seenIds, droppedTx); + RecordUniqueTransactionId(seenIds, fallbackTx); Require(natInfo.size == sizeof(natInfo)); Require(natInfo.stun_status == 2); Require(natInfo.nat_type == 0); @@ -346,12 +381,16 @@ void CheckNatDiscovery(in_addr address) { Require(cachedInfo.stun_status == 2 && cachedInfo.nat_type == 0 && cachedInfo.mapped_addr == externalIp); NetCtlSetStunServers_nid_no_patch(&server1Addr, 1, 1000); + StunTxId legacyTx{}; std::thread legacyWorker([&] { - ReplyStunBinding(server1, address.s_addr, 0, false); + sockaddr_in client{}; + legacyTx = ReceiveStunBindingRequest(server1, client); + SendStunBindingResponse(server1, client, legacyTx, address.s_addr, 0, false); }); natInfo = {sizeof(natInfo), 99, 99, 0}; Require(sceNetCtlGetNatInfo(&natInfo) == 0); legacyWorker.join(); + RecordUniqueTransactionId(seenIds, legacyTx); Require(natInfo.size == sizeof(natInfo)); Require(natInfo.stun_status == 2); Require(natInfo.nat_type == 0); From 981e36205c751ff7d2b735775f9edfcbc3f25fb3 Mon Sep 17 00:00:00 2001 From: Bipin Date: Sat, 24 Oct 2026 15:39:49 +0530 Subject: [PATCH 034/198] feat(core): implement the Opus encoder libraries - libSceOpusCeltEnc and libSceOpusSilkEnc forward Initialize, Terminate, GetSize, Create, CreateEx, Encode, EncodeFloat and Destroy to bundled libopus with OPUS_APPLICATION_RESTRICTED_CELT and OPUS_APPLICATION_RESTRICTED_SILK - state pointers are host registry keys; nothing is written into guest memory, Create without CreateEx binds an internal context id, and a live CreateEx encoder makes Terminate throw - validation mirrors the recovered libSceOpusCeltDec order: channels before sample rate before application, offsets 0/1/2/3/6/7/8/9/13 - Ctl and the priority, timeout, batch and cancel exports still throw and are documented in libSceOpusCeltEnc.md and libSceOpusSilkEnc.md - new guest_opus_celt_enc and guest_opus_silk_enc tests require bit-exact packets against a raw libopus encoder with the same restricted application, celt-mode or silk-only TOC values, an alternating sine decoded through opus_decode, and the negative error paths --- .../libs/prx/libSceOpusCeltEnc/CMakeLists.txt | 16 ++ core/libs/prx/libSceOpusCeltEnc/Export.cpp | 96 +++++----- .../libSceOpusCeltEnc/include/OpusEncoder.hpp | 150 ++++++++++++++++ .../libs/prx/libSceOpusSilkEnc/CMakeLists.txt | 16 ++ core/libs/prx/libSceOpusSilkEnc/Export.cpp | 120 ++++++++----- core/libs/tests/GuestOpusCeltEnc.cpp | 155 ++++++++++++++++ core/libs/tests/GuestOpusSilkEnc.cpp | 167 ++++++++++++++++++ docs/dev/debt/libSceOpusCeltEnc.md | 10 +- docs/dev/debt/libSceOpusSilkEnc.md | 10 +- 9 files changed, 643 insertions(+), 97 deletions(-) create mode 100644 core/libs/prx/libSceOpusCeltEnc/CMakeLists.txt create mode 100644 core/libs/prx/libSceOpusCeltEnc/include/OpusEncoder.hpp create mode 100644 core/libs/prx/libSceOpusSilkEnc/CMakeLists.txt create mode 100644 core/libs/tests/GuestOpusCeltEnc.cpp create mode 100644 core/libs/tests/GuestOpusSilkEnc.cpp diff --git a/core/libs/prx/libSceOpusCeltEnc/CMakeLists.txt b/core/libs/prx/libSceOpusCeltEnc/CMakeLists.txt new file mode 100644 index 0000000000..c988c3c633 --- /dev/null +++ b/core/libs/prx/libSceOpusCeltEnc/CMakeLists.txt @@ -0,0 +1,16 @@ +add_library(libSceOpusCeltEnc SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp) +target_include_directories(libSceOpusCeltEnc PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(libSceOpusCeltEnc PRIVATE opus) +configure_windows_unwind(libSceOpusCeltEnc) + +if(BUILD_TESTING) + add_test_executable(guest_opus_celt_enc_tests ${LIBS_INCLUDE_DIR}/tests/GuestOpusCeltEnc.cpp) + target_include_directories(guest_opus_celt_enc_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_opus_celt_enc_tests PRIVATE libSceOpusCeltEnc libc opus) + configure_windows_unwind(guest_opus_celt_enc_tests) + add_test(NAME guest_opus_celt_enc COMMAND guest_opus_celt_enc_tests) + if(WIN32) + set_tests_properties(guest_opus_celt_enc PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceOpusCeltEnc/Export.cpp b/core/libs/prx/libSceOpusCeltEnc/Export.cpp index 5f837e73e4..e19efee478 100644 --- a/core/libs/prx/libSceOpusCeltEnc/Export.cpp +++ b/core/libs/prx/libSceOpusCeltEnc/Export.cpp @@ -1,64 +1,68 @@ #include "prx/libSceOpusCeltEnc/include/Api.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libSceOpusCeltEnc/include/OpusEncoder.hpp" #include -#include -#include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { + +OpusEncoderLibrary library("OpusCeltEnc", OPUS_APPLICATION_RESTRICTED_CELT, 0x81400000u); + +} // namespace + extern "C" { -int APS5_VABI sceOpusCeltEncCreate(OpusCeltEncoder* encoder, std::int32_t sampleRate, std::int32_t channels, std::int32_t application) { +int APS5_VABI sceOpusCeltEncInitialize(std::uint32_t* context) { return library.Initialize(context); } + +int APS5_VABI sceOpusCeltEncTerminate(const std::uint32_t* context) { return library.Terminate(context); } + +int APS5_VABI sceOpusCeltEncGetSize(int channels) { return library.GetSize(channels); } + +int APS5_VABI sceOpusCeltEncCreate(OpusCeltEncoder* state, int sampleRate, int channels, int application) { + return library.Create(state, sampleRate, channels, application); +} + +int APS5_VABI sceOpusCeltEncCreateEx(const std::uint32_t* context, OpusCeltEncoder* state, int sampleRate, int channels, int application) { + return library.CreateEx(context, state, sampleRate, channels, application); +} + +int APS5_VABI sceOpusCeltEncEncode(OpusCeltEncoder* state, const std::int16_t* samples, int frameSize, std::uint8_t* data, int maxDataBytes) { + return library.Encode(state, samples, frameSize, data, maxDataBytes); +} + +int APS5_VABI sceOpusCeltEncEncodeFloat(OpusCeltEncoder* state, const float* samples, int frameSize, std::uint8_t* data, int maxDataBytes) { + return library.EncodeFloat(state, samples, frameSize, data, maxDataBytes); +} + +int APS5_VABI sceOpusCeltEncDestroy(OpusCeltEncoder* state) { return library.Destroy(state); } + +int APS5_VABI sceOpusCeltEncCtl(OpusCeltEncoder* state, int request, ...) { + (void)state; + (void)request; NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceOpusCeltEncCreateEx(const std::uint32_t* contextId, OpusCeltEncoder* encoder, std::int32_t sampleRate, std::int32_t channels, std::int32_t application) { +int APS5_VABI sceOpusCeltEncEncodeWithPriority(OpusCeltEncoder* state, const std::int16_t* samples, int frameSize, std::uint8_t* data, int maxDataBytes, std::uint32_t priority) { + (void)state; + (void)samples; + (void)frameSize; + (void)data; + (void)maxDataBytes; + (void)priority; NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceOpusCeltEncCtl(OpusCeltEncoder* encoder, std::int32_t request, ...) { +int APS5_VABI sceOpusCeltEncEncodeFloatWithPriority(OpusCeltEncoder* state, const float* samples, int frameSize, std::uint8_t* data, int maxDataBytes, std::uint32_t priority) { + (void)state; + (void)samples; + (void)frameSize; + (void)data; + (void)maxDataBytes; + (void)priority; NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceOpusCeltEncDestroy(OpusCeltEncoder* encoder) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceOpusCeltEncEncode(OpusCeltEncoder* encoder, const std::int16_t* samples, std::int32_t frameSize, std::uint8_t* data, std::int32_t maxDataBytes) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceOpusCeltEncEncodeFloat(OpusCeltEncoder* encoder, const float* samples, std::int32_t frameSize, std::uint8_t* data, std::int32_t maxDataBytes) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceOpusCeltEncEncodeFloatWithPriority(OpusCeltEncoder* encoder, const float* samples, std::int32_t frameSize, std::uint8_t* data, std::int32_t maxDataBytes, std::uint32_t priority) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceOpusCeltEncEncodeWithPriority(OpusCeltEncoder* encoder, const std::int16_t* samples, std::int32_t frameSize, std::uint8_t* data, std::int32_t maxDataBytes, std::uint32_t priority) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceOpusCeltEncGetSize(std::int32_t channels) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceOpusCeltEncInitialize(std::uint32_t* contextId) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceOpusCeltEncTerminate(const std::uint32_t* contextId) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -} +} // extern "C" diff --git a/core/libs/prx/libSceOpusCeltEnc/include/OpusEncoder.hpp b/core/libs/prx/libSceOpusCeltEnc/include/OpusEncoder.hpp new file mode 100644 index 0000000000..362c7f4b1a --- /dev/null +++ b/core/libs/prx/libSceOpusCeltEnc/include/OpusEncoder.hpp @@ -0,0 +1,150 @@ +#ifndef CORE_LIBS_PRX_LIBSCEOPUSCELTENC_INCLUDE_OPUSENCODER_HPP +#define CORE_LIBS_PRX_LIBSCEOPUSCELTENC_INCLUDE_OPUSENCODER_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace { + +class OpusEncoderLibrary { +public: + OpusEncoderLibrary(const char* libraryName, int restrictedApplication, std::uint32_t base) : name(libraryName), application(restrictedApplication), errorBase(base) {} + + int Initialize(std::uint32_t* context) { + if (!context) return error(13); + std::lock_guard lock(mutex); + for (const auto& [id, owner] : contexts) + if (owner == context) fail("context already initialized"); + if (nextContext == 0) fail("context identifiers exhausted"); + const auto id = nextContext++; + contexts.emplace(id, context); + *context = id; + return 0; + } + + int Terminate(const std::uint32_t* context) { + if (!context) fail("null context"); + std::lock_guard lock(mutex); + if (!contexts.contains(*context)) fail("unknown context"); + for (const auto& [state, encoder] : encoders) + if (encoder->context == *context) fail("termination with live encoders is not implemented"); + contexts.erase(*context); + return 0; + } + + int GetSize(int channels) { + if (channels != 1 && channels != 2) return error(3); + return opus_encoder_get_size(channels); + } + + int Create(void* state, int sampleRate, int channels, int createApplication) { + if (!state) return error(0); + if (channels != 1 && channels != 2) return error(3); + if (sampleRate != 8000 && sampleRate != 12000 && sampleRate != 16000 && sampleRate != 24000 && sampleRate != 48000) return error(2); + if (createApplication != application) return error(2); + std::lock_guard lock(mutex); + if (encoders.contains(state)) fail("state already owns an encoder"); + Make(state, 0, sampleRate, channels); + return 0; + } + + int CreateEx(const std::uint32_t* context, void* state, int sampleRate, int channels, int createApplication) { + if (!context || !state) return error(0); + if (channels != 1 && channels != 2) return error(3); + if (sampleRate != 8000 && sampleRate != 12000 && sampleRate != 16000 && sampleRate != 24000 && sampleRate != 48000) return error(2); + if (createApplication != application) return error(2); + std::lock_guard lock(mutex); + if (!contexts.contains(*context)) fail("unknown context"); + if (encoders.contains(state)) fail("state already owns an encoder"); + Make(state, *context, sampleRate, channels); + return 0; + } + + int Encode(void* state, const std::int16_t* samples, int frameSize, std::uint8_t* data, int maxDataBytes) { + if (!state) return error(0); + if (frameSize != 0 && !samples) return error(6); + if (frameSize < 0) return error(7); + if (!data) return error(8); + if (maxDataBytes < 0) return error(9); + std::lock_guard lock(mutex); + const auto found = encoders.find(state); + if (found == encoders.end()) return error(1); + if (frameSize == 0) fail("a zero-frame encode is not implemented"); + const int bytes = opus_encode(found->second->codec.get(), samples, frameSize, data, maxDataBytes); + if (bytes < 0) fail("libopus rejected the frame"); + return bytes; + } + + int EncodeFloat(void* state, const float* samples, int frameSize, std::uint8_t* data, int maxDataBytes) { + if (!state) return error(0); + if (frameSize != 0 && !samples) return error(6); + if (frameSize < 0) return error(7); + if (!data) return error(8); + if (maxDataBytes < 0) return error(9); + std::lock_guard lock(mutex); + const auto found = encoders.find(state); + if (found == encoders.end()) return error(1); + if (frameSize == 0) fail("a zero-frame encode is not implemented"); + const int bytes = opus_encode_float(found->second->codec.get(), samples, frameSize, data, maxDataBytes); + if (bytes < 0) fail("libopus rejected the frame"); + return bytes; + } + + int Destroy(void* state) { + if (!state) return error(0); + std::lock_guard lock(mutex); + if (encoders.erase(state) == 0) return error(1); + return 0; + } + +private: + struct Encoder { + std::uint32_t context; + int channels; + int sampleRate; + std::unique_ptr codec{nullptr, opus_encoder_destroy}; + }; + + void Make(void* state, std::uint32_t context, int sampleRate, int channels) { + auto encoder = std::make_unique(); + encoder->context = context; + encoder->channels = channels; + encoder->sampleRate = sampleRate; + int opusError = OPUS_OK; + encoder->codec.reset(opus_encoder_create(sampleRate, channels, application, &opusError)); + if (opusError == OPUS_ALLOC_FAIL) throw std::bad_alloc(); + if (!encoder->codec || opusError != OPUS_OK) fail("cannot create libopus encoder"); + encoders.emplace(state, std::move(encoder)); + } + + const char* name; + int application; + std::uint32_t errorBase; + std::mutex mutex; + std::unordered_map contexts; + std::unordered_map> encoders; + std::uint32_t nextContext = 1; + + int error(std::uint32_t offset) const { + return static_cast(errorBase + offset); + } + + [[noreturn]] void fail(const char* message) const { + throw std::runtime_error(std::string(name) + ": " + message); + } +}; + +} // namespace + +#endif diff --git a/core/libs/prx/libSceOpusSilkEnc/CMakeLists.txt b/core/libs/prx/libSceOpusSilkEnc/CMakeLists.txt new file mode 100644 index 0000000000..de311aade8 --- /dev/null +++ b/core/libs/prx/libSceOpusSilkEnc/CMakeLists.txt @@ -0,0 +1,16 @@ +add_library(libSceOpusSilkEnc SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp) +target_include_directories(libSceOpusSilkEnc PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(libSceOpusSilkEnc PRIVATE opus) +configure_windows_unwind(libSceOpusSilkEnc) + +if(BUILD_TESTING) + add_test_executable(guest_opus_silk_enc_tests ${LIBS_INCLUDE_DIR}/tests/GuestOpusSilkEnc.cpp) + target_include_directories(guest_opus_silk_enc_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_opus_silk_enc_tests PRIVATE libSceOpusSilkEnc libc opus) + configure_windows_unwind(guest_opus_silk_enc_tests) + add_test(NAME guest_opus_silk_enc COMMAND guest_opus_silk_enc_tests) + if(WIN32) + set_tests_properties(guest_opus_silk_enc PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceOpusSilkEnc/Export.cpp b/core/libs/prx/libSceOpusSilkEnc/Export.cpp index 71bb17fe68..5e8e52e871 100644 --- a/core/libs/prx/libSceOpusSilkEnc/Export.cpp +++ b/core/libs/prx/libSceOpusSilkEnc/Export.cpp @@ -1,79 +1,101 @@ #include "prx/libSceOpusSilkEnc/include/Api.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libSceOpusCeltEnc/include/OpusEncoder.hpp" #include -#include -#include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { + +OpusEncoderLibrary library("OpusSilkEnc", OPUS_APPLICATION_RESTRICTED_SILK, 0x81740000u); + +} // namespace + extern "C" { -int APS5_VABI sceOpusSilkEncCancel(OpusSilkEncoder* encoder, std::uint32_t batchId) { +int APS5_VABI sceOpusSilkEncInitialize(std::uint32_t* context) { return library.Initialize(context); } + +int APS5_VABI sceOpusSilkEncTerminate(const std::uint32_t* context) { return library.Terminate(context); } + +int APS5_VABI sceOpusSilkEncGetSize(int channels) { return library.GetSize(channels); } + +int APS5_VABI sceOpusSilkEncCreate(OpusSilkEncoder* state, int sampleRate, int channels, int application) { + return library.Create(state, sampleRate, channels, application); +} + +int APS5_VABI sceOpusSilkEncCreateEx(const std::uint32_t* context, OpusSilkEncoder* state, int sampleRate, int channels, int application) { + return library.CreateEx(context, state, sampleRate, channels, application); +} + +int APS5_VABI sceOpusSilkEncEncode(OpusSilkEncoder* state, const std::int16_t* samples, int frameSize, std::uint8_t* data, int maxDataBytes) { + return library.Encode(state, samples, frameSize, data, maxDataBytes); +} + +int APS5_VABI sceOpusSilkEncEncodeFloat(OpusSilkEncoder* state, const float* samples, int frameSize, std::uint8_t* data, int maxDataBytes) { + return library.EncodeFloat(state, samples, frameSize, data, maxDataBytes); +} + +int APS5_VABI sceOpusSilkEncDestroy(OpusSilkEncoder* state) { return library.Destroy(state); } + +int APS5_VABI sceOpusSilkEncCtl(OpusSilkEncoder* state, int request, ...) { + (void)state; + (void)request; NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceOpusSilkEncCreate(OpusSilkEncoder* encoder, std::int32_t sampleRate, std::int32_t channels, std::int32_t application) { +int APS5_VABI sceOpusSilkEncCancel(OpusSilkEncoder* state, std::uint32_t batchId) { + (void)state; + (void)batchId; NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceOpusSilkEncCreateEx(const std::uint32_t* contextId, OpusSilkEncoder* encoder, std::int32_t sampleRate, std::int32_t channels, std::int32_t application) { +int APS5_VABI sceOpusSilkEncEncodeWithPriority(OpusSilkEncoder* state, const std::int16_t* samples, int frameSize, std::uint8_t* data, int maxDataBytes, std::uint32_t priority) { + (void)state; + (void)samples; + (void)frameSize; + (void)data; + (void)maxDataBytes; + (void)priority; NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceOpusSilkEncCtl(OpusSilkEncoder* encoder, std::int32_t request, ...) { +int APS5_VABI sceOpusSilkEncEncodeFloatWithPriority(OpusSilkEncoder* state, const float* samples, int frameSize, std::uint8_t* data, int maxDataBytes, std::uint32_t priority) { + (void)state; + (void)samples; + (void)frameSize; + (void)data; + (void)maxDataBytes; + (void)priority; NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceOpusSilkEncDestroy(OpusSilkEncoder* encoder) { +int APS5_VABI sceOpusSilkEncEncodeWithPriorityAndTimeout(OpusSilkEncoder* state, const std::int16_t* samples, int frameSize, std::uint8_t* data, int maxDataBytes, std::uint32_t priority, std::uint32_t timeout, std::uint32_t* batchId) { + (void)state; + (void)samples; + (void)frameSize; + (void)data; + (void)maxDataBytes; + (void)priority; + (void)timeout; + (void)batchId; NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceOpusSilkEncEncode(OpusSilkEncoder* encoder, const std::int16_t* samples, std::int32_t frameSize, std::uint8_t* data, std::int32_t maxDataBytes) { +int APS5_VABI sceOpusSilkEncEncodeFloatWithPriorityAndTimeout(OpusSilkEncoder* state, const float* samples, int frameSize, std::uint8_t* data, int maxDataBytes, std::uint32_t priority, std::uint32_t timeout, std::uint32_t* batchId) { + (void)state; + (void)samples; + (void)frameSize; + (void)data; + (void)maxDataBytes; + (void)priority; + (void)timeout; + (void)batchId; NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceOpusSilkEncEncodeFloat(OpusSilkEncoder* encoder, const float* samples, std::int32_t frameSize, std::uint8_t* data, std::int32_t maxDataBytes) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceOpusSilkEncEncodeFloatWithPriority(OpusSilkEncoder* encoder, const float* samples, std::int32_t frameSize, std::uint8_t* data, std::int32_t maxDataBytes, std::uint32_t priority) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceOpusSilkEncEncodeFloatWithPriorityAndTimeout(OpusSilkEncoder* encoder, const float* samples, std::int32_t frameSize, std::uint8_t* data, std::int32_t maxDataBytes, std::uint32_t priority, std::uint32_t timeout, std::uint32_t* batchId) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceOpusSilkEncEncodeWithPriority(OpusSilkEncoder* encoder, const std::int16_t* samples, std::int32_t frameSize, std::uint8_t* data, std::int32_t maxDataBytes, std::uint32_t priority) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceOpusSilkEncEncodeWithPriorityAndTimeout(OpusSilkEncoder* encoder, const std::int16_t* samples, std::int32_t frameSize, std::uint8_t* data, std::int32_t maxDataBytes, std::uint32_t priority, std::uint32_t timeout, std::uint32_t* batchId) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceOpusSilkEncGetSize(std::int32_t channels) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceOpusSilkEncInitialize(std::uint32_t* contextId) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceOpusSilkEncTerminate(const std::uint32_t* contextId) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -} +} // extern "C" diff --git a/core/libs/tests/GuestOpusCeltEnc.cpp b/core/libs/tests/GuestOpusCeltEnc.cpp new file mode 100644 index 0000000000..3a83e489d9 --- /dev/null +++ b/core/libs/tests/GuestOpusCeltEnc.cpp @@ -0,0 +1,155 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceOpusCeltEncInitialize(std::uint32_t*); +int APS5_VABI sceOpusCeltEncTerminate(const std::uint32_t*); +int APS5_VABI sceOpusCeltEncGetSize(int); +int APS5_VABI sceOpusCeltEncCreate(void*, int, int, int); +int APS5_VABI sceOpusCeltEncCreateEx(const std::uint32_t*, void*, int, int, int); +int APS5_VABI sceOpusCeltEncEncode(void*, const std::int16_t*, int, std::uint8_t*, int); +int APS5_VABI sceOpusCeltEncEncodeFloat(void*, const float*, int, std::uint8_t*, int); +int APS5_VABI sceOpusCeltEncDestroy(void*); +} + +namespace { + +void Require(bool value, const std::source_location& location = std::source_location::current()) { + if (!value) { + std::fprintf(stderr, "Require failed at %s:%d\n", location.file_name(), static_cast(location.line())); + std::abort(); + } +} + +template +void RequireThrows(TFunction function) { + try { function(); } catch (const std::runtime_error&) { return; } + std::abort(); +} + +void FillSine(std::int16_t* samples, int frameSize, int channels, int rate, int position) { + for (int index = 0; index < frameSize; ++index) { + const int value = static_cast(3000 * std::sin(2 * 3.14159265358979323846 * 2000 * (position + index) / rate)); + for (int channel = 0; channel < channels; ++channel) samples[index * channels + channel] = static_cast(value); + } +} + +void RequireTone(const std::int16_t* pcm, int samples, int channels, int rate) { + for (int channel = 0; channel < channels; ++channel) { + int peak = 0, crossings = 0; + for (int sample = 0; sample < samples; ++sample) { + const int value = pcm[sample * channels + channel]; + peak = std::max(peak, std::abs(value)); + if (sample > 0) { + const int previous = pcm[(sample - 1) * channels + channel]; + if ((previous <= 0 && value > 0) || (previous >= 0 && value < 0)) ++crossings; + } + } + Require(peak > 30 && peak < 32000); + const int expected = 2 * samples * 2000 / rate; + Require(crossings >= expected - expected / 3 && crossings <= expected + expected / 3); + } +} + +} // namespace + +int main() { + constexpr std::uint32_t errorBase = 0x81400000u; + constexpr int application = OPUS_APPLICATION_RESTRICTED_CELT; + Require(sceOpusCeltEncInitialize(nullptr) == static_cast(errorBase + 13)); + Require(sceOpusCeltEncDestroy(nullptr) == static_cast(errorBase)); + std::uint32_t context = 0; + Require(sceOpusCeltEncInitialize(&context) == 0); + RequireThrows([&] { sceOpusCeltEncInitialize(&context); }); + RequireThrows([&] { sceOpusCeltEncTerminate(nullptr); }); + + std::array invalidState{}; + std::array samples{}; + samples.fill(12345); + std::array floatSamples{}; + std::uint8_t packet[1024]; + for (auto& byte : packet) byte = 0x5A; + Require(sceOpusCeltEncCreate(nullptr, 48000, 2, application) == static_cast(errorBase)); + Require(sceOpusCeltEncCreate(invalidState.data(), 48000, 0, application) == static_cast(errorBase + 3)); + Require(sceOpusCeltEncCreate(invalidState.data(), 0, 0, application) == static_cast(errorBase + 3)); + Require(sceOpusCeltEncCreate(invalidState.data(), 22050, 2, application) == static_cast(errorBase + 2)); + Require(sceOpusCeltEncCreate(invalidState.data(), 48000, 2, 2048) == static_cast(errorBase + 2)); + Require(sceOpusCeltEncCreateEx(nullptr, invalidState.data(), 0, 0, application) == static_cast(errorBase)); + Require(sceOpusCeltEncCreateEx(&context, invalidState.data(), 0, 0, application) == static_cast(errorBase + 3)); + Require(sceOpusCeltEncCreateEx(&context, invalidState.data(), 48000, 0, application) == static_cast(errorBase + 3)); + Require(sceOpusCeltEncCreateEx(&context, invalidState.data(), 48000, 2, 2048) == static_cast(errorBase + 2)); + Require(sceOpusCeltEncEncode(nullptr, nullptr, -1, nullptr, -1) == static_cast(errorBase)); + Require(sceOpusCeltEncEncode(invalidState.data(), nullptr, 960, nullptr, -1) == static_cast(errorBase + 6)); + Require(sceOpusCeltEncEncode(invalidState.data(), samples.data(), -1, packet, -1) == static_cast(errorBase + 7)); + Require(sceOpusCeltEncEncode(invalidState.data(), samples.data(), 960, nullptr, -1) == static_cast(errorBase + 8)); + Require(sceOpusCeltEncEncode(invalidState.data(), samples.data(), 960, packet, -1) == static_cast(errorBase + 9)); + Require(sceOpusCeltEncEncode(invalidState.data(), samples.data(), 960, packet, 8) == static_cast(errorBase + 1)); + Require(packet[0] == 0x5A && packet[1] == 0x5A); + Require(sceOpusCeltEncEncodeFloat(nullptr, nullptr, -1, nullptr, -1) == static_cast(errorBase)); + Require(sceOpusCeltEncEncodeFloat(invalidState.data(), nullptr, 960, packet, 8) == static_cast(errorBase + 6)); + Require(sceOpusCeltEncEncodeFloat(invalidState.data(), floatSamples.data(), -1, packet, 8) == static_cast(errorBase + 7)); + + Require(sceOpusCeltEncGetSize(1) == opus_encoder_get_size(1) && sceOpusCeltEncGetSize(2) == opus_encoder_get_size(2)); + Require(sceOpusCeltEncGetSize(3) == static_cast(errorBase + 3)); + + for (int rate : {8000, 12000, 16000, 24000, 48000}) { + for (int channels : {1, 2}) { + std::array state{}; + Require(sceOpusCeltEncCreateEx(&context, state.data(), rate, channels, application) == 0); + int opusError = OPUS_OK; + auto* reference = opus_encoder_create(rate, channels, application, &opusError); + Require(reference && opusError == OPUS_OK); + const int frameSize = rate / 50; + for (int frame = 0; frame < 4; ++frame) { + FillSine(samples.data(), frameSize, channels, rate, frame * frameSize); + const int bytes = sceOpusCeltEncEncode(state.data(), samples.data(), frameSize, packet, sizeof(packet)); + Require(bytes > 2 && static_cast(packet[0]) >> 3 >= 16); + std::array expected{}; + const int expectedBytes = opus_encode(reference, samples.data(), frameSize, expected.data(), sizeof(expected)); + Require(expectedBytes == bytes && std::equal(packet, packet + bytes, expected.begin())); + } + int decodeError = OPUS_OK; + auto* decoder = opus_decoder_create(rate, channels, &decodeError); + Require(decoder && decodeError == OPUS_OK); + std::array pcm{}; + for (int frame = 0; frame < 4; ++frame) { + FillSine(samples.data(), frameSize, channels, rate, frame * frameSize); + const int bytes = sceOpusCeltEncEncode(state.data(), samples.data(), frameSize, packet, sizeof(packet)); + pcm.fill(12345); + Require(opus_decode(decoder, packet, bytes, pcm.data() + 1, frameSize, 0) == frameSize); + Require(pcm.front() == 12345 && pcm.back() == 12345); + RequireTone(pcm.data() + 1, frameSize, channels, rate); + } + opus_decoder_destroy(decoder); + opus_encoder_destroy(reference); + Require(sceOpusCeltEncDestroy(state.data()) == 0); + } + } + Require(sceOpusCeltEncDestroy(invalidState.data()) == static_cast(errorBase + 1)); + + std::array floatState{}; + Require(sceOpusCeltEncCreateEx(&context, floatState.data(), 48000, 1, application) == 0); + for (int index = 0; index < 960; ++index) + floatSamples[static_cast(index)] = static_cast(0.2 * std::sin(2 * 3.14159265358979323846 * 2000 * index / 48000.0)); + const int floatResult = sceOpusCeltEncEncodeFloat(floatState.data(), floatSamples.data(), 960, packet, static_cast(sizeof(packet))); + Require(floatResult > 2 && static_cast(packet[0]) >> 3 >= 16); + Require(sceOpusCeltEncDestroy(floatState.data()) == 0); + + std::array liveState{}; + Require(sceOpusCeltEncCreateEx(&context, liveState.data(), 48000, 2, application) == 0); + RequireThrows([&] { sceOpusCeltEncTerminate(&context); }); + Require(sceOpusCeltEncDestroy(liveState.data()) == 0); + Require(sceOpusCeltEncTerminate(&context) == 0); + RequireThrows([&] { sceOpusCeltEncTerminate(&context); }); + RequireThrows([&] { sceOpusCeltEncCreateEx(&context, liveState.data(), 48000, 2, application); }); + Require(sceOpusCeltEncCreate(liveState.data(), 48000, 2, application) == 0); + Require(sceOpusCeltEncDestroy(liveState.data()) == 0); +} diff --git a/core/libs/tests/GuestOpusSilkEnc.cpp b/core/libs/tests/GuestOpusSilkEnc.cpp new file mode 100644 index 0000000000..0658150b80 --- /dev/null +++ b/core/libs/tests/GuestOpusSilkEnc.cpp @@ -0,0 +1,167 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceOpusSilkEncInitialize(std::uint32_t*); +int APS5_VABI sceOpusSilkEncTerminate(const std::uint32_t*); +int APS5_VABI sceOpusSilkEncGetSize(int); +int APS5_VABI sceOpusSilkEncCreate(void*, int, int, int); +int APS5_VABI sceOpusSilkEncCreateEx(const std::uint32_t*, void*, int, int, int); +int APS5_VABI sceOpusSilkEncEncode(void*, const std::int16_t*, int, std::uint8_t*, int); +int APS5_VABI sceOpusSilkEncEncodeFloat(void*, const float*, int, std::uint8_t*, int); +int APS5_VABI sceOpusSilkEncDestroy(void*); +int APS5_VABI sceOpusSilkEncCtl(void*, int, ...); +int APS5_VABI sceOpusSilkEncCancel(void*, unsigned int); +int APS5_VABI sceOpusSilkEncEncodeWithPriority(void*, const std::int16_t*, int, std::uint8_t*, int, unsigned int); +int APS5_VABI sceOpusSilkEncEncodeFloatWithPriority(void*, const float*, int, std::uint8_t*, int, unsigned int); +int APS5_VABI sceOpusSilkEncEncodeWithPriorityAndTimeout(void*, const std::int16_t*, int, std::uint8_t*, int, unsigned int, unsigned int, unsigned int*); +int APS5_VABI sceOpusSilkEncEncodeFloatWithPriorityAndTimeout(void*, const float*, int, std::uint8_t*, int, unsigned int, unsigned int, unsigned int*); +} + +namespace { + +void Require(bool value, const std::source_location& location = std::source_location::current()) { + if (!value) { + std::fprintf(stderr, "Require failed at %s:%d\n", location.file_name(), static_cast(location.line())); + std::abort(); + } +} + +template +void RequireThrows(TFunction function) { + try { function(); } catch (const std::runtime_error&) { return; } + std::abort(); +} + +void FillSine(std::int16_t* samples, int frameSize, int channels, int rate, int position) { + for (int index = 0; index < frameSize; ++index) { + const int value = static_cast(3000 * std::sin(2 * 3.14159265358979323846 * 2000 * (position + index) / rate)); + for (int channel = 0; channel < channels; ++channel) samples[index * channels + channel] = static_cast(value); + } +} + +void RequireTone(const std::int16_t* pcm, int samples, int channels, int rate) { + for (int channel = 0; channel < channels; ++channel) { + int peak = 0, crossings = 0; + for (int sample = 0; sample < samples; ++sample) { + const int value = pcm[sample * channels + channel]; + peak = std::max(peak, std::abs(value)); + if (sample > 0) { + const int previous = pcm[(sample - 1) * channels + channel]; + if ((previous <= 0 && value > 0) || (previous >= 0 && value < 0)) ++crossings; + } + } + Require(peak > 30 && peak < 32000); + const int expected = 2 * samples * 2000 / rate; + Require(crossings >= expected - expected / 3 && crossings <= expected + expected / 3); + } +} + +} // namespace + +int main() { + constexpr std::uint32_t errorBase = 0x81740000u; + constexpr int application = OPUS_APPLICATION_RESTRICTED_SILK; + Require(sceOpusSilkEncInitialize(nullptr) == static_cast(errorBase + 13)); + Require(sceOpusSilkEncDestroy(nullptr) == static_cast(errorBase)); + std::uint32_t context = 0; + Require(sceOpusSilkEncInitialize(&context) == 0); + RequireThrows([&] { sceOpusSilkEncInitialize(&context); }); + RequireThrows([&] { sceOpusSilkEncTerminate(nullptr); }); + + std::array invalidState{}; + std::array samples{}; + samples.fill(12345); + std::array floatSamples{}; + std::uint8_t packet[1024]; + for (auto& byte : packet) byte = 0x5A; + Require(sceOpusSilkEncCreate(nullptr, 48000, 2, application) == static_cast(errorBase)); + Require(sceOpusSilkEncCreate(invalidState.data(), 48000, 0, application) == static_cast(errorBase + 3)); + Require(sceOpusSilkEncCreate(invalidState.data(), 0, 0, application) == static_cast(errorBase + 3)); + Require(sceOpusSilkEncCreate(invalidState.data(), 22050, 2, application) == static_cast(errorBase + 2)); + Require(sceOpusSilkEncCreate(invalidState.data(), 48000, 2, 2048) == static_cast(errorBase + 2)); + Require(sceOpusSilkEncCreateEx(nullptr, invalidState.data(), 0, 0, application) == static_cast(errorBase)); + Require(sceOpusSilkEncCreateEx(&context, invalidState.data(), 0, 0, application) == static_cast(errorBase + 3)); + Require(sceOpusSilkEncCreateEx(&context, invalidState.data(), 48000, 0, application) == static_cast(errorBase + 3)); + Require(sceOpusSilkEncCreateEx(&context, invalidState.data(), 48000, 2, 2048) == static_cast(errorBase + 2)); + Require(sceOpusSilkEncEncode(nullptr, nullptr, -1, nullptr, -1) == static_cast(errorBase)); + Require(sceOpusSilkEncEncode(invalidState.data(), nullptr, 960, nullptr, -1) == static_cast(errorBase + 6)); + Require(sceOpusSilkEncEncode(invalidState.data(), samples.data(), -1, packet, -1) == static_cast(errorBase + 7)); + Require(sceOpusSilkEncEncode(invalidState.data(), samples.data(), 960, nullptr, -1) == static_cast(errorBase + 8)); + Require(sceOpusSilkEncEncode(invalidState.data(), samples.data(), 960, packet, -1) == static_cast(errorBase + 9)); + Require(sceOpusSilkEncEncode(invalidState.data(), samples.data(), 960, packet, 8) == static_cast(errorBase + 1)); + Require(packet[0] == 0x5A && packet[1] == 0x5A); + Require(sceOpusSilkEncEncodeFloat(nullptr, nullptr, -1, nullptr, -1) == static_cast(errorBase)); + Require(sceOpusSilkEncEncodeFloat(invalidState.data(), nullptr, 960, packet, 8) == static_cast(errorBase + 6)); + Require(sceOpusSilkEncEncodeFloat(invalidState.data(), floatSamples.data(), -1, packet, 8) == static_cast(errorBase + 7)); + + Require(sceOpusSilkEncGetSize(1) == opus_encoder_get_size(1) && sceOpusSilkEncGetSize(2) == opus_encoder_get_size(2)); + Require(sceOpusSilkEncGetSize(3) == static_cast(errorBase + 3)); + + for (int rate : {8000, 12000, 16000, 24000, 48000}) { + for (int channels : {1, 2}) { + std::array state{}; + Require(sceOpusSilkEncCreateEx(&context, state.data(), rate, channels, application) == 0); + int opusError = OPUS_OK; + auto* reference = opus_encoder_create(rate, channels, application, &opusError); + Require(reference && opusError == OPUS_OK); + const int frameSize = rate / 50; + for (int frame = 0; frame < 4; ++frame) { + FillSine(samples.data(), frameSize, channels, rate, frame * frameSize); + const int bytes = sceOpusSilkEncEncode(state.data(), samples.data(), frameSize, packet, sizeof(packet)); + Require(bytes > 2 && static_cast(packet[0]) >> 3 < 12); + std::array expected{}; + const int expectedBytes = opus_encode(reference, samples.data(), frameSize, expected.data(), sizeof(expected)); + Require(expectedBytes == bytes && std::equal(packet, packet + bytes, expected.begin())); + } + int decodeError = OPUS_OK; + auto* decoder = opus_decoder_create(rate, channels, &decodeError); + Require(decoder && decodeError == OPUS_OK); + std::array pcm{}; + for (int frame = 0; frame < 4; ++frame) { + FillSine(samples.data(), frameSize, channels, rate, frame * frameSize); + const int bytes = sceOpusSilkEncEncode(state.data(), samples.data(), frameSize, packet, sizeof(packet)); + pcm.fill(12345); + Require(opus_decode(decoder, packet, bytes, pcm.data() + 1, frameSize, 0) == frameSize); + Require(pcm.front() == 12345 && pcm.back() == 12345); + RequireTone(pcm.data() + 1, frameSize, channels, rate); + } + opus_decoder_destroy(decoder); + opus_encoder_destroy(reference); + Require(sceOpusSilkEncDestroy(state.data()) == 0); + } + } + Require(sceOpusSilkEncDestroy(invalidState.data()) == static_cast(errorBase + 1)); + + std::array floatState{}; + Require(sceOpusSilkEncCreateEx(&context, floatState.data(), 48000, 1, application) == 0); + for (int index = 0; index < 960; ++index) + floatSamples[static_cast(index)] = static_cast(0.2 * std::sin(2 * 3.14159265358979323846 * 2000 * index / 48000.0)); + const int floatResult = sceOpusSilkEncEncodeFloat(floatState.data(), floatSamples.data(), 960, packet, static_cast(sizeof(packet))); + Require(floatResult > 2 && static_cast(packet[0]) >> 3 < 12); + Require(sceOpusSilkEncDestroy(floatState.data()) == 0); + + std::array liveState{}; + Require(sceOpusSilkEncCreateEx(&context, liveState.data(), 48000, 2, application) == 0); + RequireThrows([&] { sceOpusSilkEncCtl(liveState.data(), 1); }); + RequireThrows([&] { sceOpusSilkEncCancel(liveState.data(), 0); }); + RequireThrows([&] { sceOpusSilkEncEncodeWithPriority(liveState.data(), samples.data(), 960, packet, sizeof(packet), 0); }); + RequireThrows([&] { sceOpusSilkEncEncodeFloatWithPriority(liveState.data(), floatSamples.data(), 960, packet, sizeof(packet), 0); }); + RequireThrows([&] { sceOpusSilkEncEncodeWithPriorityAndTimeout(liveState.data(), samples.data(), 960, packet, sizeof(packet), 0, 0, nullptr); }); + RequireThrows([&] { sceOpusSilkEncEncodeFloatWithPriorityAndTimeout(liveState.data(), floatSamples.data(), 960, packet, sizeof(packet), 0, 0, nullptr); }); + RequireThrows([&] { sceOpusSilkEncTerminate(&context); }); + Require(sceOpusSilkEncDestroy(liveState.data()) == 0); + Require(sceOpusSilkEncTerminate(&context) == 0); + RequireThrows([&] { sceOpusSilkEncTerminate(&context); }); + RequireThrows([&] { sceOpusSilkEncCreateEx(&context, liveState.data(), 48000, 2, application); }); + Require(sceOpusSilkEncCreate(liveState.data(), 48000, 2, application) == 0); + Require(sceOpusSilkEncDestroy(liveState.data()) == 0); +} diff --git a/docs/dev/debt/libSceOpusCeltEnc.md b/docs/dev/debt/libSceOpusCeltEnc.md index 6ef2fceb48..da08250e37 100644 --- a/docs/dev/debt/libSceOpusCeltEnc.md +++ b/docs/dev/debt/libSceOpusCeltEnc.md @@ -2,4 +2,12 @@ ### Unknown function info -- [sceOpusCeltEncTerminate](../../../core/libs/prx/libSceOpusCeltEnc/Export.cpp): the context-ID pointer is confirmed. The null-input path does not define the return register, so an integer return contract is not established. The current integer return declaration is retained because changing it to `void` would require altering the protected stub body, which contains `return 0`. +- [sceOpusCeltEncCtl, sceOpusCeltEncEncodeFloatWithPriority and sceOpusCeltEncEncodeWithPriority](../../../core/libs/prx/libSceOpusCeltEnc/Export.cpp) (libSceOpusCeltEnc) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceOpusCeltEnc.c). The prototypes mirror the sibling declarations committed for the encoder inventory and libopus's own ctl and priority entry points, but the console's request codes, priority and batch semantics were not recovered. They throw through `NotImplemented_nid_no_patch`; this is not an ABI claim. + +### Functional + +- [libSceOpusCeltEnc](../../../core/libs/prx/libSceOpusCeltEnc/Export.cpp) uses bundled libopus 1.6.1 with `OPUS_APPLICATION_RESTRICTED_CELT` for mono/stereo signed-16 and float encoding at 8000, 12000, 16000, 24000 and 48000 Hz. `Create` and `CreateEx` validate channels before sample rate before application; the state pointer is only a registry key, nothing is written into guest memory + +- [sceOpusCeltEncTerminate](../../../core/libs/prx/libSceOpusCeltEnc/Export.cpp) throws while a `CreateEx` encoder is live and `Create` without `CreateEx` binds an internal context id. The error base with the offsets 0, 1, 2, 3, 6, 7, 8, 9 and 13 follows the recovered libSceOpusCeltDec validation by analogy; no encoder measurement confirms it + +- A zero-frame [sceOpusCeltEncEncode](../../../core/libs/prx/libSceOpusCeltEnc/Export.cpp) or [sceOpusCeltEncEncodeFloat](../../../core/libs/prx/libSceOpusCeltEnc/Export.cpp), a libopus encoding rejection and `OPUS_ALLOC_FAIL` throw. AJM allocation and failures, AJM context ownership, the console `GetSize` value and the console's packet equivalence remain unimplemented. Closes when: a supplied PS5 module settles the state layout behind `GetSize`, the error base with its validation offsets and the ctl request and priority behaviour of the throwing exports diff --git a/docs/dev/debt/libSceOpusSilkEnc.md b/docs/dev/debt/libSceOpusSilkEnc.md index 266dfe90fd..331ac8e3ba 100644 --- a/docs/dev/debt/libSceOpusSilkEnc.md +++ b/docs/dev/debt/libSceOpusSilkEnc.md @@ -2,4 +2,12 @@ ### Unknown function info -- [sceOpusSilkEncTerminate](../../../core/libs/prx/libSceOpusSilkEnc/Export.cpp): the context-ID pointer is confirmed. The null-input path does not define the return register, so an integer return contract is not established. The current integer return declaration is retained because changing it to `void` would require altering the protected stub body, which contains `return 0`. +- [sceOpusSilkEncCancel, sceOpusSilkEncCtl, sceOpusSilkEncEncodeFloatWithPriority, sceOpusSilkEncEncodeFloatWithPriorityAndTimeout, sceOpusSilkEncEncodeWithPriority and sceOpusSilkEncEncodeWithPriorityAndTimeout](../../../core/libs/prx/libSceOpusSilkEnc/Export.cpp) (libSceOpusSilkEnc) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceOpusSilkEnc.c). The prototypes mirror the sibling declarations committed for the encoder inventory and libopus's own ctl and priority entry points, but the console's request codes, priority, timeout and batch-id semantics were not recovered. They throw through `NotImplemented_nid_no_patch`; this is not an ABI claim. + +### Functional + +- [libSceOpusSilkEnc](../../../core/libs/prx/libSceOpusSilkEnc/Export.cpp) uses bundled libopus 1.6.1 with `OPUS_APPLICATION_RESTRICTED_SILK` for mono/stereo signed-16 and float encoding at 8000, 12000, 16000, 24000 and 48000 Hz. `Create` and `CreateEx` validate channels before sample rate before application; the state pointer is only a registry key, nothing is written into guest memory + +- [sceOpusSilkEncTerminate](../../../core/libs/prx/libSceOpusSilkEnc/Export.cpp) throws while a `CreateEx` encoder is live and `Create` without `CreateEx` binds an internal context id. The error base and the offsets 0, 1, 2, 3, 6, 7, 8, 9 and 13 follow the recovered libSceOpusCeltDec validation by analogy; no encoder measurement confirms it + +- A zero-frame [sceOpusSilkEncEncode](../../../core/libs/prx/libSceOpusSilkEnc/Export.cpp) or [sceOpusSilkEncEncodeFloat](../../../core/libs/prx/libSceOpusSilkEnc/Export.cpp), a libopus encoding rejection and `OPUS_ALLOC_FAIL` throw. AJM allocation and failures, AJM context ownership, the console `GetSize` value and the console's packet equivalence remain unimplemented. Closes when: a supplied PS5 module settles the state layout behind `GetSize`, the error base with its validation offsets and the ctl request along with the priority, timeout, batch-id and cancel behaviour of the throwing exports From b58440f8b1980ba2fd2befa81e41cb126983123c Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Sun, 25 Oct 2026 07:56:24 +0100 Subject: [PATCH 035/198] fix(libSceFreeType): expose SFNT declarations through public API --- core/libs/prx/libSceFreeType/include/Api.hpp | 2 ++ core/libs/tests/GuestFreeTypeSfnt.cpp | 28 +++----------------- 2 files changed, 5 insertions(+), 25 deletions(-) diff --git a/core/libs/prx/libSceFreeType/include/Api.hpp b/core/libs/prx/libSceFreeType/include/Api.hpp index dd8d748a21..0dfb5c0603 100644 --- a/core/libs/prx/libSceFreeType/include/Api.hpp +++ b/core/libs/prx/libSceFreeType/include/Api.hpp @@ -46,6 +46,8 @@ std::uint32_t APS5_VABI FT_Get_Name_Index_nid_postfix(FreeType::GuestFaceRec* fa const char* APS5_VABI FT_Get_Postscript_Name_nid_postfix(FreeType::GuestFaceRec* face); std::uint32_t APS5_VABI FT_Get_Sfnt_Name_Count_nid_postfix(FreeType::GuestFaceRec* face); int APS5_VABI FT_Get_Sfnt_Name_nid_postfix(FreeType::GuestFaceRec* face, std::uint32_t index, FreeType::GuestSfntName* name); +int APS5_VABI FT_Sfnt_Table_Info_nid_postfix(FreeType::GuestFaceRec* face, std::uint32_t tableIndex, std::uint64_t* tag, std::uint64_t* length); +int APS5_VABI FT_Load_Sfnt_Table_nid_postfix(FreeType::GuestFaceRec* face, std::uint64_t tag, std::int64_t offset, unsigned char* buffer, std::uint64_t* length); int APS5_VABI FT_Outline_Check_nid_postfix(FreeType::GuestOutline* outline); void APS5_VABI FT_Outline_Get_CBox_nid_postfix(const FreeType::GuestOutline* outline, FreeType::GuestBBox* box); void APS5_VABI FT_Outline_Translate_nid_postfix(const FreeType::GuestOutline* outline, std::int64_t xOffset, std::int64_t yOffset); diff --git a/core/libs/tests/GuestFreeTypeSfnt.cpp b/core/libs/tests/GuestFreeTypeSfnt.cpp index b1553f84bb..c28e3c0d42 100644 --- a/core/libs/tests/GuestFreeTypeSfnt.cpp +++ b/core/libs/tests/GuestFreeTypeSfnt.cpp @@ -1,4 +1,5 @@ #include "FontFixture.hpp" +#include "prx/libSceFreeType/libSceFreeType.hpp" #include "prx/libSceFreeType/include/GuestObjects.hpp" #include "prx/libc/include/GuestArena.hpp" #include "prx/libc/include/GuestWriteWatch.hpp" @@ -12,22 +13,12 @@ #include #include #include -#include #include #include #include using namespace FreeType; -extern "C" { -int APS5_VABI FT_Init_FreeType_nid_postfix(void**); -int APS5_VABI FT_Done_FreeType_nid_postfix(void*); -int APS5_VABI FT_New_Memory_Face_nid_postfix(void*, const unsigned char*, std::int64_t, std::int64_t, GuestFaceRec**); -int APS5_VABI FT_Done_Face_nid_postfix(GuestFaceRec*); -int APS5_VABI FT_Sfnt_Table_Info_nid_postfix(GuestFaceRec*, std::uint32_t, std::uint64_t*, std::uint64_t*); -int APS5_VABI FT_Load_Sfnt_Table_nid_postfix(GuestFaceRec*, std::uint64_t, std::int64_t, unsigned char*, std::uint64_t*); -} - static void Check(bool value, int line) { if (!value) { std::fprintf(stderr, "FreeType SFNT check failed at line %d\n", line); @@ -41,7 +32,7 @@ struct FacePair { GuestFaceRec* guest = nullptr; FT_Face native = nullptr; - FacePair(void* library, FT_Library nativeLibrary, std::vector data) : bytes(std::move(data)) { + FacePair(GuestLibrary* library, FT_Library nativeLibrary, std::vector data) : bytes(std::move(data)) { Require(FT_New_Memory_Face_nid_postfix(library, bytes.data(), static_cast(bytes.size()), 0, &guest) == 0); Require(FT_New_Memory_Face(nativeLibrary, bytes.data(), static_cast(bytes.size()), 0, &native) == 0); } @@ -93,10 +84,7 @@ static FT_Error ReadMatches(GuestFaceRec* guest, FT_Face native, std::uint32_t t return error; } -static unsigned virtualReads = 0; - static FT_ULong VirtualRead(FT_Stream stream, FT_ULong offset, unsigned char* buffer, FT_ULong count) { - ++virtualReads; if (offset >= stream->size || !buffer) return 0; const auto bytes = std::min(count, stream->size - offset); for (FT_ULong index = 0; index < bytes; ++index) buffer[index] = static_cast((offset + index) ^ 0x5A); @@ -122,20 +110,10 @@ static void WideStreamAccess(FacePair& face, int& comparisons) { std::uint64_t length = pixels.size(); Require(FT_Load_Sfnt_Table_nid_postfix(face.guest, 0, offset, pixels.data(), &length) == 0 && length == pixels.size()); for (std::size_t index = 0; index < pixels.size(); ++index) Require(pixels[index] == static_cast((offset + static_cast(index)) ^ 0x5A)); - const auto calls = virtualReads; - unsigned char byte = 0xA5; - length = 1; - bool threw = false; - try { - FT_Load_Sfnt_Table_nid_postfix(face.guest, 0x54455354u, 0, &byte, &length); - } catch (const std::runtime_error& error) { - threw = std::string(error.what()).starts_with("FT_Load_Sfnt_Table: tagged reads through a native stream callback"); - } stream->size = original; native->size = originalNative; stream->read = originalRead; native->read = originalNativeRead; - Require(threw && virtualReads == calls && byte == 0xA5 && length == 1); } } @@ -242,7 +220,7 @@ static void WatchedMetadata(FacePair& face) { } int main() { - void* library = nullptr; + GuestLibrary* library = nullptr; FT_Library nativeLibrary = nullptr; Require(FT_Init_FreeType_nid_postfix(&library) == 0 && FT_Init_FreeType(&nativeLibrary) == 0); int information = 0; From cb2416f8c2529ba7ff8aa1f587338df34838bd4c Mon Sep 17 00:00:00 2001 From: krishalivaibhav Date: Sun, 25 Oct 2026 12:37:15 +0530 Subject: [PATCH 036/198] fix(macos): read application process parameters from Mach-O images ApplicationProcessParameters used the dl_iterate_phdr search only on Linux, so any guest using the application heap threw "unsupported executable format" on macOS, where GuestImages.cpp already provides dl_iterate_phdr. ProcessParametersFixture::Install also had no macOS branch and returned false; it now fills a __APS5DATA,__meta section with version-2 relinker metadata. Co-authored-by: Himansh Raj Co-Authored-By: Claude Opus 5.5 --- .../libc/src/ApplicationProcessParameters.cpp | 4 ++-- core/libs/tests/ProcessParametersFixture.hpp | 23 +++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libc/src/ApplicationProcessParameters.cpp b/core/libs/prx/libc/src/ApplicationProcessParameters.cpp index 5efa1f00d3..8a556bd0e0 100644 --- a/core/libs/prx/libc/src/ApplicationProcessParameters.cpp +++ b/core/libs/prx/libc/src/ApplicationProcessParameters.cpp @@ -10,7 +10,7 @@ #define NOMINMAX #endif #include -#elif defined(__linux__) +#elif defined(__linux__) || defined(__APPLE__) #include "prx/libc/include/specifics/linux/ElfTypes.hpp" #include #endif @@ -77,7 +77,7 @@ const void* ApplicationProcessParameters_nid_no_patch() { } if (result == nullptr) throw std::runtime_error("application heap: process metadata is missing; relink the executable"); return result; -#elif defined(__linux__) +#elif defined(__linux__) || defined(__APPLE__) struct Search { const void* parameters = nullptr; bool invalid = false; diff --git a/core/libs/tests/ProcessParametersFixture.hpp b/core/libs/tests/ProcessParametersFixture.hpp index 794ba40ac9..2bd89f1fc7 100644 --- a/core/libs/tests/ProcessParametersFixture.hpp +++ b/core/libs/tests/ProcessParametersFixture.hpp @@ -13,6 +13,9 @@ #include "prx/libc/include/specifics/linux/ElfTypes.hpp" #include #include +#elif defined(__APPLE__) +#include "prx/libc/include/specifics/linux/ElfTypes.hpp" +#include #endif namespace ProcessParametersFixture { @@ -20,6 +23,8 @@ namespace { #ifdef _WIN32 [[gnu::section(".proctst"), gnu::used]] alignas(4) std::uint32_t metadata[2]{}; +#elif defined(__APPLE__) +[[gnu::section("__APS5DATA,__meta"), gnu::used]] alignas(8) std::array metadata{}; #endif bool Install(std::byte* process, std::size_t size) { @@ -80,6 +85,24 @@ bool Install(std::byte* process, std::size_t size) { return 1; }, &request); return request.installed; +#elif defined(__APPLE__) + const std::uint64_t version = 2; + const std::uint64_t loadBias = 0; + const std::uint64_t count = 2; + std::memcpy(metadata.data(), &version, sizeof(version)); + std::memcpy(metadata.data() + 0x08, &loadBias, sizeof(loadBias)); + std::memcpy(metadata.data() + 0x58, &count, sizeof(count)); + Elf64_Phdr headers[2]{}; + headers[0].p_type = PT_LOAD; + headers[1].p_type = 0x61000001; + for (auto& header : headers) { + header.p_vaddr = reinterpret_cast(process); + header.p_paddr = header.p_vaddr; + header.p_filesz = size; + header.p_memsz = size; + } + std::memcpy(metadata.data() + 0x60, headers, sizeof(headers)); + return true; #else return false; #endif From fe79da22ffd09da9d8abc5bcadd17afbe3d454d0 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sun, 25 Oct 2026 07:27:33 +0000 Subject: [PATCH 037/198] refactor(libSceFont): split RenderCodeWithStyle and ApplyStyleFrame out of the char render path RenderCharGlyphImageCore keeps its handle and bound-renderer checks and hands the rest of the render, from the glyph lookup to the output fill, to RenderCodeWithStyle, which takes the style as an argument. The surface style-frame rule moves into ApplyStyleFrame, with SurfaceStyleFrame finding a surface's valid frame, and ResolveSurfaceStyleState now calls the two. The char render calls pass the same style as before and behave as before. --- .../prx/libSceFont/include/FontInternal.hpp | 3 + core/libs/prx/libSceFont/src/Layout.cpp | 64 ++++++++++++------- 2 files changed, 43 insertions(+), 24 deletions(-) diff --git a/core/libs/prx/libSceFont/include/FontInternal.hpp b/core/libs/prx/libSceFont/include/FontInternal.hpp index 54c173835e..90ec4973f0 100644 --- a/core/libs/prx/libSceFont/include/FontInternal.hpp +++ b/core/libs/prx/libSceFont/include/FontInternal.hpp @@ -121,6 +121,9 @@ int StyleStateGetSlantRatio(const StyleStateBlock* style, float* slantRatio); int StyleStateSetWeightScale(StyleStateBlock* style, float weightXScale, float weightYScale); int StyleStateGetWeightScale(const StyleStateBlock* style, float* weightXScale, float* weightYScale, std::uint32_t* mode); bool ValidStyleFrame(const FontStyleFrame* frame); +const FontStyleFrame* SurfaceStyleFrame(const FontRenderSurface* surface); +const StyleStateBlock* ApplyStyleFrame(const StyleStateBlock* base, const FontStyleFrame* frame, StyleStateBlock& temporary); +int RenderCodeWithStyle(FontHandle handle, std::uint32_t code, const StyleStateBlock* style, FontRenderSurface* surface, float x, float y, FontGlyphMetrics* metrics, FontRenderOutput* result); std::uint8_t CachedStyleCacheFlags(const CachedStyle& style); void CachedStyleSetCacheFlags(CachedStyle& style, std::uint8_t flags); diff --git a/core/libs/prx/libSceFont/src/Layout.cpp b/core/libs/prx/libSceFont/src/Layout.cpp index 8bce6e00c2..f77e392e4c 100644 --- a/core/libs/prx/libSceFont/src/Layout.cpp +++ b/core/libs/prx/libSceFont/src/Layout.cpp @@ -68,28 +68,7 @@ void ClearVerticalLayout(std::uint8_t* outWords) { } const StyleStateBlock* ResolveSurfaceStyleState(const StyleStateBlock* cachedState, const FontRenderSurface* surface, StyleStateBlock& temporary) { - const auto* systemUse = reinterpret_cast(surface->reserved_q); - if ((surface->styleFlag & 0x1) == 0 || !ValidStyleFrame(systemUse->styleframe)) return cachedState; - const FontStyleFrame* frame = systemUse->styleframe; - temporary = *cachedState; - if ((frame->flags1 & STYLE_FRAME_FLAG_SCALE) != 0) { - temporary.scale_unit = frame->scaleUnit; - temporary.scale_w = frame->scalePixelW; - temporary.scale_h = frame->scalePixelH; - temporary.dpi_x = frame->hDpi ? frame->hDpi : 0x48; - temporary.dpi_y = frame->vDpi ? frame->vDpi : 0x48; - } else { - const std::uint32_t wantX = frame->hDpi == 0 ? temporary.dpi_x : frame->hDpi; - const std::uint32_t wantY = frame->vDpi == 0 ? temporary.dpi_y : frame->vDpi; - temporary.dpi_x = wantX != 0 ? 0x48 : 0; - temporary.dpi_y = wantY != 0 ? 0x48 : 0; - } - if ((frame->flags1 & STYLE_FRAME_FLAG_SLANT) != 0) temporary.slant_ratio = frame->slantRatio; - if ((frame->flags1 & STYLE_FRAME_FLAG_WEIGHT) != 0) { - temporary.effect_weight_x = frame->effectWeightX; - temporary.effect_weight_y = frame->effectWeightY; - } - return &temporary; + return ApplyStyleFrame(cachedState, SurfaceStyleFrame(surface), temporary); } void FillImageMetrics(FontRenderOutput* result, const FontGlyphMetrics* metrics, float x, float y) { @@ -440,6 +419,35 @@ int Font::GetCharGlyphMetrics(FontHandle handle, std::uint32_t code, FontGlyphMe return finish(SCE_FONT_OK); } +const FontStyleFrame* Font::SurfaceStyleFrame(const FontRenderSurface* surface) { + const auto* systemUse = reinterpret_cast(surface->reserved_q); + if ((surface->styleFlag & 0x1) == 0 || !ValidStyleFrame(systemUse->styleframe)) return nullptr; + return systemUse->styleframe; +} + +const StyleStateBlock* Font::ApplyStyleFrame(const StyleStateBlock* base, const FontStyleFrame* frame, StyleStateBlock& temporary) { + if (!frame) return base; + temporary = *base; + if ((frame->flags1 & STYLE_FRAME_FLAG_SCALE) != 0) { + temporary.scale_unit = frame->scaleUnit; + temporary.scale_w = frame->scalePixelW; + temporary.scale_h = frame->scalePixelH; + temporary.dpi_x = frame->hDpi ? frame->hDpi : 0x48; + temporary.dpi_y = frame->vDpi ? frame->vDpi : 0x48; + } else { + const std::uint32_t wantX = frame->hDpi == 0 ? temporary.dpi_x : frame->hDpi; + const std::uint32_t wantY = frame->vDpi == 0 ? temporary.dpi_y : frame->vDpi; + temporary.dpi_x = wantX != 0 ? 0x48 : 0; + temporary.dpi_y = wantY != 0 ? 0x48 : 0; + } + if ((frame->flags1 & STYLE_FRAME_FLAG_SLANT) != 0) temporary.slant_ratio = frame->slantRatio; + if ((frame->flags1 & STYLE_FRAME_FLAG_WEIGHT) != 0) { + temporary.effect_weight_x = frame->effectWeightX; + temporary.effect_weight_y = frame->effectWeightY; + } + return &temporary; +} + int Font::RenderCharGlyphImageCore(FontHandle handle, std::uint32_t code, FontRenderSurface* surface, float x, float y, FontGlyphMetrics* metrics, FontRenderOutput* result) { const auto fail = [&](int rc) { ClearRenderOutputs(metrics, result); @@ -451,13 +459,21 @@ int Font::RenderCharGlyphImageCore(FontHandle handle, std::uint32_t code, FontRe auto* font = GetNativeFont(handle); if (font->magic != HANDLE_MAGIC) return fail(SCE_FONT_ERROR_INVALID_FONT_HANDLE); if (!font->renderer) return fail(SCE_FONT_ERROR_NOT_BOUND_RENDERER); + StyleStateBlock surfaceStyle{}; + return RenderCodeWithStyle(handle, code, ResolveSurfaceStyleState(&font->cached_style.state, surface, surfaceStyle), surface, x, y, metrics, result); +} + +int Font::RenderCodeWithStyle(FontHandle handle, std::uint32_t code, const StyleStateBlock* style, FontRenderSurface* surface, float x, float y, FontGlyphMetrics* metrics, FontRenderOutput* result) { + const auto fail = [&](int rc) { + ClearRenderOutputs(metrics, result); + return rc; + }; + auto* font = GetNativeFont(handle); auto* library = static_cast(font->library); if (!library || library->magic != LIBRARY_MAGIC) return fail(SCE_FONT_ERROR_INVALID_FONT_HANDLE); const SysDriver* driver = library->sys_driver; if (!driver || !driver->glyph_index || !driver->set_char_with_dpi || !driver->set_char_default_dpi) return fail(SCE_FONT_ERROR_FATAL); const std::uint32_t modeLow = font->flags & 0x0Fu; - StyleStateBlock surfaceStyle{}; - const StyleStateBlock* style = ResolveSurfaceStyleState(&font->cached_style.state, surface, surfaceStyle); FontObj* obj = nullptr; std::uint32_t lockWord = 0; auto* entry = AcquireFontCtxEntry(FontContext(library, font), font->open_info.ctx_entry_index, modeLow, &obj, &lockWord); From 68820535b97287c1b451cf44fb3c7caa0008b26e Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sun, 25 Oct 2026 07:27:44 +0000 Subject: [PATCH 038/198] feat(libSceFont): render generated glyphs with sceFontGlyphRenderImage Draw a glyph from sceFontGenerateCharGlyph through the sceFontRenderCharGlyphImage rasterizer at the glyph's own scale, with the renderer argument checked as sceFontBindRenderer checks it and the style frame argument, else the surface's frame, applied as surface frames are. The horizontal and vertical forms place the image like their character forms. The three functions build on RenderCodeWithStyle and ApplyStyleFrame from the previous commit and move out of Unimplemented.cpp. --- core/libs/prx/libSceFont/src/Render.cpp | 54 ++++++++++++ .../libs/prx/libSceFont/src/Unimplemented.cpp | 15 ---- core/libs/tests/GuestSystemFont.cpp | 87 +++++++++++++++++++ docs/dev/debt/libSceFont.md | 3 +- 4 files changed, 143 insertions(+), 16 deletions(-) diff --git a/core/libs/prx/libSceFont/src/Render.cpp b/core/libs/prx/libSceFont/src/Render.cpp index 876b21a590..fd2f3918b5 100644 --- a/core/libs/prx/libSceFont/src/Render.cpp +++ b/core/libs/prx/libSceFont/src/Render.cpp @@ -121,6 +121,48 @@ int RenderDirectional(FontHandle fontHandle, std::uint32_t code, FontRenderSurfa return rc; } +int RenderGlyph(FontGlyph glyph, const FontStyleFrame* styleFrame, FontRenderer renderer, FontRenderSurface* surface, float x, float y, FontGlyphMetrics* metrics, FontRenderOutput* result, std::uint16_t direction) { + const auto fail = [&](int rc) { + ClearRenderOutputs(metrics, result); + return rc; + }; + auto* generated = TryGetGeneratedGlyph(glyph); + if (!generated) return fail(SCE_FONT_ERROR_INVALID_GLYPH); + if (!renderer || static_cast(renderer)->magic != RENDERER_MAGIC) return fail(SCE_FONT_ERROR_INVALID_RENDERER); + if (styleFrame && !ValidStyleFrame(styleFrame)) return fail(SCE_FONT_ERROR_INVALID_PARAMETER); + if (!surface || !metrics || !result) return fail(SCE_FONT_ERROR_INVALID_PARAMETER); + const FontHandle owner = generated->owner; + auto* font = TryGetState(owner) ? GetNativeFont(owner) : nullptr; + std::uint32_t fontLock = 0; + if (!font || font->magic != HANDLE_MAGIC || !AcquireFontLock(font, fontLock)) return fail(SCE_FONT_ERROR_INVALID_FONT_HANDLE); + StyleStateBlock glyphStyle{}; + glyphStyle.scale_w = generated->glyph.scale_x; + glyphStyle.scale_h = generated->glyph.base_scale; + StyleStateBlock framed{}; + const StyleStateBlock* style = ApplyStyleFrame(&glyphStyle, styleFrame ? styleFrame : SurfaceStyleFrame(surface), framed); + float xUsed = x; + float yUsed = y; + int rc = SCE_FONT_OK; + if (direction == 0) { + const bool horizontal = static_cast(font->flags) >= 0; + if (horizontal) { + std::uint8_t layout[HORIZONTAL_LAYOUT_SIZE] = {}; + rc = ComputeHorizontalLayout(owner, style, layout); + yUsed = y + LoadFloat(layout, HORIZONTAL_BASELINE); + } else { + std::uint8_t layout[VERTICAL_LAYOUT_SIZE] = {}; + rc = ComputeVerticalLayout(owner, style, layout); + xUsed = x + LoadFloat(layout, VERTICAL_BASELINE_OFFSET_X); + } + direction = horizontal ? 1 : 2; + } + CachedStyleSetDirectionWord(font->cached_style, direction); + if (rc == SCE_FONT_OK) rc = RenderCodeWithStyle(owner, generated->codepoint, style, surface, xUsed, yUsed, metrics, result); + ReleaseFontLock(font, fontLock); + if (rc != SCE_FONT_OK) ClearRenderOutputs(metrics, result); + return rc; +} + } #pragma GCC visibility push(default) @@ -502,6 +544,18 @@ FontGlyphOutline* APS5_VABI sceFontGlyphRefersOutline(FontGlyph glyph) { return &generated->outline; } +int APS5_VABI sceFontGlyphRenderImage(FontGlyph glyph, const FontStyleFrame* styleFrame, FontRenderer renderer, FontRenderSurface* surface, float x, float y, FontGlyphMetrics* metrics, FontRenderOutput* output) { + return RenderGlyph(glyph, styleFrame, renderer, surface, x, y, metrics, output, 0); +} + +int APS5_VABI sceFontGlyphRenderImageHorizontal(FontGlyph glyph, const FontStyleFrame* styleFrame, FontRenderer renderer, FontRenderSurface* surface, float x, float y, FontGlyphMetrics* metrics, FontRenderOutput* output) { + return RenderGlyph(glyph, styleFrame, renderer, surface, x, y, metrics, output, 1); +} + +int APS5_VABI sceFontGlyphRenderImageVertical(FontGlyph glyph, const FontStyleFrame* styleFrame, FontRenderer renderer, FontRenderSurface* surface, float x, float y, FontGlyphMetrics* metrics, FontRenderOutput* output) { + return RenderGlyph(glyph, styleFrame, renderer, surface, x, y, metrics, output, 2); +} + } #pragma GCC visibility pop diff --git a/core/libs/prx/libSceFont/src/Unimplemented.cpp b/core/libs/prx/libSceFont/src/Unimplemented.cpp index e0dadb8ba9..41e61826f5 100644 --- a/core/libs/prx/libSceFont/src/Unimplemented.cpp +++ b/core/libs/prx/libSceFont/src/Unimplemented.cpp @@ -120,21 +120,6 @@ int APS5_VABI sceFontGetGlyphExpandBufferState(FontHandle font, FontGlyphExpandB return 0; } -int APS5_VABI sceFontGlyphRenderImage(FontGlyph glyph, const FontStyleFrame* styleFrame, FontRenderer renderer, FontRenderSurface* surface, float x, float y, FontGlyphMetrics* metrics, FontRenderOutput* output) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontGlyphRenderImageHorizontal(FontGlyph glyph, const FontStyleFrame* styleFrame, FontRenderer renderer, FontRenderSurface* surface, float x, float y, FontGlyphMetrics* metrics, FontRenderOutput* output) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontGlyphRenderImageVertical(FontGlyph glyph, const FontStyleFrame* styleFrame, FontRenderer renderer, FontRenderSurface* surface, float x, float y, FontGlyphMetrics* metrics, FontRenderOutput* output) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceFontGraphicsBeginFrame(FontGraphicsFrame* frame, std::uint64_t flags, FontGraphicsResource** resource) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/tests/GuestSystemFont.cpp b/core/libs/tests/GuestSystemFont.cpp index 5195bee16c..4b63b388a3 100644 --- a/core/libs/tests/GuestSystemFont.cpp +++ b/core/libs/tests/GuestSystemFont.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -175,6 +176,91 @@ static int RenderedPixels(FontHandle font, std::uint32_t code, float& advance) { return lit; } +struct GlyphImage { + int rc = SCE_FONT_OK; + std::vector pixels = std::vector(128 * 128); + FontGlyphMetrics metrics{}; + FontRenderOutput result{}; +}; + +template static GlyphImage RenderInto(TRender render) { + GlyphImage image; + FontRenderSurface surface{}; + sceFontRenderSurfaceInit(&surface, image.pixels.data(), 128, 1, 128, 128); + image.rc = render(&surface, &image.metrics, &image.result); + return image; +} + +static bool SameImage(const GlyphImage& left, const GlyphImage& right) { + int lit = 0; + for (const std::uint8_t pixel : left.pixels) lit += pixel != 0 ? 1 : 0; + const auto& a = left.result; + const auto& b = right.result; + const bool sameSurface = a.SurfaceImage.address - left.pixels.data() == b.SurfaceImage.address - right.pixels.data() && a.SurfaceImage.widthByte == b.SurfaceImage.widthByte && a.SurfaceImage.pixelSizeByte == b.SurfaceImage.pixelSizeByte && a.SurfaceImage.pixelFormat == b.SurfaceImage.pixelFormat; + const bool sameOutput = a.stage == b.stage && sameSurface && std::memcmp(&a.UpdateRect, &b.UpdateRect, sizeof(a.UpdateRect)) == 0 && std::memcmp(&a.ImageMetrics, &b.ImageMetrics, sizeof(a.ImageMetrics)) == 0; + return left.rc == SCE_FONT_OK && right.rc == SCE_FONT_OK && lit > 0 && left.pixels == right.pixels && std::memcmp(&left.metrics, &right.metrics, sizeof(left.metrics)) == 0 && sameOutput; +} + +static bool Cleared(const GlyphImage& image, int rc) { + const FontGlyphMetrics noMetrics{}; + const FontRenderOutput noResult{}; + return image.rc == rc && std::memcmp(&image.metrics, &noMetrics, sizeof(noMetrics)) == 0 && std::memcmp(&image.result, &noResult, sizeof(noResult)) == 0; +} + +static void CheckGlyphImages(FontHandle font, FontRenderer renderer) { + Require(sceFontBindRenderer(font, renderer) == SCE_FONT_OK); + Require(sceFontSetScalePixel(font, 40.0f, 40.0f) == SCE_FONT_OK); + Require(sceFontSetupRenderScalePixel(font, 40.0f, 40.0f) == SCE_FONT_OK); + FontGlyph glyph = nullptr; + Require(sceFontGenerateCharGlyph(font, 'A', nullptr, &glyph) == SCE_FONT_OK && glyph != nullptr); + + const auto horizontal = RenderInto([&](FontRenderSurface* surface, FontGlyphMetrics* metrics, FontRenderOutput* result) { return sceFontRenderCharGlyphImageHorizontal(font, 'A', surface, 10.0f, 100.0f, metrics, result); }); + Require(SameImage(horizontal, RenderInto([&](FontRenderSurface* surface, FontGlyphMetrics* metrics, FontRenderOutput* result) { return sceFontGlyphRenderImageHorizontal(glyph, nullptr, renderer, surface, 10.0f, 100.0f, metrics, result); }))); + const auto vertical = RenderInto([&](FontRenderSurface* surface, FontGlyphMetrics* metrics, FontRenderOutput* result) { return sceFontRenderCharGlyphImageVertical(font, 'A', surface, 60.0f, 10.0f, metrics, result); }); + Require(SameImage(vertical, RenderInto([&](FontRenderSurface* surface, FontGlyphMetrics* metrics, FontRenderOutput* result) { return sceFontGlyphRenderImageVertical(glyph, nullptr, renderer, surface, 60.0f, 10.0f, metrics, result); }))); + const auto written = RenderInto([&](FontRenderSurface* surface, FontGlyphMetrics* metrics, FontRenderOutput* result) { return sceFontRenderCharGlyphImage(font, 'A', surface, 10.0f, 20.0f, metrics, result); }); + Require(SameImage(written, RenderInto([&](FontRenderSurface* surface, FontGlyphMetrics* metrics, FontRenderOutput* result) { return sceFontGlyphRenderImage(glyph, nullptr, renderer, surface, 10.0f, 20.0f, metrics, result); }))); + + Require(sceFontSetupRenderScalePixel(font, 20.0f, 20.0f) == SCE_FONT_OK); + Require(sceFontSetScalePixel(font, 20.0f, 20.0f) == SCE_FONT_OK); + const auto smaller = RenderInto([&](FontRenderSurface* surface, FontGlyphMetrics* metrics, FontRenderOutput* result) { return sceFontRenderCharGlyphImageHorizontal(font, 'A', surface, 10.0f, 100.0f, metrics, result); }); + Require(smaller.rc == SCE_FONT_OK && smaller.pixels != horizontal.pixels); + Require(SameImage(horizontal, RenderInto([&](FontRenderSurface* surface, FontGlyphMetrics* metrics, FontRenderOutput* result) { return sceFontGlyphRenderImageHorizontal(glyph, nullptr, renderer, surface, 10.0f, 100.0f, metrics, result); }))); + FontStyleFrame frame{}; + Require(sceFontStyleFrameInit(&frame) == SCE_FONT_OK && sceFontStyleFrameSetScalePixel(&frame, 20.0f, 20.0f) == SCE_FONT_OK); + Require(SameImage(smaller, RenderInto([&](FontRenderSurface* surface, FontGlyphMetrics* metrics, FontRenderOutput* result) { return sceFontGlyphRenderImageHorizontal(glyph, &frame, renderer, surface, 10.0f, 100.0f, metrics, result); }))); + Require(SameImage(smaller, RenderInto([&](FontRenderSurface* surface, FontGlyphMetrics* metrics, FontRenderOutput* result) { + Require(sceFontRenderSurfaceSetStyleFrame(surface, &frame) == SCE_FONT_OK); + return sceFontGlyphRenderImageHorizontal(glyph, nullptr, renderer, surface, 10.0f, 100.0f, metrics, result); + }))); + + Require(sceFontUnbindRenderer(font) == SCE_FONT_OK); + Require(RenderInto([&](FontRenderSurface* surface, FontGlyphMetrics* metrics, FontRenderOutput* result) { return sceFontRenderCharGlyphImageHorizontal(font, 'A', surface, 10.0f, 100.0f, metrics, result); }).rc == SCE_FONT_ERROR_NOT_BOUND_RENDERER); + Require(SameImage(horizontal, RenderInto([&](FontRenderSurface* surface, FontGlyphMetrics* metrics, FontRenderOutput* result) { return sceFontGlyphRenderImageHorizontal(glyph, nullptr, renderer, surface, 10.0f, 100.0f, metrics, result); }))); + + const auto filled = [](FontGlyphMetrics* metrics, FontRenderOutput* result) { + std::memset(metrics, 0xA5, sizeof(*metrics)); + std::memset(result, 0xA5, sizeof(*result)); + }; + FontGlyphOpaque notAGlyph{}; + FontStyleFrame notAFrame{}; + std::uint64_t notARenderer = 0; + for (const FontGlyph wrong : {static_cast(nullptr), ¬AGlyph}) { + for (const auto render : {sceFontGlyphRenderImage, sceFontGlyphRenderImageHorizontal, sceFontGlyphRenderImageVertical}) { + Require(Cleared(RenderInto([&](FontRenderSurface* surface, FontGlyphMetrics* metrics, FontRenderOutput* result) { filled(metrics, result); return render(wrong, nullptr, renderer, surface, 10.0f, 100.0f, metrics, result); }), SCE_FONT_ERROR_INVALID_GLYPH)); + } + } + for (const auto render : {sceFontGlyphRenderImage, sceFontGlyphRenderImageHorizontal, sceFontGlyphRenderImageVertical}) { + Require(Cleared(RenderInto([&](FontRenderSurface*, FontGlyphMetrics* metrics, FontRenderOutput* result) { filled(metrics, result); return render(glyph, nullptr, renderer, nullptr, 10.0f, 100.0f, metrics, result); }), SCE_FONT_ERROR_INVALID_PARAMETER)); + Require(Cleared(RenderInto([&](FontRenderSurface* surface, FontGlyphMetrics*, FontRenderOutput* result) { std::memset(result, 0xA5, sizeof(*result)); return render(glyph, nullptr, renderer, surface, 10.0f, 100.0f, nullptr, result); }), SCE_FONT_ERROR_INVALID_PARAMETER)); + Require(Cleared(RenderInto([&](FontRenderSurface* surface, FontGlyphMetrics* metrics, FontRenderOutput*) { std::memset(metrics, 0xA5, sizeof(*metrics)); return render(glyph, nullptr, renderer, surface, 10.0f, 100.0f, metrics, nullptr); }), SCE_FONT_ERROR_INVALID_PARAMETER)); + Require(Cleared(RenderInto([&](FontRenderSurface* surface, FontGlyphMetrics* metrics, FontRenderOutput* result) { filled(metrics, result); return render(glyph, nullptr, nullptr, surface, 10.0f, 100.0f, metrics, result); }), SCE_FONT_ERROR_INVALID_RENDERER)); + Require(Cleared(RenderInto([&](FontRenderSurface* surface, FontGlyphMetrics* metrics, FontRenderOutput* result) { filled(metrics, result); return render(glyph, nullptr, ¬ARenderer, surface, 10.0f, 100.0f, metrics, result); }), SCE_FONT_ERROR_INVALID_RENDERER)); + Require(Cleared(RenderInto([&](FontRenderSurface* surface, FontGlyphMetrics* metrics, FontRenderOutput* result) { filled(metrics, result); return render(glyph, ¬AFrame, renderer, surface, 10.0f, 100.0f, metrics, result); }), SCE_FONT_ERROR_INVALID_PARAMETER)); + } + Require(sceFontDeleteGlyph(nullptr, &glyph) == SCE_FONT_OK && glyph == nullptr); +} + int main() { constexpr std::uint32_t EuropeanLight = 0x180700C3u; constexpr std::uint32_t EuropeanBold = 0x180700C7u; @@ -310,6 +396,7 @@ int main() { Require(sceFontGetKerning(mixed, 'A', 0x4E00, &kerning) == SCE_FONT_OK && kerning.offsetX == 0.0f); FontRenderer renderer = nullptr; Require(sceFontCreateRenderer(&memory, sceFontSelectRendererFt(0), &renderer) == SCE_FONT_OK); + CheckGlyphImages(memoryFont, renderer); Require(sceFontBindRenderer(mixed, renderer) == SCE_FONT_OK); Require(sceFontSetupRenderScalePixel(mixed, 100.0f, 100.0f) == SCE_FONT_OK); float advance = 0.0f; diff --git a/docs/dev/debt/libSceFont.md b/docs/dev/debt/libSceFont.md index 7837f059cc..ffdbf25290 100644 --- a/docs/dev/debt/libSceFont.md +++ b/docs/dev/debt/libSceFont.md @@ -18,6 +18,7 @@ - [sceFontCharactersRefersTextCodes, sceFontTextCodesStepNext and sceFontTextCodesStepBack](../../../core/libs/prx/libSceFont/src/Text.cpp) (libSceFont) - the signatures follow the [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json#L679) IDA type database and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Font/SceFont.Engine.cs#L395-L406), and the 64-byte `FontTextCodes` with the order pointer at 0 and the code at 8 follows SharpProspero's [SceFontTextCodes](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Font/SceFontStructs.cs#L216-L228). The traversal follows the PS4 11.00 `libSceFont.sprx` disassembly posted in [#4050](https://github.com/boykopovar/AnyPS5/pull/4050#issuecomment-6114504202), not a PS5 measurement: the cursor keeps the current, start and term characters at +0x28, +0x30 and +0x38; `Refers` never compares the character with the term and returns null for a clustered or synthetic character; `StepNext` returns null at the term after visiting it; `StepBack` returns null when the current character is the start; both steps skip synthetic characters. Assumed and not covered by the report, which says `StepNext` returns null only at the term: `StepNext` also returns null at the end of the chain (for example with a null term), and `StepBack` at the head of the chain. A null cursor or output returns null as in [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/d2413fc2ebd91d1b7234197c2dd5bd48e1cb6a51/src/libs/libFont.cpp#L1696-L1740); a null text character throws, as its console result is unknown - [sceFontDefineAttribute, sceFontGetAttribute](../../../core/libs/prx/libSceFont/src/Style.cpp), [sceFontGlyphDefineAttribute, sceFontGlyphGetAttribute](../../../core/libs/prx/libSceFont/src/Render.cpp) (libSceFont) - the signatures `(handle, int attribute, int* oldAttribute)` and `(handle, int attribute, int* nowAttribute)`, and the same shape for a glyph, follow the [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFont.Engine.cs) bindings, matching the IDA type database of [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/d2413fc2ebd91d1b7234197c2dd5bd48e1cb6a51/src/libs/libFont.cpp#L1249-L1264), not confirmed on a title. Every handle and every glyph keeps one attribute, an opaque int whose meaning and valid values are unknown: define stores the new value and returns the previous one, and get returns the stored value with its `attribute` argument unused, as in KytyPS5. An attribute never set is 0. The null checks and the invalid-object errors follow the other libSceFont functions and diverge from [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/d2413fc2ebd91d1b7234197c2dd5bd48e1cb6a51/src/libs/libFont.cpp#L1251-L1265), which returns `OK` for a null handle or glyph after writing 0 to the old-attribute pointer: every other handle and glyph entry point in this library reports the invalid object instead, and a title that passes one is already failing. shadPS4 leaves both exports unimplemented, so there is no second implementation to compare. PPSA01325 calls the glyph define with 0x11 on a glyph from `sceFontGenerateCharGlyph` that it deletes unused and draws with `sceFontRenderCharGlyphImageHorizontal`; the three bindings all read the third argument as an out pointer, not a value, so this call sets the glyph's attribute to 0x11. The attribute changes neither shaping nor rendering. +- [sceFontGlyphRenderImage, sceFontGlyphRenderImageHorizontal, sceFontGlyphRenderImageVertical](../../../core/libs/prx/libSceFont/src/Render.cpp) (libSceFont) - unchecked on a console or a title, and [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/font/font.cpp) only stubs them. They draw the glyph's character from its owner font the way `sceFontRenderCharGlyphImage` and its directional forms do, at the scale the glyph was generated with, as `sceFontGlyphRefersOutline` uses it. The renderer argument stands in for the bound renderer and is checked as in `sceFontBindRenderer`; the style frame argument, else the surface's frame, is applied as a surface frame is in `sceFontRenderCharGlyphImage`, and an invalid one returns `SCE_FONT_ERROR_INVALID_PARAMETER`. Assumed: the argument frame wins over the surface's, the error order, `SCE_FONT_ERROR_INVALID_FONT_HANDLE` once the owner font is closed, and the generic form taking its direction from the owner font ### Functional @@ -32,7 +33,7 @@ Frame lifecycle, drawing completion, device usage, frame policy, clipping and fill setup/update argument signatures in [Unimplemented.cpp](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp) are recovered. Frame and resource layouts remain opaque; their runtime implementation is still unavailable. -Glyph image rendering uses six pointer arguments and two scalar coordinates, with existing style, surface, metrics and output layouts. Positioning takes a mask and two floats. Control, word-character search and resource release/exchange/render signatures in [Unimplemented.cpp](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp) are recovered; control payload types depend on the command and remain unmodeled. +Positioning takes a mask and two floats. Control, word-character search and resource release/exchange/render signatures in [Unimplemented.cpp](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp) are recovered; control payload types depend on the command and remain unmodeled. Canvas, design, surface texture and fill-pattern construction signatures in [Unimplemented.cpp](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp) are recovered. Pattern mode/count semantics and the graphics surface descriptor fields still require reconstruction; distinct opaque types preserve the confirmed object categories. From 52f67778cca98be236a0cbf966b48eb1c00d0935 Mon Sep 17 00:00:00 2001 From: thuanquang Date: Sat, 24 Oct 2026 08:32:04 +0700 Subject: [PATCH 039/198] perf(agc): write CPU-path frame dumps through the frame dump writer With APS5_DUMP_FRAMES, PresentDisplayBuffer reads most dumped frames back on the GPU and hands them to the frame dump writer thread. The frames it takes from the CPU were written with WriteFrameBmp on the presenter instead: every dumped frame with APS5_NO_GPU_DUMP=1, and on an HDR10 swapchain the linear display buffers and the tiled ones not shown from a resident image. The write ran inside Driver::Present with the GPU mutex held, and VideoOut completes the flip only after Present returns, so a slow disk stopped the title and the queue workers. A trace on main 4997726a5f (PPSA14363 at 1920x1080, APS5_NO_GPU_DUMP=1, APS5_DUMP_FRAMES_EVERY=60, during a parallel build and another game) has its five longest presents, 16.0 s, 8.2 s, 3.2 s, 1.0 s and 0.84 s, spent in Driver.Present.present_display_buffer while the queue worker waited for the GPU mutex. The traced display buffer decode inside them takes 11 to 17 ms; the rest is the write of one 518 KB BMP. These frames are now reduced by the dump scale on the presenter, as WriteFrameBmp did, and queued to the writer, which writes them in order on its own thread. Every queued frame is now already reduced, so WriteFrameBmp writes it as it is and the writer no longer carries a scale. What stays the same: - the index is taken before present(), so file names and numbering are unchanged and a dropped presentation keeps its CPU dump; - the queue holds reduced frames, as for GPU readbacks since #4320, and the presenter waits only once 64 MiB are queued (#8807); - the writer skips frames once the output is disabled and writes the queued ones when the device is torn down, as for GPU dumps. What differs from GPU readbacks: those are usually queued after the GPU mutex is released, while a CPU dump is always queued inside Present, so a wait for the 64 MiB limit still holds the GPU mutex. At the default APS5_DUMP_SCALE=4 that wait starts once 129 frames of 1920x1080 are queued; at APS5_DUMP_SCALE=1 and 3840x2160, from the third. CPU dumps still queued are lost when the process ends without tearing the device down, as GPU dumps already are; before, each was on disk when Present returned. takeScreenshot takes a requested screenshot only while the writer has nothing queued. A present's CPU dump is queued after that check, so the dump of the same frame does not hold the screenshot back; it moves to a later present only while earlier frames are still being written, as with GPU dumps. video_out_frame_dump_cpu_queue runs the frame dump queue test with APS5_NO_GPU_DUMP=1: frame_000.bmp is a FIFO nobody reads until all four presents have completed. Co-Authored-By: Claude --- .../Execution/src/VulkanDevice.cpp | 42 +++++++++++-------- core/libs/prx/libSceVideoOut/CMakeLists.txt | 3 ++ 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index b550f3ac90..e486cd5c62 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -192,7 +192,7 @@ bool CaptureComputePipeline(std::size_t words) { using ResourceCache = Graphics::ResourceCache; std::uint32_t DumpScale(); -void WriteFrameBmp(int index, std::uint32_t fullWidth, std::uint32_t fullHeight, std::span full, std::uint32_t scale); +void WriteFrameBmp(int index, std::uint32_t width, std::uint32_t height, std::span pixels); FrameDumpOutput frameDumpOutput; class FrameDumpWriter { @@ -206,7 +206,7 @@ public: if (worker.joinable()) worker.join(); } - void Enqueue(int index, std::uint32_t width, std::uint32_t height, std::uint32_t scale, bool screenshot, std::vector pixels) { + void Enqueue(int index, std::uint32_t width, std::uint32_t height, bool screenshot, std::vector pixels) { std::unique_lock lock(mutex); require(!stopping, "frame dump writer has stopped"); if (!screenshot && !frameDumpOutput.Enabled_nid_no_patch()) return; @@ -215,7 +215,7 @@ public: require(!stopping, "frame dump writer has stopped"); if (!worker.joinable()) worker = std::thread([this] { run(); }); const auto bytes = pixels.size(); - pending.push_back({index, width, height, scale, screenshot, std::move(pixels)}); + pending.push_back({index, width, height, screenshot, std::move(pixels)}); queuedBytes += bytes; changed.notify_all(); } @@ -230,7 +230,6 @@ private: int index; std::uint32_t width; std::uint32_t height; - std::uint32_t scale; bool screenshot; std::vector pixels; }; @@ -247,7 +246,7 @@ private: } for (const auto& frame : batch) { if (frame.screenshot) std::fprintf(stderr, "[gpu] screenshot saved: %s\n", Screenshot::Write(GetAppTitleId_nid_postfix().value, frame.width, frame.height, frame.pixels).string().c_str()); - else WriteFrameBmp(frame.index, frame.width, frame.height, frame.pixels, frame.scale); + else WriteFrameBmp(frame.index, frame.width, frame.height, frame.pixels); std::lock_guard lock(mutex); queuedBytes -= frame.pixels.size(); changed.notify_all(); @@ -794,7 +793,7 @@ struct VulkanDevice::State { const auto index = slot.dumpIndex; if (slot.dumpScreenshot) { require(full.size() == static_cast(slot.dumpWidth) * slot.dumpHeight * 4, "invalid screenshot pixels"); - dumpWriter.Enqueue(index, slot.dumpWidth, slot.dumpHeight, 1, true, std::vector(full.begin(), full.end())); + dumpWriter.Enqueue(index, slot.dumpWidth, slot.dumpHeight, true, std::vector(full.begin(), full.end())); return; } const auto scale = slot.dumpScale; @@ -807,7 +806,7 @@ struct VulkanDevice::State { } static const auto start = std::chrono::steady_clock::now(); AgcDriver::ProfilePrint_nid_no_patch("[gpu] frame_%03d.bmp: %ux%u display read back on the GPU at %.1f s\n", index, slot.dumpWidth, slot.dumpHeight, std::chrono::duration(std::chrono::steady_clock::now() - start).count()); - dumpWriter.Enqueue(index, width, height, 1, false, std::move(pixels)); + dumpWriter.Enqueue(index, width, height, false, std::move(pixels)); } // Blocks on the fence, or polls it every 50 us with APS5_PRESENT_POLL_FENCE=1 (the difference @@ -3201,16 +3200,20 @@ std::uint32_t DumpScale() { return scale; } -// frame_.bmp: every DumpScale()-th pixel of a BGRA8 image, 32-bit top-down. -void WriteFrameBmp(int index, std::uint32_t fullWidth, std::uint32_t fullHeight, std::span full, std::uint32_t scale) { - require(scale != 0 && full.size() == static_cast(fullWidth) * fullHeight * 4, "invalid frame dump pixels"); - if (!frameDumpOutput.Enabled_nid_no_patch()) return; +void QueueFrameDump(FrameDumpWriter& writer, int index, std::uint32_t fullWidth, std::uint32_t fullHeight, std::span full) { + const auto scale = DumpScale(); + require(full.size() == static_cast(fullWidth) * fullHeight * 4, "invalid frame dump pixels"); const std::uint32_t width = (fullWidth + scale - 1) / scale; const std::uint32_t height = (fullHeight + scale - 1) / scale; std::vector pixels(static_cast(width) * height * 4); for (std::uint32_t y = 0; y < height; ++y) { for (std::uint32_t x = 0; x < width; ++x) std::memcpy(pixels.data() + (static_cast(y) * width + x) * 4, full.data() + (static_cast(y) * scale * fullWidth + x * scale) * 4, 4); } + writer.Enqueue(index, width, height, false, std::move(pixels)); +} + +void WriteFrameBmp(int index, std::uint32_t width, std::uint32_t height, std::span pixels) { + if (!frameDumpOutput.Enabled_nid_no_patch()) return; char name[32]; std::snprintf(name, sizeof(name), "frame_%03d.bmp", index); frameDumpOutput.Write_nid_no_patch(name, width, height, pixels); @@ -3326,24 +3329,25 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) { if (passthrough) ensureHdrScaler(DisplayTexelFormat(buffer.pixelFormat)); const auto pixels = passthrough ? ReadDisplayBufferTexels(buffer) : ReadDisplayBuffer(buffer); bool dumpFrame = false; + std::optional cpuDump; if (frameDumpOutput.Enabled_nid_no_patch() && dumps.dumped < dumps.limit && ++dumps.presents % static_cast(dumps.every) == 0) { - if (passthrough) { - WriteFrameBmp(dumps.dumped++, buffer.width, buffer.height, ReadDisplayBuffer(buffer), DumpScale()); - } else if (dumps.cpu) { - WriteFrameBmp(dumps.dumped++, buffer.width, buffer.height, pixels, DumpScale()); + if (passthrough || dumps.cpu) { + cpuDump = dumps.dumped++; } else { state->nextDumpIndex = dumps.dumped++; dumpFrame = true; } } const bool screenshot = takeScreenshot(dumpFrame); + if (cpuDump && passthrough) QueueFrameDump(state->dumpWriter, *cpuDump, buffer.width, buffer.height, ReadDisplayBuffer(buffer)); + else if (cpuDump) QueueFrameDump(state->dumpWriter, *cpuDump, buffer.width, buffer.height, pixels); auto shot = screenshot && passthrough ? ReadDisplayBuffer(buffer) : std::vector{}; if (!present(buffer.width, buffer.height, true, pixels, nullptr, nullptr, VK_FILTER_LINEAR, dumpFrame || (screenshot && !passthrough), false, nullptr, 0, passthrough)) { if (dumpFrame) --dumps.dumped; if (screenshot) Screenshot::Request(); return false; } - if (!shot.empty()) state->dumpWriter.Enqueue(0, buffer.width, buffer.height, 1, true, std::move(shot)); + if (!shot.empty()) state->dumpWriter.Enqueue(0, buffer.width, buffer.height, true, std::move(shot)); return true; } static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; @@ -3388,9 +3392,10 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) { } const auto cleared = buffer.dccAddress != 0 && resident == nullptr ? CompressedClearPixel(buffer, bytes) : std::nullopt; bool dumpFrame = false; + std::optional cpuDump; if (frameDumpOutput.Enabled_nid_no_patch() && dumps.dumped < dumps.limit && ++dumps.presents % static_cast(dumps.every) == 0) { if (dumps.cpu || (passthrough && resident == nullptr)) { - WriteFrameBmp(dumps.dumped++, buffer.width, buffer.height, DisplayPixels(buffer, cleared), DumpScale()); + cpuDump = dumps.dumped++; } else { state->nextDumpIndex = dumps.dumped++; dumpFrame = true; @@ -3402,6 +3407,7 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) { AgcDriver::ProfilePrint_nid_no_patch("[flip] (cumulative): %llu presents from the resident image (%llu refreshed first), through guest memory: %llu not pending, %llu unsuitable; %llu GPU frame dumps\n", static_cast(residentPresents), static_cast(refreshedPresents), static_cast(notPending), static_cast(unsuitable), static_cast(gpuDumps)); } const bool screenshot = takeScreenshot(dumpFrame); + if (cpuDump) QueueFrameDump(state->dumpWriter, *cpuDump, buffer.width, buffer.height, DisplayPixels(buffer, cleared)); const bool cpuScreenshot = screenshot && passthrough && resident == nullptr; auto shot = cpuScreenshot ? DisplayPixels(buffer, cleared) : std::vector{}; CaptureTrace::Log("present dump=%d address=%llx width=%u height=%u resident=%d generation=%llu", dumpFrame ? state->nextDumpIndex : -1, static_cast(buffer.address), buffer.width, buffer.height, resident != nullptr, static_cast(resident ? resident->Generation() : 0)); @@ -3419,7 +3425,7 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) { if (screenshot) Screenshot::Request(); return false; } - if (cpuScreenshot) state->dumpWriter.Enqueue(0, buffer.width, buffer.height, 1, true, std::move(shot)); + if (cpuScreenshot) state->dumpWriter.Enqueue(0, buffer.width, buffer.height, true, std::move(shot)); return true; } diff --git a/core/libs/prx/libSceVideoOut/CMakeLists.txt b/core/libs/prx/libSceVideoOut/CMakeLists.txt index 5acccb7a32..c9c82f49ad 100644 --- a/core/libs/prx/libSceVideoOut/CMakeLists.txt +++ b/core/libs/prx/libSceVideoOut/CMakeLists.txt @@ -141,6 +141,9 @@ if(BUILD_TESTING AND NOT WIN32) add_test(NAME video_out_frame_dump_wait COMMAND video_out_frame_dump_queue_tests wait) set_tests_properties(video_out_frame_dump_wait PROPERTIES TIMEOUT 30 SKIP_RETURN_CODE 77 RESOURCE_LOCK videoout_app0 ENVIRONMENT "SDL_VIDEODRIVER=${ANYPS5_VIDEO_OUT_TEST_DRIVER};APS5_DUMP_FRAMES=4;APS5_DUMP_SCALE=1") + add_test(NAME video_out_frame_dump_cpu_queue COMMAND video_out_frame_dump_queue_tests) + set_tests_properties(video_out_frame_dump_cpu_queue PROPERTIES TIMEOUT 30 SKIP_RETURN_CODE 77 RESOURCE_LOCK videoout_app0 + ENVIRONMENT "SDL_VIDEODRIVER=${ANYPS5_VIDEO_OUT_TEST_DRIVER};APS5_DUMP_FRAMES=4;APS5_DUMP_SCALE=4;APS5_NO_GPU_DUMP=1") endif() if(BUILD_TESTING) From f4e9c0d208884cdbab62c6495b01a580b770ebe0 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sun, 25 Oct 2026 09:30:24 +0000 Subject: [PATCH 040/198] feat(libSceAppContent): answer the entitlement-id, Sp, Shrink, IRO-tag and PFT calls Seven pending add-content exports take dlc_emu's prototypes and the answers dlc_emu gives for content it does not know, after the library's initialization guard: NOT_FOUND for the entitlement-id mount and enqueue, the Sp enqueue and Shrink; DRM_NO_ENTITLEMENT for the entitlement-id info; 0 entries for the IRO-tag list; PFT flag 0. A padded label answers PARAMETER from SDK 0x03500000. dlc_emu checks the padding at every SDK while the done label calls gate it, so Sp and Shrink throw for a padded label below 0x03500000. --- core/libs/prx/libSceAppContent/Export.cpp | 54 +++++++++++------ .../libs/prx/libSceAppContent/include/Api.hpp | 14 ++--- core/libs/tests/GuestAppContent.cpp | 60 +++++++++++++++++++ docs/dev/debt/libSceAppContent.md | 4 +- 4 files changed, 105 insertions(+), 27 deletions(-) diff --git a/core/libs/prx/libSceAppContent/Export.cpp b/core/libs/prx/libSceAppContent/Export.cpp index e5c956f2ee..97c9df1539 100644 --- a/core/libs/prx/libSceAppContent/Export.cpp +++ b/core/libs/prx/libSceAppContent/Export.cpp @@ -228,24 +228,34 @@ int APS5_VABI sceAppContentGetAddcontDownloadProgress(uint32_t service_label, co if (!validEntitlementLabel(*entitlement_label, 0x03500000)) return SCE_APP_CONTENT_ERROR_PARAMETER; return SCE_APP_CONTENT_ERROR_NOT_FOUND; } -int APS5_VABI sceAppContentAddcontEnqueueDownloadByEntitlemetId(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAppContentAddcontEnqueueDownloadByEntitlemetId(const char* entitlementId) { + if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED; + if (!entitlementId) return SCE_APP_CONTENT_ERROR_PARAMETER; + return SCE_APP_CONTENT_ERROR_NOT_FOUND; } -int APS5_VABI sceAppContentAddcontEnqueueDownloadSp(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAppContentAddcontEnqueueDownloadSp(uint32_t serviceLabel, const NpUnifiedEntitlementLabel* entitlementLabel) { + if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED; + (void)serviceLabel; + if (!entitlementLabel) return SCE_APP_CONTENT_ERROR_PARAMETER; + if (!validEntitlementLabel(*entitlementLabel, 0x03500000)) return SCE_APP_CONTENT_ERROR_PARAMETER; + if (!validEntitlementLabel(*entitlementLabel, 0)) throw std::runtime_error("sceAppContentAddcontEnqueueDownloadSp: padded entitlement label before SDK 0x03500000"); + return SCE_APP_CONTENT_ERROR_NOT_FOUND; } -int APS5_VABI sceAppContentAddcontMountByEntitlemetId(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAppContentAddcontMountByEntitlemetId(const char* entitlementId, AppContentMountPoint* mountPoint) { + if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED; + if (!entitlementId || !mountPoint) return SCE_APP_CONTENT_ERROR_PARAMETER; + return SCE_APP_CONTENT_ERROR_NOT_FOUND; } -int APS5_VABI sceAppContentAddcontShrink(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAppContentAddcontShrink(uint32_t serviceLabel, const NpUnifiedEntitlementLabel* entitlementLabel) { + if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED; + (void)serviceLabel; + if (!entitlementLabel) return SCE_APP_CONTENT_ERROR_PARAMETER; + if (!validEntitlementLabel(*entitlementLabel, 0x03500000)) return SCE_APP_CONTENT_ERROR_PARAMETER; + if (!validEntitlementLabel(*entitlementLabel, 0)) throw std::runtime_error("sceAppContentAddcontShrink: padded entitlement label before SDK 0x03500000"); + return SCE_APP_CONTENT_ERROR_NOT_FOUND; } int APS5_VABI sceAppContentAppParamGetString(void) { @@ -273,13 +283,17 @@ int APS5_VABI sceAppContentDownload1Shrink(void) { return 0; } -int APS5_VABI sceAppContentGetAddcontInfoByEntitlementId(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAppContentGetAddcontInfoByEntitlementId(const char* entitlementId, AppContentAddcontInfo* info) { + if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED; + if (!entitlementId || !info) return SCE_APP_CONTENT_ERROR_PARAMETER; + return SCE_APP_CONTENT_ERROR_DRM_NO_ENTITLEMENT; } -int APS5_VABI sceAppContentGetAddcontInfoListByIroTag(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceAppContentGetAddcontInfoListByIroTag(uint32_t iroTag, AppContentAddcontInfo* list, uint32_t listNum, uint32_t* hitNum) { + if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED; + (void)iroTag; + if ((!list || listNum == 0) && !hitNum) return SCE_APP_CONTENT_ERROR_PARAMETER; + if (hitNum) *hitNum = 0; return 0; } @@ -293,8 +307,10 @@ int APS5_VABI sceAppContentGetEntitlementKey(void) { return 0; } -int APS5_VABI sceAppContentGetPftFlag(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceAppContentGetPftFlag(uint32_t* pftFlag) { + if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED; + if (!pftFlag) return SCE_APP_CONTENT_ERROR_PARAMETER; + *pftFlag = 0; return 0; } diff --git a/core/libs/prx/libSceAppContent/include/Api.hpp b/core/libs/prx/libSceAppContent/include/Api.hpp index 938424fd88..6bd26b60f9 100644 --- a/core/libs/prx/libSceAppContent/include/Api.hpp +++ b/core/libs/prx/libSceAppContent/include/Api.hpp @@ -24,20 +24,20 @@ int APS5_VABI sceAppContentTemporaryDataUnmount(const AppContentMountPoint* moun int APS5_VABI sceAppContentAddcontEnqueueDownload(uint32_t serviceLabel, const NpUnifiedEntitlementLabel* entitlementLabel); int APS5_VABI sceAppContentAddcontDelete(uint32_t serviceLabel, const NpUnifiedEntitlementLabel* entitlementLabel); int APS5_VABI sceAppContentGetAddcontDownloadProgress(uint32_t serviceLabel, const NpUnifiedEntitlementLabel* entitlementLabel, void* progress); -int APS5_VABI sceAppContentAddcontEnqueueDownloadByEntitlemetId(void); -int APS5_VABI sceAppContentAddcontEnqueueDownloadSp(void); -int APS5_VABI sceAppContentAddcontMountByEntitlemetId(void); -int APS5_VABI sceAppContentAddcontShrink(void); +int APS5_VABI sceAppContentAddcontEnqueueDownloadByEntitlemetId(const char* entitlementId); +int APS5_VABI sceAppContentAddcontEnqueueDownloadSp(uint32_t serviceLabel, const NpUnifiedEntitlementLabel* entitlementLabel); +int APS5_VABI sceAppContentAddcontMountByEntitlemetId(const char* entitlementId, AppContentMountPoint* mountPoint); +int APS5_VABI sceAppContentAddcontShrink(uint32_t serviceLabel, const NpUnifiedEntitlementLabel* entitlementLabel); int APS5_VABI sceAppContentAppParamGetString(void); int APS5_VABI sceAppContentDownload0Expand(void); int APS5_VABI sceAppContentDownload0Shrink(void); int APS5_VABI sceAppContentDownload1Expand(void); int APS5_VABI sceAppContentDownload1Shrink(void); -int APS5_VABI sceAppContentGetAddcontInfoByEntitlementId(void); -int APS5_VABI sceAppContentGetAddcontInfoListByIroTag(void); +int APS5_VABI sceAppContentGetAddcontInfoByEntitlementId(const char* entitlementId, AppContentAddcontInfo* info); +int APS5_VABI sceAppContentGetAddcontInfoListByIroTag(uint32_t iroTag, AppContentAddcontInfo* list, uint32_t listNum, uint32_t* hitNum); int APS5_VABI sceAppContentGetDownloadedStoreCountry(void); int APS5_VABI sceAppContentGetEntitlementKey(void); -int APS5_VABI sceAppContentGetPftFlag(void); +int APS5_VABI sceAppContentGetPftFlag(uint32_t* pftFlag); int APS5_VABI sceAppContentGetRegion(void); int APS5_VABI sceAppContentRequestPatchInstall(void); int APS5_VABI sceAppContentTemporaryDataMount(AppContentMountPoint* mountPoint); diff --git a/core/libs/tests/GuestAppContent.cpp b/core/libs/tests/GuestAppContent.cpp index 876515f966..3794df6683 100644 --- a/core/libs/tests/GuestAppContent.cpp +++ b/core/libs/tests/GuestAppContent.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include "ProcessParametersFixture.hpp" static constexpr int ErrorParameter = static_cast(0x80D90002); @@ -17,6 +18,15 @@ static constexpr int ErrorBusy = static_cast(0x80D90003); static constexpr int ErrorNotMounted = static_cast(0x80D90004); static void Require(bool value) { if (!value) std::abort(); } +template static bool Throws(Call call) { + try { + call(); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + int main() { static std::array process{}; Require(ProcessParametersFixture::Install(process.data(), process.size())); @@ -37,6 +47,15 @@ int main() { Require(sceAppContentAddcontDelete(0, nullptr) == notInitialized); Require(sceAppContentAddcontEnqueueDownload(0, nullptr) == notInitialized); Require(sceAppContentGetAddcontDownloadProgress(0, nullptr, nullptr) == notInitialized); + Require(sceAppContentAddcontEnqueueDownloadByEntitlemetId(nullptr) == notInitialized); + Require(sceAppContentAddcontEnqueueDownloadSp(0, nullptr) == notInitialized); + Require(sceAppContentAddcontMountByEntitlemetId(nullptr, nullptr) == notInitialized); + Require(sceAppContentAddcontShrink(0, nullptr) == notInitialized); + Require(sceAppContentGetAddcontInfoByEntitlementId(nullptr, nullptr) == notInitialized); + Require(sceAppContentGetAddcontInfoListByIroTag(0, nullptr, 0, nullptr) == notInitialized); + std::uint32_t pftFlag = 0x5a5a5a5a; + Require(sceAppContentGetPftFlag(&pftFlag) == notInitialized); + Require(pftFlag == 0x5a5a5a5a); const auto original = std::filesystem::current_path(); const auto directory = original / ("appcontent-test-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); Require(std::filesystem::create_directory(directory)); @@ -192,6 +211,29 @@ int main() { Require(std::memcmp(info, untouchedInfo, sizeof(info)) == 0); Require(sceAppContentGetAddcontInfo(0, nullptr, reinterpret_cast(info)) == ErrorParameter); Require(sceAppContentGetAddcontInfo(0, &label, nullptr) == ErrorParameter); + const char entitlementId[] = "UP0000-PPSA00000_00-ADDCONT000000001"; + Require(sceAppContentAddcontEnqueueDownloadByEntitlemetId(entitlementId) == ErrorNotFound); + Require(sceAppContentAddcontEnqueueDownloadByEntitlemetId(nullptr) == ErrorParameter); + Require(sceAppContentAddcontEnqueueDownloadSp(0, &label) == ErrorNotFound); + Require(sceAppContentAddcontEnqueueDownloadSp(7, &label) == ErrorNotFound); + Require(sceAppContentAddcontEnqueueDownloadSp(0, nullptr) == ErrorParameter); + Require(sceAppContentAddcontShrink(0, &label) == ErrorNotFound); + Require(sceAppContentAddcontShrink(7, &label) == ErrorNotFound); + Require(sceAppContentAddcontShrink(0, nullptr) == ErrorParameter); + std::memset(&mountPoint, 0x5a, sizeof(mountPoint)); + Require(sceAppContentAddcontMountByEntitlemetId(entitlementId, &mountPoint) == ErrorNotFound); + Require(std::memcmp(&mountPoint, &untouched, sizeof(mountPoint)) == 0); + Require(sceAppContentAddcontMountByEntitlemetId(nullptr, &mountPoint) == ErrorParameter); + Require(sceAppContentAddcontMountByEntitlemetId(entitlementId, nullptr) == ErrorParameter); + Require(std::memcmp(&mountPoint, &untouched, sizeof(mountPoint)) == 0); + Require(sceAppContentGetAddcontInfoByEntitlementId(entitlementId, reinterpret_cast(info)) == ErrorDrmNoEntitlement); + Require(std::memcmp(info, untouchedInfo, sizeof(info)) == 0); + Require(sceAppContentGetAddcontInfoByEntitlementId(nullptr, reinterpret_cast(info)) == ErrorParameter); + Require(sceAppContentGetAddcontInfoByEntitlementId(entitlementId, nullptr) == ErrorParameter); + Require(std::memcmp(info, untouchedInfo, sizeof(info)) == 0); + Require(sceAppContentGetPftFlag(&pftFlag) == 0); + Require(pftFlag == 0); + Require(sceAppContentGetPftFlag(nullptr) == ErrorParameter); for (const std::uint32_t version : {0x014fffffu, 0x01500000u, 0x034fffffu, 0x03500000u}) { std::memcpy(process.data() + 16, &version, sizeof(version)); @@ -205,6 +247,13 @@ int main() { Require(sceAppContentAddcontEnqueueDownload(0, &label) == earlyError); Require(sceAppContentAddcontDelete(0, &label) == lateError); Require(sceAppContentGetAddcontDownloadProgress(0, &label, progress) == lateError); + if (version >= 0x03500000) { + Require(sceAppContentAddcontEnqueueDownloadSp(0, &label) == ErrorParameter); + Require(sceAppContentAddcontShrink(0, &label) == ErrorParameter); + } else { + Require(Throws([&] { sceAppContentAddcontEnqueueDownloadSp(0, &label); })); + Require(Throws([&] { sceAppContentAddcontShrink(0, &label); })); + } Require(std::memcmp(info, untouchedInfo, sizeof(info)) == 0); Require(std::memcmp(progress, progressBefore, sizeof(progress)) == 0); padding = 0; @@ -228,6 +277,17 @@ int main() { Require(sceAppContentGetAddcontInfoList(0, nullptr, 0, nullptr) == ErrorParameter); Require(sceAppContentGetAddcontInfoList(0, reinterpret_cast(info), 0, nullptr) == ErrorParameter); Require(sceAppContentGetAddcontInfoList(0, nullptr, 1, nullptr) == ErrorParameter); + hitNum = 0x5a5a5a5a; + Require(sceAppContentGetAddcontInfoListByIroTag(1, nullptr, 0, &hitNum) == 0); + Require(hitNum == 0); + hitNum = 0x5a5a5a5a; + Require(sceAppContentGetAddcontInfoListByIroTag(1, reinterpret_cast(info), 1, &hitNum) == 0); + Require(hitNum == 0); + Require(sceAppContentGetAddcontInfoListByIroTag(1, reinterpret_cast(info), 1, nullptr) == 0); + Require(std::memcmp(info, untouchedInfo, sizeof(info)) == 0); + Require(sceAppContentGetAddcontInfoListByIroTag(1, nullptr, 0, nullptr) == ErrorParameter); + Require(sceAppContentGetAddcontInfoListByIroTag(1, reinterpret_cast(info), 0, nullptr) == ErrorParameter); + Require(sceAppContentGetAddcontInfoListByIroTag(1, nullptr, 1, nullptr) == ErrorParameter); std::filesystem::current_path(original); std::filesystem::remove_all(directory); } diff --git a/docs/dev/debt/libSceAppContent.md b/docs/dev/debt/libSceAppContent.md index 1780d0bde0..277e96ceed 100644 --- a/docs/dev/debt/libSceAppContent.md +++ b/docs/dev/debt/libSceAppContent.md @@ -6,10 +6,12 @@ - [sceAppContentDownloadDataFormat](../../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) - empties the `/download0` directory, creating it if needed. Imported by PPSA01317. A null mount point returns `PARAMETER` and another mount point returns `NOT_MOUNTED` as in `sceAppContentTemporaryDataFormat` and `sceAppContentDownloadDataGetAvailableSpaceKb`; the console's error codes for both and its behaviour with files still open are unverified -- [libSceAppContent pending exports](../../../core/libs/prx/libSceAppContent/Export.cpp) - 16 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAppContent.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. +- [libSceAppContent pending exports](../../../core/libs/prx/libSceAppContent/Export.cpp) - 9 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAppContent.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. ### Functional - [Initialization](../../../core/libs/prx/libSceAppContent/Export.cpp) - service readiness and boot attributes are not modeled; successful initialization returns a zero boot record. Initialization-state guards, SDK 0x01500000 repeated-initialization errors and reserved-field checks are recovered. SDK-dependent service-error translation remains incomplete. - [Additional content](../../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) is not installed or mounted: `sceAppContentAddcontMount` answers `NOT_FOUND` for every entitlement label and `sceAppContentAddcontUnmount` for every mount point, as no add-content mount is ever handed out; `sceAppContentGetAddcontInfo` answers `DRM_NO_ENTITLEMENT` for every label and `sceAppContentGetAddcontInfoList` reports 0 entries; `sceAppContentAddcontEnqueueDownload` answers `PARAMETER` for a null label and `NOT_FOUND` otherwise, since no download is ever queued; `sceAppContentAddcontDelete` answers `PARAMETER` for a null label and `NOT_FOUND` otherwise, since no additional content is ever installed. `sceAppContentGetAddcontDownloadProgress` answers `PARAMETER` for a null label or progress and `NOT_FOUND` otherwise, as [dlc_emu](https://github.com/drakmor/dlc_emu/blob/fcf69b16d46a9850578bce19dc2c1b4dc867cbb5/src/dlc_modules/dlc_content.cpp#L2328-L2341) does for a label that is not installed additional content, and never writes the progress. Its 16-byte layout (`DataSize`, then `DownloadedSize`, both 64-bit) follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/AppContent/AppContent.cs#L45-L54) and is unused here. The signatures of `sceAppContentAddcontEnqueueDownload` and `sceAppContentGetAddcontDownloadProgress` follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/AppContent/AppContent.cs#L140-L148), and the signature of `sceAppContentAddcontDelete` follows [dlc_emu](https://github.com/drakmor/dlc_emu/blob/fcf69b16d46a9850578bce19dc2c1b4dc867cbb5/src/dlc_modules/dlc_content.h#L91-L92); dlc_emu is not a console measurement and no console source gives the error codes, so the signatures and the error codes are unverified +- [sceAppContentAddcontEnqueueDownloadByEntitlemetId, sceAppContentAddcontEnqueueDownloadSp, sceAppContentAddcontMountByEntitlemetId and sceAppContentAddcontShrink](../../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) - unverified on a PS5. After the initialization guard they answer `PARAMETER` for a null argument and `NOT_FOUND` otherwise, without writing the mount point, as [dlc_emu](https://github.com/drakmor/dlc_emu/blob/fcf69b16d46a9850578bce19dc2c1b4dc867cbb5/src/dlc_modules/dlc_content.cpp#L2125-L2209) does for content it does not know; the prototypes are [dlc_emu's](https://github.com/drakmor/dlc_emu/blob/fcf69b16d46a9850578bce19dc2c1b4dc867cbb5/src/dlc_modules/dlc_content.h#L88-L99). dlc_emu rejects a padded label at every SDK while the label calls above check it only from SDK 0x01500000 or 0x03500000, so `Sp` and `Shrink` answer `PARAMETER` for one from SDK 0x03500000 and throw below it +- [sceAppContentGetAddcontInfoByEntitlementId, sceAppContentGetAddcontInfoListByIroTag and sceAppContentGetPftFlag](../../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) - unverified on a PS5. After the initialization guard they give [dlc_emu's](https://github.com/drakmor/dlc_emu/blob/fcf69b16d46a9850578bce19dc2c1b4dc867cbb5/src/dlc_modules/dlc_content.cpp#L2040-L2086) answers for content it does not know: `DRM_NO_ENTITLEMENT` without writing the info, 0 entries for any tag (a null `hitNum` beside a list returns 0, unlike `sceAppContentGetAddcontInfoList`), and a [PFT flag of 0](https://github.com/drakmor/dlc_emu/blob/fcf69b16d46a9850578bce19dc2c1b4dc867cbb5/src/dlc_modules/dlc_content.cpp#L2343-L2347), and `PARAMETER` for null arguments, with dlc_emu's [prototypes](https://github.com/drakmor/dlc_emu/blob/fcf69b16d46a9850578bce19dc2c1b4dc867cbb5/src/dlc_modules/dlc_content.h#L74-L131) - [Temporary app-content storage](../../../core/libs/prx/libSceAppContent/Export.cpp) console behaviour and hardware return values are unverified, including behaviour with open files across unmount; host filesystem failures throw. Storage models one `/temp0` mount, preserving files on unmount while rejecting new path resolutions until remount, and leaving existing open host descriptors usable. `sceAppContentTemporaryDataMount` delegates to `sceAppContentTemporaryDataMount2` with option 1 (`TEMPORARY_DATA_OPTION_FORMAT`) to format on mount as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/app_content/app_content.cpp#L550-L553) does. Mount, format, space-query and unmount errors follow [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/sys/sys_path.pas); pointer ABI follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/AppContent/AppContent.cs). From 6e485fb3b9c54939ed76691e0e02862cc4f16189 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sun, 25 Oct 2026 09:40:31 +0000 Subject: [PATCH 041/198] fix(libSceNpEntitlementAccess): report the request abort and delete calls as not implemented sceNpEntitlementAccessAbortRequest and sceNpEntitlementAccessDeleteRequest threw a bare logic_error on every call, so progress.py counted them as done. The library keeps no request table, and no source gives their behaviour, so both now go through NotImplemented_nid_no_patch. Their check moves from guest_offline_imports to the library's own guest_entitlements test. --- .../prx/libSceNpEntitlementAccess/Export.cpp | 6 ++++-- core/libs/tests/GuestEntitlements.cpp | 14 ++++++++++++++ core/libs/tests/GuestOfflineImports.cpp | 18 ------------------ docs/dev/debt/libSceNpEntitlementAccess.md | 2 +- 4 files changed, 19 insertions(+), 21 deletions(-) diff --git a/core/libs/prx/libSceNpEntitlementAccess/Export.cpp b/core/libs/prx/libSceNpEntitlementAccess/Export.cpp index e0593d4afb..44530aadcd 100644 --- a/core/libs/prx/libSceNpEntitlementAccess/Export.cpp +++ b/core/libs/prx/libSceNpEntitlementAccess/Export.cpp @@ -126,11 +126,13 @@ int APS5_VABI sceNpEntitlementAccessInitialize(const NpEntitlementAccessInitPara } int APS5_VABI sceNpEntitlementAccessAbortRequest(std::int64_t requestId) { - throw std::logic_error("sceNpEntitlementAccessAbortRequest: asynchronous request tracking is unimplemented"); + NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI sceNpEntitlementAccessDeleteRequest(std::int64_t requestId) { - throw std::logic_error("sceNpEntitlementAccessDeleteRequest: asynchronous request tracking is unimplemented"); + NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI sceNpEntitlementAccessGenerateTransactionId(NpEntitlementAccessTransactionId* transactionId) { diff --git a/core/libs/tests/GuestEntitlements.cpp b/core/libs/tests/GuestEntitlements.cpp index 6960f9b349..843f3fd89b 100644 --- a/core/libs/tests/GuestEntitlements.cpp +++ b/core/libs/tests/GuestEntitlements.cpp @@ -14,6 +14,16 @@ static void Require(bool value) { if (!value) std::abort(); } +template +static bool NotImplemented(TCall call, const std::string& name) { + try { + call(); + } catch (const std::runtime_error& error) { + return std::string(error.what()) == name + " not implemented"; + } + return false; +} + static void SetEntitlements(const std::filesystem::path& file) { #ifdef _WIN32 Require(_wputenv_s(L"ANYPS5_ENTITLEMENTS", file.wstring().c_str()) == 0); @@ -50,6 +60,10 @@ int main(int argc, char** argv) { std::puts("Guest entitlements rejected a missing configured file"); return 0; } + for (std::int64_t requestId : {std::int64_t{0}, std::int64_t{1}, INT64_MAX}) { + Require(NotImplemented([requestId] { sceNpEntitlementAccessAbortRequest(requestId); }, "sceNpEntitlementAccessAbortRequest")); + Require(NotImplemented([requestId] { sceNpEntitlementAccessDeleteRequest(requestId); }, "sceNpEntitlementAccessDeleteRequest")); + } std::filesystem::create_directories(directory); { std::ofstream ini(file, std::ios::binary); diff --git a/core/libs/tests/GuestOfflineImports.cpp b/core/libs/tests/GuestOfflineImports.cpp index 3dd2c7a0b7..9e441d4b0c 100644 --- a/core/libs/tests/GuestOfflineImports.cpp +++ b/core/libs/tests/GuestOfflineImports.cpp @@ -38,24 +38,6 @@ int main() { } } - for (std::int64_t requestId : {std::int64_t{0}, std::int64_t{1}, INT64_MAX}) { - bool abortFailed = false; - bool deleteFailed = false; - try { - sceNpEntitlementAccessAbortRequest(requestId); - - } catch (const std::logic_error&) { - abortFailed = true; - } - try { - sceNpEntitlementAccessDeleteRequest(requestId); - - } catch (const std::logic_error&) { - deleteFailed = true; - } - Require(abortFailed && deleteFailed); - } - constexpr int network = static_cast(0x80431063); Require(sceHttpSendRequest(1, nullptr, 0) == network); diff --git a/docs/dev/debt/libSceNpEntitlementAccess.md b/docs/dev/debt/libSceNpEntitlementAccess.md index 7d54ea4fe1..7ce2b8189c 100644 --- a/docs/dev/debt/libSceNpEntitlementAccess.md +++ b/docs/dev/debt/libSceNpEntitlementAccess.md @@ -4,7 +4,7 @@ - [sceNpEntitlementAccessGetAddcontEntitlementInfoIndividual, sceNpEntitlementAccessPollConsumableEntitlementInfo, sceNpEntitlementAccessRequestConsumableEntitlementInfo, sceNpEntitlementAccessRequestConsumeEntitlement](../../../core/libs/prx/libSceNpEntitlementAccess/Export.cpp) (libSceNpEntitlementAccess) - their names and NIDs are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpEntitlementAccess.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim - [sceNpEntitlementAccessGetEntitlementKey](../../../core/libs/prx/libSceNpEntitlementAccess/Export.cpp) - the three-argument signature, 16-byte key and errors `0x817D0002`/`0x817D0007` follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libNet.cpp); PPSA01521 passes a service label, entitlement label and output pointer. Only the no-key path is implemented: local entitlement labels carry no keys, and the output is untouched on failure. Console error precedence and key provisioning are unverified -- [Entitlement requests and polls](../../../core/libs/prx/libSceNpEntitlementAccess/Export.cpp): native argument lists are restored for single-item, list and consume operations, plus abort/delete. Request IDs are 64-bit, labels occupy 20 bytes and transaction IDs occupy 72 bytes. List options occupy 32 bytes and results have a 64-byte stride; their field semantics and three list-result scalars remain unresolved. The existing signed-out responses do not reproduce native initialization, validation or asynchronous request handling; abort/delete throw until request tracking is implemented. +- [Entitlement requests and polls](../../../core/libs/prx/libSceNpEntitlementAccess/Export.cpp): native argument lists are restored for single-item, list and consume operations, plus abort/delete. Request IDs are 64-bit, labels occupy 20 bytes and transaction IDs occupy 72 bytes. List options occupy 32 bytes and results have a 64-byte stride; their field semantics and three list-result scalars remain unresolved. The existing signed-out responses do not reproduce native initialization, validation or asynchronous request handling; abort and delete go through `NotImplemented_nid_no_patch` until request tracking is implemented, as the library keeps no request table. ### Functional From eacd1ad6109945bef507e5446e9f26dceab374fc Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sun, 25 Oct 2026 09:41:19 +0000 Subject: [PATCH 042/198] fix(libSceContentDelete): report deletion by path and id as not implemented After initialization, sceContentDeleteByPath and sceContentDeleteById threw a bare logic_error for every valid call, so progress.py counted them as done. No source gives the console's error for content that is not in the library, so the valid calls now go through NotImplemented_nid_no_patch. The NOT_INITIALIZED and argument errors before it are unchanged. --- core/libs/prx/libSceContentDelete/Export.cpp | 6 ++++-- core/libs/tests/GuestContentDelete.cpp | 10 +++++----- docs/dev/debt/libSceContentDelete.md | 2 +- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceContentDelete/Export.cpp b/core/libs/prx/libSceContentDelete/Export.cpp index ecd7c66372..f0e72c1b2e 100644 --- a/core/libs/prx/libSceContentDelete/Export.cpp +++ b/core/libs/prx/libSceContentDelete/Export.cpp @@ -28,12 +28,14 @@ int APS5_VABI sceContentDeleteByPath(const char* contentPath) { if (!g_initialized.load()) return NotInitialized; if (contentPath == nullptr) APS5_INVALID_ARG_EX; if (*contentPath == 0) return InvalidArgument; - throw std::logic_error(std::string(__func__) + ": " + contentPath + " is not in the host content library, which is empty, and the console's error for it is unknown"); + NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI sceContentDeleteById(std::int64_t contentId) { if (!g_initialized.load()) return NotInitialized; - throw std::logic_error(std::string(__func__) + ": content id " + std::to_string(contentId) + " is not in the host content library, which is empty, and the console's error for it is unknown"); + NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI sceContentDeleteByIdListForSf(const std::int64_t* contentIds, std::size_t count) { diff --git a/core/libs/tests/GuestContentDelete.cpp b/core/libs/tests/GuestContentDelete.cpp index caafe4a5bd..ad6cfc6411 100644 --- a/core/libs/tests/GuestContentDelete.cpp +++ b/core/libs/tests/GuestContentDelete.cpp @@ -17,12 +17,12 @@ std::string LogicErrorMessage(TAction action) { return "invalid argument"; } catch (const std::logic_error& error) { return error.what(); + } catch (const std::runtime_error& error) { + return error.what(); } return "returned"; } -bool Contains(const std::string& text, const std::string& part) { return text.find(part) != std::string::npos; } - } int main() { @@ -42,9 +42,9 @@ int main() { Require(sceContentDeleteByPath("") == static_cast(0x809D5001u)); Require(LogicErrorMessage([] { sceContentDeleteByPath(nullptr); }) == "invalid argument"); - Require(Contains(LogicErrorMessage([] { sceContentDeleteByPath("/user/av_contents/photo/a.png"); }), "sceContentDeleteByPath: /user/av_contents/photo/a.png is not in the host content library")); - Require(Contains(LogicErrorMessage([] { sceContentDeleteById(0x123456789); }), "sceContentDeleteById: content id 4886718345 is not in the host content library")); - Require(Contains(LogicErrorMessage([] { sceContentDeleteById(-1); }), "sceContentDeleteById: content id -1 is not in the host content library")); + Require(LogicErrorMessage([] { sceContentDeleteByPath("/user/av_contents/photo/a.png"); }) == "sceContentDeleteByPath not implemented"); + Require(LogicErrorMessage([] { sceContentDeleteById(0x123456789); }) == "sceContentDeleteById not implemented"); + Require(LogicErrorMessage([] { sceContentDeleteById(-1); }) == "sceContentDeleteById not implemented"); Require(sceContentDeleteTerminate() == 0); Require(sceContentDeleteTerminate() == static_cast(0x809D5002u)); diff --git a/docs/dev/debt/libSceContentDelete.md b/docs/dev/debt/libSceContentDelete.md index f7365210d5..a67d4ee34f 100644 --- a/docs/dev/debt/libSceContentDelete.md +++ b/docs/dev/debt/libSceContentDelete.md @@ -2,7 +2,7 @@ ### Functional -- [sceContentDeleteById](../../../core/libs/prx/libSceContentDelete/Export.cpp) and `sceContentDeleteByPath`: content database access and deletion are not implemented. Calls with valid arguments after initialization throw; the backend result cannot be inferred from an empty host library. Null paths after initialization throw instead of reproducing the original unchecked string access. +- [sceContentDeleteById](../../../core/libs/prx/libSceContentDelete/Export.cpp) and `sceContentDeleteByPath`: content database access and deletion are not implemented. Calls with valid arguments after initialization go through `NotImplemented_nid_no_patch`: the console's error for content that is not in the library is unknown, and it cannot be inferred from an empty host library. Null paths after initialization throw instead of reproducing the original unchecked string access. - [sceContentDeleteInitialize](../../../core/libs/prx/libSceContentDelete/Export.cpp): validates the confirmed 16 KiB heap requirement and lifecycle errors, but does not create the original allocator, mutex and database client. Initialization-parameter fields other than the 64-bit heap size at offset 8 remain unresolved. ### Unknown function info From 5746326efc33bc56c24b1a7e1192186a783eda83 Mon Sep 17 00:00:00 2001 From: AZLisme <8706522+AZLisme@users.noreply.github.com> Date: Fri, 23 Oct 2026 12:18:43 +0800 Subject: [PATCH 043/198] feat(libSceJsc): implement JavaScriptCore string API --- core/libs/prx/libSceJsc/CMakeLists.txt | 15 +++ core/libs/prx/libSceJsc/Export.cpp | 150 ++++++++++++++++++++- core/libs/prx/libSceJsc/include/Api.hpp | 28 ++++ core/libs/prx/libSceJsc/libSceJsc.hpp | 2 + core/libs/tests/GuestJscString.cpp | 170 ++++++++++++++++++++++++ docs/dev/debt/libSceJsc.md | 9 ++ 6 files changed, 371 insertions(+), 3 deletions(-) create mode 100644 core/libs/prx/libSceJsc/CMakeLists.txt create mode 100644 core/libs/prx/libSceJsc/include/Api.hpp create mode 100644 core/libs/tests/GuestJscString.cpp create mode 100644 docs/dev/debt/libSceJsc.md diff --git a/core/libs/prx/libSceJsc/CMakeLists.txt b/core/libs/prx/libSceJsc/CMakeLists.txt new file mode 100644 index 0000000000..73915bc4ff --- /dev/null +++ b/core/libs/prx/libSceJsc/CMakeLists.txt @@ -0,0 +1,15 @@ +add_library(libSceJsc SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceJsc PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceJsc) + +if(BUILD_TESTING) + add_test_executable(guest_jsc_string_tests ${LIBS_INCLUDE_DIR}/tests/GuestJscString.cpp) + target_include_directories(guest_jsc_string_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_jsc_string_tests PRIVATE libSceJsc libc) + configure_windows_unwind(guest_jsc_string_tests) + add_test(NAME guest_jsc_string COMMAND guest_jsc_string_tests) + if(WIN32) + set_tests_properties(guest_jsc_string PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceJsc/Export.cpp b/core/libs/prx/libSceJsc/Export.cpp index d81f4a5e32..05794b5e68 100644 --- a/core/libs/prx/libSceJsc/Export.cpp +++ b/core/libs/prx/libSceJsc/Export.cpp @@ -1,10 +1,154 @@ +#include "prx/libSceJsc/include/Api.hpp" +#include "prx/libc/include/General.hpp" + +#include #include #include -#include "SceTypes.hpp" -#include "prx/libc/include/General.hpp" +#include +#include +#include +#include + + + +namespace { + +std::unique_ptr Decode(const char* text) { + auto result = std::make_unique(); + const auto* cursor = reinterpret_cast(text); + while (*cursor) { + std::uint32_t codepoint = *cursor++; + unsigned following = 0; + std::uint32_t minimum = 0; + if (codepoint >= 0xC2 && codepoint <= 0xDF) { + codepoint &= 0x1F; + following = 1; + minimum = 0x80; + } else if (codepoint >= 0xE0 && codepoint <= 0xEF) { + codepoint &= 0x0F; + following = 2; + minimum = 0x800; + } else if (codepoint >= 0xF0 && codepoint <= 0xF4) { + codepoint &= 7; + following = 3; + minimum = 0x10000; + } else if (codepoint >= 0x80) { + result->nullString = true; + break; + } + while (following > 0) { + --following; + if ((*cursor & 0xC0) != 0x80) { + result->nullString = true; + break; + } + codepoint = codepoint << 6 | (*cursor++ & 0x3F); + } + if (result->nullString || codepoint < minimum || codepoint > 0x10FFFF || (codepoint >= 0xD800 && codepoint <= 0xDFFF)) { + result->nullString = true; + break; + } + if (codepoint > 0xFFFF) { + const auto supplementary = codepoint - 0x10000; + result->characters.push_back(static_cast(0xD800 + (supplementary >> 10))); + result->characters.push_back(static_cast(0xDC00 + (supplementary & 0x3FF))); + } else result->characters.push_back(static_cast(codepoint)); + } + if (result->nullString) result->characters.clear(); + result->characters.push_back(0); + return result; +} + +bool Equal(const GuestJSString* first, const GuestJSString* second) { + if (first == second) return true; + if (!first || !second) return false; + return first->nullString == second->nullString && first->characters == second->characters; +} + +} extern "C" { -APS5_DUMMY_FUN +GuestJSString* APS5_VABI JSStringCreateWithCharacters(const std::uint16_t* characters, std::size_t count) { + if (!characters && count) NotImplemented_nid_no_patch(__func__); + if (count > static_cast(std::numeric_limits::max())) NotImplemented_nid_no_patch(__func__); + auto string = std::make_unique(); + string->nullString = !characters; + if (count) string->characters.assign(characters, characters + count); + string->characters.push_back(0); + return string.release(); +} + +GuestJSString* APS5_VABI JSStringCreateWithUTF8CString(const char* text) { + if (!text) NotImplemented_nid_no_patch(__func__); + return Decode(text).release(); +} + +GuestJSString* APS5_VABI JSStringRetain(GuestJSString* string) { + if (!string) NotImplemented_nid_no_patch(__func__); + auto references = string->references.load(std::memory_order_relaxed); + for (;;) { + if (references == std::numeric_limits::max()) NotImplemented_nid_no_patch(__func__); + if (string->references.compare_exchange_weak(references, references + 1, std::memory_order_relaxed)) return string; + } +} + +void APS5_VABI JSStringRelease(GuestJSString* string) { + if (!string) NotImplemented_nid_no_patch(__func__); + if (string->references.fetch_sub(1, std::memory_order_acq_rel) == 1) delete string; +} + +std::size_t APS5_VABI JSStringGetLength(const GuestJSString* string) { + return string ? string->characters.size() - 1 : 0; +} + +const std::uint16_t* APS5_VABI JSStringGetCharactersPtr(const GuestJSString* string) { + return string && !string->nullString ? string->characters.data() : nullptr; +} + +std::size_t APS5_VABI JSStringGetMaximumUTF8CStringSize(const GuestJSString* string) { + if (!string) NotImplemented_nid_no_patch(__func__); + const auto length = string->characters.size() - 1; + if (length > (std::numeric_limits::max() - 1) / 3) NotImplemented_nid_no_patch(__func__); + return length * 3 + 1; +} + +std::size_t APS5_VABI JSStringGetUTF8CString(const GuestJSString* string, char* buffer, std::size_t bufferSize) { + if (!string || !buffer || !bufferSize) return 0; + std::size_t written = 0; + const auto count = string->characters.size() - 1; + for (std::size_t index = 0; index < count; ++index) { + std::uint32_t codepoint = string->characters[index]; + if (codepoint >= 0xD800 && codepoint <= 0xDBFF) { + if (index + 1 == count) break; + const auto low = string->characters[index + 1]; + if (low < 0xDC00 || low > 0xDFFF) break; + codepoint = 0x10000 + ((codepoint - 0xD800) << 10) + low - 0xDC00; + ++index; + } else if (codepoint >= 0xDC00 && codepoint <= 0xDFFF) break; + const std::size_t bytes = codepoint < 0x80 ? 1 : codepoint < 0x800 ? 2 : codepoint < 0x10000 ? 3 : 4; + if (bytes > bufferSize - 1 - written) break; + if (bytes == 1) buffer[written++] = static_cast(codepoint); + else { + const unsigned prefix = bytes == 2 ? 0xC0 : bytes == 3 ? 0xE0 : 0xF0; + buffer[written++] = static_cast(prefix | (codepoint >> (6 * (bytes - 1)))); + for (auto remaining = bytes - 1; remaining > 0; --remaining) { + buffer[written++] = static_cast(0x80 | ((codepoint >> (6 * (remaining - 1))) & 0x3F)); + } + } + } + buffer[written] = '\0'; + return written + 1; +} + +bool APS5_VABI JSStringIsEqual(const GuestJSString* first, const GuestJSString* second) { + return Equal(first, second); +} + +bool APS5_VABI JSStringIsEqualToUTF8CString(const GuestJSString* string, const char* text) { + if (!text) NotImplemented_nid_no_patch(__func__); + const auto other = Decode(text); + return Equal(string, other.get()); +} } diff --git a/core/libs/prx/libSceJsc/include/Api.hpp b/core/libs/prx/libSceJsc/include/Api.hpp new file mode 100644 index 0000000000..43b06b1625 --- /dev/null +++ b/core/libs/prx/libSceJsc/include/Api.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include + +struct GuestJSString { + std::atomic references{1}; + std::vector characters; + bool nullString = false; +}; + +extern "C" { + +GuestJSString* APS5_VABI JSStringCreateWithCharacters(const std::uint16_t* characters, std::size_t count); +GuestJSString* APS5_VABI JSStringCreateWithUTF8CString(const char* text); +GuestJSString* APS5_VABI JSStringRetain(GuestJSString* string); +void APS5_VABI JSStringRelease(GuestJSString* string); +std::size_t APS5_VABI JSStringGetLength(const GuestJSString* string); +const std::uint16_t* APS5_VABI JSStringGetCharactersPtr(const GuestJSString* string); +std::size_t APS5_VABI JSStringGetMaximumUTF8CStringSize(const GuestJSString* string); +std::size_t APS5_VABI JSStringGetUTF8CString(const GuestJSString* string, char* buffer, std::size_t bufferSize); +bool APS5_VABI JSStringIsEqual(const GuestJSString* first, const GuestJSString* second); +bool APS5_VABI JSStringIsEqualToUTF8CString(const GuestJSString* string, const char* text); + +} diff --git a/core/libs/prx/libSceJsc/libSceJsc.hpp b/core/libs/prx/libSceJsc/libSceJsc.hpp index 6f70f09bee..28a1869c2c 100644 --- a/core/libs/prx/libSceJsc/libSceJsc.hpp +++ b/core/libs/prx/libSceJsc/libSceJsc.hpp @@ -1 +1,3 @@ #pragma once + +#include "prx/libSceJsc/include/Api.hpp" diff --git a/core/libs/tests/GuestJscString.cpp b/core/libs/tests/GuestJscString.cpp new file mode 100644 index 0000000000..673bde7533 --- /dev/null +++ b/core/libs/tests/GuestJscString.cpp @@ -0,0 +1,170 @@ +#include "prx/libSceJsc/include/Api.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "JavaScriptCore string check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +static void Unicode() { + std::u32string scalars; + for (char32_t codepoint = 1; codepoint <= 0x10FFFF; ++codepoint) { + if (codepoint < 0xD800 || codepoint > 0xDFFF) scalars.push_back(codepoint); + } + std::codecvt_utf8 encode; + std::codecvt_utf8_utf16 decode; + std::mbstate_t state{}; + const char32_t* readScalars = nullptr; + std::string utf8(scalars.size() * 4, '\0'); + char* writtenBytes = nullptr; + Require(encode.out(state, scalars.data(), scalars.data() + scalars.size(), readScalars, + utf8.data(), utf8.data() + utf8.size(), writtenBytes) == std::codecvt_base::ok); + Require(readScalars == scalars.data() + scalars.size()); + utf8.resize(writtenBytes - utf8.data()); + state = {}; + const char* readBytes = nullptr; + std::u16string utf16(utf8.size(), 0); + char16_t* writtenUnits = nullptr; + Require(decode.in(state, utf8.data(), utf8.data() + utf8.size(), readBytes, + utf16.data(), utf16.data() + utf16.size(), writtenUnits) == std::codecvt_base::ok); + Require(readBytes == utf8.data() + utf8.size()); + utf16.resize(writtenUnits - utf16.data()); + auto* string = JSStringCreateWithUTF8CString(utf8.c_str()); + Require(JSStringGetLength(string) == utf16.size()); + const auto* characters = JSStringGetCharactersPtr(string); + Require(std::equal(utf16.begin(), utf16.end(), characters)); + const std::vector units(utf16.begin(), utf16.end()); + auto* other = JSStringCreateWithCharacters(units.data(), units.size()); + Require(JSStringIsEqual(string, other)); + Require(JSStringGetMaximumUTF8CStringSize(string) == utf16.size() * 3 + 1); + std::vector buffer(utf8.size() + 2, '?'); + Require(JSStringGetUTF8CString(string, buffer.data(), buffer.size() - 1) == utf8.size() + 1); + Require(std::memcmp(buffer.data(), utf8.c_str(), utf8.size() + 1) == 0); + Require(buffer.back() == '?'); + JSStringRelease(other); + JSStringRelease(string); +} + +static void Boundaries() { + const char text[] = "A\xC2\xA2\xE2\x82\xAC\xF0\x9F\x98\x80Z"; + auto* string = JSStringCreateWithUTF8CString(text); + Require(JSStringGetLength(string) == 6); + const std::array complete{0, 1, 3, 6, 10, 11}; + for (std::size_t capacity = 1; capacity <= sizeof(text) + 1; ++capacity) { + std::array buffer; + buffer.fill('?'); + std::size_t expected = 0; + for (const auto length : complete) { + if (length < capacity) expected = length; + } + Require(JSStringGetUTF8CString(string, buffer.data(), capacity) == expected + 1); + Require(std::memcmp(buffer.data(), text, expected) == 0 && buffer[expected] == '\0'); + for (auto index = expected + 1; index < buffer.size(); ++index) Require(buffer[index] == '?'); + } + char unchanged = '!'; + Require(JSStringGetUTF8CString(string, &unchanged, 0) == 0 && unchanged == '!'); + Require(JSStringGetUTF8CString(string, nullptr, 10) == 0); + Require(JSStringGetUTF8CString(nullptr, &unchanged, 1) == 0 && unchanged == '!'); + Require(JSStringGetLength(nullptr) == 0 && JSStringGetCharactersPtr(nullptr) == nullptr); + Require(JSStringIsEqual(nullptr, nullptr) && !JSStringIsEqual(nullptr, string)); + Require(JSStringIsEqualToUTF8CString(string, text)); + Require(!JSStringIsEqualToUTF8CString(string, "a")); + JSStringRelease(string); +} + +static void Invalid() { + const auto* unavailable = reinterpret_cast(std::uintptr_t{1}); + for (const auto count : {static_cast(std::numeric_limits::max()) + 1, + static_cast(std::numeric_limits::max()), std::numeric_limits::max()}) { + bool threw = false; + try { + JSStringCreateWithCharacters(unavailable, count); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw); + } + auto* empty = JSStringCreateWithUTF8CString(""); + Require(JSStringGetLength(empty) == 0); + auto* nullString = JSStringCreateWithCharacters(nullptr, 0); + Require(JSStringGetLength(nullString) == 0 && JSStringGetCharactersPtr(nullString) == nullptr); + Require(!JSStringIsEqual(empty, nullString)); + Require(JSStringIsEqualToUTF8CString(nullString, "\xFF")); + JSStringRelease(nullString); + for (const auto* invalid : {"\x80", "\xC0\x80", "\xC1\xBF", "\xE0\x80\x80", "\xED\xA0\x80", "\xF0\x80\x80\x80", "\xF4\x90\x80\x80", "\xF5\x80\x80\x80", "\xFF", "x\xC2", "x\xE2\x82", "x\xF0\x9F\x98", "\xE2!"}) { + auto* string = JSStringCreateWithUTF8CString(invalid); + Require(JSStringGetLength(string) == 0 && JSStringGetCharactersPtr(string) == nullptr); + Require(!JSStringIsEqual(string, empty)); + Require(JSStringIsEqualToUTF8CString(string, "\xFF")); + char buffer = '!'; + Require(JSStringGetUTF8CString(string, &buffer, 1) == 1 && buffer == '\0'); + JSStringRelease(string); + } + for (const auto& units : std::vector>{{0xD800}, {0xDC00}, {'x', 0xD800}, {'x', 0xDC00, 'y'}, {'x', 0xD800, 'y'}}) { + auto* string = JSStringCreateWithCharacters(units.data(), units.size()); + Require(JSStringGetLength(string) == units.size()); + Require(std::equal(units.begin(), units.end(), JSStringGetCharactersPtr(string))); + std::array buffer; + buffer.fill('?'); + const std::size_t prefix = units[0] == 'x' ? 1 : 0; + Require(JSStringGetUTF8CString(string, buffer.data(), buffer.size()) == prefix + 1); + Require(buffer[prefix] == '\0' && buffer[prefix + 1] == '?'); + JSStringRelease(string); + } + JSStringRelease(empty); +} + +static void Ownership() { + std::array units{'A', 0, 0x03A9, 'B'}; + auto* string = JSStringCreateWithCharacters(units.data(), units.size()); + units.fill('?'); + const auto* pointer = JSStringGetCharactersPtr(string); + Require(pointer[0] == 'A' && pointer[1] == 0 && pointer[2] == 0x03A9 && pointer[3] == 'B'); + std::array buffer{}; + Require(JSStringGetUTF8CString(string, buffer.data(), buffer.size()) == 6); + const std::array expected{'A', 0, static_cast(0xCE), static_cast(0xA9), 'B', 0}; + Require(std::equal(expected.begin(), expected.end(), buffer.begin())); + auto* retained = JSStringRetain(string); + Require(retained == string); + JSStringRelease(string); + Require(JSStringGetCharactersPtr(retained) == pointer && JSStringGetLength(retained) == 4); + std::vector threads; + for (unsigned index = 0; index < 4; ++index) { + threads.emplace_back([retained] { + for (unsigned count = 0; count < 5000; ++count) { + Require(JSStringRetain(retained) == retained); + Require(JSStringGetLength(retained) == 4); + JSStringRelease(retained); + } + }); + } + for (auto& thread : threads) thread.join(); + JSStringRelease(retained); +} + +int main() { + Unicode(); + Boundaries(); + Invalid(); + Ownership(); +} diff --git a/docs/dev/debt/libSceJsc.md b/docs/dev/debt/libSceJsc.md new file mode 100644 index 0000000000..ca4325c7b8 --- /dev/null +++ b/docs/dev/debt/libSceJsc.md @@ -0,0 +1,9 @@ +# libSceJsc technical debt + +### Unknown function info + +- [JSString ABI and ownership](../../../core/libs/prx/libSceJsc/Export.cpp): the console JavaScriptCore version, allocation policy, opaque object layout and thread initialization remain unverified. These host objects cannot be passed to a title-supplied native JavaScriptCore module. The ten names occur in the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceJsc.c); ownership and signatures follow pinned WebKit [JSStringRef.cpp](https://github.com/WebKit/WebKit/blob/f274ef4c03ebace644c46f8ec55746b391acdcc8/Source/JavaScriptCore/API/JSStringRef.cpp). Null retain/release or maximum-size handles, nonempty null UTF-16 buffers, counts above INT32_MAX and reference-count exhaustion throw. The allocation-header-dependent limit below INT32_MAX is not reproduced. Other JavaScriptCore APIs remain unavailable. + +### Functional + +- [JSString conversion](../../../core/libs/prx/libSceJsc/Export.cpp): console behavior is unverified. UTF-16 storage, null/empty distinction and partial UTF-8 output follow pinned WebKit [OpaqueJSString.cpp](https://github.com/WebKit/WebKit/blob/f274ef4c03ebace644c46f8ec55746b391acdcc8/Source/JavaScriptCore/API/OpaqueJSString.cpp) and [UTF8Conversion.cpp](https://github.com/WebKit/WebKit/blob/f274ef4c03ebace644c46f8ec55746b391acdcc8/Source/WTF/wtf/unicode/UTF8Conversion.cpp). Malformed UTF-8 creates a null string, distinct from an empty string; output stops before an unpaired UTF-16 surrogate. Null UTF-8 inputs and unrepresentable maximum output sizes throw. From d1f5a3410a362c1f77a70087600eac74d8bf9819 Mon Sep 17 00:00:00 2001 From: krishalivaibhav Date: Sun, 25 Oct 2026 15:53:44 +0530 Subject: [PATCH 044/198] test(libc): install process parameters in ApplicationHeap on macOS ApplicationHeap installed process parameters only on Windows and Linux, so application_heap_lazy had no way to do it on macOS. Use the shared fixture, as a5a9184e5c does. Co-authored-by: mugurc Co-Authored-By: Claude Opus 5.5 --- core/libs/tests/ApplicationHeap.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/libs/tests/ApplicationHeap.cpp b/core/libs/tests/ApplicationHeap.cpp index 8c02b16146..27894e4131 100644 --- a/core/libs/tests/ApplicationHeap.cpp +++ b/core/libs/tests/ApplicationHeap.cpp @@ -20,6 +20,8 @@ #include #include #include +#elif defined(__APPLE__) +#include "ProcessParametersFixture.hpp" #endif extern "C" { @@ -100,6 +102,8 @@ void installProcessParameters() { return 1; }, &installed); require(installed); +#elif defined(__APPLE__) + require(ProcessParametersFixture::Install(process.data(), process.size())); #endif } From 04638bf77620a17eecc41f402281b5eae8249102 Mon Sep 17 00:00:00 2001 From: krishalivaibhav Date: Sun, 25 Oct 2026 12:58:27 +0530 Subject: [PATCH 045/198] fix(libkernel): refuse mlock on unreadable and unmapped pages on macOS Linux mlock fails with ENOMEM on PROT_NONE or unmapped pages, such as reservations. macOS mlock succeeds there without pinning anything, so check the host region protections first. A page needs read or execute permission, as in FreeBSD's vm_map_wire_locked. Co-authored-by: Himansh Raj Co-Authored-By: Claude Opus 5.5 --- .../libs/prx/libkernel/DirectMemory/Export.cpp | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/core/libs/prx/libkernel/DirectMemory/Export.cpp b/core/libs/prx/libkernel/DirectMemory/Export.cpp index 93101cb207..7956c65198 100644 --- a/core/libs/prx/libkernel/DirectMemory/Export.cpp +++ b/core/libs/prx/libkernel/DirectMemory/Export.cpp @@ -21,6 +21,10 @@ #include #include #include +#ifdef __APPLE__ +#include +#include +#endif #endif #include #include @@ -678,6 +682,20 @@ int UnlockHostPages(std::uintptr_t start, std::uintptr_t end) { int LockHostPages(std::uintptr_t start, std::uintptr_t end) { auto* const address = reinterpret_cast(start); const std::size_t bytes = end - start; +#ifdef __APPLE__ + for (mach_vm_address_t cursor = start; cursor < end;) { + mach_vm_address_t region = cursor; + mach_vm_size_t size = 0; + vm_region_basic_info_data_64_t info{}; + mach_msg_type_number_t count = VM_REGION_BASIC_INFO_COUNT_64; + mach_port_t object = MACH_PORT_NULL; + const kern_return_t status = mach_vm_region(mach_task_self(), ®ion, &size, VM_REGION_BASIC_INFO_64, reinterpret_cast(&info), &count, &object); + if (status == KERN_INVALID_ADDRESS) return SCE_KERNEL_ERROR_ENOMEM; + if (status != KERN_SUCCESS) throw std::runtime_error("mach_vm_region failed"); + if (region > cursor || (info.protection & (VM_PROT_READ | VM_PROT_EXECUTE)) == 0) return SCE_KERNEL_ERROR_ENOMEM; + cursor = region + size; + } +#endif if (::mlock(address, bytes) == 0) { RecordLockedRange(start, end); return 0; From dc510a52791fc6c11a5b1bcb0474d68693cc68ad Mon Sep 17 00:00:00 2001 From: Adnan Khan Date: Sun, 25 Oct 2026 16:32:53 +0530 Subject: [PATCH 046/198] fix(libSceAmpr): record Nop as n dwords with a one-dword header --- core/libs/prx/libSceAmpr/Export.cpp | 14 ++-- .../Apr/include/AprCommandBuffer.hpp | 10 +++ core/libs/prx/libkernel/Apr/src/Apr.cpp | 12 +++- core/libs/tests/GuestAmpr.cpp | 70 ++++++++++++++++--- docs/dev/debt/libSceAmpr.md | 2 +- 5 files changed, 87 insertions(+), 21 deletions(-) diff --git a/core/libs/prx/libSceAmpr/Export.cpp b/core/libs/prx/libSceAmpr/Export.cpp index ab976c5ed3..e0d1c792c0 100644 --- a/core/libs/prx/libSceAmpr/Export.cpp +++ b/core/libs/prx/libSceAmpr/Export.cpp @@ -88,7 +88,7 @@ static int AppendRead(Apr::CommandBufferObject* buffer, Apr::Opcode opcode, std: } static std::uint64_t NopBytes(std::uint32_t dwords) { - return sizeof(Apr::CommandHeader) + std::uint64_t{dwords} * 4u; + return std::uint64_t{dwords} * 4u; } static int AppendNop(Apr::CommandBufferObject* buffer, std::uint32_t dwords, const std::uint32_t* data) { @@ -96,10 +96,10 @@ static int AppendNop(Apr::CommandBufferObject* buffer, std::uint32_t dwords, con const std::uint64_t bytes = NopBytes(dwords); if (!buffer->base) return SCE_KERNEL_ERROR_EPERM; if (bytes > RemainingCommandBytes(*buffer)) return SCE_KERNEL_ERROR_EBUSY; - const Apr::CommandHeader header{Apr::Opcode::Nop, static_cast(bytes)}; + const std::uint32_t header = Apr::NopHeader(dwords); std::uint8_t* destination = buffer->base + buffer->offset; std::memcpy(destination, &header, sizeof(header)); - if (data) std::memcpy(destination + sizeof(header), data, std::uint64_t{dwords} * 4u); + if (data) std::memcpy(destination + sizeof(header), data, bytes - sizeof(header)); buffer->offset += static_cast(bytes); ++buffer->numCommands; return 0; @@ -373,9 +373,9 @@ int APS5_VABI sceAmprCommandBufferConstructNop(Apr::CommandBufferObject* buffer, if (payloadBytes != 0u && !payload) throw std::invalid_argument("sceAmprCommandBufferConstructNop: null payload"); const std::uint32_t dwords = (payloadBytes + 3u) / 4u + (word ? 1u : 0u); const std::uint32_t offset = buffer ? buffer->offset : 0u; - const int result = AppendNop(buffer, dwords, nullptr); + const int result = AppendNop(buffer, dwords + 1u, nullptr); if (result != 0) return result; - std::uint8_t* data = buffer->base + offset + sizeof(Apr::CommandHeader); + std::uint8_t* data = buffer->base + offset + Apr::NopHeaderBytes; if (word) { std::memcpy(data, word, sizeof(*word)); data += sizeof(*word); @@ -422,7 +422,7 @@ int APS5_VABI sceAmprCommandBufferNop(Apr::CommandBufferObject* buffer, std::uin int APS5_VABI sceAmprCommandBufferNopWithData(Apr::CommandBufferObject* buffer, std::uint32_t dwords, const std::uint32_t* data) { if (dwords > 15u) return SCE_KERNEL_ERROR_EINVAL; - return AppendNop(buffer, dwords, data); + return AppendNop(buffer, dwords + 1u, data); } int APS5_VABI sceAmprCommandBufferPopMarker(Apr::CommandBufferObject* buffer) { @@ -528,7 +528,7 @@ std::uint64_t APS5_VABI sceAmprMeasureCommandSizeNop(std::uint32_t dwords) { std::uint64_t APS5_VABI sceAmprMeasureCommandSizeNopWithData(std::uint32_t dwords) { if (dwords == 0u || dwords > 16u) return MeasureInvalid; - return NopBytes(dwords - 1u); + return NopBytes(dwords); } std::uint64_t APS5_VABI sceAmprMeasureCommandSizePopMarker() { diff --git a/core/libs/prx/libkernel/Apr/include/AprCommandBuffer.hpp b/core/libs/prx/libkernel/Apr/include/AprCommandBuffer.hpp index 7b12ec2ea2..d1c8cfb7bb 100644 --- a/core/libs/prx/libkernel/Apr/include/AprCommandBuffer.hpp +++ b/core/libs/prx/libkernel/Apr/include/AprCommandBuffer.hpp @@ -87,6 +87,16 @@ struct CompactReadFileGatherCommand { }; static_assert(sizeof(CompactReadFileGatherCommand) == 12); +inline constexpr std::uint32_t NopHeaderBytes = 4; + +constexpr std::uint32_t NopHeader(std::uint32_t dwords) { + return static_cast(Opcode::Nop) | dwords << 8u; +} + +constexpr std::uint32_t NopDwords(std::uint32_t header) { + return header >> 8u; +} + struct ResetGatherScatterStateCommand { CommandHeader header; }; diff --git a/core/libs/prx/libkernel/Apr/src/Apr.cpp b/core/libs/prx/libkernel/Apr/src/Apr.cpp index 49676567f6..746b1b41b6 100644 --- a/core/libs/prx/libkernel/Apr/src/Apr.cpp +++ b/core/libs/prx/libkernel/Apr/src/Apr.cpp @@ -590,10 +590,19 @@ struct Deferred { std::optional _execute(const Apr::CommandBufferObject& buffer, std::uint32_t start = 0, ReadCursor read = {}, bool defer = false) { std::uint32_t cursor = start; while (cursor < buffer.offset) { + if (cursor + Apr::NopHeaderBytes > buffer.offset) throw std::runtime_error("APR: truncated command buffer"); + std::uint32_t first; + std::memcpy(&first, buffer.base + cursor, sizeof(first)); + if ((first & 0xffu) == static_cast(Apr::Opcode::Nop)) { + const std::uint64_t bytes = std::uint64_t{Apr::NopDwords(first)} * 4u; + if (bytes == 0u || bytes > buffer.offset - cursor) throw std::runtime_error("APR: malformed command"); + cursor += static_cast(bytes); + continue; + } if (cursor + sizeof(Apr::CommandHeader) > buffer.offset) throw std::runtime_error("APR: truncated command buffer"); Apr::CommandHeader header; std::memcpy(&header, buffer.base + cursor, sizeof(header)); - if ((static_cast(header.opcode) & 0xffu) == static_cast(Apr::Opcode::CompactReadFileGather)) { + if ((first & 0xffu) == static_cast(Apr::Opcode::CompactReadFileGather)) { if (sizeof(Apr::CompactReadFileGatherCommand) > buffer.offset - cursor) throw std::runtime_error("APR: truncated compact gather command"); Apr::CompactReadFileGatherCommand command; std::memcpy(&command, buffer.base + cursor, sizeof(command)); @@ -605,7 +614,6 @@ std::optional _execute(const Apr::CommandBufferObject& buffer, std::ui } if (header.bytes < sizeof(header) || header.bytes > buffer.offset - cursor) throw std::runtime_error("APR: malformed command"); switch (header.opcode) { - case Apr::Opcode::Nop: case Apr::Opcode::PushMarker: case Apr::Opcode::PopMarker: case Apr::Opcode::SetMarker: diff --git a/core/libs/tests/GuestAmpr.cpp b/core/libs/tests/GuestAmpr.cpp index 7b5bc20439..0d02758385 100644 --- a/core/libs/tests/GuestAmpr.cpp +++ b/core/libs/tests/GuestAmpr.cpp @@ -57,6 +57,12 @@ struct Recorder { void RequireAppended(const Recorder& recorder, std::uint32_t offset, std::uint32_t commands, Apr::Opcode opcode, std::uint64_t measured) { Require(recorder.Offset() == offset + measured); Require(recorder.Commands() == commands + 1); + if (opcode == Apr::Opcode::Nop) { + std::uint32_t first; + std::memcpy(&first, recorder.memory.data() + offset, sizeof(first)); + Require(first == Apr::NopHeader(static_cast(measured / 4u)) && measured % 4u == 0u); + return; + } Apr::CommandHeader header; std::memcpy(&header, recorder.memory.data() + offset, sizeof(header)); if (opcode == Apr::Opcode::CompactReadFileGather) { @@ -345,12 +351,12 @@ void TestNops() { const auto commands = recorder.Commands(); Require(sceAmprCommandBufferNopWithData(&recorder.buffer, 3, data) == 0); RequireAppended(recorder, offset, commands, Apr::Opcode::Nop, sceAmprMeasureCommandSizeNopWithData(4)); - Require(std::memcmp(recorder.memory.data() + offset + sizeof(Apr::CommandHeader), data, sizeof(data)) == 0); + Require(std::memcmp(recorder.memory.data() + offset + Apr::NopHeaderBytes, data, sizeof(data)) == 0); Require(sceAmprCommandBufferNopWithData(&recorder.buffer, 0, nullptr) == 0); const auto emptyPayloadOffset = recorder.Offset(); std::memset(recorder.memory.data() + emptyPayloadOffset, 0x5A, sceAmprMeasureCommandSizeNopWithData(4)); Require(sceAmprCommandBufferNopWithData(&recorder.buffer, 3, nullptr) == 0); - for (std::size_t index = 0; index < 12; ++index) Require(recorder.memory[emptyPayloadOffset + sizeof(Apr::CommandHeader) + index] == 0x5A); + for (std::size_t index = 0; index < 12; ++index) Require(recorder.memory[emptyPayloadOffset + Apr::NopHeaderBytes + index] == 0x5A); Require(sceKernelAprSubmitCommandBuffer(&recorder.buffer, 0) == 0); const auto rejected = static_cast(static_cast(invalidArgument)); @@ -361,6 +367,37 @@ void TestNops() { Require(sceAmprMeasureCommandSizeNopWithData(0) == rejected && sceAmprMeasureCommandSizeNopWithData(17) == rejected); } +void TestNopPadding() { + Require(sceAmprMeasureCommandSizeNop(1) == 4 && sceAmprMeasureCommandSizeNop(16) == 64); + Require(sceAmprMeasureCommandSizeNopWithData(1) == 4 && sceAmprMeasureCommandSizeNopWithData(16) == 64); + + Recorder recorder(64); + alignas(8) std::uint64_t done = 0; + Require(sceAmprCommandBufferPushMarker(&recorder.buffer, "pad") == 0); + Require(sceAmprCommandBufferWriteAddressOnCompletion(&recorder.buffer, &done, 1) == 0); + const std::uint32_t remaining = 64 - recorder.Offset(); + Require(remaining % 4 == 0 && remaining / 4 >= 2); + Require(sceAmprCommandBufferNop(&recorder.buffer, remaining / 4 + 1) == busy); + Require(recorder.Offset() == 64 - remaining && recorder.Commands() == 2); + Require(sceAmprCommandBufferNop(&recorder.buffer, remaining / 4) == 0); + Require(recorder.Offset() == 64 && recorder.Commands() == 3); + Require(sceAmprCommandBufferNop(&recorder.buffer, 1) == busy); + Require(sceAmprCommandBufferNopWithData(&recorder.buffer, 0, nullptr) == busy); + Require(sceKernelAprSubmitCommandBuffer(&recorder.buffer, 0) == 0); + Require(done == 1); + + Recorder odd; + done = 0; + Require(sceAmprCommandBufferNop(&odd.buffer, 1) == 0); + Require(odd.Offset() == 4); + Require(sceAmprCommandBufferWriteAddressOnCompletion(&odd.buffer, &done, 2) == 0); + Require(sceAmprCommandBufferNopWithData(&odd.buffer, 2, nullptr) == 0); + Require(sceAmprCommandBufferNop(&odd.buffer, 1) == 0); + Require(odd.Offset() == 4 + sizeof(Apr::WriteAddressCommand) + 12 + 4); + Require(sceKernelAprSubmitCommandBuffer(&odd.buffer, 0) == 0); + Require(done == 2); +} + void TestVersionedCommands() { Recorder recorder; alignas(8) std::uint64_t value = 0x8000000000000005ull; @@ -478,7 +515,7 @@ void TestConstructed() { auto commands = recorder.Commands(); Require(sceAmprCommandBufferConstructNop(&recorder.buffer, 7, payload, sizeof(payload), &word) == 0); RequireAppended(recorder, offset, commands, Apr::Opcode::Nop, sceAmprMeasureCommandSizeNopWithData(4)); - const std::uint8_t* data = recorder.memory.data() + offset + sizeof(Apr::CommandHeader); + const std::uint8_t* data = recorder.memory.data() + offset + Apr::NopHeaderBytes; const std::uint8_t padding[3] = {}; Require(std::memcmp(data, &word, sizeof(word)) == 0 && std::memcmp(data + 4, payload, sizeof(payload)) == 0 && std::memcmp(data + 9, padding, sizeof(padding)) == 0); @@ -652,10 +689,9 @@ void TestResolveFailures() { } void TestMalformedCommandSizes() { - const auto requireFailure = [](Apr::Opcode opcode, std::uint32_t recordBytes, std::uint32_t bufferBytes, const char* expected) { + const auto requireFailure = [](const void* record, std::size_t recordBytes, std::uint32_t bufferBytes, const char* expected) { std::array memory{}; - const Apr::CommandHeader header{opcode, recordBytes}; - std::memcpy(memory.data(), &header, sizeof(header)); + std::memcpy(memory.data(), record, recordBytes); Apr::CommandBufferObject buffer{Apr::BufferType::Generic, 0, bufferBytes, 1, bufferBytes, memory.data()}; bool rejected = false; try { @@ -666,14 +702,25 @@ void TestMalformedCommandSizes() { } Require(rejected); }; + const auto requireHeaderFailure = [&](Apr::Opcode opcode, std::uint32_t recordBytes, std::uint32_t bufferBytes, const char* expected) { + const Apr::CommandHeader header{opcode, recordBytes}; + requireFailure(&header, sizeof(header), bufferBytes, expected); + }; - requireFailure(Apr::Opcode::ReadFile, sizeof(Apr::CommandHeader), sizeof(Apr::CommandHeader), "APR: truncated command payload"); - requireFailure(Apr::Opcode::WriteAddress, sizeof(Apr::CommandHeader), sizeof(Apr::CommandHeader), "APR: truncated command payload"); - requireFailure(Apr::Opcode::Nop, std::numeric_limits::max(), 2u * sizeof(Apr::CommandHeader), "APR: malformed command"); + requireHeaderFailure(Apr::Opcode::ReadFile, sizeof(Apr::CommandHeader), sizeof(Apr::CommandHeader), "APR: truncated command payload"); + requireHeaderFailure(Apr::Opcode::WriteAddress, sizeof(Apr::CommandHeader), sizeof(Apr::CommandHeader), "APR: truncated command payload"); + requireHeaderFailure(Apr::Opcode::PopMarker, std::numeric_limits::max(), 2u * sizeof(Apr::CommandHeader), "APR: malformed command"); + + const std::uint32_t emptyNop = Apr::NopHeader(0); + requireFailure(&emptyNop, sizeof(emptyNop), sizeof(emptyNop), "APR: malformed command"); + const std::uint32_t overflowingNop = Apr::NopHeader(0xffffffu); + requireFailure(&overflowingNop, sizeof(overflowingNop), 2u * sizeof(overflowingNop), "APR: malformed command"); + const std::uint32_t nops[2] = {Apr::NopHeader(1), Apr::NopHeader(1)}; + requireFailure(nops, sizeof(nops), 7u, "APR: truncated command buffer"); std::array memory{}; - const Apr::CommandHeader first{Apr::Opcode::Nop, sizeof(Apr::CommandHeader)}; - const Apr::CommandHeader overflowing{Apr::Opcode::Nop, std::numeric_limits::max()}; + const Apr::CommandHeader first{Apr::Opcode::PopMarker, sizeof(Apr::CommandHeader)}; + const Apr::CommandHeader overflowing{Apr::Opcode::PopMarker, std::numeric_limits::max()}; std::memcpy(memory.data(), &first, sizeof(first)); std::memcpy(memory.data() + sizeof(first), &overflowing, sizeof(overflowing)); Apr::CommandBufferObject buffer{Apr::BufferType::Generic, 0, sizeof(memory), 2, sizeof(memory), memory.data()}; @@ -1377,6 +1424,7 @@ int main() { TestCounters(); TestRejectedWaitsAndCounters(); TestNops(); + TestNopPadding(); TestVersionedCommands(); TestVersionedCounters(); TestClearBuffer(); diff --git a/docs/dev/debt/libSceAmpr.md b/docs/dev/debt/libSceAmpr.md index ae062de3f3..3c0545224b 100644 --- a/docs/dev/debt/libSceAmpr.md +++ b/docs/dev/debt/libSceAmpr.md @@ -5,7 +5,7 @@ - [AMPR command buffer null arguments](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - the result for a null command buffer is unknown for `sceAmprCommandBufferConstructor`, `sceAmprAprCommandBufferConstructor`, `sceAmprCommandBufferReset` and `sceAmprCommandBufferSetBuffer`; these return `SCE_KERNEL_ERROR_EINVAL` here, but the behavior has not been confirmed on a PS5. - [AMPR WriteAddressFromCounterPairOnCompletion](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - the pair of an even counter n is assumed to give counter n in the low 32 bits and n + 1 in the high 32 bits. Wait compare values, wait flush and counter index ranges follow the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation; the flush is accepted and has no effect, as commands run in order - [AMPR command buffer offsets](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - recording with a current offset greater than the bound memory size throws before calculating remaining capacity or writing a command. This validates the emulator's own command-buffer encoding; the console's response to a corrupted opaque buffer object is unknown. -- [AMPR command sizes](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - commands use the emulator's own encoding with an 8-byte header, so `Nop(n)` takes 8 + 4n bytes where the console record is n dwords including its header (`NopWithData(n)`: (n + 1) dwords). The measure functions agree with the appended sizes, but a title that pads to an exact offset or the end of the buffer with `Nop(remaining / 4)` gets `SCE_KERNEL_ERROR_EBUSY` or a shifted offset. The at-start flag of the `_04_00` writes is ignored; commands run in order, each after the previous one completes +- [AMPR command sizes](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - commands other than `Nop` use the emulator's own encoding with an 8-byte header, so their measured sizes are not the console's. `Nop(n)` occupies n dwords and `NopWithData(n)` (n + 1) dwords, as on the console, through a one-dword header; the dword packing itself is AnyPS5 internal. The at-start flag of the `_04_00` writes is ignored; commands run in order, each after the previous one completes - [APR map begin and end](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - as in ampr_emu, `MapBegin`/`MapDirectBegin` map when they run and `MapEnd` only closes the region; while a region is open a second begin and every completion write return `EPERM`, the at-start `_04_00` writes are accepted - [AMPR markers](../../../core/libs/prx/libSceAmpr/Export.cpp): colors are not recorded and command sizes use the host encoding. Native signatures confirm that set takes a color pointer and push/measure take the color value. - [__cxa_call_unexpected](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - takes the in-flight exception's unwind header and never returns, as [libcxxabi 18.1.8](https://github.com/llvm/llvm-project/blob/3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff/libcxxabi/src/cxa_personality.cpp#L1171-L1211) does. No `std::set_unexpected` is exported and the exception header's unexpected handler is never set, so the handler is always the default one, which calls `std::terminate` ([cxa_default_handlers.cpp](https://github.com/llvm/llvm-project/blob/3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff/libcxxabi/src/cxa_default_handlers.cpp#L88-L99)); it therefore forwards to libc's `__cxa_call_terminate`, which begins the catch and runs the current terminate handler. That matches libcxxabi except for which terminate handler runs: there `std::terminate` after `__cxa_begin_catch` ([cxa_exception.cpp L470-L477](https://github.com/llvm/llvm-project/blob/3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff/libcxxabi/src/cxa_exception.cpp#L470-L477)) runs the handler saved in the caught exception's header ([cxa_handlers.cpp L72-L93](https://github.com/llvm/llvm-project/blob/3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff/libcxxabi/src/cxa_handlers.cpp#L72-L93)), which `__cxa_throw` and `__cxa_init_primary_exception` set to `std::get_terminate()` when the exception is thrown ([cxa_exception.cpp L209-L214](https://github.com/llvm/llvm-project/blob/3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff/libcxxabi/src/cxa_exception.cpp#L209-L214), [L279](https://github.com/llvm/llvm-project/blob/3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff/libcxxabi/src/cxa_exception.cpp#L279)), while here the current global handler runs, as AnyPS5's own `std::terminate` does. The two differ only if `std::set_terminate` was called between the throw and this call. A null header terminates because this emulator's libc `__cxa_begin_catch` terminates on null (libcxxabi does not define that case). Dynamic exception specifications that would let a replaced unexpected handler throw a permitted type are not modelled From 81f6b28ae5ce2ec6369b9b325681a23254b03686 Mon Sep 17 00:00:00 2001 From: roboterspinne <229981445+roboterspinne@users.noreply.github.com> Date: Sun, 25 Oct 2026 11:22:55 +0100 Subject: [PATCH 047/198] fix(relinker): accept a zero-size TLS export at the end of the macOS TLS block WriteMacOsImage rejected a TLS export whose value equals PT_TLS p_memsz with "Guest TLS export is outside the TLS block", although Linux and Windows output convert the same guest module or executable. The bound is now the one GuestImageReader and MacOsMachOPatcher already apply before this point: value <= p_memsz and size <= p_memsz - value, so the deleted check could only reject that boundary case. Such an export now gets the __APS5DATA entry (TLS descriptor, p_memsz) in macOS output, also at offset 0 of an empty PT_TLS that code accesses. --- core/relinker/CMakeLists.txt | 3 + .../elfpatcher/src/macos/MacOsImage.cpp | 1 - .../relinker/tests/test_macos_tls_exports.py | 77 +++++++++++++++++++ 3 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 core/relinker/relinker/tests/test_macos_tls_exports.py diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index dd1f8daae6..9da2d4a15a 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -651,6 +651,9 @@ if(BUILD_TESTING) add_test(NAME macos_tls_overlap COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_macos_tls_overlap.py $) set_tests_properties(macos_tls_overlap PROPERTIES TIMEOUT 60) + add_test(NAME macos_tls_exports COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_macos_tls_exports.py $) + set_tests_properties(macos_tls_exports PROPERTIES TIMEOUT 30) add_test(NAME macos_gs_traps COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_macos_gs_traps.py $) set_tests_properties(macos_gs_traps PROPERTIES TIMEOUT 60) diff --git a/core/relinker/elfpatcher/src/macos/MacOsImage.cpp b/core/relinker/elfpatcher/src/macos/MacOsImage.cpp index b864c5640e..e94439f66c 100644 --- a/core/relinker/elfpatcher/src/macos/MacOsImage.cpp +++ b/core/relinker/elfpatcher/src/macos/MacOsImage.cpp @@ -513,7 +513,6 @@ std::vector WriteMacOsImage(MacOsImageInput& input) { for (std::size_t index = 0; index < input.TlsExports.size(); ++index) { const auto& variable = input.TlsExports[index]; if (!hasTls) throw Domain::RelinkerException("Guest TLS export has no TLS block: " + variable.Symbol); - if (variable.Offset >= tls.Tls->MemorySize) throw Domain::RelinkerException("Guest TLS export is outside the TLS block: " + variable.Symbol); const auto at = tlsExports + index * 16; Io::WriteU64(meta, at, imageBase + descriptor); Io::WriteU64(meta, at + 8, variable.Offset); diff --git a/core/relinker/relinker/tests/test_macos_tls_exports.py b/core/relinker/relinker/tests/test_macos_tls_exports.py new file mode 100644 index 0000000000..d1b795a630 --- /dev/null +++ b/core/relinker/relinker/tests/test_macos_tls_exports.py @@ -0,0 +1,77 @@ +"""Check `relinker --macos` on TLS exports that end at the end of PT_TLS.""" +from pathlib import Path +import struct +import subprocess +import sys +import tempfile + +from test_guest_intel_trampolines import main_fixture +from test_guest_module_directories import module_with_symbol +from test_macos_executable_exports import convert, executable, exports +from test_macos_output import MachO + + +def guest(value, size): + image = module_with_symbol(True) + struct.pack_into(" Date: Sun, 25 Oct 2026 11:23:38 +0100 Subject: [PATCH 048/198] fix(relinker): reject Windows executable imports bound into the TLS template Windows output accepted an executable import relocation whose target overlaps the PT_TLS initialization image, and every guest thread read 0 there because the .gtls template is copied before the entry stub binds the import. In ELF, ld.so relocates the image before copying it into a thread's TLS block. The PE patcher now refuses such a relocation after the relocation pass, as the Windows guest module writer already does, and reports its ELF target address. Linux and macOS outputs are unchanged, and the gap is recorded in docs/dev/debt/relinker.md. --- .../src/windows/WindowsPePatcher.cpp | 7 ++++ .../tests/test_executable_tls_relocations.py | 40 ++++++++++++++++++- docs/dev/debt/relinker.md | 1 + 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp b/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp index 36e88bf315..19202cd7bb 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp @@ -45,6 +45,13 @@ std::vector WindowsPePatcher::Patch(std::vector sour report(500); const WindowsRelocationBuilder relocationBuilder; auto relocations = relocationBuilder.Apply(image, dynamicSection, originalHeaders); + for (const auto& header : originalHeaders) { + if (header.Type != 7 || header.FileSize == 0) continue; + const auto templateRva = image.GetRva(header.MappedAddress, header.FileSize); + for (const auto& import : relocations.Imports) + if (import.TargetRva < static_cast(templateRva) + header.FileSize && static_cast(import.TargetRva) + 8 > templateRva) + throw Domain::RelinkerException("Runtime-bound executable TLS template relocation is not supported", header.MappedAddress + import.TargetRva - templateRva); + } report(1000); auto sections = image.BuildSections(); report(1500); diff --git a/core/relinker/relinker/tests/test_executable_tls_relocations.py b/core/relinker/relinker/tests/test_executable_tls_relocations.py index 8f9db5e449..7ee32046e4 100644 --- a/core/relinker/relinker/tests/test_executable_tls_relocations.py +++ b/core/relinker/relinker/tests/test_executable_tls_relocations.py @@ -25,10 +25,11 @@ TARGETS = {(): "linux", ("--lazy-binding",): "lazy", ("--windows",): "windows", TLS_MEMSZ, TLS_ALIGN = 0x24, 16 TLS_BLOCK = 0x30 DEFAULT_BASE = 0x400000 -STRINGS = b"\0Tls\0Data\0ImportedTls\0" +STRINGS = b"\0Tls\0Data\0ImportedTls\0Function\0libFunction.prx\0" TLS_NAME, DATA_NAME, IMPORT_NAME = 1, 5, 10 +FUNCTION_NAME, LIBRARY_NAME = 22, 31 -NULL, TLS_SYMBOL, DATA_SYMBOL, IMPORTED = 0, 1, 2, 3 +NULL, TLS_SYMBOL, DATA_SYMBOL, IMPORTED, FUNCTION = 0, 1, 2, 3, 4 def image(relocations, tls=True, symbol_bytes=None): @@ -45,6 +46,7 @@ def image(relocations, tls=True, symbol_bytes=None): struct.pack_into(" Date: Sun, 25 Oct 2026 16:40:31 +0530 Subject: [PATCH 049/198] fix(libkernel): keep the Nop header guard wrap-free --- core/libs/prx/libkernel/Apr/src/Apr.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/Apr/src/Apr.cpp b/core/libs/prx/libkernel/Apr/src/Apr.cpp index 746b1b41b6..3b72787aff 100644 --- a/core/libs/prx/libkernel/Apr/src/Apr.cpp +++ b/core/libs/prx/libkernel/Apr/src/Apr.cpp @@ -590,7 +590,7 @@ struct Deferred { std::optional _execute(const Apr::CommandBufferObject& buffer, std::uint32_t start = 0, ReadCursor read = {}, bool defer = false) { std::uint32_t cursor = start; while (cursor < buffer.offset) { - if (cursor + Apr::NopHeaderBytes > buffer.offset) throw std::runtime_error("APR: truncated command buffer"); + if (buffer.offset - cursor < Apr::NopHeaderBytes) throw std::runtime_error("APR: truncated command buffer"); std::uint32_t first; std::memcpy(&first, buffer.base + cursor, sizeof(first)); if ((first & 0xffu) == static_cast(Apr::Opcode::Nop)) { From 2deb5f9ed95b5dd3310737dbe4a2f06912d0b39d Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sun, 25 Oct 2026 05:27:46 -0700 Subject: [PATCH 050/198] feat(libSceRudp): implement sceRudpSetMaxSegmentSize Take the 16-bit segment size Vita3K and RPCS3 declare. As in Vita3K, the call answers NOT_INITIALIZED before sceRudpInit and NOT_ACCEPTABLE while any context exists, and otherwise stores the size. sceRudpGetMaxSegmentSize keeps the 1410 measured on a PS5: Vita3K leaves the getter unimplemented, so no source says it reports a stored size. --- core/libs/prx/libSceRudp/Export.cpp | 12 ++++++++++-- core/libs/prx/libSceRudp/include/Api.hpp | 2 +- core/libs/tests/GuestRudpPump.cpp | 8 ++++++++ docs/dev/debt/libSceRudp.md | 5 +++-- 4 files changed, 22 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceRudp/Export.cpp b/core/libs/prx/libSceRudp/Export.cpp index dd4dd2c260..e50ffaab62 100644 --- a/core/libs/prx/libSceRudp/Export.cpp +++ b/core/libs/prx/libSceRudp/Export.cpp @@ -60,6 +60,7 @@ enum RudpOption { std::mutex g_mutex; bool g_inited = false; +uint16_t g_max_segment_size = RUDP_MAX_SEGMENT_SIZE; int g_next_ctx = 0; struct RudpContext { bool bound = false; @@ -495,8 +496,15 @@ int APS5_VABI sceRudpPollCancel() { return 0; } -int APS5_VABI sceRudpSetMaxSegmentSize() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceRudpSetMaxSegmentSize(uint16_t mss) { + std::lock_guard lk(g_mutex); + if (!g_inited) { + return RUDP_ERROR_NOT_INITIALIZED; + } + if (!g_ctx.empty()) { + return RUDP_ERROR_NOT_ACCEPTABLE; + } + g_max_segment_size = mss; return 0; } diff --git a/core/libs/prx/libSceRudp/include/Api.hpp b/core/libs/prx/libSceRudp/include/Api.hpp index 44b6881d58..9ea882c3bc 100644 --- a/core/libs/prx/libSceRudp/include/Api.hpp +++ b/core/libs/prx/libSceRudp/include/Api.hpp @@ -39,6 +39,6 @@ int APS5_VABI sceRudpFlush(); int APS5_VABI sceRudpGetLocalInfo(); int APS5_VABI sceRudpListen(); int APS5_VABI sceRudpPollCancel(); -int APS5_VABI sceRudpSetMaxSegmentSize(); +int APS5_VABI sceRudpSetMaxSegmentSize(uint16_t mss); } diff --git a/core/libs/tests/GuestRudpPump.cpp b/core/libs/tests/GuestRudpPump.cpp index 26482bfeea..7cc96959f0 100644 --- a/core/libs/tests/GuestRudpPump.cpp +++ b/core/libs/tests/GuestRudpPump.cpp @@ -18,6 +18,7 @@ static void Require(bool value, int line) { int main() { constexpr int notInitialized = static_cast(0x80770001); constexpr int invalidArgument = static_cast(0x80770004); + constexpr int notAcceptable = static_cast(0x80770011); std::uint16_t segmentSize = 0x5a5a; CHECK(sceRudpProcessEvents(0) == notInitialized); CHECK(sceRudpNetFlush() == notInitialized); @@ -26,6 +27,7 @@ int main() { CHECK(sceRudpGetMaxSegmentSize(&segmentSize) == notInitialized); CHECK(segmentSize == 0x5a5a); CHECK(sceRudpGetMaxSegmentSize(nullptr) == notInitialized); + CHECK(sceRudpSetMaxSegmentSize(1200) == notInitialized); alignas(8) static unsigned char pool[0xC58]; CHECK(sceRudpInit_nid_postfix(pool, sizeof(pool)) == 0); @@ -39,6 +41,12 @@ int main() { std::uint16_t segment[3] = {0, 0, 0x5a5a}; CHECK(sceRudpGetMaxSegmentSize(&segment[1]) == 0); CHECK(segment[0] == 0 && segment[1] == 1410 && segment[2] == 0x5a5a); + CHECK(sceRudpSetMaxSegmentSize(1200) == 0); + int context = -1; + CHECK(sceRudpCreateContext(nullptr, nullptr, &context) == 0); + CHECK(sceRudpSetMaxSegmentSize(1000) == notAcceptable); + CHECK(sceRudpTerminate(context) == 0); + CHECK(sceRudpSetMaxSegmentSize(1000) == 0); CHECK(sceRudpEnd() == 0); segmentSize = 0x5a5a; diff --git a/docs/dev/debt/libSceRudp.md b/docs/dev/debt/libSceRudp.md index 6b667bd48d..8ac51e82ea 100644 --- a/docs/dev/debt/libSceRudp.md +++ b/docs/dev/debt/libSceRudp.md @@ -9,7 +9,8 @@ ### Unknown function info - [sceRudpGetStatus](../../../core/libs/prx/libSceRudp/Export.cpp) - PPSA01521 passes an output pointer and byte count `0xF8`. Measured on a PS5 (firmware 13.60): the status is 0xF8 bytes; before `sceRudpInit` every call returns `RUDP_ERROR_NOT_INITIALIZED`, and after it a null pointer or a byte count of 0 or above 0xF8 returns `RUDP_ERROR_INVALID_ARGUMENT`. The layout matches `SceRudpStatus` in [Vita3K](https://github.com/AndnixSH/Vita3K-PS-Vita-OS-Fork/blob/master/vita3k/modules/SceRudp/SceLibRudp.cpp) with `currentContexts` at 0x64 and `allocs`, `frees`, `memCurrent` and `memPeak` at 0xC8 to 0xD4. Only `currentContexts` is reported: the traffic counters stay 0 because nothing is sent, and the allocator counters stay 0 because the library does not allocate from the title's pool (the console reports 6 allocations and 0x1260 bytes right after `sceRudpInit`, and 4 more allocations and 0x3F0 bytes per context). The console also writes `currentContexts` and the allocator counters when the byte count is smaller than their offset; only the requested bytes are written here. -- [sceRudpGetMaxSegmentSize](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - measured on a PS5 (firmware 13.60) in #3073: before `sceRudpInit` it returns `RUDP_ERROR_NOT_INITIALIZED` and leaves the value untouched, also for a null pointer; after it a null pointer returns `RUDP_ERROR_INVALID_ARGUMENT` and a valid one gets 1410 with result 0. The library keeps no configurable segment size (there is no `sceRudpSetMaxSegmentSize`), so 1410 is always reported. +- [sceRudpGetMaxSegmentSize](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - measured on a PS5 (firmware 13.60) in #3073: before `sceRudpInit` it returns `RUDP_ERROR_NOT_INITIALIZED` and leaves the value untouched, also for a null pointer; after it a null pointer returns `RUDP_ERROR_INVALID_ARGUMENT` and a valid one gets 1410 with result 0. No source says whether a size stored by `sceRudpSetMaxSegmentSize` is reported instead, so 1410 is always reported. +- [sceRudpSetMaxSegmentSize](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - unverified on a PS5. `(uint16_t mss)` and the checks follow [Vita3K](https://github.com/AndnixSH/Vita3K-PS-Vita-OS-Fork/blob/c86ac732bc811057e690bed280c5076bdf95385e/vita3k/modules/SceRudp/SceLibRudp.cpp#L1573-L1584), with the argument in [RPCS3](https://github.com/RPCS3/rpcs3/blob/8b9ee60569753db8406b66d135cce112156172a1/rpcs3/Emu/Cell/Modules/cellRudp.cpp#L149-L153): `RUDP_ERROR_NOT_INITIALIZED` before `sceRudpInit`, `RUDP_ERROR_NOT_ACCEPTABLE` while a context exists, otherwise stored without a range check and kept across `sceRudpEnd`, as Vita3K keeps it in its library state. Nothing reads the stored size: Vita3K [leaves the getter unimplemented](https://github.com/AndnixSH/Vita3K-PS-Vita-OS-Fork/blob/c86ac732bc811057e690bed280c5076bdf95385e/vita3k/modules/SceRudp/SceLibRudp.cpp#L1080-L1082), so `sceRudpGetMaxSegmentSize` keeps the measured 1410 - [sceRudpGetOption, sceRudpSetOption](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - full PS5 option contracts are unverified; defaults and validation follow [Vita3K](https://github.com/AndnixSH/Vita3K-PS-Vita-OS-Fork/blob/c86ac732bc811057e690bed280c5076bdf95385e/vita3k/modules/SceRudp/SceLibRudp.cpp). Options 12 and 13 have unknown meanings; last-error option 14 is not maintained. Their setters store all 32 bits per context and return 0 for valid four-byte buffers, preserving acceptance on `main`; their getters throw. Static inspection of PPSA06888 v01.001.008's bundled Unity transport shows its context-set constructor configuring IDs 2, 3, 5 and 6; runtime usage and other paths remain unverified. The plugin wrappers pass four-byte values. No context can connect, so stored settings cannot affect traffic. Closes when: PS5 option contracts are verified and the remaining behavior is implemented. - [sceRudpPollCreate, sceRudpPollControl, sceRudpPollWait, sceRudpPollDestroy](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - unverified on a PS5; imported by PPSA31327 and not called before the main menu. Signatures and the 8-byte event layout are recovered from the supplied module. The operations (1 add, 2 modify, 3 remove) and the argument checks follow the PS Vita library in [Vita3K](https://github.com/AndnixSH/Vita3K-PS-Vita-OS-Fork/blob/master/vita3k/modules/SceRudp/SceLibRudp.cpp), and the error codes match [RPCS3](https://github.com/RPCS3/rpcs3/blob/master/rpcs3/Emu/Cell/Modules/cellRudp.h). Vita3K leaves `sceRudpPollDestroy` unimplemented, so `int sceRudpPollDestroy(int pollId)` and its checks are inferred. No context can connect, so no event is ever reported: `sceRudpPollWait` returns 0 when its timeout elapses, also for an empty poll, where Vita3K returns at once -- [sceRudpAccept, sceRudpFlush, sceRudpGetLocalInfo, sceRudpListen, sceRudpPollCancel and sceRudpSetMaxSegmentSize](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceRudp.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim. +- [sceRudpAccept, sceRudpFlush, sceRudpGetLocalInfo, sceRudpListen and sceRudpPollCancel](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceRudp.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim. From 380803b26dc6a586be22b020f573f6c7c54b914c Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sun, 25 Oct 2026 07:16:23 -0700 Subject: [PATCH 051/198] test(libSceFont): compare glyph image surface offsets as checked integers SameImage subtracted each render output's surface address from its own pixel buffer's pointer. Convert both to integers instead, check that the update rectangle starting at that address lies inside the buffer, and compare the offsets. --- core/libs/tests/GuestSystemFont.cpp | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/core/libs/tests/GuestSystemFont.cpp b/core/libs/tests/GuestSystemFont.cpp index 4b63b388a3..b09ed91645 100644 --- a/core/libs/tests/GuestSystemFont.cpp +++ b/core/libs/tests/GuestSystemFont.cpp @@ -191,12 +191,25 @@ template static GlyphImage RenderInto(TRender render) { return image; } +static bool SurfaceOffset(const GlyphImage& image, std::uintptr_t& offset) { + const auto& surface = image.result.SurfaceImage; + const auto& rect = image.result.UpdateRect; + const auto base = reinterpret_cast(image.pixels.data()); + const auto address = reinterpret_cast(surface.address); + const std::uintptr_t span = rect.w == 0 || rect.h == 0 ? 0 : static_cast(rect.h - 1) * surface.widthByte + static_cast(rect.w) * surface.pixelSizeByte; + if (address < base || address - base > image.pixels.size() || span > image.pixels.size() - (address - base)) return false; + offset = address - base; + return true; +} + static bool SameImage(const GlyphImage& left, const GlyphImage& right) { int lit = 0; for (const std::uint8_t pixel : left.pixels) lit += pixel != 0 ? 1 : 0; const auto& a = left.result; const auto& b = right.result; - const bool sameSurface = a.SurfaceImage.address - left.pixels.data() == b.SurfaceImage.address - right.pixels.data() && a.SurfaceImage.widthByte == b.SurfaceImage.widthByte && a.SurfaceImage.pixelSizeByte == b.SurfaceImage.pixelSizeByte && a.SurfaceImage.pixelFormat == b.SurfaceImage.pixelFormat; + std::uintptr_t leftOffset = 0; + std::uintptr_t rightOffset = 0; + const bool sameSurface = SurfaceOffset(left, leftOffset) && SurfaceOffset(right, rightOffset) && leftOffset == rightOffset && a.SurfaceImage.widthByte == b.SurfaceImage.widthByte && a.SurfaceImage.pixelSizeByte == b.SurfaceImage.pixelSizeByte && a.SurfaceImage.pixelFormat == b.SurfaceImage.pixelFormat; const bool sameOutput = a.stage == b.stage && sameSurface && std::memcmp(&a.UpdateRect, &b.UpdateRect, sizeof(a.UpdateRect)) == 0 && std::memcmp(&a.ImageMetrics, &b.ImageMetrics, sizeof(a.ImageMetrics)) == 0; return left.rc == SCE_FONT_OK && right.rc == SCE_FONT_OK && lit > 0 && left.pixels == right.pixels && std::memcmp(&left.metrics, &right.metrics, sizeof(left.metrics)) == 0 && sameOutput; } From 96470f0a1129c61de5ecd1cdd3848d05fa6d106d Mon Sep 17 00:00:00 2001 From: roboterspinne <229981445+roboterspinne@users.noreply.github.com> Date: Sun, 25 Oct 2026 14:24:57 +0100 Subject: [PATCH 052/198] fix(relinker): reject relocations against a non-null executable symbol entry 0 A symbolic relocation with symbol index 0 read the name, binding and type of dynamic symbol entry 0, so a named entry 0 turned STN_UNDEF relocations into named imports on Linux, Windows and macOS. The ELF gABI reserves index 0 as STN_UNDEF and requires its entry to be all zero, and the guest reader already rejects such a table as "Invalid null symbol". The relinker now fails with "Dynamic symbol table entry 0 is not a null symbol entry" when a symbolic relocation names index 0 and that entry has a nonzero byte; relocations against an all-zero entry 0 are unchanged. --- .../relinker/src/pipeline/RelinkerPipeline.cpp | 3 +++ .../tests/test_null_symbol_relocations.py | 15 +++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp index 1dec1d5a25..59f3c0449f 100644 --- a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp +++ b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp @@ -300,6 +300,9 @@ RelinkResult RelinkerPipeline::Relink() { if (symOff < dynSymTabOffset || symOff > raw.size() || raw.size() - symOff < symEntSize || (dynSymTabSize && (off > *dynSymTabSize || symEntSize > *dynSymTabSize - off))) throw RelinkerException("Symbol table entry out of bounds", symOff); + if (symIdx == 0 && std::any_of(raw.begin() + static_cast(symOff), raw.begin() + static_cast(symOff + symEntSize), + [](const std::uint8_t byte) { return byte != 0; })) + throw RelinkerException("Dynamic symbol table entry 0 is not a null symbol entry", symOff); return symOff; }; diff --git a/core/relinker/relinker/tests/test_null_symbol_relocations.py b/core/relinker/relinker/tests/test_null_symbol_relocations.py index 99f436b7b7..6e9f22b352 100644 --- a/core/relinker/relinker/tests/test_null_symbol_relocations.py +++ b/core/relinker/relinker/tests/test_null_symbol_relocations.py @@ -82,6 +82,21 @@ def main(): capture_output=True, text=True, timeout=20) assert result.returncode == 2 and "Unnamed import symbol is not a null symbol entry" in result.stderr, (target, result.stderr) assert not output.exists() + named_null = image_with(index=0) + names = b"\0foo\0libfoo.prx\0" + named_null[0x600:0x600 + len(names)] = names + struct.pack_into(" Date: Sun, 25 Oct 2026 14:56:22 +0000 Subject: [PATCH 053/198] feat(libSceUsbStorage): pass the host's USB FAT and exFAT volumes through List the FAT and exFAT volumes the host has mounted from USB mass-storage devices (Linux mountinfo and sysfs, Windows volume IOCTLs and SetupAPI), map each to /mnt/usb and report mounts and unmounts to the registered callbacks from a service thread that rescans the host once a second. Device info, map requests and getdents scans follow the recovered records of SharpProspero and shadPS4; unknown answers keep throwing. --- core/libs/prx/libSceUsbStorage/CMakeLists.txt | 6 +- core/libs/prx/libSceUsbStorage/Export.cpp | 416 +++++++++++++-- core/libs/prx/libSceUsbStorage/UsbStorage.hpp | 35 +- .../libSceUsbStorage/include/HostVolumes.hpp | 40 ++ .../prx/libSceUsbStorage/src/HostVolumes.cpp | 172 ++++++ .../src/HostVolumesWindows.cpp | 198 +++++++ core/libs/tests/GuestUsbStorage.cpp | 496 +++++++++++++----- docs/dev/debt/libSceUsbStorage.md | 22 +- 8 files changed, 1216 insertions(+), 169 deletions(-) create mode 100644 core/libs/prx/libSceUsbStorage/include/HostVolumes.hpp create mode 100644 core/libs/prx/libSceUsbStorage/src/HostVolumes.cpp create mode 100644 core/libs/prx/libSceUsbStorage/src/HostVolumesWindows.cpp diff --git a/core/libs/prx/libSceUsbStorage/CMakeLists.txt b/core/libs/prx/libSceUsbStorage/CMakeLists.txt index a4280361de..9be261a135 100644 --- a/core/libs/prx/libSceUsbStorage/CMakeLists.txt +++ b/core/libs/prx/libSceUsbStorage/CMakeLists.txt @@ -1,6 +1,10 @@ -add_library(libSceUsbStorage SHARED EXCLUDE_FROM_ALL Export.cpp) +add_library(libSceUsbStorage SHARED EXCLUDE_FROM_ALL Export.cpp src/HostVolumes.cpp) target_include_directories(libSceUsbStorage PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(libSceUsbStorage PRIVATE libc) +if(WIN32) + target_sources(libSceUsbStorage PRIVATE src/HostVolumesWindows.cpp) + target_link_libraries(libSceUsbStorage PRIVATE setupapi cfgmgr32) +endif() configure_windows_unwind(libSceUsbStorage) if(BUILD_TESTING) diff --git a/core/libs/prx/libSceUsbStorage/Export.cpp b/core/libs/prx/libSceUsbStorage/Export.cpp index 978a9dc386..563daef418 100644 --- a/core/libs/prx/libSceUsbStorage/Export.cpp +++ b/core/libs/prx/libSceUsbStorage/Export.cpp @@ -1,31 +1,277 @@ #include "prx/libSceSystemService/include/SystemService.hpp" +#include +#include +#include +#include #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libkernel/include/Api.hpp" #include "prx/libSceUsbStorage/UsbStorage.hpp" +#include "prx/libSceUsbStorage/include/HostVolumes.hpp" + +static_assert(sizeof(UsbStorageEvent) == 16); +static_assert(offsetof(UsbStorageEvent, device) == 4); +static_assert(offsetof(UsbStorageEvent, flags) == 8); +static_assert(sizeof(UsbStorageDeviceInfo) == 0x338); +static_assert(offsetof(UsbStorageDeviceInfo, vendorId) == 0x10); +static_assert(offsetof(UsbStorageDeviceInfo, productId) == 0x12); +static_assert(offsetof(UsbStorageDeviceInfo, bcdDevice) == 0x14); +static_assert(offsetof(UsbStorageDeviceInfo, vendor) == 0x16); +static_assert(offsetof(UsbStorageDeviceInfo, vendorLength) == 0x118); +static_assert(offsetof(UsbStorageDeviceInfo, product) == 0x120); +static_assert(offsetof(UsbStorageDeviceInfo, productLength) == 0x220); +static_assert(offsetof(UsbStorageDeviceInfo, serialNumber) == 0x228); +static_assert(offsetof(UsbStorageDeviceInfo, serialNumberLength) == 0x328); +static_assert(offsetof(UsbStorageDeviceInfo, flags) == 0x330); + +namespace { + +constexpr std::uint64_t GetdentsBufferSize = 0x8000; +constexpr std::size_t DirentHeaderSize = 8; +constexpr std::size_t NameLimit = 255; +constexpr std::uint8_t DirectoryType = 4; +constexpr std::uint8_t RegularType = 8; +constexpr auto RescanInterval = std::chrono::seconds(1); + +struct Device { + UsbStorage::HostVolume volume; + std::vector maps; +}; + +struct Registration { + std::int32_t eventType; + UsbStorageCallback callback; + void* arg; +}; + +struct DirectoryEntry { + std::string name; + bool directory; +}; + +struct DirectoryScan { + std::vector entries; + std::size_t next = 0; +}; + +struct Service { + std::mutex mutex; + std::condition_variable wake; + bool initialized = false; + bool stopping = false; + Pthread monitor = nullptr; + std::array, UsbStorage::MaxDevices> devices; + std::vector registrations; + std::map scans; + std::int32_t nextDescriptor = 1; +}; + +Service& State() { + static auto* service = new Service; + return *service; +} + +std::string MountPoint(std::size_t slot) { + return "/mnt/usb" + std::to_string(slot); +} + +std::uint64_t Flags(const UsbStorage::HostVolume& volume) { + std::uint64_t flags = volume.fileSystem == UsbStorage::FileSystem::ExFat ? UsbStorage::ExFatFlag : UsbStorage::FatFlag; + if (volume.readOnly) flags |= UsbStorage::ReadOnlyFlag; + return flags; +} + +UsbStorageEvent Event(std::int32_t type, std::size_t slot, const UsbStorage::HostVolume& volume) { + return {static_cast(type), static_cast(slot), Flags(volume)}; +} + +Device* Find(Service& service, std::uint32_t device) { + if (device >= service.devices.size() || !service.devices[device]) return nullptr; + return &*service.devices[device]; +} + +void Update(Service& service, std::vector volumes, std::vector& events) { + for (std::size_t slot = 0; slot < service.devices.size(); ++slot) { + auto& device = service.devices[slot]; + if (!device) continue; + const auto found = std::find_if(volumes.begin(), volumes.end(), [&](const auto& volume) { return volume.key == device->volume.key; }); + if (found != volumes.end()) { + device->volume = std::move(*found); + volumes.erase(found); + continue; + } + events.push_back(Event(UsbStorage::UnmountEvent, slot, device->volume)); + RemovePathAlias_nid_no_patch(MountPoint(slot).c_str()); + device.reset(); + } + for (auto& volume : volumes) { + const auto free = std::find_if(service.devices.begin(), service.devices.end(), [](const auto& device) { return !device; }); + if (free == service.devices.end()) break; + const auto slot = static_cast(free - service.devices.begin()); + AddPathAlias_nid_no_patch(MountPoint(slot).c_str(), volume.root.string().c_str()); + events.push_back(Event(UsbStorage::MountEvent, slot, volume)); + free->emplace(Device{std::move(volume), {}}); + } +} + +void Dispatch(const std::vector& events, const std::vector& registrations) { + for (const auto& event : events) { + for (const auto& registration : registrations) { + if (registration.eventType == static_cast(event.event)) registration.callback(&event, registration.arg); + } + } +} + +void* APS5_VABI Monitor(void*) { + auto& service = State(); + std::unique_lock lock(service.mutex); + while (!service.wake.wait_for(lock, RescanInterval, [&] { return service.stopping; })) { + lock.unlock(); + auto volumes = UsbStorage::ScanHostVolumes(); + lock.lock(); + if (service.stopping) break; + std::vector events; + Update(service, std::move(volumes), events); + const auto registrations = service.registrations; + lock.unlock(); + Dispatch(events, registrations); + lock.lock(); + } + return nullptr; +} + +std::vector::iterator Registered(Service& service, std::int32_t eventType, UsbStorageCallback callback) { + return std::find_if(service.registrations.begin(), service.registrations.end(), + [&](const auto& registration) { return registration.eventType == eventType && registration.callback == callback; }); +} + +std::string RelativeDirectory(const char* directory, const char* function) { + std::string relative; + if (directory == nullptr) return relative; + std::string_view text(directory); + if (text.find('\\') != std::string_view::npos) throw std::runtime_error(std::string(function) + ": a directory with a backslash is not implemented"); + while (!text.empty()) { + const auto separator = text.find('/'); + const auto name = text.substr(0, separator); + if (name == "." || name == "..") throw std::runtime_error(std::string(function) + ": a directory with . or .. components is not implemented"); + if (!name.empty()) { + if (!relative.empty()) relative += '/'; + relative += name; + } + if (separator == std::string_view::npos) break; + text.remove_prefix(separator + 1); + } + return relative; +} + +std::string Map(Device& device, std::uint32_t slot, const char* directory, const char* function) { + const auto relative = RelativeDirectory(directory, function); + auto host = device.volume.root; + if (!relative.empty()) host /= std::filesystem::path(std::u8string(relative.begin(), relative.end())); + std::error_code error; + if (!std::filesystem::is_directory(host, error)) throw std::runtime_error(std::string(function) + ": mapping a directory the device does not hold is not implemented"); + auto path = MountPoint(slot); + if (!relative.empty()) path += "/" + relative; + if (path.size() >= UsbStorage::MountPointSize) throw std::runtime_error(std::string(function) + ": a mount point longer than 127 bytes is not implemented"); + if (std::find(device.maps.begin(), device.maps.end(), path) != device.maps.end()) { + throw std::runtime_error(std::string(function) + ": mapping a mapped directory again is not implemented"); + } + device.maps.push_back(path); + return path; +} + +void WriteMountPoint(char* mountPoint, const std::string& path) { + if (mountPoint != nullptr) std::memcpy(mountPoint, path.c_str(), path.size() + 1); +} + +std::uint64_t CopyString(char (&destination)[256], const std::string& text, const char* function) { + if (text.size() >= sizeof(destination)) throw std::runtime_error(std::string(function) + ": a device string longer than 255 bytes is not implemented"); + std::memcpy(destination, text.data(), text.size()); + return text.size(); +} + +} extern "C" { std::int32_t APS5_VABI sceUsbStorageInit(void* attr) { - (void)attr; + if (attr != nullptr) throw std::runtime_error("sceUsbStorageInit: a thread attribute is not implemented"); + auto& service = State(); + { + std::lock_guard lock(service.mutex); + if (service.stopping) throw std::runtime_error("sceUsbStorageInit: initializing while sceUsbStorageTerm runs is not implemented"); + if (service.initialized) return 0; + } + auto volumes = UsbStorage::ScanHostVolumes(); + std::lock_guard lock(service.mutex); + if (service.stopping) throw std::runtime_error("sceUsbStorageInit: initializing while sceUsbStorageTerm runs is not implemented"); + if (service.initialized) return 0; + std::vector events; + Update(service, std::move(volumes), events); + if (scePthreadCreate(&service.monitor, nullptr, Monitor, nullptr, "SceUsbStorageMonitor") != 0) { + Update(service, {}, events); + service.monitor = nullptr; + throw std::runtime_error("sceUsbStorageInit: scePthreadCreate failed"); + } + service.initialized = true; return 0; } std::int32_t APS5_VABI sceUsbStorageTerm(void) { + auto& service = State(); + Pthread monitor = nullptr; + { + std::lock_guard lock(service.mutex); + if (!service.initialized) return 0; + if (service.stopping) throw std::runtime_error("sceUsbStorageTerm: a concurrent termination is not implemented"); + if (scePthreadEqual(scePthreadSelf(), service.monitor) != 0) { + throw std::runtime_error("sceUsbStorageTerm: terminating from a USB storage callback is not implemented"); + } + service.stopping = true; + monitor = service.monitor; + } + service.wake.notify_all(); + if (scePthreadJoin(monitor, nullptr) != 0) throw std::runtime_error("sceUsbStorageTerm: scePthreadJoin failed"); + std::lock_guard lock(service.mutex); + std::vector events; + Update(service, {}, events); + service.registrations.clear(); + service.scans.clear(); + service.monitor = nullptr; + service.stopping = false; + service.initialized = false; return 0; } std::int32_t APS5_VABI sceUsbStorageRegisterCallback(std::int32_t eventType, UsbStorageCallback callback, void* arg) { - (void)arg; if (callback == nullptr) { return UsbStorage::InvalidArgument; } - if (eventType < UsbStorage::CallbackEventTypeFirst || eventType > UsbStorage::CallbackEventTypeLast) { + if (eventType != UsbStorage::MountEvent && eventType != UsbStorage::UnmountEvent) { return UsbStorage::InvalidArgument; } - NotImplemented_nid_no_patch(__func__); + auto& service = State(); + std::lock_guard lock(service.mutex); + if (!service.initialized) throw std::runtime_error("sceUsbStorageRegisterCallback: registering before sceUsbStorageInit is not implemented"); + if (Registered(service, eventType, callback) != service.registrations.end()) { + throw std::runtime_error("sceUsbStorageRegisterCallback: registering a registered callback again is not implemented"); + } + service.registrations.push_back({eventType, callback, arg}); return 0; } @@ -33,9 +279,16 @@ std::int32_t APS5_VABI sceUsbStorageUnregisterCallback(std::int32_t eventType, U if (callback == nullptr) { return UsbStorage::InvalidArgument; } - if (eventType < UsbStorage::CallbackEventTypeFirst || eventType > UsbStorage::CallbackEventTypeLast) { + if (eventType != UsbStorage::MountEvent && eventType != UsbStorage::UnmountEvent) { return UsbStorage::InvalidArgument; } + auto& service = State(); + std::lock_guard lock(service.mutex); + const auto found = Registered(service, eventType, callback); + if (found != service.registrations.end()) { + service.registrations.erase(found); + return 0; + } NotImplemented_nid_no_patch(__func__); return 0; } @@ -79,75 +332,165 @@ std::int32_t APS5_VABI sceUsbStorageUnregisterCallbackForMapUnavailable(std::uin } std::int32_t APS5_VABI sceUsbStorageGetDeviceInfo(std::uint32_t device, UsbStorageDeviceInfo* info) { - (void)device; if (info == nullptr) { return UsbStorage::InvalidArgument; } - return UsbStorage::DeviceNotFound; + auto& service = State(); + std::lock_guard lock(service.mutex); + const auto* found = Find(service, device); + if (found == nullptr) { + return UsbStorage::DeviceNotFound; + } + const auto& volume = found->volume; + UsbStorageDeviceInfo record{}; + record.volumeSize = volume.volumeSize; + record.vendorId = volume.vendorId; + record.productId = volume.productId; + record.bcdDevice = volume.bcdDevice; + record.vendorLength = CopyString(record.vendor, volume.vendor, __func__); + record.productLength = CopyString(record.product, volume.product, __func__); + record.serialNumberLength = CopyString(record.serialNumber, volume.serialNumber, __func__); + record.flags = Flags(volume); + std::memcpy(info, &record, sizeof(record)); + return 0; } std::int32_t APS5_VABI sceUsbStorageGetDeviceList(std::uint32_t* devices, std::int32_t* count) { if (devices == nullptr || count == nullptr) { return UsbStorage::InvalidArgument; } - *count = 0; + auto& service = State(); + std::lock_guard lock(service.mutex); + std::int32_t listed = 0; + for (std::size_t slot = 0; slot < service.devices.size(); ++slot) { + if (service.devices[slot]) devices[listed++] = static_cast(slot); + } + *count = listed; return 0; } int APS5_VABI sceUsbStorageGetMountPointOfShellCore(uint32_t device, char* mountPoint) { - (void)device; - (void)mountPoint; - NotImplemented_nid_no_patch(__func__); + auto& service = State(); + std::lock_guard lock(service.mutex); + if (Find(service, device) == nullptr || mountPoint == nullptr) { + NotImplemented_nid_no_patch(__func__); + } + WriteMountPoint(mountPoint, MountPoint(device)); return 0; } bool APS5_VABI sceUsbStorageIsExist(std::uint32_t device, const char* directory, std::uint8_t* exists) { - (void)device; (void)directory; - *exists = 0; + if (exists == nullptr) throw std::runtime_error("sceUsbStorageIsExist: a null output is not implemented"); + auto& service = State(); + std::lock_guard lock(service.mutex); + *exists = Find(service, device) != nullptr ? 1 : 0; return true; } std::int32_t APS5_VABI sceUsbStorageRequestMap(std::uint32_t device, const char* directory, std::int32_t flags, std::uint64_t volumeSize, char* mountPoint, std::uint64_t* mountId) { - (void)device; - (void)directory; - (void)flags; - (void)volumeSize; - (void)mountPoint; - (void)mountId; - return UsbStorage::DeviceNotFound; + auto& service = State(); + std::lock_guard lock(service.mutex); + auto* found = Find(service, device); + if (found == nullptr) { + return UsbStorage::DeviceNotFound; + } + if (flags != 0 || volumeSize != 0) throw std::runtime_error("sceUsbStorageRequestMap: nonzero flags or a volume size are not implemented"); + WriteMountPoint(mountPoint, Map(*found, device, directory, __func__)); + if (mountId != nullptr) *mountId = 0; + return 0; } std::int32_t APS5_VABI sceUsbStorageRequestUnmap(std::uint32_t device, const char* directory) { - (void)device; if (directory == nullptr) { return UsbStorage::InvalidArgument; } + auto& service = State(); + std::lock_guard lock(service.mutex); + if (auto* found = Find(service, device)) { + const auto map = std::find(found->maps.begin(), found->maps.end(), std::string_view(directory)); + if (map != found->maps.end()) { + found->maps.erase(map); + return 0; + } + } NotImplemented_nid_no_patch(__func__); return 0; } std::int32_t APS5_VABI sceUsbStorageGetdentsOpen(std::uint32_t device, std::int32_t* fd, std::uint64_t* bufferSize) { - (void)device; if (fd == nullptr || bufferSize == nullptr) { return UsbStorage::InvalidArgument; } - return UsbStorage::DeviceNotFound; + auto& service = State(); + std::lock_guard lock(service.mutex); + const auto* found = Find(service, device); + if (found == nullptr) { + return UsbStorage::DeviceNotFound; + } + DirectoryScan scan; + std::error_code error; + for (std::filesystem::directory_iterator entry(found->volume.root, error), end; !error && entry != end; entry.increment(error)) { + const auto name = entry->path().filename().u8string(); + if (name.empty() || name.front() == u8'.') continue; + if (name.size() > NameLimit) throw std::runtime_error("sceUsbStorageGetdentsOpen: a name longer than 255 bytes is not implemented"); + std::error_code typeError; + scan.entries.push_back({std::string(name.begin(), name.end()), entry->is_directory(typeError)}); + } + if (error) throw std::runtime_error("sceUsbStorageGetdentsOpen: listing the device failed: " + error.message()); + if (service.nextDescriptor == std::numeric_limits::max()) throw std::runtime_error("sceUsbStorageGetdentsOpen: the descriptor numbers are exhausted"); + const auto descriptor = service.nextDescriptor++; + service.scans.emplace(descriptor, std::move(scan)); + *fd = descriptor; + *bufferSize = GetdentsBufferSize; + return 0; } std::int32_t APS5_VABI sceUsbStorageGetdentsRead(std::int32_t fd, void* buffer, std::uint64_t bufferSize, std::uint64_t* bytesRead) { - (void)fd; - (void)bufferSize; if (buffer == nullptr || bytesRead == nullptr) { return UsbStorage::InvalidArgument; } - *bytesRead = 0; - return UsbStorage::NotSupported; + auto& service = State(); + std::lock_guard lock(service.mutex); + const auto found = service.scans.find(fd); + if (found == service.scans.end()) { + *bytesRead = 0; + return UsbStorage::NotSupported; + } + auto& scan = found->second; + auto* const output = static_cast(buffer); + std::uint64_t written = 0; + while (scan.next < scan.entries.size()) { + const auto& entry = scan.entries[scan.next]; + const auto length = static_cast((DirentHeaderSize + entry.name.size() + 1 + 7) & ~std::size_t{7}); + if (length > bufferSize - written) break; + auto* const record = output + written; + const auto fileNumber = static_cast(scan.next + 1); + const auto type = entry.directory ? DirectoryType : RegularType; + const auto nameLength = static_cast(entry.name.size()); + std::memset(record, 0, length); + std::memcpy(record, &fileNumber, sizeof(fileNumber)); + std::memcpy(record + 4, &length, sizeof(length)); + std::memcpy(record + 6, &type, sizeof(type)); + std::memcpy(record + 7, &nameLength, sizeof(nameLength)); + std::memcpy(record + DirentHeaderSize, entry.name.data(), entry.name.size()); + written += length; + ++scan.next; + } + if (written == 0 && scan.next < scan.entries.size()) { + throw std::runtime_error("sceUsbStorageGetdentsRead: a buffer smaller than the next entry is not implemented"); + } + *bytesRead = written; + return 0; } std::int32_t APS5_VABI sceUsbStorageGetdentsClose(std::int32_t fd) { - (void)fd; + auto& service = State(); + std::lock_guard lock(service.mutex); + if (service.scans.erase(fd) == 1) { + return 0; + } NotImplemented_nid_no_patch(__func__); return 0; } @@ -159,13 +502,16 @@ int APS5_VABI sceUsbStorageClearFakeWriteProtection() { std::int32_t APS5_VABI sceUsbStorageRequestMapWSB(std::uint32_t device, const char* subpath, std::int32_t flags, std::int32_t unknown, char* mountPath, std::int32_t* status) { - (void)device; - (void)subpath; - (void)flags; - (void)unknown; - (void)mountPath; - (void)status; - return UsbStorage::DeviceNotFound; + auto& service = State(); + std::lock_guard lock(service.mutex); + auto* found = Find(service, device); + if (found == nullptr) { + return UsbStorage::DeviceNotFound; + } + if (flags != 0 || unknown != 0) throw std::runtime_error("sceUsbStorageRequestMapWSB: nonzero flags or a nonzero fourth argument are not implemented"); + WriteMountPoint(mountPath, Map(*found, device, subpath, __func__)); + if (status != nullptr) *status = 0; + return 0; } int APS5_VABI sceUsbStorageSetFakeMapLockForDebug() { diff --git a/core/libs/prx/libSceUsbStorage/UsbStorage.hpp b/core/libs/prx/libSceUsbStorage/UsbStorage.hpp index b3bc2d24ab..3ccc495a6e 100644 --- a/core/libs/prx/libSceUsbStorage/UsbStorage.hpp +++ b/core/libs/prx/libSceUsbStorage/UsbStorage.hpp @@ -12,13 +12,40 @@ inline constexpr std::int32_t InvalidArgument = static_cast(0x80f4 inline constexpr std::int32_t DeviceNotFound = static_cast(0x80f40004); inline constexpr std::int32_t NotSupported = static_cast(0x80f40005); -inline constexpr std::int32_t CallbackEventTypeFirst = 1; -inline constexpr std::int32_t CallbackEventTypeLast = 2; +inline constexpr std::int32_t MountEvent = 1; +inline constexpr std::int32_t UnmountEvent = 2; + +inline constexpr std::uint64_t ReadOnlyFlag = 1ull << 4; +inline constexpr std::uint64_t FatFlag = 1ull << 6; +inline constexpr std::uint64_t ExFatFlag = 1ull << 7; + +inline constexpr std::size_t MaxDevices = 8; +inline constexpr std::size_t MountPointSize = 128; } -struct UsbStorageEvent; -struct UsbStorageDeviceInfo; +struct UsbStorageEvent { + std::uint32_t event; + std::uint32_t device; + std::uint64_t flags; +}; + +struct UsbStorageDeviceInfo { + std::uint64_t volumeSize; + std::uint64_t reserved; + std::uint16_t vendorId; + std::uint16_t productId; + std::uint16_t bcdDevice; + char vendor[256]; + std::uint8_t padding[2]; + std::uint64_t vendorLength; + char product[256]; + std::uint64_t productLength; + char serialNumber[256]; + std::uint64_t serialNumberLength; + std::uint64_t flags; +}; + using UsbStorageCallback = void (APS5_VABI *)(const UsbStorageEvent* event, void* userData); extern "C" { diff --git a/core/libs/prx/libSceUsbStorage/include/HostVolumes.hpp b/core/libs/prx/libSceUsbStorage/include/HostVolumes.hpp new file mode 100644 index 0000000000..9117a283f5 --- /dev/null +++ b/core/libs/prx/libSceUsbStorage/include/HostVolumes.hpp @@ -0,0 +1,40 @@ +#ifndef CORE_LIBS_PRX_LIBSCEUSBSTORAGE_INCLUDE_HOSTVOLUMES_HPP +#define CORE_LIBS_PRX_LIBSCEUSBSTORAGE_INCLUDE_HOSTVOLUMES_HPP + +#include +#include +#include +#include + +namespace UsbStorage { + +enum class FileSystem { + Fat, + ExFat, +}; + +struct HostVolume { + std::string key; + std::filesystem::path root; + std::uint64_t volumeSize = 0; + std::uint16_t vendorId = 0; + std::uint16_t productId = 0; + std::uint16_t bcdDevice = 0; + std::string vendor; + std::string product; + std::string serialNumber; + bool readOnly = false; + FileSystem fileSystem = FileSystem::Fat; +}; + +std::vector ScanSysfsVolumes(const std::filesystem::path& mountInfo, const std::filesystem::path& sysRoot); +#ifdef _WIN32 +std::vector ScanWindowsVolumes(); +#endif +std::vector ScanHostVolumes(); + +} + +extern "C" void SetUsbStorageHostRootsForTest_nid_no_patch(const char* mountInfo, const char* sysRoot); + +#endif diff --git a/core/libs/prx/libSceUsbStorage/src/HostVolumes.cpp b/core/libs/prx/libSceUsbStorage/src/HostVolumes.cpp new file mode 100644 index 0000000000..f7a684771b --- /dev/null +++ b/core/libs/prx/libSceUsbStorage/src/HostVolumes.cpp @@ -0,0 +1,172 @@ +#include "prx/libSceUsbStorage/include/HostVolumes.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace UsbStorage { +namespace { + +std::mutex rootsMutex; +std::optional> testRoots; + +std::filesystem::path Utf8Path(std::string_view text) { + return std::filesystem::path(std::u8string(text.begin(), text.end())); +} + +std::optional ReadText(const std::filesystem::path& path) { + std::ifstream file(path, std::ios::binary); + if (!file) return std::nullopt; + std::string text((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + if (file.bad()) return std::nullopt; + while (!text.empty() && (text.back() == '\n' || text.back() == '\r' || text.back() == ' ' || text.back() == '\t')) text.pop_back(); + return text; +} + +std::optional ReadHex(const std::filesystem::path& path) { + const auto text = ReadText(path); + if (!text || text->empty() || text->size() > 4) return std::nullopt; + std::uint16_t value = 0; + const auto [end, error] = std::from_chars(text->data(), text->data() + text->size(), value, 16); + if (error != std::errc{} || end != text->data() + text->size()) return std::nullopt; + return value; +} + +std::string Unescape(std::string_view text) { + std::string result; + const auto octal = [&](std::size_t at) { return text[at] >= '0' && text[at] <= '7'; }; + for (std::size_t i = 0; i < text.size(); ++i) { + if (text[i] == '\\' && i + 3 < text.size() && octal(i + 1) && octal(i + 2) && octal(i + 3)) { + result += static_cast((text[i + 1] - '0') * 64 + (text[i + 2] - '0') * 8 + (text[i + 3] - '0')); + i += 3; + continue; + } + result += text[i]; + } + return result; +} + +std::vector Fields(std::string_view line) { + std::vector fields; + while (!line.empty()) { + const auto separator = line.find(' '); + if (separator != 0) fields.push_back(line.substr(0, separator)); + if (separator == std::string_view::npos) break; + line.remove_prefix(separator + 1); + } + return fields; +} + +bool ReadOnly(std::string_view options) { + while (true) { + const auto separator = options.find(','); + if (options.substr(0, separator) == "ro") return true; + if (separator == std::string_view::npos) return false; + options.remove_prefix(separator + 1); + } +} + +std::optional FileSystemOf(std::string_view type) { + if (type == "vfat" || type == "msdos") return FileSystem::Fat; + if (type == "exfat") return FileSystem::ExFat; + return std::nullopt; +} + +std::optional Describe(const std::filesystem::path& sysRoot, std::string_view device, const std::string& mountPoint, + FileSystem fileSystem, bool readOnly) { + std::error_code error; + const auto top = std::filesystem::weakly_canonical(sysRoot, error); + if (error) return std::nullopt; + const auto block = std::filesystem::canonical(sysRoot / "dev" / "block" / std::filesystem::path(std::string(device)), error); + if (error) return std::nullopt; + std::optional scsi; + std::optional usb; + for (auto current = block; current != top && current.has_relative_path(); current = current.parent_path()) { + if (!scsi && std::filesystem::is_regular_file(current / "vendor", error) && std::filesystem::is_regular_file(current / "model", error)) { + scsi = current; + } + if (ReadText(current / "bInterfaceClass") == "08") { + usb = current.parent_path(); + break; + } + } + if (!usb) return std::nullopt; + const auto vendorId = ReadHex(*usb / "idVendor"); + const auto productId = ReadHex(*usb / "idProduct"); + const auto bcdDevice = ReadHex(*usb / "bcdDevice"); + if (!vendorId || !productId || !bcdDevice) return std::nullopt; + const auto root = Utf8Path(mountPoint); + if (!std::filesystem::is_directory(root, error)) return std::nullopt; + const auto space = std::filesystem::space(root, error); + if (error) return std::nullopt; + HostVolume volume; + volume.key = std::string(device) + ' ' + mountPoint; + volume.root = root; + volume.volumeSize = space.capacity; + volume.vendorId = *vendorId; + volume.productId = *productId; + volume.bcdDevice = *bcdDevice; + if (scsi) { + volume.vendor = ReadText(*scsi / "vendor").value_or(std::string()); + volume.product = ReadText(*scsi / "model").value_or(std::string()); + } + volume.serialNumber = ReadText(*usb / "serial").value_or(std::string()); + volume.readOnly = readOnly; + volume.fileSystem = fileSystem; + return volume; +} + +} + +std::vector ScanSysfsVolumes(const std::filesystem::path& mountInfo, const std::filesystem::path& sysRoot) { + std::vector volumes; + std::ifstream file(mountInfo, std::ios::binary); + if (!file) return volumes; + std::set> devices; + std::string line; + while (std::getline(file, line)) { + const auto fields = Fields(line); + const auto separator = std::find(fields.begin(), fields.end(), std::string_view("-")); + if (separator - fields.begin() < 6 || fields.end() - separator < 3) continue; + const auto fileSystem = FileSystemOf(separator[1]); + if (!fileSystem || fields[3] != "/" || devices.contains(fields[2])) continue; + devices.emplace(fields[2]); + if (auto volume = Describe(sysRoot, fields[2], Unescape(fields[4]), *fileSystem, ReadOnly(fields[5]))) volumes.push_back(std::move(*volume)); + } + return volumes; +} + +std::vector ScanHostVolumes() { + std::optional> roots; + { + std::lock_guard lock(rootsMutex); + roots = testRoots; + } + if (roots) return ScanSysfsVolumes(roots->first, roots->second); +#ifdef _WIN32 + return ScanWindowsVolumes(); +#elif defined(__linux__) + return ScanSysfsVolumes("/proc/self/mountinfo", "/sys"); +#else + return {}; +#endif +} + +} + +extern "C" void SetUsbStorageHostRootsForTest_nid_no_patch(const char* mountInfo, const char* sysRoot) { + std::lock_guard lock(UsbStorage::rootsMutex); + if (mountInfo == nullptr || sysRoot == nullptr) { + UsbStorage::testRoots.reset(); + return; + } + UsbStorage::testRoots.emplace(UsbStorage::Utf8Path(mountInfo), UsbStorage::Utf8Path(sysRoot)); +} diff --git a/core/libs/prx/libSceUsbStorage/src/HostVolumesWindows.cpp b/core/libs/prx/libSceUsbStorage/src/HostVolumesWindows.cpp new file mode 100644 index 0000000000..edb88eaee0 --- /dev/null +++ b/core/libs/prx/libSceUsbStorage/src/HostVolumesWindows.cpp @@ -0,0 +1,198 @@ +#include "prx/libSceUsbStorage/include/HostVolumes.hpp" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace UsbStorage { +namespace { + +struct Handle { + HANDLE value; + ~Handle() { + if (value != INVALID_HANDLE_VALUE) CloseHandle(value); + } +}; + +struct ErrorMode { + DWORD previous = 0; + ErrorMode() { SetThreadErrorMode(SEM_FAILCRITICALERRORS | SEM_NOOPENFILEERRORBOX, &previous); } + ~ErrorMode() { SetThreadErrorMode(previous, nullptr); } +}; + +struct UsbIdentity { + std::uint16_t vendorId; + std::uint16_t productId; + std::uint16_t bcdDevice; +}; + +std::string Trimmed(std::string text) { + while (!text.empty() && (text.back() == ' ' || text.back() == '\t')) text.pop_back(); + while (!text.empty() && (text.front() == ' ' || text.front() == '\t')) text.erase(text.begin()); + return text; +} + +std::string DescriptorString(const std::vector& descriptor, DWORD size, DWORD offset) { + if (offset == 0 || offset >= size) return {}; + const auto* begin = reinterpret_cast(descriptor.data() + offset); + return Trimmed(std::string(begin, strnlen(begin, size - offset))); +} + +std::optional Hex(std::wstring_view text) { + if (text.size() < 4) return std::nullopt; + std::uint16_t value = 0; + for (std::size_t i = 0; i < 4; ++i) { + const wchar_t digit = text[i]; + value = static_cast(value << 4); + if (digit >= L'0' && digit <= L'9') value = static_cast(value | (digit - L'0')); + else if (digit >= L'a' && digit <= L'f') value = static_cast(value | (digit - L'a' + 10)); + else if (digit >= L'A' && digit <= L'F') value = static_cast(value | (digit - L'A' + 10)); + else return std::nullopt; + } + return value; +} + +std::optional TaggedHex(std::wstring_view text, std::wstring_view tag) { + for (std::size_t at = 0; at + tag.size() <= text.size(); ++at) { + if (_wcsnicmp(text.data() + at, tag.data(), tag.size()) == 0) return Hex(text.substr(at + tag.size())); + } + return std::nullopt; +} + +std::optional DeviceNumber(HANDLE handle) { + STORAGE_DEVICE_NUMBER number{}; + DWORD bytes = 0; + if (!DeviceIoControl(handle, IOCTL_STORAGE_GET_DEVICE_NUMBER, nullptr, 0, &number, sizeof(number), &bytes, nullptr)) return std::nullopt; + return number; +} + +std::optional DiskInstance(DWORD diskNumber) { + const HDEVINFO set = SetupDiGetClassDevsW(&GUID_DEVINTERFACE_DISK, nullptr, nullptr, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE); + if (set == INVALID_HANDLE_VALUE) return std::nullopt; + const std::unique_ptr owner(set, &SetupDiDestroyDeviceInfoList); + SP_DEVICE_INTERFACE_DATA interfaceData{}; + interfaceData.cbSize = sizeof(interfaceData); + for (DWORD index = 0; SetupDiEnumDeviceInterfaces(set, nullptr, &GUID_DEVINTERFACE_DISK, index, &interfaceData); ++index) { + DWORD required = 0; + SetupDiGetDeviceInterfaceDetailW(set, &interfaceData, nullptr, 0, &required, nullptr); + if (required < sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA_W)) continue; + std::vector storage((required + sizeof(DWORD) - 1) / sizeof(DWORD)); + auto* detail = reinterpret_cast(storage.data()); + detail->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA_W); + SP_DEVINFO_DATA deviceData{}; + deviceData.cbSize = sizeof(deviceData); + if (!SetupDiGetDeviceInterfaceDetailW(set, &interfaceData, detail, required, nullptr, &deviceData)) continue; + const Handle disk{CreateFileW(detail->DevicePath, 0, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr)}; + if (disk.value == INVALID_HANDLE_VALUE) continue; + const auto number = DeviceNumber(disk.value); + if (number && number->DeviceNumber == diskNumber) return deviceData.DevInst; + } + return std::nullopt; +} + +std::optional Identity(DEVINST instance) { + for (DEVINST current = instance;;) { + DEVINST parent = 0; + if (CM_Get_Parent(&parent, current, 0) != CR_SUCCESS) return std::nullopt; + current = parent; + ULONG length = 0; + if (CM_Get_Device_ID_Size(&length, current, 0) != CR_SUCCESS) return std::nullopt; + std::wstring id(length + 1, L'\0'); + if (CM_Get_Device_IDW(current, id.data(), length + 1, 0) != CR_SUCCESS) return std::nullopt; + id.resize(length); + if (id.size() < 8 || _wcsnicmp(id.c_str(), L"USB\\VID_", 8) != 0) continue; + const auto vendorId = TaggedHex(id, L"VID_"); + const auto productId = TaggedHex(id, L"PID_"); + ULONG size = 0; + if (CM_Get_DevNode_Registry_PropertyW(current, CM_DRP_HARDWAREID, nullptr, nullptr, &size, 0) != CR_BUFFER_SMALL || size == 0) return std::nullopt; + std::vector hardwareIds(size / sizeof(wchar_t) + 2, L'\0'); + if (CM_Get_DevNode_Registry_PropertyW(current, CM_DRP_HARDWAREID, nullptr, hardwareIds.data(), &size, 0) != CR_SUCCESS) return std::nullopt; + std::optional bcdDevice; + for (const wchar_t* entry = hardwareIds.data(); *entry != L'\0' && !bcdDevice; entry += std::wcslen(entry) + 1) { + bcdDevice = TaggedHex(entry, L"REV_"); + } + if (!vendorId || !productId || !bcdDevice) return std::nullopt; + return UsbIdentity{*vendorId, *productId, *bcdDevice}; + } +} + +std::optional Describe(wchar_t letter) { + const wchar_t root[] = {letter, L':', L'\\', L'\0'}; + const UINT type = GetDriveTypeW(root); + if (type != DRIVE_REMOVABLE && type != DRIVE_FIXED) return std::nullopt; + wchar_t fileSystemName[MAX_PATH + 1]{}; + DWORD fileSystemFlags = 0; + if (!GetVolumeInformationW(root, nullptr, 0, nullptr, nullptr, &fileSystemFlags, fileSystemName, MAX_PATH + 1)) return std::nullopt; + std::optional fileSystem; + if (_wcsicmp(fileSystemName, L"FAT") == 0 || _wcsicmp(fileSystemName, L"FAT32") == 0) fileSystem = FileSystem::Fat; + if (_wcsicmp(fileSystemName, L"exFAT") == 0) fileSystem = FileSystem::ExFat; + if (!fileSystem) return std::nullopt; + const wchar_t devicePath[] = {L'\\', L'\\', L'.', L'\\', letter, L':', L'\0'}; + const Handle volume{CreateFileW(devicePath, 0, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr)}; + if (volume.value == INVALID_HANDLE_VALUE) return std::nullopt; + STORAGE_PROPERTY_QUERY query{}; + query.PropertyId = StorageDeviceProperty; + query.QueryType = PropertyStandardQuery; + std::vector descriptor(1024); + DWORD bytes = 0; + for (int attempt = 0; attempt < 2; ++attempt) { + if (!DeviceIoControl(volume.value, IOCTL_STORAGE_QUERY_PROPERTY, &query, sizeof(query), descriptor.data(), + static_cast(descriptor.size()), &bytes, nullptr)) { + return std::nullopt; + } + if (bytes < sizeof(STORAGE_DESCRIPTOR_HEADER)) return std::nullopt; + const auto size = reinterpret_cast(descriptor.data())->Size; + if (size <= descriptor.size()) break; + descriptor.resize(size); + } + if (bytes < sizeof(STORAGE_DEVICE_DESCRIPTOR)) return std::nullopt; + STORAGE_DEVICE_DESCRIPTOR header{}; + std::memcpy(&header, descriptor.data(), sizeof(header)); + if (header.BusType != BusTypeUsb) return std::nullopt; + const auto number = DeviceNumber(volume.value); + if (!number) return std::nullopt; + const auto instance = DiskInstance(number->DeviceNumber); + if (!instance) return std::nullopt; + const auto identity = Identity(*instance); + if (!identity) return std::nullopt; + ULARGE_INTEGER total{}; + if (!GetDiskFreeSpaceExW(root, nullptr, &total, nullptr)) return std::nullopt; + wchar_t volumeName[MAX_PATH]{}; + if (!GetVolumeNameForVolumeMountPointW(root, volumeName, MAX_PATH)) return std::nullopt; + HostVolume result; + for (const wchar_t* character = volumeName; *character != L'\0'; ++character) result.key += static_cast(*character & 0x7f); + result.root = std::filesystem::path(root); + result.volumeSize = total.QuadPart; + result.vendorId = identity->vendorId; + result.productId = identity->productId; + result.bcdDevice = identity->bcdDevice; + result.vendor = DescriptorString(descriptor, bytes, header.VendorIdOffset); + result.product = DescriptorString(descriptor, bytes, header.ProductIdOffset); + result.serialNumber = DescriptorString(descriptor, bytes, header.SerialNumberOffset); + result.readOnly = (fileSystemFlags & FILE_READ_ONLY_VOLUME) != 0; + result.fileSystem = *fileSystem; + return result; +} + +} + +std::vector ScanWindowsVolumes() { + const ErrorMode errorMode; + std::vector volumes; + const DWORD drives = GetLogicalDrives(); + for (int letter = 0; letter < 26; ++letter) { + if ((drives & (DWORD{1} << letter)) == 0) continue; + if (auto volume = Describe(static_cast(L'A' + letter))) volumes.push_back(std::move(*volume)); + } + return volumes; +} + +} diff --git a/core/libs/tests/GuestUsbStorage.cpp b/core/libs/tests/GuestUsbStorage.cpp index 43a173d247..4ef5ccd7ee 100644 --- a/core/libs/tests/GuestUsbStorage.cpp +++ b/core/libs/tests/GuestUsbStorage.cpp @@ -1,15 +1,31 @@ #include "prx/libSceUsbStorage/UsbStorage.hpp" +#include "prx/libSceUsbStorage/include/HostVolumes.hpp" +#include "prx/libc/include/General.hpp" +#include +#include #include #include #include #include +#include +#include +#include +#include +#include #include +#include #include +#include +#include +#include +#include namespace { int g_attr = 0; +int g_mountTag = 0; +int g_unmountTag = 0; void APS5_VABI CallbackStub(const UsbStorageEvent*, void*) {} @@ -24,91 +40,213 @@ void Require(bool condition, const char* message) { } } +template +bool ThrowsWith(TCall call, std::string_view message) { + try { + call(); + } catch (const std::runtime_error& error) { + return std::string_view(error.what()) == message; + } + return false; +} + +struct RecordedEvent { + UsbStorageEvent event; + void* userData; +}; + +struct Recorder { + std::mutex mutex; + std::condition_variable changed; + std::vector events; +}; + +Recorder& Events() { + static Recorder recorder; + return recorder; +} + +void APS5_VABI Record(const UsbStorageEvent* event, void* userData) { + auto& recorder = Events(); + std::lock_guard lock(recorder.mutex); + recorder.events.push_back({*event, userData}); + recorder.changed.notify_all(); +} + +RecordedEvent NextEvent(std::size_t index) { + auto& recorder = Events(); + std::unique_lock lock(recorder.mutex); + const bool arrived = recorder.changed.wait_for(lock, std::chrono::seconds(10), [&] { return recorder.events.size() > index; }); + Require(arrived, "no USB storage event arrived within 10 seconds"); + return recorder.events[index]; +} + +struct FakeHost { + std::filesystem::path root; + std::filesystem::path mountInfo; + std::filesystem::path sys; + std::filesystem::path firstMount; + std::filesystem::path secondMount; +}; + +void WriteFile(const std::filesystem::path& path, std::string_view text) { + std::filesystem::create_directories(path.parent_path()); + std::ofstream file(path, std::ios::binary); + file << text; + file.close(); + Require(static_cast(file), "write a fake host file"); +} + +void WriteMounts(const FakeHost& host, const std::string& lines) { + auto staged = host.mountInfo; + staged += ".new"; + WriteFile(staged, lines); + std::filesystem::rename(staged, host.mountInfo); +} + +std::string Escaped(const std::filesystem::path& path) { + std::string escaped; + for (const char character : path.string()) { + if (character == ' ') escaped += "\\040"; + else if (character == '\\') escaped += "\\134"; + else escaped += character; + } + return escaped; +} + +std::string MountLine(std::string_view device, std::string_view root, const std::filesystem::path& mount, std::string_view options, std::string_view type) { + return "36 25 " + std::string(device) + " " + std::string(root) + " " + Escaped(mount) + " " + std::string(options) + + " shared:1 - " + std::string(type) + " /dev/fake rw\n"; +} + +std::string FirstLine(const FakeHost& host) { + return MountLine("8:17", "/", host.firstMount, "rw,nosuid,relatime", "vfat"); +} + +std::string SecondLine(const FakeHost& host) { + return MountLine("8:33", "/", host.secondMount, "ro,relatime", "exfat"); +} + +FakeHost CreateHost() { + FakeHost host; + host.root = std::filesystem::temp_directory_path() / ("anyps5_usb_storage_" + std::to_string(std::random_device{}())); + host.mountInfo = host.root / "mountinfo"; + host.sys = host.root / "sys"; + host.firstMount = host.root / "media" / "USB DRIVE"; + host.secondMount = host.root / "media" / "second"; + std::filesystem::create_directories(host.sys / "dev" / "block"); + WriteMounts(host, ""); + SetUsbStorageHostRootsForTest_nid_no_patch(host.mountInfo.string().c_str(), host.sys.string().c_str()); + return host; +} + +bool Link(const FakeHost& host, std::string_view device, const std::filesystem::path& target) { + std::filesystem::create_directories(target); + std::error_code error; + std::filesystem::create_directory_symlink(target, host.sys / "dev" / "block" / std::string(device), error); + return !error; +} + +bool CreateDevices(const FakeHost& host) { + const auto probe = host.sys / "devices" / "probe:0"; + std::error_code error; + std::filesystem::create_directories(probe, error); + if (error || !Link(host, "0:0", probe)) return false; + const auto controller = host.sys / "devices" / "pci0000:00" / "0000:00:14.0" / "usb1"; + const auto first = controller / "1-1"; + WriteFile(first / "idVendor", "0781\n"); + WriteFile(first / "idProduct", "5581\n"); + WriteFile(first / "bcdDevice", "0100\n"); + WriteFile(first / "serial", "4C530001\n"); + WriteFile(first / "1-1:1.0" / "bInterfaceClass", "08\n"); + const auto firstScsi = first / "1-1:1.0" / "host6" / "target6:0:0" / "6:0:0:0"; + WriteFile(firstScsi / "vendor", "SanDisk \n"); + WriteFile(firstScsi / "model", "Ultra Fit \n"); + const auto second = controller / "1-2"; + WriteFile(second / "idVendor", "090c\n"); + WriteFile(second / "idProduct", "1000\n"); + WriteFile(second / "bcdDevice", "1100\n"); + WriteFile(second / "1-2:1.0" / "bInterfaceClass", "08\n"); + const auto secondScsi = second / "1-2:1.0" / "host7" / "target7:0:0" / "7:0:0:0"; + WriteFile(secondScsi / "vendor", "Samsung \n"); + WriteFile(secondScsi / "model", "Flash Drive \n"); + const auto sata = host.sys / "devices" / "pci0000:00" / "0000:00:17.0" / "ata1" / "host0" / "target0:0:0" / "0:0:0:0"; + WriteFile(sata / "vendor", "ATA \n"); + WriteFile(sata / "model", "Disk \n"); + WriteFile(host.firstMount / "song.txt", "song"); + WriteFile(host.firstMount / ".hidden", "hidden"); + std::filesystem::create_directories(host.firstMount / "Music"); + WriteFile(host.secondMount / "Readme.txt", "readme"); + WriteFile(host.root / "media" / "efi" / "boot.efi", "efi"); + return Link(host, "8:17", firstScsi / "block" / "sdb" / "sdb1") && Link(host, "8:33", secondScsi / "block" / "sdc" / "sdc1") && + Link(host, "8:1", sata / "block" / "sda" / "sda1") && Link(host, "8:49", firstScsi / "block" / "sdd" / "sdd1"); +} + void TestInitializeAndTerminate() { + Require(sceUsbStorageTerm() == 0, "terminate before initialize"); Require(sceUsbStorageInit(nullptr) == 0, "initialize with a null attribute"); - Require(sceUsbStorageInit(&g_attr) == 0, "initialize with an attribute"); + Require(sceUsbStorageInit(nullptr) == 0, "initialize again"); + Require(ThrowsWith([] { sceUsbStorageInit(&g_attr); }, "sceUsbStorageInit: a thread attribute is not implemented"), + "initialize with an attribute did not throw"); Require(sceUsbStorageTerm() == 0, "terminate"); - Require(sceUsbStorageInit(nullptr) == 0, "initialize after terminate"); Require(sceUsbStorageTerm() == 0, "terminate again"); + Require(sceUsbStorageInit(nullptr) == 0, "initialize after terminate"); + Require(sceUsbStorageTerm() == 0, "terminate after initializing again"); } void TestRegisterCallbacks() { - bool threw = false; - try { - sceUsbStorageRegisterCallback(1, Callback(), nullptr); - } catch (const std::runtime_error& error) { - threw = std::string_view(error.what()) == "sceUsbStorageRegisterCallback not implemented"; - } - Require(threw, "register callback of event type 1 did not throw its marker"); - threw = false; - try { - sceUsbStorageRegisterCallback(2, Callback(), nullptr); - } catch (const std::runtime_error& error) { - threw = std::string_view(error.what()) == "sceUsbStorageRegisterCallback not implemented"; - } - Require(threw, "register callback of event type 2 did not throw its marker"); + Require(ThrowsWith([] { sceUsbStorageRegisterCallback(1, Callback(), nullptr); }, + "sceUsbStorageRegisterCallback: registering before sceUsbStorageInit is not implemented"), + "register a callback before initialize did not throw"); + Require(ThrowsWith([] { sceUsbStorageUnregisterCallback(1, Callback()); }, "sceUsbStorageUnregisterCallback not implemented"), + "unregister a callback before initialize did not throw its marker"); Require(sceUsbStorageRegisterCallback(1, nullptr, nullptr) == UsbStorage::InvalidArgument, "register a null callback"); Require(sceUsbStorageRegisterCallback(0, Callback(), nullptr) == UsbStorage::InvalidArgument, "register callback of event type 0"); Require(sceUsbStorageRegisterCallback(3, Callback(), nullptr) == UsbStorage::InvalidArgument, "register callback of event type 3"); Require(sceUsbStorageRegisterCallback(-1, Callback(), nullptr) == UsbStorage::InvalidArgument, "register callback of a negative event type"); - threw = false; - try { - sceUsbStorageUnregisterCallback(1, Callback()); - } catch (const std::runtime_error& error) { - threw = std::string_view(error.what()) == "sceUsbStorageUnregisterCallback not implemented"; - } - Require(threw, "unregister callback of event type 1 did not throw its marker"); - threw = false; - try { - sceUsbStorageUnregisterCallback(2, Callback()); - } catch (const std::runtime_error& error) { - threw = std::string_view(error.what()) == "sceUsbStorageUnregisterCallback not implemented"; - } - Require(threw, "unregister callback of event type 2 did not throw its marker"); + Require(sceUsbStorageInit(nullptr) == 0, "initialize for callbacks"); + Require(sceUsbStorageRegisterCallback(1, Callback(), nullptr) == 0, "register a mount callback"); + Require(sceUsbStorageRegisterCallback(2, Callback(), &g_attr) == 0, "register an unmount callback"); + Require(ThrowsWith([] { sceUsbStorageRegisterCallback(1, Callback(), &g_attr); }, + "sceUsbStorageRegisterCallback: registering a registered callback again is not implemented"), + "register a registered callback again did not throw"); Require(sceUsbStorageUnregisterCallback(1, nullptr) == UsbStorage::InvalidArgument, "unregister a null callback"); Require(sceUsbStorageUnregisterCallback(0, Callback()) == UsbStorage::InvalidArgument, "unregister callback of event type 0"); + Require(sceUsbStorageUnregisterCallback(1, Callback()) == 0, "unregister the mount callback"); + Require(sceUsbStorageUnregisterCallback(2, Callback()) == 0, "unregister the unmount callback"); + Require(ThrowsWith([] { sceUsbStorageUnregisterCallback(1, Callback()); }, "sceUsbStorageUnregisterCallback not implemented"), + "unregister an unregistered callback did not throw its marker"); + Require(sceUsbStorageRegisterCallback(2, Callback(), nullptr) == 0, "register a callback before terminate"); + Require(sceUsbStorageTerm() == 0, "terminate with a registered callback"); + Require(ThrowsWith([] { sceUsbStorageUnregisterCallback(2, Callback()); }, "sceUsbStorageUnregisterCallback not implemented"), + "terminate kept the registered callback"); } void TestMapCallbacks() { - bool threw = false; - try { - sceUsbStorageRegisterCallbackForMapAvailable(0, "/data", Callback(), nullptr); - } catch (const std::runtime_error& error) { - threw = std::string_view(error.what()) == "sceUsbStorageRegisterCallbackForMapAvailable not implemented"; - } - Require(threw, "register map-available callback did not throw its marker"); + Require(ThrowsWith([] { sceUsbStorageRegisterCallbackForMapAvailable(0, "/data", Callback(), nullptr); }, + "sceUsbStorageRegisterCallbackForMapAvailable not implemented"), + "register map-available callback did not throw its marker"); Require(sceUsbStorageRegisterCallbackForMapAvailable(0, nullptr, Callback(), nullptr) == UsbStorage::InvalidArgument, "register map-available callback with a null directory"); Require(sceUsbStorageRegisterCallbackForMapAvailable(0, "/data", nullptr, nullptr) == UsbStorage::InvalidArgument, "register a null map-available callback"); - threw = false; - try { - sceUsbStorageUnregisterCallbackForMapAvailable(0, "/data", Callback()); - } catch (const std::runtime_error& error) { - threw = std::string_view(error.what()) == "sceUsbStorageUnregisterCallbackForMapAvailable not implemented"; - } - Require(threw, "unregister map-available callback did not throw its marker"); + Require(ThrowsWith([] { sceUsbStorageUnregisterCallbackForMapAvailable(0, "/data", Callback()); }, + "sceUsbStorageUnregisterCallbackForMapAvailable not implemented"), + "unregister map-available callback did not throw its marker"); Require(sceUsbStorageUnregisterCallbackForMapAvailable(0, nullptr, Callback()) == UsbStorage::InvalidArgument, "unregister map-available callback with a null directory"); Require(sceUsbStorageUnregisterCallbackForMapAvailable(0, "/data", nullptr) == UsbStorage::InvalidArgument, "unregister a null map-available callback"); - threw = false; - try { - sceUsbStorageRegisterCallbackForMapUnavailable(0, "/data", Callback(), nullptr); - } catch (const std::runtime_error& error) { - threw = std::string_view(error.what()) == "sceUsbStorageRegisterCallbackForMapUnavailable not implemented"; - } - Require(threw, "register map-unavailable callback did not throw its marker"); + Require(ThrowsWith([] { sceUsbStorageRegisterCallbackForMapUnavailable(0, "/data", Callback(), nullptr); }, + "sceUsbStorageRegisterCallbackForMapUnavailable not implemented"), + "register map-unavailable callback did not throw its marker"); Require(sceUsbStorageRegisterCallbackForMapUnavailable(0, nullptr, Callback(), nullptr) == UsbStorage::InvalidArgument, "register map-unavailable callback with a null directory"); Require(sceUsbStorageRegisterCallbackForMapUnavailable(0, "/data", nullptr, nullptr) == UsbStorage::InvalidArgument, "register a null map-unavailable callback"); - threw = false; - try { - sceUsbStorageUnregisterCallbackForMapUnavailable(0, "/data", Callback()); - } catch (const std::runtime_error& error) { - threw = std::string_view(error.what()) == "sceUsbStorageUnregisterCallbackForMapUnavailable not implemented"; - } - Require(threw, "unregister map-unavailable callback did not throw its marker"); + Require(ThrowsWith([] { sceUsbStorageUnregisterCallbackForMapUnavailable(0, "/data", Callback()); }, + "sceUsbStorageUnregisterCallbackForMapUnavailable not implemented"), + "unregister map-unavailable callback did not throw its marker"); Require(sceUsbStorageUnregisterCallbackForMapUnavailable(0, nullptr, Callback()) == UsbStorage::InvalidArgument, "unregister map-unavailable callback with a null directory"); Require(sceUsbStorageUnregisterCallbackForMapUnavailable(0, "/data", nullptr) == UsbStorage::InvalidArgument, "unregister a null map-unavailable callback"); } void TestNoDevice() { + Require(sceUsbStorageInit(nullptr) == 0, "initialize without a device"); std::uint32_t devices[4]{0x5a5a5a5a, 0x5a5a5a5a, 0x5a5a5a5a, 0x5a5a5a5a}; std::int32_t count = -1; Require(sceUsbStorageGetDeviceList(devices, &count) == 0, "get the device list"); @@ -117,12 +255,11 @@ void TestNoDevice() { Require(sceUsbStorageGetDeviceList(nullptr, &count) == UsbStorage::InvalidArgument, "get the device list without a device array"); Require(sceUsbStorageGetDeviceList(devices, nullptr) == UsbStorage::InvalidArgument, "get the device list without a count"); - std::uint8_t info[16]; - std::memset(info, 0x5a, sizeof(info)); - auto* const infoRecord = reinterpret_cast(info); - Require(sceUsbStorageGetDeviceInfo(0, infoRecord) == UsbStorage::DeviceNotFound, "get device info of device 0"); - Require(sceUsbStorageGetDeviceInfo(1, infoRecord) == UsbStorage::DeviceNotFound, "get device info of device 1"); - Require(info[0] == 0x5a && info[15] == 0x5a, "device info was written"); + UsbStorageDeviceInfo info; + std::memset(&info, 0x5a, sizeof(info)); + Require(sceUsbStorageGetDeviceInfo(0, &info) == UsbStorage::DeviceNotFound, "get device info of device 0"); + Require(sceUsbStorageGetDeviceInfo(1, &info) == UsbStorage::DeviceNotFound, "get device info of device 1"); + Require(info.volumeSize == 0x5a5a5a5a5a5a5a5aull && info.flags == 0x5a5a5a5a5a5a5a5aull, "device info was written"); Require(sceUsbStorageGetDeviceInfo(0, nullptr) == UsbStorage::InvalidArgument, "get device info without an info buffer"); std::uint8_t exists = 1; @@ -131,6 +268,9 @@ void TestNoDevice() { Require(sceUsbStorageIsExist(1, nullptr, &exists) == true && exists == 0, "device 1 does not exist"); exists = 1; Require(sceUsbStorageIsExist(0xffffffffu, "/mnt/usb0", &exists) == true && exists == 0, "no device id exists"); + Require(ThrowsWith([] { sceUsbStorageIsExist(0, "/mnt/usb0", nullptr); }, "sceUsbStorageIsExist: a null output is not implemented"), + "an existence query without an output did not throw"); + Require(sceUsbStorageTerm() == 0, "terminate without a device"); } void TestMapRequests() { @@ -146,20 +286,10 @@ void TestMapRequests() { Require(sceUsbStorageRequestMapWSB(0, nullptr, 0, 0, nullptr, nullptr) == UsbStorage::DeviceNotFound, "request a WSB map without a subpath"); Require(mountPoint[0] == static_cast(0x5a) && status == 0x5a5a5a5a, "the WSB map request wrote its outputs"); Require(sceUsbStorageRequestUnmap(0, nullptr) == UsbStorage::InvalidArgument, "unmap a null directory"); - bool threw = false; - try { - sceUsbStorageRequestUnmap(0, "/mnt/usb0"); - } catch (const std::runtime_error& error) { - threw = std::string_view(error.what()) == "sceUsbStorageRequestUnmap not implemented"; - } - Require(threw, "unmap a directory did not throw its marker"); - threw = false; - try { - sceUsbStorageRequestUnmap(7, "/mnt/usb7"); - } catch (const std::runtime_error& error) { - threw = std::string_view(error.what()) == "sceUsbStorageRequestUnmap not implemented"; - } - Require(threw, "unmap a directory of another device did not throw its marker"); + Require(ThrowsWith([] { sceUsbStorageRequestUnmap(0, "/mnt/usb0"); }, "sceUsbStorageRequestUnmap not implemented"), + "unmap a directory did not throw its marker"); + Require(ThrowsWith([] { sceUsbStorageRequestUnmap(7, "/mnt/usb7"); }, "sceUsbStorageRequestUnmap not implemented"), + "unmap a directory of another device did not throw its marker"); } void TestGetdents() { @@ -180,57 +310,176 @@ void TestGetdents() { Require(sceUsbStorageGetdentsRead(1, nullptr, sizeof(buffer), &bytesRead) == UsbStorage::InvalidArgument, "read a getdents scan without a buffer"); Require(sceUsbStorageGetdentsRead(1, buffer, sizeof(buffer), nullptr) == UsbStorage::InvalidArgument, "read a getdents scan without a byte count"); - bool threw = false; - try { - sceUsbStorageGetdentsClose(1); - } catch (const std::runtime_error& error) { - threw = std::string_view(error.what()) == "sceUsbStorageGetdentsClose not implemented"; - } - Require(threw, "close a getdents scan that was never opened did not throw its marker"); - threw = false; - try { - sceUsbStorageGetdentsClose(0); - } catch (const std::runtime_error& error) { - threw = std::string_view(error.what()) == "sceUsbStorageGetdentsClose not implemented"; - } - Require(threw, "close descriptor 0 did not throw its marker"); + Require(ThrowsWith([] { sceUsbStorageGetdentsClose(1); }, "sceUsbStorageGetdentsClose not implemented"), + "close a getdents scan that was never opened did not throw its marker"); + Require(ThrowsWith([] { sceUsbStorageGetdentsClose(0); }, "sceUsbStorageGetdentsClose not implemented"), + "close descriptor 0 did not throw its marker"); } void TestThrowingExports() { char mountPoint[8]{}; - bool threw = false; - try { - sceUsbStorageGetMountPointOfShellCore(0, mountPoint); - } catch (const std::runtime_error& error) { - threw = std::string_view(error.what()) == "sceUsbStorageGetMountPointOfShellCore not implemented"; + Require(ThrowsWith([&] { sceUsbStorageGetMountPointOfShellCore(0, mountPoint); }, "sceUsbStorageGetMountPointOfShellCore not implemented"), + "get the mount point of shell core without a device did not throw its marker"); + Require(ThrowsWith([] { sceUsbStorageClearFakeWriteProtection(); }, "sceUsbStorageClearFakeWriteProtection not implemented"), + "clear fake write protection did not throw its marker"); + Require(ThrowsWith([] { sceUsbStorageSetFakeMapLockForDebug(); }, "sceUsbStorageSetFakeMapLockForDebug not implemented"), + "set fake map lock for debug did not throw its marker"); + Require(ThrowsWith([] { sceUsbStorageSetFakeWriteProtection(); }, "sceUsbStorageSetFakeWriteProtection not implemented"), + "set fake write protection did not throw its marker"); +} + +void TestDeviceInfo(const FakeHost& host) { + std::uint32_t devices[UsbStorage::MaxDevices]{}; + std::int32_t count = -1; + Require(sceUsbStorageGetDeviceList(devices, &count) == 0, "get the device list with a device"); + Require(count == 1 && devices[0] == 0, "only the mounted FAT volume of a USB mass-storage device is listed"); + + UsbStorageDeviceInfo info; + std::memset(&info, 0x5a, sizeof(info)); + Require(sceUsbStorageGetDeviceInfo(0, &info) == 0, "get device info of the attached device"); + Require(info.vendorId == 0x0781 && info.productId == 0x5581 && info.bcdDevice == 0x0100, "the device info holds the USB ids and release"); + Require(std::string_view(info.vendor) == "SanDisk" && info.vendorLength == 7, "the device info holds the vendor"); + Require(std::string_view(info.product) == "Ultra Fit" && info.productLength == 9, "the device info holds the product"); + Require(std::string_view(info.serialNumber) == "4C530001" && info.serialNumberLength == 8, "the device info holds the serial number"); + Require(info.flags == UsbStorage::FatFlag, "the device info flags a writable FAT volume"); + Require(info.reserved == 0 && info.padding[0] == 0 && info.padding[1] == 0, "the device info cleared its unknown fields"); + std::error_code error; + const auto capacity = std::filesystem::space(host.firstMount, error).capacity; + Require(!error && info.volumeSize == capacity, "the volume size is the capacity of the mounted file system"); + Require(sceUsbStorageGetDeviceInfo(1, &info) == UsbStorage::DeviceNotFound, "get device info of an absent device"); + + char mountPoint[UsbStorage::MountPointSize]; + std::memset(mountPoint, 0x5a, sizeof(mountPoint)); + Require(sceUsbStorageGetMountPointOfShellCore(0, mountPoint) == 0 && std::string_view(mountPoint) == "/mnt/usb0", "the device is mounted at /mnt/usb0"); + std::uint8_t exists = 0; + Require(sceUsbStorageIsExist(0, "/mnt/usb0", &exists) == true && exists == 1, "the attached device exists"); + exists = 1; + Require(sceUsbStorageIsExist(1, "/mnt/usb1", &exists) == true && exists == 0, "device 1 does not exist"); + const auto resolved = ResolvePath_nid_no_patch("/mnt/usb0/song.txt"); + Require(std::filesystem::equivalent(resolved, host.firstMount / "song.txt", error), "the mount point resolves to the host volume"); +} + +void TestDeviceMaps() { + char mountPoint[UsbStorage::MountPointSize]; + std::uint64_t mountId = 0x5a5a5a5a5a5a5a5aull; + Require(sceUsbStorageRequestMap(0, "/", 0, 0, mountPoint, &mountId) == 0, "map the root of the device"); + Require(std::string_view(mountPoint) == "/mnt/usb0" && mountId == 0, "the root is mapped at the mount point"); + Require(sceUsbStorageRequestMap(0, "Music/", 0, 0, mountPoint, nullptr) == 0, "map a directory of the device"); + Require(std::string_view(mountPoint) == "/mnt/usb0/Music", "the directory is mapped below the mount point"); + Require(ThrowsWith([&] { sceUsbStorageRequestMap(0, "/Music", 0, 0, mountPoint, nullptr); }, + "sceUsbStorageRequestMap: mapping a mapped directory again is not implemented"), + "map a mapped directory again did not throw"); + Require(ThrowsWith([&] { sceUsbStorageRequestMap(0, "/Missing", 0, 0, mountPoint, nullptr); }, + "sceUsbStorageRequestMap: mapping a directory the device does not hold is not implemented"), + "map a missing directory did not throw"); + Require(ThrowsWith([&] { sceUsbStorageRequestMap(0, "/song.txt", 0, 0, mountPoint, nullptr); }, + "sceUsbStorageRequestMap: mapping a directory the device does not hold is not implemented"), + "map a file did not throw"); + Require(ThrowsWith([&] { sceUsbStorageRequestMap(0, "/Music/../Music", 0, 0, mountPoint, nullptr); }, + "sceUsbStorageRequestMap: a directory with . or .. components is not implemented"), + "map a directory with a .. component did not throw"); + Require(ThrowsWith([&] { sceUsbStorageRequestMap(0, "/", 1, 0, mountPoint, nullptr); }, + "sceUsbStorageRequestMap: nonzero flags or a volume size are not implemented"), + "map with flags did not throw"); + Require(sceUsbStorageRequestUnmap(0, "/mnt/usb0/Music") == 0, "unmap the directory"); + Require(ThrowsWith([] { sceUsbStorageRequestUnmap(0, "/mnt/usb0/Music"); }, "sceUsbStorageRequestUnmap not implemented"), + "unmap an unmapped directory did not throw its marker"); + Require(sceUsbStorageRequestUnmap(0, "/mnt/usb0") == 0, "unmap the root"); + std::int32_t status = 0x5a5a5a5a; + Require(sceUsbStorageRequestMapWSB(0, nullptr, 0, 0, mountPoint, &status) == 0, "map the root through the WSB request"); + Require(std::string_view(mountPoint) == "/mnt/usb0" && status == 0, "the WSB request mapped the root at the mount point"); + Require(ThrowsWith([&] { sceUsbStorageRequestMapWSB(0, "/Music", 0, 1, mountPoint, &status); }, + "sceUsbStorageRequestMapWSB: nonzero flags or a nonzero fourth argument are not implemented"), + "a WSB request with a fourth argument did not throw"); + Require(sceUsbStorageRequestUnmap(0, "/mnt/usb0") == 0, "unmap the WSB root"); +} + +void TestDeviceGetdents() { + std::int32_t fd = -1; + std::uint64_t bufferSize = 0; + Require(sceUsbStorageGetdentsOpen(0, &fd, &bufferSize) == 0 && fd > 0 && bufferSize == 0x8000, "open a getdents scan of the device"); + std::uint8_t small[8]; + std::uint64_t bytesRead = 0; + Require(ThrowsWith([&] { sceUsbStorageGetdentsRead(fd, small, sizeof(small), &bytesRead); }, + "sceUsbStorageGetdentsRead: a buffer smaller than the next entry is not implemented"), + "read into a buffer smaller than an entry did not throw"); + std::vector buffer(0x8000, 0x5a); + Require(sceUsbStorageGetdentsRead(fd, buffer.data(), buffer.size(), &bytesRead) == 0, "read the getdents scan"); + std::set> entries; + std::set fileNumbers; + for (std::uint64_t offset = 0; offset < bytesRead;) { + std::uint32_t fileNumber = 0; + std::uint16_t length = 0; + std::memcpy(&fileNumber, buffer.data() + offset, sizeof(fileNumber)); + std::memcpy(&length, buffer.data() + offset + 4, sizeof(length)); + const std::uint8_t type = buffer[offset + 6]; + const std::uint8_t nameLength = buffer[offset + 7]; + const std::string name(reinterpret_cast(buffer.data() + offset + 8), nameLength); + Require(length == ((8 + nameLength + 1 + 7) & ~7) && buffer[offset + 8 + nameLength] == 0, "a dirent record has the packed layout"); + Require(offset + length <= bytesRead, "a dirent record ends inside the bytes read"); + fileNumbers.insert(fileNumber); + entries.emplace(name, type); + offset += length; } - Require(threw, "get the mount point of shell core did not throw its marker"); - threw = false; - try { - sceUsbStorageClearFakeWriteProtection(); - } catch (const std::runtime_error& error) { - threw = std::string_view(error.what()) == "sceUsbStorageClearFakeWriteProtection not implemented"; - } - Require(threw, "clear fake write protection did not throw its marker"); - threw = false; - try { - sceUsbStorageSetFakeMapLockForDebug(); - } catch (const std::runtime_error& error) { - threw = std::string_view(error.what()) == "sceUsbStorageSetFakeMapLockForDebug not implemented"; - } - Require(threw, "set fake map lock for debug did not throw its marker"); - threw = false; - try { - sceUsbStorageSetFakeWriteProtection(); - } catch (const std::runtime_error& error) { - threw = std::string_view(error.what()) == "sceUsbStorageSetFakeWriteProtection not implemented"; - } - Require(threw, "set fake write protection did not throw its marker"); + Require(entries == std::set>{{"Music", 4}, {"song.txt", 8}}, "the scan lists the root without dot names"); + Require(fileNumbers == std::set{1, 2}, "the scan numbers its entries from 1"); + Require(sceUsbStorageGetdentsRead(fd, buffer.data(), buffer.size(), &bytesRead) == 0 && bytesRead == 0, "the scan ends with zero bytes"); + Require(sceUsbStorageGetdentsClose(fd) == 0, "close the getdents scan"); + Require(ThrowsWith([&] { sceUsbStorageGetdentsClose(fd); }, "sceUsbStorageGetdentsClose not implemented"), "close a closed scan did not throw its marker"); +} + +void TestDevices(const FakeHost& host) { + WriteMounts(host, FirstLine(host) + MountLine("8:1", "/", host.root / "media" / "efi", "rw", "vfat") + + MountLine("8:49", "/", host.root / "media", "rw", "ext4") + MountLine("8:17", "/Music", host.root / "media" / "bound", "rw", "vfat")); + Require(sceUsbStorageInit(nullptr) == 0, "initialize with a device"); + TestDeviceInfo(host); + TestDeviceMaps(); + TestDeviceGetdents(); + Require(sceUsbStorageTerm() == 0, "terminate with a device"); + std::uint32_t devices[UsbStorage::MaxDevices]{}; + std::int32_t count = -1; + Require(sceUsbStorageGetDeviceList(devices, &count) == 0 && count == 0, "terminate released the device list"); + std::error_code error; + Require(!std::filesystem::equivalent(ResolvePath_nid_no_patch("/mnt/usb0/song.txt"), host.firstMount / "song.txt", error), + "terminate removed the mount point"); +} + +void TestHotplug(const FakeHost& host) { + WriteMounts(host, FirstLine(host)); + Require(sceUsbStorageInit(nullptr) == 0, "initialize for hotplug"); + Require(sceUsbStorageRegisterCallback(UsbStorage::MountEvent, &Record, &g_mountTag) == 0, "register the mount recorder"); + Require(sceUsbStorageRegisterCallback(UsbStorage::UnmountEvent, &Record, &g_unmountTag) == 0, "register the unmount recorder"); + + WriteMounts(host, FirstLine(host) + SecondLine(host)); + auto recorded = NextEvent(0); + Require(recorded.event.event == 1 && recorded.event.device == 1 && recorded.userData == &g_mountTag, "a mounted device raises a mount event"); + Require(recorded.event.flags == (UsbStorage::ExFatFlag | UsbStorage::ReadOnlyFlag), "the mount event flags a read-only exFAT volume"); + UsbStorageDeviceInfo info{}; + Require(sceUsbStorageGetDeviceInfo(1, &info) == 0 && info.vendorId == 0x090c && info.bcdDevice == 0x1100, "the mounted device is listed"); + Require(info.serialNumber[0] == '\0' && info.serialNumberLength == 0 && std::string_view(info.product) == "Flash Drive", "a device without a serial number reports an empty one"); + + WriteMounts(host, SecondLine(host)); + recorded = NextEvent(1); + Require(recorded.event.event == 2 && recorded.event.device == 0 && recorded.userData == &g_unmountTag, "a removed device raises an unmount event"); + Require(recorded.event.flags == UsbStorage::FatFlag, "the unmount event flags the removed volume"); + std::uint32_t devices[UsbStorage::MaxDevices]{}; + std::int32_t count = -1; + Require(sceUsbStorageGetDeviceList(devices, &count) == 0 && count == 1 && devices[0] == 1, "the remaining device keeps its id"); + + WriteMounts(host, FirstLine(host) + SecondLine(host)); + recorded = NextEvent(2); + Require(recorded.event.event == 1 && recorded.event.device == 0, "a device mounted again takes the lowest free id"); + Require(sceUsbStorageUnregisterCallback(UsbStorage::MountEvent, &Record) == 0, "unregister the mount recorder"); + Require(sceUsbStorageUnregisterCallback(UsbStorage::UnmountEvent, &Record) == 0, "unregister the unmount recorder"); + Require(sceUsbStorageTerm() == 0, "terminate after hotplug"); + std::lock_guard lock(Events().mutex); + Require(Events().events.size() == 3, "hotplug raised no other event"); } } int main() { + const auto host = CreateHost(); TestInitializeAndTerminate(); TestRegisterCallbacks(); TestMapCallbacks(); @@ -238,5 +487,14 @@ int main() { TestMapRequests(); TestGetdents(); TestThrowingExports(); + if (CreateDevices(host)) { + TestDevices(host); + TestHotplug(host); + } else { + std::printf("USBSTORAGE: directory symbolic links are unavailable, the device cases are skipped\n"); + } + SetUsbStorageHostRootsForTest_nid_no_patch(nullptr, nullptr); + std::error_code error; + std::filesystem::remove_all(host.root, error); return 0; } diff --git a/docs/dev/debt/libSceUsbStorage.md b/docs/dev/debt/libSceUsbStorage.md index c901faca35..e3877eb181 100644 --- a/docs/dev/debt/libSceUsbStorage.md +++ b/docs/dev/debt/libSceUsbStorage.md @@ -1,19 +1,21 @@ # libSceUsbStorage technical debt -### Silent stubs - -- [sceUsbStorageInit, sceUsbStorageTerm](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - no USB mass-storage device is ever attached, so there is nothing to initialize or tear down: both accept every call and keep no state, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/usbd/usbstorage.cpp#L91-L99) does with its plain ORBIS_OK returns. A passthrough that lists devices must build its list in `Init` and free it in `Term`. - ### Unknown function info -- [sceUsbStorageRegisterCallback, sceUsbStorageUnregisterCallback, sceUsbStorageRegisterCallbackForMapAvailable, sceUsbStorageUnregisterCallbackForMapAvailable, sceUsbStorageRegisterCallbackForMapUnavailable, sceUsbStorageUnregisterCallbackForMapUnavailable](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - what a successful registration stores, what unregistering a never-registered callback answers, and when any callback fires is unverified: no device is ever attached, and [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/usbd/usbstorage.cpp#L110-L174) logs all six as `(STUBBED)` and returns 0 without storing anything, the success-without-a-reference pattern #6288 was closed for. The argument validation is implemented (0x80f40002 for a null callback, an event type outside 1 and 2, or a null directory); past it they keep the marker until a reference or a measurement says what success does. -- [sceUsbStorageRequestUnmap, sceUsbStorageGetdentsClose](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - what the console answers for a directory that was never mapped and a getdents descriptor that was never opened is unknown: nothing can be mapped or opened in this model, shadPS4's [unmap](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/usbd/usbstorage.cpp#L281-L288) only logs without keeping state and its [close](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/usbd/usbstorage.cpp#L378-L383) succeeds only because `erase` ignores a missing key, and the no-device libraries throw on this path (`sceMoveClose` for a handle that was never opened, `sceContentSearchCloseMetadata` because no metadata id is ever handed out). Both keep the marker after their argument validation. +- [sceUsbStorageRegisterCallback, sceUsbStorageUnregisterCallback](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - the answers for a registration before `sceUsbStorageInit`, a second registration of the same callback for the same event type and the unregistration of a callback that is not registered are unknown, so the first two throw and the third keeps the marker. Whether the console raises mount events for devices attached before the registration is unknown; none are raised. A registration keeps its user data and receives [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Platform/UsbStorage.cs#L186-L199)'s 16-byte event (1 for a mount and 2 for an unmount, the device id, the device flags) for its event type only, on a service thread that rescans the host once a second instead of the automounter's IPC thread. Closes when: measured on a PS5 +- [sceUsbStorageRegisterCallbackForMapAvailable, sceUsbStorageUnregisterCallbackForMapAvailable, sceUsbStorageRegisterCallbackForMapUnavailable, sceUsbStorageUnregisterCallbackForMapUnavailable](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - what a successful registration stores and when the callbacks fire is unverified: [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/usbd/usbstorage.cpp#L128-L174) logs all four as `(STUBBED)` and returns 0 without storing anything, the success-without-a-reference pattern #6288 was closed for. The argument validation is implemented (0x80f40002 for a null callback or a null directory); past it they keep the marker until a reference or a measurement says what success does. +- [sceUsbStorageRequestUnmap, sceUsbStorageGetdentsClose](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - what the console answers for a mount point that is not mapped and a getdents descriptor that is not open is unknown: shadPS4's [unmap](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/usbd/usbstorage.cpp#L281-L288) only logs without keeping state and its [close](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/usbd/usbstorage.cpp#L378-L383) succeeds only because `erase` ignores a missing key. Both keep the marker on that path, after their argument validation; unmapping a mount point a map request returned, as [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Platform/UsbStorage.cs#L386-L398) does, and closing an open scan succeed. - [sceUsbStorageClearFakeWriteProtection, sceUsbStorageSetFakeMapLockForDebug, sceUsbStorageSetFakeWriteProtection](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceUsbStorage.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim. -- [sceUsbStorageGetMountPointOfShellCore](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - the prototype is known, but no reference says what a console without a mapped device writes into the mount-point buffer or returns; it keeps the marker until a reference or a console measurement answers it. +- [sceUsbStorageGetMountPointOfShellCore](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - no reference says what the console writes or returns for a device that is not attached or a null buffer; both keep the marker. An attached device writes its mount point, `/mnt/usb` after [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Platform/UsbStorage.cs#L22-L25)'s `/mnt/usb0`. +- [sceUsbStorageInit](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - the parameter is the attribute of the service's own thread ([SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Platform/UsbStorage.cs#L241-L242)), but its type is unresolved, so a non-null attribute throws. Initializing again while initialized succeeds without a rescan; that answer is not measured. ### Functional -- [the argument validation](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - the null-pointer, null-callback, event-type and null-directory refusals with 0x80f40002, and the getdents read of a never-opened descriptor answering 0x80f40005 with zero bytes, follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/usbd/usbstorage.cpp#L176-L383); the validation runs before the markers the register, unregister, unmap and getdents-close exports keep above. None of this is measured on a console. -- [the no-device answers](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - AnyPS5 has no USB mass-storage passthrough, so no device is ever attached: the device list is empty, `sceUsbStorageIsExist` succeeds and reports non-existence for every device id and directory, and the device-info query and the map and getdents-open requests answer 0x80f40004 for every device id. A passthrough that lists devices follows shadPS4's virtual-device model at the same commit for the filled paths, including its packed dirent. -- [the device-info record](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - the 0x338-byte record a passthrough fills for `sceUsbStorageGetDeviceInfo` is unverified on hardware; its layout follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Platform/UsbStorage.cs#L34-L45), which recovered it from the module's own export and was checked against #6288's "volume size at 0x0": the u64 volume size at offset 0, the vendor, product and serial strings at 0x16, 0x120 and 0x228, and the flags field the service masks with 0xd0 at 0x330. shadPS4's record starts with a u32 device id at offset 0; filling it that way puts a device id where a title reads the volume size. +- [the host passthrough](../../../core/libs/prx/libSceUsbStorage/src/HostVolumes.cpp) (libSceUsbStorage) - the devices are the FAT and exFAT volumes the host mounted from USB mass-storage devices: on Linux the whole-file-system `vfat`, `msdos` and `exfat` entries of `/proc/self/mountinfo` whose block device has a USB interface of class 08 among its sysfs ancestors, on [Windows](../../../core/libs/prx/libSceUsbStorage/src/HostVolumesWindows.cpp) the drive letters whose FAT, FAT32 or exFAT volume reports `BusTypeUsb`, and none on macOS. NTFS and a FUSE exFAT (`fuseblk`) are not listed. `sceUsbStorageInit` scans the host and a service thread rescans it once a second; a volume keeps its id, the lowest free of eight slots, while it stays mounted, and `/mnt/usb` resolves to it until a scan drops it or `sceUsbStorageTerm`, with or without a map request. How the console numbers its devices is unknown. Closes when: measured on a PS5 +- [the device-info record](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - the 0x338-byte record is unverified on hardware; its layout follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Platform/UsbStorage.cs#L34-L45), which recovered it from the module's own export: the u64 volume size at offset 0, the vendor, product and serial strings at 0x16, 0x120 and 0x228, and the flags the service masks with 0xd0 at 0x330. The vendor id, product id and release at 0x10, 0x12 and 0x14, the string lengths at 0x118, 0x220 and 0x328 and flag bits 4 (read-only), 6 (FAT) and 7 (exFAT) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/usbd/usbstorage.cpp#L28-L55), whose record otherwise starts with a device id where a title reads the volume size. Closes when: measured on a PS5 +- [the device-info values](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - which strings and which size the console reports is unknown. The volume size is the capacity of the mounted file system, not of the partition; the vendor and product are the SCSI inquiry strings (sysfs `vendor` and `model`, or the storage device descriptor on Windows) without trailing spaces, and the serial number is the USB one, empty when the device has none. A string longer than 255 bytes throws. Closes when: measured on a PS5 +- [the map requests](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - `sceUsbStorageRequestMap` and `sceUsbStorageRequestMapWSB` follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/usbd/usbstorage.cpp#L245-L279): the mount point is `/mnt/usb` followed by the requested directory (the device root for a null or empty one), the last argument receives 0 and null outputs are skipped. The directory must exist on the device; a missing one, a `.` or `..` component, a backslash, a mount point longer than 127 bytes, a second map of the same directory, nonzero flags or volume size, and a nonzero fourth `RequestMapWSB` argument throw, as their answers are unknown. shadPS4 returns a directory that starts with `/mnt/usb` unchanged; here it is a directory of the device like any other. The mapping limit SharpProspero names (0x80f40015) is not enforced. Closes when: measured on a PS5 +- [sceUsbStorageIsExist](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - it reports whether the device is attached and ignores the directory, as shadPS4's [one-argument version](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/usbd/usbstorage.cpp#L237-L243) has none; what the console does with the directory or a null output, and whether the bool result reports success or failure, is unknown; a null output throws. Closes when: measured on a PS5 +- [the getdents scan](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - `sceUsbStorageGetdentsOpen` lists the root of the device into descriptors numbered from 1 with a 0x8000-byte buffer size, skipping names that start with a dot, and `sceUsbStorageGetdentsRead` writes shadPS4's [packed dirent](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/usbd/usbstorage.cpp#L290-L376) (u32 file number from 1, u16 record length rounded to 8, type 4 for a directory and 8 otherwise, u8 name length, NUL-terminated name) until the buffer is full and zero bytes at the end. A name longer than 255 bytes, a host listing error and a buffer too small for the next record throw where shadPS4 truncates, ignores or answers zero bytes. None of this is measured on a console. Closes when: measured on a PS5 +- [the argument validation](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - the null-pointer, null-callback, event-type and null-directory refusals with 0x80f40002, the answers 0x80f40004 for a device that is not attached, and the getdents read of a descriptor that is not open answering 0x80f40005 with zero bytes, follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/usbd/usbstorage.cpp#L176-L383); the validation runs before the markers the exports keep above. None of this is measured on a console. - [the prototypes of sceUsbStorageRequestMap and sceUsbStorageGetDeviceList](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - the signatures follow the ABI restored on `main` (the firmware-database sweep of 2026-10-21), which matches [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/usbd/usbstorage.cpp#L176-L383): `RequestMap`'s six parameters, the typed `UsbStorageCallback` of the register/unregister family, `IsExist`'s three-argument bool form and `GetDeviceList`'s `std::int32_t*` count (the count also follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Platform/UsbStorage.cs#L233-L235), whose earlier eight-parameter `RequestMap` reading carried the trailing reserved pair the restored ABI drops). From 42a02f54f010397a3c63765ecf8590f4532ebbab Mon Sep 17 00:00:00 2001 From: harisibrar1 <207127467+harisibrar1@users.noreply.github.com> Date: Sun, 25 Oct 2026 10:58:50 -0400 Subject: [PATCH 054/198] feat(libSceLibreSsl): report ASN1_STRING_to_UTF8 errors through the ASN.1 error queue as LibreSSL 2.5.4 does Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceLibreSsl/src/Asn1.cpp | 34 +++++++++--- core/libs/prx/libSceLibreSsl/tests/Asn1.cpp | 58 ++++++++++++++++++++- docs/dev/debt/libSceLibreSsl.md | 2 +- 3 files changed, 86 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceLibreSsl/src/Asn1.cpp b/core/libs/prx/libSceLibreSsl/src/Asn1.cpp index 304ad8ad93..af75b30fc6 100644 --- a/core/libs/prx/libSceLibreSsl/src/Asn1.cpp +++ b/core/libs/prx/libSceLibreSsl/src/Asn1.cpp @@ -1,11 +1,13 @@ #include "prx/libSceLibreSsl/libSceLibreSsl.hpp" #include "prx/libc/include/Api.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libSceLibreSsl/include/Error.hpp" #include #include #include #include #include +#include #include namespace { @@ -14,6 +16,19 @@ static_assert(sizeof(Asn1String) == 24); static_assert(offsetof(Asn1String, data) == 8); static_assert(offsetof(Asn1String, flags) == 16); +constexpr int Asn1Library = 13; +constexpr int AnyFunction = 0xfff; +constexpr int MallocFailure = 65; +constexpr int IllegalCharacters = 124; +constexpr int InvalidBmpStringLength = 129; +constexpr int InvalidUniversalStringLength = 133; +constexpr int InvalidUtf8String = 134; + +int Asn1Error(int reason) { + LibreSslPutError_nid_no_patch(Asn1Library, AnyFunction, reason); + return -1; +} + int CharacterWidth(int type) { switch (type) { case 12: return 0; @@ -55,7 +70,7 @@ bool ReadCharacter(std::span& input, int width, std::uint32 void AppendCharacter(std::vector& output, std::uint32_t value) { const int bytes = value < 0x80 ? 1 : value < 0x800 ? 2 : value < 0x10000 ? 3 : 4; if (output.size() >= static_cast(std::numeric_limits::max() - bytes)) - NotImplemented_nid_no_patch("ASN1_STRING_to_UTF8: output length overflow"); + throw std::length_error("ASN1_STRING_to_UTF8: output length overflow"); if (bytes == 1) { output.push_back(static_cast(value)); return; @@ -106,19 +121,26 @@ int APS5_VABI ASN1_STRING_to_UTF8_nid_postfix(unsigned char** output, Asn1String const auto& string = *value; const int width = CharacterWidth(string.type); if (width < 0) return -1; - if (!output || (!string.data && string.length != 0)) NotImplemented_nid_no_patch(__func__); - std::size_t length = string.length < 0 ? std::strlen(reinterpret_cast(string.data)) + if (!string.data && string.length < 0) throw std::invalid_argument("ASN1_STRING_to_UTF8: null terminated-string data"); + const std::size_t length = string.length < 0 ? std::strlen(reinterpret_cast(string.data)) : static_cast(string.length); - if (length > static_cast(std::numeric_limits::max())) NotImplemented_nid_no_patch(__func__); + if (length > static_cast(std::numeric_limits::max())) throw std::length_error("ASN1_STRING_to_UTF8: input length overflow"); + if (width == 2 && length % 2 != 0) return Asn1Error(InvalidBmpStringLength); + if (width == 4 && length % 4 != 0) return Asn1Error(InvalidUniversalStringLength); + if (!string.data && length != 0) throw std::invalid_argument("ASN1_STRING_to_UTF8: null nonempty string data"); std::span input(string.data, length); std::vector converted; while (!input.empty()) { std::uint32_t character; - if (!ReadCharacter(input, width, character)) NotImplemented_nid_no_patch(__func__); + if (!ReadCharacter(input, width, character)) return Asn1Error(width == 0 ? InvalidUtf8String : IllegalCharacters); AppendCharacter(converted, character); } + if (!output) throw std::invalid_argument("ASN1_STRING_to_UTF8: null output"); auto* result = static_cast(malloc_nid_postfix(converted.size() + 1)); - if (!result) NotImplemented_nid_no_patch(__func__); + if (!result) { + if (width == 0) Asn1Error(MallocFailure); + return Asn1Error(MallocFailure); + } if (!converted.empty()) std::memcpy(result, converted.data(), converted.size()); result[converted.size()] = 0; *output = result; diff --git a/core/libs/prx/libSceLibreSsl/tests/Asn1.cpp b/core/libs/prx/libSceLibreSsl/tests/Asn1.cpp index 354f36a0c9..b2c6b3fb53 100644 --- a/core/libs/prx/libSceLibreSsl/tests/Asn1.cpp +++ b/core/libs/prx/libSceLibreSsl/tests/Asn1.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -53,6 +54,45 @@ void CheckConversion(int type, std::span input, std::span(reason); +} + +void RequireConversionError(unsigned char** output, int type, const unsigned char* data, int length, int reason, const char* message) { + ERR_clear_error_nid_postfix(); + Asn1String value{length, type, const_cast(data), 0}; + const unsigned char* before = output ? *output : nullptr; + Require(ASN1_STRING_to_UTF8_nid_postfix(output, &value) == -1, message); + Require(!output || *output == before, "a failed conversion leaves the output alone"); + Require(ERR_get_error_nid_postfix() == Asn1Error(reason) && ERR_get_error_nid_postfix() == 0, message); +} + +void MalformedStrings() { + unsigned char sentinel = 0xab; + unsigned char* result = &sentinel; + constexpr unsigned char oddBmp[] = {0, 0x41, 0}; + RequireConversionError(&result, 30, oddBmp, 3, 129, "odd BMPString length"); + RequireConversionError(&result, 30, nullptr, 1, 129, "BMPString length is checked before the data"); + constexpr unsigned char bmpSurrogate[] = {0, 0x41, 0xd8, 0}; + RequireConversionError(&result, 30, bmpSurrogate, 4, 124, "BMPString surrogate"); + constexpr unsigned char shortUniversal[] = {0, 0, 0, 0x41, 0}; + RequireConversionError(&result, 28, shortUniversal, 5, 133, "UniversalString length not a multiple of four"); + RequireConversionError(&result, 28, nullptr, 2, 133, "UniversalString length is checked before the data"); + constexpr unsigned char universalBeyond[] = {0, 0x11, 0, 0}; + RequireConversionError(&result, 28, universalBeyond, 4, 124, "UniversalString above U+10FFFF"); + constexpr unsigned char universalSurrogate[] = {0, 0, 0xdc, 0}; + RequireConversionError(&result, 28, universalSurrogate, 4, 124, "UniversalString surrogate"); + constexpr unsigned char overlong[] = {0xc0, 0x80}; + constexpr unsigned char truncated[] = {'A', 0xe2, 0x82}; + constexpr unsigned char surrogate[] = {0xed, 0xa0, 0x80}; + constexpr unsigned char beyond[] = {0xf5, 0x80, 0x80, 0x80}; + constexpr unsigned char continuation[] = {0x80}; + for (const auto& [data, length] : {std::pair{overlong, 2}, std::pair{truncated, 3}, std::pair{surrogate, 3}, + std::pair{beyond, 4}, std::pair{continuation, 1}}) + RequireConversionError(&result, 12, data, length, 134, "malformed UTF8String"); + RequireConversionError(nullptr, 30, oddBmp, 3, 129, "a malformed string fails before the output is used"); +} + void Accessors() { unsigned char data[] = {'a', 0, 'b'}; Asn1String value{3, 12, data, std::numeric_limits::max()}; @@ -175,12 +215,13 @@ void Strings() { std::size_t lastAllocationSize = 0; int allocationCount = 0; +bool failAllocation = false; int freeCount = 0; void* APS5_VABI Allocate_nid_no_patch(std::size_t size) { lastAllocationSize = size; ++allocationCount; - return std::malloc(size); + return failAllocation ? nullptr : std::malloc(size); } void APS5_VABI Free_nid_no_patch(void* pointer) { @@ -204,6 +245,20 @@ void Allocator() { constexpr unsigned char expected[] = {0xc3, 0xbf}; CheckConversion(20, input, expected); Require(allocationCount == 1 && lastAllocationSize == 3 && freeCount == 1, "conversion uses the application allocator"); + failAllocation = true; + unsigned char sentinel = 0xab; + unsigned char* result = &sentinel; + unsigned char latin[] = {0xff}; + Asn1String converted{1, 20, latin, 0}; + ERR_clear_error_nid_postfix(); + Require(ASN1_STRING_to_UTF8_nid_postfix(&result, &converted) == -1 && result == &sentinel, "allocation failure of a converted string"); + Require(ERR_get_error_nid_postfix() == Asn1Error(65) && ERR_get_error_nid_postfix() == 0, "one malloc failure for a converted string"); + unsigned char utf8[] = {'A'}; + Asn1String copied{1, 12, utf8, 0}; + Require(ASN1_STRING_to_UTF8_nid_postfix(&result, &copied) == -1 && result == &sentinel, "allocation failure of a UTF8String copy"); + Require(ERR_get_error_nid_postfix() == Asn1Error(65) && ERR_get_error_nid_postfix() == Asn1Error(65) && ERR_get_error_nid_postfix() == 0, + "ASN1_STRING_set and ASN1_mbstring_ncopy both report the malloc failure of a UTF8String copy"); + failAllocation = false; } } // namespace @@ -218,4 +273,5 @@ int main(int argc, char**) { Accessors(); Integers(); Strings(); + MalformedStrings(); } diff --git a/docs/dev/debt/libSceLibreSsl.md b/docs/dev/debt/libSceLibreSsl.md index a9e5271b10..b9ceef9acd 100644 --- a/docs/dev/debt/libSceLibreSsl.md +++ b/docs/dev/debt/libSceLibreSsl.md @@ -16,4 +16,4 @@ - [ASN1_INTEGER_get](../../../core/libs/prx/libSceLibreSsl/src/Asn1.cpp) has no console verification for nonpositive lengths. It follows [LibreSSL 2.5.4](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/asn1/a_int.c#L363-L389): valid integer types return zero without reading data when length is zero or negative; invalid types return -1 first. Later LibreSSL versions reject negative lengths through `ASN1_INTEGER_get_int64`; that later contract is not used for this library. -- [ASN1_INTEGER_get and ASN1_STRING_data/length/type/to_UTF8](../../../core/libs/prx/libSceLibreSsl/src/Asn1.cpp) are unverified on PS5. Signatures, the 24-byte guest string layout and conversions follow [LibreSSL 2.5.4's ASN.1 sources](https://github.com/libressl/openbsd/tree/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/asn1), the version reported for firmware 13.40 in [discussion #8981](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214). Null accessor inputs, null conversion outputs, missing nonempty data, oversized converted output and unrepresentable signed integer values throw where the source has undefined behaviour. Malformed encodings and allocation failure throw until ASN.1 error-queue reporting is implemented. Conversion buffers use the guest libc allocator and can be released with guest `free`. +- [ASN1_INTEGER_get and ASN1_STRING_data/length/type/to_UTF8](../../../core/libs/prx/libSceLibreSsl/src/Asn1.cpp) are unverified on PS5. Signatures, the 24-byte guest string layout and conversions follow [LibreSSL 2.5.4's ASN.1 sources](https://github.com/libressl/openbsd/tree/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/asn1), the version reported for firmware 13.40 in [discussion #8981](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214). Null accessor inputs, null conversion outputs, missing nonempty data, oversized converted output and unrepresentable signed integer values throw where the source has undefined behaviour. `ASN1_STRING_to_UTF8` reports malformed input and allocation failure as 2.5.4's `ASN1_mbstring_ncopy` and `ASN1_STRING_set` do: it returns -1, leaves the output alone and queues the ASN.1 error (`INVALID_BMPSTRING_LENGTH` 129 or `INVALID_UNIVERSALSTRING_LENGTH` 133 before the data is read, `INVALID_UTF8STRING` 134, `ILLEGAL_CHARACTERS` 124 for a surrogate or a value above U+10FFFF, and `ERR_R_MALLOC_FAILURE` 65, twice for a UTF8String copy), so a null output only throws once the conversion has succeeded. Conversion buffers use the guest libc allocator and can be released with guest `free`. From 30cc4ea2e6a74664bd085e281b8ccaf9717a514f Mon Sep 17 00:00:00 2001 From: Emanuele <118837721+imellag@users.noreply.github.com> Date: Sun, 25 Oct 2026 16:02:42 +0100 Subject: [PATCH 055/198] feat(libkernel): start the guest off the main thread on macOS AppKit, which SDL uses for windows, Metal surfaces, event pumping and game controllers, works only on the main thread, and a guest never hands its thread back. The macOS relinker emits a call to Aps5StartGuest as the entry of a relinked executable; no library provided it. - Aps5StartGuest runs the guest entry on a new thread with the stack size of the main thread (at least 8 MiB) and serves the main dispatch queue from the run loop of the main thread. The guest ends the process with exit; a guest entry that returns aborts. - Aps5RunOnMainThread runs a function on the main thread and waits for it, or in place where the main thread is the caller or Aps5StartGuest was never called. - libSceVideoOut sends its SDL calls through OnMainThread, which carries the exceptions back to the caller: the initialisation and shutdown of the video and game controller subsystems, the window, its Vulkan surface and drawable size, the event polling and the pad, mouse and keyboard input, which are created and destroyed there. On the other platforms the functions run in place. - The flip test starts its tests through Aps5StartGuest on macOS, as a relinked guest does. Co-authored-by: mugurc --- .../prx/libSceVideoOut/include/MainThread.hpp | 38 +++++ .../prx/libSceVideoOut/src/VideoOutDriver.cpp | 133 +++++++++++------- core/libs/prx/libSceVideoOut/tests/Flip.cpp | 18 ++- core/libs/prx/libkernel/CMakeLists.txt | 4 + .../libkernel/Pthread/src/MainThreadMacOs.cpp | 62 ++++++++ docs/dev/debt/libkernel.md | 1 + 6 files changed, 202 insertions(+), 54 deletions(-) create mode 100644 core/libs/prx/libSceVideoOut/include/MainThread.hpp create mode 100644 core/libs/prx/libkernel/Pthread/src/MainThreadMacOs.cpp diff --git a/core/libs/prx/libSceVideoOut/include/MainThread.hpp b/core/libs/prx/libSceVideoOut/include/MainThread.hpp new file mode 100644 index 0000000000..7250dd4394 --- /dev/null +++ b/core/libs/prx/libSceVideoOut/include/MainThread.hpp @@ -0,0 +1,38 @@ +#ifndef CORE_LIBS_PRX_LIBSCEVIDEOOUT_INCLUDE_MAINTHREAD_HPP +#define CORE_LIBS_PRX_LIBSCEVIDEOOUT_INCLUDE_MAINTHREAD_HPP + +#include +#include +#include +#include + +#ifdef __APPLE__ +extern "C" void Aps5RunOnMainThread_nid_no_patch(void (*function)(void*), void* context); +#endif + +template +std::invoke_result_t OnMainThread(Function&& function) { +#ifdef __APPLE__ + using Result = std::invoke_result_t; + struct Call { + std::remove_reference_t* function; + std::exception_ptr error; + std::conditional_t, bool, std::optional> result {}; + } call {&function, nullptr}; + Aps5RunOnMainThread_nid_no_patch([](void* context) { + auto& call = *static_cast(context); + try { + if constexpr (std::is_void_v) (*call.function)(); + else call.result.emplace((*call.function)()); + } catch (...) { + call.error = std::current_exception(); + } + }, &call); + if (call.error) std::rethrow_exception(call.error); + if constexpr (!std::is_void_v) return std::move(*call.result); +#else + return function(); +#endif +} + +#endif diff --git a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp index 377fb3548b..850de295b4 100644 --- a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp +++ b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp @@ -13,6 +13,7 @@ #include "SDL.h" #include "SDL_vulkan.h" +#include "prx/libSceVideoOut/include/MainThread.hpp" #include "prx/libSceVideoOut/include/PadInput.hpp" #include "prx/libSceVideoOut/include/MouseInput.hpp" #include "prx/libSceAgcDriver/Execution/include/AspectFit.hpp" @@ -64,31 +65,37 @@ std::optional configuredResolution() { } std::vector windowExtensions(SDL_Window* window) { - unsigned extensionCount = 0; - if (!SDL_Vulkan_GetInstanceExtensions(window, &extensionCount, nullptr)) throw std::runtime_error(std::string("SDL_Vulkan_GetInstanceExtensions failed: ") + SDL_GetError()); - std::vector extensions(extensionCount); - if (!SDL_Vulkan_GetInstanceExtensions(window, &extensionCount, extensions.data())) throw std::runtime_error(std::string("SDL_Vulkan_GetInstanceExtensions failed: ") + SDL_GetError()); - extensions.resize(extensionCount); - return extensions; + return OnMainThread([&] { + unsigned extensionCount = 0; + if (!SDL_Vulkan_GetInstanceExtensions(window, &extensionCount, nullptr)) throw std::runtime_error(std::string("SDL_Vulkan_GetInstanceExtensions failed: ") + SDL_GetError()); + std::vector extensions(extensionCount); + if (!SDL_Vulkan_GetInstanceExtensions(window, &extensionCount, extensions.data())) throw std::runtime_error(std::string("SDL_Vulkan_GetInstanceExtensions failed: ") + SDL_GetError()); + extensions.resize(extensionCount); + return extensions; + }); } VkSurfaceKHR createWindowSurface(void* context, VkInstance instance) { - VkSurfaceKHR surface = VK_NULL_HANDLE; - if (!SDL_Vulkan_CreateSurface(static_cast(context), instance, &surface)) throw std::runtime_error(std::string("SDL_Vulkan_CreateSurface failed: ") + SDL_GetError()); - return surface; + return OnMainThread([&] { + VkSurfaceKHR surface = VK_NULL_HANDLE; + if (!SDL_Vulkan_CreateSurface(static_cast(context), instance, &surface)) throw std::runtime_error(std::string("SDL_Vulkan_CreateSurface failed: ") + SDL_GetError()); + return surface; + }); } void windowDrawableSize(void* context, std::uint32_t* width, std::uint32_t* height) { - if ((SDL_GetWindowFlags(static_cast(context)) & SDL_WINDOW_MINIMIZED) != 0) { - *width = 0; - *height = 0; - return; - } - int drawableWidth = 0; - int drawableHeight = 0; - SDL_Vulkan_GetDrawableSize(static_cast(context), &drawableWidth, &drawableHeight); - *width = drawableWidth > 0 ? static_cast(drawableWidth) : 0; - *height = drawableHeight > 0 ? static_cast(drawableHeight) : 0; + OnMainThread([&] { + if ((SDL_GetWindowFlags(static_cast(context)) & SDL_WINDOW_MINIMIZED) != 0) { + *width = 0; + *height = 0; + return; + } + int drawableWidth = 0; + int drawableHeight = 0; + SDL_Vulkan_GetDrawableSize(static_cast(context), &drawableWidth, &drawableHeight); + *width = drawableWidth > 0 ? static_cast(drawableWidth) : 0; + *height = drawableHeight > 0 ? static_cast(drawableHeight) : 0; + }); } void checkConfig(const VideoOutConfig& cfg) { @@ -291,17 +298,19 @@ VideoOutDriver& VideoOutDriver::Get() { } VideoOutDriver::VideoOutDriver() { - SDL_SetHint(SDL_HINT_WINDOWS_DPI_AWARENESS, "permonitorv2"); + OnMainThread([] { + SDL_SetHint(SDL_HINT_WINDOWS_DPI_AWARENESS, "permonitorv2"); #ifdef _WIN32 - SDL_SetHintWithPriority(SDL_HINT_DIRECTINPUT_ENABLED, "0", SDL_HINT_DEFAULT); - if (std::getenv("APS5_NO_WGI") != nullptr) SDL_SetHintWithPriority(SDL_HINT_JOYSTICK_WGI, "0", SDL_HINT_DEFAULT); + SDL_SetHintWithPriority(SDL_HINT_DIRECTINPUT_ENABLED, "0", SDL_HINT_DEFAULT); + if (std::getenv("APS5_NO_WGI") != nullptr) SDL_SetHintWithPriority(SDL_HINT_JOYSTICK_WGI, "0", SDL_HINT_DEFAULT); #endif #ifdef __linux__ - SDL_SetHint(SDL_HINT_VIDEODRIVER, "wayland,x11"); + SDL_SetHint(SDL_HINT_VIDEODRIVER, "wayland,x11"); #endif - if (SDL_InitSubSystem(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER) < 0) { - throw std::runtime_error(std::string("SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: ") + SDL_GetError()); - } + if (SDL_InitSubSystem(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER) < 0) { + throw std::runtime_error(std::string("SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: ") + SDL_GetError()); + } + }); try { AgcDriverWaitIdle_nid_postfix(); std::promise started; @@ -329,7 +338,7 @@ VideoOutDriver::VideoOutDriver() { flipQueue->changed.NotifyAll(); presentThread.join(); } - SDL_QuitSubSystem(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER); + OnMainThread([] { SDL_QuitSubSystem(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER); }); throw; } } @@ -358,7 +367,7 @@ void VideoOutDriver::Shutdown() { if (outputs[handle]) close(handle); } } - SDL_QuitSubSystem(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER); + OnMainThread([] { SDL_QuitSubSystem(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER); }); stopped = true; std::lock_guard lock(flipQueue->mutex); if (flipQueue->failure) std::rethrow_exception(flipQueue->failure); @@ -598,7 +607,7 @@ void VideoOutDriver::processFlip(FlipRequest& req) { req.gpuComplete = true; } } else { - window.Ensure(req.width, req.height); + OnMainThread([&] { window.Ensure(req.width, req.height); }); const auto extensions = windowExtensions(window.Handle()); const AgcDriver::PresentationWindow target{window.Handle(), extensions, &createWindowSurface, &windowDrawableSize, req.width, req.height, req.timing}; timing.Mark("window_prepare"); @@ -618,7 +627,7 @@ void VideoOutDriver::processFlip(FlipRequest& req) { AgcDriverPresentClear_nid_postfix(target, req.unregistered || req.index == VIDEO_OUT_BUFFER_INDEX_BLACK, gpuReady, &req); } timing.Mark("present"); - window.UpdateTitle(); + OnMainThread([&] { window.UpdateTitle(); }); timing.Mark("window_title"); } std::lock_guard lock(req.cfg->mutex); @@ -658,20 +667,26 @@ void VideoOutDriver::processFlip(FlipRequest& req) { void VideoOutDriver::presentLoop(std::stop_token token, std::promise& started) { try { - window.Ensure(VIDEO_OUT_DEFAULT_WIDTH, VIDEO_OUT_DEFAULT_HEIGHT); - updateDisplayRefreshRate(); + OnMainThread([&] { + window.Ensure(VIDEO_OUT_DEFAULT_WIDTH, VIDEO_OUT_DEFAULT_HEIGHT); + updateDisplayRefreshRate(); + }); hdrProbe.Poll(window.Handle()); const auto extensions = windowExtensions(window.Handle()); AgcDriverAttachWindow_nid_postfix({window.Handle(), extensions, &createWindowSurface, &windowDrawableSize, VIDEO_OUT_DEFAULT_WIDTH, VIDEO_OUT_DEFAULT_HEIGHT, nullptr}); hdrGeneration = AgcDriver::CurrentHostHdr().generation; } catch (...) { hdrProbe.Release(); - window.Destroy(); + OnMainThread([&] { window.Destroy(); }); started.set_exception(std::current_exception()); return; } started.set_value(); std::shared_ptr current; + std::optional padInput; + std::optional mouseInput; + std::optional keyboardInput; + std::optional imeTextInput; #if APS5_ENABLE_TIMING_LOG std::unique_ptr timingLog; #endif @@ -679,10 +694,12 @@ void VideoOutDriver::presentLoop(std::stop_token token, std::promise& star #if APS5_ENABLE_TIMING_LOG timingLog = std::make_unique(); #endif - PadInput padInput; - MouseInput mouseInput; - KeyboardInput keyboardInput; - ImeTextInput imeTextInput; + OnMainThread([&] { + padInput.emplace(); + mouseInput.emplace(); + keyboardInput.emplace(); + imeTextInput.emplace(); + }); while (!token.stop_requested()) { { std::unique_lock lock(flipQueue->mutex); @@ -695,23 +712,27 @@ void VideoOutDriver::presentLoop(std::stop_token token, std::promise& star } } - SDL_Event event; - while (SDL_PollEvent(&event)) { - if (event.type == SDL_QUIT) { - LibcRequestExit_nid_postfix(0); - throw ProcessShutdown{}; - } - padInput.HandleEvent(event, window); - if (window.Handle() != nullptr) { - mouseInput.HandleEvent(event, SDL_GetWindowID(window.Handle())); - keyboardInput.HandleEvent(event, SDL_GetWindowID(window.Handle())); - imeTextInput.HandleEvent(event, SDL_GetWindowID(window.Handle())); + const bool quit = OnMainThread([&] { + SDL_Event event; + while (SDL_PollEvent(&event)) { + if (event.type == SDL_QUIT) return true; + padInput->HandleEvent(event, window); + if (window.Handle() != nullptr) { + mouseInput->HandleEvent(event, SDL_GetWindowID(window.Handle())); + keyboardInput->HandleEvent(event, SDL_GetWindowID(window.Handle())); + imeTextInput->HandleEvent(event, SDL_GetWindowID(window.Handle())); + } } + padInput->Update(window); + updateDisplayRefreshRate(); + return false; + }); + if (quit) { + LibcRequestExit_nid_postfix(0); + throw ProcessShutdown{}; } - padInput.Update(window); - updateDisplayRefreshRate(); pollHostHdr(); - mouseInput.UpdateCapture(!padInput.MouseModeEnabled()); + OnMainThread([&] { mouseInput->UpdateCapture(!padInput->MouseModeEnabled()); }); if (current) { require(current->timing != nullptr, "missing presentation timing"); const auto dequeued = AgcDriver::FrameTiming::Clock::now(); @@ -722,7 +743,7 @@ void VideoOutDriver::presentLoop(std::stop_token token, std::promise& star if (window.Handle() != nullptr) SDL_GetWindowSize(window.Handle(), &windowWidth, &windowHeight); if (windowWidth > 0 && windowHeight > 0) { const auto presented = AgcDriver::ComputeContainSize_nid_postfix(current->width, current->height, static_cast(windowWidth), static_cast(windowHeight), true); - mouseInput.SetPresentation(current->width, current->height, presented.width, presented.height); + mouseInput->SetPresentation(current->width, current->height, presented.width, presented.height); } const auto finished = AgcDriver::FrameTiming::Clock::now(); AgcDriver::FrameTiming::Clock::duration interval{}; @@ -775,7 +796,13 @@ void VideoOutDriver::presentLoop(std::stop_token token, std::promise& star if (!flipQueue->failure) flipQueue->failure = std::current_exception(); } hdrProbe.Release(); - window.Destroy(); + OnMainThread([&] { + padInput.reset(); + mouseInput.reset(); + keyboardInput.reset(); + imeTextInput.reset(); + window.Destroy(); + }); } void VideoOutDriver::vblankLoop(std::stop_token token) { diff --git a/core/libs/prx/libSceVideoOut/tests/Flip.cpp b/core/libs/prx/libSceVideoOut/tests/Flip.cpp index 877e19e1fa..776d434282 100644 --- a/core/libs/prx/libSceVideoOut/tests/Flip.cpp +++ b/core/libs/prx/libSceVideoOut/tests/Flip.cpp @@ -1095,7 +1095,11 @@ void testOutputResolutionOverride() { } -int main(int argc, char** argv) { +#ifdef __APPLE__ +extern "C" [[noreturn]] void Aps5StartGuest_nid_no_patch(void (*entry)(void*, void*), void* block); +#endif + +int runTests(int argc, char** argv) { VideoOutTestEnvironment environment; try { if (argc == 2 && std::string(argv[1]) == "decode") testDecode(); @@ -1136,3 +1140,15 @@ int main(int argc, char** argv) { return 1; } } + +int main(int argc, char** argv) { +#ifdef __APPLE__ + static int arguments; + static char** values; + arguments = argc; + values = argv; + Aps5StartGuest_nid_no_patch([](void*, void*) { std::exit(runTests(arguments, values)); }, nullptr); +#else + return runTests(argc, argv); +#endif +} diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 9da2728d50..c44de01a2f 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -35,6 +35,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/src/Tsd.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/src/Rwlock.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/src/Sem.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/src/MainThreadMacOs.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Atfork.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Attr.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Barrier.cpp @@ -84,6 +85,9 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/AppMetadata/src/ParamJsonParser.cpp ${CMAKE_CURRENT_SOURCE_DIR}/AppMetadata/src/ParamSfoParser.cpp ) +if(APPLE) + target_link_libraries(libkernel PRIVATE "-framework CoreFoundation") +endif() target_include_directories(libkernel PRIVATE ${LIBS_INCLUDE_DIR}) target_include_directories(libkernel PRIVATE ${LIBS_INCLUDE_DIR}/nid/include) diff --git a/core/libs/prx/libkernel/Pthread/src/MainThreadMacOs.cpp b/core/libs/prx/libkernel/Pthread/src/MainThreadMacOs.cpp new file mode 100644 index 0000000000..52543e433f --- /dev/null +++ b/core/libs/prx/libkernel/Pthread/src/MainThreadMacOs.cpp @@ -0,0 +1,62 @@ +#ifdef __APPLE__ + +#include +#include +#include +#include +#include +#include +#include + +namespace { + +struct GuestStart { + void (*entry)(void*, void*); + void* block; +}; + +std::atomic mainLoopRunning {false}; + +void* RunGuest(void* argument) { + const auto start = *static_cast(argument); + start.entry(start.block, nullptr); + std::fprintf(stderr, "guest entry returned\n"); + std::abort(); +} + +void KeepAlive(CFRunLoopTimerRef, void*) {} + +} + +extern "C" { + +[[noreturn]] void Aps5StartGuest_nid_no_patch(void (*entry)(void*, void*), void* block) { + static GuestStart start; + start = {entry, block}; + pthread_attr_t attributes; + if (pthread_attr_init(&attributes) != 0) std::abort(); + const auto stackSize = std::max(pthread_get_stacksize_np(pthread_self()), 8u << 20); + if (pthread_attr_setstacksize(&attributes, stackSize) != 0) std::abort(); + mainLoopRunning.store(true, std::memory_order_release); + pthread_t thread; + if (pthread_create(&thread, &attributes, RunGuest, &start) != 0) { + std::fprintf(stderr, "cannot start the guest thread\n"); + std::abort(); + } + pthread_attr_destroy(&attributes); + CFRunLoopTimerRef timer = CFRunLoopTimerCreate(nullptr, 1.0e10, 1.0e10, 0, 0, KeepAlive, nullptr); + CFRunLoopAddTimer(CFRunLoopGetMain(), timer, kCFRunLoopCommonModes); + for (;;) CFRunLoopRun(); +} + +void Aps5RunOnMainThread_nid_no_patch(void (*function)(void*), void* context) { + if (pthread_main_np() != 0 || !mainLoopRunning.load(std::memory_order_acquire)) { + function(context); + return; + } + dispatch_sync_f(dispatch_get_main_queue(), context, function); +} + +} + +#endif diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 4e1851fb4e..77cfde8993 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -182,6 +182,7 @@ - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) builds the handler's `ucontext` with FreeBSD's amd64 layout (16-byte sigmask, `mcontext` at 0x40, `mc_rsp` at 0xf8); the console's layout is not verified beyond the fields PS5Util reads - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) does not consult the target thread's signal mask - [pthread_sigmask](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) shares the process-wide mask of `sigprocmask` and `_sigprocmask`; FreeBSD keeps a mask per thread +- On macOS the guest runs on a second thread and the main thread serves the main dispatch queue from its run loop: AppKit (SDL's Cocoa windows, Metal surfaces, event pumping and game controllers) only works on the main thread, and a guest never hands its thread back. The macOS entry stub of the relinker starts the guest through `Aps5StartGuest` ([MainThreadMacOs.cpp](../../../core/libs/prx/libkernel/Pthread/src/MainThreadMacOs.cpp)), on a thread with the stack size of the main thread (at least 8 MiB), and the guest ends the process with `exit`; a guest entry that returns aborts. libSceVideoOut sends its SDL calls to the main thread with `Aps5RunOnMainThread` (`OnMainThread` in [MainThread.hpp](../../../core/libs/prx/libSceVideoOut/include/MainThread.hpp)), which waits for them and rethrows their exceptions; a host program that never called `Aps5StartGuest` runs them in place - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) reaches a thread that waits in an alertable `TimedWait` wait through an APC. A thread blocked anywhere else, including all waits under `APS5_COARSE_TIMED_WAITS` and host waits inside the emulator, is redirected and runs the handler only when that wait returns - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) on Windows runs the handler wherever the target thread was interrupted. It waits only while the thread is inside winpthreads or restoring a context through `NtContinue`, and throws if that lasts 1 s, so a handler that calls into other host code the thread was interrupted in, such as the C runtime's heap or a lock of the emulator, can deadlock on it or reenter it. A thread that holds the address-wait lock of `sceKernelSyncOnAddressWait`, `sceKernelSyncOnAddressWake` and `_umtx_op`, or the park queue lock of the guest mutexes, gets the raise as an APC instead, which runs once it has released the lock or while it waits; the other locks of the emulator are not deferred this way - [sigaltstack](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records each thread's alternate signal stack and reports it back, following FreeBSD's `kern_sigaltstack` (`EINVAL` for flags other than `SS_DISABLE`, `ENOMEM` below `MINSIGSTKSZ` 2048), but handlers never run on it, so `SS_ONSTACK` and `EPERM` are never reported From 4de4eeff2e46d057c9a442046f6b2bbec213f773 Mon Sep 17 00:00:00 2001 From: simnJS Date: Sun, 25 Oct 2026 15:20:52 +0000 Subject: [PATCH 056/198] fix(libSceUsbStorage): keep mapped directories on the device volume A requested directory such as C:/Windows or C:Windows carries a root name on Windows, so appending it to the volume root replaced the root and the map checked a directory outside the device. Join the validated names one by one and refuse a name with a root path. The fake sysfs of the test no longer uses colons, so its device cases can run on Windows too. --- core/libs/prx/libSceUsbStorage/Export.cpp | 27 ++++++------ core/libs/tests/GuestUsbStorage.cpp | 50 +++++++++++++++-------- docs/dev/debt/libSceUsbStorage.md | 2 +- 3 files changed, 49 insertions(+), 30 deletions(-) diff --git a/core/libs/prx/libSceUsbStorage/Export.cpp b/core/libs/prx/libSceUsbStorage/Export.cpp index 563daef418..3f3cccc814 100644 --- a/core/libs/prx/libSceUsbStorage/Export.cpp +++ b/core/libs/prx/libSceUsbStorage/Export.cpp @@ -160,33 +160,36 @@ std::vector::iterator Registered(Service& service, std::int32_t ev [&](const auto& registration) { return registration.eventType == eventType && registration.callback == callback; }); } -std::string RelativeDirectory(const char* directory, const char* function) { - std::string relative; - if (directory == nullptr) return relative; +std::filesystem::path HostName(std::string_view name) { + return std::filesystem::path(std::u8string(name.begin(), name.end())); +} + +std::vector DirectoryNames(const char* directory, const char* function) { + std::vector names; + if (directory == nullptr) return names; std::string_view text(directory); if (text.find('\\') != std::string_view::npos) throw std::runtime_error(std::string(function) + ": a directory with a backslash is not implemented"); while (!text.empty()) { const auto separator = text.find('/'); const auto name = text.substr(0, separator); if (name == "." || name == "..") throw std::runtime_error(std::string(function) + ": a directory with . or .. components is not implemented"); - if (!name.empty()) { - if (!relative.empty()) relative += '/'; - relative += name; - } + if (HostName(name).has_root_path()) throw std::runtime_error(std::string(function) + ": a directory with a root name is not implemented"); + if (!name.empty()) names.emplace_back(name); if (separator == std::string_view::npos) break; text.remove_prefix(separator + 1); } - return relative; + return names; } std::string Map(Device& device, std::uint32_t slot, const char* directory, const char* function) { - const auto relative = RelativeDirectory(directory, function); auto host = device.volume.root; - if (!relative.empty()) host /= std::filesystem::path(std::u8string(relative.begin(), relative.end())); + auto path = MountPoint(slot); + for (const auto& name : DirectoryNames(directory, function)) { + host /= HostName(name); + path += "/" + name; + } std::error_code error; if (!std::filesystem::is_directory(host, error)) throw std::runtime_error(std::string(function) + ": mapping a directory the device does not hold is not implemented"); - auto path = MountPoint(slot); - if (!relative.empty()) path += "/" + relative; if (path.size() >= UsbStorage::MountPointSize) throw std::runtime_error(std::string(function) + ": a mount point longer than 127 bytes is not implemented"); if (std::find(device.maps.begin(), device.maps.end(), path) != device.maps.end()) { throw std::runtime_error(std::string(function) + ": mapping a mapped directory again is not implemented"); diff --git a/core/libs/tests/GuestUsbStorage.cpp b/core/libs/tests/GuestUsbStorage.cpp index 4ef5ccd7ee..6fa4023d64 100644 --- a/core/libs/tests/GuestUsbStorage.cpp +++ b/core/libs/tests/GuestUsbStorage.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -101,7 +102,13 @@ void WriteMounts(const FakeHost& host, const std::string& lines) { auto staged = host.mountInfo; staged += ".new"; WriteFile(staged, lines); - std::filesystem::rename(staged, host.mountInfo); + std::error_code error; + for (int attempt = 0; attempt < 500; ++attempt) { + std::filesystem::rename(staged, host.mountInfo, error); + if (!error) return; + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + Require(false, "replace the fake mountinfo"); } std::string Escaped(const std::filesystem::path& path) { @@ -120,11 +127,11 @@ std::string MountLine(std::string_view device, std::string_view root, const std: } std::string FirstLine(const FakeHost& host) { - return MountLine("8:17", "/", host.firstMount, "rw,nosuid,relatime", "vfat"); + return MountLine("8_17", "/", host.firstMount, "rw,nosuid,relatime", "vfat"); } std::string SecondLine(const FakeHost& host) { - return MountLine("8:33", "/", host.secondMount, "ro,relatime", "exfat"); + return MountLine("8_33", "/", host.secondMount, "ro,relatime", "exfat"); } FakeHost CreateHost() { @@ -148,29 +155,26 @@ bool Link(const FakeHost& host, std::string_view device, const std::filesystem:: } bool CreateDevices(const FakeHost& host) { - const auto probe = host.sys / "devices" / "probe:0"; - std::error_code error; - std::filesystem::create_directories(probe, error); - if (error || !Link(host, "0:0", probe)) return false; - const auto controller = host.sys / "devices" / "pci0000:00" / "0000:00:14.0" / "usb1"; + if (!Link(host, "0_0", host.sys / "devices" / "probe")) return false; + const auto controller = host.sys / "devices" / "pci0000_00" / "0000_00_14.0" / "usb1"; const auto first = controller / "1-1"; WriteFile(first / "idVendor", "0781\n"); WriteFile(first / "idProduct", "5581\n"); WriteFile(first / "bcdDevice", "0100\n"); WriteFile(first / "serial", "4C530001\n"); - WriteFile(first / "1-1:1.0" / "bInterfaceClass", "08\n"); - const auto firstScsi = first / "1-1:1.0" / "host6" / "target6:0:0" / "6:0:0:0"; + WriteFile(first / "1-1_1.0" / "bInterfaceClass", "08\n"); + const auto firstScsi = first / "1-1_1.0" / "host6" / "target6_0_0" / "6_0_0_0"; WriteFile(firstScsi / "vendor", "SanDisk \n"); WriteFile(firstScsi / "model", "Ultra Fit \n"); const auto second = controller / "1-2"; WriteFile(second / "idVendor", "090c\n"); WriteFile(second / "idProduct", "1000\n"); WriteFile(second / "bcdDevice", "1100\n"); - WriteFile(second / "1-2:1.0" / "bInterfaceClass", "08\n"); - const auto secondScsi = second / "1-2:1.0" / "host7" / "target7:0:0" / "7:0:0:0"; + WriteFile(second / "1-2_1.0" / "bInterfaceClass", "08\n"); + const auto secondScsi = second / "1-2_1.0" / "host7" / "target7_0_0" / "7_0_0_0"; WriteFile(secondScsi / "vendor", "Samsung \n"); WriteFile(secondScsi / "model", "Flash Drive \n"); - const auto sata = host.sys / "devices" / "pci0000:00" / "0000:00:17.0" / "ata1" / "host0" / "target0:0:0" / "0:0:0:0"; + const auto sata = host.sys / "devices" / "pci0000_00" / "0000_00_17.0" / "ata1" / "host0" / "target0_0_0" / "0_0_0_0"; WriteFile(sata / "vendor", "ATA \n"); WriteFile(sata / "model", "Disk \n"); WriteFile(host.firstMount / "song.txt", "song"); @@ -178,8 +182,8 @@ bool CreateDevices(const FakeHost& host) { std::filesystem::create_directories(host.firstMount / "Music"); WriteFile(host.secondMount / "Readme.txt", "readme"); WriteFile(host.root / "media" / "efi" / "boot.efi", "efi"); - return Link(host, "8:17", firstScsi / "block" / "sdb" / "sdb1") && Link(host, "8:33", secondScsi / "block" / "sdc" / "sdc1") && - Link(host, "8:1", sata / "block" / "sda" / "sda1") && Link(host, "8:49", firstScsi / "block" / "sdd" / "sdd1"); + return Link(host, "8_17", firstScsi / "block" / "sdb" / "sdb1") && Link(host, "8_33", secondScsi / "block" / "sdc" / "sdc1") && + Link(host, "8_1", sata / "block" / "sda" / "sda1") && Link(host, "8_49", firstScsi / "block" / "sdd" / "sdd1"); } void TestInitializeAndTerminate() { @@ -378,6 +382,18 @@ void TestDeviceMaps() { Require(ThrowsWith([&] { sceUsbStorageRequestMap(0, "/Music/../Music", 0, 0, mountPoint, nullptr); }, "sceUsbStorageRequestMap: a directory with . or .. components is not implemented"), "map a directory with a .. component did not throw"); +#ifdef _WIN32 + constexpr std::string_view otherDrive = "sceUsbStorageRequestMap: a directory with a root name is not implemented"; +#else + constexpr std::string_view otherDrive = "sceUsbStorageRequestMap: mapping a directory the device does not hold is not implemented"; +#endif + Require(ThrowsWith([&] { sceUsbStorageRequestMap(0, "C:/Windows", 0, 0, mountPoint, nullptr); }, otherDrive), + "map a directory of another drive did not throw"); + Require(ThrowsWith([&] { sceUsbStorageRequestMap(0, "/C:Windows", 0, 0, mountPoint, nullptr); }, otherDrive), + "map a drive-relative directory did not throw"); + Require(ThrowsWith([&] { sceUsbStorageRequestMap(0, "Music\\..", 0, 0, mountPoint, nullptr); }, + "sceUsbStorageRequestMap: a directory with a backslash is not implemented"), + "map a directory with a backslash did not throw"); Require(ThrowsWith([&] { sceUsbStorageRequestMap(0, "/", 1, 0, mountPoint, nullptr); }, "sceUsbStorageRequestMap: nonzero flags or a volume size are not implemented"), "map with flags did not throw"); @@ -429,8 +445,8 @@ void TestDeviceGetdents() { } void TestDevices(const FakeHost& host) { - WriteMounts(host, FirstLine(host) + MountLine("8:1", "/", host.root / "media" / "efi", "rw", "vfat") + - MountLine("8:49", "/", host.root / "media", "rw", "ext4") + MountLine("8:17", "/Music", host.root / "media" / "bound", "rw", "vfat")); + WriteMounts(host, FirstLine(host) + MountLine("8_1", "/", host.root / "media" / "efi", "rw", "vfat") + + MountLine("8_49", "/", host.root / "media", "rw", "ext4") + MountLine("8_17", "/Music", host.root / "media" / "bound", "rw", "vfat")); Require(sceUsbStorageInit(nullptr) == 0, "initialize with a device"); TestDeviceInfo(host); TestDeviceMaps(); diff --git a/docs/dev/debt/libSceUsbStorage.md b/docs/dev/debt/libSceUsbStorage.md index e3877eb181..585966d957 100644 --- a/docs/dev/debt/libSceUsbStorage.md +++ b/docs/dev/debt/libSceUsbStorage.md @@ -14,7 +14,7 @@ - [the host passthrough](../../../core/libs/prx/libSceUsbStorage/src/HostVolumes.cpp) (libSceUsbStorage) - the devices are the FAT and exFAT volumes the host mounted from USB mass-storage devices: on Linux the whole-file-system `vfat`, `msdos` and `exfat` entries of `/proc/self/mountinfo` whose block device has a USB interface of class 08 among its sysfs ancestors, on [Windows](../../../core/libs/prx/libSceUsbStorage/src/HostVolumesWindows.cpp) the drive letters whose FAT, FAT32 or exFAT volume reports `BusTypeUsb`, and none on macOS. NTFS and a FUSE exFAT (`fuseblk`) are not listed. `sceUsbStorageInit` scans the host and a service thread rescans it once a second; a volume keeps its id, the lowest free of eight slots, while it stays mounted, and `/mnt/usb` resolves to it until a scan drops it or `sceUsbStorageTerm`, with or without a map request. How the console numbers its devices is unknown. Closes when: measured on a PS5 - [the device-info record](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - the 0x338-byte record is unverified on hardware; its layout follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Platform/UsbStorage.cs#L34-L45), which recovered it from the module's own export: the u64 volume size at offset 0, the vendor, product and serial strings at 0x16, 0x120 and 0x228, and the flags the service masks with 0xd0 at 0x330. The vendor id, product id and release at 0x10, 0x12 and 0x14, the string lengths at 0x118, 0x220 and 0x328 and flag bits 4 (read-only), 6 (FAT) and 7 (exFAT) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/usbd/usbstorage.cpp#L28-L55), whose record otherwise starts with a device id where a title reads the volume size. Closes when: measured on a PS5 - [the device-info values](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - which strings and which size the console reports is unknown. The volume size is the capacity of the mounted file system, not of the partition; the vendor and product are the SCSI inquiry strings (sysfs `vendor` and `model`, or the storage device descriptor on Windows) without trailing spaces, and the serial number is the USB one, empty when the device has none. A string longer than 255 bytes throws. Closes when: measured on a PS5 -- [the map requests](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - `sceUsbStorageRequestMap` and `sceUsbStorageRequestMapWSB` follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/usbd/usbstorage.cpp#L245-L279): the mount point is `/mnt/usb` followed by the requested directory (the device root for a null or empty one), the last argument receives 0 and null outputs are skipped. The directory must exist on the device; a missing one, a `.` or `..` component, a backslash, a mount point longer than 127 bytes, a second map of the same directory, nonzero flags or volume size, and a nonzero fourth `RequestMapWSB` argument throw, as their answers are unknown. shadPS4 returns a directory that starts with `/mnt/usb` unchanged; here it is a directory of the device like any other. The mapping limit SharpProspero names (0x80f40015) is not enforced. Closes when: measured on a PS5 +- [the map requests](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - `sceUsbStorageRequestMap` and `sceUsbStorageRequestMapWSB` follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/usbd/usbstorage.cpp#L245-L279): the mount point is `/mnt/usb` followed by the requested directory (the device root for a null or empty one), the last argument receives 0 and null outputs are skipped. The directory must exist on the device; a missing one, a `.` or `..` component, a component with a root name (a drive letter on Windows), a backslash, a mount point longer than 127 bytes, a second map of the same directory, nonzero flags or volume size, and a nonzero fourth `RequestMapWSB` argument throw, as their answers are unknown. Unlike shadPS4, a directory that starts with `/mnt/usb` is not returned unchanged. The mapping limit SharpProspero names (0x80f40015) is not enforced. Closes when: measured on a PS5 - [sceUsbStorageIsExist](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - it reports whether the device is attached and ignores the directory, as shadPS4's [one-argument version](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/usbd/usbstorage.cpp#L237-L243) has none; what the console does with the directory or a null output, and whether the bool result reports success or failure, is unknown; a null output throws. Closes when: measured on a PS5 - [the getdents scan](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - `sceUsbStorageGetdentsOpen` lists the root of the device into descriptors numbered from 1 with a 0x8000-byte buffer size, skipping names that start with a dot, and `sceUsbStorageGetdentsRead` writes shadPS4's [packed dirent](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/usbd/usbstorage.cpp#L290-L376) (u32 file number from 1, u16 record length rounded to 8, type 4 for a directory and 8 otherwise, u8 name length, NUL-terminated name) until the buffer is full and zero bytes at the end. A name longer than 255 bytes, a host listing error and a buffer too small for the next record throw where shadPS4 truncates, ignores or answers zero bytes. None of this is measured on a console. Closes when: measured on a PS5 - [the argument validation](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - the null-pointer, null-callback, event-type and null-directory refusals with 0x80f40002, the answers 0x80f40004 for a device that is not attached, and the getdents read of a descriptor that is not open answering 0x80f40005 with zero bytes, follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/usbd/usbstorage.cpp#L176-L383); the validation runs before the markers the exports keep above. None of this is measured on a console. From 92d5f20c10a82ba4d9c315529dfda954b55678b3 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sun, 25 Oct 2026 15:15:38 +0000 Subject: [PATCH 057/198] feat(libSceNpWebApi2): validate and answer the multipart request calls --- core/libs/prx/libSceNpWebApi2/Export.cpp | 11 ++++++++--- core/libs/tests/GuestNpWebApi2.cpp | 18 ++++++++++++++++++ docs/dev/debt/libSceNpWebApi2.md | 2 +- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceNpWebApi2/Export.cpp b/core/libs/prx/libSceNpWebApi2/Export.cpp index 66a26fe70b..5fed1c3a46 100644 --- a/core/libs/prx/libSceNpWebApi2/Export.cpp +++ b/core/libs/prx/libSceNpWebApi2/Export.cpp @@ -235,7 +235,10 @@ int APS5_VABI sceNpWebApi2AddMultipartPart() { } int APS5_VABI sceNpWebApi2CreateMultipartRequest(int user_ctx_id, const char* api_group, const char* path, const char* method, int64_t* request_id) { - NotImplemented_nid_no_patch(__func__); + (void)user_ctx_id; + if (!api_group || !path || !method) return SCE_NP_WEBAPI2_ERROR_INVALID_ARGUMENT; + const int64_t created = g_nextHandle.fetch_add(1, std::memory_order_relaxed); + if (request_id) *request_id = created; return 0; } @@ -270,8 +273,10 @@ int APS5_VABI sceNpWebApi2IntPushEventCreateDestPs4CtxIndFilter() { } int APS5_VABI sceNpWebApi2SendMultipartRequest(int64_t request_id, int part_index, void* data, uint64_t data_size, void* resp_info_option) { - NotImplemented_nid_no_patch(__func__); - return 0; + (void)request_id; + (void)resp_info_option; + if (part_index <= 0 || !data || data_size == 0) return SCE_NP_WEBAPI2_ERROR_INVALID_ARGUMENT; + return SCE_NP_WEBAPI2_ERROR_UNAVAILABLE; } int APS5_VABI sceNpWebApi2SetMultipartContentType() { diff --git a/core/libs/tests/GuestNpWebApi2.cpp b/core/libs/tests/GuestNpWebApi2.cpp index 6144dccf18..97bbf76e08 100644 --- a/core/libs/tests/GuestNpWebApi2.cpp +++ b/core/libs/tests/GuestNpWebApi2.cpp @@ -66,6 +66,24 @@ int main() { Require(sceNpWebApi2IntInitialize2(&args2) == invalidArgument && sceNpWebApi2IntInitialize2(nullptr) == invalidArgument); const int presence = sceNpWebApi2InitializeForPresence(1, 0x2000); Require(presence > 0 && sceNpWebApi2GetMemoryPoolStats(presence, &stats) == 0 && stats.pool_size == 0x2000); + constexpr int unavailable = static_cast(0x80553406); + const int multipartUser = sceNpWebApi2CreateUserContext(presence, 1); + std::int64_t multipart = 0; + Require(sceNpWebApi2CreateMultipartRequest(multipartUser, nullptr, "/v1/files", "POST", &multipart) == invalidArgument); + Require(sceNpWebApi2CreateMultipartRequest(multipartUser, "fileManager", nullptr, "POST", &multipart) == invalidArgument); + Require(sceNpWebApi2CreateMultipartRequest(multipartUser, "fileManager", "/v1/files", nullptr, &multipart) == invalidArgument); + Require(multipart == 0); + Require(sceNpWebApi2CreateMultipartRequest(multipartUser, "fileManager", "/v1/files", "POST", &multipart) == 0 && multipart > 0); + std::int64_t nextMultipart = 0; + Require(sceNpWebApi2CreateMultipartRequest(multipartUser, "fileManager", "/v1/files", "POST", &nextMultipart) == 0 && nextMultipart > multipart); + Require(sceNpWebApi2CreateMultipartRequest(multipartUser, "fileManager", "/v1/files", "POST", nullptr) == 0); + unsigned char part[4]{1, 2, 3, 4}; + Require(sceNpWebApi2SendMultipartRequest(multipart, 0, part, sizeof(part), nullptr) == invalidArgument); + Require(sceNpWebApi2SendMultipartRequest(multipart, -1, part, sizeof(part), nullptr) == invalidArgument); + Require(sceNpWebApi2SendMultipartRequest(multipart, 1, nullptr, sizeof(part), nullptr) == invalidArgument); + Require(sceNpWebApi2SendMultipartRequest(multipart, 1, part, 0, nullptr) == invalidArgument); + Require(sceNpWebApi2SendMultipartRequest(multipart, 1, part, sizeof(part), nullptr) == unavailable); + Require(sceNpWebApi2DeleteRequest(multipart) == 0 && sceNpWebApi2DeleteRequest(nextMultipart) == 0); for (const int created : {internal, internal2, presence}) { Require(sceNpWebApi2Terminate(created) == 0); Require(sceNpWebApi2GetMemoryPoolStats(created, &stats) == invalidContext); diff --git a/docs/dev/debt/libSceNpWebApi2.md b/docs/dev/debt/libSceNpWebApi2.md index 7c73e05da4..3049b21a2f 100644 --- a/docs/dev/debt/libSceNpWebApi2.md +++ b/docs/dev/debt/libSceNpWebApi2.md @@ -9,7 +9,7 @@ - [sceNpWebApi2AddMultipartPart, sceNpWebApi2IntCreateRequest, sceNpWebApi2IntPushEventCreateCtxIndFilter and sceNpWebApi2SetMultipartContentType](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpWebApi2.c); no-argument prototypes follow [shadPS4's PS4 stubs](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_web_api2/np_web_api2.cpp), not measured on a console. These declarations have no parameters solely to resolve known imports and make no ABI claim. - [sceNpWebApi2InitializeForPresence, sceNpWebApi2IntInitialize and sceNpWebApi2IntInitialize2](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2) - create a library context like `sceNpWebApi2Initialize`, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/8d50471b7dc141dd700b51276b8edd1f293bce26/src/core/libraries/np/np_web_api2/np_web_api2.cpp#L174-L222) does on the PS4: the argument structures (32 and 40 bytes) and the `struct_size` check that returns `SCE_NP_WEBAPI2_ERROR_INVALID_ARGUMENT` follow its [declarations](https://github.com/shadps4-emu/shadPS4/blob/8d50471b7dc141dd700b51276b8edd1f293bce26/src/core/libraries/np/np_web_api2/np_web_api2.h#L15-L31). The context type shadPS4 records (2 for the internal initializers, 3 for presence), the context name and the push configuration group are not kept, and the 0x8000 context limit is not applied. Not measured on a PS5 -- [sceNpWebApi2CreateMultipartRequest and sceNpWebApi2SendMultipartRequest](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpWebApi2.c). The signatures follow the PS4 implementations in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_web_api2/np_web_api2.cpp#L52-L221) and [its multipart sender](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_web_api2/np_web_api2.cpp#L388-L399); opaque struct pointers use `const void*` or `void*` to preserve source constness. These prototypes are not measured on a console. +- [sceNpWebApi2CreateMultipartRequest and sceNpWebApi2SendMultipartRequest](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2) - signatures and checks follow the PS4 implementations in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/e0845a12dc05f2fde85e7b203c76de03f93a585d/src/core/libraries/np/np_web_api2/np_web_api2.cpp#L52-L72) and [its multipart sender](https://github.com/shadps4-emu/shadPS4/blob/e0845a12dc05f2fde85e7b203c76de03f93a585d/src/core/libraries/np/np_web_api2/np_web_api2.cpp#L388-L399): a null API group, path or method, a part index below 1, a null part or an empty part returns `SCE_NP_WEBAPI2_ERROR_INVALID_ARGUMENT`. As `sceNpWebApi2CreateRequest` and `sceNpWebApi2SendRequest` do here, a create takes the next shared ID (written only for a non-null ID pointer) and a send answers `SCE_NP_WEBAPI2_ERROR_UNAVAILABLE`; user contexts are not checked and parts are not kept. Not measured on a PS5 - [sceNpWebApi2IntPushEventCreateDestPs4CtxIndFilter](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2) - no credible prototype was found; it is declared without parameters solely to resolve known imports and makes no ABI claim. - [Push handle lifecycle](../../../core/libs/prx/libSceNpWebApi2/Export.cpp): PS5 create/delete/abort signatures and invalid-context/invalid-handle errors are confirmed. Handles track ownership, timeout and abort state; deletion and context termination remove them. Network push operations and cancellation of active requests remain unimplemented. - [sceNpWebApi2GetMemoryPoolStats](../../../core/libs/prx/libSceNpWebApi2/Export.cpp): PS5 confirms the context/statistics arguments and 32-byte output. An invalid context returns `0x80553404` without changing output; a valid context with null output succeeds. Allocator accounting remains absent: peak and current usage still report zero instead of native pool allocations. From 684ffde83d2d24b70a99c9cfa478a8f394fed3ee Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sun, 25 Oct 2026 15:16:04 +0000 Subject: [PATCH 058/198] feat(libSceBluetoothHid): fill the HID parameter block in sceBluetoothHidParamInitialize --- core/libs/prx/libSceBluetoothHid/Export.cpp | 5 ++++- core/libs/tests/GuestBluetoothHid.cpp | 23 +++++++++++++++++++++ docs/dev/debt/libSceBluetoothHid.md | 1 + 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceBluetoothHid/Export.cpp b/core/libs/prx/libSceBluetoothHid/Export.cpp index e1e00be946..ba64feb919 100644 --- a/core/libs/prx/libSceBluetoothHid/Export.cpp +++ b/core/libs/prx/libSceBluetoothHid/Export.cpp @@ -4,6 +4,7 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include #include #include @@ -118,7 +119,9 @@ int APS5_VABI sceBluetoothHidInterruptOutput(std::uint64_t deviceHandle, std::ui } int APS5_VABI sceBluetoothHidParamInitialize(BluetoothHidParam* param, const BluetoothHidThreadParam* threadParam) { - NotImplemented_nid_no_patch(__func__); + if (param == nullptr) throw std::invalid_argument("sceBluetoothHidParamInitialize: no source gives the answer for a null parameter block"); + param->threadParam = threadParam; + param->reserved = 0; return 0; } diff --git a/core/libs/tests/GuestBluetoothHid.cpp b/core/libs/tests/GuestBluetoothHid.cpp index fbc4db06e2..26a6ca3028 100644 --- a/core/libs/tests/GuestBluetoothHid.cpp +++ b/core/libs/tests/GuestBluetoothHid.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include namespace { @@ -93,6 +94,28 @@ int main() { Require(threw, "set feature report did not throw"); Require(sceBluetoothHidUnregisterDevice(0x44f, 0x1000) == 0, "device unregistration failed"); Require(sceBluetoothHidUnregisterCallback() == 0, "callback unregistration failed"); + BluetoothHidThreadParam thread{}; + BluetoothHidParam filled; + std::memset(&filled, 0xa5, sizeof(filled)); + Require(sceBluetoothHidParamInitialize(&filled, &thread) == 0, "parameter initialization did not return 0"); + unsigned char bytes[sizeof(BluetoothHidParam)]; + std::memcpy(bytes, &filled, sizeof(bytes)); + const BluetoothHidThreadParam* stored = nullptr; + std::memcpy(&stored, bytes, sizeof(stored)); + std::uint32_t cleared = 1; + std::memcpy(&cleared, bytes + 8, sizeof(cleared)); + Require(stored == &thread, "the thread parameter was not stored at offset 0"); + Require(cleared == 0, "the word at offset 8 was not cleared"); + Require(bytes[12] == 0xa5 && bytes[13] == 0xa5 && bytes[14] == 0xa5 && bytes[15] == 0xa5, "bytes 12 to 15 of the parameter block changed"); + std::memset(&filled, 0x5a, sizeof(filled)); + Require(sceBluetoothHidParamInitialize(&filled, nullptr) == 0 && filled.threadParam == nullptr && filled.reserved == 0 && filled.padding == 0x5a5a5a5au, "a null thread parameter was not stored as given"); + threw = false; + try { + sceBluetoothHidParamInitialize(nullptr, &thread); + } catch (const std::invalid_argument&) { + threw = true; + } + Require(threw, "a null parameter block did not throw"); std::puts("BluetoothHid tests passed"); return 0; } diff --git a/docs/dev/debt/libSceBluetoothHid.md b/docs/dev/debt/libSceBluetoothHid.md index faf41e09da..ebde90f39a 100644 --- a/docs/dev/debt/libSceBluetoothHid.md +++ b/docs/dev/debt/libSceBluetoothHid.md @@ -7,4 +7,5 @@ ### Unknown function info - [sceBluetoothHidGetDeviceName and sceBluetoothHidGetDeviceInfo](../../../core/libs/prx/libSceBluetoothHid/Export.cpp): output pointers are forwarded to device ioctls without accessing the buffers. Their output sizes and layouts remain unresolved. Report APIs use 64-bit device handles; device-info and disconnect APIs use 32-bit device IDs. +- [sceBluetoothHidParamInitialize](../../../core/libs/prx/libSceBluetoothHid/Export.cpp) follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Bluetooth/SceBluetoothHid.cs#L21-L49), whose signatures were recovered from the PS5 module: it writes the thread-parameter pointer at offset 0 of the 16-byte block, clears the 32-bit word at offset 8 and returns 0. The source names no other byte, so bytes 12 to 15 keep what the caller left there. A null block throws, as no source gives its answer. Not measured on a PS5 - [sceBluetoothHidDebugGetVersion](../../../core/libs/prx/libSceBluetoothHid/Export.cpp): returns a constant without reading arguments, so the supplied implementation does not establish whether the public prototype has unused parameters. From f8213ac53d524cbe932d0efca800ed1839123b4f Mon Sep 17 00:00:00 2001 From: simnJS Date: Sun, 25 Oct 2026 15:33:46 +0000 Subject: [PATCH 059/198] test(libSceUsbStorage): run the service cases on Windows with injected volumes libstdc++ on Windows answers create_directory_symlink with function_not_supported, so the fake sysfs could never be built there and every device case was skipped. Inject the host volumes directly for the device, map, getdents and hotplug cases, and keep the fake sysfs only for the scan of mountinfo and sysfs, which is Linux's. --- .../libSceUsbStorage/include/HostVolumes.hpp | 16 ++++ .../prx/libSceUsbStorage/src/HostVolumes.cpp | 30 +++++++ core/libs/tests/GuestUsbStorage.cpp | 87 ++++++++++++++----- 3 files changed, 109 insertions(+), 24 deletions(-) diff --git a/core/libs/prx/libSceUsbStorage/include/HostVolumes.hpp b/core/libs/prx/libSceUsbStorage/include/HostVolumes.hpp index 9117a283f5..6cf25c4b02 100644 --- a/core/libs/prx/libSceUsbStorage/include/HostVolumes.hpp +++ b/core/libs/prx/libSceUsbStorage/include/HostVolumes.hpp @@ -1,6 +1,7 @@ #ifndef CORE_LIBS_PRX_LIBSCEUSBSTORAGE_INCLUDE_HOSTVOLUMES_HPP #define CORE_LIBS_PRX_LIBSCEUSBSTORAGE_INCLUDE_HOSTVOLUMES_HPP +#include #include #include #include @@ -35,6 +36,21 @@ std::vector ScanHostVolumes(); } +struct UsbStorageTestVolume { + const char* key; + const char* root; + std::uint64_t volumeSize; + std::uint16_t vendorId; + std::uint16_t productId; + std::uint16_t bcdDevice; + const char* vendor; + const char* product; + const char* serialNumber; + bool readOnly; + bool exFat; +}; + extern "C" void SetUsbStorageHostRootsForTest_nid_no_patch(const char* mountInfo, const char* sysRoot); +extern "C" void SetUsbStorageHostVolumesForTest_nid_no_patch(const UsbStorageTestVolume* volumes, std::size_t count); #endif diff --git a/core/libs/prx/libSceUsbStorage/src/HostVolumes.cpp b/core/libs/prx/libSceUsbStorage/src/HostVolumes.cpp index f7a684771b..c34a9a3ff1 100644 --- a/core/libs/prx/libSceUsbStorage/src/HostVolumes.cpp +++ b/core/libs/prx/libSceUsbStorage/src/HostVolumes.cpp @@ -17,6 +17,7 @@ namespace { std::mutex rootsMutex; std::optional> testRoots; +std::optional> testVolumes; std::filesystem::path Utf8Path(std::string_view text) { return std::filesystem::path(std::u8string(text.begin(), text.end())); @@ -148,6 +149,7 @@ std::vector ScanHostVolumes() { std::optional> roots; { std::lock_guard lock(rootsMutex); + if (testVolumes) return *testVolumes; roots = testRoots; } if (roots) return ScanSysfsVolumes(roots->first, roots->second); @@ -164,9 +166,37 @@ std::vector ScanHostVolumes() { extern "C" void SetUsbStorageHostRootsForTest_nid_no_patch(const char* mountInfo, const char* sysRoot) { std::lock_guard lock(UsbStorage::rootsMutex); + UsbStorage::testVolumes.reset(); if (mountInfo == nullptr || sysRoot == nullptr) { UsbStorage::testRoots.reset(); return; } UsbStorage::testRoots.emplace(UsbStorage::Utf8Path(mountInfo), UsbStorage::Utf8Path(sysRoot)); } + +extern "C" void SetUsbStorageHostVolumesForTest_nid_no_patch(const UsbStorageTestVolume* volumes, std::size_t count) { + std::lock_guard lock(UsbStorage::rootsMutex); + UsbStorage::testRoots.reset(); + if (volumes == nullptr) { + UsbStorage::testVolumes.reset(); + return; + } + const auto text = [](const char* value) { return value == nullptr ? std::string() : std::string(value); }; + std::vector copies; + for (std::size_t i = 0; i < count; ++i) { + UsbStorage::HostVolume volume; + volume.key = text(volumes[i].key); + volume.root = UsbStorage::Utf8Path(text(volumes[i].root)); + volume.volumeSize = volumes[i].volumeSize; + volume.vendorId = volumes[i].vendorId; + volume.productId = volumes[i].productId; + volume.bcdDevice = volumes[i].bcdDevice; + volume.vendor = text(volumes[i].vendor); + volume.product = text(volumes[i].product); + volume.serialNumber = text(volumes[i].serialNumber); + volume.readOnly = volumes[i].readOnly; + volume.fileSystem = volumes[i].exFat ? UsbStorage::FileSystem::ExFat : UsbStorage::FileSystem::Fat; + copies.push_back(std::move(volume)); + } + UsbStorage::testVolumes = std::move(copies); +} diff --git a/core/libs/tests/GuestUsbStorage.cpp b/core/libs/tests/GuestUsbStorage.cpp index 6fa4023d64..07d66238fc 100644 --- a/core/libs/tests/GuestUsbStorage.cpp +++ b/core/libs/tests/GuestUsbStorage.cpp @@ -134,6 +134,24 @@ std::string SecondLine(const FakeHost& host) { return MountLine("8_33", "/", host.secondMount, "ro,relatime", "exfat"); } +constexpr std::uint64_t FirstVolumeSize = 61530439680ull; +constexpr std::uint64_t SecondVolumeSize = 31914983424ull; + +std::string Utf8(const std::filesystem::path& path) { + const auto text = path.u8string(); + return std::string(text.begin(), text.end()); +} + +void Inject(const FakeHost& host, bool first, bool second) { + static const UsbStorageTestVolume none{}; + const auto firstRoot = Utf8(host.firstMount); + const auto secondRoot = Utf8(host.secondMount); + std::vector volumes; + if (first) volumes.push_back({"first", firstRoot.c_str(), FirstVolumeSize, 0x0781, 0x5581, 0x0100, "SanDisk", "Ultra Fit", "4C530001", false, false}); + if (second) volumes.push_back({"second", secondRoot.c_str(), SecondVolumeSize, 0x090c, 0x1000, 0x1100, "Samsung", "Flash Drive", "", true, true}); + SetUsbStorageHostVolumesForTest_nid_no_patch(volumes.empty() ? &none : volumes.data(), volumes.size()); +} + FakeHost CreateHost() { FakeHost host; host.root = std::filesystem::temp_directory_path() / ("anyps5_usb_storage_" + std::to_string(std::random_device{}())); @@ -141,9 +159,12 @@ FakeHost CreateHost() { host.sys = host.root / "sys"; host.firstMount = host.root / "media" / "USB DRIVE"; host.secondMount = host.root / "media" / "second"; - std::filesystem::create_directories(host.sys / "dev" / "block"); - WriteMounts(host, ""); - SetUsbStorageHostRootsForTest_nid_no_patch(host.mountInfo.string().c_str(), host.sys.string().c_str()); + WriteFile(host.firstMount / "song.txt", "song"); + WriteFile(host.firstMount / ".hidden", "hidden"); + std::filesystem::create_directories(host.firstMount / "Music"); + WriteFile(host.secondMount / "Readme.txt", "readme"); + WriteFile(host.root / "media" / "efi" / "boot.efi", "efi"); + Inject(host, false, false); return host; } @@ -154,7 +175,8 @@ bool Link(const FakeHost& host, std::string_view device, const std::filesystem:: return !error; } -bool CreateDevices(const FakeHost& host) { +bool CreateSysfs(const FakeHost& host) { + std::filesystem::create_directories(host.sys / "dev" / "block"); if (!Link(host, "0_0", host.sys / "devices" / "probe")) return false; const auto controller = host.sys / "devices" / "pci0000_00" / "0000_00_14.0" / "usb1"; const auto first = controller / "1-1"; @@ -177,11 +199,6 @@ bool CreateDevices(const FakeHost& host) { const auto sata = host.sys / "devices" / "pci0000_00" / "0000_00_17.0" / "ata1" / "host0" / "target0_0_0" / "0_0_0_0"; WriteFile(sata / "vendor", "ATA \n"); WriteFile(sata / "model", "Disk \n"); - WriteFile(host.firstMount / "song.txt", "song"); - WriteFile(host.firstMount / ".hidden", "hidden"); - std::filesystem::create_directories(host.firstMount / "Music"); - WriteFile(host.secondMount / "Readme.txt", "readme"); - WriteFile(host.root / "media" / "efi" / "boot.efi", "efi"); return Link(host, "8_17", firstScsi / "block" / "sdb" / "sdb1") && Link(host, "8_33", secondScsi / "block" / "sdc" / "sdc1") && Link(host, "8_1", sata / "block" / "sda" / "sda1") && Link(host, "8_49", firstScsi / "block" / "sdd" / "sdd1"); } @@ -336,7 +353,7 @@ void TestDeviceInfo(const FakeHost& host) { std::uint32_t devices[UsbStorage::MaxDevices]{}; std::int32_t count = -1; Require(sceUsbStorageGetDeviceList(devices, &count) == 0, "get the device list with a device"); - Require(count == 1 && devices[0] == 0, "only the mounted FAT volume of a USB mass-storage device is listed"); + Require(count == 1 && devices[0] == 0, "the attached volume is listed as device 0"); UsbStorageDeviceInfo info; std::memset(&info, 0x5a, sizeof(info)); @@ -347,9 +364,7 @@ void TestDeviceInfo(const FakeHost& host) { Require(std::string_view(info.serialNumber) == "4C530001" && info.serialNumberLength == 8, "the device info holds the serial number"); Require(info.flags == UsbStorage::FatFlag, "the device info flags a writable FAT volume"); Require(info.reserved == 0 && info.padding[0] == 0 && info.padding[1] == 0, "the device info cleared its unknown fields"); - std::error_code error; - const auto capacity = std::filesystem::space(host.firstMount, error).capacity; - Require(!error && info.volumeSize == capacity, "the volume size is the capacity of the mounted file system"); + Require(info.volumeSize == FirstVolumeSize, "the device info holds the volume size"); Require(sceUsbStorageGetDeviceInfo(1, &info) == UsbStorage::DeviceNotFound, "get device info of an absent device"); char mountPoint[UsbStorage::MountPointSize]; @@ -360,6 +375,7 @@ void TestDeviceInfo(const FakeHost& host) { exists = 1; Require(sceUsbStorageIsExist(1, "/mnt/usb1", &exists) == true && exists == 0, "device 1 does not exist"); const auto resolved = ResolvePath_nid_no_patch("/mnt/usb0/song.txt"); + std::error_code error; Require(std::filesystem::equivalent(resolved, host.firstMount / "song.txt", error), "the mount point resolves to the host volume"); } @@ -445,8 +461,7 @@ void TestDeviceGetdents() { } void TestDevices(const FakeHost& host) { - WriteMounts(host, FirstLine(host) + MountLine("8_1", "/", host.root / "media" / "efi", "rw", "vfat") + - MountLine("8_49", "/", host.root / "media", "rw", "ext4") + MountLine("8_17", "/Music", host.root / "media" / "bound", "rw", "vfat")); + Inject(host, true, false); Require(sceUsbStorageInit(nullptr) == 0, "initialize with a device"); TestDeviceInfo(host); TestDeviceMaps(); @@ -461,12 +476,12 @@ void TestDevices(const FakeHost& host) { } void TestHotplug(const FakeHost& host) { - WriteMounts(host, FirstLine(host)); + Inject(host, true, false); Require(sceUsbStorageInit(nullptr) == 0, "initialize for hotplug"); Require(sceUsbStorageRegisterCallback(UsbStorage::MountEvent, &Record, &g_mountTag) == 0, "register the mount recorder"); Require(sceUsbStorageRegisterCallback(UsbStorage::UnmountEvent, &Record, &g_unmountTag) == 0, "register the unmount recorder"); - WriteMounts(host, FirstLine(host) + SecondLine(host)); + Inject(host, true, true); auto recorded = NextEvent(0); Require(recorded.event.event == 1 && recorded.event.device == 1 && recorded.userData == &g_mountTag, "a mounted device raises a mount event"); Require(recorded.event.flags == (UsbStorage::ExFatFlag | UsbStorage::ReadOnlyFlag), "the mount event flags a read-only exFAT volume"); @@ -474,7 +489,7 @@ void TestHotplug(const FakeHost& host) { Require(sceUsbStorageGetDeviceInfo(1, &info) == 0 && info.vendorId == 0x090c && info.bcdDevice == 0x1100, "the mounted device is listed"); Require(info.serialNumber[0] == '\0' && info.serialNumberLength == 0 && std::string_view(info.product) == "Flash Drive", "a device without a serial number reports an empty one"); - WriteMounts(host, SecondLine(host)); + Inject(host, false, true); recorded = NextEvent(1); Require(recorded.event.event == 2 && recorded.event.device == 0 && recorded.userData == &g_unmountTag, "a removed device raises an unmount event"); Require(recorded.event.flags == UsbStorage::FatFlag, "the unmount event flags the removed volume"); @@ -482,7 +497,7 @@ void TestHotplug(const FakeHost& host) { std::int32_t count = -1; Require(sceUsbStorageGetDeviceList(devices, &count) == 0 && count == 1 && devices[0] == 1, "the remaining device keeps its id"); - WriteMounts(host, FirstLine(host) + SecondLine(host)); + Inject(host, true, true); recorded = NextEvent(2); Require(recorded.event.event == 1 && recorded.event.device == 0, "a device mounted again takes the lowest free id"); Require(sceUsbStorageUnregisterCallback(UsbStorage::MountEvent, &Record) == 0, "unregister the mount recorder"); @@ -492,6 +507,29 @@ void TestHotplug(const FakeHost& host) { Require(Events().events.size() == 3, "hotplug raised no other event"); } +void TestSysfsScan(const FakeHost& host) { + WriteMounts(host, FirstLine(host) + SecondLine(host) + MountLine("8_1", "/", host.root / "media" / "efi", "rw", "vfat") + + MountLine("8_49", "/", host.root / "media", "rw", "ext4") + MountLine("8_17", "/Music", host.root / "media" / "bound", "rw", "vfat")); + SetUsbStorageHostRootsForTest_nid_no_patch(Utf8(host.mountInfo).c_str(), Utf8(host.sys).c_str()); + Require(sceUsbStorageInit(nullptr) == 0, "initialize over the fake sysfs"); + std::uint32_t devices[UsbStorage::MaxDevices]{}; + std::int32_t count = -1; + Require(sceUsbStorageGetDeviceList(devices, &count) == 0 && count == 2 && devices[0] == 0 && devices[1] == 1, + "only the whole FAT and exFAT volumes of USB mass-storage devices are listed"); + UsbStorageDeviceInfo info{}; + Require(sceUsbStorageGetDeviceInfo(0, &info) == 0, "get the info of the scanned FAT volume"); + Require(info.vendorId == 0x0781 && info.productId == 0x5581 && info.bcdDevice == 0x0100, "the scan reads the USB ids and release"); + Require(std::string_view(info.vendor) == "SanDisk" && std::string_view(info.product) == "Ultra Fit", "the scan reads the SCSI strings without trailing spaces"); + Require(std::string_view(info.serialNumber) == "4C530001" && info.flags == UsbStorage::FatFlag, "the scan reads the serial number and a writable FAT volume"); + std::error_code error; + const auto capacity = std::filesystem::space(host.firstMount, error).capacity; + Require(!error && info.volumeSize == capacity, "the scanned volume size is the capacity of the mounted file system"); + Require(sceUsbStorageGetDeviceInfo(1, &info) == 0, "get the info of the scanned exFAT volume"); + Require(info.vendorId == 0x090c && std::string_view(info.product) == "Flash Drive" && info.serialNumberLength == 0, "the scan reads a device without a serial number"); + Require(info.flags == (UsbStorage::ExFatFlag | UsbStorage::ReadOnlyFlag), "the scan reads a read-only exFAT mount"); + Require(sceUsbStorageTerm() == 0, "terminate over the fake sysfs"); +} + } int main() { @@ -503,13 +541,14 @@ int main() { TestMapRequests(); TestGetdents(); TestThrowingExports(); - if (CreateDevices(host)) { - TestDevices(host); - TestHotplug(host); + TestDevices(host); + TestHotplug(host); + if (CreateSysfs(host)) { + TestSysfsScan(host); } else { - std::printf("USBSTORAGE: directory symbolic links are unavailable, the device cases are skipped\n"); + std::printf("USBSTORAGE: directory symbolic links are unavailable, the sysfs scan is not exercised\n"); } - SetUsbStorageHostRootsForTest_nid_no_patch(nullptr, nullptr); + SetUsbStorageHostVolumesForTest_nid_no_patch(nullptr, 0); std::error_code error; std::filesystem::remove_all(host.root, error); return 0; From 4d4cb28eb0dbc9e3e5f6b5c0bd765dda7a0b20c0 Mon Sep 17 00:00:00 2001 From: pushkarreddyy <212133856+pushkarreddyy@users.noreply.github.com> Date: Sun, 25 Oct 2026 21:39:38 +0530 Subject: [PATCH 060/198] feat(libkernel): return EFAULT for unreadable append iovec buffers on Windows --- core/libs/prx/libkernel/File/src/Stdio.cpp | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 486416f762..8eff5d45f0 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -1387,11 +1387,15 @@ static std::int64_t TransferIovecs(int d, const KernelIovec* guestIov, int iovcn } if (!write && offset == nullptr && File::IsDirectoryDescriptor(d)) return SceErrorFromErrno(GUEST_EISDIR); if (write && iovcnt > 1 && total <= static_cast(std::numeric_limits::max()) && File::Appends(d)) { - std::vector gathered; - gathered.reserve(total); + std::vector gathered(total); + std::size_t gatheredOffset = 0; for (int i = 0; i < iovcnt; ++i) { - const auto* base = static_cast(iov[i].base); - gathered.insert(gathered.end(), base, base + iov[i].length); + if (iov[i].length == 0) continue; + SIZE_T copied = 0; + if (!ReadProcessMemory(GetCurrentProcess(), iov[i].base, gathered.data() + gatheredOffset, iov[i].length, &copied) || copied != iov[i].length) { + return SceErrorFromErrno(GUEST_EFAULT); + } + gatheredOffset += iov[i].length; } const auto result = offset != nullptr ? NativePositionedTransfer(d, gathered.data(), gathered.size(), *offset, true) : NativeTransferBuffer(d, gathered.data(), gathered.size(), true); From 708cc2260d9f1c30306849473c4519d30c420688 Mon Sep 17 00:00:00 2001 From: Morten Holm Date: Sun, 25 Oct 2026 16:59:17 +0100 Subject: [PATCH 061/198] fix(agc): name the mesh limit a pipeline exceeds and its values The mesh pipeline check rejected a shader with one message for three limits, so a shader 4 bytes over the shared memory limit could not be told from one with too many output vertices. ValidateMeshLimits checks the workgroup, output vertices, output primitives and LDS size separately and names the limit and both values. Co-Authored-By: Claude Sonnet 5.5 --- .../Graphics/include/Pipeline.hpp | 1 + .../libSceAgcDriver/Graphics/src/Pipeline.cpp | 12 +++++++-- .../prx/libSceAgcDriver/tests/Graphics.cpp | 26 +++++++++++++++++++ 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp index abfb3226a6..bc8d63a04f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp @@ -142,6 +142,7 @@ bool HasStencil(VkFormat format); // Device limit checks of the viewport, which is dynamic state and so no longer checked by Pipeline. void ValidateViewport(const Context& context, const VkViewport& viewport); void ValidateDepthBounds(const Context& context, const State& state); +void ValidateMeshLimits(const Context& context, const ShaderRecompiler::MeshConfiguration& mesh, std::uint32_t invocations); void ValidateProvokingVertex(const Context& context, const State& state, std::span shaders = {}); bool BarycentricPatches(const State& state, std::span shaders); bool ValidateSampleLocations(const Context& context, const State& state); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index 8ce59dade1..4b9ba4ab68 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -251,6 +251,15 @@ bool ValidateSampleLocations(const Context& context, const State& state) { return enabled; } +void ValidateMeshLimits(const Context& context, const ShaderRecompiler::MeshConfiguration& mesh, std::uint32_t invocations) { + const auto& limits = context.meshLimits; + Require(invocations <= limits.maxMeshWorkGroupInvocations && invocations <= limits.maxMeshWorkGroupSize[0], "mesh workgroup of " + std::to_string(invocations) + " invocations exceeds device limits (" + std::to_string(limits.maxMeshWorkGroupInvocations) + " invocations, " + std::to_string(limits.maxMeshWorkGroupSize[0]) + " wide)"); + Require(mesh.maxVertices <= limits.maxMeshOutputVertices, "mesh output vertices " + std::to_string(mesh.maxVertices) + " exceed the device limit " + std::to_string(limits.maxMeshOutputVertices)); + Require(mesh.maxPrimitives <= limits.maxMeshOutputPrimitives, "mesh output primitives " + std::to_string(mesh.maxPrimitives) + " exceed the device limit " + std::to_string(limits.maxMeshOutputPrimitives)); + const auto sharedBytes = static_cast(mesh.ldsSizeDwords) * 4; + Require(sharedBytes <= limits.maxMeshSharedMemorySize, "mesh LDS " + std::to_string(sharedBytes) + " bytes exceeds the device shared memory limit " + std::to_string(limits.maxMeshSharedMemorySize)); +} + void ValidateViewport(const Context& context, const VkViewport& viewport) { Require(std::isfinite(viewport.minDepth) && std::isfinite(viewport.maxDepth), "non-finite viewport depth range"); Require(context.depthRangeUnrestricted || (viewport.minDepth >= 0 && viewport.minDepth <= 1 && viewport.maxDepth >= 0 && viewport.maxDepth <= 1), "viewport depth outside [0, 1] requires VK_EXT_depth_range_unrestricted"); @@ -479,8 +488,7 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput Require(context.meshShader, "device does not support VK_EXT_mesh_shader"); const auto& mesh = *state.stages.mesh; const auto invocations = state.stages.vertexWaveSize == 64u && context.subgroup.subgroupSize == 32u ? mesh.threadsPerGroup / 2u : mesh.threadsPerGroup; - Require(invocations <= context.meshLimits.maxMeshWorkGroupInvocations && invocations <= context.meshLimits.maxMeshWorkGroupSize[0], "mesh workgroup exceeds device limits"); - Require(mesh.maxVertices <= context.meshLimits.maxMeshOutputVertices && mesh.maxPrimitives <= context.meshLimits.maxMeshOutputPrimitives && static_cast(mesh.ldsSizeDwords) * 4 <= context.meshLimits.maxMeshSharedMemorySize, "mesh output or LDS exceeds device limits"); + ValidateMeshLimits(context, mesh, invocations); } const auto pushStages = PushConstantStages(shaders); Require(pushStages == 0 || context.limits.maxPushConstantsSize >= PushBlockBytes(dynamicRendering), "graphics push constant range exceeds device limit"); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 2595184b0c..db00c19d58 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -11134,6 +11134,31 @@ void meshArgumentTests() { Require(same(ResolveMeshArguments(record(0xffffffffu, 1, 0), rules(points, 0xffffffffu)), {0, 0, 0, 0xffffffffu, 0}), "groups over the device limit draw nothing"); } +void meshLimitTests() { + AgcDriver::Graphics::Context context{}; + context.meshLimits.maxMeshWorkGroupInvocations = 128; + context.meshLimits.maxMeshWorkGroupSize[0] = 96; + context.meshLimits.maxMeshOutputVertices = 256; + context.meshLimits.maxMeshOutputPrimitives = 512; + context.meshLimits.maxMeshSharedMemorySize = 12284; + ShaderRecompiler::MeshConfiguration mesh{4u, 32u, 96u, 96u, 32u, 128u, 2048u, 0u, 4u}; + mesh.maxVertices = 256; + mesh.maxPrimitives = 512; + mesh.ldsSizeDwords = 3071; + AgcDriver::Graphics::ValidateMeshLimits(context, mesh, 96); + expectFailure([&] { AgcDriver::Graphics::ValidateMeshLimits(context, mesh, 97); }, "mesh workgroup of 97 invocations exceeds device limits (128 invocations, 96 wide)"); + mesh.maxVertices = 257; + expectFailure([&] { AgcDriver::Graphics::ValidateMeshLimits(context, mesh, 96); }, "mesh output vertices 257 exceed the device limit 256"); + mesh.maxVertices = 256; + mesh.maxPrimitives = 513; + expectFailure([&] { AgcDriver::Graphics::ValidateMeshLimits(context, mesh, 96); }, "mesh output primitives 513 exceed the device limit 512"); + mesh.maxPrimitives = 512; + mesh.ldsSizeDwords = 3072; + expectFailure([&] { AgcDriver::Graphics::ValidateMeshLimits(context, mesh, 96); }, "mesh LDS 12288 bytes exceeds the device shared memory limit 12284"); + mesh.ldsSizeDwords = 3071; + AgcDriver::Graphics::ValidateMeshLimits(context, mesh, 96); +} + void debugBranchTests() { for (const auto opcode : {0x17u, 0x18u, 0x19u, 0x1au}) Require(recompilesDebugBranch(opcode), "a conditional debug branch did not recompile"); } @@ -12245,6 +12270,7 @@ int main(int argc, char** argv) { storageTextureBudgetTests(); sampledBudgetReportTests(); meshArgumentTests(); + meshLimitTests(); meshIndexBufferTests(); stagingPoolBudgetTests(); validationTests(); From 7cfbb837c43378e5b2138b1a2cbaee16bdd0d1c5 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Sun, 25 Oct 2026 12:56:59 -0400 Subject: [PATCH 062/198] fix(libc): preserve frexpf decomposition and exception behavior --- core/libs/prx/libc/src/Math.cpp | 17 ++++++++++- core/libs/tests/GuestMath.cpp | 53 +++++++++++++++++++++++++++++++++ docs/dev/debt/libc.md | 2 ++ 3 files changed, 71 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index e635b0b1e1..3eba975438 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -385,7 +385,22 @@ long double APS5_VABI scalbnl_nid_postfix(long double x, int exponent) { return float APS5_VABI significandf_nid_postfix(float x) { return x == 0.0f || !std::isfinite(x) ? x : std::scalbn(x, -std::ilogb(x)); } double APS5_VABI significand_nid_postfix(double x) { return x == 0.0 || !std::isfinite(x) ? x : std::scalbn(x, -std::ilogb(x)); } double APS5_VABI frexp_nid_postfix(double x, int* exponent) { return std::frexp(x, exponent); } -float APS5_VABI frexpf_nid_postfix(float x, int* exponent) { return std::frexp(x, exponent); } +float APS5_VABI frexpf_nid_postfix(float x, int* exponent) { + auto bits = std::bit_cast(x); + auto magnitude = bits & 0x7fffffffu; + *exponent = 0; + if (magnitude == 0 || magnitude >= 0x7f800000u) return x; + int adjustment = 0; + if (magnitude < 0x00800000u) { + volatile float subnormal = x; + x = subnormal * 0x1p25f; + bits = std::bit_cast(x); + magnitude = bits & 0x7fffffffu; + adjustment = -25; + } + *exponent = static_cast(magnitude >> 23) - 126 + adjustment; + return std::bit_cast((bits & 0x807fffffu) | 0x3f000000u); +} // Guest long is 64-bit, including on Windows where native long is 32-bit. std::int64_t APS5_VABI lround_nid_postfix(double x) { return RoundToInt64(x); } std::int64_t APS5_VABI lroundf_nid_postfix(float x) { return RoundToInt64(x); } diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index 7c6bd1e44e..b00bf138fb 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -1180,6 +1180,58 @@ static void CheckLogfConsoleBits() { } } +static void CheckFrexpfEnvironment() { + constexpr struct { + std::uint32_t input; + std::uint32_t fraction; + int exponent; + } cases[] = { + {0x00000000u, 0x00000000u, 0}, {0x80000000u, 0x80000000u, 0}, + {0x00000001u, 0x3f000000u, -148}, {0x80000001u, 0xbf000000u, -148}, + {0x007fffffu, 0x3f7ffffeu, -126}, {0x807fffffu, 0xbf7ffffeu, -126}, + {0x00800000u, 0x3f000000u, -125}, {0x80800000u, 0xbf000000u, -125}, + {0x3f000000u, 0x3f000000u, 0}, {0x3f800000u, 0x3f000000u, 1}, + {0xbf800000u, 0xbf000000u, 1}, {0x41400000u, 0x3f400000u, 4}, + {0x7f7fffffu, 0x3f7fffffu, 128}, {0xff7fffffu, 0xbf7fffffu, 128}, + {0x7f800000u, 0x7f800000u, 0}, {0xff800000u, 0xff800000u, 0}, + {0x7f800001u, 0x7f800001u, 0}, {0xff800001u, 0xff800001u, 0}, + {0x7fc01234u, 0x7fc01234u, 0}, {0xffc01234u, 0xffc01234u, 0}, + }; + std::fenv_t saved; + Require(std::fegetenv(&saved) == 0); + const unsigned savedCsr = _mm_getcsr(); + constexpr int rounding[] = {FE_TONEAREST, FE_DOWNWARD, FE_UPWARD, FE_TOWARDZERO}; + const int savedError = *__error_nid_postfix(); + for (unsigned mode = 0; mode != 4; ++mode) { + for (unsigned denorm = 0; denorm != 4; ++denorm) { + const unsigned control = 0x1f80u | (mode << 13) | ((denorm & 1u) ? 0x40u : 0u) | ((denorm & 2u) ? 0x8000u : 0u); + for (const unsigned sticky : {0u, 5u, 32u}) { + for (const auto& item : cases) { + Require(std::fesetenv(FE_DFL_ENV) == 0); + Require(std::fesetround(rounding[mode]) == 0); + *__error_nid_postfix() = 97; + _mm_setcsr(control | sticky); + int exponent = 77; + const auto fraction = std::bit_cast(frexpf_nid_postfix(std::bit_cast(item.input), &exponent)); + const unsigned after = _mm_getcsr(); + const bool subnormal = (item.input & 0x7fffffffu) != 0 && (item.input & 0x7fffffffu) < 0x00800000u; + const bool flushed = subnormal && (denorm & 1u) != 0; + const unsigned flags = sticky | (subnormal && !flushed ? 2u : 0u); + const auto expectedFraction = flushed ? (item.input & 0x80000000u) | 0x3f000000u : item.fraction; + const int expectedExponent = flushed ? -151 : item.exponent; + if (fraction != expectedFraction || exponent != expectedExponent || after != (control | flags) || *__error_nid_postfix() != 97) { + std::fprintf(stderr, "frexpf input=%08x mode=%u denorm=%u sticky=%u fraction=%08x exponent=%d csr=%x\n", item.input, mode, denorm, sticky, fraction, exponent, after); + std::abort(); + } + } + } + } + } + Require(std::fesetenv(&saved) == 0); + _mm_setcsr(savedCsr); + *__error_nid_postfix() = savedError; +} + static void CheckDinkumwareConstants() { RequireBytes(&_Inf_nid_postfix, {0, 0, 0, 0, 0, 0, 0xf0, 0x7f}); RequireBytes(&_Denorm_nid_postfix, {1}); @@ -1745,4 +1797,5 @@ int main() { CheckScaling(); CheckLongDoubleFunctions(); CheckDinkumwareFloatCharacteristics(); + CheckFrexpfEnvironment(); } diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 6baa225040..7341e34f06 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -2,6 +2,8 @@ ### Unknown function info +- [frexpf](../../../core/libs/prx/libc/src/Math.cpp) (libc) - follows the bundled PPSA03766 implementation: exponent-bit decomposition, conditional single-precision subnormal scaling, zero exponent for zero/NaN/infinity, and unchanged signaling NaNs. PPSA07923 agrees on results/exponents but unconditionally scales finite inputs, additionally raising overflow/inexact on large values; that SDK variant is not reproduced. Masked-exception observations cover rounding, DAZ/FTZ and sticky flags; unmasked traps and other SDK versions are unverified. + - [strtonum](../../../core/libs/prx/libc/src/StringExtensions.cpp) (libc) is unverified on the console. Its signature, decimal parsing, inclusive bounds, error strings, error precedence and errno preservation follow [FreeBSD 11.4 strtonum.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdlib/strtonum.c). Parsing uses the host `strtoll` with base 10. It is not exported by `libSceLibcInternal.sprx` on a PS5 (firmware 13.60), so a payload process cannot call it there - [fegetexcept](../../../core/libs/prx/libc/src/FloatingPoint.cpp) (libSceLibcInternal, implemented in libc) has not been read from the PS5 library or measured on a console. It follows FreeBSD 11.4 amd64 [fenv.h](https://github.com/freebsd/freebsd-src/blob/4554bee35405b36ea19e9e60d6abe6ea534e36e0/lib/msun/x86/fenv.h#L121-L133), reporting the six unmasked x87 exceptions independently of MXCSR; whether the PS5 uses the same rule when the units disagree is unverified. From ada5ad09905e2c1cc18faa3910ce7d9aee95e5ce Mon Sep 17 00:00:00 2001 From: Adnan Khan Date: Sun, 25 Oct 2026 22:46:51 +0530 Subject: [PATCH 063/198] fix(libSceAmpr): write the console's Nop header word and report a null base first --- core/libs/prx/libSceAmpr/Export.cpp | 30 +++++++------- .../Apr/include/AprCommandBuffer.hpp | 12 ++++-- core/libs/prx/libkernel/Apr/src/Apr.cpp | 8 ++-- core/libs/tests/GuestAmpr.cpp | 41 +++++++++++++++---- docs/dev/debt/libSceAmpr.md | 2 +- 5 files changed, 63 insertions(+), 30 deletions(-) diff --git a/core/libs/prx/libSceAmpr/Export.cpp b/core/libs/prx/libSceAmpr/Export.cpp index e0d1c792c0..41456c7650 100644 --- a/core/libs/prx/libSceAmpr/Export.cpp +++ b/core/libs/prx/libSceAmpr/Export.cpp @@ -91,17 +91,14 @@ static std::uint64_t NopBytes(std::uint32_t dwords) { return std::uint64_t{dwords} * 4u; } -static int AppendNop(Apr::CommandBufferObject* buffer, std::uint32_t dwords, const std::uint32_t* data) { - if (!buffer) return SCE_KERNEL_ERROR_EINVAL; - const std::uint64_t bytes = NopBytes(dwords); - if (!buffer->base) return SCE_KERNEL_ERROR_EPERM; - if (bytes > RemainingCommandBytes(*buffer)) return SCE_KERNEL_ERROR_EBUSY; - const std::uint32_t header = Apr::NopHeader(dwords); - std::uint8_t* destination = buffer->base + buffer->offset; +static int AppendNop(Apr::CommandBufferObject& buffer, std::uint32_t header, const std::uint32_t* data) { + const std::uint64_t bytes = NopBytes(Apr::NopDwords(header)); + if (bytes > RemainingCommandBytes(buffer)) return SCE_KERNEL_ERROR_EBUSY; + std::uint8_t* destination = buffer.base + buffer.offset; std::memcpy(destination, &header, sizeof(header)); if (data) std::memcpy(destination + sizeof(header), data, bytes - sizeof(header)); - buffer->offset += static_cast(bytes); - ++buffer->numCommands; + buffer.offset += static_cast(bytes); + ++buffer.numCommands; return 0; } @@ -368,12 +365,13 @@ int APS5_VABI sceAmprCommandBufferConstructMarker(Apr::CommandBufferObject* buff } int APS5_VABI sceAmprCommandBufferConstructNop(Apr::CommandBufferObject* buffer, std::int16_t type, const void* payload, std::uint32_t payloadBytes, const std::uint32_t* word) { - (void)type; + if (!buffer) return SCE_KERNEL_ERROR_EINVAL; + if (!buffer->base) return SCE_KERNEL_ERROR_EPERM; if (payloadBytes > (word ? 56u : 60u)) return SCE_KERNEL_ERROR_EINVAL; if (payloadBytes != 0u && !payload) throw std::invalid_argument("sceAmprCommandBufferConstructNop: null payload"); const std::uint32_t dwords = (payloadBytes + 3u) / 4u + (word ? 1u : 0u); - const std::uint32_t offset = buffer ? buffer->offset : 0u; - const int result = AppendNop(buffer, dwords + 1u, nullptr); + const std::uint32_t offset = buffer->offset; + const int result = AppendNop(*buffer, Apr::NopHeader(dwords, static_cast(type)), nullptr); if (result != 0) return result; std::uint8_t* data = buffer->base + offset + Apr::NopHeaderBytes; if (word) { @@ -416,13 +414,17 @@ uint32_t APS5_VABI sceAmprCommandBufferGetType(const Apr::CommandBufferObject* b } int APS5_VABI sceAmprCommandBufferNop(Apr::CommandBufferObject* buffer, std::uint32_t dwords) { + if (!buffer) return SCE_KERNEL_ERROR_EINVAL; + if (!buffer->base) return SCE_KERNEL_ERROR_EPERM; if (dwords == 0u || dwords > 16u) return SCE_KERNEL_ERROR_EINVAL; - return AppendNop(buffer, dwords, nullptr); + return AppendNop(*buffer, Apr::NopHeader(dwords - 1u), nullptr); } int APS5_VABI sceAmprCommandBufferNopWithData(Apr::CommandBufferObject* buffer, std::uint32_t dwords, const std::uint32_t* data) { + if (!buffer) return SCE_KERNEL_ERROR_EINVAL; + if (!buffer->base) return SCE_KERNEL_ERROR_EPERM; if (dwords > 15u) return SCE_KERNEL_ERROR_EINVAL; - return AppendNop(buffer, dwords + 1u, data); + return AppendNop(*buffer, Apr::NopHeader(dwords), data); } int APS5_VABI sceAmprCommandBufferPopMarker(Apr::CommandBufferObject* buffer) { diff --git a/core/libs/prx/libkernel/Apr/include/AprCommandBuffer.hpp b/core/libs/prx/libkernel/Apr/include/AprCommandBuffer.hpp index d1c8cfb7bb..73d5182355 100644 --- a/core/libs/prx/libkernel/Apr/include/AprCommandBuffer.hpp +++ b/core/libs/prx/libkernel/Apr/include/AprCommandBuffer.hpp @@ -88,13 +88,19 @@ struct CompactReadFileGatherCommand { static_assert(sizeof(CompactReadFileGatherCommand) == 12); inline constexpr std::uint32_t NopHeaderBytes = 4; +inline constexpr std::uint32_t NopHeaderMask = 0xffff00ffu; +inline constexpr std::uint32_t NopHeaderTag = 0x5452000fu; -constexpr std::uint32_t NopHeader(std::uint32_t dwords) { - return static_cast(Opcode::Nop) | dwords << 8u; +constexpr std::uint32_t NopHeader(std::uint32_t payloadDwords, std::uint32_t type = 0) { + return NopHeaderTag | (payloadDwords & 0xfu) << 8u | (type & 0xfu) << 12u; +} + +constexpr bool IsNopHeader(std::uint32_t header) { + return (header & NopHeaderMask) == NopHeaderTag; } constexpr std::uint32_t NopDwords(std::uint32_t header) { - return header >> 8u; + return (header >> 8u & 0xfu) + 1u; } struct ResetGatherScatterStateCommand { diff --git a/core/libs/prx/libkernel/Apr/src/Apr.cpp b/core/libs/prx/libkernel/Apr/src/Apr.cpp index 3b72787aff..fa1649e3c7 100644 --- a/core/libs/prx/libkernel/Apr/src/Apr.cpp +++ b/core/libs/prx/libkernel/Apr/src/Apr.cpp @@ -593,10 +593,10 @@ std::optional _execute(const Apr::CommandBufferObject& buffer, std::ui if (buffer.offset - cursor < Apr::NopHeaderBytes) throw std::runtime_error("APR: truncated command buffer"); std::uint32_t first; std::memcpy(&first, buffer.base + cursor, sizeof(first)); - if ((first & 0xffu) == static_cast(Apr::Opcode::Nop)) { - const std::uint64_t bytes = std::uint64_t{Apr::NopDwords(first)} * 4u; - if (bytes == 0u || bytes > buffer.offset - cursor) throw std::runtime_error("APR: malformed command"); - cursor += static_cast(bytes); + if (Apr::IsNopHeader(first)) { + const std::uint32_t bytes = Apr::NopDwords(first) * Apr::NopHeaderBytes; + if (bytes > buffer.offset - cursor) throw std::runtime_error("APR: malformed command"); + cursor += bytes; continue; } if (cursor + sizeof(Apr::CommandHeader) > buffer.offset) throw std::runtime_error("APR: truncated command buffer"); diff --git a/core/libs/tests/GuestAmpr.cpp b/core/libs/tests/GuestAmpr.cpp index 0d02758385..1f1490df6b 100644 --- a/core/libs/tests/GuestAmpr.cpp +++ b/core/libs/tests/GuestAmpr.cpp @@ -54,13 +54,18 @@ struct Recorder { std::uint32_t Commands() const { return sceAmprCommandBufferGetNumCommands(&buffer); } }; +std::uint32_t HeaderAt(const Recorder& recorder, std::uint32_t offset) { + std::uint32_t first; + std::memcpy(&first, recorder.memory.data() + offset, sizeof(first)); + return first; +} + void RequireAppended(const Recorder& recorder, std::uint32_t offset, std::uint32_t commands, Apr::Opcode opcode, std::uint64_t measured) { Require(recorder.Offset() == offset + measured); Require(recorder.Commands() == commands + 1); if (opcode == Apr::Opcode::Nop) { - std::uint32_t first; - std::memcpy(&first, recorder.memory.data() + offset, sizeof(first)); - Require(first == Apr::NopHeader(static_cast(measured / 4u)) && measured % 4u == 0u); + const std::uint32_t first = HeaderAt(recorder, offset); + Require(Apr::IsNopHeader(first) && Apr::NopDwords(first) * 4u == measured); return; } Apr::CommandHeader header; @@ -344,15 +349,20 @@ void TestNops() { for (std::uint32_t dwords = 1; dwords <= 16; ++dwords) { const auto offset = recorder.Offset(); const auto commands = recorder.Commands(); + std::memset(recorder.memory.data() + offset, 0x5A, dwords * 4u); Require(sceAmprCommandBufferNop(&recorder.buffer, dwords) == 0); RequireAppended(recorder, offset, commands, Apr::Opcode::Nop, sceAmprMeasureCommandSizeNop(dwords)); + Require(HeaderAt(recorder, offset) == (0x5452000Fu | (dwords - 1u) << 8u)); + for (std::size_t index = Apr::NopHeaderBytes; index < dwords * 4u; ++index) Require(recorder.memory[offset + index] == 0x5A); } const auto offset = recorder.Offset(); const auto commands = recorder.Commands(); Require(sceAmprCommandBufferNopWithData(&recorder.buffer, 3, data) == 0); RequireAppended(recorder, offset, commands, Apr::Opcode::Nop, sceAmprMeasureCommandSizeNopWithData(4)); + Require(HeaderAt(recorder, offset) == 0x5452030Fu); Require(std::memcmp(recorder.memory.data() + offset + Apr::NopHeaderBytes, data, sizeof(data)) == 0); Require(sceAmprCommandBufferNopWithData(&recorder.buffer, 0, nullptr) == 0); + Require(HeaderAt(recorder, offset + 16u) == 0x5452000Fu); const auto emptyPayloadOffset = recorder.Offset(); std::memset(recorder.memory.data() + emptyPayloadOffset, 0x5A, sceAmprMeasureCommandSizeNopWithData(4)); Require(sceAmprCommandBufferNopWithData(&recorder.buffer, 3, nullptr) == 0); @@ -365,6 +375,14 @@ void TestNops() { Require(sceAmprCommandBufferNopWithData(&recorder.buffer, 16, data) == invalidArgument); Require(sceAmprMeasureCommandSizeNop(0) == rejected && sceAmprMeasureCommandSizeNop(17) == rejected); Require(sceAmprMeasureCommandSizeNopWithData(0) == rejected && sceAmprMeasureCommandSizeNopWithData(17) == rejected); + + Apr::CommandBufferObject unbound{}; + Require(sceAmprCommandBufferConstructor(&unbound) == 0); + Require(sceAmprCommandBufferNop(&unbound, 0) == permissionDenied); + Require(sceAmprCommandBufferNop(&unbound, 17) == permissionDenied); + Require(sceAmprCommandBufferNopWithData(&unbound, 16, data) == permissionDenied); + Require(sceAmprCommandBufferConstructNop(&unbound, 0, data, 61, nullptr) == permissionDenied); + Require(unbound.offset == 0 && unbound.numCommands == 0); } void TestNopPadding() { @@ -515,9 +533,16 @@ void TestConstructed() { auto commands = recorder.Commands(); Require(sceAmprCommandBufferConstructNop(&recorder.buffer, 7, payload, sizeof(payload), &word) == 0); RequireAppended(recorder, offset, commands, Apr::Opcode::Nop, sceAmprMeasureCommandSizeNopWithData(4)); + Require(HeaderAt(recorder, offset) == 0x5452730Fu); const std::uint8_t* data = recorder.memory.data() + offset + Apr::NopHeaderBytes; const std::uint8_t padding[3] = {}; Require(std::memcmp(data, &word, sizeof(word)) == 0 && std::memcmp(data + 4, payload, sizeof(payload)) == 0 && std::memcmp(data + 9, padding, sizeof(padding)) == 0); + offset = recorder.Offset(); + Require(sceAmprCommandBufferConstructNop(&recorder.buffer, 0x15, payload, 4, nullptr) == 0); + Require(HeaderAt(recorder, offset) == 0x5452510Fu); + offset = recorder.Offset(); + Require(sceAmprCommandBufferConstructNop(&recorder.buffer, -1, payload, 4, nullptr) == 0); + Require(HeaderAt(recorder, offset) == 0x5452F10Fu); const std::array large{}; offset = recorder.Offset(); @@ -711,11 +736,11 @@ void TestMalformedCommandSizes() { requireHeaderFailure(Apr::Opcode::WriteAddress, sizeof(Apr::CommandHeader), sizeof(Apr::CommandHeader), "APR: truncated command payload"); requireHeaderFailure(Apr::Opcode::PopMarker, std::numeric_limits::max(), 2u * sizeof(Apr::CommandHeader), "APR: malformed command"); - const std::uint32_t emptyNop = Apr::NopHeader(0); - requireFailure(&emptyNop, sizeof(emptyNop), sizeof(emptyNop), "APR: malformed command"); - const std::uint32_t overflowingNop = Apr::NopHeader(0xffffffu); - requireFailure(&overflowingNop, sizeof(overflowingNop), 2u * sizeof(overflowingNop), "APR: malformed command"); - const std::uint32_t nops[2] = {Apr::NopHeader(1), Apr::NopHeader(1)}; + const std::uint32_t overflowingNop = Apr::NopHeader(15); + requireFailure(&overflowingNop, sizeof(overflowingNop), sizeof(overflowingNop), "APR: malformed command"); + const Apr::CommandHeader eightByteNop{Apr::Opcode::Nop, sizeof(Apr::CommandHeader)}; + requireFailure(&eightByteNop, sizeof(eightByteNop), sizeof(eightByteNop), "APR: unknown opcode 0"); + const std::uint32_t nops[2] = {Apr::NopHeader(0), Apr::NopHeader(0)}; requireFailure(nops, sizeof(nops), 7u, "APR: truncated command buffer"); std::array memory{}; diff --git a/docs/dev/debt/libSceAmpr.md b/docs/dev/debt/libSceAmpr.md index 3c0545224b..115bc092b3 100644 --- a/docs/dev/debt/libSceAmpr.md +++ b/docs/dev/debt/libSceAmpr.md @@ -5,7 +5,7 @@ - [AMPR command buffer null arguments](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - the result for a null command buffer is unknown for `sceAmprCommandBufferConstructor`, `sceAmprAprCommandBufferConstructor`, `sceAmprCommandBufferReset` and `sceAmprCommandBufferSetBuffer`; these return `SCE_KERNEL_ERROR_EINVAL` here, but the behavior has not been confirmed on a PS5. - [AMPR WriteAddressFromCounterPairOnCompletion](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - the pair of an even counter n is assumed to give counter n in the low 32 bits and n + 1 in the high 32 bits. Wait compare values, wait flush and counter index ranges follow the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation; the flush is accepted and has no effect, as commands run in order - [AMPR command buffer offsets](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - recording with a current offset greater than the bound memory size throws before calculating remaining capacity or writing a command. This validates the emulator's own command-buffer encoding; the console's response to a corrupted opaque buffer object is unknown. -- [AMPR command sizes](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - commands other than `Nop` use the emulator's own encoding with an 8-byte header, so their measured sizes are not the console's. `Nop(n)` occupies n dwords and `NopWithData(n)` (n + 1) dwords, as on the console, through a one-dword header; the dword packing itself is AnyPS5 internal. The at-start flag of the `_04_00` writes is ignored; commands run in order, each after the previous one completes +- [AMPR command sizes](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - commands other than `Nop` use the emulator's own encoding with an 8-byte header, so their measured sizes are not the console's. `Nop(n)` occupies n dwords and `NopWithData(n)` (n + 1) dwords with the header word `0x5452000F | payloadDwords << 8 | (type & 0xF) << 12`, as observed from firmware 13.42's `libSceAmpr.sprx` (#10936); a null `base` is reported before the count or payload range. Two console behaviours are not reproduced: `NopWithData` with data and `ConstructNop` write four zero bytes past the end of the record, even past the end of the buffer, which AnyPS5 does not do; and the position of `ConstructNop`'s `word` relative to its payload has not been observed, so the word is written first. The at-start flag of the `_04_00` writes is ignored; commands run in order, each after the previous one completes - [APR map begin and end](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - as in ampr_emu, `MapBegin`/`MapDirectBegin` map when they run and `MapEnd` only closes the region; while a region is open a second begin and every completion write return `EPERM`, the at-start `_04_00` writes are accepted - [AMPR markers](../../../core/libs/prx/libSceAmpr/Export.cpp): colors are not recorded and command sizes use the host encoding. Native signatures confirm that set takes a color pointer and push/measure take the color value. - [__cxa_call_unexpected](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - takes the in-flight exception's unwind header and never returns, as [libcxxabi 18.1.8](https://github.com/llvm/llvm-project/blob/3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff/libcxxabi/src/cxa_personality.cpp#L1171-L1211) does. No `std::set_unexpected` is exported and the exception header's unexpected handler is never set, so the handler is always the default one, which calls `std::terminate` ([cxa_default_handlers.cpp](https://github.com/llvm/llvm-project/blob/3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff/libcxxabi/src/cxa_default_handlers.cpp#L88-L99)); it therefore forwards to libc's `__cxa_call_terminate`, which begins the catch and runs the current terminate handler. That matches libcxxabi except for which terminate handler runs: there `std::terminate` after `__cxa_begin_catch` ([cxa_exception.cpp L470-L477](https://github.com/llvm/llvm-project/blob/3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff/libcxxabi/src/cxa_exception.cpp#L470-L477)) runs the handler saved in the caught exception's header ([cxa_handlers.cpp L72-L93](https://github.com/llvm/llvm-project/blob/3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff/libcxxabi/src/cxa_handlers.cpp#L72-L93)), which `__cxa_throw` and `__cxa_init_primary_exception` set to `std::get_terminate()` when the exception is thrown ([cxa_exception.cpp L209-L214](https://github.com/llvm/llvm-project/blob/3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff/libcxxabi/src/cxa_exception.cpp#L209-L214), [L279](https://github.com/llvm/llvm-project/blob/3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff/libcxxabi/src/cxa_exception.cpp#L279)), while here the current global handler runs, as AnyPS5's own `std::terminate` does. The two differ only if `std::set_terminate` was called between the throw and this call. A null header terminates because this emulator's libc `__cxa_begin_catch` terminates on null (libcxxabi does not define that case). Dynamic exception specifications that would let a replaced unexpected handler throw a permitted type are not modelled From dbfde882ddb52d72af05e7e0d19bebe7cb118da9 Mon Sep 17 00:00:00 2001 From: slopgobblercobbler Date: Sun, 25 Oct 2026 18:20:45 +0100 Subject: [PATCH 064/198] fix(agc): validate multi-submit count overflow and null command buffers --- core/libs/prx/libSceAgcDriver/Submit/src/Dcb.cpp | 8 ++++++++ core/libs/prx/libSceAgcDriver/tests/Submit.cpp | 8 ++++++++ docs/dev/debt/libSceAgcDriver.md | 1 + 3 files changed, 17 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Submit/src/Dcb.cpp b/core/libs/prx/libSceAgcDriver/Submit/src/Dcb.cpp index 04702130a0..f07d5c39c5 100644 --- a/core/libs/prx/libSceAgcDriver/Submit/src/Dcb.cpp +++ b/core/libs/prx/libSceAgcDriver/Submit/src/Dcb.cpp @@ -4,6 +4,8 @@ #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include #include +#include +#include #include #include @@ -12,10 +14,16 @@ int AgcDriver::SubmitMany(std::uint32_t* const* addresses, const std::uint32_t* sizes, std::uint32_t count, std::uint32_t queue) { if (count == 0) return 0; + if (count > std::numeric_limits::max() / sizeof(std::uint32_t*)) { + throw std::runtime_error("AGC driver: multi-submit count overflow"); + } if (!addresses || !sizes) throw std::runtime_error("AGC driver: null multi-submit arrays"); AgcDriver::GuestMemory::CheckRange(addresses, count * sizeof(*addresses), alignof(std::uint32_t*)); AgcDriver::GuestMemory::CheckRange(sizes, count * sizeof(*sizes), alignof(std::uint32_t)); for (std::uint32_t i = 0; i < count; ++i) { + if (sizes[i] != 0 && addresses[i] == nullptr) { + throw std::runtime_error("AGC driver: null multi-submit command buffer"); + } const Packet packet{addresses[i], sizes[i], 0, {}}; AgcDriver::Submit(&packet, queue); } diff --git a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp index d2e874a808..e8d41c4e57 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp @@ -445,6 +445,9 @@ void testMultiAcbs() { check(sceAgcDriverSubmitMultiAcbs(0, nullptr, nullptr, 0) == 0, "empty multi-ACB submit checked its queue"); expectFailure([&] { sceAgcDriverSubmitMultiAcbs(0x20, nullptr, sizes.data(), 1); }); expectFailure([&] { sceAgcDriverSubmitMultiAcbs(0x20, addresses.data(), nullptr, 1); }); + std::array nullAcbs{nullptr}; + std::array nonZeroAcbSizes{4}; + expectFailure([&] { sceAgcDriverSubmitMultiAcbs(0x20, nullAcbs.data(), nonZeroAcbSizes.data(), 1); }); check(value == 0, "a rejected multi-ACB submit ran a command buffer"); for (std::uint32_t queue : {0x20u, 0x57u}) { value = 0; @@ -500,6 +503,11 @@ void testMultiSubmissions() { expectFailure([&] { sceAgcDriverAgrSubmitMultiDcbs(invalidAddresses.data(), nullptr, 1); }); expectFailure([&] { sceAgcDriverSubmitMultiDcbs(invalidAddresses.data(), invalidSizes.data(), 1); }); expectFailure([&] { sceAgcDriverAgrSubmitMultiDcbs(invalidAddresses.data(), invalidSizes.data(), 1); }); + std::array nullAddresses{nullptr}; + std::array nonZeroSizes{4}; + expectFailure([&] { sceAgcDriverSubmitMultiDcbs(nullAddresses.data(), nonZeroSizes.data(), 1); }); + expectFailure([&] { sceAgcDriverAgrSubmitMultiDcbs(nullAddresses.data(), nonZeroSizes.data(), 1); }); + expectFailure([&] { sceAgcDriverSubmitMultiDcbs(invalidAddresses.data(), invalidSizes.data(), std::numeric_limits::max()); }); for (bool agr : {false, true}) { value = 0; done = 0; diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index e715b5b83e..7e761e8899 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -273,3 +273,4 @@ ### Opt-in draw paths - [Draw finisher](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp) (`APS5_DRAW_PIPELINE`) - opt-in. The Linux main-image page table it adds matches what Windows already does by default. +- [Multi-submit count overflow and buffer validation](../../../core/libs/prx/libSceAgcDriver/Submit/src/Dcb.cpp) (libSceAgcDriver) - whether the console driver imposes an architectural submission packet limit or allows null buffer pointers with non-zero sizes in multi-submit arrays is unmeasured; AgcDriver::SubmitMany validates count against 32-bit pointer array bounds overflow and rejects null addresses for non-empty command buffers with a runtime exception. From 0d14468ccc1b1186b60b1fc85c802ace1594ca23 Mon Sep 17 00:00:00 2001 From: Adnan Khan Date: Sun, 25 Oct 2026 22:56:04 +0530 Subject: [PATCH 065/198] fix(libSceAmpr): zero the padding of a constructed Nop payload --- core/libs/prx/libSceAmpr/Export.cpp | 1 + core/libs/tests/GuestAmpr.cpp | 2 ++ docs/dev/debt/libSceAmpr.md | 2 +- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAmpr/Export.cpp b/core/libs/prx/libSceAmpr/Export.cpp index 41456c7650..0ddb53db0a 100644 --- a/core/libs/prx/libSceAmpr/Export.cpp +++ b/core/libs/prx/libSceAmpr/Export.cpp @@ -379,6 +379,7 @@ int APS5_VABI sceAmprCommandBufferConstructNop(Apr::CommandBufferObject* buffer, data += sizeof(*word); } if (payloadBytes != 0u) std::memcpy(data, payload, payloadBytes); + std::memset(data + payloadBytes, 0, (4u - payloadBytes % 4u) % 4u); return 0; } diff --git a/core/libs/tests/GuestAmpr.cpp b/core/libs/tests/GuestAmpr.cpp index 1f1490df6b..2795ed3172 100644 --- a/core/libs/tests/GuestAmpr.cpp +++ b/core/libs/tests/GuestAmpr.cpp @@ -531,12 +531,14 @@ void TestConstructed() { const std::uint32_t word = 0xCAFEF00Du; auto offset = recorder.Offset(); auto commands = recorder.Commands(); + std::memset(recorder.memory.data() + offset, 0x5A, 20); Require(sceAmprCommandBufferConstructNop(&recorder.buffer, 7, payload, sizeof(payload), &word) == 0); RequireAppended(recorder, offset, commands, Apr::Opcode::Nop, sceAmprMeasureCommandSizeNopWithData(4)); Require(HeaderAt(recorder, offset) == 0x5452730Fu); const std::uint8_t* data = recorder.memory.data() + offset + Apr::NopHeaderBytes; const std::uint8_t padding[3] = {}; Require(std::memcmp(data, &word, sizeof(word)) == 0 && std::memcmp(data + 4, payload, sizeof(payload)) == 0 && std::memcmp(data + 9, padding, sizeof(padding)) == 0); + Require(recorder.memory[offset + 16] == 0x5A); offset = recorder.Offset(); Require(sceAmprCommandBufferConstructNop(&recorder.buffer, 0x15, payload, 4, nullptr) == 0); Require(HeaderAt(recorder, offset) == 0x5452510Fu); diff --git a/docs/dev/debt/libSceAmpr.md b/docs/dev/debt/libSceAmpr.md index 115bc092b3..83ecbcb38d 100644 --- a/docs/dev/debt/libSceAmpr.md +++ b/docs/dev/debt/libSceAmpr.md @@ -5,7 +5,7 @@ - [AMPR command buffer null arguments](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - the result for a null command buffer is unknown for `sceAmprCommandBufferConstructor`, `sceAmprAprCommandBufferConstructor`, `sceAmprCommandBufferReset` and `sceAmprCommandBufferSetBuffer`; these return `SCE_KERNEL_ERROR_EINVAL` here, but the behavior has not been confirmed on a PS5. - [AMPR WriteAddressFromCounterPairOnCompletion](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - the pair of an even counter n is assumed to give counter n in the low 32 bits and n + 1 in the high 32 bits. Wait compare values, wait flush and counter index ranges follow the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation; the flush is accepted and has no effect, as commands run in order - [AMPR command buffer offsets](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - recording with a current offset greater than the bound memory size throws before calculating remaining capacity or writing a command. This validates the emulator's own command-buffer encoding; the console's response to a corrupted opaque buffer object is unknown. -- [AMPR command sizes](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - commands other than `Nop` use the emulator's own encoding with an 8-byte header, so their measured sizes are not the console's. `Nop(n)` occupies n dwords and `NopWithData(n)` (n + 1) dwords with the header word `0x5452000F | payloadDwords << 8 | (type & 0xF) << 12`, as observed from firmware 13.42's `libSceAmpr.sprx` (#10936); a null `base` is reported before the count or payload range. Two console behaviours are not reproduced: `NopWithData` with data and `ConstructNop` write four zero bytes past the end of the record, even past the end of the buffer, which AnyPS5 does not do; and the position of `ConstructNop`'s `word` relative to its payload has not been observed, so the word is written first. The at-start flag of the `_04_00` writes is ignored; commands run in order, each after the previous one completes +- [AMPR command sizes](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - commands other than `Nop` use the emulator's own encoding with an 8-byte header, so their measured sizes are not the console's. `Nop(n)` occupies n dwords and `NopWithData(n)` (n + 1) dwords with the header word `0x5452000F | payloadDwords << 8 | (type & 0xF) << 12`, and `ConstructNop` writes its word before the payload and zeroes the payload's padding, as observed from firmware 13.42's `libSceAmpr.sprx` (#10936); a null `base` is reported before the count or payload range. One console behaviour is not reproduced: `NopWithData` with data and `ConstructNop` write four zero bytes right after the payload even where they fall past the end of the record, and of the buffer; AnyPS5 stops at the record. The at-start flag of the `_04_00` writes is ignored; commands run in order, each after the previous one completes - [APR map begin and end](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - as in ampr_emu, `MapBegin`/`MapDirectBegin` map when they run and `MapEnd` only closes the region; while a region is open a second begin and every completion write return `EPERM`, the at-start `_04_00` writes are accepted - [AMPR markers](../../../core/libs/prx/libSceAmpr/Export.cpp): colors are not recorded and command sizes use the host encoding. Native signatures confirm that set takes a color pointer and push/measure take the color value. - [__cxa_call_unexpected](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - takes the in-flight exception's unwind header and never returns, as [libcxxabi 18.1.8](https://github.com/llvm/llvm-project/blob/3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff/libcxxabi/src/cxa_personality.cpp#L1171-L1211) does. No `std::set_unexpected` is exported and the exception header's unexpected handler is never set, so the handler is always the default one, which calls `std::terminate` ([cxa_default_handlers.cpp](https://github.com/llvm/llvm-project/blob/3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff/libcxxabi/src/cxa_default_handlers.cpp#L88-L99)); it therefore forwards to libc's `__cxa_call_terminate`, which begins the catch and runs the current terminate handler. That matches libcxxabi except for which terminate handler runs: there `std::terminate` after `__cxa_begin_catch` ([cxa_exception.cpp L470-L477](https://github.com/llvm/llvm-project/blob/3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff/libcxxabi/src/cxa_exception.cpp#L470-L477)) runs the handler saved in the caught exception's header ([cxa_handlers.cpp L72-L93](https://github.com/llvm/llvm-project/blob/3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff/libcxxabi/src/cxa_handlers.cpp#L72-L93)), which `__cxa_throw` and `__cxa_init_primary_exception` set to `std::get_terminate()` when the exception is thrown ([cxa_exception.cpp L209-L214](https://github.com/llvm/llvm-project/blob/3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff/libcxxabi/src/cxa_exception.cpp#L209-L214), [L279](https://github.com/llvm/llvm-project/blob/3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff/libcxxabi/src/cxa_exception.cpp#L279)), while here the current global handler runs, as AnyPS5's own `std::terminate` does. The two differ only if `std::set_terminate` was called between the throw and this call. A null header terminates because this emulator's libc `__cxa_begin_catch` terminates on null (libcxxabi does not define that case). Dynamic exception specifications that would let a replaced unexpected handler throw a permitted type are not modelled From c5dd9f9cbce1d90cde2d2c07109d1689596736b1 Mon Sep 17 00:00:00 2001 From: orezraey Date: Sun, 25 Oct 2026 15:06:45 -0300 Subject: [PATCH 066/198] fix(libSceRtc): check the seconds before the pointer in sceRtcSetTime_t as measured on a PS5 On PS5 firmware 13.60 sceRtcSetTime_t(NULL, -1) returns SCE_RTC_ERROR_INVALID_VALUE and sceRtcSetTime_t(NULL, 0) SCE_RTC_ERROR_INVALID_POINTER: negative seconds are rejected before a null destination under the later SDK rules. The other measured GetDayOfWeek, SetTime_t and FormatRFC3339 results already matched. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceRtc/Export.cpp | 3 ++- core/libs/tests/GuestRtc.cpp | 2 ++ docs/dev/debt/libSceRtc.md | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceRtc/Export.cpp b/core/libs/prx/libSceRtc/Export.cpp index 91c6591395..4362736ee4 100644 --- a/core/libs/prx/libSceRtc/Export.cpp +++ b/core/libs/prx/libSceRtc/Export.cpp @@ -481,12 +481,13 @@ int APS5_VABI sceRtcGetTime_t(const RtcDateTime* time, int64_t* seconds) { } int APS5_VABI sceRtcSetTime_t(RtcDateTime* time, int64_t seconds) { - if (!time) return SCE_RTC_ERROR_INVALID_POINTER; if (rangeChecked()) { if (seconds < 0) return SCE_RTC_ERROR_INVALID_VALUE; + if (!time) return SCE_RTC_ERROR_INVALID_POINTER; *time = fromTick(UNIX_EPOCH_TICK + static_cast(seconds) * TICKS_PER_SECOND); return 0; } + if (!time) return SCE_RTC_ERROR_INVALID_POINTER; *time = fromTick(UNIX_EPOCH_TICK + static_cast(static_cast(seconds)) * TICKS_PER_SECOND); return 0; } diff --git a/core/libs/tests/GuestRtc.cpp b/core/libs/tests/GuestRtc.cpp index 8d9b302b49..800f4918c5 100644 --- a/core/libs/tests/GuestRtc.cpp +++ b/core/libs/tests/GuestRtc.cpp @@ -944,6 +944,8 @@ int main(int argc, char** argv) { RtcDateTime sentinel{0xaaaa, 0xaaaa, 0xaaaa, 0xaaaa, 0xaaaa, 0xaaaa, 0xaaaaaaaau}; RtcDateTime ranged = sentinel; Require(sceRtcSetTime_t(&ranged, -1) == invalidValue && Equal(ranged, sentinel)); + Require(sceRtcSetTime_t(nullptr, -1) == invalidValue && sceRtcSetTime_t(nullptr, std::numeric_limits::min()) == invalidValue); + Require(sceRtcSetTime_t(nullptr, 0) == static_cast(0x80B50002) && sceRtcSetTime_t(nullptr, 1) == static_cast(0x80B50002)); Require(sceRtcSetTime_t(&ranged, std::numeric_limits::min()) == invalidValue && Equal(ranged, sentinel)); Require(sceRtcSetTime_t(&ranged, 4294967296ll) == 0 && Equal(ranged, RtcDateTime{2106, 2, 7, 6, 28, 16, 0})); Require(sceRtcSetTime_t(&ranged, 253402300800ll) == 0 && Equal(ranged, RtcDateTime{10000, 1, 1, 0, 0, 0, 0})); diff --git a/docs/dev/debt/libSceRtc.md b/docs/dev/debt/libSceRtc.md index 12645457b4..c220bd9f30 100644 --- a/docs/dev/debt/libSceRtc.md +++ b/docs/dev/debt/libSceRtc.md @@ -8,7 +8,7 @@ - [sceRtcParseDateTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - a blank zone after seconds is accepted. With fully allocated input storage on PS5 firmware 13.42 and again on 13.60, the same NUL-terminated text returned `BAD_PARSE` when the byte after its terminator was zero and succeeded when that byte was uppercase `T`. This dependence on storage after the terminator is not modeled; controlled backing-byte measurements in a title process are unavailable. - [sceRtcFormatRFC2822, sceRtcFormatRFC2822LocalTime, sceRtcFormatRFC3339LocalTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - measured on PS4 firmware 10.71, not on a PS5: the RFC 2822 text, `INVALID_YEAR` when the zone-shifted tick is past 9999 as a signed value, otherwise `INVALID_VALUE` when the source tick or the zone-shifted tick is negative as a signed value, and the local-time variants using the offset in effect at the given instant (+01:00 in January, +02:00 in July on a Central European console). A null tick formatting the current time and the ±1439 minute offset limit come from the PS5 measurement in the next entry and are not measured on the PS4 -- [sceRtcFormatRFC3339, sceRtcGetDayOfWeek, sceRtcSetTime_t](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - measured on PS4 firmware 10.71 from processes compiled with SDK versions 0x01000051, 0x02ffffff, 0x03000000 and 0x05050001, not on a PS5. From 0x03000000 on: `FormatRFC3339` returns `INVALID_VALUE` when adding the zone shift to the tick overflows or underflows 64 bits (earlier SDKs wrap); `GetDayOfWeek` checks the month before the year and rejects a year past 9999 (earlier SDKs check the year first, with no upper bound); `SetTime_t` rejects negative seconds with `INVALID_VALUE` and converts all 64 bits (earlier SDKs take the low 32 bits). 0x02ffffff still behaves the earlier way and 0x03000000 the later way. The earlier rules apply only to a PS4 title (a CUSA title ID): a PS5 title has its own SDK numbering, so it always gets the later rules, the behaviour main had before; those are not measured on a PS5. +- [sceRtcFormatRFC3339, sceRtcGetDayOfWeek, sceRtcSetTime_t](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - measured on PS4 firmware 10.71 from processes compiled with SDK versions 0x01000051, 0x02ffffff, 0x03000000 and 0x05050001, not on a PS5. From 0x03000000 on: `FormatRFC3339` returns `INVALID_VALUE` when adding the zone shift to the tick overflows or underflows 64 bits (earlier SDKs wrap); `GetDayOfWeek` checks the month before the year and rejects a year past 9999 (earlier SDKs check the year first, with no upper bound); `SetTime_t` rejects negative seconds with `INVALID_VALUE` and converts all 64 bits (earlier SDKs take the low 32 bits). 0x02ffffff still behaves the earlier way and 0x03000000 the later way. The earlier rules apply only to a PS4 title (a CUSA title ID): a PS5 title has its own SDK numbering, so it always gets the later rules, which PS5 firmware 13.60 follows; there `SetTime_t` checks for negative seconds before a null pointer. - [sceRtcGetDosTime, sceRtcSetDosTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - bit layout from shadPS4 (PS4, the standard FAT date/time). A year outside 1980-2107 makes `GetDosTime` return `INVALID_YEAR` after writing 0 for an earlier one and `0xff9fbf7d` (2107-12-31 23:59:58, the largest encodable time) for a later one, and `SetDosTime` decodes the year as the unsigned 7-bit FAT field (1980-2107) without validating the fields, as the disassembly-derived [fpPS4](https://github.com/red-prig/fpPS4) reimplementation of the PS4 library does; not confirmed on a PS5 title, and shadPS4 instead masks out-of-range years into the packed value and sign-extends the 16-bit date - [sceRtcFormatRFC2822LocalTime, sceRtcFormatRFC3339LocalTime](../../../core/libs/prx/libSceRtc/Export.cpp): the native wrappers obtain the zone from a kernel timezone query, while the host uses the offset at the supplied instant. The kernel timezone configuration and its failure paths are not modeled. Native formatting confirms that null tick pointers select current time, RFC3339 uses two fractional digits, and explicit offsets are limited to -1439 through 1439 minutes. - [sceRtcGetCurrentRawNetworkTick](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - the console reads clock 0x13 and returns its time plus the Unix epoch in ticks, or `0x80B50001` when the clock is unsupported (errno 5). The raw network clock is not modelled; this returns the host clock. Read from the disassembly of libSceRtc.sprx on firmware 13.60, not measured on a console From 69ead78b5ea6190a419345b25cd03e51ed85a609 Mon Sep 17 00:00:00 2001 From: Gruzzberry Date: Sun, 25 Oct 2026 16:01:43 +0000 Subject: [PATCH 067/198] feat(libkernel_sys): implement profil argument checks Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel_sys/Export.Discovery.cpp | 5 ----- core/libs/prx/libkernel_sys/Export.cpp | 10 ++++++++++ core/libs/prx/libkernel_sys/include/Api.hpp | 2 +- core/libs/tests/GuestKernelSys.cpp | 4 ++++ 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libkernel_sys/Export.Discovery.cpp b/core/libs/prx/libkernel_sys/Export.Discovery.cpp index 73945eb7ce..52aa1272d5 100644 --- a/core/libs/prx/libkernel_sys/Export.Discovery.cpp +++ b/core/libs/prx/libkernel_sys/Export.Discovery.cpp @@ -262,11 +262,6 @@ int APS5_VABI openintr_nid_postfix(void) { return 0; } -int APS5_VABI profil_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI pthread_attr_getaffinity_np_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index 4125e11eea..91becfb41a 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -333,6 +333,16 @@ int APS5_VABI setsid_nid_postfix(void) { return static_cast(Fail(GuestEperm)); } +int APS5_VABI profil_nid_postfix(void* samples, std::size_t size, std::size_t offset, unsigned int scale) { + (void)samples; + (void)size; + (void)offset; + constexpr unsigned int MaxScale = 1u << 16; + if (scale > MaxScale) return static_cast(Fail(GuestEinval)); + if (scale != 0) NotImplemented_nid_no_patch("profil: the profiling clock"); + return 0; +} + std::uint16_t APS5_VABI umask_nid_postfix(std::uint16_t mask) { return File::ExchangeCreationMask_nid_no_patch(mask); } diff --git a/core/libs/prx/libkernel_sys/include/Api.hpp b/core/libs/prx/libkernel_sys/include/Api.hpp index 0504ad157b..c1a45cb3d8 100644 --- a/core/libs/prx/libkernel_sys/include/Api.hpp +++ b/core/libs/prx/libkernel_sys/include/Api.hpp @@ -119,7 +119,7 @@ int APS5_VABI mdbg_call_nid_postfix(void); int APS5_VABI mdbg_service_nid_postfix(void); int APS5_VABI mount_nid_postfix(void); int APS5_VABI openintr_nid_postfix(void); -int APS5_VABI profil_nid_postfix(void); +int APS5_VABI profil_nid_postfix(void* samples, std::size_t size, std::size_t offset, unsigned int scale); int APS5_VABI pthread_attr_getaffinity_np_nid_postfix(void); int APS5_VABI pthread_attr_getsolosched_np_nid_postfix(void); int APS5_VABI pthread_attr_setaffinity_np_nid_postfix(void); diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index 81997c91e1..f378499610 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -530,6 +530,10 @@ int main() { #endif CheckProcessQueries(); Require(setsid_nid_postfix() == -1 && Error() == 1); + Require(profil_nid_postfix(nullptr, 0, 0, (1u << 16) + 1) == -1 && Error() == 22); + Require(profil_nid_postfix(nullptr, 0, 0, 0xFFFFFFFFu) == -1 && Error() == 22); + Require(profil_nid_postfix(reinterpret_cast(0x10), 4096, 0, 0) == 0); + Require(profil_nid_postfix(nullptr, 0, 0, 0) == 0); Require(umask_nid_postfix(0) == 022); Require(umask_nid_postfix(07777) == 0); Require(umask_nid_postfix(022) == 07777); From 2a6e50b70645b07d1677154032bf53f36c648580 Mon Sep 17 00:00:00 2001 From: pushkarreddyy <212133856+pushkarreddyy@users.noreply.github.com> Date: Sun, 25 Oct 2026 21:40:40 +0530 Subject: [PATCH 068/198] test(libkernel): verify append vector writes fail with EFAULT on unreadable buffers on Windows --- core/libs/tests/GuestAppendWrites.cpp | 34 +++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/core/libs/tests/GuestAppendWrites.cpp b/core/libs/tests/GuestAppendWrites.cpp index 806611cae1..353fffb2f0 100644 --- a/core/libs/tests/GuestAppendWrites.cpp +++ b/core/libs/tests/GuestAppendWrites.cpp @@ -1,5 +1,6 @@ #include "prx/libkernel/include/Api.hpp" #include "prx/libkernel/File/include/File.hpp" +#include "prx/libc/include/Api.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include #include @@ -13,6 +14,9 @@ #include #include #include +#ifdef _WIN32 +#include +#endif static void Check(bool condition, int line) { if (!condition) { @@ -110,6 +114,36 @@ int main() { Require(appended.substr(0, LineLength) == head); Require(appended.substr(static_cast(size) + LineLength) == positioned + gathered); +#ifdef _WIN32 + void* unreadable = VirtualAlloc(nullptr, 4096, MEM_COMMIT | MEM_RESERVE, PAGE_NOACCESS); + Require(unreadable != nullptr); + char validChar = 'x'; + const KernelIovec badPieces[2]{{&validChar, 1}, {unreadable, 10}}; + const KernelIovec badPiecesFirst[2]{{unreadable, 10}, {&validChar, 1}}; + const KernelIovec badPiecesZeroLength[2]{{unreadable, 0}, {&validChar, 1}}; + + *__error_nid_postfix() = 0; + Require(writev_nid_postfix(first, badPieces, 2) == -1 && *__error_nid_postfix() == 14); + *__error_nid_postfix() = 0; + Require(pwritev_nid_postfix(first, badPieces, 2, 0) == -1 && *__error_nid_postfix() == 14); + Require(sceKernelWritev(first, badPieces, 2) == static_cast(0x8002000eu)); + Require(sceKernelPwritev(first, badPieces, 2, 0) == static_cast(0x8002000eu)); + + *__error_nid_postfix() = 0; + Require(writev_nid_postfix(first, badPiecesFirst, 2) == -1 && *__error_nid_postfix() == 14); + *__error_nid_postfix() = 0; + Require(pwritev_nid_postfix(first, badPiecesFirst, 2, 0) == -1 && *__error_nid_postfix() == 14); + Require(sceKernelWritev(first, badPiecesFirst, 2) == static_cast(0x8002000eu)); + Require(sceKernelPwritev(first, badPiecesFirst, 2, 0) == static_cast(0x8002000eu)); + + Require(Contents().size() == static_cast(size) + 3 * LineLength); + + Require(writev_nid_postfix(first, badPiecesZeroLength, 2) == 1); + Require(Contents().size() == static_cast(size) + 3 * LineLength + 1); + + VirtualFree(unreadable, 0, MEM_RELEASE); +#endif + for (const int fd : descriptors) Require(sceKernelClose(fd) == 0); std::remove(Path); } From 159007bd43ad08108862f4fb19cb327cf5b063b6 Mon Sep 17 00:00:00 2001 From: Emanuele <118837721+imellag@users.noreply.github.com> Date: Sun, 25 Oct 2026 19:58:46 +0100 Subject: [PATCH 069/198] perf(agc): copy moved draw words into the draw upload ring --- .../Graphics/include/ShaderResources.hpp | 8 -------- .../Graphics/src/ShaderResources.cpp | 20 +++++-------------- .../prx/libSceAgcDriver/tests/Graphics.cpp | 6 +++--- 3 files changed, 8 insertions(+), 26 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index c140e57ec6..3c0fcb13e5 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -596,14 +596,6 @@ private: std::vector descriptorBuffers; std::vector descriptorImages; std::vector descriptorWrites; - struct MovedWordBuffer { - std::size_t allocation; - std::vector words; - std::size_t size; - std::shared_ptr buffer; - }; - mutable std::vector movedWordBuffers; - mutable std::size_t movedWordBufferNext = 0; mutable std::vector drawPoolSizes; // Guest buffer elements bound read-only: each use of this object skips that many pending-write // notes (counted in MarkGpuWrites for the [buffers] line). diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index f73b2c2b0e..26df014cf3 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -5628,23 +5628,13 @@ std::shared_ptr ShaderResources::PrepareDrawBindi const auto& item = allocations[index]; const auto override = std::find_if(moved.begin(), moved.end(), [&](const MovedBuffer& entry) { return entry.allocation == index; }); if (override != moved.end() && !override->words.empty()) { - constexpr std::size_t MovedWordBufferSlots = 8; - const auto held = std::find_if(movedWordBuffers.begin(), movedWordBuffers.end(), [&](const MovedWordBuffer& entry) { return entry.allocation == index && entry.size == override->size && std::ranges::equal(entry.words, override->words); }); - std::shared_ptr buffer; - if (held != movedWordBuffers.end()) { - buffer = held->buffer; - } else { - buffer = std::make_shared(context, override->size, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); - std::memcpy(buffer->Bytes().data(), override->words.data(), override->size); - for (const auto& patch : dataPatches) { - if (patch.allocation == index && patch.byte < override->size) buffer->Bytes()[patch.byte] = static_cast(patch.adjustment); - } - MovedWordBuffer entry{index, {override->words.begin(), override->words.end()}, override->size, buffer}; - if (movedWordBuffers.size() < MovedWordBufferSlots) movedWordBuffers.push_back(std::move(entry)); - else movedWordBuffers[movedWordBufferNext++ % MovedWordBufferSlots] = std::move(entry); + auto slice = recorder.DrawUpload(override->size); + std::memcpy(slice.bytes, override->words.data(), override->size); + for (const auto& patch : dataPatches) { + if (patch.allocation == index && patch.byte < override->size) slice.bytes[patch.byte] = static_cast(patch.adjustment); } selected.push_back(index); - keep({0, std::move(buffer), 0, override->size}); + keep({0, std::move(slice.buffer), slice.offset, override->size}); continue; } std::uint64_t address = item.address; diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 632b31720d..9517723643 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -6076,8 +6076,8 @@ void descriptorSnapshotTests() { Require(descriptor.buffer == original[index].buffer && descriptor.offset == original[index].offset && descriptor.range == original[index].range, "snapshot update changed an untouched buffer descriptor"); } else { const auto& item = snapshots->snapshots[snapshot]; - Require(descriptor.buffer == item.buffer->Handle() && descriptor.offset == 0 && descriptor.range == moved[snapshot].size, "snapshot descriptor used another element's buffer or range"); - Require(sameBytes(bufferBytes(descriptor.buffer), moved[snapshot].words.data(), moved[snapshot].size), "snapshot descriptor lost its buffer data"); + Require(descriptor.buffer == item.buffer->Handle() && descriptor.offset == item.offset && descriptor.range == moved[snapshot].size, "snapshot descriptor used another element's buffer or range"); + Require(sameBytes(bufferBytes(descriptor.buffer).subspan(descriptor.offset), moved[snapshot].words.data(), moved[snapshot].size), "snapshot descriptor lost its buffer data"); ++snapshot; } } @@ -6677,7 +6677,7 @@ void imageTableSnapshotAllocationTests() { const auto snapshots = resources.PrepareDrawBindings(recorder, moved); Require(snapshots != nullptr && snapshots->snapshots.size() == 1 && cache.Counters().sets == sets + 1, "an empty image table prevented draw snapshot allocation"); Require(mock.variableDescriptorCount == 1u, "an empty draw snapshot table did not reserve one physical image descriptor"); - Require(sameBytes(bufferBytes(snapshots->snapshots[0].buffer->Handle()), moved[0].words.data(), moved[0].size), "an empty image table changed draw snapshot contents"); + Require(sameBytes(bufferBytes(snapshots->snapshots[0].buffer->Handle()).subspan(snapshots->snapshots[0].offset), moved[0].words.data(), moved[0].size), "an empty image table changed draw snapshot contents"); const auto& currentMap = findWrite(map.binding).buffers.at(0); Require(currentMap.buffer == originalMap.buffer && currentMap.offset == originalMap.offset && currentMap.range == originalMap.range, "draw snapshots replaced the empty image table map"); std::array header{}; From ffe1b52b4419d3be25bbb6f1d2ad057c9e7cfa34 Mon Sep 17 00:00:00 2001 From: mzaeemnasir Date: Sun, 25 Oct 2026 14:30:36 +0000 Subject: [PATCH 070/198] fix(relinker): collect reachable branch targets without signed overflow Code analysis added a branch displacement to the next instruction's address as std::int64_t. For a code segment mapped across 2^63 the sum overflowed. Add it as an unsigned value, as the function-symbol sweep in the same collector already does. --- .../src/analysis/CodeInstructionCollector.cpp | 2 +- .../relinker/tests/ConversionTests.cpp | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp index 2e7d216a7f..c27d5a72ac 100644 --- a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp +++ b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp @@ -496,7 +496,7 @@ std::set CodeInstructionCollector::Collect(const std::ve const auto offset = address - header.MappedAddress; const auto info = decoder.DecodeInstruction(text.data() + offset, text.size() - offset); addAddressTakenRoot(address, text.data() + offset, info); - if (info.HasBranchTarget && !info.HasRipRelativeDisp) addRoot(static_cast(static_cast(address + info.Length) + info.BranchDisp)); + if (info.HasBranchTarget && !info.HasRipRelativeDisp) addRoot(address + info.Length + static_cast(info.BranchDisp)); } } } while (roots.size() != previousRoots); diff --git a/core/relinker/relinker/tests/ConversionTests.cpp b/core/relinker/relinker/tests/ConversionTests.cpp index 4b27916736..9e809d38bb 100644 --- a/core/relinker/relinker/tests/ConversionTests.cpp +++ b/core/relinker/relinker/tests/ConversionTests.cpp @@ -226,6 +226,25 @@ void scanning() { require(progress.Events.back().Completed < progress.Events.back().Total, "Failed scan reported completion"); } +void branchAcrossSignedBoundary() { + std::vector image(64 + 32, 0x90); + image[64] = 0xc3; + const std::array jump{0xe9, 0xeb, 0xff, 0xff, 0xff}; + std::copy(jump.begin(), jump.end(), image.begin() + 64 + 16); + image.back() = 0xc3; + Domain::ProgramHeader header{}; + header.Type = 1; + header.Flags = 1; + header.Offset = 64; + header.MappedAddress = 0x7ffffffffffffff0; + header.FileSize = image.size() - 64; + header.MemorySize = header.FileSize; + Io::WriteU64(image, 24, 0x8000000000000000); + const auto instructions = Relinker::CodeInstructionCollector().Collect(image, {header}); + require(instructions.contains(0x8000000000000000), "Lost the entry above the signed boundary"); + require(instructions.contains(0x7ffffffffffffff0), "Lost the branch target below the signed boundary"); +} + class FailingProgress final : public Domain::IProgress { public: std::string InputPath; @@ -380,6 +399,7 @@ int main() { runtimeLifecyclePolicy(); fixedBase(); scanning(); + branchAcrossSignedBoundary(); diagnosticContext(); cancellation(); baseRelocations(); From 96794cc8da2e389b6cca95d3d6c8936b874872e6 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Mon, 26 Oct 2026 09:41:47 +1300 Subject: [PATCH 071/198] fix(freetype): declare FT_Bitmap_Init once in the public header FT_Bitmap_Init_nid_postfix was only declared locally in GuestFreeTypeBitmapInit.cpp; it is now declared in libSceFreeType Api.hpp next to FT_Bitmap_New with the signature the implementation uses, and the test includes the header. --- core/libs/prx/libSceFreeType/include/Api.hpp | 1 + core/libs/tests/GuestFreeTypeBitmapInit.cpp | 5 +---- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceFreeType/include/Api.hpp b/core/libs/prx/libSceFreeType/include/Api.hpp index edf90a2637..01ff78e37d 100644 --- a/core/libs/prx/libSceFreeType/include/Api.hpp +++ b/core/libs/prx/libSceFreeType/include/Api.hpp @@ -56,6 +56,7 @@ void APS5_VABI FT_Outline_Reverse_nid_postfix(FreeType::GuestOutline* outline); int APS5_VABI FT_Request_Size_nid_postfix(FreeType::GuestFaceRec* face, const FreeType::GuestSizeRequestRec* request); void APS5_VABI FT_Bitmap_New_nid_postfix(FreeType::GuestBitmap* bitmap); +void APS5_VABI FT_Bitmap_Init_nid_postfix(FreeType::GuestBitmap* bitmap); int APS5_VABI FT_Bitmap_Copy_nid_postfix(FreeType::GuestLibrary* library, const FreeType::GuestBitmap* source, FreeType::GuestBitmap* target); int APS5_VABI FT_Bitmap_Convert_nid_postfix(FreeType::GuestLibrary* library, const FreeType::GuestBitmap* source, FreeType::GuestBitmap* target, std::int32_t alignment); int APS5_VABI FT_Bitmap_Done_nid_postfix(FreeType::GuestLibrary* library, FreeType::GuestBitmap* bitmap); diff --git a/core/libs/tests/GuestFreeTypeBitmapInit.cpp b/core/libs/tests/GuestFreeTypeBitmapInit.cpp index 0671beafd3..f9b13b1c23 100644 --- a/core/libs/tests/GuestFreeTypeBitmapInit.cpp +++ b/core/libs/tests/GuestFreeTypeBitmapInit.cpp @@ -1,4 +1,5 @@ #include "FontFixture.hpp" +#include "prx/libSceFreeType/include/Api.hpp" #include "prx/libSceFreeType/include/GuestObjects.hpp" #include "prx/libc/include/general/VabiMacros.hpp" @@ -11,10 +12,6 @@ using namespace FreeType; -extern "C" { -void APS5_VABI FT_Bitmap_Init_nid_postfix(GuestBitmap*); -} - static void Check(bool value, int line) { if (!value) { std::fprintf(stderr, "FreeType bitmap init check failed at line %d\n", line); From 3436e8fe7ea2789b7d780a8495d8079eea2dc508 Mon Sep 17 00:00:00 2001 From: p3nicillin Date: Sun, 25 Oct 2026 21:04:17 +0000 Subject: [PATCH 072/198] fix(libScePad): refuse scePadGetExtControllerInformation on a standard pad On a standard-port handle the console answers NOT_PERMITTED (0x80920101): Halo CE Universal's pad probe measured it on firmware 13.59 for two connected DualSenses in three runs. The function returned 0 with invented controller information instead. The special port keeps its zeroed answer. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libScePad/Export.cpp | 13 ++----------- core/libs/tests/GuestPad.cpp | 20 +++++++------------- docs/dev/debt/libScePad.md | 1 + 3 files changed, 10 insertions(+), 24 deletions(-) diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index 0787a07dbf..4a6ac13c18 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -37,6 +37,7 @@ constexpr int PAD_ERROR_ALREADY_OPENED = static_cast(0x80920004); constexpr int PAD_ERROR_UNKNOWN_USER = static_cast(0x809b0081); constexpr int PAD_ERROR_INVALID_USER_ID = static_cast(0x809b0001); constexpr int PAD_ERROR_INVALID_PORT_INDEX = static_cast(0x809b0003); +constexpr int PAD_ERROR_NOT_PERMITTED = static_cast(0x80920101); constexpr int PAD_USER_ID_INVALID = -1; std::array g_opened{false}; @@ -228,18 +229,8 @@ int APS5_VABI scePadGetTriggerEffectState(int handle, PadTriggerEffectStateInfor int APS5_VABI scePadGetExtControllerInformation(int handle, void* info) { if (!KnownHandle(handle)) return PAD_ERROR_INVALID_HANDLE; if (info == nullptr) return PAD_ERROR_INVALID_ARG; + if (!SpecialHandle(handle)) return PAD_ERROR_NOT_PERMITTED; std::memset(info, 0, 0x2c); - if (SpecialHandle(handle)) return PAD_OK; - PadControllerInformation* base = static_cast(info); - base->touchPadInfo.pixelDensity = 44.86f; - base->touchPadInfo.resolution.x = kTouchPadWidth; - base->touchPadInfo.resolution.y = kTouchPadHeight; - base->stickInfo.deadZoneLeft = 13; - base->stickInfo.deadZoneRight = 13; - base->connectionType = PAD_CONNECTION_TYPE_LOCAL; - base->connectedCount = 1; - base->connected = true; - base->deviceClass = PAD_DEVICE_CLASS_STANDARD; return PAD_OK; } diff --git a/core/libs/tests/GuestPad.cpp b/core/libs/tests/GuestPad.cpp index beab5d7f6a..27e840e71b 100644 --- a/core/libs/tests/GuestPad.cpp +++ b/core/libs/tests/GuestPad.cpp @@ -130,11 +130,6 @@ static void CheckStickDeadZones(int handle) { PadControllerInformation info{}; Require(scePadGetControllerInformation(handle, &info) == PAD_OK); Require(info.stickInfo.deadZoneLeft == 13 && info.stickInfo.deadZoneRight == 13); - std::array extended{}; - Require(scePadGetExtControllerInformation(handle, extended.data()) == PAD_OK); - PadControllerInformation base{}; - std::memcpy(&base, extended.data(), sizeof(base)); - Require(base.stickInfo.deadZoneLeft == 13 && base.stickInfo.deadZoneRight == 13); } static void CheckTouchContact() { @@ -159,15 +154,13 @@ static void CheckTouchPadInformation(int handle) { Require(info.touchPadInfo.resolution.y == kTouchPadHeight); Require(info.touchPadInfo.pixelDensity == 44.86f); Require(scePadGetControllerInformation(handle + 1, &info) == PAD_ERROR_INVALID_HANDLE); +} - std::array extended{}; - extended.fill(0x5a); - Require(scePadGetExtControllerInformation(handle, extended.data()) == PAD_OK); - PadControllerInformation base{}; - std::memcpy(&base, extended.data(), sizeof(base)); - Require(base.touchPadInfo.resolution.x == kTouchPadWidth); - Require(base.touchPadInfo.resolution.y == kTouchPadHeight); - Require(base.touchPadInfo.pixelDensity == 44.86f); +static void CheckExtControllerInformation(int handle) { + constexpr int notPermitted = static_cast(0x80920101); + std::array extended{}; + Require(scePadGetExtControllerInformation(handle, extended.data()) == notPermitted); + Require(scePadGetExtControllerInformation(handle + 1, extended.data()) == PAD_ERROR_INVALID_HANDLE); } static void CheckVirtualTouchContact() { @@ -629,6 +622,7 @@ int main() { CheckVirtualTouchContact(); CheckTouchPadInformation(handle); CheckStickDeadZones(handle); + CheckExtControllerInformation(handle); CheckPartialTrigger(); CheckReadQueue(handle); CheckTapBetweenReads(); diff --git a/docs/dev/debt/libScePad.md b/docs/dev/debt/libScePad.md index 911bec0271..f2807ba5bb 100644 --- a/docs/dev/debt/libScePad.md +++ b/docs/dev/debt/libScePad.md @@ -14,6 +14,7 @@ - [scePadSetAngularVelocityDeadbandState](../../../core/libs/prx/libScePad/src/PadState.cpp) (libScePad) - with the deadband on, each angular velocity axis whose magnitude is below 0.017453292 rad/s (1 degree per second) reads as 0, before the orientation is integrated; it starts off. Threshold, per-axis test and default follow [duaLib](https://github.com/WujekFoliarz/duaLib/blob/03bad1bea2a36561b520846776f2a07ced6773e0/src/source/duaLib.cpp#L44), which reimplements the PC `libScePad.dll`; the console's threshold is not measured, and the deadband is kept per port slot - [PadQueueLegacyAudioHaptics, PadClearLegacyAudioHaptics](../../../core/libs/prx/libScePad/src/PadState.cpp) (libScePad) - PadQueueLegacyAudioHaptics and PadClearLegacyAudioHaptics route legacy audio haptics only to port 0; multi-port queueing is not modelled - [scePadGetControllerInformation](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - scePadGetControllerInformation always reports connectedCount = 1 and connected = true; behaviour across multiple open controller handles is not measured on hardware +- [scePadGetExtControllerInformation](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - on a standard-port handle it returns `NOT_PERMITTED` (0x80920101) without writing the buffer. Halo CE Universal (PPSA99500) [measured]() that answer on firmware 13.59 for two connected DualSenses opened by the title, in three runs, with its [probe]() zeroing the buffer first; it logged only bytes 28 to 47, so whether the console writes bytes 0 to 27 is not known. The special port (type 2) still answers 0 with 0x2c zeroed bytes, which is not measured, and so are the order of the handle and null checks and the answer on a console that grants the permission. - [scePadOpen and scePadOpenExt](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the special port (type 2) opens a handle per user (`PAD_SPECIAL_HANDLE` + slot, 16 slots for the console's 16 accounts, a 17th user gets the unknown-user code 0x809b0081) that every pad call accepts: reads give the rest record (sticks 0x80, orientation w 1, acceleration y 1, `connected` false), information is zeroed, extended information has device class -1, setters succeed without effect. Not measured: the console's own handle values (fresh per open), a PS5, and the calls the PS4 Pro survey (firmware 10.71, no special controller) did not cover; it measured open, `ALREADY_OPENED`, `scePadGetHandle`, both reads, both informations, the five setters above, close, and that two logged-in users get independent handles. `scePadOpenExt` returns `DEVICE_NOT_CONNECTED` (0x80920007) for special ports, unmeasured; PPSA01317 skips a user on any negative result. `scePadOpenExt` throws for other port types - [scePadOpen, scePadGetHandle, scePadRead and scePadReadState for the remote control port](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - whether a console without a media remote opens the system user's remote control port (type 16), returns `DEVICE_NOT_CONNECTED` or fills other fields is not measured. The host has no remote, so the port opens its own handle (`PAD_REMOTE_HANDLE`) that reads one zeroed entry with no buttons and `connected` false; the other pad calls reject it with `INVALID_HANDLE`. SDL's PS5 video driver maps this port's buttons to keys, so a remote that read the DualSense turned Cross into Return (Raptor PPSA94002). - [scePadOpen](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - port types 3 and 4 throw: a payload process on a PS5 (firmware 13.60) let them past the type check, but it cannot open a port for its own logged-in user (0x809b0081), so what they open is unknown. The argument checks it did reach are followed: types 1, 5, 6, 255 and -1, and 16 for a user other than the system user, fail with `INVALID_ARG` before the user is looked at, user -1 with 0x809b0001 on types 0, 2, 3 and 4 and on index 1, and index 1 of a logged-in user with 0x809b0003; whether index 1 of types 3 and 4 fails the same way was not measured; the payload never tried type 16 with the system user (255), which opens the remote control port as below From 28544a4f7d5aefa0931c4eeb44af1bd5d7706b0a Mon Sep 17 00:00:00 2001 From: p3nicillin Date: Sun, 25 Oct 2026 21:41:19 +0000 Subject: [PATCH 073/198] docs(libScePad): use plain links in the extended information entry Co-Authored-By: Claude Opus 5.5 --- docs/dev/debt/libScePad.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libScePad.md b/docs/dev/debt/libScePad.md index f2807ba5bb..2cc1be046e 100644 --- a/docs/dev/debt/libScePad.md +++ b/docs/dev/debt/libScePad.md @@ -14,7 +14,7 @@ - [scePadSetAngularVelocityDeadbandState](../../../core/libs/prx/libScePad/src/PadState.cpp) (libScePad) - with the deadband on, each angular velocity axis whose magnitude is below 0.017453292 rad/s (1 degree per second) reads as 0, before the orientation is integrated; it starts off. Threshold, per-axis test and default follow [duaLib](https://github.com/WujekFoliarz/duaLib/blob/03bad1bea2a36561b520846776f2a07ced6773e0/src/source/duaLib.cpp#L44), which reimplements the PC `libScePad.dll`; the console's threshold is not measured, and the deadband is kept per port slot - [PadQueueLegacyAudioHaptics, PadClearLegacyAudioHaptics](../../../core/libs/prx/libScePad/src/PadState.cpp) (libScePad) - PadQueueLegacyAudioHaptics and PadClearLegacyAudioHaptics route legacy audio haptics only to port 0; multi-port queueing is not modelled - [scePadGetControllerInformation](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - scePadGetControllerInformation always reports connectedCount = 1 and connected = true; behaviour across multiple open controller handles is not measured on hardware -- [scePadGetExtControllerInformation](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - on a standard-port handle it returns `NOT_PERMITTED` (0x80920101) without writing the buffer. Halo CE Universal (PPSA99500) [measured]() that answer on firmware 13.59 for two connected DualSenses opened by the title, in three runs, with its [probe]() zeroing the buffer first; it logged only bytes 28 to 47, so whether the console writes bytes 0 to 27 is not known. The special port (type 2) still answers 0 with 0x2c zeroed bytes, which is not measured, and so are the order of the handle and null checks and the answer on a console that grants the permission. +- [scePadGetExtControllerInformation](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - on a standard-port handle it returns `NOT_PERMITTED` (0x80920101) without writing the buffer. Halo CE Universal (PPSA99500) [measured](https://github.com/thelinkin3000/halo-ce-universal/blob/377459f9655971a2f59c09e192c5220042121e6f/port/ps5/probe/results/pad-probe-run2.txt#L2-L29) that answer on firmware 13.59 for two connected DualSenses opened by the title, in three runs, with its [probe](https://github.com/thelinkin3000/halo-ce-universal/blob/377459f9655971a2f59c09e192c5220042121e6f/port/ps5/probe/pad/main.c#L263-L268) zeroing the buffer first; it logged only bytes 28 to 47, so whether the console writes bytes 0 to 27 is not known. The special port (type 2) still answers 0 with 0x2c zeroed bytes, which is not measured, and so are the order of the handle and null checks and the answer on a console that grants the permission. - [scePadOpen and scePadOpenExt](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the special port (type 2) opens a handle per user (`PAD_SPECIAL_HANDLE` + slot, 16 slots for the console's 16 accounts, a 17th user gets the unknown-user code 0x809b0081) that every pad call accepts: reads give the rest record (sticks 0x80, orientation w 1, acceleration y 1, `connected` false), information is zeroed, extended information has device class -1, setters succeed without effect. Not measured: the console's own handle values (fresh per open), a PS5, and the calls the PS4 Pro survey (firmware 10.71, no special controller) did not cover; it measured open, `ALREADY_OPENED`, `scePadGetHandle`, both reads, both informations, the five setters above, close, and that two logged-in users get independent handles. `scePadOpenExt` returns `DEVICE_NOT_CONNECTED` (0x80920007) for special ports, unmeasured; PPSA01317 skips a user on any negative result. `scePadOpenExt` throws for other port types - [scePadOpen, scePadGetHandle, scePadRead and scePadReadState for the remote control port](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - whether a console without a media remote opens the system user's remote control port (type 16), returns `DEVICE_NOT_CONNECTED` or fills other fields is not measured. The host has no remote, so the port opens its own handle (`PAD_REMOTE_HANDLE`) that reads one zeroed entry with no buttons and `connected` false; the other pad calls reject it with `INVALID_HANDLE`. SDL's PS5 video driver maps this port's buttons to keys, so a remote that read the DualSense turned Cross into Return (Raptor PPSA94002). - [scePadOpen](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - port types 3 and 4 throw: a payload process on a PS5 (firmware 13.60) let them past the type check, but it cannot open a port for its own logged-in user (0x809b0081), so what they open is unknown. The argument checks it did reach are followed: types 1, 5, 6, 255 and -1, and 16 for a user other than the system user, fail with `INVALID_ARG` before the user is looked at, user -1 with 0x809b0001 on types 0, 2, 3 and 4 and on index 1, and index 1 of a logged-in user with 0x809b0003; whether index 1 of types 3 and 4 fails the same way was not measured; the payload never tried type 16 with the system user (255), which opens the remote control port as below From 08e595fc15e72dccb5dcf40a778dea5a248b1e0e Mon Sep 17 00:00:00 2001 From: p3nicillin Date: Sun, 25 Oct 2026 21:43:09 +0000 Subject: [PATCH 074/198] docs(libScePad): keep the buffer side effect of the refusal unmeasured Co-Authored-By: Claude Opus 5.5 --- docs/dev/debt/libScePad.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libScePad.md b/docs/dev/debt/libScePad.md index 2cc1be046e..42e2512134 100644 --- a/docs/dev/debt/libScePad.md +++ b/docs/dev/debt/libScePad.md @@ -14,7 +14,7 @@ - [scePadSetAngularVelocityDeadbandState](../../../core/libs/prx/libScePad/src/PadState.cpp) (libScePad) - with the deadband on, each angular velocity axis whose magnitude is below 0.017453292 rad/s (1 degree per second) reads as 0, before the orientation is integrated; it starts off. Threshold, per-axis test and default follow [duaLib](https://github.com/WujekFoliarz/duaLib/blob/03bad1bea2a36561b520846776f2a07ced6773e0/src/source/duaLib.cpp#L44), which reimplements the PC `libScePad.dll`; the console's threshold is not measured, and the deadband is kept per port slot - [PadQueueLegacyAudioHaptics, PadClearLegacyAudioHaptics](../../../core/libs/prx/libScePad/src/PadState.cpp) (libScePad) - PadQueueLegacyAudioHaptics and PadClearLegacyAudioHaptics route legacy audio haptics only to port 0; multi-port queueing is not modelled - [scePadGetControllerInformation](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - scePadGetControllerInformation always reports connectedCount = 1 and connected = true; behaviour across multiple open controller handles is not measured on hardware -- [scePadGetExtControllerInformation](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - on a standard-port handle it returns `NOT_PERMITTED` (0x80920101) without writing the buffer. Halo CE Universal (PPSA99500) [measured](https://github.com/thelinkin3000/halo-ce-universal/blob/377459f9655971a2f59c09e192c5220042121e6f/port/ps5/probe/results/pad-probe-run2.txt#L2-L29) that answer on firmware 13.59 for two connected DualSenses opened by the title, in three runs, with its [probe](https://github.com/thelinkin3000/halo-ce-universal/blob/377459f9655971a2f59c09e192c5220042121e6f/port/ps5/probe/pad/main.c#L263-L268) zeroing the buffer first; it logged only bytes 28 to 47, so whether the console writes bytes 0 to 27 is not known. The special port (type 2) still answers 0 with 0x2c zeroed bytes, which is not measured, and so are the order of the handle and null checks and the answer on a console that grants the permission. +- [scePadGetExtControllerInformation](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - on a standard-port handle it returns `NOT_PERMITTED` (0x80920101) and leaves the buffer as it is. Halo CE Universal (PPSA99500) [measured](https://github.com/thelinkin3000/halo-ce-universal/blob/377459f9655971a2f59c09e192c5220042121e6f/port/ps5/probe/results/pad-probe-run2.txt#L2-L29) that answer on firmware 13.59 for two connected DualSenses opened by the title, in three runs, with its [probe](https://github.com/thelinkin3000/halo-ce-universal/blob/377459f9655971a2f59c09e192c5220042121e6f/port/ps5/probe/pad/main.c#L263-L268) zeroing the buffer first. The probe logged only bytes 28 to 47, which stayed zero, so whether the console writes any of the buffer is not known. The special port (type 2) still answers 0 with 0x2c zeroed bytes, which is not measured, and so are the order of the handle and null checks and the answer on a console that grants the permission. - [scePadOpen and scePadOpenExt](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the special port (type 2) opens a handle per user (`PAD_SPECIAL_HANDLE` + slot, 16 slots for the console's 16 accounts, a 17th user gets the unknown-user code 0x809b0081) that every pad call accepts: reads give the rest record (sticks 0x80, orientation w 1, acceleration y 1, `connected` false), information is zeroed, extended information has device class -1, setters succeed without effect. Not measured: the console's own handle values (fresh per open), a PS5, and the calls the PS4 Pro survey (firmware 10.71, no special controller) did not cover; it measured open, `ALREADY_OPENED`, `scePadGetHandle`, both reads, both informations, the five setters above, close, and that two logged-in users get independent handles. `scePadOpenExt` returns `DEVICE_NOT_CONNECTED` (0x80920007) for special ports, unmeasured; PPSA01317 skips a user on any negative result. `scePadOpenExt` throws for other port types - [scePadOpen, scePadGetHandle, scePadRead and scePadReadState for the remote control port](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - whether a console without a media remote opens the system user's remote control port (type 16), returns `DEVICE_NOT_CONNECTED` or fills other fields is not measured. The host has no remote, so the port opens its own handle (`PAD_REMOTE_HANDLE`) that reads one zeroed entry with no buttons and `connected` false; the other pad calls reject it with `INVALID_HANDLE`. SDL's PS5 video driver maps this port's buttons to keys, so a remote that read the DualSense turned Cross into Return (Raptor PPSA94002). - [scePadOpen](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - port types 3 and 4 throw: a payload process on a PS5 (firmware 13.60) let them past the type check, but it cannot open a port for its own logged-in user (0x809b0081), so what they open is unknown. The argument checks it did reach are followed: types 1, 5, 6, 255 and -1, and 16 for a user other than the system user, fail with `INVALID_ARG` before the user is looked at, user -1 with 0x809b0001 on types 0, 2, 3 and 4 and on index 1, and index 1 of a logged-in user with 0x809b0003; whether index 1 of types 3 and 4 fails the same way was not measured; the payload never tried type 16 with the system user (255), which opens the remote control port as below From eb943993a73394b85adae79053956e1d849dc9cd Mon Sep 17 00:00:00 2001 From: Alexandre Figueiredo Date: Sun, 25 Oct 2026 21:57:32 +0000 Subject: [PATCH 075/198] docs(debt): cut the long-double Dinkumware entry to _LLog with the bundled libc.prx code --- docs/dev/debt/libc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 6baa225040..943355f7d7 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -98,7 +98,7 @@ - [strtod, strtof, strtold](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - `strtod` and `wcstod` (and `atof`, which calls `strtod` here) read a `NAN(...)` sequence as FreeBSD 11's gdtoa [`hexnan`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/contrib/gdtoa/hexnan.c) does: hexadecimal digits with an optional `0x`, blanks starting a new 32-bit word, the first 16 digits kept and the payload cut to 52 bits, a zero payload made 1, any other character giving the default NaN but consuming up to the closing parenthesis, and an unterminated sequence keeping its payload while consuming only `nan`. The payload goes under the quiet bit with the sign, as in gdtoa's [`strtod`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/contrib/gdtoa/strtod.c#L316-L328). `strtof`, `wcstof`, `strtold` and `wcstold` return the positive default quiet NaN (`0x7fc00000`; exponent `0x7fff`, significand `0xc000000000000000`) whatever the sign and the sequence, and consume a sequence of letters, digits and underscores up to its closing parenthesis. The console's `libSceLibcInternal.sprx`, called from a payload process on a PS5 (firmware 12.70), gives these results for the 64 NaN inputs tried through `strtod` and `strtof`, 50 of them also through `strtold` and `wcstod`, and 18 through `wcstof` and `wcstold`, and, through `strtod`, for `nan(1 2)` and for `nan( ` followed by NUL and `)`; that a title's own `libc.prx` does the same is assumed. In `hexnan`, a blank after a payload digit, while a 32-bit word of the payload is still free, starts a blank skip that has no NUL check, so when only blanks follow up to the terminating NUL the console's `strtod` reads past it: `nan(1 ` followed by NUL and `)` consumes the `)` after the NUL, so the result depends on the bytes after the string, and here that case throws. A blank right after `nan(` is skipped with a NUL check, and the console stopped at the NUL there - [remquol](../../../core/libs/prx/libc/src/MathExact.cpp) (libc) leaves `*quo` unchanged when the result is NaN; the `libc.prx` bundled with PPSA08427 writes a value that depends on earlier x87 state (`remquol(1, 0)` stored 0 on a first call and 4 on later ones) - [_Stod, _Stodx, _Stofx, _Stoldx, _Stof and _Stold](../../../core/libs/prx/libc/src/DinkumwareInternals.cpp) (libSceLibcInternal, implemented in libc) throw for hexadecimal text with a non-zero power-of-ten scale. The console's result there does not follow a simple rule: `0x3` with a scale of -1 gives 0.29999999999999999. A nonzero subnormal result sets neither `errno` nor the error argument, as measured in firmware 13.60's `libSceLibcInternal.sprx` run in a Linux host process, not on a console; there about a fifth of decimal inputs with a subnormal result come out one unit apart in the last place (`_Stodx("2.2250738585072014e-308", 0)` is the largest subnormal), which is not reproduced -- [_LDivide, _LRecip and _LLog](../../../core/libs/prx/libc/src/DinkumwareInternals.cpp) (libc) follow the public Dinkumware CRT contracts: long-double division and reciprocal, and the `_LLog` base flag (`0` natural, positive base 10, negative base 2). The exact console results, signaling-NaN behavior and floating-point exception behavior are not verified. +- [_LLog](../../../core/libs/prx/libc/src/DinkumwareInternals.cpp) (libc) follows the public Dinkumware CRT contract for its base flag (`0` natural, positive base 10, negative base 2). The exact console results, signaling-NaN behavior and floating-point exception behavior are not verified. - [Long double math in MathExact.cpp](../../../core/libs/prx/libc/src/MathExact.cpp) (libc) was compared with the `libc.prx` bundled with PPSA08427 only for canonical 80-bit encodings; unnormals, pseudo-denormals and pseudo-infinities passed to arithmetic functions (not the classifiers) follow the host x87 instead of the console - [atanf](../../../core/libs/prx/libc/src/Math.cpp) (libc) is FreeBSD msun's [`s_atanf.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/s_atanf.c), with `atanlo[3]` read at run time as msun's `volatile` does. Tiny arguments return `x` without raising `FE_INEXACT`. It gives the same bits as the `atanf` of the `libc.prx` bundled with PPSA07923, PPSA05916 and PPSA13934 for all 2^32 arguments, run on a Windows host, and none of those sets `errno`. Whether other titles' builds differ is unverified. - [rename, sceKernelRename](../../../core/libs/prx/libc/include/RenameDot.hpp) (libc, libkernel) fail a last path component of `.` or `..` with `EINVAL`, after FreeBSD 11.4's [`lookup`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_lookup.c), which refuses `.` and, through its `ISDOTDOT` and `ISLASTCN` flags, `..` as the last component for the `DELETE` and `RENAME` operations. `unlink`, `rmdir`, `sceKernelUnlink` and `sceKernelRmdir` follow the same rule. Both arguments are checked before any host access, so `rename` of a missing source onto `dir/.` answers `EINVAL`, where FreeBSD's [`kern_renameat`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_syscalls.c) looks the source up first and answers `ENOENT`. A backslash counts as a path separator here (the guest path resolver turns it into `/`); FreeBSD treats it as an ordinary name character. None of this was measured on a console. From e54e3569f38e815640fe0861734a1a1fd399448b Mon Sep 17 00:00:00 2001 From: JJenkx Date: Thu, 22 Oct 2026 10:40:54 -0400 Subject: [PATCH 076/198] fix(libc): compute atan2f with FreeBSD msun's algorithm, as titles' libc.prx does atan2f returned the host atan2, so its last bit depended on the host: on Windows it differs from the libc.prx that titles ship on 7,126,466 of 200,001,024 sampled argument pairs. That libc.prx is msun's e_atan2f.c over its atanf; with two NaN arguments it returns y's. The port gives the same bits as PPSA07923's, PPSA05916's and PPSA13934's copies on every pair. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libc/src/Math.cpp | 67 ++++++++++++++++++++++++++++++++- core/libs/tests/GuestMath.cpp | 17 +++++++++ docs/dev/debt/libc.md | 1 + 3 files changed, 84 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index e635b0b1e1..cd3abc29d2 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -315,7 +315,6 @@ float APS5_VABI acosf_nid_postfix(float x) { errno = saved_errno; return result; } -float APS5_VABI atan2f_nid_postfix(float y, float x) { return std::atan2(y, x); } float APS5_VABI hypotf_nid_postfix(float x, float y) { return static_cast(std::hypot(static_cast(x), static_cast(y))); } double APS5_VABI hypot_nid_postfix(double x, double y) { const std::uint64_t hx = std::bit_cast(x); @@ -1123,6 +1122,72 @@ float APS5_VABI atanf_nid_postfix(float x) { const float result = atanhi[id] - ((x * (s1 + s2) - atanlo[id]) - x); return hx < 0 ? -result : result; } + +float APS5_VABI atan2f_nid_postfix(float y, float x) { + static volatile float tiny = 1.0e-30f; + static volatile float pi_lo = static_cast(-8.7422776573e-08); + const float pi_o_4 = static_cast(7.8539818525e-01); + const float pi_o_2 = static_cast(1.5707963705e+00); + const float pi = static_cast(3.1415927410e+00); + const std::int32_t hx = std::bit_cast(x); + const std::int32_t ix = hx & 0x7fffffff; + const std::int32_t hy = std::bit_cast(y); + const std::int32_t iy = hy & 0x7fffffff; + if (iy > 0x7f800000) { + return y + y; + } + if (ix > 0x7f800000) { + return x + x; + } + if (hx == 0x3f800000) { + return atanf_nid_postfix(y); + } + int m = ((hy >> 31) & 1) | ((hx >> 30) & 2); + if (iy == 0) { + if (m < 2) { + return y; + } + return m == 2 ? pi + tiny : -pi - tiny; + } + if (ix == 0) { + return hy < 0 ? -pi_o_2 - tiny : pi_o_2 + tiny; + } + if (ix == 0x7f800000) { + if (iy == 0x7f800000) { + switch (m) { + case 0: return pi_o_4 + tiny; + case 1: return -pi_o_4 - tiny; + case 2: return 3.0f * pi_o_4 + tiny; + default: return -3.0f * pi_o_4 - tiny; + } + } + switch (m) { + case 0: return 0.0f; + case 1: return -0.0f; + case 2: return pi + tiny; + default: return -pi - tiny; + } + } + if (iy == 0x7f800000) { + return hy < 0 ? -pi_o_2 - tiny : pi_o_2 + tiny; + } + const std::int32_t k = (iy - ix) >> 23; + float z; + if (k > 26) { + z = pi_o_2 + 0.5f * pi_lo; + m &= 1; + } else if (k < -26 && hx < 0) { + z = 0.0f; + } else { + z = atanf_nid_postfix(std::fabs(y / x)); + } + switch (m) { + case 0: return z; + case 1: return -z; + case 2: return pi - (z - pi_lo); + default: return (z - pi_lo) - pi; + } +} double APS5_VABI atan2_nid_postfix(double y, double x) { return std::atan2(y, x); } float APS5_VABI powf_nid_postfix(float base, float exp) { if (base == 1.0f) return 1.0f; diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index 7c6bd1e44e..ace88092ba 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -374,6 +374,22 @@ static void CheckAtanfConsoleBits() { } } +static void CheckAtan2fConsoleBits() { + constexpr std::uint32_t cases[][3] = { + {0x3d6f53e1u, 0x3fb6a73eu, 0x3d279f68u}, {0x3e587cf9u, 0x3f8f2054u, 0x3e3f5992u}, {0xbe8d427du, 0x3e57a3bau, 0xbf6b3a09u}, + {0x40162421u, 0xbd5afe7bu, 0x3fcbfa85u}, {0xc0d9c130u, 0xbd9ebbffu, 0xbfca8510u}, {0xbe077b06u, 0x40786368u, 0xbd0b9407u}, + {0xbe51a574u, 0xbdf4773fu, 0xc006505au}, {0x3fe6cdabu, 0xbf7fff9bu, 0x4004f006u}, {0x3f800000u, 0x3f800000u, 0x3f490fdbu}, + {0x40000000u, 0x3f800000u, 0x3f8db70du}, {0x00000000u, 0xbf800000u, 0x40490fdbu}, {0x80000000u, 0xbf800000u, 0xc0490fdbu}, + {0x3f800000u, 0x00000000u, 0x3fc90fdbu}, {0xbf800000u, 0x80000000u, 0xbfc90fdbu}, {0x7f800000u, 0x7f800000u, 0x3f490fdbu}, + {0xff800000u, 0xff800000u, 0xc016cbe4u}, {0x3f800000u, 0xff800000u, 0x40490fdbu}, {0xbf800000u, 0x7f800000u, 0x80000000u}, + {0x7f800000u, 0x3f000000u, 0x3fc90fdbu}, {0x4e000000u, 0x3f800001u, 0x3fc90fdbu}, {0x30000000u, 0xc0000000u, 0x40490fdbu}, + {0x7fc00000u, 0xffc00001u, 0x7fc00000u}, {0x3f800000u, 0x7fc00002u, 0x7fc00002u}, + }; + for (const auto& [y, x, expected] : cases) { + Require(std::bit_cast(atan2f_nid_postfix(std::bit_cast(y), std::bit_cast(x))) == expected); + } +} + static void CheckHypotSpecialValue() { Require(std::bit_cast(hypot_nid_postfix(INFINITY, std::bit_cast(0x7ff0000000000001ull))) == 0x7ff0000000000000ull); Require(std::bit_cast(hypot_nid_postfix(std::bit_cast(0x7ff0000000000001ull), INFINITY)) == 0x7ff0000000000000ull); @@ -1737,6 +1753,7 @@ int main() { CheckLogfConsoleBits(); CheckHypotSpecialValue(); CheckAtanfConsoleBits(); + CheckAtan2fConsoleBits(); CheckFloatUnderflowErrno(); CheckFmodErrno(); CheckFmodfErrno(); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 6092ffb8a1..7769e0f23c 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -103,6 +103,7 @@ - [_LDivide, _LRecip and _LLog](../../../core/libs/prx/libc/src/DinkumwareInternals.cpp) (libc) follow the public Dinkumware CRT contracts: long-double division and reciprocal, and the `_LLog` base flag (`0` natural, positive base 10, negative base 2). The exact console results, signaling-NaN behavior and floating-point exception behavior are not verified. - [Long double math in MathExact.cpp](../../../core/libs/prx/libc/src/MathExact.cpp) (libc) was compared with the `libc.prx` bundled with PPSA08427 only for canonical 80-bit encodings; unnormals, pseudo-denormals and pseudo-infinities passed to arithmetic functions (not the classifiers) follow the host x87 instead of the console - [atanf](../../../core/libs/prx/libc/src/Math.cpp) (libc) is FreeBSD msun's [`s_atanf.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/s_atanf.c), with `atanlo[3]` read at run time as msun's `volatile` does. Tiny arguments return `x` without raising `FE_INEXACT`. It gives the same bits as the `atanf` of the `libc.prx` bundled with PPSA07923, PPSA05916 and PPSA13934 for all 2^32 arguments, run on a Windows host, and none of those sets `errno`. Whether other titles' builds differ is unverified. +- [atan2f](../../../core/libs/prx/libc/src/Math.cpp) (libc) is FreeBSD msun's [`e_atan2f.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/e_atan2f.c) over this `atanf`, with `tiny` and `pi_lo` read at run time as msun's `volatile` reads do. With two NaN arguments it returns `y`'s, as the titles' compiled `x + y` does. On 200,001,024 argument pairs (random, near-equal exponents, x near ±1, zero y, and every pair of 32 special values), run on a Windows host, it gives the same bits as the `atan2f` of the `libc.prx` bundled with PPSA07923, PPSA05916 and PPSA13934, and none of those sets `errno`. The other pairs are unverified. - [rename, sceKernelRename](../../../core/libs/prx/libc/include/RenameDot.hpp) (libc, libkernel) fail a last path component of `.` or `..` with `EINVAL`, after FreeBSD 11.4's [`lookup`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_lookup.c), which refuses `.` and, through its `ISDOTDOT` and `ISLASTCN` flags, `..` as the last component for the `DELETE` and `RENAME` operations. `unlink`, `rmdir`, `sceKernelUnlink` and `sceKernelRmdir` follow the same rule. Both arguments are checked before any host access, so `rename` of a missing source onto `dir/.` answers `EINVAL`, where FreeBSD's [`kern_renameat`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_syscalls.c) looks the source up first and answers `ENOENT`. A backslash counts as a path separator here (the guest path resolver turns it into `/`); FreeBSD treats it as an ordinary name character. None of this was measured on a console. - [nan and nanf](../../../core/libs/prx/libc/src/Math.cpp) (libc) return fixed quiet NaN values (`0x7fc00000` and `0x7ff8000000000000`), ignoring the tag argument as observed in the console's `libc.prx` (both exports load a global constant via `mov rax,[rip+..]; vmovsd xmm0,[rax]; ret`); VLC (PPSA85300) imports nanf. - [jrand48 and mrand48](../../../core/libs/prx/libc/src/Random.cpp) (libSceLibcInternal, implemented in libc) return the high 32 bits of the new 48-bit state zero-extended to 64 bits, as the console's `libSceLibcInternal.sprx` does when called from a homebrew payload process on a PS5 (firmware 12.70): a result with bit 31 set comes back between 0x80000000 and 0xFFFFFFFF, as FreeBSD 11.0's [`jrand48`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/lib/libc/gen/jrand48.c#L23) and [`mrand48`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/lib/libc/gen/mrand48.c#L25) return it; FreeBSD 11.1 and later cast it to `int32_t` ([`jrand48`](https://github.com/freebsd/freebsd-src/blob/release/11.1.0/lib/libc/gen/jrand48.c#L26), [`mrand48`](https://github.com/freebsd/freebsd-src/blob/release/11.1.0/lib/libc/gen/mrand48.c#L28)), which makes it negative. `lrand48` and `nrand48` matched FreeBSD there. Whether a title's own libc does the same is unverified From a69e912b16428a0b1168cf36fa67e9d6d5028ec0 Mon Sep 17 00:00:00 2001 From: orezraey Date: Sun, 25 Oct 2026 19:09:29 -0300 Subject: [PATCH 077/198] docs(libkernel): record timed self-locks, event flag open and create, and the reltimed calls as measured on a PS5 Co-Authored-By: Claude Opus 5.5 --- docs/dev/debt/libkernel.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 875084ff1b..e4db466629 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -66,16 +66,14 @@ - [sceKernelSyncOnAddressWait](../../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - the only known caller passes a null timeout and a name string as the fourth argument; the timeout is assumed to point to microseconds like the other kernel waits, and the name is ignored - [sceKernelSyncOnAddressWait8 / sceKernelSyncOnAddressWait16 / sceKernelSyncOnAddressWait32 / sceKernelSyncOnAddressWait64](../../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - libc's `__std_atomic_wait_direct_8/16/32/64` pass a microsecond timeout or null and ignore the result; the by-value comparand, return values and the 2- and 8-byte alignment of Wait16 and Wait64 are assumed - [scePthreadRwlockDestroy, pthread_rwlock_destroy](../../../core/libs/prx/libkernel/Pthread/src/Rwlock.cpp) (libkernel) - measured on a PS5 (firmware 13.60): destroying a rwlock returns 0 whether it is free or held for reading or writing, and leaves 1 in the handle; every call on that handle then returns `EINVAL` until it is initialized again, and a zero handle is initialized on first use by the `scePthreadRwlock` calls as by the POSIX ones. A rwlock destroyed while held is not freed, so its memory leaks. Destroying a rwlock while another thread is acquiring it is not guarded -- [scePthreadMutexTimedlock](../../../core/libs/prx/libkernel/Pthread/src/Mutex.cpp), [scePthreadRwlockTimedrdlock, scePthreadRwlockTimedwrlock](../../../core/libs/prx/libkernel/Pthread/src/Rwlock.cpp) (libkernel) - a timed lock of a mutex the caller holds, or of a rwlock it holds for writing, sleeps until its timeout before returning `ETIMEDOUT` (`0x8002003c`), as FreeBSD 11.4 libthr does ([mutex](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_mutex.c#L878-L928)); whether the console waits is not measured. The prosper oracle measured only the result, for a default mutex and a rwlock with a 1000 us timeout ([kernelx.golden.tsv](https://github.com/mattias800/prosper/blob/3ea5cb9a846b6dcd00f5073360462bbda4f72288/prosper/tests/data/console_oracle/kernelx.golden.tsv#L16-L47)); the other mutex types and the POSIX timed locks follow the same rule unmeasured. - [scePthreadCondWait / scePthreadCondTimedwait](../../../core/libs/prx/libkernel/Pthread/src/Cond.cpp) (libkernel) - before waiting, FreeBSD libthr's [`cond_wait_common`](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_cond.c#L1992-L2029) checks ownership through [`_mutex_owned`](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_mutex.c#L1166-L1178): an unowned mutex returns `EPERM`, while its destroyed marker returns `EINVAL`. An untouched adaptive static initializer (slot value 1) therefore returns `EPERM`; the test covers this, an uninitialized slot, a mutex owned by another thread, and the destroyed marker for both timed and untimed sce and POSIX waits. A recursive mutex locked several times is released completely for the wait and relocked to the same depth afterwards, as FreeBSD libthr does (`_mutex_cv_unlock`, `_mutex_cv_lock`). These FreeBSD-derived behaviors remain unverified on console hardware. Cancellation follows the [POSIX condition wait contract](https://pubs.opengroup.org/onlinepubs/9799919799/functions/pthread_cond_clockwait.html): the original mutex and recursive depth are restored before cleanup, and a canceled waiter forwards a wake to the remaining waiters. The host condition state is retained across mutex reacquisition so the guest can destroy and free its condition after notification; cancellation ordering and signal replacement are not measured on a console -- [sceKernelOpenEventFlag](../../../core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp) (libkernel) - opens only flags this process created with attribute 0x100. On the console, other processes' 0x100 flags, such as the shell's `SceShellCoreUtilAppFocus`, can be opened too. They do not exist here, so opening them returns `SCE_KERNEL_ERROR_ESRCH`. A null output with the name of an openable flag returns `SCE_KERNEL_ERROR_EFAULT`, the host's answer to the console's fault there, not a measured code. With a plain flag's name it returns `SCE_KERNEL_ERROR_ESRCH`, as measured; with a missing name it also returns `SCE_KERNEL_ERROR_ESRCH`, which was not measured. -- [sceKernelCreateEventFlag](../../../core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp) (libkernel) - when a call has both an invalid attribute and a name of 32 bytes or more, `SCE_KERNEL_ERROR_EINVAL` is returned, because the attribute checks run before the name length check. The console was only measured with long names and valid attributes (0x20 and 0x120), so which error it returns first is not known. Event flag handles are host addresses here, while the console returns small IDs. +- [sceKernelOpenEventFlag](../../../core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp) (libkernel) - opens only flags this process created with attribute 0x100. On the console, other processes' 0x100 flags, such as the shell's `SceShellCoreUtilAppFocus`, can be opened too. They do not exist here, so opening them returns `SCE_KERNEL_ERROR_ESRCH`. A null output with the name of an openable flag returns `SCE_KERNEL_ERROR_EFAULT`, the host's answer to the console's fault there, not a measured code. With a plain flag's name it returns `SCE_KERNEL_ERROR_ESRCH`, as measured; with a missing name, or a null output and a missing name, it returns `SCE_KERNEL_ERROR_ESRCH`, as a payload process on PS5 firmware 13.60 got. +- [sceKernelCreateEventFlag](../../../core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp) (libkernel) - event flag handles are host addresses here, while the console returns small IDs (0x22 for the first in a payload process on PS5 firmware 13.60). That console checks the attribute before the name length, as here: attributes 0x3, 0x30, 0x40 and 0x200 return `SCE_KERNEL_ERROR_EINVAL` with a 32-byte name too, and attributes 0 and 0x100 return `SCE_KERNEL_ERROR_ENAMETOOLONG` with it - [sceKernelAioSubmitReadCommandsMultiple / sceKernelAioSubmitWriteCommandsMultiple / sceKernelAioWaitRequests / sceKernelAioPollRequests / sceKernelAioCancelRequest / sceKernelAioCancelRequests / sceKernelAioDeleteRequests](../../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) - one id per request for the Multiple submits and wait mode 2 returning once one request completed follow shadPS4. Requests run synchronously on submit, so cancel never finds one processing. The console's ids are not the small numbers used here (it returned 33, 0x2021, 0x4021) - [sceKernelWaitSema, sceKernelPollSema, sceKernelSignalSema, sceKernelCancelSema, sceKernelDeleteSema](../../../core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp) (libkernel) - `SCE_KERNEL_ERROR_ESRCH` for a null handle follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/60c42b925c3d1e00008621b8587819999260ec31/src/core/libraries/kernel/threads/semaphore.cpp#L329-L366) (PS4), unverified on PS5. Handles are pointers, so a non-null handle that is not a live semaphore is not detected; shadPS4 returns `ESRCH` for any unallocated handle. - [scePthreadAttr setters and getters](../../../core/libs/prx/libkernel/Pthread/src/Attr.cpp) (libkernel) - measured on a PS5 (firmware 13.60): a null or uninitialized attribute returns `EINVAL`, as does a null output of the stack size, detach state and scheduling parameter getters; a policy other than 1 and 3, an inheritance other than 0 and 4, or a priority outside 256 to 767 (or a null parameter) returns `ENOTSUP` (`0x8002002D`); a detach state other than 0 and 1 and a stack size below 16384 return `EINVAL`; any affinity mask is accepted. A null attribute pointer to `scePthreadAttrInit` and a null output of `scePthreadAttrGetaffinity` crashed the payload, so they still throw. A new attribute holds a 64 KiB stack size on the console and 1 MiB here, which the host code that runs on guest threads needs. `scePthreadAttrGetschedpolicy` and `scePthreadAttrGetinheritsched` were not measured and follow [FreeBSD 11.4 libthr](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_attr.c#L216-L262); a null output of `scePthreadAttrGetinheritsched` throws, since libthr writes through it unchecked - [scePthreadAttrGet](../../../core/libs/prx/libkernel/Pthread/src/Attr.cpp) and [pthread_attr_get_np](../../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) - the returned scheduling policy is unmeasured on a console. Both report the recorded guest policy, following [FreeBSD 11.4 libthr](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_attr.c#L134-L164) - [pthread_getprio](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) is unmeasured on the console. NID `aO+WS2xWuo4` is named in the firmware 3.20 [export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libkernel.c). Its return and errno contract follows FreeBSD 11 [thr_getprio.c](https://github.com/freebsd/freebsd-src/blob/671fb7df97631020c42ce55527dc49fffe0656b7/lib/libthr/thread/thr_getprio.c): return the recorded guest priority, or -1 with `ESRCH` for an unknown or finished thread. The live-thread registry lock protects the read from a concurrent join. This reports guest scheduler state, not the host scheduling priority. -- [pthread_rwlock_reltimedrdlock_np, pthread_rwlock_reltimedwrlock_np](../../../core/libs/prx/libkernel/Pthread/Posix/Rwlock.cpp) (libkernel) take `(rwlock, usec)`, a relative timeout in microseconds, and are `scePthreadRwlockTimedrdlock` and `scePthreadRwlockTimedwrlock` with POSIX error numbers, so they return what those return (`ETIMEDOUT` when the time runs out, at once for 0 while another thread holds the lock, and `EINVAL` for a destroyed lock); a zero lock is initialized as by `pthread_rwlock_rdlock`. FreeBSD 11.4 libthr has no such functions; the signature comes only from shadPS4, which registers one function for each NID and its `scePthreadRwlockTimed*` twin ([`rwlock.cpp`](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/libraries/kernel/threads/rwlock.cpp#L167-L179), 32-bit `usec`). Unverified on the console - [sceKernelGetAvailableCpumask](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - returns the default thread affinity, 0x1FFF (CPUs 0 to 12), as KytyPS5 does. Titles pin threads within it (PPSA02664 passes 0x1FFB, Hades II PPSA36082 0x3 and 0x3F); whether CPU 13 is available to a title is not confirmed - [sceKernelGetCurrentCpu](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - guest affinities are not applied to host threads, so the host CPU index is mapped into the calling thread's affinity clipped to `sceKernelGetAvailableCpumask` (the available mask when they do not overlap): host CPU n reports the (n modulo the CPU count)-th CPU of that mask, so a thread pinned to one CPU reports that CPU, as a FreeBSD `cpuset` guarantees on the console. Which CPU the console reports for a thread allowed on several is not known; shadPS4 returns 0 - [pthread_attr_setsolosched_np](../../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) takes `(attr, int)` and stores the value through `scePthreadAttrSetsolosched`, returning `EINVAL` (22) for a null or uninitialized attribute like the other POSIX attribute setters. Only the name is public (aerolib `2+pVfgiEd7A`); the signature and behaviour follow the SCE twin, which [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/947627d10d4ef4b68ba3c0189aab6c368e70a8d2/src/kernel/pthread.cpp#L2188-L2199) and [sharpemu](https://github.com/sharpemu/sharpemu/blob/a2983692930564815a77f81e74c151268f5b935f/src/SharpEmu.Libs/Kernel/KernelPthreadExtendedCompatExports.cs#L891-L913) store as an `int` without a range check, and [PS5PCEM](https://github.com/iStark/PS5PCEM/blob/71de7681f06c5db59a1120a3bb554c86780c3f5f/src/hle/libs/kernel_threading.zig#L1396-L1398) implements this alias over it. On a PS5 (firmware 13.60) only 0 and 16 are accepted; every other value returns `ENOTSUP` (45, `0x8002002D` from the SCE call) and keeps the stored one, and the SCE calls return `EINVAL` for a null or uninitialized attribute. PPSA15878 passes 16. Threads created from the attribute are not scheduled differently; what solo scheduling does on the console is unknown. Returnal (PPSA01285 v03.000.005) stopped on it with `what(): pthread_attr_setsolosched_np_nid_postfix not implemented` (#2872, #1155); LEGO Horizon Adventures (PPSA14632, #734) and Palworld (PPSA23126, #2598) import it @@ -281,8 +279,6 @@ - [pthread_cond_signalto_np, scePthreadCondSignalto](../../../core/libs/prx/libkernel/Pthread/src/Cond.cpp) (libkernel) wake every waiter of the condition variable, not only the given thread, and do not check the thread; the other waiters see a spurious wakeup. Which waiters the console wakes, and its result for a thread that is not waiting, are unverified - [pthread_rwlockattr_gettype_np, scePthreadRwlockattrGettype](../../../core/libs/prx/libkernel/Pthread/Posix/Rwlock.cpp) (libkernel) report the type stored by `scePthreadRwlockattrSettype`, which accepts any value and which read-write locks do not apply; the console's valid types and their effect are unverified. A null or uninitialized attribute throws, as for `pthread_rwlockattr_getpshared` - [sceKernelGetEventError](../../../core/libs/prx/libkernel/Equeue/src/EventAccessors.cpp) (libkernel) throws for `fflags` above 0xFFFF, where OR and addition of 0x80020000 differ and the console was not measured. Otherwise it returns 0 for `fflags` 0 and `0x80020000 | fflags`, whatever `flags` and `data` hold, as [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/kernel/ps4_queue.pas#L643) does and a PS5 (firmware 13.40, measured 2026-10-19 in a payload process) answered: `{flags 0x4000, fflags 0, data 22}`, `{0x4000, 0, -1}`, `{0x4000, 0, 0x80020016}` and `{0, 0, 0}` gave 0, `{0x4000, 5, 0}` and `{0x4000, 5, 22}` 0x80020005, `{0, 7, 22}` 0x80020007, `{0x8000, 3, 9}` 0x80020003. Video-out events carry their count in `fflags`, so they get 0x80020001 and up; only events built by the caller were measured. Closes when: `fflags` above 0xFFFF is measured -- [pthread_cond_reltimedwait_np](../../../core/libs/prx/libkernel/Pthread/Posix/Cond.cpp) (libkernel) takes `(cond, mutex, usec)`, a relative timeout in microseconds, and is `scePthreadCondTimedwait` with POSIX error numbers. FreeBSD 11.4 has no such function; the signature comes only from shadPS4, which registers one function for this NID and for `scePthreadCondTimedwait` ([`condvar.cpp`](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/libraries/kernel/threads/condvar.cpp#L229-L234)). shadPS4 reads a 64-bit count; here it is the 32-bit `SceKernelUseconds` of the SCE twin, as for `sem_reltimedwait_np`. Unverified on the console -- [pthread_mutex_reltimedlock_np](../../../core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp) (libkernel) takes `(mutex, usec)`, a relative timeout in microseconds, and is `scePthreadMutexTimedlock` with POSIX error numbers. FreeBSD 11.4 has no such function; the signature comes only from shadPS4, which registers one function for this NID and for `scePthreadMutexTimedlock` ([`mutex.cpp`](https://github.com/shadps4-emu/shadPS4/blob/7c960eae136b956cf4c1d72aacd2426b650330df/src/core/libraries/kernel/threads/mutex.cpp#L284-L287)). shadPS4 reads a 64-bit count; here it is the 32-bit `SceKernelUseconds` of the SCE twin, as for `pthread_cond_reltimedwait_np`, so a count of 2^32 microseconds or more would be truncated if the console reads 64 bits. Unverified on the console - [sysctl kern.arandom](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) serves FreeBSD 11's MIB {1, 37} (CTL_KERN, KERN_ARANDOM) and the "kern.arandom" sysctlbyname lookup through std::random_device, returning min(*oldLength, 256) random bytes. Following FreeBSD's sysctl_kern_arnd, a null buffer reports min(*oldLength, 256) without writing, a null length returns 0 without writing, and requests with a new value return EPERM (1) as the node is read-only. Cryptographic quality depends on the host's random device implementation. - [pthread_atfork](../../../core/libs/prx/libkernel/Pthread/Posix/Atfork.cpp) (libkernel) appends the three handlers to a process-wide list and returns 0, or `ENOMEM` when the entry cannot be allocated, as FreeBSD 11.4's [`_pthread_atfork`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_fork.c#L78-L100) does; null handlers are accepted. The handlers never run: `fork` throws and `vfork` is absent, so no child process is created. Handlers of a module that is unloaded stay in the list, where FreeBSD's `__pthread_cxa_finalize` removes them. PPSA97358 has a source-level call from the constructor `rcu_init` ([rcu.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/rcu.c#L488-L495)), but current-main runtime reachability is unverified - [sceKernelGetPageTableStats](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) reports 8192 entries on each side and counts them as a PS5 (firmware 13.60) counted them for a payload process: one cpu entry for each 2 MiB granule that holds a committed mapping, whatever its protection (none included), and one gpu entry for each 1 GiB region that holds a GPU-accessible mapping, plus one for each 2 MiB granule of such a mapping that is not a whole granule of direct memory at a 2 MiB-aligned physical address. A granule or region shared by several mappings counts once, and reservations do not count. On the console the first GPU mapping of a process also takes three gpu entries and one cpu entry that stay after it is unmapped, which is not modelled, and only the ranges whose protection is recorded here are counted. A null pointer returns `SCE_KERNEL_ERROR_EFAULT`, where the console faults. Resonance: A Plague Tale Legacy (PPSA30601) calls it at start-up (#4067) From f75ede7a7f2512eda9e9f7e9cb453aaf195a8cfa Mon Sep 17 00:00:00 2001 From: Alexandre Figueiredo Date: Sun, 25 Oct 2026 22:27:46 +0000 Subject: [PATCH 078/198] docs(debt): close the __isfinitel entry with the bundled libc.prx code --- docs/dev/debt/libc.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 6baa225040..abcd2f986c 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -78,7 +78,6 @@ - [Guest time conversions](../../../core/libs/prx/libc/src/Time.cpp) rewrite the host process `TZ` when a guest changes its own `TZ`, so other host consumers of that variable (including libSceRtc local time) shift too; code outside libc does not take the zone lock. Zone syntax follows the host C runtime, not FreeBSD: Windows only accepts the `std offset [dst]` form, treats an empty guest `TZ` as `UTC0` and reads `:Europe/Berlin` as an abbreviation (for example `rop`); only Linux honours zone file names. If the guest accepts a `TZ` value that the host rejects (on Windows, for example, one longer than the host environment limit), conversions throw `std::runtime_error`. A `tm_zone` saved before a `TZ` change can refer to storage now holding the new zone name; `strftime` does not read it. - [cbrtf](../../../core/libs/prx/libc/src/Math.cpp) (libc) is FreeBSD msun's [`s_cbrtf.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/s_cbrtf.c): a 5-bit first guess from the exponent bits, then two Newton steps in double precision. It sets no `errno`, as the `libc.prx` that titles ship does not. Floating-point exceptions are not compared. It gives the same bits as the `cbrtf` of the `libc.prx` bundled with PPSA07923, PPSA05916 and PPSA13934 for all 2^32 arguments, run on a Windows host. -- [__isfinitel](../../../core/libs/prx/libc/src/Math.cpp) - classifies the x87 80-bit exponent without narrowing to double or performing floating-point comparisons, following [FreeBSD msun](https://github.com/freebsd/freebsd-src/blob/main/lib/msun/src/s_isfinite.c). The signature and classification follow FreeBSD; console behaviour is unmeasured. Requires little-endian x87 long double stored in 16 bytes, as the existing guest long-double ABI does. On a PS5 (firmware 13.60) `__isfinitel` returned 1 for 0, -0, 1, a denormal and ±1e4000, and 0 for a NaN and for the maximum exponent without the integer bit, which this classification gives too - [logf](../../../core/libs/prx/libc/src/Math.cpp) (libc) is FreeBSD msun's [`e_logf.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/e_logf.c), with `vzero` read at run time as msun's `volatile` read does. As in the `libc.prx` that titles ship, `errno` is set to `ERANGE` for ±0 and to `EDOM` for every argument with the sign bit set, negative NaNs and -inf included; positive NaNs and +inf leave it unchanged. Floating-point exceptions are not compared. It gives the same bits and `errno` as the `logf` of the `libc.prx` bundled with PPSA07923 and PPSA05916 for all 2^32 arguments, run on a Windows host. The FMA-contracted build bundled with PPSA13934 differs from it by 1 ulp on 961,881 arguments, so no single port matches every title's last bit. - Windows direct-memory backing reserves a paging-file section. A [direct memory page](../../../core/libs/prx/libc/include/WindowsMappings.hpp) mapped at one guest address is private write-watched memory at that address and is not committed in the section; a page mapped at a second address moves its whole resident run into the section (committed there) and every address becomes a view of it, tracked by write protection. An unmapped resident page that is not all zeros is kept in a private reservation until it is mapped again. Committed section contents remain across unmap/remap and are released when the backing and its remaining views are destroyed. A host import alias over part of a resident run is refused. Linux retains its existing backing. Peak gameplay memory and Windows versions other than the tested host remain unverified. From f940729f007f5cd9cbc36be31251db83cd2d8536 Mon Sep 17 00:00:00 2001 From: orezraey Date: Sun, 25 Oct 2026 19:41:35 -0300 Subject: [PATCH 079/198] docs(libkernel): record the text location, unwind info, SoC id and model queries as measured on a PS5 Co-Authored-By: Claude Opus 5.5 --- docs/dev/debt/libkernel.md | 5 +++-- docs/dev/debt/libkernel_sys.md | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 875084ff1b..a4bd3807c3 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -7,7 +7,7 @@ - [scePthreadMutexattrSetprotocol, scePthreadMutexattrGetprotocol, scePthreadMutexattrSetprioceiling, scePthreadMutexattrGetprioceiling](../../../core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp) (libkernel) - `scePthreadMutexattrSetprotocol` accepts 0 to 2 as a PS5 (firmware 13.60) does and returns `EINVAL` otherwise, but the protocol is not applied: priority inheritance and protection are not modelled. On the console a null or uninitialized mutex attribute returns `EINVAL` from the SCE attribute calls, `scePthreadMutexInit` takes an uninitialized attribute as the default one, and a destroyed mutex returns `EINVAL` from `scePthreadMutexDestroy`; a null mutex pointer crashed the payload and still throws - [pthread_mutexattr_setprotocol, pthread_mutexattr_getprotocol, pthread_mutexattr_setprioceiling, pthread_mutexattr_getprioceiling](../../../core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp) (libkernel) - the protocol and the priority ceiling are stored and reported as in [FreeBSD 11 libthr](https://github.com/freebsd/freebsd-src/blob/releng/11.0/lib/libthr/thread/thr_mutexattr.c#L192-L245) (the ceiling is set or read only under `PTHREAD_PRIO_PROTECT`, and setting a protocol resets it to the highest `SCHED_RR` priority). A null, uninitialized or destroyed attribute returns `EINVAL` before the other arguments are checked; this error behavior is unverified on the console. Mutexes do not apply the protocol or ceiling: a priority-inherit or priority-protect mutex locks like a normal one and does not change its owner's priority - [sceCoredumpSetUserDataType](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel, NID `Uxqkdta7wEg`) - `(int type)` follows the PS5 12.00 `libkernel.sprx` at 0x138B0: the sign-extended `type` goes to syscall 601 (command 29, third argument 0), and the raw result comes back with no SCE code mapping, so a kernel error is -1 with `errno` set. Which types the kernel accepts is not measured. No dump is produced here, so every type returns 0. Palworld (PPSA23126, #2598) and Hello Neighbor (PPSA35154, discussion #6991) import it; #4144 left it throwing because no signature was known -- [sceKernelGetMainSocId and sceKernelGetCpumode](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) both return 0, and [sceKernelGetCpuFrequency](../../../core/libs/prx/libkernel/Time/Time.cpp) returns the host TSC frequency measured for `sceKernelGetTscFrequency`, not the console CPU clock. The PS5's SoC id and CPU mode are unknown; shadPS4's PS4 values are [0x710f10/0x740f30 and 0, 2 or 5 from the param.sfo CPU-mode flags](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1a/src/core/libraries/kernel/process.cpp). PPSA99203 only logs them (`[console] main SoC id: result=%#x out=%#x`, `CPU: %llu Hz, cpumode %d`), passing a pointer that `sceKernelGetMainSocId` leaves untouched +- [sceKernelGetMainSocId and sceKernelGetCpumode](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) both return 0, and [sceKernelGetCpuFrequency](../../../core/libs/prx/libkernel/Time/Time.cpp) returns the host TSC frequency measured for `sceKernelGetTscFrequency`, where a payload process on a CFI-2014 PS5 (firmware 13.60) got 0x840f80, 1 and 800000000. The SoC id may differ between console models, and the CPU mode of a title may differ from a payload's (shadPS4 takes the PS4's from the param.sfo flags), so neither is copied. `sceKernelGetMainSocId` ignores a pointer argument there, as here. PPSA99203 only logs them (`[console] main SoC id: result=%#x out=%#x`, `CPU: %llu Hz, cpumode %d`) - [sceKernelEnableDmemAliasing](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) returns 0 and changes nothing: direct memory can always be mapped more than once here, as `DoMapDirect` in [DirectMemory.cpp](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) only checks that the physical pages are allocated, so there is no aliasing to turn on. [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/kernel/memory.cpp#L39-L43) (PS4) sets a flag there that lifts the `EBUSY` its [`sceKernelMapDirectMemory2`](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/kernel/memory.cpp#L299-L301), and [`sceKernelMapDirectMemory` for SDK 2.50 and later](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/kernel/memory.cpp#L237-L242), return for direct memory that is [already mapped](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/memory.cpp#L548-L553); that check is not applied here. The no-argument `int` signature follows shadPS4 and ProsperoEden (PPSA99008), which calls it before mapping the same direct memory again. Whether the PS5 refuses such a mapping without the call is unverified - [sceKernelInternalMemoryGetAvailableSize](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - internal pool allocation and statistics are not implemented, so a non-null query throws without changing its output. Native null output returns -1; an absent pool returns -2, a failed statistics query returns -3, and success writes total pool bytes minus used bytes. - [sceKernelIsThreadSanitizerEnabled](../../../core/libs/prx/libkernel/System/src/Sanitizer.cpp) - the native zero-argument query returns bit 3 of the process sanitizer flags loaded from sysctl `{1, 14, 41}`. The host always reports disabled because guest sanitizer activation is not modeled. @@ -47,13 +47,14 @@ - [sceCoredumpConfigDumpMode](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel, NID `gzLt9Qrauk0`) - `(int mode)` follows the same `libkernel.sprx` at 0x134D0: a mode other than 1 returns `0x81180000`; mode 1 issues syscall 601 (command 16, arguments 0 and 1) and returns 0 whether or not it fails, since only a raw return of 22 would map to `0x81180000`. No dump is produced here, so mode 1 returns 0 without the syscall. PPSA16833 imports it - [sceCoredumpGetStopInfoGpu_Agc](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel, NID `Jrs7UUkGOFo`) - `(void* info, size_t size)` follows the same `libkernel.sprx` at 0x137E0: a null `info` returns `0x81180000`; syscall 601 (command 21) failing with `EPERM` returns `NOT_IN_COREDUMP_HANDLER`; otherwise two `libSceAgcDriver` imports (NIDs `+NIAY3DWpOw` and `LgctzJSGy7s`, names unknown) decide between `0x81180005` and, for a `size` of 8, storing the second one's 64-bit result and returning 0, with any other size returning `0x81180000`. The handler is never invoked here, so a non-null `info` returns `NOT_IN_COREDUMP_HANDLER` without writing, assuming `EPERM` outside the handler as above; the AGC branch is not reached - [sceKernelInternalMemoryGetModuleSegmentInfo](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - returns the main program's first segment, as [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/kernel/ps4_libkernel.pas#L343-L355) does through its `sceKernelGetLibkernelTextLocation`. In a payload process on a PS5 (firmware 13.60) both returned 0 with address and size 0, and a null result faulted (here it throws); what a title process gets is not known. 44 audited titles that bundle their own `libc.prx` import it; no call has been observed +- [sceKernelGetModuleInfoForUnwind](../../../core/libs/prx/libkernel/Module/src/Module.cpp) (libkernel) - reports the mapped image path as the name and answers for any mapped region, where a payload process on PS5 firmware 13.60 got an empty name for libkernel and libSceLibcInternal and `0x80020003` (ESRCH), with `st_size` cleared to 0, for an address in the payload, which is in no loaded module. Segment 0 matched each module's first segment there - [sceKernelGetModuleInfoForUnwind](../../../core/libs/prx/libkernel/Module/src/Module.cpp) (libkernel) - on Linux the tables come from `sceKernelGetModuleInfoFromAddr`, whose `eh_frame_size` includes the zero terminator, as measured on firmware 13.60 (reported in [#2419](https://github.com/boykopovar/AnyPS5/issues/2419), not reproduced here); on Windows it is assumed to stop before the terminator. LLVM libunwind's `CFI_Parser::findFDE` stops at the terminator or the length, whichever comes first, so it finds the same FDEs either way. Only the main executable and `.guest.prx` images report tables; other host images report none, as on Windows - [sceKernelGetModuleInfoForUnwind](../../../core/libs/prx/libkernel/Module/src/Module.cpp) (libkernel) - on Windows the `.ehmeta` tables and the image file name are cached per allocation base and dropped after a loader free (`dlclose`, `sceKernelStopUnloadModule`, the thread atexit release) when the module is no longer loaded. A table changed in place, or an image freed by anything other than the loader, is not noticed. An address inside an image of the Windows loader's module list whose tables are cached is resolved with `RtlPcToFileHeader` and no `VirtualQuery`; other addresses, and images without tables, still use `VirtualQuery`. The `.eh_frame` walk that finds its end is done once per image instead of once per call - [sceKernelGetModuleInfoFromAddr](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - as in a payload process on a PS5 (firmware 13.60), flags 1 finds any module and flags 2 refuses the system ones (`ESRCH`), here the host libraries; other flags clear the 0x1A8 bytes and return `EINVAL`, any failure clears them, `st_size` is left 0, and a null info returns `EFAULT` (with other flags the console faulted; here it throws). Flags 2 finding a title module was not measured. The layout follows [shadPS4](https://github.com/shadps4-emu/shadPS4). The id is the loader handle or a stable id from that range, and the name is the host file name without `.guest.prx`, where the console keeps `.sprx`. Only the first 4 load segments are reported. `tls_offset` is 0 and `ref_count` 1 (the console gave 5 for system modules). On Windows a relinked image reports its `.elf` sections and the `.ehmeta` eh_frame_hdr; the TLS fields and init/fini are 0 - [sceKernelGetModuleList](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - lists the main program and every loaded `.prx` image in load order; host runtime libraries are left out. The handles are the ids `sceKernelGetModuleInfoFromAddr` reports, not the console's. The `(int32_t* handles, size_t count, size_t* actual)` signature and returning `ENOMEM` without writing `actual` after filling the first `count` handles when more images are loaded follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/kernel/process.cpp#L244-L264); whether the console writes any handles in that case is unknown - [sceKernelGetModuleInfo](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the 0x160-byte layout (`st_size`, `name[256]`, 4 segments, `segment_count`, `fingerprint[20]`) follows [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/sys/sys_types.pas#L319-L337) and [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/module.h#L15-L31). Only that `st_size` is accepted, as in shadPS4; other sizes return `EINVAL`, a null pointer `EFAULT` and a handle `sceKernelGetModuleList` does not list `ESRCH`. Name and segments are the ones `sceKernelGetModuleInfoFromAddr` reports, on Windows from the same PE sections, so `prot` uses the read/write/execute bits 1/2/4, while fpPS4 and shadPS4 store the ELF `p_flags`. The fingerprint is left zero, as the host image does not carry it - [sceKernelGetModuleList2 and sceKernelGetModuleInfo2](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the title-module variants follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/kernel/process.cpp): `sceKernelGetModuleList2` lists what `sceKernelGetModuleList` lists without system libraries, and `sceKernelGetModuleInfo2` answers `EPERM` for a system library after the `EFAULT`, `EINVAL` and `ESRCH` checks of `sceKernelGetModuleInfo`. A system library here is a loaded `.prx` image that is not a relinked title module (`.guest.prx`); the main program and the title's own modules are listed. Not verified on the console. PPSA34063 imports both and fails to load without them -- [sceKernelGetLibkernelTextLocation](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the `(void** address, std::size_t* size)` signature and the choice of the main program's first segment follow [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/kernel/ps4_libkernel.pas#L338-L348); whether the console reports that segment, the text of libkernel itself or the calling module's is unverified. The segment is the first one `sceKernelGetModuleInfo` reports for the main program, and 0 is returned. A null argument throws. +- [sceKernelGetLibkernelTextLocation](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - returns 0 and the main program's first segment, as [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/kernel/ps4_libkernel.pas#L338-L348) does, where a payload process on PS5 firmware 13.60 got 0 with address 0 and size 0, twice, though libkernel's text was at 0x800000000; what a title gets is not measured. A null address or size faulted there and throws here. libkernel.sprx and libkernel_sys.sprx export the function at the same address - [sceKernelReleaseFlexibleMemory](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - on a PS5 (firmware 13.60) the released range stays mapped, with its protection and contents (64 MiB checked after 0.5 s), and a fixed no-overwrite map over it fails with `ENOMEM`, as here. What it does to the flexible budget was not measured, because a payload process has none (`sceKernelAvailableFlexibleMemorySize` did not change on map or release); here the released bytes return to the budget while the pages stay mapped, and a later map over them charges them again. The host pages behind a released range stay committed until `sceKernelMunmap`, as `madvise` keeps them for every advice; freeing them would lose the contents the console still returns. - [sceKernelAvailableFlexibleMemorySize](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - the segments of the title's executable and of its `.guest.prx` modules count against the configured flexible-memory budget (448 MiB by default) while loaded, for this size and for flexible maps. A title may declare a larger budget through `kernel.flexibleMemorySize` in `param.json`; this does not reduce the 13.5 GiB direct-memory size. Rejection of zero or a value not aligned to 16 KiB has not been measured on a console. This segment-accounting model follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/module.cpp#L165), which maps every segment of a module outside its system module directory as flexible memory on the PS4. [Counted](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) are the segments the title's ELF gives permissions, each rounded to 16 KiB pages; not counted are segments without permissions, the relinker's own blocks (on Linux both are the read/write/execute `PT_LOAD`s, plus the header block at file offset 0; on Windows everything outside the `.elf` sections) and the host system libraries. The Linux image carries no marker for the relinker's blocks, so they are told apart by those flags and that offset only: a title segment that is writable and executable, or starts at file offset 0, is not counted. A module whose segments exceed what is left still loads, and the available size then reads 0; shadPS4 fails the segment map instead ([memory.cpp](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/memory.cpp#L519-L524) returns `EINVAL`, [module.cpp](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/module.cpp#L168-L169) asserts). Which segments the PS5 charges, its rounding, whether system modules use the title's budget and when an unload releases the charge are not measured. PPSA15508 subtracts the reported size and a 10 MiB module allowance from its own 448 MiB budget at start-up and stops when the result exceeds its 146 MiB code and data limit ([#2883](https://github.com/boykopovar/AnyPS5/issues/2883)) - [sceKernelAvailableDirectMemorySize](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - its arguments follow a payload process on a PS5 (firmware 13.60) ([probes](https://gist.github.com/orezraey/42a06a7ea0d9fb57ac9c06eaf9e6756c)). One answer is not modelled: in a process that had not yet called `sceKernelGetDirectMemorySize` or otherwise touched direct memory, the console returned `EBADF` (`0x80020009`); here the call always searches the pool. Titles open the direct-memory device during start-up, so they are not expected to see it diff --git a/docs/dev/debt/libkernel_sys.md b/docs/dev/debt/libkernel_sys.md index fd691b6f0d..c852044dd9 100644 --- a/docs/dev/debt/libkernel_sys.md +++ b/docs/dev/debt/libkernel_sys.md @@ -5,6 +5,7 @@ ### Unknown function info +- [sceKernelGetHwModelName, sceKernelIsProspero](../../../core/libs/prx/libkernel_sys/Export.Discovery.cpp) (libkernel_sys) - both throw. On a CFI-2014 PS5 (firmware 13.60) a payload process got 0 from `sceKernelGetHwModelName(char*)`, which wrote "CFI-2014 A01X" and a terminator (14 bytes), and 0 from `sceKernelIsProspero()`. The model string belongs to one console and the buffer size and null behaviour are not known, and 0 from `sceKernelIsProspero` on a PS5 suggests it answers something other than the model, so neither is implemented - [sceKernelGetCompiledSdkVersion](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - which SDK version the console returns is unmeasured. The [PS5 payload SDK](https://github.com/ps5-payload-dev/sdk/blob/a7685ce8e97ce1f8a4f38253a41d54241c9124f1/crt/kernel.c#L257-L275) declares the process parameters with a 32-bit PS4 SDK version at offset `0x10` and a 32-bit PS5 SDK version at `0x14`; this export returns the field at `0x10`, the one [libSceHmd2](../../../core/libs/prx/libSceHmd2/Export.cpp) reads and the one [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/615bcba5e21b82dc59588575f7ecbce8da09ca2c/src/emulator.cpp#L110-L138) (PS4, a single 64-bit `sdk_version` there) reads from the eboot. The `int (int32_t*)` signature and `EINVAL` (`0x80020016`) for a null output follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/615bcba5e21b82dc59588575f7ecbce8da09ca2c/src/core/libraries/kernel/process.cpp#L56-L62). Closes when: measured on a PS5 - [scePthreadCondattrGetclock](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - the `int (const PthreadCondattr*, KernelClockid*)` signature and checks follow FreeBSD 11.4 libthr's [`_pthread_condattr_getclock`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_condattr.c#L82-L90) with the error code of libkernel's `scePthreadCondattrSetclock`: a null or destroyed attribute returns `SCE_KERNEL_ERROR_EINVAL`, and a null clock pointer throws, since libthr writes through it unchecked. Console behaviour is unverified From 5a6dc65069115a0d324f2e06d28f1b20a9ab39ca Mon Sep 17 00:00:00 2001 From: orezraey Date: Sun, 25 Oct 2026 19:47:14 -0300 Subject: [PATCH 080/198] docs(libkernel): record the socket options an accepted socket inherits as measured on a PS5 Co-Authored-By: Claude Opus 5.5 --- docs/dev/debt/libkernel.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 875084ff1b..cb5681a2bd 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -237,14 +237,14 @@ - [kevent on a kqueue](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) - `EVFILT_READ` on a kqueue counts the watched queue's enabled notes that are ready when it is evaluated, where [`filt_kqueue`](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/sys/kern/kern_event.c#L363-L370) reports `kq_count`, the queued notes: a queued note counts until that kqueue is scanned, even once disabled or no longer ready (a socket or pipe read empty, a user note cleared, a watched kqueue drained), so FreeBSD can report it readable where this reports nothing. A queued `EV_ONESHOT` note is reported there with its last count and deleted; here it is evaluated again and, if the watched queue is empty by then, neither reported nor deleted. `EV_CLEAR` throws, as FreeBSD re-arms it only when the watched kqueue queues a note. A cycle of watches throws; a note on the queue itself is `EINVAL`. Not measured on a console - [Socket error numbers](../../../core/libs/prx/libkernel/Socket/include/SocketError.hpp) (libkernel) - a host socket error becomes the FreeBSD 11 `errno.h` number of the same name; the numbers are not checked on the console. Winsock `WSAESHUTDOWN` becomes `EPIPE`, as a send after `shutdown` does on FreeBSD, and a host error with no FreeBSD socket counterpart becomes `EIO`. On Windows a refused nonblocking `connect` is never reported (`SO_ERROR` is unsupported), so a repeated one keeps reporting `EALREADY` where FreeBSD reports `ECONNREFUSED`. A receive after `shutdown(SHUT_RD)` reports `EPIPE` on Windows where FreeBSD returns 0 - [getsockopt, setsockopt](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - a guest socket has the SOL_SOCKET level, eight `IPPROTO_TCP` options, `IPV6_V6ONLY` and ten `IPPROTO_IP` options. `SO_ACCEPTFILTER` (0x1000) fails with `ENOPROTOOPT` (42), where a payload process on a PS5 (firmware 13.60) failed it with `EINVAL` for a set and a get on a non-listening socket, as FreeBSD 11.4 does; the other options of these levels fail with `ENOPROTOOPT` and were not all measured. `SO_KEEPALIVE`, `SO_DONTROUTE`, `SO_BROADCAST`, `SO_OOBINLINE` and `SO_LINGER` act on the host socket, and a boolean one reads back as its own bit, as there. Winsock does not clear a pending error when `SO_ERROR` reads it, so on Windows a pending error is reported once and again only after a new `connect`; a second failure with the same code without a `connect` in between is hidden -- [TCP_NOPUSH, TCP_NOOPT, TCP option 0x80, IP_RECVDSTADDR, IP_RECVIF, IP_RECVTTL](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept and read back as a payload process on a PS5 (firmware 13.60) did: the TCP ones as the bits 0x1000, 0x8 and 0x10000 (so 0x80 is not FreeBSD's `TCP_KEEPINIT` there), the IP ones as 0 or 1. None of them changes what the socket does: `TCP_NOPUSH` does not hold back sends, and the IP ones add no control message to `recvmsg`. `IP_RECVTTL` was only read, never set, on the console, and an accepted socket takes `TCP_NOPUSH` from its listener as FreeBSD 11.4's `syncache_socket` does, which was not measured -- [SO_DEBUG, SO_REUSEADDR, SO_USELOOPBACK, SO_REUSEPORT, SO_TIMESTAMP, SO_BINTIME and option 0x10000](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept as bits that read back as on a PS5 (firmware 13.60, payload process), and options 0x4000 and 0x8000 are accepted and cannot be read, as there; only `SO_REUSEADDR` and `SO_REUSEPORT` change what the socket does. Both are set on the host, `SO_REUSEPORT` as `SO_REUSEADDR` on Windows, which has no `SO_REUSEPORT` and there also lets the socket bind over one that did not set it; Linux spreads unicast datagrams across the sockets, where FreeBSD gives each to one. Option 0x10000 is only kept: if the console treats it as FreeBSD 12's `SO_REUSEPORT_LB`, two sockets that set it share a port there, where the second `bind` here fails with `EADDRINUSE`. An accepted socket takes the bits from its listener, as FreeBSD 11.4's `sonewconn` copies `so_options`; that was not measured +- [TCP_NOPUSH, TCP_NOOPT, TCP option 0x80, IP_RECVDSTADDR, IP_RECVIF, IP_RECVTTL](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept and read back as a payload process on a PS5 (firmware 13.60) did: the TCP ones as the bits 0x1000, 0x8 and 0x10000 (so 0x80 is not FreeBSD's `TCP_KEEPINIT` there), the IP ones as 0 or 1. None of them changes what the socket does: `TCP_NOPUSH` does not hold back sends, and the IP ones add no control message to `recvmsg`. `IP_RECVTTL` was only read, never set, on the console, and an accepted socket takes `TCP_NOPUSH` from its listener, as FreeBSD 11.4's `syncache_socket` does and as measured on that console +- [SO_DEBUG, SO_REUSEADDR, SO_USELOOPBACK, SO_REUSEPORT, SO_TIMESTAMP, SO_BINTIME and option 0x10000](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept as bits that read back as on a PS5 (firmware 13.60, payload process), and options 0x4000 and 0x8000 are accepted and cannot be read, as there; only `SO_REUSEADDR` and `SO_REUSEPORT` change what the socket does. Both are set on the host, `SO_REUSEPORT` as `SO_REUSEADDR` on Windows, which has no `SO_REUSEPORT` and there also lets the socket bind over one that did not set it; Linux spreads unicast datagrams across the sockets, where FreeBSD gives each to one. Option 0x10000 is only kept: if the console treats it as FreeBSD 12's `SO_REUSEPORT_LB`, two sockets that set it share a port there, where the second `bind` here fails with `EADDRINUSE`. An accepted socket takes the bits from its listener, as FreeBSD 11.4's `sonewconn` copies `so_options` and as measured for `SO_REUSEADDR`, `SO_KEEPALIVE` and 0x10000 on that console - [IP_ADD_MEMBERSHIP, IP_DROP_MEMBERSHIP, IP_MULTICAST_IF](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - set on the host socket after the checks a PS5 (firmware 13.60, payload process) showed: `EOPNOTSUPP` on a stream socket, `EINVAL` for a short value or a group outside 224.0.0.0/4, `EADDRINUSE` for a group joined twice and `EADDRNOTAVAIL` for one not joined, tracked per socket. An interface the host lacks fails with the host's error (`EADDRNOTAVAIL` on the console). A group joined through `INADDR_ANY` and through that interface's address counts as two here. Not measured, after FreeBSD 11.4: a drop through `INADDR_ANY` leaves the first such group on any interface (`inp_leave_group`), and the other multicast options fail with `EOPNOTSUPP` on a stream socket (`inp_setmoptions`). An `ip_mreqn` with a non-zero index or address throws, as only a zeroed one was measured, and so does reading `IP_MULTICAST_IF` into 12 bytes, where FreeBSD returns an `ip_mreqn` - [TCP_MAXSEG](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept per stream socket, read as 536 at first and failing with `EINVAL` for 1000, 70000 and 100, as a payload process on a PS5 (firmware 13.60) did. No segment set succeeded there, so the rule here, `EINVAL` above the current segment or below 176 and any segment between accepted, follows FreeBSD 11.4's `tcp_default_ctloutput` (`optval + 40 >= V_tcp_minmss`) and is not measured. An `AF_INET6` socket also starts at 536 here, where FreeBSD's `tcp_newtcpcb` uses 1220; not measured. The segment is not set on the host socket, so the host's segment size is its own, and a connected socket still reads the segment it was given instead of the negotiated one - [TCP_KEEPIDLE, TCP_KEEPINTVL, TCP_KEEPCNT](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept per stream socket and read back as a payload process on a PS5 (firmware 13.60) did: 7200, 75 and 8 at first, 0 restoring a keepalive default, and a count stored as an unsigned value. They are not set on the host socket, so the host's keepalive timers are its own while `SO_KEEPALIVE` is applied there. An interval above 4294967 seconds fails with `EINVAL`, after FreeBSD 11.4's `tcp_default_ctloutput` with `hz` taken as 1000, and an accepted socket takes the keepalive values from its listener as `syncache_socket` copies them; neither was measured - [IP_TTL, IP_TOS, IP_MULTICAST_TTL, IP_MULTICAST_LOOP](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept per `AF_INET` socket with the defaults (64, 0, 1, 1), the low-byte storage of `IP_TTL` and `IP_TOS`, the one-byte forms and the `EINVAL` above 255 of the multicast options that a payload process on a PS5 (firmware 13.60) showed. `IP_TTL` and the multicast options are also set on the host socket, except a time to live of 0, which hosts refuse; `IP_TOS` is only kept, so the host's packets carry its own type of service, and the host's default time to live (128 on Windows) is not changed to 64. An accepted socket takes `IP_TTL` and `IP_TOS` from its listener, as FreeBSD's `syncache_socket` does, and an `int` `IP_MULTICAST_LOOP` keeps only its low byte, as FreeBSD 11.4's `inp_setmoptions` does; neither was measured. Winsock applies `IP_MULTICAST_LOOP` on the receiving socket, not the sender, so on Windows the receiver's setting decides loopback -- [SO_SNDBUF, SO_RCVBUF, SO_SNDLOWAT, SO_RCVLOWAT](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept per socket with the defaults, the 524288-byte limit, the `ENOBUFS` above 5592405 bytes and the low-water clipping of a payload process on a PS5 (firmware 13.60); the host socket gets the size asked for, so a request above the limit still enlarges the host's buffer. The host's real buffering, FreeBSD's automatic growth of a connected TCP socket's buffers, and a low-water mark's effect on `select`, `poll` and a receive are not reproduced. An accepted socket takes all four from its listener, as FreeBSD's [`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L603) does; that was not measured -- [SO_NOSIGPIPE](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept per socket and read back as its bit. On Linux a `send`, `sendto`, `sendmsg` or `write` on that socket passes `MSG_NOSIGNAL` to the host, as [`kern_sendit`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_syscalls.c#L890-L895) and [`soo_write`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_socket.c#L147-L151) leave out the signal, and an accepted socket takes it from its listener, as [`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L603) copies the options. Not measured on a console +- [SO_SNDBUF, SO_RCVBUF, SO_SNDLOWAT, SO_RCVLOWAT](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept per socket with the defaults, the 524288-byte limit, the `ENOBUFS` above 5592405 bytes and the low-water clipping of a payload process on a PS5 (firmware 13.60); the host socket gets the size asked for, so a request above the limit still enlarges the host's buffer. A connected TCP socket keeps the sizes asked for, where that console rounds a buffer larger than the segment up to a multiple of it, as FreeBSD's `tcp_mss` does (a listener's 100000 and 50000 read 114408 and 65376 on the accepted socket, and the client's defaults 49032 and 81720, with a 16344-byte segment). The host's real buffering, FreeBSD's automatic growth of those buffers, and a low-water mark's effect on `select`, `poll` and a receive are not reproduced. An accepted socket takes all four from its listener, as FreeBSD's [`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L603) does and as measured for the low-water marks +- [SO_NOSIGPIPE](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept per socket and read back as its bit. On Linux a `send`, `sendto`, `sendmsg` or `write` on that socket passes `MSG_NOSIGNAL` to the host, as [`kern_sendit`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_syscalls.c#L890-L895) and [`soo_write`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_socket.c#L147-L151) leave out the signal, and an accepted socket takes it from its listener, as [`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L603) copies the options and as measured on PS5 firmware 13.60. Whether a write on it raises `SIGPIPE` there was not measured - [SO_SNDTIMEO, SO_RCVTIMEO](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - the value checks and the 32.32 fixed-point read-back follow [`sosetopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L2657-L2689), as a payload process on a PS5 (firmware 13.60) answered them. A receive or send on a blocking socket waits that long for the host to report the socket readable or writable and fails with `EWOULDBLOCK`, but only before the first transfer: once writable the host send has no limit, where FreeBSD returns the bytes sent so far, and a receive whose data another thread takes after the wait blocks again. `sendfile` ignores the send timeout, the host wait has millisecond steps, and an accepted socket takes both from its listener - [IPV6_V6ONLY](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - the `IPPROTO_IPV6` option 27 is set on the host socket and reads back as 0 or 1, after FreeBSD's [`ip6_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet6/ip6_output.c#L1716-L1732): a value that is not exactly an `int` fails with `EINVAL`, as does setting it on a bound socket, which the host refuses too. `socket` turns it on for every `AF_INET6` socket, as a payload process on a PS5 (firmware 13.60) reported, and Java turns it off on the sockets it opens: Java 25 Advanced Network (PPSA99871) stops at `java.net.SocketException: Protocol not available` without it. The value checks and the bound-socket answer were not measured - [connect to AF_UNSPEC](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) dissolves a datagram association as FreeBSD 11.4's `udp_disconnect()` does, which resets the local address to the wildcard address and keeps the port. On Linux and on Windows a socket bound to an explicit port keeps its local address as well (`127.0.0.1:` stays `127.0.0.1:`), because a bound socket cannot be bound again, and `getsockname` reports that host address. On Windows a `sendto` to a non-loopback address from such a socket then fails with `ENETUNREACH` (Winsock 10051, measured on Windows 10), where FreeBSD sends it from the wildcard address. One bound to port 0 is bound again to the wildcard address with the port it had From 18e69a439f45b17e8690ac4ec23fe869feae7df9 Mon Sep 17 00:00:00 2001 From: orezraey Date: Sun, 25 Oct 2026 19:50:05 -0300 Subject: [PATCH 081/198] docs(libc): record reallocalign to size 0 as measured on a PS5 Co-Authored-By: Claude Opus 5.5 --- docs/dev/debt/libc.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 0609afdb86..d75970fdd7 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -28,7 +28,8 @@ - [sceLibcPafMspaceCreate, Destroy, Malloc, Memalign, PosixMemalign, Free, Calloc, Realloc, Reallocalign, MallocStats and MallocStatsFast](../../../core/libs/prx/libc/src/Mspace.cpp) - PAF exports delegate to `sceLibcMspace` without malloc hooks; `Memalign` and `PosixMemalign` forward to their `Mspace` counterparts with owner ignored and are unverified on console. `Create`'s options block (version, type 1 or 2 at +2, field at +0x10) throws when non-null as its meaning is unknown. Block footers are not modelled, so owners are ignored: on PS5 firmware 13.60 mismatched owners in `Reallocalign` or in-place `Realloc` kill the process and `MallocStats` returns 1 if non-heap-owned blocks exist, which is not reproduced. `MallocUsableSize`, `Trim`, `IsHeapEmpty`, `CheckMemoryBounds`, `ReportMemoryBlocks` and `GetFooterValue` are not implemented. - [malloc_usable_size](../../../core/libs/prx/libc/src/ApplicationHeap.cpp) (libc) - with the default allocator it returns the size the block was requested with (the range registered for it), not the rounded-up chunk size Sony's allocator reports; both satisfy "at least the requested size", and bytes past the request would not be write-watched. With an application allocator it calls entry 9 of the replacement table (`malloc_usable_size` in KytyPS5's `MallocReplace` order) and throws when the table leaves it null, like `malloc_stats_fast` - [malloc_stats_fast](../../../core/libs/prx/libc/src/GuestHeap.cpp) (libc) - with the default allocator it fills the 0x28-byte managed-size block as `sceLibcMspaceMallocStatsFast` does, from the default heap's own accounting, not Sony's allocator: the system sizes count the 1 MiB spans of its small-block classes and its committed large blocks, cached free ones included, and the in-use sizes count each handed-out block at its size-class or 64 KiB-granule size, header included. A block size below 0x28 or a null pointer returns 22, as in the mspace version; the version field is not checked. Ship of Harkinian (PPSA99320) calls it about 10 s after start-up. -- [malloc, calloc, realloc, memalign, aligned_alloc, reallocalign and posix_memalign](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) - the console's own heap was measured in a payload process on firmware 13.60, not in a title. A heap a title replaces was not measured and keeps the FreeBSD jemalloc behaviour: `ENOMEM` on failure, `EINVAL` for an alignment that is zero or not a power of two, and `realloc(ptr, 0)` frees the block. With the console's heap, failures set `EINVAL`, and `errno` is left alone for a size above `SIZE_MAX - 0x1008f` (`SIZE_MAX - (A + 0x100af)` for an alignment over 32 rounded up to A), a `calloc` overflow or an alignment of 2^63 or more. Alignments round up to a power of two, and `realloc(ptr, 0)` keeps the block with 0 usable bytes. `reallocalign(ptr, 0, alignment)` for a block without that alignment was not measured and throws +- [malloc, calloc, realloc, memalign, aligned_alloc, reallocalign and posix_memalign](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) - the console's own heap was measured in a payload process on firmware 13.60, not in a title. A heap a title replaces was not measured and keeps the FreeBSD jemalloc behaviour: `ENOMEM` on failure, `EINVAL` for an alignment that is zero or not a power of two, and `realloc(ptr, 0)` frees the block. With the console's heap, failures set `EINVAL`, and `errno` is left alone for a size above `SIZE_MAX - 0x1008f` (`SIZE_MAX - (A + 0x100af)` for an alignment over 32 rounded up to A), a `calloc` overflow or an alignment of 2^63 or more. Alignments round up to a power of two, and `realloc(ptr, 0)` keeps the block with 0 usable bytes. +- [reallocalign](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) - `reallocalign(ptr, 0, alignment)` for a block without that alignment throws, where the console's heap in a payload process on PS5 firmware 13.60 returned a different block with that alignment and 0 usable bytes, `errno` untouched (a 64-aligned block given 256); whether it frees the old block is not known. There, as here, `reallocalign(ptr, 0, 16)` on a 256-aligned block returned it unchanged, `reallocalign(NULL, 0, 256)` a 256-aligned block with 0 usable bytes, and alignment 3 a moved block that kept its bytes - [reallocf](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libSceLibcInternal, implemented in libc) - follows FreeBSD 11 `lib/libc/stdlib/reallocf.c` on top of this libc's `realloc`, as a payload process on a PS5 (firmware 13.60) does with the default heap (null and 0, size 0, 2^40 with `errno` 22 and the block freed, `SIZE_MAX` with `errno` untouched): size 0 does what `realloc` does here, keeping the block with 0 usable bytes with the default heap and freeing it and returning null with an application allocator, so the block is not freed twice; when `realloc` fails, the block is freed through the same allocator and null is returned with the `errno` that `realloc` left. Other failures (a commit failure that is not out of memory, an unknown pointer) throw, as they do for `realloc` - [sceLibcMspaceReallocalign](../../../core/libs/prx/libc/src/Mspace.cpp) (libSceLibcInternal, implemented in libc) takes `(void* heap, void* block, size_t alignment, size_t size)`, the order of the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x13880, whose internal routine at 0x12390 treats the third argument as the alignment and, for a null block, passes it to the routine `sceLibcMspaceMemalign` uses). Measured on a PS5 (firmware 13.60) from a homebrew payload: a null block with alignment 4096 and size 100 returns a 4096-aligned block; an alignment of 0 or 3 succeeds and 100 gives a 128-aligned block, as the routine raises it to at least 32 and to the next power of two; an alignment of 2^63 or more returns null, keeps the block and leaves `errno` alone; and a size of 0 returns a block (the same one when it is already suitable) instead of freeing it. A block that is aligned and large enough is kept where it is, without giving back the bytes past the new size as the console's in-place resize does - [acosl](../../../core/libs/prx/libc/src/Math.cpp) (libc) - computed by the host's math library, like the other libc math functions here, not by the console's (FreeBSD msun ld80 code in Sony's `libc.prx`): results can differ from the console's in the last bit, and on Linux glibc sets `errno` to `EDOM` for `acosl` outside [-1, 1] where msun only raises the floating-point exception. `long double` is the x87 80-bit format, as Sony's `frexpl` uses it (`fldt 0x8(%rsp)`, result in `st(0)`) From f853f07ae322c1d47b6e5008c364952b3a57dbcd Mon Sep 17 00:00:00 2001 From: orezraey Date: Sun, 25 Oct 2026 20:16:25 -0300 Subject: [PATCH 082/198] docs(libkernel): limit the measured accept inheritance of the SO_ bits to the options the probe set Co-Authored-By: Claude Opus 5.5 --- docs/dev/debt/libkernel.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index cb5681a2bd..1cf956af4f 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -238,7 +238,7 @@ - [Socket error numbers](../../../core/libs/prx/libkernel/Socket/include/SocketError.hpp) (libkernel) - a host socket error becomes the FreeBSD 11 `errno.h` number of the same name; the numbers are not checked on the console. Winsock `WSAESHUTDOWN` becomes `EPIPE`, as a send after `shutdown` does on FreeBSD, and a host error with no FreeBSD socket counterpart becomes `EIO`. On Windows a refused nonblocking `connect` is never reported (`SO_ERROR` is unsupported), so a repeated one keeps reporting `EALREADY` where FreeBSD reports `ECONNREFUSED`. A receive after `shutdown(SHUT_RD)` reports `EPIPE` on Windows where FreeBSD returns 0 - [getsockopt, setsockopt](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - a guest socket has the SOL_SOCKET level, eight `IPPROTO_TCP` options, `IPV6_V6ONLY` and ten `IPPROTO_IP` options. `SO_ACCEPTFILTER` (0x1000) fails with `ENOPROTOOPT` (42), where a payload process on a PS5 (firmware 13.60) failed it with `EINVAL` for a set and a get on a non-listening socket, as FreeBSD 11.4 does; the other options of these levels fail with `ENOPROTOOPT` and were not all measured. `SO_KEEPALIVE`, `SO_DONTROUTE`, `SO_BROADCAST`, `SO_OOBINLINE` and `SO_LINGER` act on the host socket, and a boolean one reads back as its own bit, as there. Winsock does not clear a pending error when `SO_ERROR` reads it, so on Windows a pending error is reported once and again only after a new `connect`; a second failure with the same code without a `connect` in between is hidden - [TCP_NOPUSH, TCP_NOOPT, TCP option 0x80, IP_RECVDSTADDR, IP_RECVIF, IP_RECVTTL](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept and read back as a payload process on a PS5 (firmware 13.60) did: the TCP ones as the bits 0x1000, 0x8 and 0x10000 (so 0x80 is not FreeBSD's `TCP_KEEPINIT` there), the IP ones as 0 or 1. None of them changes what the socket does: `TCP_NOPUSH` does not hold back sends, and the IP ones add no control message to `recvmsg`. `IP_RECVTTL` was only read, never set, on the console, and an accepted socket takes `TCP_NOPUSH` from its listener, as FreeBSD 11.4's `syncache_socket` does and as measured on that console -- [SO_DEBUG, SO_REUSEADDR, SO_USELOOPBACK, SO_REUSEPORT, SO_TIMESTAMP, SO_BINTIME and option 0x10000](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept as bits that read back as on a PS5 (firmware 13.60, payload process), and options 0x4000 and 0x8000 are accepted and cannot be read, as there; only `SO_REUSEADDR` and `SO_REUSEPORT` change what the socket does. Both are set on the host, `SO_REUSEPORT` as `SO_REUSEADDR` on Windows, which has no `SO_REUSEPORT` and there also lets the socket bind over one that did not set it; Linux spreads unicast datagrams across the sockets, where FreeBSD gives each to one. Option 0x10000 is only kept: if the console treats it as FreeBSD 12's `SO_REUSEPORT_LB`, two sockets that set it share a port there, where the second `bind` here fails with `EADDRINUSE`. An accepted socket takes the bits from its listener, as FreeBSD 11.4's `sonewconn` copies `so_options` and as measured for `SO_REUSEADDR`, `SO_KEEPALIVE` and 0x10000 on that console +- [SO_DEBUG, SO_REUSEADDR, SO_USELOOPBACK, SO_REUSEPORT, SO_TIMESTAMP, SO_BINTIME and option 0x10000](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept as bits that read back as on a PS5 (firmware 13.60, payload process), and options 0x4000 and 0x8000 are accepted and cannot be read, as there; only `SO_REUSEADDR` and `SO_REUSEPORT` change what the socket does. Both are set on the host, `SO_REUSEPORT` as `SO_REUSEADDR` on Windows, which has no `SO_REUSEPORT` and there also lets the socket bind over one that did not set it; Linux spreads unicast datagrams across the sockets, where FreeBSD gives each to one. Option 0x10000 is only kept: if the console treats it as FreeBSD 12's `SO_REUSEPORT_LB`, two sockets that set it share a port there, where the second `bind` here fails with `EADDRINUSE`. An accepted socket takes the bits from its listener, as FreeBSD 11.4's `sonewconn` copies `so_options`; measured only for `SO_REUSEADDR` and 0x10000 - [IP_ADD_MEMBERSHIP, IP_DROP_MEMBERSHIP, IP_MULTICAST_IF](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - set on the host socket after the checks a PS5 (firmware 13.60, payload process) showed: `EOPNOTSUPP` on a stream socket, `EINVAL` for a short value or a group outside 224.0.0.0/4, `EADDRINUSE` for a group joined twice and `EADDRNOTAVAIL` for one not joined, tracked per socket. An interface the host lacks fails with the host's error (`EADDRNOTAVAIL` on the console). A group joined through `INADDR_ANY` and through that interface's address counts as two here. Not measured, after FreeBSD 11.4: a drop through `INADDR_ANY` leaves the first such group on any interface (`inp_leave_group`), and the other multicast options fail with `EOPNOTSUPP` on a stream socket (`inp_setmoptions`). An `ip_mreqn` with a non-zero index or address throws, as only a zeroed one was measured, and so does reading `IP_MULTICAST_IF` into 12 bytes, where FreeBSD returns an `ip_mreqn` - [TCP_MAXSEG](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept per stream socket, read as 536 at first and failing with `EINVAL` for 1000, 70000 and 100, as a payload process on a PS5 (firmware 13.60) did. No segment set succeeded there, so the rule here, `EINVAL` above the current segment or below 176 and any segment between accepted, follows FreeBSD 11.4's `tcp_default_ctloutput` (`optval + 40 >= V_tcp_minmss`) and is not measured. An `AF_INET6` socket also starts at 536 here, where FreeBSD's `tcp_newtcpcb` uses 1220; not measured. The segment is not set on the host socket, so the host's segment size is its own, and a connected socket still reads the segment it was given instead of the negotiated one - [TCP_KEEPIDLE, TCP_KEEPINTVL, TCP_KEEPCNT](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept per stream socket and read back as a payload process on a PS5 (firmware 13.60) did: 7200, 75 and 8 at first, 0 restoring a keepalive default, and a count stored as an unsigned value. They are not set on the host socket, so the host's keepalive timers are its own while `SO_KEEPALIVE` is applied there. An interval above 4294967 seconds fails with `EINVAL`, after FreeBSD 11.4's `tcp_default_ctloutput` with `hz` taken as 1000, and an accepted socket takes the keepalive values from its listener as `syncache_socket` copies them; neither was measured From 40619ba1cc683093d75fd6450e052a258673522c Mon Sep 17 00:00:00 2001 From: Alexandre Figueiredo Date: Sun, 25 Oct 2026 23:25:35 +0000 Subject: [PATCH 083/198] fix(libc): return Dinkumware codes from __fpclassifyf --- core/libs/prx/libc/src/Math.cpp | 11 +---------- core/libs/tests/GuestMath.cpp | 3 +-- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index e635b0b1e1..72432ed699 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -1333,16 +1333,7 @@ int APS5_VABI __isnanf_nid_postfix(float x) { return std::isnan(x) ? 1 : 0; } int APS5_VABI isnanf_nid_postfix(float x) { return __isnanf_nid_postfix(x); } int APS5_VABI isnan_nid_postfix(double x) { return __isnan_nid_postfix(x); } int APS5_VABI isinf_nid_postfix(double x) { return __isinf_nid_postfix(x); } -int APS5_VABI __fpclassifyf_nid_postfix(float x) { - constexpr int Infinite = 0x01, NotANumber = 0x02, Normal = 0x04, Subnormal = 0x08, Zero = 0x10; - std::uint32_t bits; - std::memcpy(&bits, &x, sizeof(bits)); - const auto exponent = bits & 0x7f800000u; - const auto fraction = bits & 0x007fffffu; - if (exponent == 0) return fraction != 0 ? Subnormal : Zero; - if (exponent == 0x7f800000u) return fraction != 0 ? NotANumber : Infinite; - return Normal; -} +int APS5_VABI __fpclassifyf_nid_postfix(float x) { return _FDtest_nid_postfix(&x); } int APS5_VABI __signbitf_nid_postfix(float x) { return std::signbit(x) ? 1 : 0; } int APS5_VABI __signbitl_nid_postfix(long double x) { static_assert(sizeof(long double) == 16, "guest long double requires x87 extended precision storage"); diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index 7c6bd1e44e..206a195356 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -1142,8 +1142,7 @@ static void CheckFloatClassification() { std::fprintf(stderr, "Guest _FDtest failed for %08x\n", test.bits); std::abort(); } - const int fpclass = test.code == 0 ? 0x10 : test.code == -2 ? 0x08 : test.code == -1 ? 0x04 : test.code == 1 ? 0x01 : 0x02; - if (__fpclassifyf_nid_postfix(value) != fpclass) { + if (__fpclassifyf_nid_postfix(value) != test.code) { std::fprintf(stderr, "Guest __fpclassifyf failed for %08x\n", test.bits); std::abort(); } From d2bb979ffcd13da56425d3792cc25e0f629831e8 Mon Sep 17 00:00:00 2001 From: harisibrar1 <207127467+harisibrar1@users.noreply.github.com> Date: Sun, 25 Oct 2026 19:33:25 -0400 Subject: [PATCH 084/198] fix(libSceLibreSsl): convert ASN1_STRING_to_UTF8 in two passes Validate the string and count its UTF-8 size first, then make the one guest allocation and write into it, so no std::vector can throw std::bad_alloc past the guest. Approach from @thepoppfish's review. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceLibreSsl/src/Asn1.cpp | 39 ++++++++++++--------- core/libs/prx/libSceLibreSsl/tests/Asn1.cpp | 12 +++++++ 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/core/libs/prx/libSceLibreSsl/src/Asn1.cpp b/core/libs/prx/libSceLibreSsl/src/Asn1.cpp index af75b30fc6..8f6ef75183 100644 --- a/core/libs/prx/libSceLibreSsl/src/Asn1.cpp +++ b/core/libs/prx/libSceLibreSsl/src/Asn1.cpp @@ -8,7 +8,6 @@ #include #include #include -#include namespace { @@ -67,18 +66,20 @@ bool ReadCharacter(std::span& input, int width, std::uint32 return value <= 0x10ffff && (value < 0xd800 || value > 0xdfff); } -void AppendCharacter(std::vector& output, std::uint32_t value) { - const int bytes = value < 0x80 ? 1 : value < 0x800 ? 2 : value < 0x10000 ? 3 : 4; - if (output.size() >= static_cast(std::numeric_limits::max() - bytes)) - throw std::length_error("ASN1_STRING_to_UTF8: output length overflow"); +int CharacterBytes(std::uint32_t value) { + return value < 0x80 ? 1 : value < 0x800 ? 2 : value < 0x10000 ? 3 : 4; +} + +void WriteCharacter(unsigned char*& output, std::uint32_t value) { + const int bytes = CharacterBytes(value); if (bytes == 1) { - output.push_back(static_cast(value)); + *output++ = static_cast(value); return; } constexpr unsigned char prefix[] = {0, 0, 0xc0, 0xe0, 0xf0}; - output.push_back(static_cast(prefix[bytes] | (value >> (6 * (bytes - 1))))); + *output++ = static_cast(prefix[bytes] | (value >> (6 * (bytes - 1)))); for (int index = bytes - 2; index >= 0; --index) - output.push_back(static_cast(0x80 | ((value >> (6 * index)) & 0x3f))); + *output++ = static_cast(0x80 | ((value >> (6 * index)) & 0x3f)); } } // namespace @@ -128,23 +129,29 @@ int APS5_VABI ASN1_STRING_to_UTF8_nid_postfix(unsigned char** output, Asn1String if (width == 2 && length % 2 != 0) return Asn1Error(InvalidBmpStringLength); if (width == 4 && length % 4 != 0) return Asn1Error(InvalidUniversalStringLength); if (!string.data && length != 0) throw std::invalid_argument("ASN1_STRING_to_UTF8: null nonempty string data"); - std::span input(string.data, length); - std::vector converted; - while (!input.empty()) { + const std::span characters(string.data, length); + std::size_t size = 0; + for (auto input = characters; !input.empty();) { std::uint32_t character; if (!ReadCharacter(input, width, character)) return Asn1Error(width == 0 ? InvalidUtf8String : IllegalCharacters); - AppendCharacter(converted, character); + size += static_cast(CharacterBytes(character)); + if (size > static_cast(std::numeric_limits::max() - 1)) throw std::length_error("ASN1_STRING_to_UTF8: output length overflow"); } if (!output) throw std::invalid_argument("ASN1_STRING_to_UTF8: null output"); - auto* result = static_cast(malloc_nid_postfix(converted.size() + 1)); + auto* result = static_cast(malloc_nid_postfix(size + 1)); if (!result) { if (width == 0) Asn1Error(MallocFailure); return Asn1Error(MallocFailure); } - if (!converted.empty()) std::memcpy(result, converted.data(), converted.size()); - result[converted.size()] = 0; + unsigned char* cursor = result; + for (auto input = characters; !input.empty();) { + std::uint32_t character; + ReadCharacter(input, width, character); + WriteCharacter(cursor, character); + } + *cursor = 0; *output = result; - return static_cast(converted.size()); + return static_cast(size); } } diff --git a/core/libs/prx/libSceLibreSsl/tests/Asn1.cpp b/core/libs/prx/libSceLibreSsl/tests/Asn1.cpp index b2c6b3fb53..ff7ded8a89 100644 --- a/core/libs/prx/libSceLibreSsl/tests/Asn1.cpp +++ b/core/libs/prx/libSceLibreSsl/tests/Asn1.cpp @@ -245,6 +245,18 @@ void Allocator() { constexpr unsigned char expected[] = {0xc3, 0xbf}; CheckConversion(20, input, expected); Require(allocationCount == 1 && lastAllocationSize == 3 && freeCount == 1, "conversion uses the application allocator"); + constexpr unsigned char universal[] = {0, 0, 0, 'A', 0, 0, 0x03, 0xa9, 0, 0, 0x20, 0xac, 0, 0x01, 0xf6, 0x00}; + constexpr unsigned char mixed[] = {'A', 0xce, 0xa9, 0xe2, 0x82, 0xac, 0xf0, 0x9f, 0x98, 0x80}; + CheckConversion(28, universal, mixed); + Require(allocationCount == 2 && lastAllocationSize == sizeof(mixed) + 1 && freeCount == 2, + "a conversion with every UTF-8 length makes one allocation of the exact size"); + unsigned char badUtf8[] = {'A', 0xc0, 0x80}; + Asn1String malformed{3, 12, badUtf8, 0}; + unsigned char* untouched = nullptr; + ERR_clear_error_nid_postfix(); + Require(ASN1_STRING_to_UTF8_nid_postfix(&untouched, &malformed) == -1 && untouched == nullptr && allocationCount == 2, + "a malformed string allocates nothing"); + ERR_clear_error_nid_postfix(); failAllocation = true; unsigned char sentinel = 0xab; unsigned char* result = &sentinel; From 0f7056d67bde93eec09453764c5b5ead5c5a98b1 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Fri, 23 Oct 2026 14:07:50 -0400 Subject: [PATCH 085/198] fix(libc): compute logb and logbf with FreeBSD msun's algorithm, as titles' libc.prx does logb and logbf returned the host's logb, which sets ERANGE for a zero argument on Windows and returns signalling NaNs unquieted. The titles' copies are FreeBSD msun's s_logb.c and s_logbf.c, which set no errno and quiet a NaN through x * x; both functions now compute that algorithm. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libc/src/Math.cpp | 30 +++++++++++++++++-- core/libs/tests/GuestMath.cpp | 52 +++++++++++++++++++++++++++++++++ docs/dev/debt/libc.md | 1 + 3 files changed, 81 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index 30b5412c7a..5c7d1f2ad3 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -348,7 +348,20 @@ double APS5_VABI hypot_nid_postfix(double x, double y) { return std::hypot(x, y); } float APS5_VABI log10f_nid_postfix(float x) { return std::log10(x); } -float APS5_VABI logbf_nid_postfix(float x) { return std::logb(x); } +float APS5_VABI logbf_nid_postfix(float x) { + const std::uint32_t bits = std::bit_cast(x) & 0x7fffffffu; + if (bits == 0) { + return -1.0f / std::fabs(x); + } + if (bits >= 0x7f800000u) { + return x * x; + } + if (bits < 0x00800000u) { + const std::uint32_t scaled = std::bit_cast(x * 0x1p25f) & 0x7fffffffu; + return static_cast(static_cast(scaled >> 23) - 127 - 25); + } + return static_cast(static_cast(bits >> 23) - 127); +} float APS5_VABI exp2f_nid_postfix(float x) { static constexpr double exp2ft[16] = { 0x1.6a09e667f3bcdp-1, 0x1.7a11473eb0187p-1, 0x1.8ace5422aa0dbp-1, 0x1.9c49182a3f090p-1, @@ -1304,7 +1317,20 @@ long double APS5_VABI ldexpl_nid_postfix(long double x, int exp) { return std::l long double APS5_VABI truncl_nid_postfix(long double x) { return std::trunc(x); } long double APS5_VABI ceill_nid_postfix(long double x) { return std::ceil(x); } long double APS5_VABI floorl_nid_postfix(long double x) { return std::floor(x); } -double APS5_VABI logb_nid_postfix(double x) { return std::logb(x); } +double APS5_VABI logb_nid_postfix(double x) { + const std::uint64_t bits = std::bit_cast(x) & 0x7fffffffffffffffull; + if (bits == 0) { + return -1.0 / std::fabs(x); + } + if (bits >= 0x7ff0000000000000ull) { + return x * x; + } + if (bits < 0x0010000000000000ull) { + const std::uint64_t scaled = std::bit_cast(x * 0x1p54) & 0x7fffffffffffffffull; + return static_cast(static_cast(scaled >> 52) - 1023 - 54); + } + return static_cast(static_cast(bits >> 52) - 1023); +} int APS5_VABI ilogb_nid_postfix(double x) { if (x == 0.0) { return INT_MIN; diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index fafb56f58f..ca433f849e 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -1494,6 +1494,57 @@ static void CheckDinkumwareFloatCharacteristics() { for (std::size_t index = 24; index < 32; ++index) Require(reinterpret_cast(table)[index] == 0); } +static void CheckLogbConsoleBits() { + constexpr struct { + std::uint64_t input; + std::uint64_t expected; + } cases[] = { + {0x0000000000000000ull, 0xfff0000000000000ull}, + {0x8000000000000000ull, 0xfff0000000000000ull}, + {0x0000000000000001ull, 0xc090c80000000000ull}, + {0x8000000000000001ull, 0xc090c80000000000ull}, + {0x000fffffffffffffull, 0xc08ff80000000000ull}, + {0x0010000000000000ull, 0xc08ff00000000000ull}, + {0x3fe8000000000000ull, 0xbff0000000000000ull}, + {0x4020000000000000ull, 0x4008000000000000ull}, + {0xc020000000000000ull, 0x4008000000000000ull}, + {0x7fefffffffffffffull, 0x408ff80000000000ull}, + {0x7ff0000000000000ull, 0x7ff0000000000000ull}, + {0xfff0000000000000ull, 0x7ff0000000000000ull}, + {0x7ff0000000000001ull, 0x7ff8000000000001ull}, + {0xfff4000000000123ull, 0xfffc000000000123ull}, + {0x7ff8000000000456ull, 0x7ff8000000000456ull}, + }; + for (const auto& item : cases) { + errno = 1234; + Require(std::bit_cast(logb_nid_postfix(std::bit_cast(item.input))) == item.expected && errno == 1234); + } + constexpr struct { + std::uint32_t input; + std::uint32_t expected; + } floatCases[] = { + {0x00000000u, 0xff800000u}, + {0x80000000u, 0xff800000u}, + {0x00000001u, 0xc3150000u}, + {0x80000001u, 0xc3150000u}, + {0x007fffffu, 0xc2fe0000u}, + {0x00800000u, 0xc2fc0000u}, + {0x3f400000u, 0xbf800000u}, + {0x41000000u, 0x40400000u}, + {0xc1000000u, 0x40400000u}, + {0x7f7fffffu, 0x42fe0000u}, + {0x7f800000u, 0x7f800000u}, + {0xff800000u, 0x7f800000u}, + {0x7f800001u, 0x7fc00001u}, + {0xffa00123u, 0xffe00123u}, + {0x7fc00456u, 0x7fc00456u}, + }; + for (const auto& item : floatCases) { + errno = 1234; + Require(std::bit_cast(logbf_nid_postfix(std::bit_cast(item.input))) == item.expected && errno == 1234); + } +} + int main() { CheckFloatClassification(); CheckDinkumwareConstants(); @@ -1660,6 +1711,7 @@ int main() { CheckFegetenv(); CheckLongDouble(); CheckNearbyintl(); + CheckLogbConsoleBits(); CheckExpm1ConsoleBits(); CheckLog1pConsoleBits(); CheckAsinl(); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index b9ada6aa5b..37f8241a91 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -184,3 +184,4 @@ - [fprintf_s, vfprintf_s, vprintf_s, fwprintf_s, wprintf_s, vfwprintf_s and vwprintf_s](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) follow C11 K.3.5.3.1, K.3.5.3.10 and K.3.9.1.1 with the constraints of `snprintf_s` and `swprintf_s`: a null stream, format or argument list, any `%n`, and a null `%s`, `%ls` or `%S` argument return -1 and write nothing, because the output is formatted completely before it is written. A violation only returns: the constraint handler is not invoked, `errno` is untouched, and the value is the one the narrow `snprintf_s` returns on this branch, not a result measured for a stream function on a PS5. A closed stream sets `EBADF` (9) as `fprintf` does. `vprintf_s` and `vwprintf_s` write to the guest standard output stream, and the wide forms count characters while the narrow ones count bytes, as their unchecked siblings do - [scanf_s, fscanf_s, vscanf_s and vfscanf_s](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) run the scanner of `sscanf_s` over the stream, taking the size of `%s`, `%c` and `%[` as `unsigned int` after the pointer, so everything listed for `sscanf_s` applies, including the count of items converted before an undersized destination, whose first element is set to zero. Each directive is read from the stream with the host `fscanf`, with one character of look-ahead pushed back, so the stream stops after the last character a conversion used. A null stream, null format or null destination returns `EOF`, a closed stream also sets `EBADF`, and the constraint handler is not invoked. The 16-bit wide conversions (`%ls`, `%lc`, `%l[`, `%S`, `%C`) throw on Linux, as in `fscanf`. None of the stream forms were measured on a PS5, and the size type follows `sscanf_s` rather than `rsize_t` - [freopen_s](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) follows C11 K.3.5.2.2 through `freopen`: a null result, mode or stream returns `EINVAL` (22) with `*result` set to null when it exists, and a null name is passed to `freopen`, which reports mode-only reopening as unsupported (`ENOTSUP`, 45) with `*result` null, and the constraint handler is not invoked, as in `fopen_s`. A successful reopen stores the stream in `*result` and returns 0, and a failed one stores null and returns the errno `freopen` left, closing the stream as `freopen` does. The console's return values are unverified +- [logb and logbf](../../../core/libs/prx/libc/src/Math.cpp) (libc) do not compare floating-point exceptions, and their double inputs are checked by sampling, not exhaustively. They are FreeBSD msun's [`s_logb.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/s_logb.c) and [`s_logbf.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/s_logbf.c): ±0 returns `-inf` without `errno`, and a signalling NaN is returned quieted. The `libc.prx` bundled with eleven titles (PPSA04158 to PPSA35169) gives the same bits and no `errno` on 4,032,768 inputs per function on a Windows host. From 4ccb760d12ecb78bdbe3e02b2eb0e8cf4438a4e7 Mon Sep 17 00:00:00 2001 From: Gruzzberry Date: Sun, 25 Oct 2026 22:46:48 +0000 Subject: [PATCH 086/198] fix(libc): realpath resolves symbolic links like FreeBSD Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libc/src/RealPath.cpp | 119 ++++++++++++++++++++++++++-- core/libs/tests/GuestRealpath.cpp | 55 +++++++++++++ docs/dev/debt/libc.md | 2 +- 3 files changed, 169 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libc/src/RealPath.cpp b/core/libs/prx/libc/src/RealPath.cpp index 92c58204d9..2404bbcfbd 100644 --- a/core/libs/prx/libc/src/RealPath.cpp +++ b/core/libs/prx/libc/src/RealPath.cpp @@ -2,6 +2,11 @@ #include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libc/include/General.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#ifdef _WIN32 +#include +#include +#endif +#include #include #include #include @@ -22,6 +27,7 @@ constexpr int NotDirectory = 20; constexpr int InvalidArgument = 22; constexpr int TooManyLinks = 62; constexpr int NameTooLong = 63; +constexpr unsigned MaxSymbolicLinks = 32; int StatusError(const std::error_code& error) { if (error == std::errc::no_such_file_or_directory) return NoEntry; @@ -48,11 +54,70 @@ void DropLastComponent(std::string& result) { bool IsSeparator(char character) { return Separators.find(character) != std::string_view::npos; } +bool ReadGuestLink(const std::filesystem::path& host, std::string& target, bool& isLink, bool& absolute) { + isLink = false; + absolute = false; +#ifdef _WIN32 + const DWORD attributes = GetFileAttributesW(host.c_str()); + if (attributes == INVALID_FILE_ATTRIBUTES || (attributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0) return true; + const HANDLE handle = CreateFileW(host.c_str(), 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, + FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr); + if (handle == INVALID_HANDLE_VALUE) return true; + struct ReparseHeader { + ULONG tag; + USHORT dataLength; + USHORT reserved; + USHORT substituteNameOffset; + USHORT substituteNameLength; + USHORT printNameOffset; + USHORT printNameLength; + ULONG flags; + } header{}; + alignas(8) std::array data{}; + DWORD size = 0; + const BOOL read = DeviceIoControl(handle, FSCTL_GET_REPARSE_POINT, nullptr, 0, data.data(), static_cast(data.size()), &size, nullptr); + CloseHandle(handle); + if (!read || size < sizeof(header)) return true; + std::memcpy(&header, data.data(), sizeof(header)); + if (header.tag != IO_REPARSE_TAG_SYMLINK) return true; + constexpr ULONG relativeFlag = 1; + if ((header.flags & relativeFlag) == 0) { + absolute = true; + return true; + } + isLink = true; + const std::size_t begin = sizeof(header) + header.substituteNameOffset; + if (header.substituteNameLength % sizeof(wchar_t) != 0 || begin + header.substituteNameLength > size) return false; + std::wstring name(header.substituteNameLength / sizeof(wchar_t), L'\0'); + if (!name.empty()) std::memcpy(name.data(), data.data() + begin, header.substituteNameLength); + if (!name.empty()) { + const int length = WideCharToMultiByte(CP_UTF8, 0, name.data(), static_cast(name.size()), nullptr, 0, nullptr, nullptr); + target.assign(static_cast(length), '\0'); + WideCharToMultiByte(CP_UTF8, 0, name.data(), static_cast(name.size()), target.data(), length, nullptr, nullptr); + } +#else + std::error_code code; + if (!std::filesystem::is_symlink(std::filesystem::symlink_status(host, code))) return true; + isLink = true; + target = std::filesystem::read_symlink(host, code).generic_string(); + if (code) return false; +#endif + absolute = !target.empty() && IsSeparator(target.front()); +#ifdef _WIN32 + absolute = absolute || (target.size() > 1 && target[1] == ':'); +#endif + if (absolute) { + isLink = false; + target.clear(); + } + return true; +} + bool ResolveGuest(const char* path, std::string& result, int& error) { - std::string_view left(path); + std::string left(path); if (IsSeparator(left.front())) { result = "/"; - left.remove_prefix(1); + left.erase(0, 1); } else { char current[PathMax]; if (getcwd_nid_postfix(current, sizeof(current)) == nullptr) { @@ -66,11 +131,13 @@ bool ResolveGuest(const char* path, std::string& result, int& error) { error = NameTooLong; return false; } + unsigned links = 0; + bool expandLinks = true; while (!left.empty()) { const auto separator = left.find_first_of(Separators); - const auto token = left.substr(0, separator); - const bool moreFollows = separator != std::string_view::npos; - left = moreFollows ? left.substr(separator + 1) : std::string_view(); + const std::string token = left.substr(0, separator); + const bool moreFollows = separator != std::string::npos; + left = moreFollows ? left.substr(separator + 1) : std::string(); if (result.size() > 1 && result.size() + 1 >= PathMax) { error = NameTooLong; return false; @@ -88,8 +155,48 @@ bool ResolveGuest(const char* path, std::string& result, int& error) { error = NameTooLong; return false; } + const auto host = ResolvePath_nid_no_patch(candidate.c_str()); + std::string target; + bool isLink = false; + bool absolute = false; + if (expandLinks && !ReadGuestLink(host, target, isLink, absolute)) { + error = InputOutput; + return false; + } + if (absolute) expandLinks = false; + if (isLink) { + if (links++ > MaxSymbolicLinks) { + error = TooManyLinks; + return false; + } + if (target.empty()) { + error = NoEntry; + return false; + } + if (target.size() >= PathMax) { + error = NameTooLong; + return false; + } + DropLastComponent(result); + if (moreFollows) { + if (!IsSeparator(target.back())) { + if (target.size() + 1 >= PathMax) { + error = NameTooLong; + return false; + } + target += '/'; + } + target += left; + if (target.size() >= PathMax) { + error = NameTooLong; + return false; + } + } + left = target; + continue; + } std::error_code code; - const auto status = std::filesystem::status(ResolvePath_nid_no_patch(candidate.c_str()), code); + const auto status = std::filesystem::status(host, code); if (code && code != std::errc::no_such_file_or_directory && code != std::errc::not_a_directory) { error = StatusError(code); return false; diff --git a/core/libs/tests/GuestRealpath.cpp b/core/libs/tests/GuestRealpath.cpp index 52240b0a7f..ec6494ff2b 100644 --- a/core/libs/tests/GuestRealpath.cpp +++ b/core/libs/tests/GuestRealpath.cpp @@ -11,6 +11,10 @@ #include #include #include +#include +#ifdef _WIN32 +#include +#endif static void Require(bool value) { if (!value) std::abort(); } @@ -95,6 +99,57 @@ int main() { *__error_nid_postfix() = 0; Require(realpath_nid_postfix((guest + "/missing").c_str(), nullptr) == nullptr && *__error_nid_postfix() == 2); + const auto createLink = [&](const std::filesystem::path& link, const std::string& target, bool toDirectory) { +#ifdef _WIN32 + return CreateSymbolicLinkW(link.c_str(), std::filesystem::path(target).make_preferred().c_str(), + (toDirectory ? SYMBOLIC_LINK_FLAG_DIRECTORY : 0) | SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) != 0; +#else + std::error_code linkError; + if (toDirectory) std::filesystem::create_directory_symlink(target, link, linkError); + else std::filesystem::create_symlink(target, link, linkError); + return !linkError; +#endif + }; + std::vector files, directories; + const auto fileLink = [&](const std::filesystem::path& link, const std::string& target) { + files.push_back(link); + return createLink(link, target, false); + }; + if (fileLink(directory / "file-link", "file.txt")) { + directories.push_back(directory / "directory-link"); + Require(createLink(directory / "directory-link", "sub", true)); + Require(fileLink(directory / "dangling-link", "missing")); + Require(fileLink(directory / "loop-a", "loop-b") && fileLink(directory / "loop-b", "loop-a")); + Require(fileLink(directory / "chain-link", "file-link")); + Require(fileLink(directory / "sub" / "up-link", "../file.txt")); + Require(Resolves(guest + "/file-link", guest + "/file.txt")); + Require(Resolves(guest + "/directory-link", guest + "/sub")); + Require(Resolves(guest + "/directory-link/inner.txt", guest + "/sub/inner.txt")); + Require(Resolves(guest + "/directory-link/../file.txt", guest + "/file.txt")); + Require(Resolves(guest + "/chain-link", guest + "/file.txt")); + Require(Resolves(guest + "/sub/up-link", guest + "/file.txt")); + Require(Resolves(guest + "/directory-link/up-link", guest + "/file.txt")); + Require(Resolves(guest + "/directory-link/", guest + "/sub")); + directories.push_back(directory / "absolute-link"); + Require(createLink(directory / "absolute-link", (directory / "sub").string(), true)); + Require(Resolves(guest + "/absolute-link/inner.txt", guest + "/absolute-link/inner.txt")); + Require(Resolves(guest + "/absolute-link/up-link", guest + "/absolute-link/up-link")); + Require(Fails((guest + "/dangling-link").c_str(), 2, guest + "/missing")); + Require(Fails((guest + "/dangling-link/x").c_str(), 2, guest + "/missing")); + Require(Fails((guest + "/loop-a").c_str(), 62)); + Require(Fails((guest + "/file-link/x").c_str(), 20)); + Require(Fails((guest + "/file-link/").c_str(), 20)); + for (int index = 0; index < 34; ++index) { + Require(fileLink(directory / ("long-" + std::to_string(index)), index == 33 ? "file.txt" : "long-" + std::to_string(index + 1))); + } + Require(Resolves(guest + "/long-1", guest + "/file.txt")); + Require(Fails((guest + "/long-0").c_str(), 62)); + } +#ifdef _WIN32 + for (const auto& link : files) DeleteFileW(link.c_str()); + for (const auto& link : directories) RemoveDirectoryW(link.c_str()); +#endif + std::filesystem::remove_all(directory); return 0; } diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index c648774059..6fe7642829 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -97,7 +97,7 @@ - [sinf, cosf, tanf and sincosf](../../../core/libs/prx/libc/src/Math.cpp) (libc) are FreeBSD msun's [`s_sinf.c`, `s_cosf.c` and `s_tanf.c`](https://github.com/freebsd/freebsd-src/tree/release/11.4.0/lib/msun/src) over its double-precision kernels and `e_rem_pio2f.c`, with `k_rem_pio2.c` (for `|x| >= 2^28 * pi/2`) ported only for one input word and `prec` 0; `sincosf` returns the two. As in the `libc.prx` that titles ship, only an infinite argument sets `errno` (`EDOM`); NaN leaves it unchanged. Floating-point exceptions are not compared. All four give the same bits and `errno` as the copies bundled with PPSA07923, PPSA05916 and PPSA13934 for all 2^32 arguments, run on a Windows host. - [fgetln](../../../core/libs/prx/libc/src/StandardStreams.cpp) (libSceLibcInternal, implemented in libc) throws: in the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x5FE80) the export is a `hlt` instruction, which faults in user mode and ends the process - [Wide-character conversions](../../../core/libs/prx/libc/src/Multibyte.cpp) (libc) - The wide `wcsto*` functions parse the ASCII prefix of the guest string, as the C locale accepts only ASCII white space and digits. The multibyte conversions follow the C locale of the console's `libSceLibcInternal.sprx`: UTF-8 of at most three bytes, so `_Getmbcurmax` returns 3, overlong forms and encoded surrogates decode, and four-byte forms are invalid. `__mb_cur_max` stays 1 like the console's variable; `___mb_cur_max` is not in that module and also stays 1, unverified. Other locales are not supported. With a null destination, `wcstombs` returns the byte count; the console's count is wrong when the capacity argument equals the running count, because it reads past the terminator -- [realpath](../../../core/libs/prx/libc/src/RealPath.cpp) (libc) follows FreeBSD 11's [`realpath.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libc/stdlib/realpath.c) except for symlinks. The console's guest filesystem has none, so a host symlink under the guest root is followed for existence and type but not expanded, and `..` drops the previous component lexically, as `Resolve` does for every guest path. Through a host link `linkdir -> sub/deep`, `linkdir/../x` gives `x` beside the link, not beside its target. +- [realpath](../../../core/libs/prx/libc/src/RealPath.cpp) (libc) follows FreeBSD 11's [`realpath.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libc/stdlib/realpath.c) with two gaps. A host symbolic link with a relative target under the guest root is expanded as `realpath1` does (a dangling link fails with `ENOENT`, more than 32 links with `ELOOP`), but a link with an absolute target is not expanded, nor is anything below it, because the target is a host path with no guest path to map to. Only Windows symbolic links are expanded there, not junctions. Expansion works on the guest path, so a host link whose text climbs above a path alias root is resolved against the guest tree, not the host one. Otherwise `..` drops the previous component lexically, as `Resolve` does for every guest path. - [strtod, strtof, strtold](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - `strtod` and `wcstod` (and `atof`, which calls `strtod` here) read a `NAN(...)` sequence as FreeBSD 11's gdtoa [`hexnan`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/contrib/gdtoa/hexnan.c) does: hexadecimal digits with an optional `0x`, blanks starting a new 32-bit word, the first 16 digits kept and the payload cut to 52 bits, a zero payload made 1, any other character giving the default NaN but consuming up to the closing parenthesis, and an unterminated sequence keeping its payload while consuming only `nan`. The payload goes under the quiet bit with the sign, as in gdtoa's [`strtod`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/contrib/gdtoa/strtod.c#L316-L328). `strtof`, `wcstof`, `strtold` and `wcstold` return the positive default quiet NaN (`0x7fc00000`; exponent `0x7fff`, significand `0xc000000000000000`) whatever the sign and the sequence, and consume a sequence of letters, digits and underscores up to its closing parenthesis. The console's `libSceLibcInternal.sprx`, called from a payload process on a PS5 (firmware 12.70), gives these results for the 64 NaN inputs tried through `strtod` and `strtof`, 50 of them also through `strtold` and `wcstod`, and 18 through `wcstof` and `wcstold`, and, through `strtod`, for `nan(1 2)` and for `nan( ` followed by NUL and `)`; that a title's own `libc.prx` does the same is assumed. In `hexnan`, a blank after a payload digit, while a 32-bit word of the payload is still free, starts a blank skip that has no NUL check, so when only blanks follow up to the terminating NUL the console's `strtod` reads past it: `nan(1 ` followed by NUL and `)` consumes the `)` after the NUL, so the result depends on the bytes after the string, and here that case throws. A blank right after `nan(` is skipped with a NUL check, and the console stopped at the NUL there - [remquol](../../../core/libs/prx/libc/src/MathExact.cpp) (libc) leaves `*quo` unchanged when the result is NaN; the `libc.prx` bundled with PPSA08427 writes a value that depends on earlier x87 state (`remquol(1, 0)` stored 0 on a first call and 4 on later ones) - [_Stod, _Stodx, _Stofx, _Stoldx, _Stof and _Stold](../../../core/libs/prx/libc/src/DinkumwareInternals.cpp) (libSceLibcInternal, implemented in libc) throw for hexadecimal text with a non-zero power-of-ten scale. The console's result there does not follow a simple rule: `0x3` with a scale of -1 gives 0.29999999999999999. A nonzero subnormal result sets neither `errno` nor the error argument, as measured in firmware 13.60's `libSceLibcInternal.sprx` run in a Linux host process, not on a console; there about a fifth of decimal inputs with a subnormal result come out one unit apart in the last place (`_Stodx("2.2250738585072014e-308", 0)` is the largest subnormal), which is not reproduced From 9a914b76aa7dd3103f91511acf93b9fa10934a82 Mon Sep 17 00:00:00 2001 From: Rookie <175737347+rooklz@users.noreply.github.com> Date: Sat, 24 Oct 2026 20:25:34 +0100 Subject: [PATCH 087/198] fix(libSceCesCs): map the GB 18030 profiles as the console does --- core/libs/prx/libSceCesCs/Export.cpp | 45 ++++++---- core/libs/prx/libSceCesCs/Gb18030.hpp | 68 --------------- core/libs/tests/GuestCesChinese.cpp | 114 +++++++++++++++----------- docs/dev/debt/libSceCesCs.md | 2 +- 4 files changed, 93 insertions(+), 136 deletions(-) diff --git a/core/libs/prx/libSceCesCs/Export.cpp b/core/libs/prx/libSceCesCs/Export.cpp index 54697f632f..5ef4d69230 100644 --- a/core/libs/prx/libSceCesCs/Export.cpp +++ b/core/libs/prx/libSceCesCs/Export.cpp @@ -356,11 +356,17 @@ constexpr uint32_t GB_CELLS = 126 * 190; struct Gb18030Tables { std::span twoByte; std::span fourByte; - std::span fallback; + bool edition2005; }; -constexpr Gb18030Tables GB18030_2000{Gb18030::TWO_BYTE_2000, Gb18030::FOUR_BYTE_2000, {}}; -constexpr Gb18030Tables GB18030_2005{Gb18030::TWO_BYTE_2005, Gb18030::FOUR_BYTE_2005, Gb18030::FALLBACK_2005}; +constexpr Gb18030Tables GB18030_2000{Gb18030::TWO_BYTE_2000, Gb18030::FOUR_BYTE_2000, false}; +constexpr Gb18030Tables GB18030_2005{Gb18030::TWO_BYTE_2000, Gb18030::FOUR_BYTE_2000, true}; +constexpr uint32_t GB18030_SWAPPED_CELL = (0xA8 - 0x81) * 190 + (0xBC - 0x41); +constexpr uint32_t GB18030_SWAPPED_LINEAR = (5 * 126 + (0xF4 - 0x81)) * 10 + 7; + +bool mapsGb18030Supplementary(const Gb18030Tables& tables, uint32_t code) { + return tables.edition2005 && code >= 0x20000 && code <= 0x2FFFF; +} const Gb18030Tables* gb18030Tables(const void* profile) { if (profile == nullptr) return nullptr; @@ -385,10 +391,20 @@ const uint32_t* findRun(std::span runs, uint32_t key) { } uint32_t gb18030Cell(const Gb18030Tables& tables, uint32_t cell) { + if (tables.edition2005 && cell == GB18030_SWAPPED_CELL) return 0x1E3F; const uint32_t* run = findRun(tables.twoByte, cell); return run ? run[1] + cell - run[0] : Cp936::TO_UCS2[cell]; } +void putGbLinear(uint32_t linear, uint8_t* mbcs) { + mbcs[3] = static_cast(0x30 + linear % 10); + linear /= 10; + mbcs[2] = static_cast(0x81 + linear % 126); + linear /= 126; + mbcs[1] = static_cast(0x30 + linear % 10); + mbcs[0] = static_cast(0x81 + linear / 10); +} + void putGbCell(uint32_t cell, uint8_t* mbcs) { const uint32_t column = cell % 190; mbcs[0] = static_cast(0x81 + cell / 190); @@ -412,8 +428,9 @@ int decodeGb18030(const Gb18030Tables& tables, const uint8_t* mbcs, uint32_t mbc const uint32_t linear = (((lead - 0x81) * 10 + (second - 0x30)) * 126 + (mbcs[2] - 0x81)) * 10 + (mbcs[3] - 0x30); const uint32_t* run = findRun(tables.fourByte, linear); if (!run) return unassigned(4, length); + code = tables.edition2005 && linear == GB18030_SWAPPED_LINEAR ? 0xE7C7 : run[1] + linear - run[0]; + if (code > 0xFFFF && !mapsGb18030Supplementary(tables, code)) return unassigned(4, length); length = 4; - code = run[1] + linear - run[0]; return 0; } if (second < 0x40 || second == 0x7F || second == 0xFF) return rejectTrail(second, 2, length); @@ -448,22 +465,16 @@ int encodeGb18030(const Gb18030Tables& tables, uint32_t code, uint8_t* mbcs, uin length = 2; return 0; } - for (const auto& run : tables.fourByte) { - if (code - run[1] >= run[2]) continue; - uint32_t linear = run[0] + code - run[1]; - mbcs[3] = static_cast(0x30 + linear % 10); - linear /= 10; - mbcs[2] = static_cast(0x81 + linear % 126); - linear /= 126; - mbcs[1] = static_cast(0x30 + linear % 10); - mbcs[0] = static_cast(0x81 + linear / 10); + if (code > 0xFFFF && !mapsGb18030Supplementary(tables, code)) return CES_ERROR_UNASSIGNED_CODE; + if (tables.edition2005 && code == 0xE7C7) { + putGbLinear(GB18030_SWAPPED_LINEAR, mbcs); length = 4; return 0; } - for (const auto& entry : tables.fallback) { - if (entry[0] != code) continue; - putGbCell(entry[1], mbcs); - length = 2; + for (const auto& run : tables.fourByte) { + if (code - run[1] >= run[2]) continue; + putGbLinear(run[0] + code - run[1], mbcs); + length = 4; return 0; } return CES_ERROR_UNASSIGNED_CODE; diff --git a/core/libs/prx/libSceCesCs/Gb18030.hpp b/core/libs/prx/libSceCesCs/Gb18030.hpp index 5f13d60813..2927db6663 100644 --- a/core/libs/prx/libSceCesCs/Gb18030.hpp +++ b/core/libs/prx/libSceCesCs/Gb18030.hpp @@ -64,72 +64,4 @@ inline constexpr uint32_t FOUR_BYTE_2000[207][3] = { {0x9961, 0xFF5F, 0x81}, {0x99E2, 0xFFE6, 0x1A}, {0x2E248, 0x10000, 0x100000}, }; -inline constexpr uint32_t TWO_BYTE_2005[118][3] = { - {0x17C0, 0xE4C6, 0x60}, {0x187E, 0xE526, 0x60}, {0x18E8, 0xE766, 0x6}, {0x1920, 0x20AC, 0x1}, {0x1921, 0xE76D, 0x1}, {0x192C, 0xE76E, 0x2}, - {0x193A, 0xE770, 0x2}, {0x193C, 0xE586, 0x60}, {0x19FA, 0xE5E6, 0x60}, {0x1AAD, 0xE772, 0xB}, {0x1AB8, 0xE646, 0x60}, {0x1B6E, 0xE77D, 0x8}, - {0x1B76, 0xE6A6, 0x60}, {0x1BEE, 0xE785, 0x8}, {0x1C0E, 0xFE10, 0x1}, {0x1C0F, 0xFE12, 0x1}, {0x1C10, 0xFE11, 0x1}, {0x1C11, 0xFE13, 0x4}, - {0x1C21, 0xFE17, 0x2}, {0x1C28, 0xFE19, 0x1}, {0x1C2B, 0xE797, 0x9}, {0x1C34, 0xE706, 0x60}, {0x1CB5, 0xE7A0, 0xF}, {0x1CE5, 0xE7AF, 0xD}, - {0x1D47, 0xE7BC, 0xB}, {0x1D6D, 0x1E3F, 0x1}, {0x1D70, 0x1F9, 0x1}, {0x1D72, 0xE7C9, 0x4}, {0x1D9B, 0xE7CD, 0x15}, {0x1DC8, 0xE7E2, 0x1}, - {0x1DCB, 0xE7E3, 0x1}, {0x1DCD, 0xE7E4, 0x3}, {0x1DF8, 0x303E, 0x1}, {0x1DF9, 0x2FF0, 0xC}, {0x1E06, 0xE7F4, 0xD}, {0x1E5F, 0xE801, 0xF}, - {0x1ECE, 0xE000, 0x5E}, {0x1F8C, 0xE05E, 0x5E}, {0x204A, 0xE0BC, 0x5E}, {0x2108, 0xE11A, 0x5E}, {0x21C6, 0xE178, 0x5E}, {0x2284, 0xE1D6, 0x5E}, - {0x408D, 0xE810, 0x5}, {0x58B2, 0xE234, 0x5E}, {0x5970, 0xE292, 0x5E}, {0x5A2E, 0xE2F0, 0x5E}, {0x5AEC, 0xE34E, 0x5E}, {0x5BAA, 0xE3AC, 0x5E}, - {0x5C68, 0xE40A, 0x5E}, {0x5CD6, 0x2E81, 0x1}, {0x5CD7, 0x20087, 0x1}, {0x5CD8, 0x20089, 0x1}, {0x5CD9, 0x200CC, 0x1}, {0x5CDA, 0x2E84, 0x1}, - {0x5CDB, 0x3473, 0x1}, {0x5CDC, 0x3447, 0x1}, {0x5CDD, 0x2E88, 0x1}, {0x5CDE, 0x2E8B, 0x1}, {0x5CDF, 0x9FB4, 0x1}, {0x5CE0, 0x359E, 0x1}, - {0x5CE1, 0x361A, 0x1}, {0x5CE2, 0x360E, 0x1}, {0x5CE3, 0x2E8C, 0x1}, {0x5CE4, 0x2E97, 0x1}, {0x5CE5, 0x396E, 0x1}, {0x5CE6, 0x3918, 0x1}, - {0x5CE7, 0x9FB5, 0x1}, {0x5CE8, 0x39CF, 0x1}, {0x5CE9, 0x39DF, 0x1}, {0x5CEA, 0x3A73, 0x1}, {0x5CEB, 0x39D0, 0x1}, {0x5CEC, 0x9FB6, 0x2}, - {0x5CEE, 0x3B4E, 0x1}, {0x5CEF, 0x3C6E, 0x1}, {0x5CF0, 0x3CE0, 0x1}, {0x5CF1, 0x2EA7, 0x1}, {0x5CF2, 0x215D7, 0x1}, {0x5CF3, 0x9FB8, 0x1}, - {0x5CF4, 0x2EAA, 0x1}, {0x5CF5, 0x4056, 0x1}, {0x5CF6, 0x415F, 0x1}, {0x5CF7, 0x2EAE, 0x1}, {0x5CF8, 0x4337, 0x1}, {0x5CF9, 0x2EB3, 0x1}, - {0x5CFA, 0x2EB6, 0x2}, {0x5CFC, 0x2298F, 0x1}, {0x5CFD, 0x43B1, 0x1}, {0x5CFE, 0x43AC, 0x1}, {0x5CFF, 0x2EBB, 0x1}, {0x5D00, 0x43DD, 0x1}, - {0x5D01, 0x44D6, 0x1}, {0x5D02, 0x4661, 0x1}, {0x5D03, 0x464C, 0x1}, {0x5D04, 0x9FB9, 0x1}, {0x5D05, 0x4723, 0x1}, {0x5D06, 0x4729, 0x1}, - {0x5D07, 0x477C, 0x1}, {0x5D08, 0x478D, 0x1}, {0x5D09, 0x2ECA, 0x1}, {0x5D0A, 0x4947, 0x1}, {0x5D0B, 0x497A, 0x1}, {0x5D0C, 0x497D, 0x1}, - {0x5D0D, 0x4982, 0x2}, {0x5D0F, 0x4985, 0x2}, {0x5D11, 0x499F, 0x1}, {0x5D12, 0x499B, 0x1}, {0x5D13, 0x49B7, 0x1}, {0x5D14, 0x49B6, 0x1}, - {0x5D15, 0x9FBA, 0x1}, {0x5D16, 0x241FE, 0x1}, {0x5D17, 0x4CA3, 0x1}, {0x5D18, 0x4C9F, 0x3}, {0x5D1B, 0x4C77, 0x1}, {0x5D1C, 0x4CA2, 0x1}, - {0x5D1D, 0x4D13, 0x7}, {0x5D24, 0x4DAE, 0x1}, {0x5D25, 0x9FBB, 0x1}, {0x5D26, 0xE468, 0x5E}, -}; - -inline constexpr uint32_t FOUR_BYTE_2005[209][3] = { - {0x0, 0x80, 0x24}, {0x24, 0xA5, 0x2}, {0x26, 0xA9, 0x7}, {0x2D, 0xB2, 0x5}, {0x32, 0xB8, 0x1F}, {0x51, 0xD8, 0x8}, - {0x59, 0xE2, 0x6}, {0x5F, 0xEB, 0x1}, {0x60, 0xEE, 0x4}, {0x64, 0xF4, 0x3}, {0x67, 0xF8, 0x1}, {0x68, 0xFB, 0x1}, - {0x69, 0xFD, 0x4}, {0x6D, 0x102, 0x11}, {0x7E, 0x114, 0x7}, {0x85, 0x11C, 0xF}, {0x94, 0x12C, 0x18}, {0xAC, 0x145, 0x3}, - {0xAF, 0x149, 0x4}, {0xB3, 0x14E, 0x1D}, {0xD0, 0x16C, 0x62}, {0x132, 0x1CF, 0x1}, {0x133, 0x1D1, 0x1}, {0x134, 0x1D3, 0x1}, - {0x135, 0x1D5, 0x1}, {0x136, 0x1D7, 0x1}, {0x137, 0x1D9, 0x1}, {0x138, 0x1DB, 0x1}, {0x139, 0x1DD, 0x1C}, {0x155, 0x1FA, 0x57}, - {0x1AC, 0x252, 0xF}, {0x1BB, 0x262, 0x65}, {0x220, 0x2C8, 0x1}, {0x221, 0x2CC, 0xD}, {0x22E, 0x2DA, 0xB7}, {0x2E5, 0x3A2, 0x1}, - {0x2E6, 0x3AA, 0x7}, {0x2ED, 0x3C2, 0x1}, {0x2EE, 0x3CA, 0x37}, {0x325, 0x402, 0xE}, {0x333, 0x450, 0x1}, {0x334, 0x452, 0x19ED}, - {0x1D21, 0xE7C7, 0x1}, {0x1D22, 0x1E40, 0x1D0}, {0x1EF2, 0x2011, 0x2}, {0x1EF4, 0x2017, 0x1}, {0x1EF5, 0x201A, 0x2}, {0x1EF7, 0x201E, 0x7}, - {0x1EFE, 0x2027, 0x9}, {0x1F07, 0x2031, 0x1}, {0x1F08, 0x2034, 0x1}, {0x1F09, 0x2036, 0x5}, {0x1F0E, 0x203C, 0x70}, {0x1F7E, 0x20AD, 0x56}, - {0x1FD4, 0x2104, 0x1}, {0x1FD5, 0x2106, 0x3}, {0x1FD8, 0x210A, 0xC}, {0x1FE4, 0x2117, 0xA}, {0x1FEE, 0x2122, 0x3E}, {0x202C, 0x216C, 0x4}, - {0x2030, 0x217A, 0x16}, {0x2046, 0x2194, 0x2}, {0x2048, 0x219A, 0x6E}, {0x20B6, 0x2209, 0x6}, {0x20BC, 0x2210, 0x1}, {0x20BD, 0x2212, 0x3}, - {0x20C0, 0x2216, 0x4}, {0x20C4, 0x221B, 0x2}, {0x20C6, 0x2221, 0x2}, {0x20C8, 0x2224, 0x1}, {0x20C9, 0x2226, 0x1}, {0x20CA, 0x222C, 0x2}, - {0x20CC, 0x222F, 0x5}, {0x20D1, 0x2238, 0x5}, {0x20D6, 0x223E, 0xA}, {0x20E0, 0x2249, 0x3}, {0x20E3, 0x224D, 0x5}, {0x20E8, 0x2253, 0xD}, - {0x20F5, 0x2262, 0x2}, {0x20F7, 0x2268, 0x6}, {0x20FD, 0x2270, 0x25}, {0x2122, 0x2296, 0x3}, {0x2125, 0x229A, 0xB}, {0x2130, 0x22A6, 0x19}, - {0x2149, 0x22C0, 0x52}, {0x219B, 0x2313, 0x14D}, {0x22E8, 0x246A, 0xA}, {0x22F2, 0x249C, 0x64}, {0x2356, 0x254C, 0x4}, {0x235A, 0x2574, 0xD}, - {0x2367, 0x2590, 0x3}, {0x236A, 0x2596, 0xA}, {0x2374, 0x25A2, 0x10}, {0x2384, 0x25B4, 0x8}, {0x238C, 0x25BE, 0x8}, {0x2394, 0x25C8, 0x3}, - {0x2397, 0x25CC, 0x2}, {0x2399, 0x25D0, 0x12}, {0x23AB, 0x25E6, 0x1F}, {0x23CA, 0x2607, 0x2}, {0x23CC, 0x260A, 0x36}, {0x2402, 0x2641, 0x1}, - {0x2403, 0x2643, 0x83E}, {0x2C41, 0x2E82, 0x2}, {0x2C43, 0x2E85, 0x3}, {0x2C46, 0x2E89, 0x2}, {0x2C48, 0x2E8D, 0xA}, {0x2C52, 0x2E98, 0xF}, - {0x2C61, 0x2EA8, 0x2}, {0x2C63, 0x2EAB, 0x3}, {0x2C66, 0x2EAF, 0x4}, {0x2C6A, 0x2EB4, 0x2}, {0x2C6C, 0x2EB8, 0x3}, {0x2C6F, 0x2EBC, 0xE}, - {0x2C7D, 0x2ECB, 0x125}, {0x2DA2, 0x2FFC, 0x4}, {0x2DA6, 0x3004, 0x1}, {0x2DA7, 0x3018, 0x5}, {0x2DAC, 0x301F, 0x2}, {0x2DAE, 0x302A, 0x14}, - {0x2DC2, 0x303F, 0x2}, {0x2DC4, 0x3094, 0x7}, {0x2DCB, 0x309F, 0x2}, {0x2DCD, 0x30F7, 0x5}, {0x2DD2, 0x30FF, 0x6}, {0x2DD8, 0x312A, 0xF6}, - {0x2ECE, 0x322A, 0x7}, {0x2ED5, 0x3232, 0x71}, {0x2F46, 0x32A4, 0xEA}, {0x3030, 0x3390, 0xC}, {0x303C, 0x339F, 0x2}, {0x303E, 0x33A2, 0x22}, - {0x3060, 0x33C5, 0x9}, {0x3069, 0x33CF, 0x2}, {0x306B, 0x33D3, 0x2}, {0x306D, 0x33D6, 0x71}, {0x30DE, 0x3448, 0x2B}, {0x3109, 0x3474, 0x12A}, - {0x3233, 0x359F, 0x6F}, {0x32A2, 0x360F, 0xB}, {0x32AD, 0x361B, 0x2FD}, {0x35AA, 0x3919, 0x55}, {0x35FF, 0x396F, 0x60}, {0x365F, 0x39D1, 0xE}, - {0x366D, 0x39E0, 0x93}, {0x3700, 0x3A74, 0xDA}, {0x37DA, 0x3B4F, 0x11F}, {0x38F9, 0x3C6F, 0x71}, {0x396A, 0x3CE1, 0x375}, {0x3CDF, 0x4057, 0x108}, - {0x3DE7, 0x4160, 0x1D7}, {0x3FBE, 0x4338, 0x74}, {0x4032, 0x43AD, 0x4}, {0x4036, 0x43B2, 0x2B}, {0x4061, 0x43DE, 0xF8}, {0x4159, 0x44D7, 0x175}, - {0x42CE, 0x464D, 0x14}, {0x42E2, 0x4662, 0xC1}, {0x43A3, 0x4724, 0x5}, {0x43A8, 0x472A, 0x52}, {0x43FA, 0x477D, 0x10}, {0x440A, 0x478E, 0x1B9}, - {0x45C3, 0x4948, 0x32}, {0x45F5, 0x497B, 0x2}, {0x45F7, 0x497E, 0x4}, {0x45FB, 0x4984, 0x1}, {0x45FC, 0x4987, 0x14}, {0x4610, 0x499C, 0x3}, - {0x4613, 0x49A0, 0x16}, {0x4629, 0x49B8, 0x2BF}, {0x48E8, 0x4C78, 0x27}, {0x490F, 0x4CA4, 0x6F}, {0x497E, 0x4D1A, 0x94}, {0x4A12, 0x4DAF, 0x51}, - {0x4A63, 0x9FA6, 0x385A}, {0x82BD, 0xE76C, 0x1}, {0x82BE, 0xE7C8, 0x1}, {0x82BF, 0xE7E7, 0xD}, {0x82CC, 0xE815, 0x1}, {0x82CD, 0xE819, 0x5}, - {0x82D2, 0xE81F, 0x7}, {0x82D9, 0xE827, 0x4}, {0x82DD, 0xE82D, 0x4}, {0x82E1, 0xE833, 0x8}, {0x82E9, 0xE83C, 0x7}, {0x82F0, 0xE844, 0x10}, - {0x8300, 0xE856, 0xE}, {0x830E, 0xE865, 0x10C7}, {0x93D5, 0xF92D, 0x4C}, {0x9421, 0xF97A, 0x1B}, {0x943C, 0xF996, 0x51}, {0x948D, 0xF9E8, 0x9}, - {0x9496, 0xF9F2, 0x1A}, {0x94B0, 0xFA10, 0x1}, {0x94B1, 0xFA12, 0x1}, {0x94B2, 0xFA15, 0x3}, {0x94B5, 0xFA19, 0x6}, {0x94BB, 0xFA22, 0x1}, - {0x94BC, 0xFA25, 0x2}, {0x94BE, 0xFA2A, 0x406}, {0x98C4, 0xFE32, 0x1}, {0x98C5, 0xFE45, 0x4}, {0x98C9, 0xFE53, 0x1}, {0x98CA, 0xFE58, 0x1}, - {0x98CB, 0xFE67, 0x1}, {0x98CC, 0xFE6C, 0x95}, {0x9961, 0xFF5F, 0x81}, {0x99E2, 0xFFE6, 0x1A}, {0x2E248, 0x10000, 0x100000}, -}; - -inline constexpr uint32_t FALLBACK_2005[24][2] = { - {0xE78D, 0x1C0E}, {0xE78E, 0x1C0F}, {0xE78F, 0x1C10}, {0xE790, 0x1C11}, {0xE791, 0x1C12}, {0xE792, 0x1C13}, - {0xE793, 0x1C14}, {0xE794, 0x1C21}, {0xE795, 0x1C22}, {0xE796, 0x1C28}, {0xE816, 0x5CD7}, {0xE817, 0x5CD8}, - {0xE818, 0x5CD9}, {0xE81E, 0x5CDF}, {0xE826, 0x5CE7}, {0xE82B, 0x5CEC}, {0xE82C, 0x5CED}, {0xE831, 0x5CF2}, - {0xE832, 0x5CF3}, {0xE83B, 0x5CFC}, {0xE843, 0x5D04}, {0xE854, 0x5D15}, {0xE855, 0x5D16}, {0xE864, 0x5D25}, -}; - } diff --git a/core/libs/tests/GuestCesChinese.cpp b/core/libs/tests/GuestCesChinese.cpp index 424a2a27bd..d5c3ba5365 100644 --- a/core/libs/tests/GuestCesChinese.cpp +++ b/core/libs/tests/GuestCesChinese.cpp @@ -1,10 +1,13 @@ +#include #include "prx/libSceCesCs/include/Api.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include +#include #include #include -static void Require(bool value) { if (!value) std::abort(); } +static void RequireImpl(bool value, int line) { if (!value) { std::fprintf(stderr, "REQFAIL line %d\n", line); std::fflush(stderr); std::abort(); } } +#define Require(v) RequireImpl((v), __LINE__) static const std::uint8_t* Bytes(const char* text) { return reinterpret_cast(text); } @@ -44,24 +47,18 @@ static constexpr Gb18030Sequence GB18030_BOUNDARIES[88] = { {0x8336CF39, 0xE863, 0xE863}, {0x8336D030, 0xE865, 0xE865}, {0x84308534, 0xF92B, 0xF92B}, {0x84308535, 0xF92D, 0xF92D}, {0x84309C37, 0xFA26, 0xFA26}, {0x84309C38, 0xFA2A, 0xFA2A}, {0x84318537, 0xFE2F, 0xFE2F}, {0x84318538, 0xFE32, 0xFE32}, {0x8431A233, 0xFFDF, 0xFFDF}, {0x8431A234, 0xFFE6, 0xFFE6}, {0x8431A439, 0xFFFF, 0xFFFF}, {0x8431A530, 0x0, 0x0}, - {0x8F39FE39, 0x0, 0x0}, {0x90308130, 0x10000, 0x10000}, {0x95328235, 0x1FFFF, 0x1FFFF}, {0x95328236, 0x20000, 0x20000}, - {0x9A348431, 0x2FFFF, 0x2FFFF}, {0x9A348432, 0x30000, 0x30000}, {0x9F368537, 0x3FFFF, 0x3FFFF}, {0x9F368538, 0x40000, 0x40000}, - {0xA4388733, 0x4FFFF, 0x4FFFF}, {0xA4388734, 0x50000, 0x50000}, {0xAA308839, 0x5FFFF, 0x5FFFF}, {0xAA308930, 0x60000, 0x60000}, - {0xAF328A35, 0x6FFFF, 0x6FFFF}, {0xAF328A36, 0x70000, 0x70000}, {0xB4348C31, 0x7FFFF, 0x7FFFF}, {0xB4348C32, 0x80000, 0x80000}, - {0xB9368D37, 0x8FFFF, 0x8FFFF}, {0xB9368D38, 0x90000, 0x90000}, {0xBE388F33, 0x9FFFF, 0x9FFFF}, {0xBE388F34, 0xA0000, 0xA0000}, - {0xC4309039, 0xAFFFF, 0xAFFFF}, {0xC4309130, 0xB0000, 0xB0000}, {0xC9329235, 0xBFFFF, 0xBFFFF}, {0xC9329236, 0xC0000, 0xC0000}, - {0xCE349431, 0xCFFFF, 0xCFFFF}, {0xCE349432, 0xD0000, 0xD0000}, {0xD3369537, 0xDFFFF, 0xDFFFF}, {0xD3369538, 0xE0000, 0xE0000}, - {0xD8389733, 0xEFFFF, 0xEFFFF}, {0xD8389734, 0xF0000, 0xF0000}, {0xDE309839, 0xFFFFF, 0xFFFFF}, {0xDE309930, 0x100000, 0x100000}, - {0xE3329A35, 0x10FFFF, 0x10FFFF}, {0xE3329A36, 0x0, 0x0}, {0xFD308130, 0x0, 0x0}, {0xFE39FE39, 0x0, 0x0}, + {0x8F39FE39, 0x0, 0x0}, {0x90308130, 0x0, 0x0}, {0x95328235, 0x0, 0x0}, {0x95328236, 0x0, 0x20000}, + {0x9A348431, 0x0, 0x2FFFF}, {0x9A348432, 0x0, 0x0}, {0x9F368537, 0x0, 0x0}, {0x9F368538, 0x0, 0x0}, + {0xA4388733, 0x0, 0x0}, {0xA4388734, 0x0, 0x0}, {0xAA308839, 0x0, 0x0}, {0xAA308930, 0x0, 0x0}, + {0xAF328A35, 0x0, 0x0}, {0xAF328A36, 0x0, 0x0}, {0xB4348C31, 0x0, 0x0}, {0xB4348C32, 0x0, 0x0}, + {0xB9368D37, 0x0, 0x0}, {0xB9368D38, 0x0, 0x0}, {0xBE388F33, 0x0, 0x0}, {0xBE388F34, 0x0, 0x0}, + {0xC4309039, 0x0, 0x0}, {0xC4309130, 0x0, 0x0}, {0xC9329235, 0x0, 0x0}, {0xC9329236, 0x0, 0x0}, + {0xCE349431, 0x0, 0x0}, {0xCE349432, 0x0, 0x0}, {0xD3369537, 0x0, 0x0}, {0xD3369538, 0x0, 0x0}, + {0xD8389733, 0x0, 0x0}, {0xD8389734, 0x0, 0x0}, {0xDE309839, 0x0, 0x0}, {0xDE309930, 0x0, 0x0}, + {0xE3329A35, 0x0, 0x0}, {0xE3329A36, 0x0, 0x0}, {0xFD308130, 0x0, 0x0}, {0xFE39FE39, 0x0, 0x0}, }; -static constexpr Gb18030Difference GB18030_DIFFERENCES[26] = { - {0xA6D9, 0xE78D, 0xFE10, true}, {0xA6DA, 0xE78E, 0xFE12, true}, {0xA6DB, 0xE78F, 0xFE11, true}, {0xA6DC, 0xE790, 0xFE13, true}, - {0xA6DD, 0xE791, 0xFE14, true}, {0xA6DE, 0xE792, 0xFE15, true}, {0xA6DF, 0xE793, 0xFE16, true}, {0xA6EC, 0xE794, 0xFE17, true}, - {0xA6ED, 0xE795, 0xFE18, true}, {0xA6F3, 0xE796, 0xFE19, true}, {0xA8BC, 0xE7C7, 0x1E3F, false}, {0xFE51, 0xE816, 0x20087, true}, - {0xFE52, 0xE817, 0x20089, true}, {0xFE53, 0xE818, 0x200CC, true}, {0xFE59, 0xE81E, 0x9FB4, true}, {0xFE61, 0xE826, 0x9FB5, true}, - {0xFE66, 0xE82B, 0x9FB6, true}, {0xFE67, 0xE82C, 0x9FB7, true}, {0xFE6C, 0xE831, 0x215D7, true}, {0xFE6D, 0xE832, 0x9FB8, true}, - {0xFE76, 0xE83B, 0x2298F, true}, {0xFE7E, 0xE843, 0x9FB9, true}, {0xFE90, 0xE854, 0x9FBA, true}, {0xFE91, 0xE855, 0x241FE, true}, - {0xFEA0, 0xE864, 0x9FBB, true}, {0x8135F437, 0x1E3F, 0xE7C7, false}, +static constexpr Gb18030Difference GB18030_DIFFERENCES[2] = { + {0xA8BC, 0xE7C7, 0x1E3F, false}, {0x8135F437, 0x1E3F, 0xE7C7, false}, }; static std::uint32_t Gb18030Bytes(std::uint32_t linear) { return (0x81 + linear / 12600) << 24 | (0x30 + linear / 1260 % 10) << 16 | (0x81 + linear / 10 % 126) << 8 | (0x30 + linear % 10); @@ -92,7 +89,9 @@ static std::uint32_t Gb18030Decode(const void* gb, std::uint32_t packed) { static std::uint32_t Gb18030Encode(const void* gb, std::uint32_t code) { std::uint8_t bytes[4] = {}; std::uint32_t size = 0; - Require(sceCesUtf32ToGb(code, gb, bytes, sizeof(bytes), &size) == 0 && (size == 2 || size == 4)); + const int result = sceCesUtf32ToGb(code, gb, bytes, sizeof(bytes), &size); + if (result == unassignedCode) return 0; + Require(result == 0 && (size == 2 || size == 4)); std::uint32_t packed = 0; for (std::uint32_t i = 0; i < size; ++i) packed = packed << 8 | bytes[i]; return packed; @@ -149,10 +148,10 @@ static void CheckGb18030(const void* gb, bool revision2005, std::uint32_t twoByt Fnv(hash, Gb18030Decode(gb, Gb18030Bytes(linear))); ++samples; } - Require(hash == 0x9E77B552 && samples == 1052); + Require(hash == (revision2005 ? 0x0F212E49u : 0x9832CC85u) && samples == 1052); hash = 0x811C9DC5; for (std::uint32_t code = 0x10000; code < 0x110000; code += 997) Fnv(hash, Gb18030Encode(gb, code)); - Require(hash == 0x3D088D7E); + Require(hash == (revision2005 ? 0x2EB90A7Bu : 0x9832CC85u)); for (const Gb18030Sequence& entry : GB18030_BOUNDARIES) { const std::uint32_t code = revision2005 ? entry.code2005 : entry.code2000; @@ -188,55 +187,70 @@ static void CheckGb18030(const void* gb, bool revision2005, std::uint32_t twoByt std::uint32_t consumed = 0x55; std::uint32_t produced = 0x55; std::uint16_t ucs2 = 0xBEEF; - Require(sceCesGbToUcs2(gb, Bytes("\x90\x30\x81\x30"), 4, &consumed, &ucs2) == unassignedCode && consumed == 0x55 && ucs2 == 0xBEEF); std::uint8_t utf8[4] = {1, 2, 3, 4}; - Require(sceCesGbToUtf8(gb, Bytes("\x90\x30\x81\x30"), 4, &consumed, utf8, 3, &produced) == dstBufferEnd && consumed == 0x55 && produced == 0x55); - Require(std::memcmp(utf8, "\x01\x02\x03\x04", 4) == 0); - Require(sceCesGbToUtf8(gb, Bytes("\x90\x30\x81\x30"), 4, &consumed, utf8, 4, &produced) == 0 && consumed == 4 && produced == 4); - Require(std::memcmp(utf8, "\xF0\x90\x80\x80", 4) == 0); std::uint16_t utf16[2] = {0xBEEF, 0xBEEF}; - produced = 0x55; - Require(sceCesGbToUtf16(gb, Bytes("\xE3\x32\x9A\x35"), 4, &consumed, utf16, 1, &produced) == dstBufferEnd && produced == 0x55 && utf16[0] == 0xBEEF); - Require(sceCesGbToUtf16(gb, Bytes("\xE3\x32\x9A\x35"), 4, &consumed, utf16, 2, &produced) == 0 && produced == 2 && utf16[0] == 0xDBFF && utf16[1] == 0xDFFF); - Require(sceCesGbToUtf16be(gb, Bytes("\x90\x30\x81\x30"), 4, &consumed, utf16, 2, &produced) == 0 && std::memcmp(utf16, "\xD8\x00\xDC\x00", 4) == 0); - Require(sceCesMbcToUtf16le(gb, Bytes("\x94\x39\xFC\x36"), 4, &consumed, utf16, 2, &produced) == 0 && consumed == 4 && std::memcmp(utf16, "\x3D\xD8\x00\xDE", 4) == 0); + for (const char* refused : {"\x90\x30\x81\x30", "\x94\x39\xFC\x36", "\xE3\x32\x9A\x35"}) { + Require(sceCesGbToUtf8(gb, Bytes(refused), 4, &consumed, utf8, 4, &produced) == unassignedCode && consumed == 4 && produced == 0); + } + consumed = 0x55; + ucs2 = 0xBEEF; + Require(sceCesGbToUcs2(gb, Bytes("\x95\x32\x82\x36"), 4, &consumed, &ucs2) == unassignedCode); + if (revision2005) { + Require(consumed == 0x55 && ucs2 == 0xBEEF); + consumed = 0x55; + produced = 0x55; + Require(sceCesGbToUtf8(gb, Bytes("\x95\x32\x82\x36"), 4, &consumed, utf8, 3, &produced) == dstBufferEnd && consumed == 0x55 && produced == 0x55); + Require(sceCesGbToUtf8(gb, Bytes("\x95\x32\x82\x36"), 4, &consumed, utf8, 4, &produced) == 0 && consumed == 4 && produced == 4); + Require(std::memcmp(utf8, "\xF0\xA0\x80\x80", 4) == 0); + produced = 0x55; + Require(sceCesGbToUtf16(gb, Bytes("\x9A\x34\x84\x31"), 4, &consumed, utf16, 1, &produced) == dstBufferEnd && produced == 0x55 && utf16[0] == 0xBEEF); + Require(sceCesGbToUtf16(gb, Bytes("\x9A\x34\x84\x31"), 4, &consumed, utf16, 2, &produced) == 0 && produced == 2 && utf16[0] == 0xD87F && utf16[1] == 0xDFFF); + Require(sceCesGbToUtf16be(gb, Bytes("\x95\x32\x82\x36"), 4, &consumed, utf16, 2, &produced) == 0 && std::memcmp(utf16, "\xD8\x40\xDC\x00", 4) == 0); + } else { + Require(sceCesGbToUtf8(gb, Bytes("\x95\x32\x82\x36"), 4, &consumed, utf8, 4, &produced) == unassignedCode && consumed == 4 && produced == 0); + } Require(sceCesGbToUcs2(gb, Bytes("\xA2\xE3"), 2, &consumed, &ucs2) == 0 && consumed == 2 && ucs2 == 0x20AC); std::uint8_t mbcs[4] = {1, 2, 3, 4}; produced = 0x55; - Require(sceCesUtf32ToGb(0x10000, gb, mbcs, 3, &produced) == dstBufferEnd && produced == 4 && std::memcmp(mbcs, "\x01\x02\x03\x04", 4) == 0); + Require(sceCesUtf32ToGb(0x10000, gb, mbcs, sizeof(mbcs), &produced) == unassignedCode && produced == 0); + produced = 0x55; + Require(sceCesUtf32ToGb(0x20000, gb, mbcs, 3, &produced) == (revision2005 ? dstBufferEnd : unassignedCode) && produced == (revision2005 ? 4u : 0u) && std::memcmp(mbcs, "\x01\x02\x03\x04", 4) == 0); Require(sceCesUtf32ToGb(0xD800, gb, mbcs, sizeof(mbcs), &produced) == outOfCodeSpace); Require(sceCesUtf32ToGb(0x110000, gb, mbcs, sizeof(mbcs), &produced) == invalidEncode); Require(sceCesUcs2ToGb(0x80, gb, mbcs, sizeof(mbcs), &produced) == 0 && produced == 4 && std::memcmp(mbcs, "\x81\x30\x81\x30", 4) == 0); Require(sceCesUcs2ToGb(0x20AC, gb, mbcs, sizeof(mbcs), &produced) == 0 && produced == 2 && std::memcmp(mbcs, "\xA2\xE3", 2) == 0); - Require(sceCesUtf8ToGb(Bytes("\xF0\x9F\x98\x80"), 4, &consumed, gb, mbcs, sizeof(mbcs), &produced) == 0 && consumed == 4 && produced == 4); - Require(std::memcmp(mbcs, "\x94\x39\xFC\x36", 4) == 0); - const std::uint16_t pair[2] = {0xD83D, 0xDE00}; - Require(sceCesUtf16ToGb(pair, 2, &consumed, gb, mbcs, sizeof(mbcs), &produced) == 0 && consumed == 2 && produced == 4 && std::memcmp(mbcs, "\x94\x39\xFC\x36", 4) == 0); + Require(sceCesUtf8ToGb(Bytes("\xF0\x9F\x98\x80"), 4, &consumed, gb, mbcs, sizeof(mbcs), &produced) == unassignedCode); + const std::uint16_t pair[2] = {0xD840, 0xDC00}; + if (revision2005) Require(sceCesUtf16ToGb(pair, 2, &consumed, gb, mbcs, sizeof(mbcs), &produced) == 0 && consumed == 2 && produced == 4 && std::memcmp(mbcs, "\x95\x32\x82\x36", 4) == 0); + else Require(sceCesUtf16ToGb(pair, 2, &consumed, gb, mbcs, sizeof(mbcs), &produced) == unassignedCode); Require(sceCesUtf16ToGb(pair, 1, &consumed, gb, mbcs, sizeof(mbcs), &produced) == srcBufferEnd); alignas(8) std::uint8_t context[64] = {}; Require(sceCesMbcsUcsContextInit(context, gb) == 0); - const char* utf8Text = "A\xE4\xB8\xAD\xC2\x80\xF0\x9F\x98\x80\xEF\xB8\x90"; - const char* gbText = revision2005 ? "A\xD6\xD0\x81\x30\x81\x30\x94\x39\xFC\x36\xA6\xD9" : "A\xD6\xD0\x81\x30\x81\x30\x94\x39\xFC\x36\x84\x31\x82\x36"; - const std::uint32_t gbSize = revision2005 ? 13 : 15; + const char* utf8Text = revision2005 ? "A\xE4\xB8\xAD\xC2\x80\xF0\xA0\x80\x80\xEF\xB8\x90" : "A\xE4\xB8\xAD\xC2\x80\xEF\xB8\x90"; + const std::uint32_t utf8Size = revision2005 ? 13 : 9; + const char* gbText = revision2005 ? "A\xD6\xD0\x81\x30\x81\x30\x95\x32\x82\x36\x84\x31\x82\x36" : "A\xD6\xD0\x81\x30\x81\x30\x84\x31\x82\x36"; + const std::uint32_t gbSize = revision2005 ? 15 : 11; + const std::uint32_t units = revision2005 ? 6 : 4; std::uint8_t text[16] = {}; - Require(sceCesUtf8StrToMbcsStr(context, Bytes(utf8Text), 13, &consumed, text, sizeof(text), &produced) == 0 && consumed == 13 && produced == gbSize); + Require(sceCesUtf8StrToMbcsStr(context, Bytes(utf8Text), utf8Size, &consumed, text, sizeof(text), &produced) == 0 && consumed == utf8Size && produced == gbSize); Require(std::memcmp(text, gbText, gbSize) == 0); std::uint8_t backUtf8[16] = {}; - Require(sceCesMbcsStrToUtf8Str(context, text, gbSize, &consumed, backUtf8, sizeof(backUtf8), &produced) == 0 && consumed == gbSize && produced == 13); - Require(std::memcmp(backUtf8, utf8Text, 13) == 0); + Require(sceCesMbcsStrToUtf8Str(context, text, gbSize, &consumed, backUtf8, sizeof(backUtf8), &produced) == 0 && consumed == gbSize && produced == utf8Size); + Require(std::memcmp(backUtf8, utf8Text, utf8Size) == 0); std::uint16_t backUtf16[8] = {}; - Require(sceCesMbcsStrToUtf16Str(context, text, gbSize, &consumed, backUtf16, 8, &produced) == 0 && consumed == gbSize && produced == 6); - Require(backUtf16[0] == 'A' && backUtf16[1] == 0x4E2D && backUtf16[2] == 0x80 && backUtf16[3] == 0xD83D && backUtf16[4] == 0xDE00 && backUtf16[5] == 0xFE10); - std::uint16_t shortUtf16[4] = {}; - Require(sceCesMbcsStrToUtf16Str(context, text, gbSize, &consumed, shortUtf16, 4, &produced) == dstBufferEnd); - Require(sceCesMbcsStrGetUtf16Len(context, text, gbSize, &consumed, &produced) == 0 && consumed == gbSize && produced == 6); + Require(sceCesMbcsStrToUtf16Str(context, text, gbSize, &consumed, backUtf16, 8, &produced) == 0 && consumed == gbSize && produced == units); + Require(backUtf16[0] == 'A' && backUtf16[1] == 0x4E2D && backUtf16[2] == 0x80 && backUtf16[units - 1] == 0xFE10); + if (revision2005) Require(backUtf16[3] == 0xD840 && backUtf16[4] == 0xDC00); + std::uint16_t shortUtf16[8] = {}; + Require(sceCesMbcsStrToUtf16Str(context, text, gbSize, &consumed, shortUtf16, units - 2, &produced) == dstBufferEnd); + Require(sceCesMbcsStrGetUtf16Len(context, text, gbSize, &consumed, &produced) == 0 && consumed == gbSize && produced == units); std::uint8_t again[16] = {}; - Require(sceCesUtf16StrToMbcsStr(context, backUtf16, 6, &consumed, again, sizeof(again), &produced) == 0 && consumed == 6 && produced == gbSize); + Require(sceCesUtf16StrToMbcsStr(context, backUtf16, units, &consumed, again, sizeof(again), &produced) == 0 && consumed == units && produced == gbSize); Require(std::memcmp(again, gbText, gbSize) == 0); std::uint16_t ucs2Text[8] = {}; - Require(sceCesMbcsStrToUcs2Str(context, text, gbSize, &consumed, ucs2Text, 8, &produced) == unassignedCode); + if (revision2005) Require(sceCesMbcsStrToUcs2Str(context, text, gbSize, &consumed, ucs2Text, 8, &produced) == unassignedCode); Require(sceCesUtf8StrToMbcsStr(context, Bytes("\xC2\x80"), 2, &consumed, text, 3, &produced) == dstBufferEnd); } @@ -345,6 +359,6 @@ int main() { Require(sceCesEucCnToUcs2(gb2000, Bytes("\xD6\xD0"), 2, &consumed, &ucs2) == invalidParameter); Require(sceCesGbToUcs2(gb2005, Bytes("\xD6\xD0"), 2, &consumed, &ucs2) == 0 && consumed == 2 && ucs2 == 0x4E2D); CheckGb18030(gb2000, false, 0xE61D3BD3, 0x83476043, 0x469CFCE5, 0, 0); - CheckGb18030(gb2005, true, 0x83EDDA69, 0x8D188DFC, 0x31CD0B55, 18, 24); + CheckGb18030(gb2005, true, 0xE4CF78F0, 0x8D188DFC, 0xD675EFA1, 0, 0); return 0; } diff --git a/docs/dev/debt/libSceCesCs.md b/docs/dev/debt/libSceCesCs.md index 72af448a70..71e9215ea7 100644 --- a/docs/dev/debt/libSceCesCs.md +++ b/docs/dev/debt/libSceCesCs.md @@ -14,7 +14,7 @@ - [Strict JIS X 0208 profiles](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - the tables were measured on a PS5 (`sceCesMbcToUtf32` and `sceCesUtf32ToMbc` over every one- and two-byte sequence and every scalar); the errors follow the other JIS profiles, a missing SS2 or SS3 answering `INVALID_ENCODE`. The table is CP932's without rows 13 and 89-92 and with seven cells changed (`0x213D` U+2014 and the six of [JIS0208.TXT](https://unicode.org/Public/MAPPINGS/OBSOLETE/EASTASIA/JIS/JIS0208.TXT)). Cell `0x2140` is U+005C in `sceCesUcsProfileInitSJis1997X0208`, whose bytes `0x5C` and `0x7E` are U+00A5 and U+203E, and U+FF3C in the EUC-JP profiles, which keep ASCII and also encode U+00A5 and U+203E as `0xA1EF` and `0xA1B1`. Per [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Text/Ces.cs#L57-L67), `X0208Ss2` adds SS2 katakana and `X0208Ss2Ss3` SS3 JIS X 0212 - [UTF-16 and UTF-32 forms of the single-byte and MBCS string conversions](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - errors, their precedence and the optional length pointers of `sceCesSbcTo{Ucs2,Utf16,Utf32}*`, `sceCes{Ucs2,Utf16,Utf32}*ToSbc`, `sceCesMbcsStr{To,Get}{Utf16,Utf32}*` and `sceCes{Utf16,Utf32}Str{To,Get}Mbcs*` are unverified against the console. Signatures follow the Orbital type database linked above. They follow `sceCesSbcToUtf8`, `sceCesUtf8ToSbc` and the UTF-8 MBCS string forms with the profiles and tables listed above. A surrogate or a value above U+10FFFF answers `INVALID_ENCODE`, a valid code point no profile maps `UNASSIGNED_CODE`. The string forms stop at a zero code unit, write no terminator and count code units - [GBK (CP936) and EUC-CN conversions](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - `sceCesUcsProfileInitGbkCp936` and `sceCesUcsProfileInitEucCnGb2312` mark the caller's sheet with an 8-byte tag; the console's profile layout is not known. Both use one table ([Cp936.hpp](../../../core/libs/prx/libSceCesCs/Cp936.hpp)) read from Microsoft's [CP936.TXT](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP936.TXT); the EUC-CN profile accepts only its `0xA1`-`0xFE` lead and trail bytes, so it includes the GBK cells inside that range and the CP936 forms of `0xA1A4` (U+00B7) and `0xA1AA` (U+2014) instead of GB 2312's. That the console's GB 2312 and CP936 tables equal it is not measured. User-defined areas and `sceCesGbUcsProfileSet*` are not handled. Errors, their precedence and the length pointers follow the Shift-JIS forms above and are unverified -- [GB 18030 profiles](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - the console's tables are not measured, and the sources disagree on 26 sequences. The 2000 profile follows ICU's `gb-18030-2000.ucm` at icu-data d9d3a6ed (before ICU-8427), which matches Python's codec: `0xA8BC` is U+E7C7 and `0x8135F437` U+1E3F. The 2005 profile follows `gb-18030-2005.ucm` (91850aec), which swaps those two and maps 24 PUA cells to U+FE10-U+FE19, U+9FB4-U+9FBB and six supplementary ideographs, keeping the PUA code points as encode fallbacks. Two-byte cells are CP936 plus [Gb18030.hpp](../../../core/libs/prx/libSceCesCs/Gb18030.hpp); four-byte runs include ICU's `gb18030Ranges`. The inits tag the sheet like GBK's; the `sceCesGb*`, `sceCesMbc*` and MBCS string forms accept them. The full mapping is assumed as the default of `sceCesGbUcsProfileSetUdaMapping` and `Set4ByteCharRange`, which throw. Supplementary characters to UCS-2 answer `UNASSIGNED_CODE`; other errors follow the GBK forms +- [GB 18030 profiles](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - the errors follow the GBK forms, unmeasured; the tables were measured on a PS5 (`sceCesMbcToUtf32` over every one- to four-byte sequence below `0x8431A530`, sampled and boundary supplementary sequences, `sceCesUtf32ToMbc` over U+0000-U+110001). The 2000 profile matches ICU's `gb-18030-2000.ucm` at icu-data d9d3a6ed (`0xA8BC` U+E7C7, `0x8135F437` U+1E3F) for the BMP and maps no supplementary character. The 2005 profile is the same with those two swapped, keeps the 24 PUA cells that `gb-18030-2005.ucm` remaps, and maps only plane 2 (U+20000-U+2FFFF, `0x95328236`-`0x9A348431`) above the BMP. Two-byte cells are CP936 plus [Gb18030.hpp](../../../core/libs/prx/libSceCesCs/Gb18030.hpp). The inits tag the sheet like GBK's. The mapping is assumed as the default of `sceCesGbUcsProfileSetUdaMapping` and `Set4ByteCharRange`, which throw. Supplementary characters to UCS-2 answer `UNASSIGNED_CODE` - [sceCesRefersUcsProfileCp1250](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - returns a profile accepted by `sceCesSbcToUtf8`, `sceCesUtf8ToSbc` and the MBCS context conversions, with the Windows-1250 table of Microsoft's [bestfit1250.txt](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WindowsBestFit/bestfit1250.txt), except bytes 0x81, 0x83, 0x88, 0x90 and 0x98, which are unassigned. PPSA34501 fails to start without this export (`qYu6y1E0Qbg`) - [sceCesSbcToUtf8 and sceCesUtf8ToSbc](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) follow a payload process on a PS5 (firmware 13.60): the three tables match the console byte for byte, with Cp1252 bytes 0x81, 0x8D, 0x8F, 0x90 and 0x9D unassigned; a null profile returns `0x805C0004`; and the length and output are cleared first, as the console does. `sceCesUtf8ToSbc` decodes leads up to 0xFD (5- and 6-byte forms), returns `0x805C0015` for overlong forms and surrogates and reports the measured lengths. The console reads any other profile pointer as a table, where these return the invalid-parameter error; which of a null profile and a scalar above U+10FFFF it reports first is not measured - [MBCS string conversions](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - `sceCesUtf8StrToMbcsStr`, `sceCesUcs2StrToMbcsStr`, `sceCesMbcsStrToUtf8Str`, `sceCesMbcsStrToUcs2Str` and their `Get*Len` forms follow a payload process on a PS5 (firmware 13.60) with the Cp1252 and EUC-JP profiles: the lengths are cleared and a terminator is written first, then the context, the source and the output are checked in that order; a source limit of 0 reads up to the terminating zero; the output keeps room for a terminator; and on an error the characters before it stay converted, terminated and counted. They decode UTF-8 as `sceCesUtf8ToSbc` does, and a UTF-16 surrogate answers `0x805C0015`. The UCS-2 forms and the other profiles were not measured beyond these cases. A zero-filled context converted nothing and returned 0 on the console, where this returns the invalid-parameter error From 04ab37b35d17c49d29d8136a8e18eeb53e2196fc Mon Sep 17 00:00:00 2001 From: Bipin Date: Mon, 26 Oct 2026 08:41:34 +0530 Subject: [PATCH 088/198] fix(core): apply the measured opus encoder console behaviour Create, CreateEx, Destroy, GetSize and Initialize follow the firmware 13.42 export measurements reported on the pull request: bases 0x813F0000 and 0x81770000, the pointer validated first (+0x01), then sample rate (+0x10/+0x0F), channels (+0x0D/+0x0C) and application (+0x11/+0x10), the required caller application 2051 for CELT and 2048 for SILK, the 0x17B2 state marker at +0xCC/+0xE8 behind Destroy (+0x16/+0x15) and fixed 728/752-byte GetSize values; a successful create zero-fills the console state size. The internal encoder keeps the restricted celt and silk applications whose packet identity the tests compare, float included. The tests declare entry points through the libraries' Api.hpp instead of local prototypes, cast states to the opaque encoder types, drop the silk ctl/cancel/priority/timeout and live-encoder Terminate throw pins and compare both float sections against opus_encode_float. Ctl, Encode*WithPriority and the silk cancel and timeout entry points keep their throwing stubs, and the debt notes carry the measurement with the remaining gaps. --- core/libs/prx/libSceOpusCeltEnc/Export.cpp | 4 +- .../libSceOpusCeltEnc/include/OpusEncoder.hpp | 48 +++++--- core/libs/prx/libSceOpusSilkEnc/Export.cpp | 4 +- core/libs/tests/GuestOpusCeltEnc.cpp | 97 +++++++-------- core/libs/tests/GuestOpusSilkEnc.cpp | 110 ++++++++---------- docs/dev/debt/libSceOpusCeltEnc.md | 9 +- docs/dev/debt/libSceOpusSilkEnc.md | 9 +- 7 files changed, 145 insertions(+), 136 deletions(-) diff --git a/core/libs/prx/libSceOpusCeltEnc/Export.cpp b/core/libs/prx/libSceOpusCeltEnc/Export.cpp index e19efee478..5250169acc 100644 --- a/core/libs/prx/libSceOpusCeltEnc/Export.cpp +++ b/core/libs/prx/libSceOpusCeltEnc/Export.cpp @@ -6,7 +6,9 @@ namespace { -OpusEncoderLibrary library("OpusCeltEnc", OPUS_APPLICATION_RESTRICTED_CELT, 0x81400000u); +constexpr EncoderCodes codes{0x813F0000u, 1u, 0x10u, 0x0Du, 0x11u, 0x16u, 0x19u, 728, 0xCCu, 0x17B2u}; + +OpusEncoderLibrary library("OpusCeltEnc", OPUS_APPLICATION_RESTRICTED_LOWDELAY, OPUS_APPLICATION_RESTRICTED_CELT, codes); } // namespace diff --git a/core/libs/prx/libSceOpusCeltEnc/include/OpusEncoder.hpp b/core/libs/prx/libSceOpusCeltEnc/include/OpusEncoder.hpp index 362c7f4b1a..cbadf08658 100644 --- a/core/libs/prx/libSceOpusCeltEnc/include/OpusEncoder.hpp +++ b/core/libs/prx/libSceOpusCeltEnc/include/OpusEncoder.hpp @@ -17,12 +17,25 @@ namespace { +struct EncoderCodes { + std::uint32_t baseError; + std::uint32_t nullErrorOffset; + std::uint32_t rateErrorOffset; + std::uint32_t channelsErrorOffset; + std::uint32_t applicationErrorOffset; + std::uint32_t destroyStateErrorOffset; + std::uint32_t initializeNullErrorOffset; + std::size_t stateSize; + std::size_t stateMagicOffset; + std::uint16_t stateMagic; +}; + class OpusEncoderLibrary { public: - OpusEncoderLibrary(const char* libraryName, int restrictedApplication, std::uint32_t base) : name(libraryName), application(restrictedApplication), errorBase(base) {} + OpusEncoderLibrary(const char* libraryName, int callerApplication, int codecApplication, const EncoderCodes& codes) : name(libraryName), requiredApplication(callerApplication), application(codecApplication), codes(codes), errorBase(codes.baseError) {} int Initialize(std::uint32_t* context) { - if (!context) return error(13); + if (!context) return error(codes.initializeNullErrorOffset); std::lock_guard lock(mutex); for (const auto& [id, owner] : contexts) if (owner == context) fail("context already initialized"); @@ -44,15 +57,15 @@ public: } int GetSize(int channels) { - if (channels != 1 && channels != 2) return error(3); - return opus_encoder_get_size(channels); + if (channels != 1 && channels != 2) return error(codes.channelsErrorOffset); + return static_cast(codes.stateSize); } int Create(void* state, int sampleRate, int channels, int createApplication) { - if (!state) return error(0); - if (channels != 1 && channels != 2) return error(3); - if (sampleRate != 8000 && sampleRate != 12000 && sampleRate != 16000 && sampleRate != 24000 && sampleRate != 48000) return error(2); - if (createApplication != application) return error(2); + if (!state) return error(codes.nullErrorOffset); + if (sampleRate != 8000 && sampleRate != 12000 && sampleRate != 16000 && sampleRate != 24000 && sampleRate != 48000) return error(codes.rateErrorOffset); + if (channels != 1 && channels != 2) return error(codes.channelsErrorOffset); + if (createApplication != requiredApplication) return error(codes.applicationErrorOffset); std::lock_guard lock(mutex); if (encoders.contains(state)) fail("state already owns an encoder"); Make(state, 0, sampleRate, channels); @@ -60,10 +73,10 @@ public: } int CreateEx(const std::uint32_t* context, void* state, int sampleRate, int channels, int createApplication) { - if (!context || !state) return error(0); - if (channels != 1 && channels != 2) return error(3); - if (sampleRate != 8000 && sampleRate != 12000 && sampleRate != 16000 && sampleRate != 24000 && sampleRate != 48000) return error(2); - if (createApplication != application) return error(2); + if (!context || !state) return error(codes.nullErrorOffset); + if (sampleRate != 8000 && sampleRate != 12000 && sampleRate != 16000 && sampleRate != 24000 && sampleRate != 48000) return error(codes.rateErrorOffset); + if (channels != 1 && channels != 2) return error(codes.channelsErrorOffset); + if (createApplication != requiredApplication) return error(codes.applicationErrorOffset); std::lock_guard lock(mutex); if (!contexts.contains(*context)) fail("unknown context"); if (encoders.contains(state)) fail("state already owns an encoder"); @@ -102,9 +115,12 @@ public: } int Destroy(void* state) { - if (!state) return error(0); + if (!state) return error(codes.nullErrorOffset); + std::uint16_t magic = 0; + std::memcpy(&magic, static_cast(state) + codes.stateMagicOffset, sizeof magic); + if (magic != codes.stateMagic) return error(codes.destroyStateErrorOffset); std::lock_guard lock(mutex); - if (encoders.erase(state) == 0) return error(1); + if (encoders.erase(state) == 0) return error(codes.destroyStateErrorOffset); return 0; } @@ -117,6 +133,7 @@ private: }; void Make(void* state, std::uint32_t context, int sampleRate, int channels) { + std::memset(state, 0, codes.stateSize); auto encoder = std::make_unique(); encoder->context = context; encoder->channels = channels; @@ -125,11 +142,14 @@ private: encoder->codec.reset(opus_encoder_create(sampleRate, channels, application, &opusError)); if (opusError == OPUS_ALLOC_FAIL) throw std::bad_alloc(); if (!encoder->codec || opusError != OPUS_OK) fail("cannot create libopus encoder"); + std::memcpy(static_cast(state) + codes.stateMagicOffset, &codes.stateMagic, sizeof codes.stateMagic); encoders.emplace(state, std::move(encoder)); } const char* name; + int requiredApplication; int application; + EncoderCodes codes; std::uint32_t errorBase; std::mutex mutex; std::unordered_map contexts; diff --git a/core/libs/prx/libSceOpusSilkEnc/Export.cpp b/core/libs/prx/libSceOpusSilkEnc/Export.cpp index 5e8e52e871..08a4529138 100644 --- a/core/libs/prx/libSceOpusSilkEnc/Export.cpp +++ b/core/libs/prx/libSceOpusSilkEnc/Export.cpp @@ -6,7 +6,9 @@ namespace { -OpusEncoderLibrary library("OpusSilkEnc", OPUS_APPLICATION_RESTRICTED_SILK, 0x81740000u); +constexpr EncoderCodes codes{0x81770000u, 1u, 0x0Fu, 0x0Cu, 0x10u, 0x15u, 0x18u, 752, 0xE8u, 0x17B2u}; + +OpusEncoderLibrary library("OpusSilkEnc", OPUS_APPLICATION_VOIP, OPUS_APPLICATION_RESTRICTED_SILK, codes); } // namespace diff --git a/core/libs/tests/GuestOpusCeltEnc.cpp b/core/libs/tests/GuestOpusCeltEnc.cpp index 3a83e489d9..f2fe99babd 100644 --- a/core/libs/tests/GuestOpusCeltEnc.cpp +++ b/core/libs/tests/GuestOpusCeltEnc.cpp @@ -1,3 +1,4 @@ +#include "prx/libSceOpusCeltEnc/include/Api.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include #include @@ -9,17 +10,6 @@ #include #include -extern "C" { -int APS5_VABI sceOpusCeltEncInitialize(std::uint32_t*); -int APS5_VABI sceOpusCeltEncTerminate(const std::uint32_t*); -int APS5_VABI sceOpusCeltEncGetSize(int); -int APS5_VABI sceOpusCeltEncCreate(void*, int, int, int); -int APS5_VABI sceOpusCeltEncCreateEx(const std::uint32_t*, void*, int, int, int); -int APS5_VABI sceOpusCeltEncEncode(void*, const std::int16_t*, int, std::uint8_t*, int); -int APS5_VABI sceOpusCeltEncEncodeFloat(void*, const float*, int, std::uint8_t*, int); -int APS5_VABI sceOpusCeltEncDestroy(void*); -} - namespace { void Require(bool value, const std::source_location& location = std::source_location::current()) { @@ -35,6 +25,11 @@ void RequireThrows(TFunction function) { std::abort(); } +template +OpusCeltEncoder* ToState(std::array& block) { + return reinterpret_cast(block.data()); +} + void FillSine(std::int16_t* samples, int frameSize, int channels, int rate, int position) { for (int index = 0; index < frameSize; ++index) { const int value = static_cast(3000 * std::sin(2 * 3.14159265358979323846 * 2000 * (position + index) / rate)); @@ -62,10 +57,11 @@ void RequireTone(const std::int16_t* pcm, int samples, int channels, int rate) { } // namespace int main() { - constexpr std::uint32_t errorBase = 0x81400000u; - constexpr int application = OPUS_APPLICATION_RESTRICTED_CELT; - Require(sceOpusCeltEncInitialize(nullptr) == static_cast(errorBase + 13)); - Require(sceOpusCeltEncDestroy(nullptr) == static_cast(errorBase)); + constexpr std::uint32_t errorBase = 0x813F0000u; + constexpr int callerApplication = OPUS_APPLICATION_RESTRICTED_LOWDELAY; + constexpr int codecApplication = OPUS_APPLICATION_RESTRICTED_CELT; + Require(sceOpusCeltEncInitialize(nullptr) == static_cast(errorBase + 0x19)); + Require(sceOpusCeltEncDestroy(nullptr) == static_cast(errorBase + 0x01)); std::uint32_t context = 0; Require(sceOpusCeltEncInitialize(&context) == 0); RequireThrows([&] { sceOpusCeltEncInitialize(&context); }); @@ -77,40 +73,40 @@ int main() { std::array floatSamples{}; std::uint8_t packet[1024]; for (auto& byte : packet) byte = 0x5A; - Require(sceOpusCeltEncCreate(nullptr, 48000, 2, application) == static_cast(errorBase)); - Require(sceOpusCeltEncCreate(invalidState.data(), 48000, 0, application) == static_cast(errorBase + 3)); - Require(sceOpusCeltEncCreate(invalidState.data(), 0, 0, application) == static_cast(errorBase + 3)); - Require(sceOpusCeltEncCreate(invalidState.data(), 22050, 2, application) == static_cast(errorBase + 2)); - Require(sceOpusCeltEncCreate(invalidState.data(), 48000, 2, 2048) == static_cast(errorBase + 2)); - Require(sceOpusCeltEncCreateEx(nullptr, invalidState.data(), 0, 0, application) == static_cast(errorBase)); - Require(sceOpusCeltEncCreateEx(&context, invalidState.data(), 0, 0, application) == static_cast(errorBase + 3)); - Require(sceOpusCeltEncCreateEx(&context, invalidState.data(), 48000, 0, application) == static_cast(errorBase + 3)); - Require(sceOpusCeltEncCreateEx(&context, invalidState.data(), 48000, 2, 2048) == static_cast(errorBase + 2)); + Require(sceOpusCeltEncCreate(nullptr, 48000, 2, callerApplication) == static_cast(errorBase + 0x01)); + Require(sceOpusCeltEncCreate(ToState(invalidState), 48000, 0, callerApplication) == static_cast(errorBase + 0x0D)); + Require(sceOpusCeltEncCreate(ToState(invalidState), 0, 0, callerApplication) == static_cast(errorBase + 0x10)); + Require(sceOpusCeltEncCreate(ToState(invalidState), 22050, 2, callerApplication) == static_cast(errorBase + 0x10)); + Require(sceOpusCeltEncCreate(ToState(invalidState), 48000, 2, OPUS_APPLICATION_VOIP) == static_cast(errorBase + 0x11)); + Require(sceOpusCeltEncCreateEx(nullptr, ToState(invalidState), 0, 0, callerApplication) == static_cast(errorBase + 0x01)); + Require(sceOpusCeltEncCreateEx(&context, ToState(invalidState), 0, 0, callerApplication) == static_cast(errorBase + 0x10)); + Require(sceOpusCeltEncCreateEx(&context, ToState(invalidState), 48000, 0, callerApplication) == static_cast(errorBase + 0x0D)); + Require(sceOpusCeltEncCreateEx(&context, ToState(invalidState), 48000, 2, OPUS_APPLICATION_VOIP) == static_cast(errorBase + 0x11)); Require(sceOpusCeltEncEncode(nullptr, nullptr, -1, nullptr, -1) == static_cast(errorBase)); - Require(sceOpusCeltEncEncode(invalidState.data(), nullptr, 960, nullptr, -1) == static_cast(errorBase + 6)); - Require(sceOpusCeltEncEncode(invalidState.data(), samples.data(), -1, packet, -1) == static_cast(errorBase + 7)); - Require(sceOpusCeltEncEncode(invalidState.data(), samples.data(), 960, nullptr, -1) == static_cast(errorBase + 8)); - Require(sceOpusCeltEncEncode(invalidState.data(), samples.data(), 960, packet, -1) == static_cast(errorBase + 9)); - Require(sceOpusCeltEncEncode(invalidState.data(), samples.data(), 960, packet, 8) == static_cast(errorBase + 1)); + Require(sceOpusCeltEncEncode(ToState(invalidState), nullptr, 960, nullptr, -1) == static_cast(errorBase + 6)); + Require(sceOpusCeltEncEncode(ToState(invalidState), samples.data(), -1, packet, -1) == static_cast(errorBase + 7)); + Require(sceOpusCeltEncEncode(ToState(invalidState), samples.data(), 960, nullptr, -1) == static_cast(errorBase + 8)); + Require(sceOpusCeltEncEncode(ToState(invalidState), samples.data(), 960, packet, -1) == static_cast(errorBase + 9)); + Require(sceOpusCeltEncEncode(ToState(invalidState), samples.data(), 960, packet, 8) == static_cast(errorBase + 1)); Require(packet[0] == 0x5A && packet[1] == 0x5A); Require(sceOpusCeltEncEncodeFloat(nullptr, nullptr, -1, nullptr, -1) == static_cast(errorBase)); - Require(sceOpusCeltEncEncodeFloat(invalidState.data(), nullptr, 960, packet, 8) == static_cast(errorBase + 6)); - Require(sceOpusCeltEncEncodeFloat(invalidState.data(), floatSamples.data(), -1, packet, 8) == static_cast(errorBase + 7)); + Require(sceOpusCeltEncEncodeFloat(ToState(invalidState), nullptr, 960, packet, 8) == static_cast(errorBase + 6)); + Require(sceOpusCeltEncEncodeFloat(ToState(invalidState), floatSamples.data(), -1, packet, 8) == static_cast(errorBase + 7)); - Require(sceOpusCeltEncGetSize(1) == opus_encoder_get_size(1) && sceOpusCeltEncGetSize(2) == opus_encoder_get_size(2)); - Require(sceOpusCeltEncGetSize(3) == static_cast(errorBase + 3)); + Require(sceOpusCeltEncGetSize(1) == 728 && sceOpusCeltEncGetSize(2) == 728); + Require(sceOpusCeltEncGetSize(3) == static_cast(errorBase + 0x0D)); for (int rate : {8000, 12000, 16000, 24000, 48000}) { for (int channels : {1, 2}) { std::array state{}; - Require(sceOpusCeltEncCreateEx(&context, state.data(), rate, channels, application) == 0); + Require(sceOpusCeltEncCreateEx(&context, ToState(state), rate, channels, callerApplication) == 0); int opusError = OPUS_OK; - auto* reference = opus_encoder_create(rate, channels, application, &opusError); + auto* reference = opus_encoder_create(rate, channels, codecApplication, &opusError); Require(reference && opusError == OPUS_OK); const int frameSize = rate / 50; for (int frame = 0; frame < 4; ++frame) { FillSine(samples.data(), frameSize, channels, rate, frame * frameSize); - const int bytes = sceOpusCeltEncEncode(state.data(), samples.data(), frameSize, packet, sizeof(packet)); + const int bytes = sceOpusCeltEncEncode(ToState(state), samples.data(), frameSize, packet, sizeof(packet)); Require(bytes > 2 && static_cast(packet[0]) >> 3 >= 16); std::array expected{}; const int expectedBytes = opus_encode(reference, samples.data(), frameSize, expected.data(), sizeof(expected)); @@ -122,7 +118,7 @@ int main() { std::array pcm{}; for (int frame = 0; frame < 4; ++frame) { FillSine(samples.data(), frameSize, channels, rate, frame * frameSize); - const int bytes = sceOpusCeltEncEncode(state.data(), samples.data(), frameSize, packet, sizeof(packet)); + const int bytes = sceOpusCeltEncEncode(ToState(state), samples.data(), frameSize, packet, sizeof(packet)); pcm.fill(12345); Require(opus_decode(decoder, packet, bytes, pcm.data() + 1, frameSize, 0) == frameSize); Require(pcm.front() == 12345 && pcm.back() == 12345); @@ -130,26 +126,31 @@ int main() { } opus_decoder_destroy(decoder); opus_encoder_destroy(reference); - Require(sceOpusCeltEncDestroy(state.data()) == 0); + Require(sceOpusCeltEncDestroy(ToState(state)) == 0); } } - Require(sceOpusCeltEncDestroy(invalidState.data()) == static_cast(errorBase + 1)); + Require(sceOpusCeltEncDestroy(ToState(invalidState)) == static_cast(errorBase + 0x16)); std::array floatState{}; - Require(sceOpusCeltEncCreateEx(&context, floatState.data(), 48000, 1, application) == 0); + Require(sceOpusCeltEncCreateEx(&context, ToState(floatState), 48000, 1, callerApplication) == 0); for (int index = 0; index < 960; ++index) floatSamples[static_cast(index)] = static_cast(0.2 * std::sin(2 * 3.14159265358979323846 * 2000 * index / 48000.0)); - const int floatResult = sceOpusCeltEncEncodeFloat(floatState.data(), floatSamples.data(), 960, packet, static_cast(sizeof(packet))); + const int floatResult = sceOpusCeltEncEncodeFloat(ToState(floatState), floatSamples.data(), 960, packet, static_cast(sizeof(packet))); Require(floatResult > 2 && static_cast(packet[0]) >> 3 >= 16); - Require(sceOpusCeltEncDestroy(floatState.data()) == 0); + int floatOpusError = OPUS_OK; + auto* floatReference = opus_encoder_create(48000, 1, codecApplication, &floatOpusError); Require(floatReference && floatOpusError == OPUS_OK); + std::array floatExpected{}; + const int floatExpectedBytes = opus_encode_float(floatReference, floatSamples.data(), 960, floatExpected.data(), sizeof(floatExpected)); + Require(floatExpectedBytes == floatResult && std::equal(packet, packet + floatResult, floatExpected.begin())); + opus_encoder_destroy(floatReference); + Require(sceOpusCeltEncDestroy(ToState(floatState)) == 0); std::array liveState{}; - Require(sceOpusCeltEncCreateEx(&context, liveState.data(), 48000, 2, application) == 0); - RequireThrows([&] { sceOpusCeltEncTerminate(&context); }); - Require(sceOpusCeltEncDestroy(liveState.data()) == 0); + Require(sceOpusCeltEncCreateEx(&context, ToState(liveState), 48000, 2, callerApplication) == 0); + Require(sceOpusCeltEncDestroy(ToState(liveState)) == 0); Require(sceOpusCeltEncTerminate(&context) == 0); RequireThrows([&] { sceOpusCeltEncTerminate(&context); }); - RequireThrows([&] { sceOpusCeltEncCreateEx(&context, liveState.data(), 48000, 2, application); }); - Require(sceOpusCeltEncCreate(liveState.data(), 48000, 2, application) == 0); - Require(sceOpusCeltEncDestroy(liveState.data()) == 0); + RequireThrows([&] { sceOpusCeltEncCreateEx(&context, ToState(liveState), 48000, 2, callerApplication); }); + Require(sceOpusCeltEncCreate(ToState(liveState), 48000, 2, callerApplication) == 0); + Require(sceOpusCeltEncDestroy(ToState(liveState)) == 0); } diff --git a/core/libs/tests/GuestOpusSilkEnc.cpp b/core/libs/tests/GuestOpusSilkEnc.cpp index 0658150b80..67e28b3819 100644 --- a/core/libs/tests/GuestOpusSilkEnc.cpp +++ b/core/libs/tests/GuestOpusSilkEnc.cpp @@ -1,3 +1,4 @@ +#include "prx/libSceOpusSilkEnc/include/Api.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include #include @@ -9,23 +10,6 @@ #include #include -extern "C" { -int APS5_VABI sceOpusSilkEncInitialize(std::uint32_t*); -int APS5_VABI sceOpusSilkEncTerminate(const std::uint32_t*); -int APS5_VABI sceOpusSilkEncGetSize(int); -int APS5_VABI sceOpusSilkEncCreate(void*, int, int, int); -int APS5_VABI sceOpusSilkEncCreateEx(const std::uint32_t*, void*, int, int, int); -int APS5_VABI sceOpusSilkEncEncode(void*, const std::int16_t*, int, std::uint8_t*, int); -int APS5_VABI sceOpusSilkEncEncodeFloat(void*, const float*, int, std::uint8_t*, int); -int APS5_VABI sceOpusSilkEncDestroy(void*); -int APS5_VABI sceOpusSilkEncCtl(void*, int, ...); -int APS5_VABI sceOpusSilkEncCancel(void*, unsigned int); -int APS5_VABI sceOpusSilkEncEncodeWithPriority(void*, const std::int16_t*, int, std::uint8_t*, int, unsigned int); -int APS5_VABI sceOpusSilkEncEncodeFloatWithPriority(void*, const float*, int, std::uint8_t*, int, unsigned int); -int APS5_VABI sceOpusSilkEncEncodeWithPriorityAndTimeout(void*, const std::int16_t*, int, std::uint8_t*, int, unsigned int, unsigned int, unsigned int*); -int APS5_VABI sceOpusSilkEncEncodeFloatWithPriorityAndTimeout(void*, const float*, int, std::uint8_t*, int, unsigned int, unsigned int, unsigned int*); -} - namespace { void Require(bool value, const std::source_location& location = std::source_location::current()) { @@ -41,6 +25,11 @@ void RequireThrows(TFunction function) { std::abort(); } +template +OpusSilkEncoder* ToState(std::array& block) { + return reinterpret_cast(block.data()); +} + void FillSine(std::int16_t* samples, int frameSize, int channels, int rate, int position) { for (int index = 0; index < frameSize; ++index) { const int value = static_cast(3000 * std::sin(2 * 3.14159265358979323846 * 2000 * (position + index) / rate)); @@ -68,10 +57,11 @@ void RequireTone(const std::int16_t* pcm, int samples, int channels, int rate) { } // namespace int main() { - constexpr std::uint32_t errorBase = 0x81740000u; - constexpr int application = OPUS_APPLICATION_RESTRICTED_SILK; - Require(sceOpusSilkEncInitialize(nullptr) == static_cast(errorBase + 13)); - Require(sceOpusSilkEncDestroy(nullptr) == static_cast(errorBase)); + constexpr std::uint32_t errorBase = 0x81770000u; + constexpr int callerApplication = OPUS_APPLICATION_VOIP; + constexpr int codecApplication = OPUS_APPLICATION_RESTRICTED_SILK; + Require(sceOpusSilkEncInitialize(nullptr) == static_cast(errorBase + 0x18)); + Require(sceOpusSilkEncDestroy(nullptr) == static_cast(errorBase + 0x01)); std::uint32_t context = 0; Require(sceOpusSilkEncInitialize(&context) == 0); RequireThrows([&] { sceOpusSilkEncInitialize(&context); }); @@ -83,40 +73,40 @@ int main() { std::array floatSamples{}; std::uint8_t packet[1024]; for (auto& byte : packet) byte = 0x5A; - Require(sceOpusSilkEncCreate(nullptr, 48000, 2, application) == static_cast(errorBase)); - Require(sceOpusSilkEncCreate(invalidState.data(), 48000, 0, application) == static_cast(errorBase + 3)); - Require(sceOpusSilkEncCreate(invalidState.data(), 0, 0, application) == static_cast(errorBase + 3)); - Require(sceOpusSilkEncCreate(invalidState.data(), 22050, 2, application) == static_cast(errorBase + 2)); - Require(sceOpusSilkEncCreate(invalidState.data(), 48000, 2, 2048) == static_cast(errorBase + 2)); - Require(sceOpusSilkEncCreateEx(nullptr, invalidState.data(), 0, 0, application) == static_cast(errorBase)); - Require(sceOpusSilkEncCreateEx(&context, invalidState.data(), 0, 0, application) == static_cast(errorBase + 3)); - Require(sceOpusSilkEncCreateEx(&context, invalidState.data(), 48000, 0, application) == static_cast(errorBase + 3)); - Require(sceOpusSilkEncCreateEx(&context, invalidState.data(), 48000, 2, 2048) == static_cast(errorBase + 2)); + Require(sceOpusSilkEncCreate(nullptr, 48000, 2, callerApplication) == static_cast(errorBase + 0x01)); + Require(sceOpusSilkEncCreate(ToState(invalidState), 48000, 0, callerApplication) == static_cast(errorBase + 0x0C)); + Require(sceOpusSilkEncCreate(ToState(invalidState), 0, 0, callerApplication) == static_cast(errorBase + 0x0F)); + Require(sceOpusSilkEncCreate(ToState(invalidState), 22050, 2, callerApplication) == static_cast(errorBase + 0x0F)); + Require(sceOpusSilkEncCreate(ToState(invalidState), 48000, 2, OPUS_APPLICATION_RESTRICTED_SILK) == static_cast(errorBase + 0x10)); + Require(sceOpusSilkEncCreateEx(nullptr, ToState(invalidState), 0, 0, callerApplication) == static_cast(errorBase + 0x01)); + Require(sceOpusSilkEncCreateEx(&context, ToState(invalidState), 0, 0, callerApplication) == static_cast(errorBase + 0x0F)); + Require(sceOpusSilkEncCreateEx(&context, ToState(invalidState), 48000, 0, callerApplication) == static_cast(errorBase + 0x0C)); + Require(sceOpusSilkEncCreateEx(&context, ToState(invalidState), 48000, 2, OPUS_APPLICATION_RESTRICTED_SILK) == static_cast(errorBase + 0x10)); Require(sceOpusSilkEncEncode(nullptr, nullptr, -1, nullptr, -1) == static_cast(errorBase)); - Require(sceOpusSilkEncEncode(invalidState.data(), nullptr, 960, nullptr, -1) == static_cast(errorBase + 6)); - Require(sceOpusSilkEncEncode(invalidState.data(), samples.data(), -1, packet, -1) == static_cast(errorBase + 7)); - Require(sceOpusSilkEncEncode(invalidState.data(), samples.data(), 960, nullptr, -1) == static_cast(errorBase + 8)); - Require(sceOpusSilkEncEncode(invalidState.data(), samples.data(), 960, packet, -1) == static_cast(errorBase + 9)); - Require(sceOpusSilkEncEncode(invalidState.data(), samples.data(), 960, packet, 8) == static_cast(errorBase + 1)); + Require(sceOpusSilkEncEncode(ToState(invalidState), nullptr, 960, nullptr, -1) == static_cast(errorBase + 6)); + Require(sceOpusSilkEncEncode(ToState(invalidState), samples.data(), -1, packet, -1) == static_cast(errorBase + 7)); + Require(sceOpusSilkEncEncode(ToState(invalidState), samples.data(), 960, nullptr, -1) == static_cast(errorBase + 8)); + Require(sceOpusSilkEncEncode(ToState(invalidState), samples.data(), 960, packet, -1) == static_cast(errorBase + 9)); + Require(sceOpusSilkEncEncode(ToState(invalidState), samples.data(), 960, packet, 8) == static_cast(errorBase + 1)); Require(packet[0] == 0x5A && packet[1] == 0x5A); Require(sceOpusSilkEncEncodeFloat(nullptr, nullptr, -1, nullptr, -1) == static_cast(errorBase)); - Require(sceOpusSilkEncEncodeFloat(invalidState.data(), nullptr, 960, packet, 8) == static_cast(errorBase + 6)); - Require(sceOpusSilkEncEncodeFloat(invalidState.data(), floatSamples.data(), -1, packet, 8) == static_cast(errorBase + 7)); + Require(sceOpusSilkEncEncodeFloat(ToState(invalidState), nullptr, 960, packet, 8) == static_cast(errorBase + 6)); + Require(sceOpusSilkEncEncodeFloat(ToState(invalidState), floatSamples.data(), -1, packet, 8) == static_cast(errorBase + 7)); - Require(sceOpusSilkEncGetSize(1) == opus_encoder_get_size(1) && sceOpusSilkEncGetSize(2) == opus_encoder_get_size(2)); - Require(sceOpusSilkEncGetSize(3) == static_cast(errorBase + 3)); + Require(sceOpusSilkEncGetSize(1) == 752 && sceOpusSilkEncGetSize(2) == 752); + Require(sceOpusSilkEncGetSize(3) == static_cast(errorBase + 0x0C)); for (int rate : {8000, 12000, 16000, 24000, 48000}) { for (int channels : {1, 2}) { std::array state{}; - Require(sceOpusSilkEncCreateEx(&context, state.data(), rate, channels, application) == 0); + Require(sceOpusSilkEncCreateEx(&context, ToState(state), rate, channels, callerApplication) == 0); int opusError = OPUS_OK; - auto* reference = opus_encoder_create(rate, channels, application, &opusError); + auto* reference = opus_encoder_create(rate, channels, codecApplication, &opusError); Require(reference && opusError == OPUS_OK); const int frameSize = rate / 50; for (int frame = 0; frame < 4; ++frame) { FillSine(samples.data(), frameSize, channels, rate, frame * frameSize); - const int bytes = sceOpusSilkEncEncode(state.data(), samples.data(), frameSize, packet, sizeof(packet)); + const int bytes = sceOpusSilkEncEncode(ToState(state), samples.data(), frameSize, packet, sizeof(packet)); Require(bytes > 2 && static_cast(packet[0]) >> 3 < 12); std::array expected{}; const int expectedBytes = opus_encode(reference, samples.data(), frameSize, expected.data(), sizeof(expected)); @@ -128,7 +118,7 @@ int main() { std::array pcm{}; for (int frame = 0; frame < 4; ++frame) { FillSine(samples.data(), frameSize, channels, rate, frame * frameSize); - const int bytes = sceOpusSilkEncEncode(state.data(), samples.data(), frameSize, packet, sizeof(packet)); + const int bytes = sceOpusSilkEncEncode(ToState(state), samples.data(), frameSize, packet, sizeof(packet)); pcm.fill(12345); Require(opus_decode(decoder, packet, bytes, pcm.data() + 1, frameSize, 0) == frameSize); Require(pcm.front() == 12345 && pcm.back() == 12345); @@ -136,32 +126,32 @@ int main() { } opus_decoder_destroy(decoder); opus_encoder_destroy(reference); - Require(sceOpusSilkEncDestroy(state.data()) == 0); + Require(sceOpusSilkEncDestroy(ToState(state)) == 0); } } - Require(sceOpusSilkEncDestroy(invalidState.data()) == static_cast(errorBase + 1)); + Require(sceOpusSilkEncDestroy(ToState(invalidState)) == static_cast(errorBase + 0x15)); std::array floatState{}; - Require(sceOpusSilkEncCreateEx(&context, floatState.data(), 48000, 1, application) == 0); + Require(sceOpusSilkEncCreateEx(&context, ToState(floatState), 48000, 1, callerApplication) == 0); for (int index = 0; index < 960; ++index) floatSamples[static_cast(index)] = static_cast(0.2 * std::sin(2 * 3.14159265358979323846 * 2000 * index / 48000.0)); - const int floatResult = sceOpusSilkEncEncodeFloat(floatState.data(), floatSamples.data(), 960, packet, static_cast(sizeof(packet))); + const int floatResult = sceOpusSilkEncEncodeFloat(ToState(floatState), floatSamples.data(), 960, packet, static_cast(sizeof(packet))); Require(floatResult > 2 && static_cast(packet[0]) >> 3 < 12); - Require(sceOpusSilkEncDestroy(floatState.data()) == 0); + int floatOpusError = OPUS_OK; + auto* floatReference = opus_encoder_create(48000, 1, codecApplication, &floatOpusError); + Require(floatReference && floatOpusError == OPUS_OK); + std::array floatExpected{}; + const int floatExpectedBytes = opus_encode_float(floatReference, floatSamples.data(), 960, floatExpected.data(), sizeof(floatExpected)); + Require(floatExpectedBytes == floatResult && std::equal(packet, packet + floatResult, floatExpected.begin())); + opus_encoder_destroy(floatReference); + Require(sceOpusSilkEncDestroy(ToState(floatState)) == 0); std::array liveState{}; - Require(sceOpusSilkEncCreateEx(&context, liveState.data(), 48000, 2, application) == 0); - RequireThrows([&] { sceOpusSilkEncCtl(liveState.data(), 1); }); - RequireThrows([&] { sceOpusSilkEncCancel(liveState.data(), 0); }); - RequireThrows([&] { sceOpusSilkEncEncodeWithPriority(liveState.data(), samples.data(), 960, packet, sizeof(packet), 0); }); - RequireThrows([&] { sceOpusSilkEncEncodeFloatWithPriority(liveState.data(), floatSamples.data(), 960, packet, sizeof(packet), 0); }); - RequireThrows([&] { sceOpusSilkEncEncodeWithPriorityAndTimeout(liveState.data(), samples.data(), 960, packet, sizeof(packet), 0, 0, nullptr); }); - RequireThrows([&] { sceOpusSilkEncEncodeFloatWithPriorityAndTimeout(liveState.data(), floatSamples.data(), 960, packet, sizeof(packet), 0, 0, nullptr); }); - RequireThrows([&] { sceOpusSilkEncTerminate(&context); }); - Require(sceOpusSilkEncDestroy(liveState.data()) == 0); + Require(sceOpusSilkEncCreateEx(&context, ToState(liveState), 48000, 2, callerApplication) == 0); + Require(sceOpusSilkEncDestroy(ToState(liveState)) == 0); Require(sceOpusSilkEncTerminate(&context) == 0); RequireThrows([&] { sceOpusSilkEncTerminate(&context); }); - RequireThrows([&] { sceOpusSilkEncCreateEx(&context, liveState.data(), 48000, 2, application); }); - Require(sceOpusSilkEncCreate(liveState.data(), 48000, 2, application) == 0); - Require(sceOpusSilkEncDestroy(liveState.data()) == 0); + RequireThrows([&] { sceOpusSilkEncCreateEx(&context, ToState(liveState), 48000, 2, callerApplication); }); + Require(sceOpusSilkEncCreate(ToState(liveState), 48000, 2, callerApplication) == 0); + Require(sceOpusSilkEncDestroy(ToState(liveState)) == 0); } diff --git a/docs/dev/debt/libSceOpusCeltEnc.md b/docs/dev/debt/libSceOpusCeltEnc.md index da08250e37..2054453e74 100644 --- a/docs/dev/debt/libSceOpusCeltEnc.md +++ b/docs/dev/debt/libSceOpusCeltEnc.md @@ -2,12 +2,9 @@ ### Unknown function info -- [sceOpusCeltEncCtl, sceOpusCeltEncEncodeFloatWithPriority and sceOpusCeltEncEncodeWithPriority](../../../core/libs/prx/libSceOpusCeltEnc/Export.cpp) (libSceOpusCeltEnc) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceOpusCeltEnc.c). The prototypes mirror the sibling declarations committed for the encoder inventory and libopus's own ctl and priority entry points, but the console's request codes, priority and batch semantics were not recovered. They throw through `NotImplemented_nid_no_patch`; this is not an ABI claim. +- [sceOpusCeltEncCtl, sceOpusCeltEncEncodeFloatWithPriority and sceOpusCeltEncEncodeWithPriority](../../../core/libs/prx/libSceOpusCeltEnc/Export.cpp) (libSceOpusCeltEnc) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceOpusCeltEnc.c). The prototypes mirror the sibling encoder declarations and libopus's own ctl and priority entry points, but the console's request codes, priority and batch semantics were not recovered. They throw through `NotImplemented_nid_no_patch`; this is not an ABI claim. ### Functional -- [libSceOpusCeltEnc](../../../core/libs/prx/libSceOpusCeltEnc/Export.cpp) uses bundled libopus 1.6.1 with `OPUS_APPLICATION_RESTRICTED_CELT` for mono/stereo signed-16 and float encoding at 8000, 12000, 16000, 24000 and 48000 Hz. `Create` and `CreateEx` validate channels before sample rate before application; the state pointer is only a registry key, nothing is written into guest memory - -- [sceOpusCeltEncTerminate](../../../core/libs/prx/libSceOpusCeltEnc/Export.cpp) throws while a `CreateEx` encoder is live and `Create` without `CreateEx` binds an internal context id. The error base with the offsets 0, 1, 2, 3, 6, 7, 8, 9 and 13 follows the recovered libSceOpusCeltDec validation by analogy; no encoder measurement confirms it - -- A zero-frame [sceOpusCeltEncEncode](../../../core/libs/prx/libSceOpusCeltEnc/Export.cpp) or [sceOpusCeltEncEncodeFloat](../../../core/libs/prx/libSceOpusCeltEnc/Export.cpp), a libopus encoding rejection and `OPUS_ALLOC_FAIL` throw. AJM allocation and failures, AJM context ownership, the console `GetSize` value and the console's packet equivalence remain unimplemented. Closes when: a supplied PS5 module settles the state layout behind `GetSize`, the error base with its validation offsets and the ctl request and priority behaviour of the throwing exports +- [libSceOpusCeltEnc](../../../core/libs/prx/libSceOpusCeltEnc/Export.cpp): the state layout behind [GetSize](../../../core/libs/prx/libSceOpusCeltEnc/Export.cpp) is only partially recovered from the [firmware 13.42 export measurements](https://github.com/boykopovar/AnyPS5/pull/10563#issuecomment-6401539314). A successful encode setup writes the 728-byte zero-fill and the 0x17B2 marker at +0xCC into the state; the console's remaining setup bytes and whether its packets equal ours stay unknown. The console requires application 2051 (`OPUS_APPLICATION_RESTRICTED_LOWDELAY`) and the library builds its encoder with the restricted celt application instead; that mapping is this implementation's choice, not recovered from the setup +- [Create, CreateEx, Destroy, GetSize and Initialize](../../../core/libs/prx/libSceOpusCeltEnc/Export.cpp) refusal codes follow the same measurement, base 0x813F0000 with the offsets +0x01, +0x10, +0x0D and +0x11 for null state or context, sample rate, channels and application, +0x16 for a wrong state marker in `Destroy` and +0x19 for `Initialize(nullptr)`; other firmware versions remain unverified. [Encode and EncodeFloat](../../../core/libs/prx/libSceOpusCeltEnc/Export.cpp) still validate with the offsets 0, 1, 6, 7, 8 and 9 that the recovered libSceOpusCeltDec validation suggests; the measurement did not cover them. `Terminate` throwing for live `CreateEx`, a zero-frame encode, a libopus rejection and `OPUS_ALLOC_FAIL` also throw as that analogue does. Closes when: a supplied PS5 module settles the state layout, the `Encode` validation and the console's packet equivalence diff --git a/docs/dev/debt/libSceOpusSilkEnc.md b/docs/dev/debt/libSceOpusSilkEnc.md index 331ac8e3ba..ac02155546 100644 --- a/docs/dev/debt/libSceOpusSilkEnc.md +++ b/docs/dev/debt/libSceOpusSilkEnc.md @@ -2,12 +2,9 @@ ### Unknown function info -- [sceOpusSilkEncCancel, sceOpusSilkEncCtl, sceOpusSilkEncEncodeFloatWithPriority, sceOpusSilkEncEncodeFloatWithPriorityAndTimeout, sceOpusSilkEncEncodeWithPriority and sceOpusSilkEncEncodeWithPriorityAndTimeout](../../../core/libs/prx/libSceOpusSilkEnc/Export.cpp) (libSceOpusSilkEnc) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceOpusSilkEnc.c). The prototypes mirror the sibling declarations committed for the encoder inventory and libopus's own ctl and priority entry points, but the console's request codes, priority, timeout and batch-id semantics were not recovered. They throw through `NotImplemented_nid_no_patch`; this is not an ABI claim. +- [sceOpusSilkEncCancel, sceOpusSilkEncCtl, sceOpusSilkEncEncodeFloatWithPriority, sceOpusSilkEncEncodeFloatWithPriorityAndTimeout, sceOpusSilkEncEncodeWithPriority and sceOpusSilkEncEncodeWithPriorityAndTimeout](../../../core/libs/prx/libSceOpusSilkEnc/Export.cpp) (libSceOpusSilkEnc) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceOpusSilkEnc.c). The prototypes mirror the sibling encoder declarations and libopus's own ctl and priority entry points, but the console's request codes, priority, timeout and batch-id semantics were not recovered. They throw through `NotImplemented_nid_no_patch`; this is not an ABI claim. ### Functional -- [libSceOpusSilkEnc](../../../core/libs/prx/libSceOpusSilkEnc/Export.cpp) uses bundled libopus 1.6.1 with `OPUS_APPLICATION_RESTRICTED_SILK` for mono/stereo signed-16 and float encoding at 8000, 12000, 16000, 24000 and 48000 Hz. `Create` and `CreateEx` validate channels before sample rate before application; the state pointer is only a registry key, nothing is written into guest memory - -- [sceOpusSilkEncTerminate](../../../core/libs/prx/libSceOpusSilkEnc/Export.cpp) throws while a `CreateEx` encoder is live and `Create` without `CreateEx` binds an internal context id. The error base and the offsets 0, 1, 2, 3, 6, 7, 8, 9 and 13 follow the recovered libSceOpusCeltDec validation by analogy; no encoder measurement confirms it - -- A zero-frame [sceOpusSilkEncEncode](../../../core/libs/prx/libSceOpusSilkEnc/Export.cpp) or [sceOpusSilkEncEncodeFloat](../../../core/libs/prx/libSceOpusSilkEnc/Export.cpp), a libopus encoding rejection and `OPUS_ALLOC_FAIL` throw. AJM allocation and failures, AJM context ownership, the console `GetSize` value and the console's packet equivalence remain unimplemented. Closes when: a supplied PS5 module settles the state layout behind `GetSize`, the error base with its validation offsets and the ctl request along with the priority, timeout, batch-id and cancel behaviour of the throwing exports +- [libSceOpusSilkEnc](../../../core/libs/prx/libSceOpusSilkEnc/Export.cpp): the state layout behind [GetSize](../../../core/libs/prx/libSceOpusSilkEnc/Export.cpp) is only partially recovered from the [firmware 13.42 export measurements](https://github.com/boykopovar/AnyPS5/pull/10563#issuecomment-6401539314). A successful encode setup writes the 752-byte zero-fill and the 0x17B2 marker at +0xE8 into the state; the console's remaining setup bytes and whether its packets equal ours stay unknown. The console requires application 2048 (`OPUS_APPLICATION_VOIP`) and the library builds its encoder with the restricted silk application instead; that mapping is this implementation's choice, not recovered from the setup +- [Create, CreateEx, Destroy, GetSize and Initialize](../../../core/libs/prx/libSceOpusSilkEnc/Export.cpp) refusal codes follow the same measurement, base 0x81770000 with the offsets +0x01, +0x0F, +0x0C and +0x10 for null state or context, sample rate, channels and application, +0x15 for a wrong state marker in `Destroy` and +0x18 for `Initialize(nullptr)`; other firmware versions remain unverified. [Encode and EncodeFloat](../../../core/libs/prx/libSceOpusSilkEnc/Export.cpp) still validate with the offsets 0, 1, 6, 7, 8 and 9 that the recovered libSceOpusCeltDec validation suggests; the measurement did not cover them. `Terminate` throwing for live `CreateEx`, a zero-frame encode, a libopus rejection and `OPUS_ALLOC_FAIL` also throw as that analogue does. Closes when: a supplied PS5 module settles the state layout, the `Encode` validation and the console's packet equivalence From 87d01722ce128129cfb0598242b411b7debce02c Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sun, 25 Oct 2026 23:17:56 -0700 Subject: [PATCH 089/198] test(libScePad): stop requiring the unmodelled port types to throw Port types 3, 4 and 16 are valid ones the console answers. scePadResetLightBarAllByPortType still refuses them, but a test that requires the refusal would fail once they are supported. The positive and invalid-argument checks stay. Asked for in oneandonlydean's review. --- core/libs/tests/GuestPad.cpp | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/core/libs/tests/GuestPad.cpp b/core/libs/tests/GuestPad.cpp index 595e84a16e..7ebbb256a0 100644 --- a/core/libs/tests/GuestPad.cpp +++ b/core/libs/tests/GuestPad.cpp @@ -456,15 +456,6 @@ static bool LightBarIs(int port, bool valid, std::uint8_t r, std::uint8_t g, std return output.lightBarValid == valid && output.lightBar[0] == r && output.lightBar[1] == g && output.lightBar[2] == b; } -static bool ResetByPortTypeThrows(std::uint32_t portType) { - try { - scePadResetLightBarAllByPortType(portType); - } catch (const std::exception&) { - return true; - } - return false; -} - static void CheckLightBarResets(int handle) { const PadLightBarParam color{10, 20, 30}; const auto setBoth = [&] { @@ -480,7 +471,6 @@ static void CheckLightBarResets(int handle) { setBoth(); Require(scePadResetLightBarAllByPortType(PAD_PORT_TYPE_SPECIAL) == PAD_OK); for (const std::uint32_t type : {1u, 5u, 6u, 0xffu, 0xffffffffu}) Require(scePadResetLightBarAllByPortType(type) == PAD_ERROR_INVALID_ARG); - for (const std::uint32_t type : {3u, 4u, 16u}) Require(ResetByPortTypeThrows(type)); Require(LightBarIs(0, true, 10, 20, 30) && LightBarIs(2, true, 40, 50, 60)); Require(scePadResetLightBarAllByPortType(PAD_PORT_TYPE_STANDARD) == PAD_OK); for (int port = 0; port < PAD_MAX_CONTROLLERS; ++port) Require(LightBarIs(port, false, 0, 0, 0)); From 8403d317449b49a6fb714fbfd1e0a0c144a891b1 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sun, 25 Oct 2026 23:24:58 -0700 Subject: [PATCH 090/198] test(libSceAppContent): drop two placeholder-throw checks Below SDK 0x03500000, sceAppContentAddcontEnqueueDownloadSp and sceAppContentAddcontShrink throw for a padded entitlement label. The console accepts that input, so checks requiring the throw would fail once the case is answered. The throw stays in the code, and the checks from SDK 0x03500000 up stay. Asked for in oneandonlydean's review. --- core/libs/tests/GuestAppContent.cpp | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/core/libs/tests/GuestAppContent.cpp b/core/libs/tests/GuestAppContent.cpp index f38916c7df..98b22a153c 100644 --- a/core/libs/tests/GuestAppContent.cpp +++ b/core/libs/tests/GuestAppContent.cpp @@ -8,7 +8,6 @@ #include #include #include -#include #include "ProcessParametersFixture.hpp" static constexpr int ErrorParameter = static_cast(0x80D90002); @@ -18,15 +17,6 @@ static constexpr int ErrorBusy = static_cast(0x80D90003); static constexpr int ErrorNotMounted = static_cast(0x80D90004); static void Require(bool value) { if (!value) std::abort(); } -template static bool Throws(Call call) { - try { - call(); - } catch (const std::runtime_error&) { - return true; - } - return false; -} - int main() { static std::array process{}; Require(ProcessParametersFixture::Install(process.data(), process.size())); @@ -263,9 +253,6 @@ int main() { if (version >= 0x03500000) { Require(sceAppContentAddcontEnqueueDownloadSp(0, &label) == ErrorParameter); Require(sceAppContentAddcontShrink(0, &label) == ErrorParameter); - } else { - Require(Throws([&] { sceAppContentAddcontEnqueueDownloadSp(0, &label); })); - Require(Throws([&] { sceAppContentAddcontShrink(0, &label); })); } Require(std::memcmp(info, untouchedInfo, sizeof(info)) == 0); Require(std::memcmp(progress, progressBefore, sizeof(progress)) == 0); From 7bf29ea91b310493e1ebb0190f9529b04e46a302 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Mon, 26 Oct 2026 05:09:24 +0000 Subject: [PATCH 091/198] ci(windows): link host-only tests on pull requests --- .github/workflows/build.yml | 29 +++- CMakeLists.txt | 5 + tools/build_mingw_host_only_tests.py | 161 ++++++++++++++++++ .../test_mingw_host_only_test_targets.py | 128 ++++++++++++++ 4 files changed, 320 insertions(+), 3 deletions(-) create mode 100644 tools/build_mingw_host_only_tests.py create mode 100644 tools/tests/test_mingw_host_only_test_targets.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7813e6b53e..bcaaba9cc4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -56,6 +56,7 @@ jobs: relinker: ${{ steps.gate.outputs.relinker }} register-context: ${{ steps.gate.outputs.register-context }} write-watch: ${{ steps.gate.outputs.write-watch }} + mingw-host-tests: ${{ steps.gate.outputs.mingw-host-tests }} full: ${{ steps.gate.outputs.full }} relinker-hosts: ${{ steps.gate.outputs.relinker-hosts }} steps: @@ -134,6 +135,10 @@ jobs: registerContext: edits('register-context') || touches('core/libs/tests/register_context/', 'core/libs/tests/RegisterContext.cpp', libc), writeWatch: edits('write-watch') || touches('core/libs/tests/write_watch/', 'core/libs/tests/WriteWatch.cpp', libc), + mingwHostTests: edits('eligibility') || edits('windows') || touches( + 'core/libs/', 'tools/build_mingw_host_only_tests.py', + 'tools/tests/test_mingw_host_only_test_targets.py', 'CMakeLists.txt' + ), }; }; const relinkerHosts = (full, linux) => JSON.stringify([ @@ -168,6 +173,7 @@ jobs: core.setOutput('relinker', run.relinker); core.setOutput('register-context', run.registerContext); core.setOutput('write-watch', run.writeWatch); + core.setOutput('mingw-host-tests', false); core.setOutput('full', true); core.setOutput('relinker-hosts', relinkerHosts(true, true)); await core.summary.addRaw( @@ -257,6 +263,7 @@ jobs: core.setOutput('relinker', relinker); core.setOutput('register-context', registerContext); core.setOutput('write-watch', writeWatch); + core.setOutput('mingw-host-tests', !manual && run.mingwHostTests); core.setOutput('full', full); core.setOutput('relinker-hosts', relinkerHosts(full, linux)); const merged = pulls.map((pull) => `PR #${pull.number} at ${pull.head} merged into main at ${main.commit.sha}`); @@ -557,7 +564,7 @@ jobs: windows: needs: eligibility - if: ${{ (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && needs.eligibility.outputs.full == 'true' }} + if: ${{ ((github.event_name == 'push' || github.event_name == 'workflow_dispatch') && needs.eligibility.outputs.full == 'true') || (github.event_name == 'pull_request' && needs.eligibility.outputs.mingw-host-tests == 'true') }} runs-on: windows-latest timeout-minutes: 45 env: @@ -642,6 +649,9 @@ jobs: with: path: configure-seed.cmake key: configure-seed-${{ env.WINLIBS_ZIP }}-${{ steps.submodules.outputs.key }}-${{ hashFiles('CMakeLists.txt', 'cmake/**', 'core/libs/CMakeLists.txt', 'tools/configure_seed.py', '.github/workflows/build.yml') }} + - if: github.event_name == 'pull_request' + shell: bash + run: mkdir -p build/.cmake/api/v1/query && touch build/.cmake/api/v1/query/codemodel-v2 - shell: cmd run: if exist configure-seed.cmake (cmake -C configure-seed.cmake -S . -B build %CONFIGURE% "-DANYPS5_AGC_EXECUTION_TESTS=F16ToF32Unpack;F64FmaSubnormals;RegistrationPrepareShutdown") else (cmake -S . -B build %CONFIGURE% "-DANYPS5_AGC_EXECUTION_TESTS=F16ToF32Unpack;F64FmaSubnormals;RegistrationPrepareShutdown") - if: steps.seed-cache.outputs.cache-hit != 'true' @@ -677,14 +687,27 @@ jobs: git diff --name-only %BASE% HEAD > changed.txt python tools/select_tests.py select build test-graph.json changed.txt - id: build + if: github.event_name != 'pull_request' shell: cmd run: if exist build\select.ninja (ninja -C build -f select.ninja selected) else (cmake --build build) + - name: List changed files for host-only test linking + if: github.event_name == 'pull_request' + env: + BASE: ${{ needs.eligibility.outputs.sha }} + run: git diff --name-only "$BASE" HEAD > changed.txt + - name: Link changed host-only core/libs test executables + if: github.event_name == 'pull_request' + shell: cmd + run: python tools/build_mingw_host_only_tests.py build changed.txt - id: libs + if: github.event_name != 'pull_request' shell: cmd run: cmake --build build --target libs - - shell: cmd + - if: github.event_name != 'pull_request' + shell: cmd run: python tools/select_tests.py ctest build --output-on-failure --timeout 120 --parallel 4 --output-log ctest.log --output-junit ctest.xml - - run: python tools/check_skips.py build/ctest.xml --job windows + - if: github.event_name != 'pull_request' + run: python tools/check_skips.py build/ctest.xml --job windows - name: Write the test graph of main if: ${{ !cancelled() && github.event_name == 'push' && steps.build.outcome == 'success' && steps.libs.outcome == 'success' }} continue-on-error: true diff --git a/CMakeLists.txt b/CMakeLists.txt index dd86602fde..b63d0a076d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -299,6 +299,11 @@ if(BUILD_TESTING) endif() endif() +if(BUILD_TESTING AND Python3_Interpreter_FOUND) + add_test(NAME mingw_host_only_test_targets COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_mingw_host_only_test_targets.py) +endif() + if(ANYPS5_RELINKER_ONLY) add_subdirectory(core/relinker) return() diff --git a/tools/build_mingw_host_only_tests.py b/tools/build_mingw_host_only_tests.py new file mode 100644 index 0000000000..ad8f7d09d6 --- /dev/null +++ b/tools/build_mingw_host_only_tests.py @@ -0,0 +1,161 @@ +import argparse +import json +from pathlib import Path +import re +import subprocess +import sys + + +def read_reply(build_dir): + reply_dir = build_dir / ".cmake" / "api" / "v1" / "reply" + indexes = list(reply_dir.glob("index-*.json")) + if not indexes: + raise ValueError(f"{reply_dir}: no CMake File API index; configure with the codemodel-v2 query first") + index = max(indexes, key=lambda path: path.stat().st_mtime_ns) + document = json.loads(index.read_text(encoding="utf-8")) + codemodel = document.get("reply", {}).get("codemodel-v2") + if not codemodel: + raise ValueError(f"{index}: missing codemodel-v2 reply") + model = json.loads((reply_dir / codemodel["jsonFile"]).read_text(encoding="utf-8")) + configurations = model.get("configurations", []) + if len(configurations) != 1: + raise ValueError(f"{index}: expected one CMake configuration, found {len(configurations)}") + targets = {} + for target in configurations[0].get("targets", []): + targets[target["id"]] = json.loads((reply_dir / target["jsonFile"]).read_text(encoding="utf-8")) + return Path(model["paths"]["source"]).resolve(), targets + + +def is_libc_link(name, target): + if name == "libc" or name == "libs": + return True + link = target.get("link", {}) + fragments = link.get("commandFragments", []) + command = " ".join(fragment.get("fragment", "") for fragment in fragments) + return re.search(r"(?i)(?:^|[/\\\s])libc(?:\.prx|\.dll\.a)(?:$|\s)", command) is not None + + +def links_guest_libc(target, targets, visited=None): + visited = set() if visited is None else visited + target_id = target["id"] + if target_id in visited: + return False + visited.add(target_id) + if is_libc_link(target["name"], target): + return True + for dependency in target.get("dependencies", []): + linked_target = targets.get(dependency["id"]) + if linked_target is not None and links_guest_libc(linked_target, targets, visited): + return True + return False + + +def has_product_library_dependency(target, targets, source_root, visited=None): + visited = set() if visited is None else visited + target_id = target["id"] + if target_id in visited: + return False + visited.add(target_id) + for dependency in target.get("dependencies", []): + linked_target = targets.get(dependency["id"]) + if linked_target is None: + continue + if linked_target.get("type") in {"STATIC_LIBRARY", "SHARED_LIBRARY", "MODULE_LIBRARY"}: + source_dir = (source_root / linked_target["paths"]["source"]).resolve() + try: + source_dir.relative_to(source_root / "core" / "libs") + in_project_libs = True + except ValueError: + in_project_libs = False + try: + source_dir.relative_to(source_root / "core" / "libs" / "tests") + in_test_tree = True + except ValueError: + in_test_tree = False + if in_project_libs and not in_test_tree: + return True + if has_product_library_dependency(linked_target, targets, source_root, visited): + return True + return False + + +def source_relative_path(source_root, source): + try: + return source.relative_to(source_root).as_posix() + except ValueError: + return None + + +def host_only_test_targets(source_root, targets, changed_paths=None): + selected = [] + for target in targets.values(): + if target.get("type") != "EXECUTABLE": + continue + target_source_dir = (source_root / target["paths"]["source"]).resolve() + try: + target_source_dir.relative_to(source_root / "core" / "libs") + except ValueError: + continue + sources = [source_root / source["path"] for source in target.get("sources", []) if not source.get("isGenerated")] + if not any("tests" in source.parts and source.is_relative_to(source_root / "core" / "libs") for source in sources): + continue + if links_guest_libc(target, targets) or has_product_library_dependency(target, targets, source_root): + continue + selected.append(target["name"]) + selected = sorted(set(selected), key=str.casefold) + if changed_paths is None: + return selected + changed = {Path(path).as_posix() for path in changed_paths if path} + changed_source = { + relative + for target in targets.values() + if target.get("name") in selected + for item in target.get("sources", []) if not item.get("isGenerated") + for relative in [source_relative_path(source_root, source_root / item["path"])] + if relative is not None + } + matched_sources = changed & changed_source + if matched_sources: + return [ + target["name"] for target in targets.values() + if target.get("name") in selected + and any( + source_relative_path(source_root, source_root / item["path"]) in matched_sources + for item in target.get("sources", []) if not item.get("isGenerated") + ) + ] + broad_change = any( + path in {"CMakeLists.txt", ".github/workflows/build.yml", "tools/build_mingw_host_only_tests.py"} + or path.startswith("core/libs/") and ( + path.endswith((".h", ".hpp", ".hh", ".inl")) or Path(path).name == "CMakeLists.txt" + ) + for path in changed + ) + return selected if broad_change else [] + + +def main(argv=None): + parser = argparse.ArgumentParser(description="build core/libs test executables that do not link guest libc") + parser.add_argument("build_dir", type=Path) + parser.add_argument("changed_paths", nargs="?", type=Path) + parser.add_argument("--list-only", action="store_true") + args = parser.parse_args(argv) + try: + source_root, targets = read_reply(args.build_dir.resolve()) + changed_paths = args.changed_paths.read_text(encoding="utf-8").splitlines() if args.changed_paths else None + selected = host_only_test_targets(source_root, targets, changed_paths) + if not selected: + print("No changed host-only core/libs test executables require a link check") + return 0 + print("Host-only core/libs test executables:") + print("\n".join(selected)) + if not args.list_only: + subprocess.run(["cmake", "--build", str(args.build_dir), "--parallel", "3", "--target", *selected], check=True) + except (OSError, KeyError, TypeError, ValueError, json.JSONDecodeError, subprocess.CalledProcessError) as error: + print(f"{parser.prog}: {error}", file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/tests/test_mingw_host_only_test_targets.py b/tools/tests/test_mingw_host_only_test_targets.py new file mode 100644 index 0000000000..1cc367161c --- /dev/null +++ b/tools/tests/test_mingw_host_only_test_targets.py @@ -0,0 +1,128 @@ +import json +from pathlib import Path +import sys +import tempfile +import unittest + + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import build_mingw_host_only_tests + + +class HostOnlyTargets(unittest.TestCase): + def target(self, name, target_type="EXECUTABLE", source="core/libs/tests/HostOnly.cpp", dependencies=(), fragment=""): + return { + "artifacts": [{"path": f"tests/{name}.exe"}], + "dependencies": [{"id": dependency} for dependency in dependencies], + "id": name, + "link": {"commandFragments": [{"fragment": fragment}] if fragment else []}, + "name": name, + "paths": {"build": "core/libs", "source": "core/libs"}, + "sources": [{"path": source}], + "type": target_type, + } + + def fixture(self, targets): + directory = Path(self.temporary.name) + reply = directory / "build" / ".cmake" / "api" / "v1" / "reply" + reply.mkdir(parents=True) + entries = [] + for target in targets: + filename = f"target-{target['name']}.json" + (reply / filename).write_text(json.dumps(target), encoding="utf-8") + entries.append({"id": target["id"], "jsonFile": filename, "name": target["name"]}) + model = {"configurations": [{"targets": entries}], "paths": {"source": str(directory)}} + (reply / "codemodel.json").write_text(json.dumps(model), encoding="utf-8") + index = {"reply": {"codemodel-v2": {"jsonFile": "codemodel.json"}}} + (reply / "index-0001.json").write_text(json.dumps(index), encoding="utf-8") + return directory / "build" + + def setUp(self): + self.temporary = tempfile.TemporaryDirectory() + + def tearDown(self): + self.temporary.cleanup() + + def test_selects_core_libs_test_executables_without_guest_libc(self): + host = self.target("host_tests") + guest = self.target("guest_tests", dependencies=("libc-id",)) + libc = self.target("libc", target_type="SHARED_LIBRARY") + libc["id"] = "libc-id" + other = self.target("outside_tests", source="core/relinker/tests/Outside.cpp") + utility = self.target("not_a_test", source="core/libs/nid/src/main.cpp") + build_dir = self.fixture([host, guest, libc, other, utility]) + source_root, targets = build_mingw_host_only_tests.read_reply(build_dir) + self.assertEqual(build_mingw_host_only_tests.host_only_test_targets(source_root, targets), ["host_tests"]) + + def test_excludes_a_transitive_guest_libc_dependency(self): + helper = self.target("guest_helper", target_type="STATIC_LIBRARY", source="core/libs/tests/GuestHelper.cpp", + dependencies=("libc-id",)) + guest = self.target("guest_tests", dependencies=("helper-id",)) + libc = self.target("libc", target_type="SHARED_LIBRARY") + helper["id"] = "helper-id" + libc["id"] = "libc-id" + build_dir = self.fixture([guest, helper, libc]) + source_root, targets = build_mingw_host_only_tests.read_reply(build_dir) + self.assertEqual(build_mingw_host_only_tests.host_only_test_targets(source_root, targets), []) + + def test_excludes_a_direct_libc_file_and_the_full_libraries_target(self): + direct_file = self.target("direct_file_tests", fragment="/build/libs/libc.prx") + all_libraries = self.target("all_libraries_tests", dependencies=("libs-id",)) + libraries = self.target("libs", target_type="UTILITY") + libraries["id"] = "libs-id" + build_dir = self.fixture([direct_file, all_libraries, libraries]) + source_root, targets = build_mingw_host_only_tests.read_reply(build_dir) + self.assertEqual(build_mingw_host_only_tests.host_only_test_targets(source_root, targets), []) + + def test_excludes_product_library_dependencies_but_keeps_test_helpers(self): + product = self.target("product_lib", target_type="STATIC_LIBRARY", source="core/libs/utility/src/Utility.cpp") + product["id"] = "product-id" + production_dependent = self.target("production_dependent", dependencies=("product-id",)) + helper = self.target("test_helper", target_type="STATIC_LIBRARY", source="core/libs/tests/GuestHelper.cpp") + helper["paths"]["source"] = "core/libs/tests" + helper["id"] = "helper-id" + helper_dependent = self.target("helper_dependent", dependencies=("helper-id",), source="core/libs/tests/HostOnly.cpp") + external = self.target("external", target_type="STATIC_LIBRARY", source="3rdparty/sdl/src/SDL.cpp") + external["paths"]["source"] = "3rdparty/sdl/src" + external["id"] = "external-id" + external_dependent = self.target("external_dependent", dependencies=("external-id",)) + build_dir = self.fixture([product, production_dependent, helper, helper_dependent, external, external_dependent]) + source_root, targets = build_mingw_host_only_tests.read_reply(build_dir) + self.assertEqual(build_mingw_host_only_tests.host_only_test_targets(source_root, targets), ["external_dependent", "helper_dependent"]) + + def test_selects_only_changed_host_test_sources(self): + first = self.target("first_tests") + second = self.target("second_tests", source="core/libs/tests/Second.cpp") + build_dir = self.fixture([first, second]) + source_root, targets = build_mingw_host_only_tests.read_reply(build_dir) + selected = build_mingw_host_only_tests.host_only_test_targets( + source_root, targets, ["core/libs/tests/Second.cpp"] + ) + self.assertEqual(selected, ["second_tests"]) + + def test_cmake_or_header_changes_check_all_host_targets(self): + first = self.target("first_tests") + second = self.target("second_tests", source="core/libs/tests/Second.cpp") + build_dir = self.fixture([first, second]) + source_root, targets = build_mingw_host_only_tests.read_reply(build_dir) + self.assertEqual( + build_mingw_host_only_tests.host_only_test_targets( + source_root, targets, ["core/libs/CMakeLists.txt"] + ), + ["first_tests", "second_tests"], + ) + self.assertEqual( + build_mingw_host_only_tests.host_only_test_targets( + source_root, targets, ["core/libs/tests/HostOnly.hpp"] + ), + ["first_tests", "second_tests"], + ) + + def test_fails_when_cmake_did_not_write_a_file_api_index(self): + with tempfile.TemporaryDirectory() as directory: + with self.assertRaisesRegex(ValueError, "no CMake File API index"): + build_mingw_host_only_tests.read_reply(Path(directory)) + + +if __name__ == "__main__": + unittest.main() From c57ba015f63532405340fa8873daf2faff6c501d Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Mon, 26 Oct 2026 05:56:12 +0000 Subject: [PATCH 092/198] fix(ci): select all host tests for header changes --- tools/build_mingw_host_only_tests.py | 18 ++++++++++-------- .../tests/test_mingw_host_only_test_targets.py | 13 +++++++++++++ 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/tools/build_mingw_host_only_tests.py b/tools/build_mingw_host_only_tests.py index ad8f7d09d6..b70d691173 100644 --- a/tools/build_mingw_host_only_tests.py +++ b/tools/build_mingw_host_only_tests.py @@ -106,6 +106,15 @@ def host_only_test_targets(source_root, targets, changed_paths=None): if changed_paths is None: return selected changed = {Path(path).as_posix() for path in changed_paths if path} + broad_change = any( + path in {"CMakeLists.txt", ".github/workflows/build.yml", "tools/build_mingw_host_only_tests.py"} + or path.startswith("core/libs/") and ( + path.endswith((".h", ".hpp", ".hh", ".inl")) or Path(path).name == "CMakeLists.txt" + ) + for path in changed + ) + if broad_change: + return selected changed_source = { relative for target in targets.values() @@ -124,14 +133,7 @@ def host_only_test_targets(source_root, targets, changed_paths=None): for item in target.get("sources", []) if not item.get("isGenerated") ) ] - broad_change = any( - path in {"CMakeLists.txt", ".github/workflows/build.yml", "tools/build_mingw_host_only_tests.py"} - or path.startswith("core/libs/") and ( - path.endswith((".h", ".hpp", ".hh", ".inl")) or Path(path).name == "CMakeLists.txt" - ) - for path in changed - ) - return selected if broad_change else [] + return [] def main(argv=None): diff --git a/tools/tests/test_mingw_host_only_test_targets.py b/tools/tests/test_mingw_host_only_test_targets.py index 1cc367161c..e12edad542 100644 --- a/tools/tests/test_mingw_host_only_test_targets.py +++ b/tools/tests/test_mingw_host_only_test_targets.py @@ -118,6 +118,19 @@ class HostOnlyTargets(unittest.TestCase): ["first_tests", "second_tests"], ) + def test_header_listed_as_a_target_source_still_selects_all_host_targets(self): + first = self.target("first_tests") + second = self.target("second_tests", source="core/libs/tests/Second.cpp") + second["sources"].append({"path": "core/libs/tests/Shared.hpp"}) + build_dir = self.fixture([first, second]) + source_root, targets = build_mingw_host_only_tests.read_reply(build_dir) + self.assertEqual( + build_mingw_host_only_tests.host_only_test_targets( + source_root, targets, ["core/libs/tests/Shared.hpp"] + ), + ["first_tests", "second_tests"], + ) + def test_fails_when_cmake_did_not_write_a_file_api_index(self): with tempfile.TemporaryDirectory() as directory: with self.assertRaisesRegex(ValueError, "no CMake File API index"): From f90468c219666ba49dc84d6cf78fc59f392e5a18 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Mon, 26 Oct 2026 06:52:05 +0000 Subject: [PATCH 093/198] fix(ci): inspect linker inputs for host-only tests --- tools/build_mingw_host_only_tests.py | 95 +++++++++++-------- .../test_mingw_host_only_test_targets.py | 31 +++++- 2 files changed, 81 insertions(+), 45 deletions(-) diff --git a/tools/build_mingw_host_only_tests.py b/tools/build_mingw_host_only_tests.py index b70d691173..396fe945b0 100644 --- a/tools/build_mingw_host_only_tests.py +++ b/tools/build_mingw_host_only_tests.py @@ -26,55 +26,70 @@ def read_reply(build_dir): return Path(model["paths"]["source"]).resolve(), targets -def is_libc_link(name, target): - if name == "libc" or name == "libs": - return True +def link_command(target): link = target.get("link", {}) fragments = link.get("commandFragments", []) - command = " ".join(fragment.get("fragment", "") for fragment in fragments) - return re.search(r"(?i)(?:^|[/\\\s])libc(?:\.prx|\.dll\.a)(?:$|\s)", command) is not None + return " ".join(fragment.get("fragment", "") for fragment in fragments) -def links_guest_libc(target, targets, visited=None): - visited = set() if visited is None else visited - target_id = target["id"] - if target_id in visited: +def is_libc_link(name, target): + if name == "libc": + return True + command = link_command(target) + return re.search(r"(?i)(?:^|[/\\\s])libc(?:\.prx|\.dll\.a)(?:$|[\s\"'])", command) is not None + + +def links_guest_libc(target): + return is_libc_link(target["name"], target) + + +def is_product_library(target, source_root): + if target.get("type") not in {"STATIC_LIBRARY", "SHARED_LIBRARY", "MODULE_LIBRARY"}: return False - visited.add(target_id) - if is_libc_link(target["name"], target): + source_dir = (source_root / target["paths"]["source"]).resolve() + try: + source_dir.relative_to(source_root / "core" / "libs") + except ValueError: + return False + try: + source_dir.relative_to(source_root / "core" / "libs" / "tests") + except ValueError: return True - for dependency in target.get("dependencies", []): - linked_target = targets.get(dependency["id"]) - if linked_target is not None and links_guest_libc(linked_target, targets, visited): - return True return False -def has_product_library_dependency(target, targets, source_root, visited=None): - visited = set() if visited is None else visited - target_id = target["id"] - if target_id in visited: +def link_command_mentions_target(command, target): + link_names = set() + for artifact in target.get("artifacts", []): + name = Path(artifact.get("path", "")).name + if name and name in command: + return True + for suffix in (".dll.a", ".dylib", ".prx", ".so", ".a", ".lib"): + if name.casefold().endswith(suffix): + name = name[:-len(suffix)] + break + if name.startswith("lib") and target.get("name", "").casefold() != "libc": + name = name[3:] + if name: + link_names.add(name) + target_name = target.get("name", "") + if target_name: + link_names.add(target_name) + return any( + re.search(rf"(?i)(?:^|\s)-l{re.escape(name)}(?=$|\s)", command) is not None + for name in link_names + ) + + +def links_product_library(target, targets, source_root): + # The File API dependency graph includes add_dependencies edges. The + # executable's link fragments contain the libraries actually passed to + # the linker, including transitive link libraries. + command = link_command(target) + if not command: return False - visited.add(target_id) - for dependency in target.get("dependencies", []): - linked_target = targets.get(dependency["id"]) - if linked_target is None: - continue - if linked_target.get("type") in {"STATIC_LIBRARY", "SHARED_LIBRARY", "MODULE_LIBRARY"}: - source_dir = (source_root / linked_target["paths"]["source"]).resolve() - try: - source_dir.relative_to(source_root / "core" / "libs") - in_project_libs = True - except ValueError: - in_project_libs = False - try: - source_dir.relative_to(source_root / "core" / "libs" / "tests") - in_test_tree = True - except ValueError: - in_test_tree = False - if in_project_libs and not in_test_tree: - return True - if has_product_library_dependency(linked_target, targets, source_root, visited): + for linked_target in targets.values(): + if is_product_library(linked_target, source_root) and link_command_mentions_target(command, linked_target): return True return False @@ -99,7 +114,7 @@ def host_only_test_targets(source_root, targets, changed_paths=None): sources = [source_root / source["path"] for source in target.get("sources", []) if not source.get("isGenerated")] if not any("tests" in source.parts and source.is_relative_to(source_root / "core" / "libs") for source in sources): continue - if links_guest_libc(target, targets) or has_product_library_dependency(target, targets, source_root): + if links_guest_libc(target) or links_product_library(target, targets, source_root): continue selected.append(target["name"]) selected = sorted(set(selected), key=str.casefold) diff --git a/tools/tests/test_mingw_host_only_test_targets.py b/tools/tests/test_mingw_host_only_test_targets.py index e12edad542..f1927be0fa 100644 --- a/tools/tests/test_mingw_host_only_test_targets.py +++ b/tools/tests/test_mingw_host_only_test_targets.py @@ -45,7 +45,8 @@ class HostOnlyTargets(unittest.TestCase): def test_selects_core_libs_test_executables_without_guest_libc(self): host = self.target("host_tests") - guest = self.target("guest_tests", dependencies=("libc-id",)) + guest = self.target("guest_tests", dependencies=("libc-id",), + fragment="C:/build/core/libs/prx/libc/libc.dll.a") libc = self.target("libc", target_type="SHARED_LIBRARY") libc["id"] = "libc-id" other = self.target("outside_tests", source="core/relinker/tests/Outside.cpp") @@ -57,7 +58,8 @@ class HostOnlyTargets(unittest.TestCase): def test_excludes_a_transitive_guest_libc_dependency(self): helper = self.target("guest_helper", target_type="STATIC_LIBRARY", source="core/libs/tests/GuestHelper.cpp", dependencies=("libc-id",)) - guest = self.target("guest_tests", dependencies=("helper-id",)) + guest = self.target("guest_tests", dependencies=("helper-id",), + fragment="C:/build/core/libs/prx/libc/libc.dll.a") libc = self.target("libc", target_type="SHARED_LIBRARY") helper["id"] = "helper-id" libc["id"] = "libc-id" @@ -65,19 +67,21 @@ class HostOnlyTargets(unittest.TestCase): source_root, targets = build_mingw_host_only_tests.read_reply(build_dir) self.assertEqual(build_mingw_host_only_tests.host_only_test_targets(source_root, targets), []) - def test_excludes_a_direct_libc_file_and_the_full_libraries_target(self): + def test_excludes_a_direct_libc_link_but_ignores_the_full_libraries_build_target(self): direct_file = self.target("direct_file_tests", fragment="/build/libs/libc.prx") all_libraries = self.target("all_libraries_tests", dependencies=("libs-id",)) libraries = self.target("libs", target_type="UTILITY") libraries["id"] = "libs-id" build_dir = self.fixture([direct_file, all_libraries, libraries]) source_root, targets = build_mingw_host_only_tests.read_reply(build_dir) - self.assertEqual(build_mingw_host_only_tests.host_only_test_targets(source_root, targets), []) + self.assertEqual(build_mingw_host_only_tests.host_only_test_targets(source_root, targets), ["all_libraries_tests"]) def test_excludes_product_library_dependencies_but_keeps_test_helpers(self): product = self.target("product_lib", target_type="STATIC_LIBRARY", source="core/libs/utility/src/Utility.cpp") + product["artifacts"] = [{"path": "core/libs/utility/libproduct_lib.a"}] product["id"] = "product-id" - production_dependent = self.target("production_dependent", dependencies=("product-id",)) + production_dependent = self.target("production_dependent", dependencies=("product-id",), + fragment="/build/core/libs/utility/libproduct_lib.a") helper = self.target("test_helper", target_type="STATIC_LIBRARY", source="core/libs/tests/GuestHelper.cpp") helper["paths"]["source"] = "core/libs/tests" helper["id"] = "helper-id" @@ -90,6 +94,23 @@ class HostOnlyTargets(unittest.TestCase): source_root, targets = build_mingw_host_only_tests.read_reply(build_dir) self.assertEqual(build_mingw_host_only_tests.host_only_test_targets(source_root, targets), ["external_dependent", "helper_dependent"]) + def test_ignores_build_order_dependencies_that_are_not_linked(self): + host = self.target( + "guest_sigvec_modules_tests", + dependencies=("libc-id", "kernel-id", "internal-id", "patcher-id"), + ) + libc = self.target("libc", target_type="SHARED_LIBRARY", source="core/libs/libc/src/libc.cpp") + kernel = self.target("libkernel", target_type="SHARED_LIBRARY", source="core/libs/libkernel/src/kernel.cpp") + internal = self.target("libSceLibcInternal", target_type="SHARED_LIBRARY", source="core/libs/internal/src/internal.cpp") + patcher = self.target("nid_patcher", target_type="STATIC_LIBRARY", source="core/libs/nid_patcher/src/patcher.cpp") + libc["id"], kernel["id"], internal["id"], patcher["id"] = "libc-id", "kernel-id", "internal-id", "patcher-id" + build_dir = self.fixture([host, libc, kernel, internal, patcher]) + source_root, targets = build_mingw_host_only_tests.read_reply(build_dir) + self.assertEqual( + build_mingw_host_only_tests.host_only_test_targets(source_root, targets), + ["guest_sigvec_modules_tests"], + ) + def test_selects_only_changed_host_test_sources(self): first = self.target("first_tests") second = self.target("second_tests", source="core/libs/tests/Second.cpp") From b52d5974776fcff01a3b1dc3c3c4d0033c8b3843 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Mon, 26 Oct 2026 06:53:12 +0000 Subject: [PATCH 094/198] style(ci): remove selector comments --- tools/build_mingw_host_only_tests.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tools/build_mingw_host_only_tests.py b/tools/build_mingw_host_only_tests.py index 396fe945b0..15b58d7eda 100644 --- a/tools/build_mingw_host_only_tests.py +++ b/tools/build_mingw_host_only_tests.py @@ -82,9 +82,6 @@ def link_command_mentions_target(command, target): def links_product_library(target, targets, source_root): - # The File API dependency graph includes add_dependencies edges. The - # executable's link fragments contain the libraries actually passed to - # the linker, including transitive link libraries. command = link_command(target) if not command: return False From 1dc7ca79af5970b2405d32e55202c0f8a7b373a7 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Mon, 26 Oct 2026 07:09:47 +0000 Subject: [PATCH 095/198] fix(ci): register selector test with Python tests --- CMakeLists.txt | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b63d0a076d..d565d5ba67 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -296,14 +296,11 @@ if(BUILD_TESTING) add_test(NAME performance_compare COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_performance_compare.py) set_tests_properties(performance_compare PROPERTIES TIMEOUT 20) + add_test(NAME mingw_host_only_test_targets COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_mingw_host_only_test_targets.py) endif() endif() -if(BUILD_TESTING AND Python3_Interpreter_FOUND) - add_test(NAME mingw_host_only_test_targets COMMAND ${Python3_EXECUTABLE} - ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_mingw_host_only_test_targets.py) -endif() - if(ANYPS5_RELINKER_ONLY) add_subdirectory(core/relinker) return() From a059a925cb987b828114c4676d8d79d75ca85b6e Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Mon, 26 Oct 2026 07:11:35 +0000 Subject: [PATCH 096/198] fix(ci): avoid CMake test registration overlap --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index d565d5ba67..472bd29b9f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -132,6 +132,8 @@ if(BUILD_TESTING) add_test(NAME pad_command_file COMMAND pad_command_file_tests) set_tests_properties(pad_command_file PROPERTIES TIMEOUT 20) if(Python3_Interpreter_FOUND) + add_test(NAME mingw_host_only_test_targets COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_mingw_host_only_test_targets.py) add_test(NAME shader_cache_native_paths COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler/tests/test_shader_cache_native_paths.py ${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler/ShaderCacheVersion.cmake ${CMAKE_COMMAND}) @@ -296,8 +298,6 @@ if(BUILD_TESTING) add_test(NAME performance_compare COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_performance_compare.py) set_tests_properties(performance_compare PROPERTIES TIMEOUT 20) - add_test(NAME mingw_host_only_test_targets COMMAND ${Python3_EXECUTABLE} - ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_mingw_host_only_test_targets.py) endif() endif() From 0a8d4229dac880894a3b0c58c8ef2588b300da9d Mon Sep 17 00:00:00 2001 From: Santhosh Kumar Date: Mon, 26 Oct 2026 14:37:57 +0530 Subject: [PATCH 097/198] test(libSceLibreSsl): cover supported protocol selection paths --- core/libs/tests/GuestLibreSslProtocol.cpp | 41 +---------------------- 1 file changed, 1 insertion(+), 40 deletions(-) diff --git a/core/libs/tests/GuestLibreSslProtocol.cpp b/core/libs/tests/GuestLibreSslProtocol.cpp index c4978d8174..e4ced5478f 100644 --- a/core/libs/tests/GuestLibreSslProtocol.cpp +++ b/core/libs/tests/GuestLibreSslProtocol.cpp @@ -9,8 +9,6 @@ #include #include #include -#include -#include #include #include @@ -68,20 +66,6 @@ void CheckSelection(SelectProtocol select, const Protocols& peerNames, const Pro Require(peer == peerCopy && supported == supportedCopy); } -void CheckInvalid(SelectProtocol select, const unsigned char* peer, std::uint32_t peerLength, - const unsigned char* supported, std::uint32_t supportedLength) { - unsigned char sentinel = 0; - unsigned char* output = &sentinel; - unsigned char length = 0xA5; - bool threw = false; - try { - select(&output, &length, peer, peerLength, supported, supportedLength); - } catch (const std::runtime_error& error) { - threw = std::string(error.what()).starts_with("SSL_select_next_proto:"); - } - Require(threw && output == &sentinel && length == 0xA5); -} - void Run(SelectProtocol select) { const Protocol http{'h', 't', 't', 'p', '/', '1', '.', '1'}; const Protocol h2{'h', '2'}; @@ -115,30 +99,7 @@ void Run(SelectProtocol select) { CheckSelection(select, large, {h2}); CheckSelection(select, {h2}, large); - const auto valid = Encode({h2, http}); - const std::vector malformed{{}, {0}, {2, 'h'}, {255, 'x'}, {1, 'a', 0}, {1, 'a', 2, 'b'}}; - for (const auto& list : malformed) { - CheckInvalid(select, list.data(), static_cast(list.size()), valid.data(), static_cast(valid.size())); - CheckInvalid(select, valid.data(), static_cast(valid.size()), list.data(), static_cast(list.size())); - } - CheckInvalid(select, nullptr, 0, valid.data(), static_cast(valid.size())); - CheckInvalid(select, nullptr, 1, valid.data(), static_cast(valid.size())); - CheckInvalid(select, valid.data(), static_cast(valid.size()), nullptr, 0); - CheckInvalid(select, valid.data(), static_cast(valid.size()), nullptr, 1); - CheckInvalid(select, valid.data(), 2, valid.data(), static_cast(valid.size())); - unsigned char* output = nullptr; - unsigned char length = 0xA5; - for (const bool missingPointer : {false, true}) { - bool threw = false; - try { - select(missingPointer ? nullptr : &output, missingPointer ? &length : nullptr, - valid.data(), static_cast(valid.size()), valid.data(), static_cast(valid.size())); - } catch (const std::runtime_error&) { - threw = true; - } - Require(threw && output == nullptr && length == 0xA5); - } - std::printf("LibreSSL protocol selection: 7056 generated pairs, boundaries and invalid inputs passed\n"); + std::printf("LibreSSL protocol selection: 7056 generated pairs, and valid-list boundaries passed\n"); } int main(int argc, char** argv) { From 14807c4628286b2a4b945605b5ede26b7a0ef687 Mon Sep 17 00:00:00 2001 From: Santhosh Kumar Date: Mon, 26 Oct 2026 14:58:51 +0530 Subject: [PATCH 098/198] docs(libSceLibreSsl): distinguish reported and unmeasured gaps --- docs/dev/debt/libSceLibreSsl.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceLibreSsl.md b/docs/dev/debt/libSceLibreSsl.md index ab02b01cc6..b0392c1717 100644 --- a/docs/dev/debt/libSceLibreSsl.md +++ b/docs/dev/debt/libSceLibreSsl.md @@ -2,7 +2,7 @@ ### Functional -- [SSL_select_next_proto](../../../core/libs/prx/libSceLibreSsl/src/ProtocolSelection.cpp) rejects null outputs, empty lists, zero-length names and truncated encodings instead of implementing the reported firmware behavior. [Independent firmware 13.42 evidence](https://github.com/boykopovar/AnyPS5/pull/9785#issuecomment-6406712390) reports host-PC execution of `libSceLibreSsl.sprx` at `0xda6d0`, with matching six-argument valid-list selection, status, borrowed pointer and length. That report also observes negotiation of zero-length entries, unchecked fallback lengths and an empty-supported-list read; these paths remain unsupported here. Null outputs and console-hardware behavior remain unmeasured. The author-sourced signature and valid-list contract follow LibreSSL 3.4.1. +- [SSL_select_next_proto](../../../core/libs/prx/libSceLibreSsl/src/ProtocolSelection.cpp) leaves empty lists, zero-length names and truncated encodings unsupported and throws instead of following the reported firmware behavior. Null outputs also throw; their firmware behavior is unmeasured. [Independent firmware 13.42 evidence](https://github.com/boykopovar/AnyPS5/pull/9785#issuecomment-6406712390) reports host-PC execution of `libSceLibreSsl.sprx` at `0xda6d0`, with matching six-argument valid-list selection, status, borrowed pointer and length. That report also observes negotiation of zero-length entries, unchecked fallback lengths and an empty-supported-list read; these paths remain unsupported here. Null outputs and console-hardware behavior remain unmeasured. The author-sourced signature and valid-list contract follow LibreSSL 3.4.1. ### Unknown function info From f6e523db3a82f8000987d64b2536621b42c9a6d7 Mon Sep 17 00:00:00 2001 From: Santhosh Kumar Date: Mon, 26 Oct 2026 15:00:50 +0530 Subject: [PATCH 099/198] fix(libSceFreeTypeOt): name identified export helpers --- core/libs/prx/libSceFreeTypeOt/Export.cpp | 8 ++++---- docs/dev/debt/libSceFreeTypeOt.md | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceFreeTypeOt/Export.cpp b/core/libs/prx/libSceFreeTypeOt/Export.cpp index c4ec1cd9ca..ad725720b0 100644 --- a/core/libs/prx/libSceFreeTypeOt/Export.cpp +++ b/core/libs/prx/libSceFreeTypeOt/Export.cpp @@ -241,15 +241,15 @@ int APS5_VABI sceFreeTypeOtUnknown_0Lv6K1d47jg_nid_no_patch() { } APS5_EXPORT("0Lv6K1d47jg", sceFreeTypeOtUnknown_0Lv6K1d47jg_nid_no_patch); -int APS5_VABI sceFreeTypeOtUnknown_8fAMdZCnewA_nid_no_patch() { +int APS5_VABI sceFreeTypeOtPropertyGet_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("8fAMdZCnewA", sceFreeTypeOtUnknown_8fAMdZCnewA_nid_no_patch); +APS5_EXPORT("8fAMdZCnewA", sceFreeTypeOtPropertyGet_nid_no_patch); -int APS5_VABI sceFreeTypeOtUnknown_LsB538_IPrE_nid_no_patch() { +int APS5_VABI sceFreeTypeOtPropertySet_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("LsB538+IPrE", sceFreeTypeOtUnknown_LsB538_IPrE_nid_no_patch); +APS5_EXPORT("LsB538+IPrE", sceFreeTypeOtPropertySet_nid_no_patch); int APS5_VABI sceFreeTypeOtUnknown_hz_J3hohXW8_nid_no_patch() { NotImplemented_nid_no_patch(__func__); diff --git a/docs/dev/debt/libSceFreeTypeOt.md b/docs/dev/debt/libSceFreeTypeOt.md index 6e93bdf35f..a0f692fcaa 100644 --- a/docs/dev/debt/libSceFreeTypeOt.md +++ b/docs/dev/debt/libSceFreeTypeOt.md @@ -4,6 +4,6 @@ - [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. -- The 140 `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 import-resolution helpers in [Export.cpp](../../../core/libs/prx/libSceFreeTypeOt/Export.cpp) retain the PS5 3.20 NIDs from [PS5-3.20_Libs](https://github.com/DNNDHH/PS5-3.20_Libs), including identified names and still-unidentified exports. Their provider-specific 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 FT_Outline_Get_Orientation, FT_Request_Size, FT_Sin, FT_Tan, FT_Vector_Unit, FT_New_Size, FT_Outline_Copy and FT_Outline_Decompose 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. From 379b8a550dd17796d1b2fa4fa5455ae7d41f541c Mon Sep 17 00:00:00 2001 From: Santhosh Kumar Date: Mon, 26 Oct 2026 15:00:51 +0530 Subject: [PATCH 100/198] fix(libSceFreeTypeOt): name identified export helpers --- core/libs/prx/libSceFreeTypeOt/Export.cpp | 4 ++-- docs/dev/debt/libSceFreeTypeOt.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceFreeTypeOt/Export.cpp b/core/libs/prx/libSceFreeTypeOt/Export.cpp index c4ec1cd9ca..e4d923afc8 100644 --- a/core/libs/prx/libSceFreeTypeOt/Export.cpp +++ b/core/libs/prx/libSceFreeTypeOt/Export.cpp @@ -206,10 +206,10 @@ int APS5_VABI sceFreeTypeOtUnknown_BgUMBGvhATU_nid_no_patch() { } APS5_EXPORT("BgUMBGvhATU", sceFreeTypeOtUnknown_BgUMBGvhATU_nid_no_patch); -int APS5_VABI sceFreeTypeOtUnknown_0Aal_TIC4so_nid_no_patch() { +int APS5_VABI sceFreeTypeOtOutlineGetBBox_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("0Aal-TIC4so", sceFreeTypeOtUnknown_0Aal_TIC4so_nid_no_patch); +APS5_EXPORT("0Aal-TIC4so", sceFreeTypeOtOutlineGetBBox_nid_no_patch); int APS5_VABI sceFreeTypeOtUnknown_4RRjK_Wd1lU_nid_no_patch() { NotImplemented_nid_no_patch(__func__); diff --git a/docs/dev/debt/libSceFreeTypeOt.md b/docs/dev/debt/libSceFreeTypeOt.md index 6e93bdf35f..a0f692fcaa 100644 --- a/docs/dev/debt/libSceFreeTypeOt.md +++ b/docs/dev/debt/libSceFreeTypeOt.md @@ -4,6 +4,6 @@ - [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. -- The 140 `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 import-resolution helpers in [Export.cpp](../../../core/libs/prx/libSceFreeTypeOt/Export.cpp) retain the PS5 3.20 NIDs from [PS5-3.20_Libs](https://github.com/DNNDHH/PS5-3.20_Libs), including identified names and still-unidentified exports. Their provider-specific 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 FT_Outline_Get_Orientation, FT_Request_Size, FT_Sin, FT_Tan, FT_Vector_Unit, FT_New_Size, FT_Outline_Copy and FT_Outline_Decompose 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. From e1a28b400bc1fe7cb0fbec52c56ae4692f2fd00f Mon Sep 17 00:00:00 2001 From: Bipin Date: Mon, 26 Oct 2026 15:12:27 +0530 Subject: [PATCH 101/198] fix(core): adopt the measured opus encoder module protocol Encode and EncodeFloat run the decrypted-module measurements: the null state +0x01, null samples +0x02 before the state marker, a markerless state +0x16/+0x15, null data +0x04 before the frame check, maxDataBytes 0 +0x05 and frame sizes outside the accepted durations +0x03, while a negative maxDataBytes reaches the backend as it reaches AJM on the console. Create zero-fills the state and writes the measured word and bitrate tables, Destroy clears the 0x17B2 marker leaving the other bytes, the encoder registry keys on the instance id at +0x00, and EncodeFloat aborts for a stack reservation that cannot fit, matching the console process death. The tests byte-check the measured layout with the per-rate bitrates and the new check orders behind both libraries' Api.hpp, and the debt notes carry the module runs with the remaining AJM gaps. --- core/libs/prx/libSceOpusCeltEnc/Export.cpp | 44 +++++- .../libSceOpusCeltEnc/include/OpusEncoder.hpp | 109 +++++++++---- core/libs/prx/libSceOpusSilkEnc/Export.cpp | 39 ++++- core/libs/tests/GuestOpusCeltEnc.cpp | 145 +++++++++++++++--- core/libs/tests/GuestOpusSilkEnc.cpp | 141 ++++++++++++++--- docs/dev/debt/libSceOpusCeltEnc.md | 11 +- docs/dev/debt/libSceOpusSilkEnc.md | 13 +- 7 files changed, 424 insertions(+), 78 deletions(-) diff --git a/core/libs/prx/libSceOpusCeltEnc/Export.cpp b/core/libs/prx/libSceOpusCeltEnc/Export.cpp index 5250169acc..47556b14d1 100644 --- a/core/libs/prx/libSceOpusCeltEnc/Export.cpp +++ b/core/libs/prx/libSceOpusCeltEnc/Export.cpp @@ -2,11 +2,53 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libSceOpusCeltEnc/include/OpusEncoder.hpp" #include +#include #include "prx/libc/include/General.hpp" namespace { -constexpr EncoderCodes codes{0x813F0000u, 1u, 0x10u, 0x0Du, 0x11u, 0x16u, 0x19u, 728, 0xCCu, 0x17B2u}; +std::uint32_t bitrate(int rate, int channels) { + if (rate == 8000) return channels == 1 ? 11000u : 19000u; + if (rate == 12000) return channels == 1 ? 15000u : 27000u; + if (rate == 16000) return channels == 1 ? 19000u : 35000u; + if (rate == 24000) return channels == 1 ? 27000u : 51000u; + return channels == 1 ? 51000u : 99000u; +} + +void writeWord(std::uint8_t* state, std::size_t offset, std::uint32_t value) { + std::memcpy(state + offset, &value, sizeof value); +} + +void writeState(void* state, std::uint32_t instanceId, int rate, int channels) { + auto* bytes = static_cast(state); + const auto rateWord = static_cast(rate); + const auto frameWord = static_cast(rate / 50); + const auto channelWord = static_cast(channels); + const auto bandwidth = bitrate(rate, channels); + writeWord(bytes, 0x00u, instanceId); + writeWord(bytes, 0x08u, 8u * channelWord); + writeWord(bytes, 0x0Cu, 2u); + writeWord(bytes, 0x10u, channelWord); + writeWord(bytes, 0x14u, rateWord); + writeWord(bytes, 0x1Cu, bandwidth); + writeWord(bytes, 0x20u, 2u); + writeWord(bytes, 0x24u, 0x451u); + writeWord(bytes, 0x28u, 0x5E0u); + writeWord(bytes, 0x2Cu, 10u); + writeWord(bytes, 0x40u, frameWord); + writeWord(bytes, 0xA8u, 0x803u); + writeWord(bytes, 0xACu, 0x451u); + writeWord(bytes, 0xB0u, 0x451u); + writeWord(bytes, 0xB4u, 1u); + writeWord(bytes, 0xB8u, 1u); + writeWord(bytes, 0xBCu, 0xFFFFFC18u); + writeWord(bytes, 0xC0u, bandwidth); + writeWord(bytes, 0xC4u, 0x10u); + writeWord(bytes, 0xC8u, frameWord); + writeWord(bytes, 0xCCu, 0x17B2u); +} + +constexpr EncoderCodes codes{0x813F0000u, 1u, 2u, 3u, 4u, 5u, 0x10u, 0x0Du, 0x11u, 0x16u, 0x19u, 728, 0xCCu, 0x00u, 0x14u, 0x10u, 0x17B2u, {1u, 2u, 4u, 8u, 16u, 24u}, 6, writeState}; OpusEncoderLibrary library("OpusCeltEnc", OPUS_APPLICATION_RESTRICTED_LOWDELAY, OPUS_APPLICATION_RESTRICTED_CELT, codes); diff --git a/core/libs/prx/libSceOpusCeltEnc/include/OpusEncoder.hpp b/core/libs/prx/libSceOpusCeltEnc/include/OpusEncoder.hpp index cbadf08658..dd87d8316c 100644 --- a/core/libs/prx/libSceOpusCeltEnc/include/OpusEncoder.hpp +++ b/core/libs/prx/libSceOpusCeltEnc/include/OpusEncoder.hpp @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -20,6 +21,10 @@ namespace { struct EncoderCodes { std::uint32_t baseError; std::uint32_t nullErrorOffset; + std::uint32_t nullSamplesErrorOffset; + std::uint32_t frameErrorOffset; + std::uint32_t nullDataErrorOffset; + std::uint32_t sizeErrorOffset; std::uint32_t rateErrorOffset; std::uint32_t channelsErrorOffset; std::uint32_t applicationErrorOffset; @@ -27,12 +32,18 @@ struct EncoderCodes { std::uint32_t initializeNullErrorOffset; std::size_t stateSize; std::size_t stateMagicOffset; + std::size_t stateInstanceOffset; + std::size_t stateRateOffset; + std::size_t stateChannelsOffset; std::uint16_t stateMagic; + std::uint8_t frameSteps[6]; + std::size_t frameStepCount; + void (*setupState)(void* state, std::uint32_t instanceId, int rate, int channels); }; class OpusEncoderLibrary { public: - OpusEncoderLibrary(const char* libraryName, int callerApplication, int codecApplication, const EncoderCodes& codes) : name(libraryName), requiredApplication(callerApplication), application(codecApplication), codes(codes), errorBase(codes.baseError) {} + OpusEncoderLibrary(const char* libraryName, int callerApplication, int codecApplication, const EncoderCodes& libraryCodes) : name(libraryName), requiredApplication(callerApplication), application(codecApplication), codes(libraryCodes), errorBase(libraryCodes.baseError) {} int Initialize(std::uint32_t* context) { if (!context) return error(codes.initializeNullErrorOffset); @@ -50,7 +61,7 @@ public: if (!context) fail("null context"); std::lock_guard lock(mutex); if (!contexts.contains(*context)) fail("unknown context"); - for (const auto& [state, encoder] : encoders) + for (const auto& [id, encoder] : encoders) if (encoder->context == *context) fail("termination with live encoders is not implemented"); contexts.erase(*context); return 0; @@ -63,52 +74,54 @@ public: int Create(void* state, int sampleRate, int channels, int createApplication) { if (!state) return error(codes.nullErrorOffset); - if (sampleRate != 8000 && sampleRate != 12000 && sampleRate != 16000 && sampleRate != 24000 && sampleRate != 48000) return error(codes.rateErrorOffset); + if (!acceptedRate(sampleRate)) return error(codes.rateErrorOffset); if (channels != 1 && channels != 2) return error(codes.channelsErrorOffset); if (createApplication != requiredApplication) return error(codes.applicationErrorOffset); std::lock_guard lock(mutex); - if (encoders.contains(state)) fail("state already owns an encoder"); Make(state, 0, sampleRate, channels); return 0; } int CreateEx(const std::uint32_t* context, void* state, int sampleRate, int channels, int createApplication) { if (!context || !state) return error(codes.nullErrorOffset); - if (sampleRate != 8000 && sampleRate != 12000 && sampleRate != 16000 && sampleRate != 24000 && sampleRate != 48000) return error(codes.rateErrorOffset); + if (!acceptedRate(sampleRate)) return error(codes.rateErrorOffset); if (channels != 1 && channels != 2) return error(codes.channelsErrorOffset); if (createApplication != requiredApplication) return error(codes.applicationErrorOffset); std::lock_guard lock(mutex); if (!contexts.contains(*context)) fail("unknown context"); - if (encoders.contains(state)) fail("state already owns an encoder"); Make(state, *context, sampleRate, channels); return 0; } int Encode(void* state, const std::int16_t* samples, int frameSize, std::uint8_t* data, int maxDataBytes) { - if (!state) return error(0); - if (frameSize != 0 && !samples) return error(6); - if (frameSize < 0) return error(7); - if (!data) return error(8); - if (maxDataBytes < 0) return error(9); + if (!state) return error(codes.nullErrorOffset); + if (!samples) return error(codes.nullSamplesErrorOffset); + if (frameSize == 0) return error(codes.frameErrorOffset); std::lock_guard lock(mutex); - const auto found = encoders.find(state); - if (found == encoders.end()) return error(1); - if (frameSize == 0) fail("a zero-frame encode is not implemented"); + if (!hasMarker(state)) return error(codes.destroyStateErrorOffset); + if (!data) return error(codes.nullDataErrorOffset); + if (maxDataBytes == 0) return error(codes.sizeErrorOffset); + if (!acceptedFrame(stateRate(state), frameSize)) return error(codes.frameErrorOffset); + const auto found = encoders.find(stateWord(state, codes.stateInstanceOffset)); + if (found == encoders.end()) fail("state does not reference a live encoder"); const int bytes = opus_encode(found->second->codec.get(), samples, frameSize, data, maxDataBytes); if (bytes < 0) fail("libopus rejected the frame"); return bytes; } int EncodeFloat(void* state, const float* samples, int frameSize, std::uint8_t* data, int maxDataBytes) { - if (!state) return error(0); - if (frameSize != 0 && !samples) return error(6); - if (frameSize < 0) return error(7); - if (!data) return error(8); - if (maxDataBytes < 0) return error(9); + if (!state) return error(codes.nullErrorOffset); + if (!samples) return error(codes.nullSamplesErrorOffset); + if (frameSize == 0) return error(codes.frameErrorOffset); std::lock_guard lock(mutex); - const auto found = encoders.find(state); - if (found == encoders.end()) return error(1); - if (frameSize == 0) fail("a zero-frame encode is not implemented"); + if (!hasMarker(state)) return error(codes.destroyStateErrorOffset); + if (frameSize < 0) std::abort(); + if (static_cast(frameSize) * stateWord(state, codes.stateChannelsOffset) * 4 > 0x800000) std::abort(); + if (!data) return error(codes.nullDataErrorOffset); + if (maxDataBytes == 0) return error(codes.sizeErrorOffset); + if (!acceptedFrame(stateRate(state), frameSize)) return error(codes.frameErrorOffset); + const auto found = encoders.find(stateWord(state, codes.stateInstanceOffset)); + if (found == encoders.end()) fail("state does not reference a live encoder"); const int bytes = opus_encode_float(found->second->codec.get(), samples, frameSize, data, maxDataBytes); if (bytes < 0) fail("libopus rejected the frame"); return bytes; @@ -120,7 +133,8 @@ public: std::memcpy(&magic, static_cast(state) + codes.stateMagicOffset, sizeof magic); if (magic != codes.stateMagic) return error(codes.destroyStateErrorOffset); std::lock_guard lock(mutex); - if (encoders.erase(state) == 0) return error(codes.destroyStateErrorOffset); + encoders.erase(stateWord(state, codes.stateInstanceOffset)); + setWord(state, codes.stateMagicOffset, 0); return 0; } @@ -133,17 +147,55 @@ private: }; void Make(void* state, std::uint32_t context, int sampleRate, int channels) { + const auto instanceId = nextInstanceId++; std::memset(state, 0, codes.stateSize); + codes.setupState(state, instanceId, sampleRate, channels); auto encoder = std::make_unique(); encoder->context = context; encoder->channels = channels; encoder->sampleRate = sampleRate; int opusError = OPUS_OK; encoder->codec.reset(opus_encoder_create(sampleRate, channels, application, &opusError)); - if (opusError == OPUS_ALLOC_FAIL) throw std::bad_alloc(); - if (!encoder->codec || opusError != OPUS_OK) fail("cannot create libopus encoder"); - std::memcpy(static_cast(state) + codes.stateMagicOffset, &codes.stateMagic, sizeof codes.stateMagic); - encoders.emplace(state, std::move(encoder)); + if (opusError == OPUS_ALLOC_FAIL) { + std::memset(state, 0, codes.stateSize); + throw std::bad_alloc(); + } + if (!encoder->codec || opusError != OPUS_OK) { + std::memset(state, 0, codes.stateSize); + fail("cannot create libopus encoder"); + } + encoders.emplace(instanceId, std::move(encoder)); + } + + static bool acceptedRate(int rate) { + return rate == 8000 || rate == 12000 || rate == 16000 || rate == 24000 || rate == 48000; + } + + bool acceptedFrame(int rate, int frameSize) const { + if (frameSize < 0 || rate <= 0) return false; + for (std::size_t index = 0; index < codes.frameStepCount; ++index) + if (frameSize == rate * codes.frameSteps[index] / 400) return true; + return false; + } + + std::uint32_t stateWord(const void* state, std::size_t offset) const { + std::uint32_t word = 0; + std::memcpy(&word, static_cast(state) + offset, sizeof word); + return word; + } + + void setWord(void* state, std::size_t offset, std::uint32_t value) const { + std::memcpy(static_cast(state) + offset, &value, sizeof value); + } + + int stateRate(void* state) const { + return static_cast(stateWord(state, codes.stateRateOffset)); + } + + bool hasMarker(void* state) const { + std::uint16_t magic = 0; + std::memcpy(&magic, static_cast(state) + codes.stateMagicOffset, sizeof magic); + return magic == codes.stateMagic; } const char* name; @@ -153,8 +205,9 @@ private: std::uint32_t errorBase; std::mutex mutex; std::unordered_map contexts; - std::unordered_map> encoders; + std::unordered_map> encoders; std::uint32_t nextContext = 1; + std::uint32_t nextInstanceId = 1; int error(std::uint32_t offset) const { return static_cast(errorBase + offset); diff --git a/core/libs/prx/libSceOpusSilkEnc/Export.cpp b/core/libs/prx/libSceOpusSilkEnc/Export.cpp index 08a4529138..213a7b1672 100644 --- a/core/libs/prx/libSceOpusSilkEnc/Export.cpp +++ b/core/libs/prx/libSceOpusSilkEnc/Export.cpp @@ -2,11 +2,48 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libSceOpusCeltEnc/include/OpusEncoder.hpp" #include +#include #include "prx/libc/include/General.hpp" namespace { -constexpr EncoderCodes codes{0x81770000u, 1u, 0x0Fu, 0x0Cu, 0x10u, 0x15u, 0x18u, 752, 0xE8u, 0x17B2u}; +std::uint32_t bitrate(int rate, int channels) { + if (rate == 8000) return channels == 1 ? 12000u : 19000u; + if (rate == 12000) return channels == 1 ? 15000u : 27000u; + if (rate == 16000) return channels == 1 ? 19000u : 35000u; + if (rate == 24000) return channels == 1 ? 27000u : 51000u; + return channels == 1 ? 51000u : 99000u; +} + +void writeWord(std::uint8_t* state, std::size_t offset, std::uint32_t value) { + std::memcpy(state + offset, &value, sizeof value); +} + +void writeState(void* state, std::uint32_t instanceId, int rate, int channels) { + auto* bytes = static_cast(state); + const auto rateWord = static_cast(rate); + const auto frameWord = static_cast(rate / 50); + const auto channelWord = static_cast(channels); + const auto bandwidth = bitrate(rate, channels); + writeWord(bytes, 0x00u, instanceId); + writeWord(bytes, 0x08u, 8u * channelWord); + writeWord(bytes, 0x10u, 0x800u); + writeWord(bytes, 0x14u, rateWord); + writeWord(bytes, 0x18u, channelWord); + writeWord(bytes, 0x1Cu, bandwidth); + writeWord(bytes, 0x28u, 0x44Fu); + writeWord(bytes, 0x2Cu, 0x44Fu); + writeWord(bytes, 0x30u, 0x5E0u); + writeWord(bytes, 0x34u, 10u); + writeWord(bytes, 0x48u, 0x3E8u); + writeWord(bytes, 0x58u, frameWord); + writeWord(bytes, 0xD8u, 0xFFFFFC18u); + writeWord(bytes, 0xDCu, bandwidth); + writeWord(bytes, 0xE0u, 0x10u); + writeWord(bytes, 0xE8u, 0x17B2u); +} + +constexpr EncoderCodes codes{0x81770000u, 1u, 2u, 3u, 4u, 5u, 0x0Fu, 0x0Cu, 0x10u, 0x15u, 0x18u, 752, 0xE8u, 0x00u, 0x14u, 0x18u, 0x17B2u, {4u, 8u, 16u, 24u, 0u, 0u}, 4, writeState}; OpusEncoderLibrary library("OpusSilkEnc", OPUS_APPLICATION_VOIP, OPUS_APPLICATION_RESTRICTED_SILK, codes); diff --git a/core/libs/tests/GuestOpusCeltEnc.cpp b/core/libs/tests/GuestOpusCeltEnc.cpp index f2fe99babd..705c4b089e 100644 --- a/core/libs/tests/GuestOpusCeltEnc.cpp +++ b/core/libs/tests/GuestOpusCeltEnc.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -30,6 +31,67 @@ OpusCeltEncoder* ToState(std::array& block) { return reinterpret_cast(block.data()); } +std::uint32_t Word(const std::uint8_t* data, std::size_t offset) { + std::uint32_t word = 0; + std::memcpy(&word, data + offset, sizeof word); + return word; +} + +struct SetupWord { + std::size_t offset; + std::uint32_t value; +}; + +void RequireState(const std::uint8_t* state, std::size_t stateSize, const SetupWord* words, std::size_t wordCount, std::uint8_t canary) { + std::array covered{}; + for (std::size_t index = 0; index < wordCount; ++index) { + Require(words[index].offset + 4 <= stateSize); + Require(Word(state, words[index].offset) == words[index].value); + for (std::size_t byte = 0; byte < 4; ++byte) covered[words[index].offset + byte] = true; + } + for (std::size_t index = 0; index < stateSize; ++index) + if (!covered[index]) Require(state[index] == 0); + Require(state[stateSize] == canary); +} + +std::uint32_t Bitrate(int rate, int channels) { + if (rate == 8000) return channels == 1 ? 11000u : 19000u; + if (rate == 12000) return channels == 1 ? 15000u : 27000u; + if (rate == 16000) return channels == 1 ? 19000u : 35000u; + if (rate == 24000) return channels == 1 ? 27000u : 51000u; + return channels == 1 ? 51000u : 99000u; +} + +std::array SetupWords(std::uint32_t instanceId, int rate, int channels) { + const auto bandwidth = Bitrate(rate, channels); + const auto rateWord = static_cast(rate); + const auto frameWord = static_cast(rate / 50); + const auto channelWord = static_cast(channels); + return {{ + {0x00, instanceId}, + {0x08, 8u * channelWord}, + {0x0C, 2u}, + {0x10, channelWord}, + {0x14, rateWord}, + {0x1C, bandwidth}, + {0x20, 2u}, + {0x24, 0x451u}, + {0x28, 0x5E0u}, + {0x2C, 10u}, + {0x40, frameWord}, + {0xA8, 0x803u}, + {0xAC, 0x451u}, + {0xB0, 0x451u}, + {0xB4, 1u}, + {0xB8, 1u}, + {0xBC, 0xFFFFFC18u}, + {0xC0, bandwidth}, + {0xC4, 0x10u}, + {0xC8, frameWord}, + {0xCC, 0x17B2u}, + }}; +} + void FillSine(std::int16_t* samples, int frameSize, int channels, int rate, int position) { for (int index = 0; index < frameSize; ++index) { const int value = static_cast(3000 * std::sin(2 * 3.14159265358979323846 * 2000 * (position + index) / rate)); @@ -58,8 +120,10 @@ void RequireTone(const std::int16_t* pcm, int samples, int channels, int rate) { int main() { constexpr std::uint32_t errorBase = 0x813F0000u; + constexpr std::size_t stateSize = 728; constexpr int callerApplication = OPUS_APPLICATION_RESTRICTED_LOWDELAY; constexpr int codecApplication = OPUS_APPLICATION_RESTRICTED_CELT; + std::uint32_t instanceId = 1; Require(sceOpusCeltEncInitialize(nullptr) == static_cast(errorBase + 0x19)); Require(sceOpusCeltEncDestroy(nullptr) == static_cast(errorBase + 0x01)); std::uint32_t context = 0; @@ -68,9 +132,9 @@ int main() { RequireThrows([&] { sceOpusCeltEncTerminate(nullptr); }); std::array invalidState{}; - std::array samples{}; + std::array samples{}; samples.fill(12345); - std::array floatSamples{}; + std::array floatSamples{}; std::uint8_t packet[1024]; for (auto& byte : packet) byte = 0x5A; Require(sceOpusCeltEncCreate(nullptr, 48000, 2, callerApplication) == static_cast(errorBase + 0x01)); @@ -82,31 +146,61 @@ int main() { Require(sceOpusCeltEncCreateEx(&context, ToState(invalidState), 0, 0, callerApplication) == static_cast(errorBase + 0x10)); Require(sceOpusCeltEncCreateEx(&context, ToState(invalidState), 48000, 0, callerApplication) == static_cast(errorBase + 0x0D)); Require(sceOpusCeltEncCreateEx(&context, ToState(invalidState), 48000, 2, OPUS_APPLICATION_VOIP) == static_cast(errorBase + 0x11)); - Require(sceOpusCeltEncEncode(nullptr, nullptr, -1, nullptr, -1) == static_cast(errorBase)); - Require(sceOpusCeltEncEncode(ToState(invalidState), nullptr, 960, nullptr, -1) == static_cast(errorBase + 6)); - Require(sceOpusCeltEncEncode(ToState(invalidState), samples.data(), -1, packet, -1) == static_cast(errorBase + 7)); - Require(sceOpusCeltEncEncode(ToState(invalidState), samples.data(), 960, nullptr, -1) == static_cast(errorBase + 8)); - Require(sceOpusCeltEncEncode(ToState(invalidState), samples.data(), 960, packet, -1) == static_cast(errorBase + 9)); - Require(sceOpusCeltEncEncode(ToState(invalidState), samples.data(), 960, packet, 8) == static_cast(errorBase + 1)); - Require(packet[0] == 0x5A && packet[1] == 0x5A); - Require(sceOpusCeltEncEncodeFloat(nullptr, nullptr, -1, nullptr, -1) == static_cast(errorBase)); - Require(sceOpusCeltEncEncodeFloat(ToState(invalidState), nullptr, 960, packet, 8) == static_cast(errorBase + 6)); - Require(sceOpusCeltEncEncodeFloat(ToState(invalidState), floatSamples.data(), -1, packet, 8) == static_cast(errorBase + 7)); - - Require(sceOpusCeltEncGetSize(1) == 728 && sceOpusCeltEncGetSize(2) == 728); + Require(sceOpusCeltEncGetSize(1) == 728); + Require(sceOpusCeltEncGetSize(2) == 728); Require(sceOpusCeltEncGetSize(3) == static_cast(errorBase + 0x0D)); + std::array state{}; + state.fill(0x5A); + Require(sceOpusCeltEncCreateEx(&context, ToState(state), 48000, 2, callerApplication) == 0); + const auto words = SetupWords(instanceId, 48000, 2); + RequireState(state.data(), stateSize, words.data(), words.size(), 0x5A); + ++instanceId; + Require(sceOpusCeltEncEncode(nullptr, samples.data(), 960, packet, sizeof(packet)) == static_cast(errorBase + 0x01)); + Require(sceOpusCeltEncEncode(ToState(state), nullptr, 960, packet, sizeof(packet)) == static_cast(errorBase + 0x02)); + Require(sceOpusCeltEncEncode(ToState(state), nullptr, 0, packet, sizeof(packet)) == static_cast(errorBase + 0x02)); + Require(sceOpusCeltEncEncode(ToState(state), samples.data(), 0, packet, sizeof(packet)) == static_cast(errorBase + 0x03)); + Require(sceOpusCeltEncEncode(ToState(state), samples.data(), -1, nullptr, sizeof(packet)) == static_cast(errorBase + 0x04)); + Require(sceOpusCeltEncEncode(ToState(state), samples.data(), -1, packet, 0) == static_cast(errorBase + 0x05)); + Require(sceOpusCeltEncEncode(ToState(state), samples.data(), -1, nullptr, 0) == static_cast(errorBase + 0x04)); + Require(sceOpusCeltEncEncode(ToState(state), samples.data(), 1, packet, sizeof(packet)) == static_cast(errorBase + 0x03)); + Require(sceOpusCeltEncEncode(ToState(state), samples.data(), 119, packet, sizeof(packet)) == static_cast(errorBase + 0x03)); + Require(sceOpusCeltEncEncode(ToState(state), samples.data(), 959, packet, sizeof(packet)) == static_cast(errorBase + 0x03)); + Require(sceOpusCeltEncEncode(ToState(state), samples.data(), 5760, packet, sizeof(packet)) == static_cast(errorBase + 0x03)); + Require(packet[0] == 0x5A && packet[1] == 0x5A); + Require(sceOpusCeltEncEncode(ToState(state), samples.data(), 120, packet, sizeof(packet)) > 2); + Require(sceOpusCeltEncEncode(ToState(state), samples.data(), 240, packet, sizeof(packet)) > 2); + RequireThrows([&] { sceOpusCeltEncEncode(ToState(state), samples.data(), 960, packet, -1); }); + Require(sceOpusCeltEncEncodeFloat(nullptr, floatSamples.data(), 960, packet, sizeof(packet)) == static_cast(errorBase + 0x01)); + Require(sceOpusCeltEncEncodeFloat(ToState(state), nullptr, 960, packet, sizeof(packet)) == static_cast(errorBase + 0x02)); + Require(sceOpusCeltEncEncodeFloat(ToState(state), floatSamples.data(), 0, packet, sizeof(packet)) == static_cast(errorBase + 0x03)); + Require(sceOpusCeltEncEncodeFloat(ToState(invalidState), floatSamples.data(), -1, packet, sizeof(packet)) == static_cast(errorBase + 0x16)); + Require(sceOpusCeltEncEncodeFloat(ToState(invalidState), nullptr, 960, packet, sizeof(packet)) == static_cast(errorBase + 0x02)); + + auto snapshot = state; + Require(sceOpusCeltEncDestroy(ToState(state)) == 0); + for (std::size_t index = 0; index < stateSize; ++index) + if (index < 0xCC || index >= 0xD0) Require(state[index] == snapshot[index]); + Require(Word(state.data(), 0xCC) == 0); + Require(sceOpusCeltEncDestroy(ToState(state)) == static_cast(errorBase + 0x16)); + Require(sceOpusCeltEncEncode(ToState(state), samples.data(), 960, packet, sizeof(packet)) == static_cast(errorBase + 0x16)); + Require(sceOpusCeltEncEncodeFloat(ToState(state), floatSamples.data(), 960, packet, sizeof(packet)) == static_cast(errorBase + 0x16)); + for (int rate : {8000, 12000, 16000, 24000, 48000}) { for (int channels : {1, 2}) { - std::array state{}; - Require(sceOpusCeltEncCreateEx(&context, ToState(state), rate, channels, callerApplication) == 0); + std::array matrixState{}; + matrixState.fill(0x5A); + Require(sceOpusCeltEncCreateEx(&context, ToState(matrixState), rate, channels, callerApplication) == 0); + const auto matrixWords = SetupWords(instanceId, rate, channels); + RequireState(matrixState.data(), stateSize, matrixWords.data(), matrixWords.size(), 0x5A); + ++instanceId; int opusError = OPUS_OK; auto* reference = opus_encoder_create(rate, channels, codecApplication, &opusError); Require(reference && opusError == OPUS_OK); const int frameSize = rate / 50; for (int frame = 0; frame < 4; ++frame) { FillSine(samples.data(), frameSize, channels, rate, frame * frameSize); - const int bytes = sceOpusCeltEncEncode(ToState(state), samples.data(), frameSize, packet, sizeof(packet)); + const int bytes = sceOpusCeltEncEncode(ToState(matrixState), samples.data(), frameSize, packet, sizeof(packet)); Require(bytes > 2 && static_cast(packet[0]) >> 3 >= 16); std::array expected{}; const int expectedBytes = opus_encode(reference, samples.data(), frameSize, expected.data(), sizeof(expected)); @@ -115,10 +209,10 @@ int main() { int decodeError = OPUS_OK; auto* decoder = opus_decoder_create(rate, channels, &decodeError); Require(decoder && decodeError == OPUS_OK); - std::array pcm{}; + std::array pcm{}; for (int frame = 0; frame < 4; ++frame) { FillSine(samples.data(), frameSize, channels, rate, frame * frameSize); - const int bytes = sceOpusCeltEncEncode(ToState(state), samples.data(), frameSize, packet, sizeof(packet)); + const int bytes = sceOpusCeltEncEncode(ToState(matrixState), samples.data(), frameSize, packet, sizeof(packet)); pcm.fill(12345); Require(opus_decode(decoder, packet, bytes, pcm.data() + 1, frameSize, 0) == frameSize); Require(pcm.front() == 12345 && pcm.back() == 12345); @@ -126,19 +220,24 @@ int main() { } opus_decoder_destroy(decoder); opus_encoder_destroy(reference); - Require(sceOpusCeltEncDestroy(ToState(state)) == 0); + Require(sceOpusCeltEncDestroy(ToState(matrixState)) == 0); } } Require(sceOpusCeltEncDestroy(ToState(invalidState)) == static_cast(errorBase + 0x16)); std::array floatState{}; + floatState.fill(0x5A); Require(sceOpusCeltEncCreateEx(&context, ToState(floatState), 48000, 1, callerApplication) == 0); + const auto floatWords = SetupWords(instanceId, 48000, 1); + RequireState(floatState.data(), stateSize, floatWords.data(), floatWords.size(), 0x5A); + ++instanceId; for (int index = 0; index < 960; ++index) floatSamples[static_cast(index)] = static_cast(0.2 * std::sin(2 * 3.14159265358979323846 * 2000 * index / 48000.0)); const int floatResult = sceOpusCeltEncEncodeFloat(ToState(floatState), floatSamples.data(), 960, packet, static_cast(sizeof(packet))); Require(floatResult > 2 && static_cast(packet[0]) >> 3 >= 16); int floatOpusError = OPUS_OK; - auto* floatReference = opus_encoder_create(48000, 1, codecApplication, &floatOpusError); Require(floatReference && floatOpusError == OPUS_OK); + auto* floatReference = opus_encoder_create(48000, 1, codecApplication, &floatOpusError); + Require(floatReference && floatOpusError == OPUS_OK); std::array floatExpected{}; const int floatExpectedBytes = opus_encode_float(floatReference, floatSamples.data(), 960, floatExpected.data(), sizeof(floatExpected)); Require(floatExpectedBytes == floatResult && std::equal(packet, packet + floatResult, floatExpected.begin())); @@ -147,10 +246,14 @@ int main() { std::array liveState{}; Require(sceOpusCeltEncCreateEx(&context, ToState(liveState), 48000, 2, callerApplication) == 0); + ++instanceId; Require(sceOpusCeltEncDestroy(ToState(liveState)) == 0); Require(sceOpusCeltEncTerminate(&context) == 0); RequireThrows([&] { sceOpusCeltEncTerminate(&context); }); RequireThrows([&] { sceOpusCeltEncCreateEx(&context, ToState(liveState), 48000, 2, callerApplication); }); + Require(sceOpusCeltEncCreate(ToState(liveState), 48000, 1, callerApplication) == 0); + ++instanceId; Require(sceOpusCeltEncCreate(ToState(liveState), 48000, 2, callerApplication) == 0); + ++instanceId; Require(sceOpusCeltEncDestroy(ToState(liveState)) == 0); } diff --git a/core/libs/tests/GuestOpusSilkEnc.cpp b/core/libs/tests/GuestOpusSilkEnc.cpp index 67e28b3819..c7b8a1aff0 100644 --- a/core/libs/tests/GuestOpusSilkEnc.cpp +++ b/core/libs/tests/GuestOpusSilkEnc.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -30,6 +31,62 @@ OpusSilkEncoder* ToState(std::array& block) { return reinterpret_cast(block.data()); } +std::uint32_t Word(const std::uint8_t* data, std::size_t offset) { + std::uint32_t word = 0; + std::memcpy(&word, data + offset, sizeof word); + return word; +} + +struct SetupWord { + std::size_t offset; + std::uint32_t value; +}; + +void RequireState(const std::uint8_t* state, std::size_t stateSize, const SetupWord* words, std::size_t wordCount, std::uint8_t canary) { + std::array covered{}; + for (std::size_t index = 0; index < wordCount; ++index) { + Require(words[index].offset + 4 <= stateSize); + Require(Word(state, words[index].offset) == words[index].value); + for (std::size_t byte = 0; byte < 4; ++byte) covered[words[index].offset + byte] = true; + } + for (std::size_t index = 0; index < stateSize; ++index) + if (!covered[index]) Require(state[index] == 0); + Require(state[stateSize] == canary); +} + +std::uint32_t Bitrate(int rate, int channels) { + if (rate == 8000) return channels == 1 ? 12000u : 19000u; + if (rate == 12000) return channels == 1 ? 15000u : 27000u; + if (rate == 16000) return channels == 1 ? 19000u : 35000u; + if (rate == 24000) return channels == 1 ? 27000u : 51000u; + return channels == 1 ? 51000u : 99000u; +} + +std::array SetupWords(std::uint32_t instanceId, int rate, int channels) { + const auto bandwidth = Bitrate(rate, channels); + const auto rateWord = static_cast(rate); + const auto frameWord = static_cast(rate / 50); + const auto channelWord = static_cast(channels); + return {{ + {0x00, instanceId}, + {0x08, 8u * channelWord}, + {0x10, 0x800u}, + {0x14, rateWord}, + {0x18, channelWord}, + {0x1C, bandwidth}, + {0x28, 0x44Fu}, + {0x2C, 0x44Fu}, + {0x30, 0x5E0u}, + {0x34, 10u}, + {0x48, 0x3E8u}, + {0x58, frameWord}, + {0xD8, 0xFFFFFC18u}, + {0xDC, bandwidth}, + {0xE0, 0x10u}, + {0xE8, 0x17B2u}, + }}; +} + void FillSine(std::int16_t* samples, int frameSize, int channels, int rate, int position) { for (int index = 0; index < frameSize; ++index) { const int value = static_cast(3000 * std::sin(2 * 3.14159265358979323846 * 2000 * (position + index) / rate)); @@ -58,8 +115,10 @@ void RequireTone(const std::int16_t* pcm, int samples, int channels, int rate) { int main() { constexpr std::uint32_t errorBase = 0x81770000u; + constexpr std::size_t stateSize = 752; constexpr int callerApplication = OPUS_APPLICATION_VOIP; constexpr int codecApplication = OPUS_APPLICATION_RESTRICTED_SILK; + std::uint32_t instanceId = 1; Require(sceOpusSilkEncInitialize(nullptr) == static_cast(errorBase + 0x18)); Require(sceOpusSilkEncDestroy(nullptr) == static_cast(errorBase + 0x01)); std::uint32_t context = 0; @@ -68,45 +127,75 @@ int main() { RequireThrows([&] { sceOpusSilkEncTerminate(nullptr); }); std::array invalidState{}; - std::array samples{}; + std::array samples{}; samples.fill(12345); - std::array floatSamples{}; + std::array floatSamples{}; std::uint8_t packet[1024]; for (auto& byte : packet) byte = 0x5A; Require(sceOpusSilkEncCreate(nullptr, 48000, 2, callerApplication) == static_cast(errorBase + 0x01)); Require(sceOpusSilkEncCreate(ToState(invalidState), 48000, 0, callerApplication) == static_cast(errorBase + 0x0C)); Require(sceOpusSilkEncCreate(ToState(invalidState), 0, 0, callerApplication) == static_cast(errorBase + 0x0F)); Require(sceOpusSilkEncCreate(ToState(invalidState), 22050, 2, callerApplication) == static_cast(errorBase + 0x0F)); - Require(sceOpusSilkEncCreate(ToState(invalidState), 48000, 2, OPUS_APPLICATION_RESTRICTED_SILK) == static_cast(errorBase + 0x10)); + Require(sceOpusSilkEncCreate(ToState(invalidState), 48000, 2, OPUS_APPLICATION_RESTRICTED_LOWDELAY) == static_cast(errorBase + 0x10)); Require(sceOpusSilkEncCreateEx(nullptr, ToState(invalidState), 0, 0, callerApplication) == static_cast(errorBase + 0x01)); Require(sceOpusSilkEncCreateEx(&context, ToState(invalidState), 0, 0, callerApplication) == static_cast(errorBase + 0x0F)); Require(sceOpusSilkEncCreateEx(&context, ToState(invalidState), 48000, 0, callerApplication) == static_cast(errorBase + 0x0C)); - Require(sceOpusSilkEncCreateEx(&context, ToState(invalidState), 48000, 2, OPUS_APPLICATION_RESTRICTED_SILK) == static_cast(errorBase + 0x10)); - Require(sceOpusSilkEncEncode(nullptr, nullptr, -1, nullptr, -1) == static_cast(errorBase)); - Require(sceOpusSilkEncEncode(ToState(invalidState), nullptr, 960, nullptr, -1) == static_cast(errorBase + 6)); - Require(sceOpusSilkEncEncode(ToState(invalidState), samples.data(), -1, packet, -1) == static_cast(errorBase + 7)); - Require(sceOpusSilkEncEncode(ToState(invalidState), samples.data(), 960, nullptr, -1) == static_cast(errorBase + 8)); - Require(sceOpusSilkEncEncode(ToState(invalidState), samples.data(), 960, packet, -1) == static_cast(errorBase + 9)); - Require(sceOpusSilkEncEncode(ToState(invalidState), samples.data(), 960, packet, 8) == static_cast(errorBase + 1)); - Require(packet[0] == 0x5A && packet[1] == 0x5A); - Require(sceOpusSilkEncEncodeFloat(nullptr, nullptr, -1, nullptr, -1) == static_cast(errorBase)); - Require(sceOpusSilkEncEncodeFloat(ToState(invalidState), nullptr, 960, packet, 8) == static_cast(errorBase + 6)); - Require(sceOpusSilkEncEncodeFloat(ToState(invalidState), floatSamples.data(), -1, packet, 8) == static_cast(errorBase + 7)); - - Require(sceOpusSilkEncGetSize(1) == 752 && sceOpusSilkEncGetSize(2) == 752); + Require(sceOpusSilkEncCreateEx(&context, ToState(invalidState), 48000, 2, OPUS_APPLICATION_RESTRICTED_LOWDELAY) == static_cast(errorBase + 0x10)); + Require(sceOpusSilkEncGetSize(1) == 752); + Require(sceOpusSilkEncGetSize(2) == 752); Require(sceOpusSilkEncGetSize(3) == static_cast(errorBase + 0x0C)); + std::array state{}; + state.fill(0x5A); + Require(sceOpusSilkEncCreateEx(&context, ToState(state), 48000, 2, callerApplication) == 0); + const auto words = SetupWords(instanceId, 48000, 2); + RequireState(state.data(), stateSize, words.data(), words.size(), 0x5A); + ++instanceId; + Require(sceOpusSilkEncEncode(nullptr, samples.data(), 960, packet, sizeof(packet)) == static_cast(errorBase + 0x01)); + Require(sceOpusSilkEncEncode(ToState(state), nullptr, 960, packet, sizeof(packet)) == static_cast(errorBase + 0x02)); + Require(sceOpusSilkEncEncode(ToState(state), nullptr, 0, packet, sizeof(packet)) == static_cast(errorBase + 0x02)); + Require(sceOpusSilkEncEncode(ToState(state), samples.data(), 0, packet, sizeof(packet)) == static_cast(errorBase + 0x03)); + Require(sceOpusSilkEncEncode(ToState(state), samples.data(), -1, nullptr, sizeof(packet)) == static_cast(errorBase + 0x04)); + Require(sceOpusSilkEncEncode(ToState(state), samples.data(), -1, packet, 0) == static_cast(errorBase + 0x05)); + Require(sceOpusSilkEncEncode(ToState(state), samples.data(), -1, nullptr, 0) == static_cast(errorBase + 0x04)); + Require(sceOpusSilkEncEncode(ToState(state), samples.data(), 1, packet, sizeof(packet)) == static_cast(errorBase + 0x03)); + Require(sceOpusSilkEncEncode(ToState(state), samples.data(), 119, packet, sizeof(packet)) == static_cast(errorBase + 0x03)); + Require(sceOpusSilkEncEncode(ToState(state), samples.data(), 959, packet, sizeof(packet)) == static_cast(errorBase + 0x03)); + Require(sceOpusSilkEncEncode(ToState(state), samples.data(), 5760, packet, sizeof(packet)) == static_cast(errorBase + 0x03)); + Require(sceOpusSilkEncEncode(ToState(state), samples.data(), 120, packet, sizeof(packet)) == static_cast(errorBase + 0x03)); + Require(sceOpusSilkEncEncode(ToState(state), samples.data(), 240, packet, sizeof(packet)) == static_cast(errorBase + 0x03)); + Require(packet[0] == 0x5A && packet[1] == 0x5A); + RequireThrows([&] { sceOpusSilkEncEncode(ToState(state), samples.data(), 960, packet, -1); }); + Require(sceOpusSilkEncEncodeFloat(nullptr, floatSamples.data(), 960, packet, sizeof(packet)) == static_cast(errorBase + 0x01)); + Require(sceOpusSilkEncEncodeFloat(ToState(state), nullptr, 960, packet, sizeof(packet)) == static_cast(errorBase + 0x02)); + Require(sceOpusSilkEncEncodeFloat(ToState(state), floatSamples.data(), 0, packet, sizeof(packet)) == static_cast(errorBase + 0x03)); + Require(sceOpusSilkEncEncodeFloat(ToState(invalidState), floatSamples.data(), -1, packet, sizeof(packet)) == static_cast(errorBase + 0x15)); + Require(sceOpusSilkEncEncodeFloat(ToState(invalidState), nullptr, 960, packet, sizeof(packet)) == static_cast(errorBase + 0x02)); + + auto snapshot = state; + Require(sceOpusSilkEncDestroy(ToState(state)) == 0); + for (std::size_t index = 0; index < stateSize; ++index) + if (index < 0xE8 || index >= 0xEC) Require(state[index] == snapshot[index]); + Require(Word(state.data(), 0xE8) == 0); + Require(sceOpusSilkEncDestroy(ToState(state)) == static_cast(errorBase + 0x15)); + Require(sceOpusSilkEncEncode(ToState(state), samples.data(), 960, packet, sizeof(packet)) == static_cast(errorBase + 0x15)); + Require(sceOpusSilkEncEncodeFloat(ToState(state), floatSamples.data(), 960, packet, sizeof(packet)) == static_cast(errorBase + 0x15)); + for (int rate : {8000, 12000, 16000, 24000, 48000}) { for (int channels : {1, 2}) { - std::array state{}; - Require(sceOpusSilkEncCreateEx(&context, ToState(state), rate, channels, callerApplication) == 0); + std::array matrixState{}; + matrixState.fill(0x5A); + Require(sceOpusSilkEncCreateEx(&context, ToState(matrixState), rate, channels, callerApplication) == 0); + const auto matrixWords = SetupWords(instanceId, rate, channels); + RequireState(matrixState.data(), stateSize, matrixWords.data(), matrixWords.size(), 0x5A); + ++instanceId; int opusError = OPUS_OK; auto* reference = opus_encoder_create(rate, channels, codecApplication, &opusError); Require(reference && opusError == OPUS_OK); const int frameSize = rate / 50; for (int frame = 0; frame < 4; ++frame) { FillSine(samples.data(), frameSize, channels, rate, frame * frameSize); - const int bytes = sceOpusSilkEncEncode(ToState(state), samples.data(), frameSize, packet, sizeof(packet)); + const int bytes = sceOpusSilkEncEncode(ToState(matrixState), samples.data(), frameSize, packet, sizeof(packet)); Require(bytes > 2 && static_cast(packet[0]) >> 3 < 12); std::array expected{}; const int expectedBytes = opus_encode(reference, samples.data(), frameSize, expected.data(), sizeof(expected)); @@ -115,10 +204,10 @@ int main() { int decodeError = OPUS_OK; auto* decoder = opus_decoder_create(rate, channels, &decodeError); Require(decoder && decodeError == OPUS_OK); - std::array pcm{}; + std::array pcm{}; for (int frame = 0; frame < 4; ++frame) { FillSine(samples.data(), frameSize, channels, rate, frame * frameSize); - const int bytes = sceOpusSilkEncEncode(ToState(state), samples.data(), frameSize, packet, sizeof(packet)); + const int bytes = sceOpusSilkEncEncode(ToState(matrixState), samples.data(), frameSize, packet, sizeof(packet)); pcm.fill(12345); Require(opus_decode(decoder, packet, bytes, pcm.data() + 1, frameSize, 0) == frameSize); Require(pcm.front() == 12345 && pcm.back() == 12345); @@ -126,13 +215,17 @@ int main() { } opus_decoder_destroy(decoder); opus_encoder_destroy(reference); - Require(sceOpusSilkEncDestroy(ToState(state)) == 0); + Require(sceOpusSilkEncDestroy(ToState(matrixState)) == 0); } } Require(sceOpusSilkEncDestroy(ToState(invalidState)) == static_cast(errorBase + 0x15)); std::array floatState{}; + floatState.fill(0x5A); Require(sceOpusSilkEncCreateEx(&context, ToState(floatState), 48000, 1, callerApplication) == 0); + const auto floatWords = SetupWords(instanceId, 48000, 1); + RequireState(floatState.data(), stateSize, floatWords.data(), floatWords.size(), 0x5A); + ++instanceId; for (int index = 0; index < 960; ++index) floatSamples[static_cast(index)] = static_cast(0.2 * std::sin(2 * 3.14159265358979323846 * 2000 * index / 48000.0)); const int floatResult = sceOpusSilkEncEncodeFloat(ToState(floatState), floatSamples.data(), 960, packet, static_cast(sizeof(packet))); @@ -148,10 +241,14 @@ int main() { std::array liveState{}; Require(sceOpusSilkEncCreateEx(&context, ToState(liveState), 48000, 2, callerApplication) == 0); + ++instanceId; Require(sceOpusSilkEncDestroy(ToState(liveState)) == 0); Require(sceOpusSilkEncTerminate(&context) == 0); RequireThrows([&] { sceOpusSilkEncTerminate(&context); }); RequireThrows([&] { sceOpusSilkEncCreateEx(&context, ToState(liveState), 48000, 2, callerApplication); }); + Require(sceOpusSilkEncCreate(ToState(liveState), 48000, 1, callerApplication) == 0); + ++instanceId; Require(sceOpusSilkEncCreate(ToState(liveState), 48000, 2, callerApplication) == 0); + ++instanceId; Require(sceOpusSilkEncDestroy(ToState(liveState)) == 0); } diff --git a/docs/dev/debt/libSceOpusCeltEnc.md b/docs/dev/debt/libSceOpusCeltEnc.md index 2054453e74..4ffce420f9 100644 --- a/docs/dev/debt/libSceOpusCeltEnc.md +++ b/docs/dev/debt/libSceOpusCeltEnc.md @@ -6,5 +6,12 @@ ### Functional -- [libSceOpusCeltEnc](../../../core/libs/prx/libSceOpusCeltEnc/Export.cpp): the state layout behind [GetSize](../../../core/libs/prx/libSceOpusCeltEnc/Export.cpp) is only partially recovered from the [firmware 13.42 export measurements](https://github.com/boykopovar/AnyPS5/pull/10563#issuecomment-6401539314). A successful encode setup writes the 728-byte zero-fill and the 0x17B2 marker at +0xCC into the state; the console's remaining setup bytes and whether its packets equal ours stay unknown. The console requires application 2051 (`OPUS_APPLICATION_RESTRICTED_LOWDELAY`) and the library builds its encoder with the restricted celt application instead; that mapping is this implementation's choice, not recovered from the setup -- [Create, CreateEx, Destroy, GetSize and Initialize](../../../core/libs/prx/libSceOpusCeltEnc/Export.cpp) refusal codes follow the same measurement, base 0x813F0000 with the offsets +0x01, +0x10, +0x0D and +0x11 for null state or context, sample rate, channels and application, +0x16 for a wrong state marker in `Destroy` and +0x19 for `Initialize(nullptr)`; other firmware versions remain unverified. [Encode and EncodeFloat](../../../core/libs/prx/libSceOpusCeltEnc/Export.cpp) still validate with the offsets 0, 1, 6, 7, 8 and 9 that the recovered libSceOpusCeltDec validation suggests; the measurement did not cover them. `Terminate` throwing for live `CreateEx`, a zero-frame encode, a libopus rejection and `OPUS_ALLOC_FAIL` also throw as that analogue does. Closes when: a supplied PS5 module settles the state layout, the `Encode` validation and the console's packet equivalence +- [libSceOpusCeltEnc](../../../core/libs/prx/libSceOpusCeltEnc/Export.cpp) is an AJM frontend on the console: apart from libc runtime support and `sceKernelUsleep` the module imports only `sceAjmi*` calls, Create runs `sceAjmiInstanceCreate` with codec 15 and the flags 0x200000009 mono or 0x200000011 stereo, `sceAjmiBatchJobControlBufferRa` with request 0x6001 over state +0x10 with size 0x38 and results at state +0xA0, then `sceAjmiBatchStartBuffer` and `sceAjmiBatchWait`, Encode hands the samples to `sceAjmiBatchJobRunBufferRa`, and Destroy calls `sceAjmiInstanceDestroy` [per module runs with stubbed AJM](https://github.com/boykopovar/AnyPS5/pull/10563#issuecomment-6412138282). Our encoders build libopus without AJM, so packets come from libopus and whether they equal the console's is unknown. Closes when: encoder packets are compared against console-produced ones or the library runs through our AJM + +- [Create and CreateEx](../../../core/libs/prx/libSceOpusCeltEnc/Export.cpp) zero-fill the 728-byte state and write the word table [measured on the modules](https://github.com/boykopovar/AnyPS5/pull/10563#issuecomment-6412138282): the instance id at +0x00, this implementation's id because our AJM does not run, 8 × channels at +0x08, channels at +0x10, rate at +0x14 and rate/50 at +0x40 and +0xC8, the bitrate at +0x1C and +0xC0 with the per-rate per-channel table 11000/19000 to 51000/99000, the constants 2, 0x451, 0x5E0, 10, 0x803, 1, 0xFFFFFC18 and 0x10, and the 0x17B2 marker at +0xCC. The console's live AJM may write further result bytes into +0xA0 and +0xD8. Closes when: a console run with live AJM settles those result bytes + +- [Encode and EncodeFloat](../../../core/libs/prx/libSceOpusCeltEnc/Export.cpp) refuse with the [measured codes](https://github.com/boykopovar/AnyPS5/pull/10563#issuecomment-6412138282): null state +0x01, null samples +0x02 before the state marker, a markerless state +0x16, null data +0x04 before the frame check and maxDataBytes 0 +0x05; maxDataBytes below zero is not refused and reaches our libopus backend, where a rejection throws. Frame sizes outside the durations 2.5, 5, 10, 20, 40 and 60 ms, the measured 120 to 2880 at 48000, give +0x03; we scale the counts by rate while the module's rule for other rates is unmeasured. Closes when: the frame rule and the console outcome for a buffer AJM rejects are measured + +- [sceOpusCeltEncEncodeFloat](../../../core/libs/prx/libSceOpusCeltEnc/Export.cpp) checks null state, null samples, frame 0 and the marker, then reserves the converted frame on the stack before its remaining checks; the frame × channels × 4 byte reservation kills the process for the [measured frame -1](https://github.com/boykopovar/AnyPS5/pull/10563#issuecomment-6412138282), and our implementation aborts above 8 MiB. The threshold between the measured deaths and the accepted frames is our host stack model, not a console measurement. Closes when: the console's stack behaviour for such frames is measured + +- [Create, CreateEx, Destroy, GetSize, Terminate and Initialize](../../../core/libs/prx/libSceOpusCeltEnc/Export.cpp) refuse with base 0x813F0000, the null pointer +0x01, the sample rate +0x10, channels +0x0D, application +0x11, the marker in Destroy +0x16 and `Initialize(nullptr)` +0x19, all from the [firmware 13.42 export measurements](https://github.com/boykopovar/AnyPS5/pull/10563#issuecomment-6401539314); Destroy clears the marker and leaves the other bytes, GetSize returns 728 on channels 1 and 2 and refuses other counts with +0x0D. CreateEx context handling, duplicate initialization and termination with live encoders throw as the recovered decoder analogue does. Closes when: those paths are measured on the console diff --git a/docs/dev/debt/libSceOpusSilkEnc.md b/docs/dev/debt/libSceOpusSilkEnc.md index ac02155546..7d892eadcd 100644 --- a/docs/dev/debt/libSceOpusSilkEnc.md +++ b/docs/dev/debt/libSceOpusSilkEnc.md @@ -2,9 +2,16 @@ ### Unknown function info -- [sceOpusSilkEncCancel, sceOpusSilkEncCtl, sceOpusSilkEncEncodeFloatWithPriority, sceOpusSilkEncEncodeFloatWithPriorityAndTimeout, sceOpusSilkEncEncodeWithPriority and sceOpusSilkEncEncodeWithPriorityAndTimeout](../../../core/libs/prx/libSceOpusSilkEnc/Export.cpp) (libSceOpusSilkEnc) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceOpusSilkEnc.c). The prototypes mirror the sibling encoder declarations and libopus's own ctl and priority entry points, but the console's request codes, priority, timeout and batch-id semantics were not recovered. They throw through `NotImplemented_nid_no_patch`; this is not an ABI claim. +- [sceOpusSilkEncCancel, sceOpusSilkEncCtl, sceOpusSilkEncEncodeFloatWithPriority, sceOpusSilkEncEncodeFloatWithPriorityAndTimeout, sceOpusSilkEncEncodeWithPriority and sceOpusSilkEncEncodeWithPriorityAndTimeout](../../../core/libs/prx/libSceOpusSilkEnc/Export.cpp) (libSceOpusSilkEnc) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceOpusSilkEnc.c). The prototypes mirror the sibling encoder declarations and libopus's own ctl, priority and batch-id entry points, but the console's request codes, priority, timeout and batch semantics were not recovered. They throw through `NotImplemented_nid_no_patch`; this is not an ABI claim. ### Functional -- [libSceOpusSilkEnc](../../../core/libs/prx/libSceOpusSilkEnc/Export.cpp): the state layout behind [GetSize](../../../core/libs/prx/libSceOpusSilkEnc/Export.cpp) is only partially recovered from the [firmware 13.42 export measurements](https://github.com/boykopovar/AnyPS5/pull/10563#issuecomment-6401539314). A successful encode setup writes the 752-byte zero-fill and the 0x17B2 marker at +0xE8 into the state; the console's remaining setup bytes and whether its packets equal ours stay unknown. The console requires application 2048 (`OPUS_APPLICATION_VOIP`) and the library builds its encoder with the restricted silk application instead; that mapping is this implementation's choice, not recovered from the setup -- [Create, CreateEx, Destroy, GetSize and Initialize](../../../core/libs/prx/libSceOpusSilkEnc/Export.cpp) refusal codes follow the same measurement, base 0x81770000 with the offsets +0x01, +0x0F, +0x0C and +0x10 for null state or context, sample rate, channels and application, +0x15 for a wrong state marker in `Destroy` and +0x18 for `Initialize(nullptr)`; other firmware versions remain unverified. [Encode and EncodeFloat](../../../core/libs/prx/libSceOpusSilkEnc/Export.cpp) still validate with the offsets 0, 1, 6, 7, 8 and 9 that the recovered libSceOpusCeltDec validation suggests; the measurement did not cover them. `Terminate` throwing for live `CreateEx`, a zero-frame encode, a libopus rejection and `OPUS_ALLOC_FAIL` also throw as that analogue does. Closes when: a supplied PS5 module settles the state layout, the `Encode` validation and the console's packet equivalence +- [libSceOpusSilkEnc](../../../core/libs/prx/libSceOpusSilkEnc/Export.cpp) is an AJM frontend on the console: apart from libc runtime support and `sceKernelUsleep` the module imports only `sceAjmi*` calls, Create runs `sceAjmiInstanceCreate` with codec 17 and the flags 0x9 mono or 0x11 stereo, `sceAjmiBatchJobControlBufferRa` with request 0x6001 over state +0x10 with size 0x4C and results at state +0xD0, then `sceAjmiBatchStartBuffer` and `sceAjmiBatchWait`, Encode hands the samples to `sceAjmiBatchJobRunBufferRa`, and Destroy calls `sceAjmiInstanceDestroy` [per module runs with stubbed AJM](https://github.com/boykopovar/AnyPS5/pull/10563#issuecomment-6412138282). Our encoders build libopus without AJM, so packets come from libopus and whether they equal the console's is unknown. Closes when: encoder packets are compared against console-produced ones or the library runs through our AJM + +- [Create and CreateEx](../../../core/libs/prx/libSceOpusSilkEnc/Export.cpp) zero-fill the 752-byte state and write the word table [measured on the modules](https://github.com/boykopovar/AnyPS5/pull/10563#issuecomment-6412138282): the instance id at +0x00, this implementation's id because our AJM does not run, 8 × channels at +0x08, 0x800 at +0x10, rate at +0x14, channels at +0x18 and rate/50 at +0x58 and +0xE4, the bitrate at +0x1C and +0xDC with the per-rate per-channel table 12000/19000 to 51000/99000, the constants 0x44F, 0x5E0, 10, 0x3E8, 0xFFFFFC18 and 0x10, and the 0x17B2 marker at +0xE8. The console's live AJM may write further result bytes into +0xD0 and +0x11C. Closes when: a console run with live AJM settles those result bytes + +- [Encode and EncodeFloat](../../../core/libs/prx/libSceOpusSilkEnc/Export.cpp) refuse with the [measured codes](https://github.com/boykopovar/AnyPS5/pull/10563#issuecomment-6412138282): null state +0x01, null samples +0x02 before the state marker, a markerless state +0x15, null data +0x04 before the frame check and maxDataBytes 0 +0x05; maxDataBytes below zero is not refused and reaches our libopus backend, where a rejection throws. Frame sizes outside the durations 10, 20, 40 and 60 ms, the measured 480 to 2880 at 48000 with 120 and 240 refused, give +0x03; we scale the counts by rate while the module's rule for other rates is unmeasured. Closes when: the frame rule and the console outcome for a buffer AJM rejects are measured + +- [sceOpusSilkEncEncodeFloat](../../../core/libs/prx/libSceOpusSilkEnc/Export.cpp) shares the CELT module's check order: null state, null samples, frame 0 and the marker, then the frame × channels × 4 byte stack reservation for the converted frame before its remaining checks, which kills the process for negative frames [per the measured CELT run](https://github.com/boykopovar/AnyPS5/pull/10563#issuecomment-6412138282); our implementation aborts above 8 MiB. The threshold between the measured deaths and the accepted frames is our host stack model, not a console measurement. Closes when: the console's stack behaviour for such frames is measured + +- [Create, CreateEx, Destroy, GetSize, Terminate and Initialize](../../../core/libs/prx/libSceOpusSilkEnc/Export.cpp) refuse with base 0x81770000, the null pointer +0x01, the sample rate +0x0F, channels +0x0C, application +0x10, the marker in Destroy +0x15 and `Initialize(nullptr)` +0x18, all from the [firmware 13.42 export measurements](https://github.com/boykopovar/AnyPS5/pull/10563#issuecomment-6401539314); Destroy clears the marker and leaves the other bytes, GetSize returns 752 on channels 1 and 2 and refuses other counts with +0x0C. CreateEx context handling, duplicate initialization and termination with live encoders throw as the recovered decoder analogue does. Closes when: those paths are measured on the console From 8529647f017ae14a2f2e80773bdbc8bde33e3245 Mon Sep 17 00:00:00 2001 From: Bipin Date: Mon, 26 Oct 2026 15:33:03 +0530 Subject: [PATCH 102/198] fix(core): complete the silk layout and retire replaced encoders The SILK setup writes rate/50 at +0xE4 next to +0x58 and the state test asserts it, and a create over a marked state replaces the entry its instance id names so the surviving Destroy frees the codec and the following Terminate runs clean. EncodeFloat reports the reservation that cannot fit with a throw at the checks where the console module dies after the marker and frame 0, with the 8 MiB bound recorded as our host stack model, and the tests cover the negative frame and the bound setup. --- .../libSceOpusCeltEnc/include/OpusEncoder.hpp | 8 +++++--- core/libs/prx/libSceOpusSilkEnc/Export.cpp | 1 + core/libs/tests/GuestOpusCeltEnc.cpp | 13 +++++++++++++ core/libs/tests/GuestOpusSilkEnc.cpp | 16 +++++++++++++++- docs/dev/debt/libSceOpusCeltEnc.md | 2 +- docs/dev/debt/libSceOpusSilkEnc.md | 2 +- 6 files changed, 36 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceOpusCeltEnc/include/OpusEncoder.hpp b/core/libs/prx/libSceOpusCeltEnc/include/OpusEncoder.hpp index dd87d8316c..b3d1b9040a 100644 --- a/core/libs/prx/libSceOpusCeltEnc/include/OpusEncoder.hpp +++ b/core/libs/prx/libSceOpusCeltEnc/include/OpusEncoder.hpp @@ -3,11 +3,11 @@ #include #include -#include #include #include #include #include +#include #include #include #include @@ -115,8 +115,8 @@ public: if (frameSize == 0) return error(codes.frameErrorOffset); std::lock_guard lock(mutex); if (!hasMarker(state)) return error(codes.destroyStateErrorOffset); - if (frameSize < 0) std::abort(); - if (static_cast(frameSize) * stateWord(state, codes.stateChannelsOffset) * 4 > 0x800000) std::abort(); + if (frameSize < 0) fail("the converted frame reservation does not fit the stack"); + if (static_cast(frameSize) * stateWord(state, codes.stateChannelsOffset) * 4 > 0x800000) fail("the converted frame reservation does not fit the stack"); if (!data) return error(codes.nullDataErrorOffset); if (maxDataBytes == 0) return error(codes.sizeErrorOffset); if (!acceptedFrame(stateRate(state), frameSize)) return error(codes.frameErrorOffset); @@ -147,6 +147,7 @@ private: }; void Make(void* state, std::uint32_t context, int sampleRate, int channels) { + const auto retired = hasMarker(state) ? std::optional{stateWord(state, codes.stateInstanceOffset)} : std::nullopt; const auto instanceId = nextInstanceId++; std::memset(state, 0, codes.stateSize); codes.setupState(state, instanceId, sampleRate, channels); @@ -164,6 +165,7 @@ private: std::memset(state, 0, codes.stateSize); fail("cannot create libopus encoder"); } + if (retired) encoders.erase(*retired); encoders.emplace(instanceId, std::move(encoder)); } diff --git a/core/libs/prx/libSceOpusSilkEnc/Export.cpp b/core/libs/prx/libSceOpusSilkEnc/Export.cpp index 213a7b1672..826f776fd1 100644 --- a/core/libs/prx/libSceOpusSilkEnc/Export.cpp +++ b/core/libs/prx/libSceOpusSilkEnc/Export.cpp @@ -37,6 +37,7 @@ void writeState(void* state, std::uint32_t instanceId, int rate, int channels) { writeWord(bytes, 0x34u, 10u); writeWord(bytes, 0x48u, 0x3E8u); writeWord(bytes, 0x58u, frameWord); + writeWord(bytes, 0xE4u, frameWord); writeWord(bytes, 0xD8u, 0xFFFFFC18u); writeWord(bytes, 0xDCu, bandwidth); writeWord(bytes, 0xE0u, 0x10u); diff --git a/core/libs/tests/GuestOpusCeltEnc.cpp b/core/libs/tests/GuestOpusCeltEnc.cpp index 705c4b089e..99fae83ff6 100644 --- a/core/libs/tests/GuestOpusCeltEnc.cpp +++ b/core/libs/tests/GuestOpusCeltEnc.cpp @@ -176,6 +176,8 @@ int main() { Require(sceOpusCeltEncEncodeFloat(ToState(state), floatSamples.data(), 0, packet, sizeof(packet)) == static_cast(errorBase + 0x03)); Require(sceOpusCeltEncEncodeFloat(ToState(invalidState), floatSamples.data(), -1, packet, sizeof(packet)) == static_cast(errorBase + 0x16)); Require(sceOpusCeltEncEncodeFloat(ToState(invalidState), nullptr, 960, packet, sizeof(packet)) == static_cast(errorBase + 0x02)); + RequireThrows([&] { sceOpusCeltEncEncodeFloat(ToState(state), floatSamples.data(), -1, packet, sizeof(packet)); }); + RequireThrows([&] { sceOpusCeltEncEncodeFloat(ToState(state), floatSamples.data(), 1100000, packet, sizeof(packet)); }); auto snapshot = state; Require(sceOpusCeltEncDestroy(ToState(state)) == 0); @@ -225,6 +227,17 @@ int main() { } Require(sceOpusCeltEncDestroy(ToState(invalidState)) == static_cast(errorBase + 0x16)); + std::array retireState{}; + retireState.fill(0x5A); + Require(sceOpusCeltEncCreateEx(&context, ToState(retireState), 48000, 1, callerApplication) == 0); + ++instanceId; + Require(sceOpusCeltEncCreateEx(&context, ToState(retireState), 48000, 2, callerApplication) == 0); + const auto retireWords = SetupWords(instanceId, 48000, 2); + RequireState(retireState.data(), stateSize, retireWords.data(), retireWords.size(), 0x5A); + ++instanceId; + Require(sceOpusCeltEncEncode(ToState(retireState), samples.data(), 960, packet, sizeof(packet)) > 2); + Require(sceOpusCeltEncDestroy(ToState(retireState)) == 0); + std::array floatState{}; floatState.fill(0x5A); Require(sceOpusCeltEncCreateEx(&context, ToState(floatState), 48000, 1, callerApplication) == 0); diff --git a/core/libs/tests/GuestOpusSilkEnc.cpp b/core/libs/tests/GuestOpusSilkEnc.cpp index c7b8a1aff0..b1cd42762d 100644 --- a/core/libs/tests/GuestOpusSilkEnc.cpp +++ b/core/libs/tests/GuestOpusSilkEnc.cpp @@ -62,7 +62,7 @@ std::uint32_t Bitrate(int rate, int channels) { return channels == 1 ? 51000u : 99000u; } -std::array SetupWords(std::uint32_t instanceId, int rate, int channels) { +std::array SetupWords(std::uint32_t instanceId, int rate, int channels) { const auto bandwidth = Bitrate(rate, channels); const auto rateWord = static_cast(rate); const auto frameWord = static_cast(rate / 50); @@ -80,6 +80,7 @@ std::array SetupWords(std::uint32_t instanceId, int rate, int cha {0x34, 10u}, {0x48, 0x3E8u}, {0x58, frameWord}, + {0xE4, frameWord}, {0xD8, 0xFFFFFC18u}, {0xDC, bandwidth}, {0xE0, 0x10u}, @@ -171,6 +172,8 @@ int main() { Require(sceOpusSilkEncEncodeFloat(ToState(state), floatSamples.data(), 0, packet, sizeof(packet)) == static_cast(errorBase + 0x03)); Require(sceOpusSilkEncEncodeFloat(ToState(invalidState), floatSamples.data(), -1, packet, sizeof(packet)) == static_cast(errorBase + 0x15)); Require(sceOpusSilkEncEncodeFloat(ToState(invalidState), nullptr, 960, packet, sizeof(packet)) == static_cast(errorBase + 0x02)); + RequireThrows([&] { sceOpusSilkEncEncodeFloat(ToState(state), floatSamples.data(), -1, packet, sizeof(packet)); }); + RequireThrows([&] { sceOpusSilkEncEncodeFloat(ToState(state), floatSamples.data(), 1100000, packet, sizeof(packet)); }); auto snapshot = state; Require(sceOpusSilkEncDestroy(ToState(state)) == 0); @@ -220,6 +223,17 @@ int main() { } Require(sceOpusSilkEncDestroy(ToState(invalidState)) == static_cast(errorBase + 0x15)); + std::array retireState{}; + retireState.fill(0x5A); + Require(sceOpusSilkEncCreateEx(&context, ToState(retireState), 48000, 1, callerApplication) == 0); + ++instanceId; + Require(sceOpusSilkEncCreateEx(&context, ToState(retireState), 48000, 2, callerApplication) == 0); + const auto retireWords = SetupWords(instanceId, 48000, 2); + RequireState(retireState.data(), stateSize, retireWords.data(), retireWords.size(), 0x5A); + ++instanceId; + Require(sceOpusSilkEncEncode(ToState(retireState), samples.data(), 960, packet, sizeof(packet)) > 2); + Require(sceOpusSilkEncDestroy(ToState(retireState)) == 0); + std::array floatState{}; floatState.fill(0x5A); Require(sceOpusSilkEncCreateEx(&context, ToState(floatState), 48000, 1, callerApplication) == 0); diff --git a/docs/dev/debt/libSceOpusCeltEnc.md b/docs/dev/debt/libSceOpusCeltEnc.md index 4ffce420f9..d5a11b005c 100644 --- a/docs/dev/debt/libSceOpusCeltEnc.md +++ b/docs/dev/debt/libSceOpusCeltEnc.md @@ -12,6 +12,6 @@ - [Encode and EncodeFloat](../../../core/libs/prx/libSceOpusCeltEnc/Export.cpp) refuse with the [measured codes](https://github.com/boykopovar/AnyPS5/pull/10563#issuecomment-6412138282): null state +0x01, null samples +0x02 before the state marker, a markerless state +0x16, null data +0x04 before the frame check and maxDataBytes 0 +0x05; maxDataBytes below zero is not refused and reaches our libopus backend, where a rejection throws. Frame sizes outside the durations 2.5, 5, 10, 20, 40 and 60 ms, the measured 120 to 2880 at 48000, give +0x03; we scale the counts by rate while the module's rule for other rates is unmeasured. Closes when: the frame rule and the console outcome for a buffer AJM rejects are measured -- [sceOpusCeltEncEncodeFloat](../../../core/libs/prx/libSceOpusCeltEnc/Export.cpp) checks null state, null samples, frame 0 and the marker, then reserves the converted frame on the stack before its remaining checks; the frame × channels × 4 byte reservation kills the process for the [measured frame -1](https://github.com/boykopovar/AnyPS5/pull/10563#issuecomment-6412138282), and our implementation aborts above 8 MiB. The threshold between the measured deaths and the accepted frames is our host stack model, not a console measurement. Closes when: the console's stack behaviour for such frames is measured +- [sceOpusCeltEncEncodeFloat](../../../core/libs/prx/libSceOpusCeltEnc/Export.cpp) checks null state, null samples, frame 0 and the marker, then reserves the converted frame on the stack before its remaining checks; the reservation frame × channels × 4 bytes kills the console module for the [measured frame -1](https://github.com/boykopovar/AnyPS5/pull/10563#issuecomment-6412138282), and our implementation throws once it exceeds 8 MiB instead, so the failure reports. The bound beyond the measured deaths is our host stack model, not a console measurement. Closes when: the console's stack behaviour for such frames is measured - [Create, CreateEx, Destroy, GetSize, Terminate and Initialize](../../../core/libs/prx/libSceOpusCeltEnc/Export.cpp) refuse with base 0x813F0000, the null pointer +0x01, the sample rate +0x10, channels +0x0D, application +0x11, the marker in Destroy +0x16 and `Initialize(nullptr)` +0x19, all from the [firmware 13.42 export measurements](https://github.com/boykopovar/AnyPS5/pull/10563#issuecomment-6401539314); Destroy clears the marker and leaves the other bytes, GetSize returns 728 on channels 1 and 2 and refuses other counts with +0x0D. CreateEx context handling, duplicate initialization and termination with live encoders throw as the recovered decoder analogue does. Closes when: those paths are measured on the console diff --git a/docs/dev/debt/libSceOpusSilkEnc.md b/docs/dev/debt/libSceOpusSilkEnc.md index 7d892eadcd..5221d87c57 100644 --- a/docs/dev/debt/libSceOpusSilkEnc.md +++ b/docs/dev/debt/libSceOpusSilkEnc.md @@ -12,6 +12,6 @@ - [Encode and EncodeFloat](../../../core/libs/prx/libSceOpusSilkEnc/Export.cpp) refuse with the [measured codes](https://github.com/boykopovar/AnyPS5/pull/10563#issuecomment-6412138282): null state +0x01, null samples +0x02 before the state marker, a markerless state +0x15, null data +0x04 before the frame check and maxDataBytes 0 +0x05; maxDataBytes below zero is not refused and reaches our libopus backend, where a rejection throws. Frame sizes outside the durations 10, 20, 40 and 60 ms, the measured 480 to 2880 at 48000 with 120 and 240 refused, give +0x03; we scale the counts by rate while the module's rule for other rates is unmeasured. Closes when: the frame rule and the console outcome for a buffer AJM rejects are measured -- [sceOpusSilkEncEncodeFloat](../../../core/libs/prx/libSceOpusSilkEnc/Export.cpp) shares the CELT module's check order: null state, null samples, frame 0 and the marker, then the frame × channels × 4 byte stack reservation for the converted frame before its remaining checks, which kills the process for negative frames [per the measured CELT run](https://github.com/boykopovar/AnyPS5/pull/10563#issuecomment-6412138282); our implementation aborts above 8 MiB. The threshold between the measured deaths and the accepted frames is our host stack model, not a console measurement. Closes when: the console's stack behaviour for such frames is measured +- [sceOpusSilkEncEncodeFloat](../../../core/libs/prx/libSceOpusSilkEnc/Export.cpp) shares the CELT module's check order: null state, null samples, frame 0 and the marker, then the frame × channels × 4 byte stack reservation for the converted frame before its remaining checks, which kills the console module for negative frames [per the measured CELT run](https://github.com/boykopovar/AnyPS5/pull/10563#issuecomment-6412138282); our implementation throws once it exceeds 8 MiB instead, so the failure reports. The bound beyond the measured deaths is our host stack model, not a console measurement. Closes when: the console's stack behaviour for such frames is measured - [Create, CreateEx, Destroy, GetSize, Terminate and Initialize](../../../core/libs/prx/libSceOpusSilkEnc/Export.cpp) refuse with base 0x81770000, the null pointer +0x01, the sample rate +0x0F, channels +0x0C, application +0x10, the marker in Destroy +0x15 and `Initialize(nullptr)` +0x18, all from the [firmware 13.42 export measurements](https://github.com/boykopovar/AnyPS5/pull/10563#issuecomment-6401539314); Destroy clears the marker and leaves the other bytes, GetSize returns 752 on channels 1 and 2 and refuses other counts with +0x0C. CreateEx context handling, duplicate initialization and termination with live encoders throw as the recovered decoder analogue does. Closes when: those paths are measured on the console From 27ed844d7a7065f797f113012d0bcc10c3c20964 Mon Sep 17 00:00:00 2001 From: Alexandre Date: Mon, 26 Oct 2026 10:54:21 +0100 Subject: [PATCH 103/198] feat(libSceInvitationDialog): finish the PS4 invitation dialog as canceled without a window Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RoucYS7mQpV23WDUEwXLuU --- .../prx/libSceInvitationDialog/CMakeLists.txt | 16 +++ .../prx/libSceInvitationDialog/Export.cpp | 109 ++++++++++++--- .../libSceInvitationDialog/include/Api.hpp | 34 ++++- core/libs/tests/GuestInvitationDialog.cpp | 132 ++++++++++++++++++ docs/dev/debt/libSceInvitationDialog.md | 12 +- 5 files changed, 275 insertions(+), 28 deletions(-) create mode 100644 core/libs/prx/libSceInvitationDialog/CMakeLists.txt create mode 100644 core/libs/tests/GuestInvitationDialog.cpp diff --git a/core/libs/prx/libSceInvitationDialog/CMakeLists.txt b/core/libs/prx/libSceInvitationDialog/CMakeLists.txt new file mode 100644 index 0000000000..3354dc9ff8 --- /dev/null +++ b/core/libs/prx/libSceInvitationDialog/CMakeLists.txt @@ -0,0 +1,16 @@ +add_library(libSceInvitationDialog SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceInvitationDialog PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(libSceInvitationDialog PRIVATE libc) +configure_windows_unwind(libSceInvitationDialog) + +if(BUILD_TESTING) + add_test_executable(guest_invitation_dialog_tests ${LIBS_INCLUDE_DIR}/tests/GuestInvitationDialog.cpp) + target_include_directories(guest_invitation_dialog_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_invitation_dialog_tests PRIVATE libSceInvitationDialog libc) + configure_windows_unwind(guest_invitation_dialog_tests) + add_test(NAME guest_invitation_dialog COMMAND guest_invitation_dialog_tests) + if(WIN32) + set_tests_properties(guest_invitation_dialog PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceInvitationDialog/Export.cpp b/core/libs/prx/libSceInvitationDialog/Export.cpp index 9053e853ee..62cf9728ee 100644 --- a/core/libs/prx/libSceInvitationDialog/Export.cpp +++ b/core/libs/prx/libSceInvitationDialog/Export.cpp @@ -1,37 +1,108 @@ #include "prx/libSceInvitationDialog/include/Api.hpp" #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr int COMMON_DIALOG_STATUS_NONE = 0; +constexpr int COMMON_DIALOG_STATUS_INITIALIZED = 1; +constexpr int COMMON_DIALOG_STATUS_FINISHED = 3; +constexpr int COMMON_DIALOG_ERROR_NOT_INITIALIZED = static_cast(0x80B80003u); +constexpr int COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast(0x80B80005u); +constexpr int COMMON_DIALOG_ERROR_ARG_NULL = static_cast(0x80B8000Du); +constexpr int COMMON_DIALOG_RESULT_USER_CANCELED = 1; + +constexpr std::int32_t INVITATION_DIALOG_MODE_SEND = 1; +constexpr std::int32_t INVITATION_DIALOG_MODE_RECV = 2; + +struct DialogState { + std::mutex mutex; + int status = COMMON_DIALOG_STATUS_NONE; + void* callbackArg = nullptr; +}; + +DialogState& Dialog() { + static DialogState state; + return state; +} + +[[noreturn]] void Unsettled(const char* function, const std::string& what) { + throw std::runtime_error(std::string(function) + ": " + what); +} + +} extern "C" { -int APS5_VABI sceInvitationDialogGetResult(InvitationDialogResult* result) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceInvitationDialogInitialize(void) { + DialogState& dialog = Dialog(); + std::lock_guard lock(dialog.mutex); + if (dialog.status != COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_ALREADY_INITIALIZED; + dialog.status = COMMON_DIALOG_STATUS_INITIALIZED; + dialog.callbackArg = nullptr; + return COMMON_DIALOG_OK; } -int APS5_VABI sceInvitationDialogInitialize() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceInvitationDialogTerminate(void) { + DialogState& dialog = Dialog(); + std::lock_guard lock(dialog.mutex); + if (dialog.status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + dialog.status = COMMON_DIALOG_STATUS_NONE; + dialog.callbackArg = nullptr; + return COMMON_DIALOG_OK; } -int APS5_VABI sceInvitationDialogOpen() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceInvitationDialogUpdateStatus(void) { + DialogState& dialog = Dialog(); + std::lock_guard lock(dialog.mutex); + return dialog.status; } -int APS5_VABI sceInvitationDialogTerminate() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceInvitationDialogUpdateStatus() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceInvitationDialogOpen(const InvitationDialogParam* param) { + DialogState& dialog = Dialog(); + std::lock_guard lock(dialog.mutex); + if (dialog.status != COMMON_DIALOG_STATUS_INITIALIZED && dialog.status != COMMON_DIALOG_STATUS_FINISHED) { + Unsettled(__func__, "status " + std::to_string(dialog.status) + " is neither INITIALIZED nor FINISHED, and the error for it is unsettled"); + } + if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + if (param->size != sizeof(InvitationDialogParam)) { + Unsettled(__func__, "param size " + std::to_string(param->size) + " is not the " + std::to_string(sizeof(InvitationDialogParam)) + "-byte layout this library reads"); + } + if (param->mode != INVITATION_DIALOG_MODE_SEND && param->mode != INVITATION_DIALOG_MODE_RECV) { + Unsettled(__func__, "mode " + std::to_string(param->mode) + " is neither SEND nor RECV, and the error for it is unsettled"); + } + dialog.callbackArg = param->callbackArg; + dialog.status = COMMON_DIALOG_STATUS_FINISHED; + return COMMON_DIALOG_OK; } int APS5_VABI sceInvitationDialogClose(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + DialogState& dialog = Dialog(); + std::lock_guard lock(dialog.mutex); + if (dialog.status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + Unsettled(__func__, "status " + std::to_string(dialog.status) + " is not RUNNING, and the error for closing a dialog that is not running is unsettled"); +} + +int APS5_VABI sceInvitationDialogGetResult(InvitationDialogResult* result) { + DialogState& dialog = Dialog(); + std::lock_guard lock(dialog.mutex); + if (dialog.status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (dialog.status != COMMON_DIALOG_STATUS_FINISHED) return COMMON_DIALOG_ERROR_NOT_FINISHED; + if (result == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + if (std::any_of(std::begin(result->reserved), std::end(result->reserved), [](std::uint8_t value) { return value != 0; })) { + Unsettled(__func__, "the module checks the reserved bytes 0x18..0x37 of the result, and the error for a non-zero one is unsettled"); + } + result->callbackArg = dialog.callbackArg; + result->errorCode = COMMON_DIALOG_OK; + result->result = COMMON_DIALOG_RESULT_USER_CANCELED; + if (result->recipients != nullptr) result->recipients->count = 0; + return COMMON_DIALOG_OK; } } diff --git a/core/libs/prx/libSceInvitationDialog/include/Api.hpp b/core/libs/prx/libSceInvitationDialog/include/Api.hpp index 884ec82465..7e53014e52 100644 --- a/core/libs/prx/libSceInvitationDialog/include/Api.hpp +++ b/core/libs/prx/libSceInvitationDialog/include/Api.hpp @@ -1,27 +1,47 @@ #pragma once +#include "prx/libSceCommonDialog/CommonDialog.hpp" #include "SceTypes.hpp" #include #include +struct InvitationDialogParam { + CommonDialogBaseParam baseParam; + std::uint32_t size; + std::int32_t mode; + std::int32_t userId; + std::uint32_t padding; + void* callbackArg; + const void* dataParam; + std::uint8_t reserved[64]; +}; + struct InvitationDialogRecipients { std::uint32_t count; NpOnlineId onlineIds[16]; }; struct InvitationDialogResult { - std::uint8_t opaque00[8]; + void* callbackArg; std::int32_t errorCode; - std::uint32_t opaque0C; + std::int32_t result; InvitationDialogRecipients* recipients; std::uint8_t reserved[32]; }; +static_assert(sizeof(CommonDialogBaseParam) == 48); +static_assert(offsetof(InvitationDialogParam, size) == 48); +static_assert(offsetof(InvitationDialogParam, mode) == 52); +static_assert(offsetof(InvitationDialogParam, userId) == 56); +static_assert(offsetof(InvitationDialogParam, callbackArg) == 64); +static_assert(offsetof(InvitationDialogParam, dataParam) == 72); +static_assert(offsetof(InvitationDialogParam, reserved) == 80); +static_assert(sizeof(InvitationDialogParam) == 144); static_assert(sizeof(NpOnlineId) == 20); static_assert(offsetof(InvitationDialogRecipients, onlineIds) == 4); static_assert(sizeof(InvitationDialogRecipients) == 324); static_assert(offsetof(InvitationDialogResult, errorCode) == 0x08); -static_assert(offsetof(InvitationDialogResult, opaque0C) == 0x0C); +static_assert(offsetof(InvitationDialogResult, result) == 0x0C); static_assert(offsetof(InvitationDialogResult, recipients) == 0x10); static_assert(offsetof(InvitationDialogResult, reserved) == 0x18); static_assert(sizeof(InvitationDialogResult) == 0x38); @@ -29,10 +49,10 @@ static_assert(sizeof(InvitationDialogResult) == 0x38); extern "C" { int APS5_VABI sceInvitationDialogGetResult(InvitationDialogResult* result); -int APS5_VABI sceInvitationDialogInitialize(); -int APS5_VABI sceInvitationDialogOpen(); -int APS5_VABI sceInvitationDialogTerminate(); -int APS5_VABI sceInvitationDialogUpdateStatus(); +int APS5_VABI sceInvitationDialogInitialize(void); +int APS5_VABI sceInvitationDialogOpen(const InvitationDialogParam* param); +int APS5_VABI sceInvitationDialogTerminate(void); +int APS5_VABI sceInvitationDialogUpdateStatus(void); int APS5_VABI sceInvitationDialogClose(void); } diff --git a/core/libs/tests/GuestInvitationDialog.cpp b/core/libs/tests/GuestInvitationDialog.cpp new file mode 100644 index 0000000000..1e73a6125d --- /dev/null +++ b/core/libs/tests/GuestInvitationDialog.cpp @@ -0,0 +1,132 @@ +#include "prx/libSceInvitationDialog/include/Api.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr int StatusNone = 0; +constexpr int StatusInitialized = 1; +constexpr int StatusFinished = 3; +constexpr int NotInitialized = static_cast(0x80B80003u); +constexpr int AlreadyInitialized = static_cast(0x80B80004u); +constexpr int NotFinished = static_cast(0x80B80005u); +constexpr int ArgNull = static_cast(0x80B8000Du); +constexpr std::int32_t ResultUserCanceled = 1; +constexpr std::int32_t ModeSend = 1; +constexpr std::int32_t ModeRecv = 2; + +void Require(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "InvitationDialog: %s\n", message); + std::abort(); + } +} + +template +void RequireUnsettled(TCall call, const char* message) { + bool threw = false; + try { + call(); + } catch (const std::runtime_error& error) { + threw = std::string(error.what()).find("not implemented") == std::string::npos; + } + Require(threw, message); +} + +InvitationDialogParam MakeParam(std::int32_t mode, void* callbackArg) { + InvitationDialogParam param{}; + param.baseParam.size = sizeof(CommonDialogBaseParam); + param.size = sizeof(InvitationDialogParam); + param.mode = mode; + param.userId = 0x10000000; + param.callbackArg = callbackArg; + return param; +} + +void TestBeforeInitialize() { + InvitationDialogResult result{}; + Require(sceInvitationDialogUpdateStatus() == StatusNone, "status before initialization"); + Require(sceInvitationDialogGetResult(&result) == NotInitialized, "result before initialization"); + Require(sceInvitationDialogGetResult(nullptr) == NotInitialized, "null result before initialization"); + Require(sceInvitationDialogClose() == NotInitialized, "close before initialization"); + Require(sceInvitationDialogTerminate() == NotInitialized, "terminate before initialization"); + const InvitationDialogParam param = MakeParam(ModeSend, nullptr); + RequireUnsettled([&] { sceInvitationDialogOpen(¶m); }, "open before initialization"); +} + +void TestOpenChecks() { + InvitationDialogResult result{}; + Require(sceInvitationDialogInitialize() == 0, "initialize"); + Require(sceInvitationDialogInitialize() == AlreadyInitialized, "initialize twice"); + Require(sceInvitationDialogUpdateStatus() == StatusInitialized, "status after initialization"); + Require(sceInvitationDialogGetResult(&result) == NotFinished, "result before the dialog opened"); + Require(sceInvitationDialogGetResult(nullptr) == NotFinished, "null result before the dialog finished"); + RequireUnsettled([] { sceInvitationDialogClose(); }, "close of a dialog that is not running"); + + Require(sceInvitationDialogOpen(nullptr) == ArgNull, "open without a param"); + InvitationDialogParam param = MakeParam(ModeSend, nullptr); + param.size = sizeof(InvitationDialogParam) - 8; + RequireUnsettled([&] { sceInvitationDialogOpen(¶m); }, "open with another param size"); + param = MakeParam(0, nullptr); + RequireUnsettled([&] { sceInvitationDialogOpen(¶m); }, "open in the invalid mode"); + param = MakeParam(3, nullptr); + RequireUnsettled([&] { sceInvitationDialogOpen(¶m); }, "open in an unknown mode"); + Require(sceInvitationDialogUpdateStatus() == StatusInitialized, "a rejected open leaves the status"); +} + +void TestCanceled() { + int first = 0; + int second = 0; + const InvitationDialogParam send = MakeParam(ModeSend, &first); + Require(sceInvitationDialogOpen(&send) == 0, "open to send"); + Require(sceInvitationDialogUpdateStatus() == StatusFinished, "the dialog finishes at once"); + RequireUnsettled([] { sceInvitationDialogClose(); }, "close of a finished dialog"); + + Require(sceInvitationDialogGetResult(nullptr) == ArgNull, "null result after the dialog finished"); + InvitationDialogResult result{}; + result.reserved[31] = 1; + RequireUnsettled([&] { sceInvitationDialogGetResult(&result); }, "result with a non-zero reserved byte"); + + InvitationDialogRecipients recipients{}; + recipients.count = 5; + std::memset(recipients.onlineIds, 'x', sizeof(recipients.onlineIds)); + result = InvitationDialogResult{}; + result.errorCode = 7; + result.result = 7; + result.recipients = &recipients; + Require(sceInvitationDialogGetResult(&result) == 0, "result of the canceled dialog"); + Require(result.callbackArg == &first, "the result carries the callback argument"); + Require(result.errorCode == 0 && result.result == ResultUserCanceled, "the user canceled"); + Require(result.recipients == &recipients && recipients.count == 0, "no invitation was sent"); + Require(recipients.onlineIds[0].data[0] == 'x' && recipients.onlineIds[15].pad[2] == 'x', "the online ids are left alone"); + for (const std::uint8_t value : result.reserved) Require(value == 0, "the reserved bytes stay zero"); + + result = InvitationDialogResult{}; + Require(sceInvitationDialogGetResult(&result) == 0 && result.recipients == nullptr, "result without a recipient list"); + + const InvitationDialogParam receive = MakeParam(ModeRecv, &second); + Require(sceInvitationDialogOpen(&receive) == 0, "open again to receive"); + result = InvitationDialogResult{}; + Require(sceInvitationDialogGetResult(&result) == 0 && result.callbackArg == &second, "the second open carries its own callback argument"); + + Require(sceInvitationDialogTerminate() == 0, "terminate"); + Require(sceInvitationDialogUpdateStatus() == StatusNone, "status after termination"); + Require(sceInvitationDialogGetResult(&result) == NotInitialized, "result after termination"); + Require(sceInvitationDialogInitialize() == 0 && sceInvitationDialogGetResult(&result) == NotFinished, "a new initialization forgets the result"); + Require(sceInvitationDialogTerminate() == 0, "terminate again"); +} + +} + +int main() { + TestBeforeInitialize(); + TestOpenChecks(); + TestCanceled(); + return 0; +} diff --git a/docs/dev/debt/libSceInvitationDialog.md b/docs/dev/debt/libSceInvitationDialog.md index 9038d9da62..e86bfc5b43 100644 --- a/docs/dev/debt/libSceInvitationDialog.md +++ b/docs/dev/debt/libSceInvitationDialog.md @@ -1,6 +1,14 @@ # libSceInvitationDialog technical debt +### Silent stubs + +- [sceInvitationDialogOpen, sceInvitationDialogGetResult](../../../core/libs/prx/libSceInvitationDialog/Export.cpp) (libSceInvitationDialog, PS4) - no dialog is shown and no invitation is sent: `Open` moves the dialog straight to `FINISHED`, and `GetResult` returns 0 with `errorCode` 0, `result` `USER_CANCELED` (1), the `callbackArg` of the last `Open` and a recipient count of 0, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/2cdefaa1298c6e4ece6c74bd84e90f3906282d51/src/core/libraries/invitation_dialog/invitation_dialog.cpp) reports when its window is dismissed and as [libSceNpProfileDialog](libSceNpProfileDialog.md) does. Not measured on a console. + ### Unknown function info -- [sceInvitationDialogGetResult](../../../core/libs/prx/libSceInvitationDialog/include/Api.hpp) - still throws; SDK field names and types at offsets 0x00 and 0x0C are unknown. In the supplied `libSceInvitationDialog.sprx`, export `8XKR6wa64iQ` at 0xFF0 takes one result pointer, checks reserved bytes 0x18..0x37 and writes the returned status at 0x08. The helper at 0x410 copies eight bytes from the saved open parameter to 0x00 and eight result bytes to 0x08, then copies 20-byte online IDs through the pointer at 0x10, after a four-byte count. Validation at 0xC20 limits recipients to 1..16. `InvitationDialogRecipients` provides the accessed 324-byte storage; any unused SDK tail is unverified. `InvitationDialogResult` provides the accessed 56 bytes, with unknown fields kept opaque; this is not a recovered SDK source declaration. -- [sceInvitationDialogClose](../../../core/libs/prx/libSceInvitationDialog/Export.cpp) (libSceInvitationDialog) - imported by PS4 titles, which do not load without it, and added without an implementation: declared without parameters, every call throws +- [sceInvitationDialogOpen, sceInvitationDialogGetResult](../../../core/libs/prx/libSceInvitationDialog/include/Api.hpp) (libSceInvitationDialog, PS4) - the field names and the 144-byte `InvitationDialogParam` come from the [shadPS4 header](https://github.com/shadps4-emu/shadPS4/blob/2cdefaa1298c6e4ece6c74bd84e90f3906282d51/src/core/libraries/invitation_dialog/invitation_dialog.h). They match the supplied `libSceInvitationDialog.sprx`: export `8XKR6wa64iQ` at 0xFF0 checks the reserved bytes 0x18..0x37 of the result and writes its status at 0x08, and the helper at 0x410 copies eight bytes of the saved open parameter to 0x00, the eight result bytes to 0x08 and up to 16 20-byte online ids through the pointer at 0x10, after a four-byte count. A param whose `size` is not 144, or whose `mode` is neither 1 (send) nor 2 (receive), throws, and so does a result with a non-zero reserved byte. `baseParam`, `userId` and `dataParam`, with its 1 to 16 recipients, are not checked. +- [sceInvitationDialogOpen, sceInvitationDialogClose, sceInvitationDialogGetResult](../../../core/libs/prx/libSceInvitationDialog/Export.cpp) (libSceInvitationDialog, PS4) - two paths throw because shadPS4 and the other common dialogs here return different errors: `Open` before `Initialize` (shadPS4 `INVALID_STATE`, the Signin family `NOT_INITIALIZED`) and `Close` on a dialog that is not running, which is every `Close` after `Initialize` here (shadPS4 `NOT_RUNNING`, the Signin family 0). `GetResult` before the dialog finishes returns `NOT_FINISHED` even for a null result, as in shadPS4. shadPS4 returns the `result` value from `GetResult`; this returns 0, which the module writes at 0x08 as `errorCode`. + +### Functional + +- [sceInvitationDialogInitialize](../../../core/libs/prx/libSceInvitationDialog/Export.cpp) (libSceInvitationDialog, PS4) - never returns shadPS4's `NOT_SYSTEM_INITIALIZED` (`sceCommonDialogInitialize` not called) or `BUSY` (another common dialog in use): libSceCommonDialog shares neither state here. From 2b640c8ad0165bf04159908067c2120bbc2b4efa Mon Sep 17 00:00:00 2001 From: thuanquang Date: Mon, 26 Oct 2026 11:54:58 +0700 Subject: [PATCH 104/198] fix(libc): reset every page of a run view that Commit reprotects Co-Authored-By: Claude --- .../libs/prx/libc/include/WindowsMappings.hpp | 3 +- core/libs/tests/WindowsWriteWatch.cpp | 43 +++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libc/include/WindowsMappings.hpp b/core/libs/prx/libc/include/WindowsMappings.hpp index 7a7a3f7869..fe43c331c6 100644 --- a/core/libs/prx/libc/include/WindowsMappings.hpp +++ b/core/libs/prx/libc/include/WindowsMappings.hpp @@ -87,8 +87,7 @@ public: cursor += size; } else { if (memory.State != MEM_COMMIT) throw std::runtime_error("guest memory is not committed"); - const auto mapped = views.find(cursor & ~(pageBytes - 1)); - if (mapped != views.end()) { + for (auto mapped = views.lower_bound(cursor & ~(pageBytes - 1)); mapped != views.end() && mapped->first < stop; ++mapped) { mapped->second.protection = protection; mapped->second.armed = 0; invalidate(*mapped->second.page); diff --git a/core/libs/tests/WindowsWriteWatch.cpp b/core/libs/tests/WindowsWriteWatch.cpp index 65d98f68e2..0fe438beca 100644 --- a/core/libs/tests/WindowsWriteWatch.cpp +++ b/core/libs/tests/WindowsWriteWatch.cpp @@ -630,6 +630,48 @@ void CheckSharedRunIsOneView() { VirtualFree(other, 0, MEM_RELEASE); CloseHandle(section); } + +void CheckCommitResetsEveryPageOfARunView() { + auto& mappings = Mappings::Get(); + constexpr std::size_t Page = 16384; + constexpr std::size_t Span = 4 * Page; + HANDLE section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE | SEC_RESERVE, 0, Span, nullptr); + auto* memory = static_cast(mappings.Reserve(nullptr, Span)); + Require(section && memory, "cannot allocate recommitted run test memory"); + mappings.ForgetSection(section); + const auto address = reinterpret_cast(memory); + const auto collect = [&](std::size_t page, std::size_t count, Collection collection) { + std::array pages{}; + std::size_t available = pages.size(); + Require(mappings.Collect(address + page * Page, count * Page, pages.data(), &available, true, collection), "recommitted run collection failed"); + std::set result; + for (std::size_t i = 0; i < available; ++i) result.insert((reinterpret_cast(pages[i]) - address) / Small); + return result; + }; + mappings.Map(memory, Span, section, 0, PAGE_READWRITE); + Require(MappedViews(memory, Span) == 1, "the recommitted run was not mapped as one view"); + Require(collect(0, 4, Collection::All) == Pages(0, Span / Small), "the run's first collection did not report its fresh pages"); + MEMORY_BASIC_INFORMATION armed{}; + Require(VirtualQuery(memory, &armed, sizeof(armed)) == sizeof(armed) && armed.Protect == PAGE_READONLY && armed.RegionSize == Span, "collection did not arm every page of the run alike"); + mappings.Commit(memory, Span, PAGE_READWRITE, Page, false); + Require(ProtectionAt(memory + 3 * Page) == PAGE_READWRITE, "Commit did not reprotect the whole run"); + memory[Page + 8] = 0x5a; + Require(collect(1, 1, Collection::Armed) == Pages(4, 8), "a store into the second page of a run that Commit reprotected was not collected"); + Require(ProtectionAt(memory + Page) == PAGE_READONLY, "collection did not arm the second page of the recommitted run again"); + const auto before = faults.load(); + memory[Page + Small + 8] = 0x5b; + Require(faults == before + 1, "a store into the rearmed second page of the run did not fault"); + Require(collect(1, 1, Collection::Armed) == Pages(5, 6), "a store into the rearmed second page of the run was not collected once"); + mappings.Commit(memory, Span, PAGE_READONLY, Page, false); + for (std::size_t page = 0; page < 4; ++page) { + std::uint32_t protection = 0; + Require(mappings.Protection(address + page * Page, &protection) && protection == PAGE_READONLY, ("Commit left page " + std::to_string(page) + " of a run with its old guest protection").c_str()); + } + mappings.Reset(memory, Span); + VirtualFree(memory, 0, MEM_RELEASE); + mappings.ForgetSection(section); + CloseHandle(section); +} } int main() { @@ -653,6 +695,7 @@ int main() { CheckPrivateCommitCarvesPlaceholders(); CheckPrivateCommitEdgeLayouts(); CheckPrivateCommitTails(); + CheckCommitResetsEveryPageOfARunView(); std::cout << "Windows deferred write-watch tests passed\n"; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; From aac9d6470c76921edf91260c929fa1c96f4aa191 Mon Sep 17 00:00:00 2001 From: Alexandre Date: Mon, 26 Oct 2026 11:14:58 +0100 Subject: [PATCH 105/198] fix(libSceInvitationDialog): return NOT_SYSTEM_INITIALIZED and BUSY through libSceCommonDialog Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RoucYS7mQpV23WDUEwXLuU --- core/libs/prx/libSceCommonDialog/Export.cpp | 17 +++++++++++++- .../libSceCommonDialog/include/DialogUse.hpp | 8 +++++++ .../prx/libSceInvitationDialog/CMakeLists.txt | 4 ++-- .../prx/libSceInvitationDialog/Export.cpp | 6 +++++ core/libs/tests/GuestCommonDialog.cpp | 9 ++++++++ core/libs/tests/GuestInvitationDialog.cpp | 23 ++++++++++++++++++- docs/dev/debt/libSceCommonDialog.md | 1 + docs/dev/debt/libSceInvitationDialog.md | 2 +- 8 files changed, 65 insertions(+), 5 deletions(-) create mode 100644 core/libs/prx/libSceCommonDialog/include/DialogUse.hpp diff --git a/core/libs/prx/libSceCommonDialog/Export.cpp b/core/libs/prx/libSceCommonDialog/Export.cpp index 8d090d7d35..7f5c42a321 100644 --- a/core/libs/prx/libSceCommonDialog/Export.cpp +++ b/core/libs/prx/libSceCommonDialog/Export.cpp @@ -1,4 +1,5 @@ #include "prx/libSceCommonDialog/include/Api.hpp" +#include "prx/libSceCommonDialog/include/DialogUse.hpp" #include #include #include @@ -7,6 +8,7 @@ #include "prx/libc/include/General.hpp" static std::atomic g_initialized{false}; +static std::atomic g_used{false}; extern "C" { @@ -19,7 +21,20 @@ int APS5_VABI sceCommonDialogInitialize(void) { } bool APS5_VABI sceCommonDialogIsUsed(void) { - return false; + return g_used.load(); +} + +bool CommonDialogSystemInitialized_nid_no_patch() { + return g_initialized.load(); +} + +bool CommonDialogAcquire_nid_no_patch() { + bool expected = false; + return g_used.compare_exchange_strong(expected, true); +} + +void CommonDialogRelease_nid_no_patch() { + g_used.store(false); } } diff --git a/core/libs/prx/libSceCommonDialog/include/DialogUse.hpp b/core/libs/prx/libSceCommonDialog/include/DialogUse.hpp new file mode 100644 index 0000000000..19b3e4779b --- /dev/null +++ b/core/libs/prx/libSceCommonDialog/include/DialogUse.hpp @@ -0,0 +1,8 @@ +#ifndef CORE_LIBS_PRX_LIBSCECOMMONDIALOG_INCLUDE_DIALOGUSE_HPP +#define CORE_LIBS_PRX_LIBSCECOMMONDIALOG_INCLUDE_DIALOGUSE_HPP + +extern "C" bool CommonDialogSystemInitialized_nid_no_patch(); +extern "C" bool CommonDialogAcquire_nid_no_patch(); +extern "C" void CommonDialogRelease_nid_no_patch(); + +#endif diff --git a/core/libs/prx/libSceInvitationDialog/CMakeLists.txt b/core/libs/prx/libSceInvitationDialog/CMakeLists.txt index 3354dc9ff8..56b44db4a6 100644 --- a/core/libs/prx/libSceInvitationDialog/CMakeLists.txt +++ b/core/libs/prx/libSceInvitationDialog/CMakeLists.txt @@ -1,12 +1,12 @@ add_library(libSceInvitationDialog SHARED EXCLUDE_FROM_ALL Export.cpp) target_include_directories(libSceInvitationDialog PRIVATE ${LIBS_INCLUDE_DIR}) -target_link_libraries(libSceInvitationDialog PRIVATE libc) +target_link_libraries(libSceInvitationDialog PRIVATE libSceCommonDialog libc) configure_windows_unwind(libSceInvitationDialog) if(BUILD_TESTING) add_test_executable(guest_invitation_dialog_tests ${LIBS_INCLUDE_DIR}/tests/GuestInvitationDialog.cpp) target_include_directories(guest_invitation_dialog_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_invitation_dialog_tests PRIVATE libSceInvitationDialog libc) + target_link_libraries(guest_invitation_dialog_tests PRIVATE libSceInvitationDialog libSceCommonDialog libc) configure_windows_unwind(guest_invitation_dialog_tests) add_test(NAME guest_invitation_dialog COMMAND guest_invitation_dialog_tests) if(WIN32) diff --git a/core/libs/prx/libSceInvitationDialog/Export.cpp b/core/libs/prx/libSceInvitationDialog/Export.cpp index 62cf9728ee..e198f7e1b0 100644 --- a/core/libs/prx/libSceInvitationDialog/Export.cpp +++ b/core/libs/prx/libSceInvitationDialog/Export.cpp @@ -1,4 +1,5 @@ #include "prx/libSceInvitationDialog/include/Api.hpp" +#include "prx/libSceCommonDialog/include/DialogUse.hpp" #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include @@ -13,7 +14,9 @@ namespace { constexpr int COMMON_DIALOG_STATUS_NONE = 0; constexpr int COMMON_DIALOG_STATUS_INITIALIZED = 1; constexpr int COMMON_DIALOG_STATUS_FINISHED = 3; +constexpr int COMMON_DIALOG_ERROR_NOT_SYSTEM_INITIALIZED = static_cast(0x80B80001u); constexpr int COMMON_DIALOG_ERROR_NOT_INITIALIZED = static_cast(0x80B80003u); +constexpr int COMMON_DIALOG_ERROR_BUSY = static_cast(0x80B80008u); constexpr int COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast(0x80B80005u); constexpr int COMMON_DIALOG_ERROR_ARG_NULL = static_cast(0x80B8000Du); constexpr int COMMON_DIALOG_RESULT_USER_CANCELED = 1; @@ -43,7 +46,9 @@ extern "C" { int APS5_VABI sceInvitationDialogInitialize(void) { DialogState& dialog = Dialog(); std::lock_guard lock(dialog.mutex); + if (!CommonDialogSystemInitialized_nid_no_patch()) return COMMON_DIALOG_ERROR_NOT_SYSTEM_INITIALIZED; if (dialog.status != COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_ALREADY_INITIALIZED; + if (!CommonDialogAcquire_nid_no_patch()) return COMMON_DIALOG_ERROR_BUSY; dialog.status = COMMON_DIALOG_STATUS_INITIALIZED; dialog.callbackArg = nullptr; return COMMON_DIALOG_OK; @@ -55,6 +60,7 @@ int APS5_VABI sceInvitationDialogTerminate(void) { if (dialog.status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; dialog.status = COMMON_DIALOG_STATUS_NONE; dialog.callbackArg = nullptr; + CommonDialogRelease_nid_no_patch(); return COMMON_DIALOG_OK; } diff --git a/core/libs/tests/GuestCommonDialog.cpp b/core/libs/tests/GuestCommonDialog.cpp index b0d74a3b47..c326b83ea2 100644 --- a/core/libs/tests/GuestCommonDialog.cpp +++ b/core/libs/tests/GuestCommonDialog.cpp @@ -1,6 +1,7 @@ #include "prx/libSceCommonDialog/include/Api.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libSceCommonDialog/CommonDialog.hpp" +#include "prx/libSceCommonDialog/include/DialogUse.hpp" #include namespace { @@ -15,9 +16,17 @@ constexpr int kErrAlreadySystemInitialized = static_cast(0x80B80002); int main() { Require(!sceCommonDialogIsUsed()); + Require(!CommonDialogSystemInitialized_nid_no_patch()); Require(sceCommonDialogInitialize() == COMMON_DIALOG_OK); + Require(CommonDialogSystemInitialized_nid_no_patch()); Require(!sceCommonDialogIsUsed()); Require(sceCommonDialogInitialize() == kErrAlreadySystemInitialized); Require(sceCommonDialogInitialize() == kErrAlreadySystemInitialized); Require(!sceCommonDialogIsUsed()); + Require(CommonDialogAcquire_nid_no_patch() && sceCommonDialogIsUsed()); + Require(!CommonDialogAcquire_nid_no_patch() && sceCommonDialogIsUsed()); + CommonDialogRelease_nid_no_patch(); + Require(!sceCommonDialogIsUsed()); + Require(CommonDialogAcquire_nid_no_patch()); + CommonDialogRelease_nid_no_patch(); } diff --git a/core/libs/tests/GuestInvitationDialog.cpp b/core/libs/tests/GuestInvitationDialog.cpp index 1e73a6125d..0552fd1e08 100644 --- a/core/libs/tests/GuestInvitationDialog.cpp +++ b/core/libs/tests/GuestInvitationDialog.cpp @@ -1,4 +1,6 @@ #include "prx/libSceInvitationDialog/include/Api.hpp" +#include "prx/libSceCommonDialog/include/Api.hpp" +#include "prx/libSceCommonDialog/include/DialogUse.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include #include @@ -13,9 +15,11 @@ namespace { constexpr int StatusNone = 0; constexpr int StatusInitialized = 1; constexpr int StatusFinished = 3; +constexpr int NotSystemInitialized = static_cast(0x80B80001u); constexpr int NotInitialized = static_cast(0x80B80003u); constexpr int AlreadyInitialized = static_cast(0x80B80004u); constexpr int NotFinished = static_cast(0x80B80005u); +constexpr int Busy = static_cast(0x80B80008u); constexpr int ArgNull = static_cast(0x80B8000Du); constexpr std::int32_t ResultUserCanceled = 1; constexpr std::int32_t ModeSend = 1; @@ -60,10 +64,26 @@ void TestBeforeInitialize() { RequireUnsettled([&] { sceInvitationDialogOpen(¶m); }, "open before initialization"); } +void TestCommonDialogPreconditions() { + Require(sceInvitationDialogInitialize() == NotSystemInitialized, "initialize before sceCommonDialogInitialize"); + Require(sceInvitationDialogUpdateStatus() == StatusNone && !sceCommonDialogIsUsed(), "a failed initialization leaves no state"); + Require(sceCommonDialogInitialize() == 0, "initialize the common dialog system"); + + Require(CommonDialogAcquire_nid_no_patch(), "another common dialog takes the system"); + Require(sceInvitationDialogInitialize() == Busy, "initialize while another common dialog is in use"); + Require(sceInvitationDialogUpdateStatus() == StatusNone, "a busy initialization leaves the status"); + CommonDialogRelease_nid_no_patch(); + + Require(sceInvitationDialogInitialize() == 0 && sceCommonDialogIsUsed(), "the dialog takes the system"); + Require(!CommonDialogAcquire_nid_no_patch(), "another common dialog is busy while this one is initialized"); + Require(sceInvitationDialogTerminate() == 0 && !sceCommonDialogIsUsed(), "termination frees the system"); +} + void TestOpenChecks() { InvitationDialogResult result{}; Require(sceInvitationDialogInitialize() == 0, "initialize"); Require(sceInvitationDialogInitialize() == AlreadyInitialized, "initialize twice"); + Require(sceCommonDialogIsUsed(), "the system stays in use after a repeated initialization"); Require(sceInvitationDialogUpdateStatus() == StatusInitialized, "status after initialization"); Require(sceInvitationDialogGetResult(&result) == NotFinished, "result before the dialog opened"); Require(sceInvitationDialogGetResult(nullptr) == NotFinished, "null result before the dialog finished"); @@ -115,7 +135,7 @@ void TestCanceled() { result = InvitationDialogResult{}; Require(sceInvitationDialogGetResult(&result) == 0 && result.callbackArg == &second, "the second open carries its own callback argument"); - Require(sceInvitationDialogTerminate() == 0, "terminate"); + Require(sceInvitationDialogTerminate() == 0 && !sceCommonDialogIsUsed(), "terminate"); Require(sceInvitationDialogUpdateStatus() == StatusNone, "status after termination"); Require(sceInvitationDialogGetResult(&result) == NotInitialized, "result after termination"); Require(sceInvitationDialogInitialize() == 0 && sceInvitationDialogGetResult(&result) == NotFinished, "a new initialization forgets the result"); @@ -126,6 +146,7 @@ void TestCanceled() { int main() { TestBeforeInitialize(); + TestCommonDialogPreconditions(); TestOpenChecks(); TestCanceled(); return 0; diff --git a/docs/dev/debt/libSceCommonDialog.md b/docs/dev/debt/libSceCommonDialog.md index f46aaf0394..505bb357a0 100644 --- a/docs/dev/debt/libSceCommonDialog.md +++ b/docs/dev/debt/libSceCommonDialog.md @@ -7,3 +7,4 @@ ### Functional - [sceCommonDialogInitialize](../../../core/libs/prx/libSceCommonDialog/Export.cpp) (libSceCommonDialog) - returning `ALREADY_SYSTEM_INITIALIZED` (`0x80B80002`) from a repeated call is not measured on a console by the project. A third-party PS5 log ([obSCEne](https://github.com/project-oops/obSCEne/blob/67b4c529eb3c271184117e26bb54330531ef2cf6/reports/hardware/report-20261014-004800.log#L260-L261), firmware 12.40) records two calls, `0x0` and then `0x80b80002`; a third call is not covered. The SDK error names, shadPS4 (a PS4 emulator) and SharpProspero give the same value; PPSA09695 compares the result with `0x80b80002` (call site 0x5F7BD0) and PPSA06327 stops on `0x80b80004`. Closes when: measured on a PS5, including a third call. +- [sceCommonDialogIsUsed](../../../core/libs/prx/libSceCommonDialog/Export.cpp) (libSceCommonDialog) - reports whether a common dialog holds the system, like shadPS4's `g_isUsed`. Only [libSceInvitationDialog](libSceInvitationDialog.md) takes and frees it so far, through `CommonDialogAcquire_nid_no_patch` and `CommonDialogRelease_nid_no_patch`, so a dialog of another library that is in use is not seen. Not measured on a console. diff --git a/docs/dev/debt/libSceInvitationDialog.md b/docs/dev/debt/libSceInvitationDialog.md index e86bfc5b43..9f2880d0fa 100644 --- a/docs/dev/debt/libSceInvitationDialog.md +++ b/docs/dev/debt/libSceInvitationDialog.md @@ -11,4 +11,4 @@ ### Functional -- [sceInvitationDialogInitialize](../../../core/libs/prx/libSceInvitationDialog/Export.cpp) (libSceInvitationDialog, PS4) - never returns shadPS4's `NOT_SYSTEM_INITIALIZED` (`sceCommonDialogInitialize` not called) or `BUSY` (another common dialog in use): libSceCommonDialog shares neither state here. +- [sceInvitationDialogInitialize, sceInvitationDialogTerminate](../../../core/libs/prx/libSceInvitationDialog/Export.cpp) (libSceInvitationDialog, PS4) - `Initialize` returns `NOT_SYSTEM_INITIALIZED` before `sceCommonDialogInitialize`, then `ALREADY_INITIALIZED`, then `BUSY` while another dialog holds the common dialog system, in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/2cdefaa1298c6e4ece6c74bd84e90f3906282d51/src/core/libraries/invitation_dialog/invitation_dialog.cpp)'s order, and `Terminate` frees the system. Only this library takes the system through [libSceCommonDialog](libSceCommonDialog.md) so far, so the other common dialogs neither return `BUSY` because of it nor make it return `BUSY`. Not measured on a console. From acbab704c8c4fae627ef34ad54e80949ded445b8 Mon Sep 17 00:00:00 2001 From: Bipin Date: Mon, 26 Oct 2026 16:06:06 +0530 Subject: [PATCH 106/198] fix(core): retire the replaced opus encoder before the replacement A create over a marked state now retires the encoder its instance id names before any state write or allocation, so a replacement lost to a host allocation failure or the libopus OPUS_ALLOC_FAIL branch cannot strand an entry the state no longer names: Destroy and Terminate stay consistent on every failure path. The debt notes record the replacement rule as our choice with the console failed-replace behaviour unmeasured and why the tests cannot force the failure paths. --- core/libs/prx/libSceOpusCeltEnc/include/OpusEncoder.hpp | 4 +--- docs/dev/debt/libSceOpusCeltEnc.md | 2 ++ docs/dev/debt/libSceOpusSilkEnc.md | 2 ++ 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceOpusCeltEnc/include/OpusEncoder.hpp b/core/libs/prx/libSceOpusCeltEnc/include/OpusEncoder.hpp index b3d1b9040a..be8801cd6d 100644 --- a/core/libs/prx/libSceOpusCeltEnc/include/OpusEncoder.hpp +++ b/core/libs/prx/libSceOpusCeltEnc/include/OpusEncoder.hpp @@ -7,7 +7,6 @@ #include #include #include -#include #include #include #include @@ -147,7 +146,7 @@ private: }; void Make(void* state, std::uint32_t context, int sampleRate, int channels) { - const auto retired = hasMarker(state) ? std::optional{stateWord(state, codes.stateInstanceOffset)} : std::nullopt; + if (hasMarker(state)) encoders.erase(stateWord(state, codes.stateInstanceOffset)); const auto instanceId = nextInstanceId++; std::memset(state, 0, codes.stateSize); codes.setupState(state, instanceId, sampleRate, channels); @@ -165,7 +164,6 @@ private: std::memset(state, 0, codes.stateSize); fail("cannot create libopus encoder"); } - if (retired) encoders.erase(*retired); encoders.emplace(instanceId, std::move(encoder)); } diff --git a/docs/dev/debt/libSceOpusCeltEnc.md b/docs/dev/debt/libSceOpusCeltEnc.md index d5a11b005c..89a73964f2 100644 --- a/docs/dev/debt/libSceOpusCeltEnc.md +++ b/docs/dev/debt/libSceOpusCeltEnc.md @@ -10,6 +10,8 @@ - [Create and CreateEx](../../../core/libs/prx/libSceOpusCeltEnc/Export.cpp) zero-fill the 728-byte state and write the word table [measured on the modules](https://github.com/boykopovar/AnyPS5/pull/10563#issuecomment-6412138282): the instance id at +0x00, this implementation's id because our AJM does not run, 8 × channels at +0x08, channels at +0x10, rate at +0x14 and rate/50 at +0x40 and +0xC8, the bitrate at +0x1C and +0xC0 with the per-rate per-channel table 11000/19000 to 51000/99000, the constants 2, 0x451, 0x5E0, 10, 0x803, 1, 0xFFFFFC18 and 0x10, and the 0x17B2 marker at +0xCC. The console's live AJM may write further result bytes into +0xA0 and +0xD8. Closes when: a console run with live AJM settles those result bytes +- [Create and CreateEx](../../../core/libs/prx/libSceOpusCeltEnc/Export.cpp) replacement rule is this implementation's choice: a create over a marked state retires the encoder its instance id names before any state write or allocation, so a later make-unique or libopus `OPUS_ALLOC_FAIL` failure cannot strand an entry the state no longer names and the failed call leaves the state zeroed. The console behaviour for its own allocation failure after a create over a live state is unmeasured, and the tests cannot force our failure paths because the library is a shared module against libopus, which has no allocator hook, while a process-wide memory limit fails an unknown allocation first. Closes when: a deterministic allocation-failure injection or a console measurement of the failed replace exists + - [Encode and EncodeFloat](../../../core/libs/prx/libSceOpusCeltEnc/Export.cpp) refuse with the [measured codes](https://github.com/boykopovar/AnyPS5/pull/10563#issuecomment-6412138282): null state +0x01, null samples +0x02 before the state marker, a markerless state +0x16, null data +0x04 before the frame check and maxDataBytes 0 +0x05; maxDataBytes below zero is not refused and reaches our libopus backend, where a rejection throws. Frame sizes outside the durations 2.5, 5, 10, 20, 40 and 60 ms, the measured 120 to 2880 at 48000, give +0x03; we scale the counts by rate while the module's rule for other rates is unmeasured. Closes when: the frame rule and the console outcome for a buffer AJM rejects are measured - [sceOpusCeltEncEncodeFloat](../../../core/libs/prx/libSceOpusCeltEnc/Export.cpp) checks null state, null samples, frame 0 and the marker, then reserves the converted frame on the stack before its remaining checks; the reservation frame × channels × 4 bytes kills the console module for the [measured frame -1](https://github.com/boykopovar/AnyPS5/pull/10563#issuecomment-6412138282), and our implementation throws once it exceeds 8 MiB instead, so the failure reports. The bound beyond the measured deaths is our host stack model, not a console measurement. Closes when: the console's stack behaviour for such frames is measured diff --git a/docs/dev/debt/libSceOpusSilkEnc.md b/docs/dev/debt/libSceOpusSilkEnc.md index 5221d87c57..2b868f29e4 100644 --- a/docs/dev/debt/libSceOpusSilkEnc.md +++ b/docs/dev/debt/libSceOpusSilkEnc.md @@ -10,6 +10,8 @@ - [Create and CreateEx](../../../core/libs/prx/libSceOpusSilkEnc/Export.cpp) zero-fill the 752-byte state and write the word table [measured on the modules](https://github.com/boykopovar/AnyPS5/pull/10563#issuecomment-6412138282): the instance id at +0x00, this implementation's id because our AJM does not run, 8 × channels at +0x08, 0x800 at +0x10, rate at +0x14, channels at +0x18 and rate/50 at +0x58 and +0xE4, the bitrate at +0x1C and +0xDC with the per-rate per-channel table 12000/19000 to 51000/99000, the constants 0x44F, 0x5E0, 10, 0x3E8, 0xFFFFFC18 and 0x10, and the 0x17B2 marker at +0xE8. The console's live AJM may write further result bytes into +0xD0 and +0x11C. Closes when: a console run with live AJM settles those result bytes +- [Create and CreateEx](../../../core/libs/prx/libSceOpusSilkEnc/Export.cpp) replacement rule is this implementation's choice: a create over a marked state retires the encoder its instance id names before any state write or allocation, so a later make-unique or libopus `OPUS_ALLOC_FAIL` failure cannot strand an entry the state no longer names and the failed call leaves the state zeroed. The console behaviour for its own allocation failure after a create over a live state is unmeasured, and the tests cannot force our failure paths because the library is a shared module against libopus, which has no allocator hook, while a process-wide memory limit fails an unknown allocation first. Closes when: a deterministic allocation-failure injection or a console measurement of the failed replace exists + - [Encode and EncodeFloat](../../../core/libs/prx/libSceOpusSilkEnc/Export.cpp) refuse with the [measured codes](https://github.com/boykopovar/AnyPS5/pull/10563#issuecomment-6412138282): null state +0x01, null samples +0x02 before the state marker, a markerless state +0x15, null data +0x04 before the frame check and maxDataBytes 0 +0x05; maxDataBytes below zero is not refused and reaches our libopus backend, where a rejection throws. Frame sizes outside the durations 10, 20, 40 and 60 ms, the measured 480 to 2880 at 48000 with 120 and 240 refused, give +0x03; we scale the counts by rate while the module's rule for other rates is unmeasured. Closes when: the frame rule and the console outcome for a buffer AJM rejects are measured - [sceOpusSilkEncEncodeFloat](../../../core/libs/prx/libSceOpusSilkEnc/Export.cpp) shares the CELT module's check order: null state, null samples, frame 0 and the marker, then the frame × channels × 4 byte stack reservation for the converted frame before its remaining checks, which kills the console module for negative frames [per the measured CELT run](https://github.com/boykopovar/AnyPS5/pull/10563#issuecomment-6412138282); our implementation throws once it exceeds 8 MiB instead, so the failure reports. The bound beyond the measured deaths is our host stack model, not a console measurement. Closes when: the console's stack behaviour for such frames is measured From b05ca4e4bf1fe486377ea127a3f70278301c8af2 Mon Sep 17 00:00:00 2001 From: Emanuele <118837721+imellag@users.noreply.github.com> Date: Mon, 26 Oct 2026 12:07:59 +0100 Subject: [PATCH 107/198] fix(libSceVideoOut): read the window size after a flip on the main thread on macOS --- core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp index 850de295b4..5dec8f0393 100644 --- a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp +++ b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp @@ -740,7 +740,7 @@ void VideoOutDriver::presentLoop(std::stop_token token, std::promise& star processFlip(*current); int windowWidth = 0; int windowHeight = 0; - if (window.Handle() != nullptr) SDL_GetWindowSize(window.Handle(), &windowWidth, &windowHeight); + if (window.Handle() != nullptr) OnMainThread([&] { SDL_GetWindowSize(window.Handle(), &windowWidth, &windowHeight); }); if (windowWidth > 0 && windowHeight > 0) { const auto presented = AgcDriver::ComputeContainSize_nid_postfix(current->width, current->height, static_cast(windowWidth), static_cast(windowHeight), true); mouseInput->SetPresentation(current->width, current->height, presented.width, presented.height); From 8d878818b2eb1e4267de7e21887f16c91b0c9075 Mon Sep 17 00:00:00 2001 From: Bipin Date: Mon, 26 Oct 2026 16:40:34 +0530 Subject: [PATCH 108/198] docs(opus): clarify failed encoder replacement outcomes in debt --- docs/dev/debt/libSceOpusCeltEnc.md | 2 +- docs/dev/debt/libSceOpusSilkEnc.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/dev/debt/libSceOpusCeltEnc.md b/docs/dev/debt/libSceOpusCeltEnc.md index 89a73964f2..6540f74459 100644 --- a/docs/dev/debt/libSceOpusCeltEnc.md +++ b/docs/dev/debt/libSceOpusCeltEnc.md @@ -10,7 +10,7 @@ - [Create and CreateEx](../../../core/libs/prx/libSceOpusCeltEnc/Export.cpp) zero-fill the 728-byte state and write the word table [measured on the modules](https://github.com/boykopovar/AnyPS5/pull/10563#issuecomment-6412138282): the instance id at +0x00, this implementation's id because our AJM does not run, 8 × channels at +0x08, channels at +0x10, rate at +0x14 and rate/50 at +0x40 and +0xC8, the bitrate at +0x1C and +0xC0 with the per-rate per-channel table 11000/19000 to 51000/99000, the constants 2, 0x451, 0x5E0, 10, 0x803, 1, 0xFFFFFC18 and 0x10, and the 0x17B2 marker at +0xCC. The console's live AJM may write further result bytes into +0xA0 and +0xD8. Closes when: a console run with live AJM settles those result bytes -- [Create and CreateEx](../../../core/libs/prx/libSceOpusCeltEnc/Export.cpp) replacement rule is this implementation's choice: a create over a marked state retires the encoder its instance id names before any state write or allocation, so a later make-unique or libopus `OPUS_ALLOC_FAIL` failure cannot strand an entry the state no longer names and the failed call leaves the state zeroed. The console behaviour for its own allocation failure after a create over a live state is unmeasured, and the tests cannot force our failure paths because the library is a shared module against libopus, which has no allocator hook, while a process-wide memory limit fails an unknown allocation first. Closes when: a deterministic allocation-failure injection or a console measurement of the failed replace exists +- [Create and CreateEx](../../../core/libs/prx/libSceOpusCeltEnc/Export.cpp) replacement rule is this implementation's choice: a create over a marked state retires the encoder its instance id names before any state write or allocation, so a later failure cannot strand an entry the state no longer names; explicit libopus error branches leave the state zeroed, while make-unique throwing after setupState or a later registry insert throw leaves the filled state without a live encoder. The console behaviour for its own allocation failure after a create over a live state is unmeasured, and the tests cannot force our failure paths because the library is a shared module against libopus, which has no allocator hook, while a process-wide memory limit fails an unknown allocation first. Closes when: a deterministic allocation-failure injection or a console measurement of the failed replace exists - [Encode and EncodeFloat](../../../core/libs/prx/libSceOpusCeltEnc/Export.cpp) refuse with the [measured codes](https://github.com/boykopovar/AnyPS5/pull/10563#issuecomment-6412138282): null state +0x01, null samples +0x02 before the state marker, a markerless state +0x16, null data +0x04 before the frame check and maxDataBytes 0 +0x05; maxDataBytes below zero is not refused and reaches our libopus backend, where a rejection throws. Frame sizes outside the durations 2.5, 5, 10, 20, 40 and 60 ms, the measured 120 to 2880 at 48000, give +0x03; we scale the counts by rate while the module's rule for other rates is unmeasured. Closes when: the frame rule and the console outcome for a buffer AJM rejects are measured diff --git a/docs/dev/debt/libSceOpusSilkEnc.md b/docs/dev/debt/libSceOpusSilkEnc.md index 2b868f29e4..ef82072a5a 100644 --- a/docs/dev/debt/libSceOpusSilkEnc.md +++ b/docs/dev/debt/libSceOpusSilkEnc.md @@ -10,7 +10,7 @@ - [Create and CreateEx](../../../core/libs/prx/libSceOpusSilkEnc/Export.cpp) zero-fill the 752-byte state and write the word table [measured on the modules](https://github.com/boykopovar/AnyPS5/pull/10563#issuecomment-6412138282): the instance id at +0x00, this implementation's id because our AJM does not run, 8 × channels at +0x08, 0x800 at +0x10, rate at +0x14, channels at +0x18 and rate/50 at +0x58 and +0xE4, the bitrate at +0x1C and +0xDC with the per-rate per-channel table 12000/19000 to 51000/99000, the constants 0x44F, 0x5E0, 10, 0x3E8, 0xFFFFFC18 and 0x10, and the 0x17B2 marker at +0xE8. The console's live AJM may write further result bytes into +0xD0 and +0x11C. Closes when: a console run with live AJM settles those result bytes -- [Create and CreateEx](../../../core/libs/prx/libSceOpusSilkEnc/Export.cpp) replacement rule is this implementation's choice: a create over a marked state retires the encoder its instance id names before any state write or allocation, so a later make-unique or libopus `OPUS_ALLOC_FAIL` failure cannot strand an entry the state no longer names and the failed call leaves the state zeroed. The console behaviour for its own allocation failure after a create over a live state is unmeasured, and the tests cannot force our failure paths because the library is a shared module against libopus, which has no allocator hook, while a process-wide memory limit fails an unknown allocation first. Closes when: a deterministic allocation-failure injection or a console measurement of the failed replace exists +- [Create and CreateEx](../../../core/libs/prx/libSceOpusSilkEnc/Export.cpp) replacement rule is this implementation's choice: a create over a marked state retires the encoder its instance id names before any state write or allocation, so a later failure cannot strand an entry the state no longer names; explicit libopus error branches leave the state zeroed, while make-unique throwing after setupState or a later registry insert throw leaves the filled state without a live encoder. The console behaviour for its own allocation failure after a create over a live state is unmeasured, and the tests cannot force our failure paths because the library is a shared module against libopus, which has no allocator hook, while a process-wide memory limit fails an unknown allocation first. Closes when: a deterministic allocation-failure injection or a console measurement of the failed replace exists - [Encode and EncodeFloat](../../../core/libs/prx/libSceOpusSilkEnc/Export.cpp) refuse with the [measured codes](https://github.com/boykopovar/AnyPS5/pull/10563#issuecomment-6412138282): null state +0x01, null samples +0x02 before the state marker, a markerless state +0x15, null data +0x04 before the frame check and maxDataBytes 0 +0x05; maxDataBytes below zero is not refused and reaches our libopus backend, where a rejection throws. Frame sizes outside the durations 10, 20, 40 and 60 ms, the measured 480 to 2880 at 48000 with 120 and 240 refused, give +0x03; we scale the counts by rate while the module's rule for other rates is unmeasured. Closes when: the frame rule and the console outcome for a buffer AJM rejects are measured From b135644b23477799e1fa6eaa6c6f1f954fa72f8c Mon Sep 17 00:00:00 2001 From: LeandroLP23 Date: Mon, 26 Oct 2026 12:24:16 +0100 Subject: [PATCH 109/198] fix(libSceZlib): end sceZlibWaitForDone timeouts on time on Windows The timed wait went through std::condition_variable, which winpthreads ends only on 15.6 ms GetTickCount64 steps (#1471), so a 1 ms timeout returned after about 15.6 ms. It now waits on TimedWait::Condition until a deadline from TimedWait::DeadlineNanos, like Rudp and EventFlag. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceZlib/CMakeLists.txt | 2 +- core/libs/prx/libSceZlib/Export.cpp | 13 ++++++------- core/libs/tests/GuestZlib.cpp | 5 +++++ 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceZlib/CMakeLists.txt b/core/libs/prx/libSceZlib/CMakeLists.txt index 6beeb76258..4b216a85a4 100644 --- a/core/libs/prx/libSceZlib/CMakeLists.txt +++ b/core/libs/prx/libSceZlib/CMakeLists.txt @@ -1,4 +1,4 @@ -add_library(libSceZlib SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp) +add_library(libSceZlib SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../libkernel/Time/TimedWait.cpp) target_include_directories(libSceZlib PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/libjpeg-turbo/src/spng/zlib) set_target_properties(libSceZlib PROPERTIES CXX_EXTENSIONS OFF) diff --git a/core/libs/prx/libSceZlib/Export.cpp b/core/libs/prx/libSceZlib/Export.cpp index f908068b6b..cf7ea91785 100644 --- a/core/libs/prx/libSceZlib/Export.cpp +++ b/core/libs/prx/libSceZlib/Export.cpp @@ -1,8 +1,7 @@ #include "prx/libSceZlib/include/Api.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/Time/include/TimedWait.hpp" #include "zlib.h" -#include -#include #include #include #include @@ -33,7 +32,7 @@ std::uint64_t nextId = 1; std::uint64_t session = 0; std::unordered_map results; std::deque completed; -std::condition_variable completion; +TimedWait::Condition completion; } // namespace @@ -55,7 +54,7 @@ std::int32_t APS5_VABI sceZlibFinalize() { completed.clear(); initialized = false; ++session; - completion.notify_all(); + completion.NotifyAll(); return 0; } @@ -82,7 +81,7 @@ std::int32_t APS5_VABI sceZlibInflate(const void* source, std::uint32_t sourceLe const std::uint64_t id = nextId++; results[id] = InflateResult{produced, status}; completed.push_back(id); - completion.notify_one(); + completion.NotifyOne(); *requestId = id; return 0; } @@ -93,8 +92,8 @@ std::int32_t APS5_VABI sceZlibWaitForDone(std::uint64_t* requestId, std::uint32_ if (!requestId) return INVALID_ARGUMENT; const std::uint64_t waitSession = session; const auto finished = [waitSession] { return !completed.empty() || session != waitSession; }; - if (!timeout) completion.wait(lock, finished); - else if (!completion.wait_for(lock, std::chrono::microseconds(*timeout), finished)) return TIMED_OUT; + if (!timeout) completion.Wait(lock, finished); + else if (!completion.WaitUntil(lock, TimedWait::DeadlineNanos(*timeout), finished)) return TIMED_OUT; if (session != waitSession) throw std::runtime_error("sceZlibWaitForDone: finalization during a wait semantics unknown"); *requestId = completed.front(); completed.pop_front(); diff --git a/core/libs/tests/GuestZlib.cpp b/core/libs/tests/GuestZlib.cpp index 9f8a857137..15ad0686d0 100644 --- a/core/libs/tests/GuestZlib.cpp +++ b/core/libs/tests/GuestZlib.cpp @@ -107,6 +107,11 @@ int main() { Require(sceZlibWaitForDone(&completed, &timeout) == static_cast(0x81120027)); const auto waited = std::chrono::steady_clock::now() - start; Require(waited >= std::chrono::microseconds(20000) && waited < std::chrono::seconds(1)); + timeout = 1000; + const auto shortStart = std::chrono::steady_clock::now(); + for (int round = 0; round < 20; ++round) Require(sceZlibWaitForDone(&completed, &timeout) == static_cast(0x81120027)); + const auto shortWaits = std::chrono::steady_clock::now() - shortStart; + Require(shortWaits >= std::chrono::milliseconds(20) && shortWaits < std::chrono::milliseconds(200)); std::thread waiter([&] { Require(sceZlibWaitForDone(&completed, nullptr) == 0); }); std::this_thread::sleep_for(std::chrono::milliseconds(10)); Require(sceZlibInflate(compressed.data(), compressed.size(), output, 8192, &id) == 0); From 3cf8170ff88e7dc1396d494e7e809ebf2905c2bd Mon Sep 17 00:00:00 2001 From: Celegans12 <53881228+Celegans12@users.noreply.github.com> Date: Tue, 20 Oct 2026 01:06:44 +0200 Subject: [PATCH 110/198] feat(agc): toggle runtime switches in alternating phases of one run Two runs of the same scene drift apart by more than many effects worth measuring. APS5_AB=:[+...] alternates phase A (switches as the environment sets them) and phase B (the listed switches toggled) at flip packets, under the GPU mutex after the draw pipeline drained, and prints "[ab] phase A|B begins/settled at present (t s)". APS5_AB_START (a flip count, or s) delays the first phase and APS5_AB_SETTLE (default 2 s) marks when a phase's reports are clean. Switches live in a registry (RuntimeSwitches) of relaxed atomics that start as the environment sets them; APS5_NO_BARRIER_BATCH is the first, since every elision it gates is decided per barrier. Any other name fails the first flip. Without APS5_AB nothing changes. AI-assisted: yes --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 13 ++ .../src/Driver/Packets/PacketExecution.cpp | 11 + .../Graphics/include/RuntimeSwitches.hpp | 67 ++++++ .../libSceAgcDriver/Graphics/src/Recorder.cpp | 4 +- .../Graphics/src/RuntimeSwitches.cpp | 212 ++++++++++++++++++ .../libSceAgcDriver/tests/RuntimeSwitches.cpp | 151 +++++++++++++ docs/dev/ENVIRONMENT.md | 5 +- docs/dev/TRACING.md | 1 + 8 files changed, 461 insertions(+), 3 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/Graphics/include/RuntimeSwitches.hpp create mode 100644 core/libs/prx/libSceAgcDriver/Graphics/src/RuntimeSwitches.cpp create mode 100644 core/libs/prx/libSceAgcDriver/tests/RuntimeSwitches.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index eac723686d..b6fa72a419 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -326,6 +326,7 @@ set(agcDriverSources ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/PipelineLibrary.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/PipelineCache.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/PipelineJournal.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/RuntimeSwitches.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Resources.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/DeferredDestruction.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/PlaceholderImages.cpp @@ -522,6 +523,18 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test_executable(agc_runtime_switches_tests tests/RuntimeSwitches.cpp Graphics/src/RuntimeSwitches.cpp) + target_include_directories(agc_runtime_switches_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(agc_runtime_switches_tests PRIVATE libc) + configure_windows_unwind(agc_runtime_switches_tests) + add_test(NAME agc_runtime_switches COMMAND agc_runtime_switches_tests) + add_test(NAME agc_runtime_switches_refused COMMAND agc_runtime_switches_tests refused) + set_tests_properties(agc_runtime_switches agc_runtime_switches_refused PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(agc_runtime_switches agc_runtime_switches_refused PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(agc_async_log_tests tests/AsyncLog.cpp) target_include_directories(agc_async_log_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(agc_async_log_tests PRIVATE libc) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp index 5972698258..7ab5bd1f27 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp @@ -2,6 +2,7 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "ThreadOwned.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" +#include "prx/libSceAgcDriver/Graphics/include/RuntimeSwitches.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/SynchronizationStatistics.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/DeferredLabels.hpp" @@ -408,6 +409,16 @@ void Driver::execute(const Submission& submission) { ++flipsCounted; if (batchesAtFlip != 0) flipSerial = batchesAtFlip; flipBatchesUnsignaled += unsignaledAtFlip; + if (Graphics::AbConfigured()) { + static const auto abClock = std::chrono::steady_clock::now(); + std::vector abLines; + { + GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Flush); + std::lock_guard gpuLock(GuestMemory::GpuMutex()); + abLines = Graphics::AbAtPresent(flipsCounted.load(), std::chrono::duration(std::chrono::steady_clock::now() - abClock).count()); + } + for (const auto& line : abLines) AgcDriver::ProfilePrint_nid_no_patch("%s", line.c_str()); + } timing.Mark("flip_flush"); timing.FinishPacket(submission.serial, cursor, 0xffffu, count); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/RuntimeSwitches.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/RuntimeSwitches.hpp new file mode 100644 index 0000000000..199ea68922 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/RuntimeSwitches.hpp @@ -0,0 +1,67 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_RUNTIMESWITCHES_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_RUNTIMESWITCHES_HPP + +#include +#include +#include +#include +#include +#include + +namespace AgcDriver::Graphics { + +enum class RuntimeSwitch : std::size_t { + NoBarrierBatch, + Count, +}; + +namespace RuntimeSwitches { + +const char* Name(RuntimeSwitch id); +bool Set(RuntimeSwitch id); +void Assign(RuntimeSwitch id, bool set); +std::optional Find(std::string_view name); + +} + +class AbSchedule { +public: + struct Config { + double phaseSeconds = 0; + double settleSeconds = 2; + std::uint64_t startPresent = 0; + std::optional startSeconds; + std::vector switches; + }; + + enum class Event { None, Begin, Settled }; + + struct Step { + Event event = Event::None; + std::uint64_t phase = 0; + bool b = false; + }; + + static Config Parse(std::string_view ab, std::string_view start, std::string_view settle); + + explicit AbSchedule(Config config); + + Step AtPresent(std::uint64_t present, double seconds); + + const Config& Settings() const { return config; } + +private: + Config config; + bool started = false; + bool settled = false; + std::uint64_t phase = 0; + double phaseStart = 0; + std::optional firstSeconds; +}; + +bool AbConfigured(); +std::vector AbAtPresent(std::uint64_t present, double seconds); + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp index 3abc0aec26..4a00f823f8 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp @@ -6,6 +6,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libkernel/Time/include/SleepWake.hpp" #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/RuntimeSwitches.hpp" #include "prx/libSceAgcDriver/Graphics/include/ParallelCompare.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" #include "prx/libSceAgcDriver/Graphics/include/DeferredDestruction.hpp" @@ -1930,8 +1931,7 @@ bool Recorder::MergeBarriers() { } bool Recorder::BarrierBatch() { - static const bool batch = MergeBarriers() && std::getenv("APS5_NO_BARRIER_BATCH") == nullptr; - return batch; + return MergeBarriers() && !RuntimeSwitches::Set(RuntimeSwitch::NoBarrierBatch); } bool Recorder::ElideCovered(CommandClass which, const BarrierMask& wanted, VkAccessFlags covered) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/RuntimeSwitches.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/RuntimeSwitches.cpp new file mode 100644 index 0000000000..ebc1eac247 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/RuntimeSwitches.cpp @@ -0,0 +1,212 @@ +#include "prx/libSceAgcDriver/Graphics/include/RuntimeSwitches.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver::Graphics { + +namespace { + +constexpr std::size_t SwitchCount = static_cast(RuntimeSwitch::Count); + +constexpr std::array SwitchNames{ + "APS5_NO_BARRIER_BATCH", +}; + +struct SwitchValues { + std::array, SwitchCount> set{}; + SwitchValues() { + const std::array environment{ + std::getenv("APS5_NO_BARRIER_BATCH") != nullptr, + }; + for (std::size_t index = 0; index < SwitchCount; ++index) set[index].store(environment[index], std::memory_order_relaxed); + } +}; + +SwitchValues& Values() { + static SwitchValues values; + return values; +} + +std::string_view Trim(std::string_view text) { + while (!text.empty() && (text.front() == ' ' || text.front() == '\t')) text.remove_prefix(1); + while (!text.empty() && (text.back() == ' ' || text.back() == '\t' || text.back() == '\r' || text.back() == '\n')) text.remove_suffix(1); + return text; +} + +double ParseSeconds(std::string_view text, const char* what) { + const std::string copy(Trim(text)); + char* end = nullptr; + const double value = std::strtod(copy.c_str(), &end); + if (copy.empty() || end != copy.c_str() + copy.size() || !std::isfinite(value) || value < 0) throw std::invalid_argument(std::string(what) + ": '" + copy + "' is not a number of seconds"); + return value; +} + +std::string FlippableList() { + std::string list; + for (const auto* name : SwitchNames) list += std::string(list.empty() ? "" : ", ") + name; + return list; +} + +} + +namespace RuntimeSwitches { + +const char* Name(RuntimeSwitch id) { + return SwitchNames.at(static_cast(id)); +} + +bool Set(RuntimeSwitch id) { + return Values().set.at(static_cast(id)).load(std::memory_order_relaxed); +} + +void Assign(RuntimeSwitch id, bool set) { + Values().set.at(static_cast(id)).store(set, std::memory_order_relaxed); +} + +std::optional Find(std::string_view name) { + for (std::size_t index = 0; index < SwitchCount; ++index) { + if (name == SwitchNames[index]) return static_cast(index); + } + return std::nullopt; +} + +} + +AbSchedule::Config AbSchedule::Parse(std::string_view ab, std::string_view start, std::string_view settle) { + Config config; + ab = Trim(ab); + const auto colon = ab.find(':'); + if (colon == std::string_view::npos) throw std::invalid_argument("APS5_AB: expected :[+...], got '" + std::string(ab) + "'"); + config.phaseSeconds = ParseSeconds(ab.substr(0, colon), "APS5_AB phase length"); + if (config.phaseSeconds < 1) throw std::invalid_argument("APS5_AB: a phase must last at least 1 s"); + auto list = ab.substr(colon + 1); + if (Trim(list).empty()) throw std::invalid_argument("APS5_AB: no switch listed after the phase length"); + for (;;) { + const auto plus = list.find('+'); + const auto name = Trim(list.substr(0, plus)); + if (name.empty()) throw std::invalid_argument("APS5_AB: an empty switch name in '" + std::string(ab) + "'"); + const auto found = RuntimeSwitches::Find(name); + if (!found.has_value()) throw std::invalid_argument("APS5_AB: " + std::string(name) + " cannot change while the title runs; switches the A/B can change: " + FlippableList()); + if (std::find(config.switches.begin(), config.switches.end(), *found) != config.switches.end()) throw std::invalid_argument("APS5_AB: " + std::string(name) + " is listed twice"); + config.switches.push_back(*found); + if (plus == std::string_view::npos) break; + list = list.substr(plus + 1); + } + start = Trim(start); + if (!start.empty()) { + if (start.back() == 's') { + config.startSeconds = ParseSeconds(start.substr(0, start.size() - 1), "APS5_AB_START"); + } else { + const std::string copy(start); + char* end = nullptr; + errno = 0; + const auto value = std::strtoull(copy.c_str(), &end, 10); + if (end != copy.c_str() + copy.size() || errno == ERANGE || copy.front() < '0' || copy.front() > '9') throw std::invalid_argument("APS5_AB_START: '" + copy + "' is neither a present index nor s"); + config.startPresent = value; + } + } + if (!Trim(settle).empty()) config.settleSeconds = ParseSeconds(settle, "APS5_AB_SETTLE"); + if (config.settleSeconds >= config.phaseSeconds) throw std::invalid_argument("APS5_AB_SETTLE must be shorter than the phase"); + return config; +} + +AbSchedule::AbSchedule(Config config) : config(std::move(config)) {} + +AbSchedule::Step AbSchedule::AtPresent(std::uint64_t present, double seconds) { + if (!firstSeconds.has_value()) firstSeconds = seconds; + if (!started) { + const bool due = config.startSeconds.has_value() ? seconds - *firstSeconds >= *config.startSeconds : present >= config.startPresent; + if (!due) return {}; + started = true; + settled = false; + phase = 0; + phaseStart = seconds; + return {Event::Begin, 0, false}; + } + if (seconds - phaseStart >= config.phaseSeconds) { + ++phase; + settled = false; + phaseStart = seconds; + return {Event::Begin, phase, (phase & 1u) != 0}; + } + if (!settled && seconds - phaseStart >= config.settleSeconds) { + settled = true; + return {Event::Settled, phase, (phase & 1u) != 0}; + } + return {}; +} + +namespace { + +struct AbState { + std::mutex mutex; + std::optional schedule; + std::array environment{}; +}; + +AbState& Ab() { + static AbState* state = [] { + auto* created = new AbState(); + const char* ab = std::getenv("APS5_AB"); + const char* start = std::getenv("APS5_AB_START"); + const char* settle = std::getenv("APS5_AB_SETTLE"); + if (ab != nullptr) { + created->schedule.emplace(AbSchedule::Parse(ab, start != nullptr ? start : "", settle != nullptr ? settle : "")); + for (std::size_t index = 0; index < SwitchCount; ++index) created->environment[index] = RuntimeSwitches::Set(static_cast(index)); + } + return created; + }(); + return *state; +} + +std::string SwitchSettings(const AbState& state, bool b) { + std::string text; + for (const auto id : state.schedule->Settings().switches) { + const bool set = state.environment[static_cast(id)] != b; + text += std::string(text.empty() ? "" : ", ") + RuntimeSwitches::Name(id) + (set ? " set" : " unset"); + } + return text; +} + +} + +bool AbConfigured() { + static const bool configured = std::getenv("APS5_AB") != nullptr; + return configured; +} + +std::vector AbAtPresent(std::uint64_t present, double seconds) { + if (!AbConfigured()) return {}; + auto& state = Ab(); + std::lock_guard lock(state.mutex); + std::vector lines; + char text[512]; + const auto& config = state.schedule->Settings(); + const auto step = state.schedule->AtPresent(present, seconds); + if (step.event == AbSchedule::Event::None) return lines; + if (step.event == AbSchedule::Event::Begin) { + if (step.phase == 0) { + char from[96]; + if (config.startSeconds.has_value()) std::snprintf(from, sizeof(from), "%.1f s after the first present", *config.startSeconds); + else std::snprintf(from, sizeof(from), "present %llu", static_cast(config.startPresent)); + std::snprintf(text, sizeof(text), "[ab] in-run A/B: phases of %.1f s from %s, settle %.1f s; A: %s; B: %s\n", config.phaseSeconds, from, config.settleSeconds, SwitchSettings(state, false).c_str(), SwitchSettings(state, true).c_str()); + lines.emplace_back(text); + } + for (const auto id : config.switches) RuntimeSwitches::Assign(id, state.environment[static_cast(id)] != step.b); + std::snprintf(text, sizeof(text), "[ab] phase %c %llu begins at present %llu (t %.3f s): %s\n", step.b ? 'B' : 'A', static_cast(step.phase), static_cast(present), seconds, SwitchSettings(state, step.b).c_str()); + } else { + std::snprintf(text, sizeof(text), "[ab] phase %c %llu settled at present %llu (t %.3f s)\n", step.b ? 'B' : 'A', static_cast(step.phase), static_cast(present), seconds); + } + lines.emplace_back(text); + return lines; +} + +} diff --git a/core/libs/prx/libSceAgcDriver/tests/RuntimeSwitches.cpp b/core/libs/prx/libSceAgcDriver/tests/RuntimeSwitches.cpp new file mode 100644 index 0000000000..fe37684a2d --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/RuntimeSwitches.cpp @@ -0,0 +1,151 @@ +#include "prx/libSceAgcDriver/Graphics/include/RuntimeSwitches.hpp" +#include +#include +#include +#include +#include +#include + +using AgcDriver::Graphics::AbSchedule; +using AgcDriver::Graphics::RuntimeSwitch; +namespace RuntimeSwitches = AgcDriver::Graphics::RuntimeSwitches; + +namespace { + +void Require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +void SetEnvironment(const char* name, const char* value) { +#ifdef _WIN32 + _putenv_s(name, value != nullptr ? value : ""); +#else + if (value != nullptr) setenv(name, value, 1); + else unsetenv(name); +#endif +} + +std::string Refusal(const char* ab, const char* start = "", const char* settle = "") { + try { + AbSchedule::Parse(ab, start, settle); + } catch (const std::invalid_argument& error) { + return error.what(); + } + return {}; +} + +void registry() { + for (std::size_t index = 0; index < static_cast(RuntimeSwitch::Count); ++index) { + const auto id = static_cast(index); + Require(RuntimeSwitches::Find(RuntimeSwitches::Name(id)) == id, std::string("the name of a switch does not find it: ") + RuntimeSwitches::Name(id)); + Require(RuntimeSwitches::Set(id) == (std::getenv(RuntimeSwitches::Name(id)) != nullptr), std::string("a switch does not start as the environment sets it: ") + RuntimeSwitches::Name(id)); + } + Require(RuntimeSwitches::Find("APS5_NO_BARRIER_BATCH") == RuntimeSwitch::NoBarrierBatch, "APS5_NO_BARRIER_BATCH is not a runtime switch"); + Require(RuntimeSwitches::Set(RuntimeSwitch::NoBarrierBatch), "a switch set in the environment did not start set"); + Require(!RuntimeSwitches::Find("APS5_FULL_BARRIERS").has_value() && !RuntimeSwitches::Find("APS5_NO_BARRIER_BATCH ").has_value(), "a name that is not a runtime switch was found"); + RuntimeSwitches::Assign(RuntimeSwitch::NoBarrierBatch, false); + Require(!RuntimeSwitches::Set(RuntimeSwitch::NoBarrierBatch), "an assigned switch did not read back unset"); + RuntimeSwitches::Assign(RuntimeSwitch::NoBarrierBatch, true); + Require(RuntimeSwitches::Set(RuntimeSwitch::NoBarrierBatch), "an assigned switch did not read back set"); +} + +void parse() { + const auto config = AbSchedule::Parse(" 30 : APS5_NO_BARRIER_BATCH ", "815", "3"); + Require(config.phaseSeconds == 30 && config.settleSeconds == 3 && config.startPresent == 815 && !config.startSeconds.has_value(), "the phase settings were not parsed"); + Require(config.switches.size() == 1 && config.switches[0] == RuntimeSwitch::NoBarrierBatch, "the switch list was not parsed"); + const auto bySeconds = AbSchedule::Parse("60:APS5_NO_BARRIER_BATCH", "90s", ""); + Require(bySeconds.startSeconds == 90.0 && bySeconds.settleSeconds == 2 && bySeconds.startPresent == 0, "a start in seconds was not parsed"); + Require(Refusal("60:APS5_FULL_BARRIERS").find("APS5_FULL_BARRIERS cannot change while the title runs; switches the A/B can change: APS5_NO_BARRIER_BATCH") != std::string::npos, "a startup-only variable was not refused with the list of switches"); + Require(Refusal("60:APS5_NO_BARRIER_BATCH+APS5_NO_BARRIER_BATCH").find("listed twice") != std::string::npos, "a duplicate switch was not refused"); + Require(Refusal("60:+APS5_NO_BARRIER_BATCH").find("empty switch name") != std::string::npos && Refusal("60:APS5_NO_BARRIER_BATCH+").find("empty switch name") != std::string::npos, "an empty switch name was not refused"); + Require(Refusal("60:").find("no switch listed") != std::string::npos, "an empty switch list was not refused"); + Require(!Refusal("APS5_NO_BARRIER_BATCH").empty(), "a missing phase length was not refused"); + Require(!Refusal("0.5:APS5_NO_BARRIER_BATCH").empty() && !Refusal("x:APS5_NO_BARRIER_BATCH").empty() && !Refusal("-5:APS5_NO_BARRIER_BATCH").empty() && !Refusal("inf:APS5_NO_BARRIER_BATCH").empty(), "a bad phase length was not refused"); + Require(!Refusal("10:APS5_NO_BARRIER_BATCH", "", "10").empty() && !Refusal("10:APS5_NO_BARRIER_BATCH", "", "x").empty(), "a bad settle was not refused"); + for (const char* start : {"-3", "abc", "s", "+3", "3x", "99999999999999999999999"}) { + Require(Refusal("10:APS5_NO_BARRIER_BATCH", start).find("APS5_AB_START") != std::string::npos, std::string("the start '") + start + "' was not refused"); + } +} + +void schedule() { + AbSchedule plan(AbSchedule::Parse("60:APS5_NO_BARRIER_BATCH", "10", "2")); + using Event = AbSchedule::Event; + const auto expect = [&](std::uint64_t present, double seconds, Event event, std::uint64_t phase, bool b) { + const auto step = plan.AtPresent(present, seconds); + Require(step.event == event && (event == Event::None || (step.phase == phase && step.b == b)), "unexpected step at present " + std::to_string(present)); + }; + expect(1, 0.0, Event::None, 0, false); + expect(9, 100.0, Event::None, 0, false); + expect(10, 102.0, Event::Begin, 0, false); + expect(11, 103.0, Event::None, 0, false); + expect(12, 104.5, Event::Settled, 0, false); + expect(13, 150.0, Event::None, 0, false); + expect(14, 161.9, Event::None, 0, false); + expect(15, 162.0, Event::Begin, 1, true); + expect(16, 163.0, Event::None, 1, true); + expect(17, 300.0, Event::Begin, 2, false); + expect(18, 301.0, Event::None, 2, false); + expect(19, 302.5, Event::Settled, 2, false); + expect(20, 362.5, Event::Begin, 3, true); + + AbSchedule timed(AbSchedule::Parse("30:APS5_NO_BARRIER_BATCH", "90s", "")); + Require(timed.AtPresent(500, 1000.0).event == Event::None, "a start in seconds began at the first present"); + Require(timed.AtPresent(501, 1089.0).event == Event::None, "a start in seconds began early"); + const auto begin = timed.AtPresent(502, 1090.0); + Require(begin.event == Event::Begin && begin.phase == 0 && !begin.b, "a start in seconds did not begin phase A"); +} + +void glue() { + Require(AgcDriver::Graphics::AbConfigured(), "APS5_AB is not seen"); + Require(AgcDriver::Graphics::AbAtPresent(1, 0.0).empty(), "the A/B printed before its start"); + RuntimeSwitches::Assign(RuntimeSwitch::NoBarrierBatch, false); + auto lines = AgcDriver::Graphics::AbAtPresent(3, 1.0); + Require(lines.size() == 2 && lines[0] == "[ab] in-run A/B: phases of 5.0 s from present 3, settle 2.0 s; A: APS5_NO_BARRIER_BATCH set; B: APS5_NO_BARRIER_BATCH unset\n" && lines[1] == "[ab] phase A 0 begins at present 3 (t 1.000 s): APS5_NO_BARRIER_BATCH set\n", "the start lines differ: " + (lines.empty() ? std::string() : lines.front())); + Require(RuntimeSwitches::Set(RuntimeSwitch::NoBarrierBatch), "phase A does not have the environment's setting"); + lines = AgcDriver::Graphics::AbAtPresent(4, 3.5); + Require(lines.size() == 1 && lines[0] == "[ab] phase A 0 settled at present 4 (t 3.500 s)\n", "the settle line differs"); + lines = AgcDriver::Graphics::AbAtPresent(5, 6.0); + Require(lines.size() == 1 && lines[0] == "[ab] phase B 1 begins at present 5 (t 6.000 s): APS5_NO_BARRIER_BATCH unset\n", "the phase B line differs"); + Require(!RuntimeSwitches::Set(RuntimeSwitch::NoBarrierBatch), "phase B did not toggle the listed switch"); + Require(AgcDriver::Graphics::AbAtPresent(6, 7.0).empty(), "a present inside the settle printed"); + lines = AgcDriver::Graphics::AbAtPresent(7, 11.5); + Require(lines.size() == 1 && lines[0] == "[ab] phase A 2 begins at present 7 (t 11.500 s): APS5_NO_BARRIER_BATCH set\n", "phase A did not come back"); + Require(RuntimeSwitches::Set(RuntimeSwitch::NoBarrierBatch), "phase A did not restore the environment's setting"); +} + +int refused() { + SetEnvironment("APS5_AB", "5:APS5_FULL_BARRIERS"); + try { + AgcDriver::Graphics::AbAtPresent(1, 0.0); + } catch (const std::invalid_argument& error) { + if (std::string_view(error.what()).find("APS5_FULL_BARRIERS cannot change while the title runs") != std::string_view::npos) { + std::printf("runtime switch refusal test passed\n"); + return 0; + } + std::fprintf(stderr, "FAILED: unexpected refusal: %s\n", error.what()); + return 1; + } + std::fprintf(stderr, "FAILED: APS5_AB naming a startup-only variable was not refused at the first present\n"); + return 1; +} + +} + +int main(int argc, char** argv) { + if (argc > 1 && std::string_view(argv[1]) == "refused") return refused(); + try { + SetEnvironment("APS5_NO_BARRIER_BATCH", "1"); + SetEnvironment("APS5_AB", "5:APS5_NO_BARRIER_BATCH"); + SetEnvironment("APS5_AB_START", "3"); + SetEnvironment("APS5_AB_SETTLE", nullptr); + registry(); + parse(); + schedule(); + glue(); + } catch (const std::exception& error) { + std::fprintf(stderr, "FAILED: %s\n", error.what()); + return 1; + } + std::printf("runtime switch tests passed\n"); + return 0; +} diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 7b29095640..bce6bb18af 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -81,6 +81,9 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `ANYPS5_REQUIRE_ROBUST2` | presence | `core/libs/prx/libSceAgcDriver/tests/execution/LdsMixedWidthAtomics.cpp` | | fails the LDS lock-budget test when the device does not enable robustBufferAccess2, instead of only printing it | | `ANYPS5_REQUIRE_VULKAN` | presence | `core/libs/prx/libSceAgcDriver/tests/BdaDevice.cpp` | | fails a test instead of skipping when no usable Vulkan device is present | | `ANYPS5_SCREENSHOTS` | string | `core/libs/prx/libSceAgcDriver/Execution/src/Screenshot.cpp` | | directory the screenshot key saves its PNG files to, defaulting to anyps5-screenshots beside the executable | +| `APS5_AB` | string | `core/libs/prx/libSceAgcDriver/Graphics/src/RuntimeSwitches.cpp` | | `:[+...]`: alternates phase A (switches as the environment sets them) and phase B (the listed runtime switches toggled) at flip packets, each phase at least `` long; prints `[ab]` lines; a name that is not a runtime switch fails the first flip | +| `APS5_AB_SETTLE` | string | `core/libs/prx/libSceAgcDriver/Graphics/src/RuntimeSwitches.cpp` | 2 | seconds into a phase after which APS5_AB prints the phase's `settled` line; shorter than the phase | +| `APS5_AB_START` | string | `core/libs/prx/libSceAgcDriver/Graphics/src/RuntimeSwitches.cpp` | 0 | flip packet count (from 1) at which APS5_AB starts, or `s` for seconds after the first flip | | `APS5_ADDRESS_COPY_MAX_MIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | 64 | upper bound in MiB on the guest memory one address-based build copies; exceeding it aborts | | `APS5_ALL_BUFFERS_WRITTEN` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | notes every shader buffer element as written, as before bufferWritten existed | | `APS5_ASSERT_GPU_LOCK` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp` | | aborts when a guarded path is reached without holding GuestMemory::GpuMutex | @@ -204,7 +207,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_NO_ADJACENT_GENERATION` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | leaves adjacent pending images at their generation during a write-back, as before | | `APS5_NO_ALIAS_BORROW` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | disables borrowing a pending aliased storage image | | `APS5_NO_ATOMIC_STAGING` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | binds atomic shader elements in place instead of staging them through a device-local buffer | -| `APS5_NO_BARRIER_BATCH` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | records the staging, write-back and label-run leading barriers that a covering barrier already orders, instead of leaving them out | +| `APS5_NO_BARRIER_BATCH` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/RuntimeSwitches.cpp` | | records the staging, write-back and label-run leading barriers that a covering barrier already orders, instead of leaving them out; APS5_AB can toggle it | | `APS5_NO_BARRIER_ELISION` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | records every leading barrier instead of merging it into the following submission | | `APS5_NO_BDA_STAGING` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | binds the written elements of address-based compute builds in place in the host imports instead of staging them device-local | | `APS5_NO_BDA_TABLE_CACHE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp` | | builds every BDA descriptor table instead of reusing the recent-table cache | diff --git a/docs/dev/TRACING.md b/docs/dev/TRACING.md index 51854e22c3..10da6a1e96 100644 --- a/docs/dev/TRACING.md +++ b/docs/dev/TRACING.md @@ -69,6 +69,7 @@ See [Build](BUILD.md#performance-trace) for the trace format, summary fields and | `APS5_TRACE_SUMMARY_EVERY=` | Writes periodic summary lines without recording trace events. Can be combined with either window switch. | | `APS5_TRACE_EVENTS=` | Sets the per-thread event-buffer capacity for a frame or seconds window (1–4,194,304; default 262,144). | | `APS5_TRACE_DIR=` | Writes performance capture and summary files to this directory instead of the working directory. | +| `APS5_AB=:[+...]` | Alternates phase A (the environment's settings) and phase B (the listed switches toggled) at flip packets within one run, each phase at least `` long. Every phase change prints `[ab] phase A\|B begins at present (t s)` and, `APS5_AB_SETTLE` seconds (default 2) later, `[ab] phase A\|B settled at present (t s)`; `t` counts from the first flip. `APS5_AB_START=` starts at flip ``, `APS5_AB_START=s` `` seconds after the first flip. Only `APS5_NO_BARRIER_BATCH` can be listed; any other name fails the first flip. | | `APS5_VIDEOOUT_TELEMETRY_DIR=` | Writes the presents and the pad changes of a run to `/videoout.jsonl`; see [Usage](../user/USAGE.md#video-out-and-pad-telemetry). | ## Shader registers From 2801497e0c733638b62d1499a1ea3fb015ae4420 Mon Sep 17 00:00:00 2001 From: Celegans12 <53881228+Celegans12@users.noreply.github.com> Date: Tue, 20 Oct 2026 01:09:29 +0200 Subject: [PATCH 111/198] feat(tools): split the periodic reports of an APS5_AB run by phase An APS5_AB run interleaves phases A and B, and every periodic report covers the time since the previous report of its kind, so its numbers need sorting before A and B can be compared. tools/ab_split.py reads the [ab] lines and counts a [present], [gputime], [draw] pipelined or [syncwait] report for a phase only when its whole span lies inside the phase after its settled line. It prints presents per 10 s and frame time per phase, GPU busy and idle time per frame, GPU time per present (also per --program key and --class), committer, drain and hook-wait times, the A and B means, and each B phase against the mean of its neighbouring A phases, which cancels a linear drift. AI-assisted: yes --- CMakeLists.txt | 3 + docs/dev/TRACING.md | 2 +- tools/ab_split.py | 215 +++++++++++++++++++++++++++++++++++ tools/tests/test_ab_split.py | 136 ++++++++++++++++++++++ 4 files changed, 355 insertions(+), 1 deletion(-) create mode 100644 tools/ab_split.py create mode 100644 tools/tests/test_ab_split.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 155214af16..97b0c33042 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -263,6 +263,9 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_trace_digest.py) add_test(NAME symbolize_samples COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_symbolize_samples.py) + add_test(NAME ab_split COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_ab_split.py) + set_tests_properties(ab_split PROPERTIES TIMEOUT 20) add_test(NAME conventions_bom_shebang COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_bom_shebang.py) set_tests_properties(conventions_bom_shebang PROPERTIES TIMEOUT 20) diff --git a/docs/dev/TRACING.md b/docs/dev/TRACING.md index 10da6a1e96..57168055f8 100644 --- a/docs/dev/TRACING.md +++ b/docs/dev/TRACING.md @@ -69,7 +69,7 @@ See [Build](BUILD.md#performance-trace) for the trace format, summary fields and | `APS5_TRACE_SUMMARY_EVERY=` | Writes periodic summary lines without recording trace events. Can be combined with either window switch. | | `APS5_TRACE_EVENTS=` | Sets the per-thread event-buffer capacity for a frame or seconds window (1–4,194,304; default 262,144). | | `APS5_TRACE_DIR=` | Writes performance capture and summary files to this directory instead of the working directory. | -| `APS5_AB=:[+...]` | Alternates phase A (the environment's settings) and phase B (the listed switches toggled) at flip packets within one run, each phase at least `` long. Every phase change prints `[ab] phase A\|B begins at present (t s)` and, `APS5_AB_SETTLE` seconds (default 2) later, `[ab] phase A\|B settled at present (t s)`; `t` counts from the first flip. `APS5_AB_START=` starts at flip ``, `APS5_AB_START=s` `` seconds after the first flip. Only `APS5_NO_BARRIER_BATCH` can be listed; any other name fails the first flip. | +| `APS5_AB=:[+...]` | Alternates phase A (the environment's settings) and phase B (the listed switches toggled) at flip packets within one run, each phase at least `` long. Every phase change prints `[ab] phase A\|B begins at present (t s)` and, `APS5_AB_SETTLE` seconds (default 2) later, `[ab] phase A\|B settled at present (t s)`; `t` counts from the first flip. `APS5_AB_START=` starts at flip ``, `APS5_AB_START=s` `` seconds after the first flip. Only `APS5_NO_BARRIER_BATCH` can be listed; any other name fails the first flip. `python tools/ab_split.py ` prints the mean of each periodic report (`[present]`, `[gputime]`, `[draw] pipelined`, `[syncwait]`) per phase, counting only reports that lie wholly inside a settled phase, and each B phase against its neighbouring A phases. | | `APS5_VIDEOOUT_TELEMETRY_DIR=` | Writes the presents and the pad changes of a run to `/videoout.jsonl`; see [Usage](../user/USAGE.md#video-out-and-pad-telemetry). | ## Shader registers diff --git a/tools/ab_split.py b/tools/ab_split.py new file mode 100644 index 0000000000..4bedf518ea --- /dev/null +++ b/tools/ab_split.py @@ -0,0 +1,215 @@ +import argparse +import json +import math +import re +import sys +from pathlib import Path + +DESCRIPTION = """Per-phase means of an in-run A/B (APS5_AB) log. The driver prints "[ab] phase A|B begins at present + (t s)" when a phase starts and "[ab] phase A|B settled at present (t s)" APS5_AB_SETTLE seconds +later. Each periodic report covers the time since the previous report of its kind, so a report counts for phase k +only when the previous report of its kind came after phase k's settled line and the report came before phase k+1 +began. Presents per 10 s and frame time come from the [ab] lines (settled line to the next phase's begin), GPU busy +and idle gaps per frame from [present], GPU time per present from [gputime] (APS5_PROFILE_GPU), the committer and +drains from [draw] pipelined, unlocked hook waits from [syncwait] (APS5_PROFILE_DRAW). The noise column compares +each B phase with the mean of its neighbouring A phases, which cancels a linear drift: mean difference, standard +error, standard deviation and count.""" + +PHASE = re.compile(r"^\[ab\] phase ([AB]) (\d+) (begins|settled) at present (\d+) \(t ([\d.]+) s\)") +KINDS = (("present", "[present] "), ("gputime", "[gputime] "), ("draw", "[draw] pipelined"), ("syncwait", "[syncwait] ")) + + +def number(pattern, text): + match = re.search(pattern, text) + return float(match.group(1)) if match else None + + +def parse_report(kind, text): + if kind == "present": + presents, dropped = number(r"^\[present\] (\d+) presents", text), number(r"\((\d+) dropped", text) + busy, gaps = number(r"GPU busy ([\d.]+) ms", text), number(r"idle gaps ([\d.]+) ms", text) + if None in (presents, dropped, busy, gaps): + return None + return {"frames": presents + dropped, "busy": busy, "gaps": gaps} + if kind == "gputime": + total, presents = number(r"^\[gputime\] ([\d.]+) ms of GPU time", text), number(r"; (\d+) presents;", text) + if total is None or presents is None or "; by class:" not in text: + return None + programs_text, classes_text = text.split("by program:", 1)[-1].split("; by class:", 1) + programs, classes = {}, {} + for key, ms in re.findall(r" (0x[0-9a-f]+) x\d+ ([\d.]+)ms", programs_text): + programs[key] = programs.get(key, 0.0) + float(ms) + for name, ms in re.findall(r" ([a-z][a-z-]*) x\d+ ([\d.]+)ms [\d.]+MiB \(per present", classes_text): + classes[name] = classes.get(name, 0.0) + float(ms) + return {"total": total, "presents": presents, "programs": programs, "classes": classes} + if kind == "draw": + commits, per_commit = number(r": (\d+) commits at", text), number(r"commits at ([\d.]+) us each", text) + enqueued, room = number(r"; (\d+) enqueued behind", text), number(r"waiting ([\d.]+) us per draw for room", text) + if None in (commits, per_commit, enqueued, room) or "; drains:" not in text: + return None + drains = text.split("; drains:", 1)[1].split(";", 1)[0] + return {"commits": commits, "per_commit": per_commit, "enqueued": enqueued, "room": room, + "drain_ms": sum(float(ms) for ms in re.findall(r"\(([\d.]+) ms\)", drains))} + if kind == "syncwait": + if "):" not in text: + return None + sites = text.split("):", 1)[1].split("; batches ahead", 1)[0] + return {"wait_ms": sum(float(ms) for ms in re.findall(r" \d+/([\d.]+)ms", sites))} + return None + + +def read_log(lines): + config, phases, current = None, [], None + previous = {kind: None for kind, _ in KINDS} + for line in lines: + text = line.rstrip("\r\n") + if text.startswith("[ab] in-run A/B:"): + config = text + continue + match = PHASE.match(text) + if match: + kind, index, event, present, seconds = match.group(1), int(match.group(2)), match.group(3), int(match.group(4)), float(match.group(5)) + if event == "begins": + if current is not None: + current["end"] = (present, seconds) + current = {"kind": kind, "index": index, "begin": (present, seconds), "settled": None, "end": None, + "reports": {name: [] for name, _ in KINDS}} + phases.append(current) + elif current is not None and current["index"] == index: + current["settled"] = (present, seconds) + continue + for name, prefix in KINDS: + if text.startswith(prefix): + state = (current["index"], current["settled"] is not None) if current is not None else None + if state is not None and state[1] and previous[name] == state: + report = parse_report(name, text) + if report is not None: + current["reports"][name].append(report) + previous[name] = state + break + return config, phases + + +def weighted(rows, value, weight): + total = sum(row[weight] for row in rows) + return sum(row[value] * row[weight] for row in rows) / total if total else None + + +def metrics(phase, programs, classes): + out = {} + if phase["settled"] is not None and phase["end"] is not None and phase["end"][1] > phase["settled"][1]: + presents, seconds = phase["end"][0] - phase["settled"][0], phase["end"][1] - phase["settled"][1] + out["presents/10s"] = presents / seconds * 10 + if presents > 0: + out["frame ms"] = seconds / presents * 1000 + rows = phase["reports"]["present"] + out["GPU busy ms/frame"], out["GPU idle gaps ms/frame"] = weighted(rows, "busy", "frames"), weighted(rows, "gaps", "frames") + rows = phase["reports"]["gputime"] + presents = sum(row["presents"] for row in rows) + if presents: + out["gputime ms/present"] = sum(row["total"] for row in rows) / presents + for key in programs: + out[f"{key} ms/present"] = sum(row["programs"].get(key, 0.0) for row in rows) / presents + for name in classes: + out[f"{name} ms/present"] = sum(row["classes"].get(name, 0.0) for row in rows) / presents + rows = phase["reports"]["draw"] + out["committer us/commit"], out["room wait us/draw"] = weighted(rows, "per_commit", "commits"), weighted(rows, "room", "enqueued") + out["drain ms/report"] = sum(row["drain_ms"] for row in rows) / len(rows) if rows else None + rows = phase["reports"]["syncwait"] + out["syncwait GPU ms/report"] = sum(row["wait_ms"] for row in rows) / len(rows) if rows else None + return {name: value for name, value in out.items() if value is not None} + + +def noise(values, phases): + differences = [] + for index, phase in enumerate(phases): + if phase["kind"] != "B" or values[index] is None: + continue + neighbours = [values[other] for other in (index - 1, index + 1) + if 0 <= other < len(phases) and phases[other]["kind"] == "A" and values[other] is not None] + if neighbours: + differences.append(values[index] - sum(neighbours) / len(neighbours)) + if not differences: + return None + mean = sum(differences) / len(differences) + if len(differences) == 1: + return {"mean": mean, "se": None, "sd": None, "n": 1} + sd = math.sqrt(sum((value - mean) ** 2 for value in differences) / (len(differences) - 1)) + return {"mean": mean, "se": sd / math.sqrt(len(differences)), "sd": sd, "n": len(differences)} + + +def split(lines, programs=(), classes=()): + config, phases = read_log(lines) + if not phases: + raise ValueError("no [ab] phase lines (was APS5_AB set?)") + table = [metrics(phase, programs, classes) for phase in phases] + names = list(dict.fromkeys(name for row in table for name in row)) + rows = [] + for name in names: + values = [row.get(name) for row in table] + a = [value for phase, value in zip(phases, values) if phase["kind"] == "A" and value is not None] + b = [value for phase, value in zip(phases, values) if phase["kind"] == "B" and value is not None] + if a and b: + mean_a, mean_b = sum(a) / len(a), sum(b) / len(b) + rows.append({"metric": name, "a": mean_a, "b": mean_b, "change": mean_b - mean_a, + "change_pct": (mean_b / mean_a - 1) * 100 if mean_a else None, "noise": noise(values, phases)}) + return {"config": config, + "phases": [{"kind": phase["kind"], "index": phase["index"], "begin_present": phase["begin"][0], + "settled_present": phase["settled"][0] if phase["settled"] else None, + "end_present": phase["end"][0] if phase["end"] else None, + "clean_reports": {name: len(reports) for name, reports in phase["reports"].items()}, + "metrics": row} for phase, row in zip(phases, table)], + "metrics": rows} + + +def text_report(result): + lines = [result["config"] or "[ab] configuration line missing", "", + "phase begin / settled / end clean reports (present/gputime/draw/syncwait) presents/10s"] + for phase in result["phases"]: + settled = "-" if phase["settled_present"] is None else phase["settled_present"] + end = "-" if phase["end_present"] is None else phase["end_present"] + reports = "/".join(str(phase["clean_reports"][name]) for name, _ in KINDS) + rate = phase["metrics"].get("presents/10s") + lines.append(f"{phase['kind']} {phase['index']:<3} {phase['begin_present']:>7} / {settled:>7} / {end:>7} {reports:<45} " + + ("unfinished" if rate is None else f"{rate:.2f}")) + lines.extend(["", f"{'metric':<30} {'A':>10} {'B':>10} {'B-A':>10} {'B/A-1':>8} B vs neighbouring A: mean +- SE (SD, n)"]) + for row in result["metrics"]: + spread = row["noise"] + if spread is None: + text = "-" + elif spread["n"] == 1: + text = f"{spread['mean']:+.2f} (n 1)" + else: + text = f"{spread['mean']:+.2f} +- {spread['se']:.2f} (SD {spread['sd']:.2f}, n {spread['n']})" + percent = "n/a" if row["change_pct"] is None else f"{row['change_pct']:+.1f}%" + lines.append(f"{row['metric']:<30} {row['a']:>10.2f} {row['b']:>10.2f} {row['change']:>+10.2f} {percent:>8} {text}") + return "\n".join(lines) + "\n" + + +def program_key(value): + if not re.fullmatch(r"0x[0-9a-fA-F]+", value): + raise argparse.ArgumentTypeError("must be a hexadecimal program key such as 0x1000") + return "0x" + value[2:].lower().lstrip("0").rjust(1, "0") + + +def main(arguments=None): + parser = argparse.ArgumentParser(description=DESCRIPTION) + parser.add_argument("log", help="the run's standard error log with the [ab] lines") + parser.add_argument("--program", action="append", type=program_key, default=[], help="also report this [gputime] program key per present (repeatable)") + parser.add_argument("--class", dest="classes", action="append", default=[], help="also report this [gputime] command class per present (repeatable)") + parser.add_argument("--json", action="store_true", help="print the phases and metrics as JSON") + options = parser.parse_args(arguments) + with Path(options.log).open(encoding="utf-8", errors="replace") as source: + result = split(source, options.program, options.classes) + sys.stdout.write(json.dumps(result, indent=1) + "\n" if options.json else text_report(result)) + return 0 + + +if __name__ == "__main__": + if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8", errors="replace") + try: + sys.exit(main()) + except (OSError, ValueError) as error: + print(f"FAIL: {error}", file=sys.stderr) + sys.exit(2) diff --git a/tools/tests/test_ab_split.py b/tools/tests/test_ab_split.py new file mode 100644 index 0000000000..319872dc3d --- /dev/null +++ b/tools/tests/test_ab_split.py @@ -0,0 +1,136 @@ +import json +import os +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import ab_split + + +def present(busy, gaps, presents=40, dropped=0): + return (f"[present] {presents} presents over 10 s ({dropped} dropped: no free swapchain image; 1 may trail on the GPU); " + f"inflight_wait p50 0.00 ms, p90 0.18 ms, max 0.37 ms over {presents}; per frame, presented or dropped: " + f"GPU busy {busy:.1f} ms, idle gaps {gaps:.1f} ms (0 batches without a completion record); per flip: 3.0 batches " + "recorded (0.0 unsignaled at the flip); per present: 1.0 batches ahead of the blit, 0.0 after it; after the flip " + "packet: last submit 0.1 ms, blit submit 0.2 ms; in-place reads overwritten before execution: 0 of 0 checked") + + +def gputime(total, program, staging, presents=40): + return (f"[gputime] {total:.0f} ms of GPU time in 90 batches over 10 s (batch 900 ms first to last command; classes 300 ms, " + f"all ranges 800 ms, untimed 100 ms outside every range; {presents} presents; 0 ranges dropped at the 512 cap); " + f"by program: 0x1000 x10 {program:.0f}ms 0x2000 x4 5ms; by class: staging-in x80 {staging:.1f}ms 2.0MiB " + "(per present x2.0 9.99ms 0.05MiB) present-blit x40 4.0ms 0.0MiB (per present x1.0 0.10ms 0.00MiB)") + + +def draw(per_commit, room, drain, commits=1000, enqueued=1000): + return (f"[draw] pipelined (10 s, depth 64): {commits} commits at {per_commit:.1f} us each on the committer, 0 failed; " + f"{enqueued} enqueued behind 2.00 on average, waiting {room:.1f} us per draw for room; 0 read ranges, 0 stores held; " + f"drains: packet 10 ({drain:.1f} ms) submission 2 (1.0 ms); packet drains by opcode: 0x49 x3 0 read ranges, 0 stores held") + + +def syncwait(first, second): + return ("[syncwait] unlocked hook waits by read site (10 s; count/GPU wait, avg batches up to the target, avg of them " + "unsignaled at the start, target was the newest submission, avg target submit-to-signal ms):" + f" copy 3/{first:.0f}ms 1.0 0.0 100% 2.0 fill 1/{second:.0f}ms 1.0 0.0 0% 1.0; batches ahead by queue: 0x0 4") + + +def reports(busy, total, program, staging, per_commit, wait): + return [present(busy, 50.0), gputime(total, program, staging), draw(per_commit, 2.0, 5.0), syncwait(wait, 10.0)] + + +def phase_lines(kind, index, begin, seconds, busy, total, program, staging, per_commit, wait, presents=40): + lines = [f"[ab] phase {kind} {index} begins at present {begin} (t {seconds:.3f} s): APS5_NO_BARRIER_BATCH {'set' if kind == 'A' else 'unset'}"] + lines += reports(999.0, 999.0, 999.0, 999.0, 999.0, 999.0) + lines.append(f"[ab] phase {kind} {index} settled at present {begin + 2} (t {seconds + 2:.3f} s)") + lines += reports(888.0, 888.0, 888.0, 888.0, 888.0, 888.0) + lines += reports(busy, total, program, staging, per_commit, wait) + lines += reports(busy, total, program, staging, per_commit, wait) + return lines + + +def log(): + lines = ["[gpu] startup", "[ab] in-run A/B: phases of 30.0 s from present 100, settle 2.0 s; A: APS5_NO_BARRIER_BATCH set; B: APS5_NO_BARRIER_BATCH unset"] + lines += reports(777.0, 777.0, 777.0, 777.0, 777.0, 777.0) + lines += phase_lines("A", 0, 100, 10.0, 100.0, 400.0, 40.0, 8.0, 200.0, 30.0) + lines += phase_lines("B", 1, 140, 40.0, 130.0, 480.0, 60.0, 9.0, 180.0, 50.0) + lines += phase_lines("A", 2, 180, 70.0, 110.0, 420.0, 44.0, 8.0, 200.0, 34.0) + lines.append("[ab] phase B 3 begins at present 220 (t 100.000 s): APS5_NO_BARRIER_BATCH unset") + return [line + "\n" for line in lines] + + +class AbSplitTests(unittest.TestCase): + def test_phases_count_only_reports_inside_a_settled_phase(self): + result = ab_split.split(log(), ["0x1000"], ["staging-in"]) + self.assertTrue(result["config"].startswith("[ab] in-run A/B: phases of 30.0 s")) + self.assertEqual([(phase["kind"], phase["index"], phase["begin_present"], phase["settled_present"], phase["end_present"]) + for phase in result["phases"]], + [("A", 0, 100, 102, 140), ("B", 1, 140, 142, 180), ("A", 2, 180, 182, 220), ("B", 3, 220, None, None)]) + self.assertEqual(result["phases"][0]["clean_reports"], {"present": 2, "gputime": 2, "draw": 2, "syncwait": 2}) + self.assertEqual(result["phases"][3]["clean_reports"], {"present": 0, "gputime": 0, "draw": 0, "syncwait": 0}) + first = result["phases"][0]["metrics"] + self.assertAlmostEqual(first["presents/10s"], 38 / 28 * 10) + self.assertAlmostEqual(first["frame ms"], 28 / 38 * 1000) + self.assertAlmostEqual(first["GPU busy ms/frame"], 100.0) + self.assertAlmostEqual(first["GPU idle gaps ms/frame"], 50.0) + self.assertAlmostEqual(first["gputime ms/present"], 10.0) + self.assertAlmostEqual(first["0x1000 ms/present"], 1.0) + self.assertAlmostEqual(first["staging-in ms/present"], 0.2) + self.assertAlmostEqual(first["committer us/commit"], 200.0) + self.assertAlmostEqual(first["room wait us/draw"], 2.0) + self.assertAlmostEqual(first["drain ms/report"], 6.0) + self.assertAlmostEqual(first["syncwait GPU ms/report"], 40.0) + + def test_b_is_compared_with_its_neighbouring_a_phases(self): + rows = {row["metric"]: row for row in ab_split.split(log())["metrics"]} + busy = rows["GPU busy ms/frame"] + self.assertAlmostEqual(busy["a"], 105.0) + self.assertAlmostEqual(busy["b"], 130.0) + self.assertAlmostEqual(busy["change"], 25.0) + self.assertAlmostEqual(busy["change_pct"], 25 / 105 * 100) + self.assertEqual(busy["noise"], {"mean": 25.0, "se": None, "sd": None, "n": 1}) + self.assertAlmostEqual(rows["syncwait GPU ms/report"]["noise"]["mean"], 60.0 - 42.0) + + def test_classes_are_read_without_their_per_present_figures(self): + row = ab_split.parse_report("gputime", gputime(400.0, 40.0, 8.0)) + self.assertEqual(row["classes"], {"staging-in": 8.0, "present-blit": 4.0}) + self.assertEqual(row["programs"], {"0x1000": 40.0, "0x2000": 5.0}) + + def test_dropped_presents_count_as_frames(self): + row = ab_split.parse_report("present", present(90.0, 10.0, presents=30, dropped=10)) + self.assertEqual(row, {"frames": 40, "busy": 90.0, "gaps": 10.0}) + + def test_unrecognised_reports_are_skipped(self): + self.assertIsNone(ab_split.parse_report("gputime", "[gputime] disabled")) + self.assertIsNone(ab_split.parse_report("draw", "[draw] pipelined depth 64")) + self.assertIsNone(ab_split.parse_report("present", "[present] flip 3")) + + def test_a_log_without_phases_is_refused(self): + with self.assertRaisesRegex(ValueError, "no \\[ab\\] phase lines"): + ab_split.split([present(1.0, 1.0) + "\n"]) + + def test_command_line(self): + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "run.err.log" + path.write_text("".join(log()), encoding="utf-8") + run = lambda *args: subprocess.run([sys.executable, str(Path(ab_split.__file__)), *args], capture_output=True, text=True, + encoding="utf-8", timeout=10, env=dict(os.environ, PYTHONIOENCODING="ascii")) + text = run(str(path), "--program", "0x01000") + self.assertEqual(text.returncode, 0, text.stderr) + self.assertIn("0x1000 ms/present", text.stdout) + self.assertRegex(text.stdout, r"GPU busy ms/frame +105\.00 +130\.00 +\+25\.00 +\+23\.8% +\+25\.00 \(n 1\)") + self.assertIn("unfinished", text.stdout) + data = json.loads(run(str(path), "--json").stdout) + self.assertEqual(len(data["phases"]), 4) + self.assertEqual(run(str(path), "--program", "1000").returncode, 2) + empty = Path(directory) / "empty.log" + empty.write_text("[gpu] nothing\n", encoding="utf-8") + failed = run(str(empty)) + self.assertEqual(failed.returncode, 2) + self.assertIn("FAIL: no [ab] phase lines", failed.stderr) + + +if __name__ == "__main__": + unittest.main() From 78571ca2fb9fcfed62e3de7ebfb4b4071a3fe1d4 Mon Sep 17 00:00:00 2001 From: Disaster Date: Mon, 26 Oct 2026 13:56:38 +0300 Subject: [PATCH 112/198] fix(libkernel): check settimeofday and clock_settime pointers Both read their caller's structures in place before validation, so an unreadable pointer crashed the host. Copy them in with a checked read first and return EFAULT, as FreeBSD 11.4 does. Fixes #11370 --- core/libs/prx/libkernel/Time/Time.cpp | 53 ++++++++++++++++++++++---- core/libs/tests/GuestFreebsdClocks.cpp | 3 ++ core/libs/tests/GuestKernelSys.cpp | 10 ++++- 3 files changed, 58 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libkernel/Time/Time.cpp b/core/libs/prx/libkernel/Time/Time.cpp index 60703eeb0e..59c9141e4a 100644 --- a/core/libs/prx/libkernel/Time/Time.cpp +++ b/core/libs/prx/libkernel/Time/Time.cpp @@ -8,6 +8,7 @@ #include "prx/libkernel/Pthread/include/Cancel.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/GuestArena.hpp" #include "prx/libkernel/KernelErrors.hpp" #include "prx/libkernel/Pthread/include/Pthread.hpp" #include @@ -39,7 +40,11 @@ #include #include #include -#endif // __linux__ +#include +#elif defined(__APPLE__) +#include +#include +#endif #endif static constexpr int GUEST_EFAULT = 14; @@ -48,6 +53,29 @@ static constexpr int GUEST_CLOCK_REALTIME = 0; static constexpr int GUEST_CLOCK_THREAD_CPUTIME_ID = 14; static constexpr int GUEST_CLOCK_PROCESS_CPUTIME_ID = 15; +static bool ReadGuestTime(void* destination, const void* source, std::size_t bytes) { + if (bytes == 0) return true; + const auto start = reinterpret_cast(source); + if (start == 0 || bytes > UINTPTR_MAX - start) return false; + GuestArena::GuestArenaPrepareHostRead_nid_no_patch(source, bytes); +#ifdef _WIN32 + SIZE_T copied = 0; + return ReadProcessMemory(GetCurrentProcess(), source, destination, bytes, &copied) != 0 && copied == bytes; +#elif defined(__linux__) + iovec local{destination, bytes}; + iovec remote{const_cast(source), bytes}; + const auto copied = ::process_vm_readv(::getpid(), &local, 1, &remote, 1, 0); + if (copied < 0 && errno != EFAULT) throw std::runtime_error("time: process_vm_readv failed with errno " + std::to_string(errno)); + return copied >= 0 && static_cast(copied) == bytes; +#elif defined(__APPLE__) + mach_vm_size_t copied = 0; + return ::mach_vm_read_overwrite(::mach_task_self(), reinterpret_cast(source), bytes, + reinterpret_cast(destination), &copied) == KERN_SUCCESS && copied == bytes; +#else + throw std::runtime_error("time: checked guest reads are not implemented on this host"); +#endif +} + static bool IsCpuClock(int clockId) { return (static_cast(clockId) & CPU_CLOCK_BIT) != 0; } @@ -654,32 +682,43 @@ int APS5_VABI clock_getres_nid_postfix(int clockId, KernelTimespec* res) { } int APS5_VABI clock_settime_nid_postfix(int clockId, const KernelTimespec* tp) { - if (tp == nullptr) { + KernelTimespec time{}; + if (tp == nullptr || !ReadGuestTime(&time, tp, sizeof(time))) { *__error_nid_postfix() = GUEST_EFAULT; return -1; } - if (clockId != GUEST_CLOCK_REALTIME || tp->tv_nsec < 0 || tp->tv_nsec >= 1000000000 || tp->tv_sec < 0) { + if (clockId != GUEST_CLOCK_REALTIME || time.tv_nsec < 0 || time.tv_nsec >= 1000000000 || time.tv_sec < 0) { *__error_nid_postfix() = GUEST_EINVAL; return -1; } char requested[48]; - std::snprintf(requested, sizeof(requested), "%lld.%09lld", static_cast(tp->tv_sec), static_cast(tp->tv_nsec)); + std::snprintf(requested, sizeof(requested), "%lld.%09lld", static_cast(time.tv_sec), static_cast(time.tv_nsec)); throw std::runtime_error(std::string("clock_settime: setting CLOCK_REALTIME to ") + requested + " is not supported, the host clock is not changed"); } int SetTimeOfDay_nid_no_patch(const KernelTimeval* tv, const KernelTimezone* tz) { - if (tv != nullptr && (tv->tv_usec < 0 || tv->tv_usec >= 1000000 || tv->tv_sec < 0)) { + KernelTimeval time{}; + KernelTimezone zone{}; + if (tv != nullptr && !ReadGuestTime(&time, tv, sizeof(time))) { + *__error_nid_postfix() = GUEST_EFAULT; + return -1; + } + if (tz != nullptr && !ReadGuestTime(&zone, tz, sizeof(zone))) { + *__error_nid_postfix() = GUEST_EFAULT; + return -1; + } + if (tv != nullptr && (time.tv_usec < 0 || time.tv_usec >= 1000000 || time.tv_sec < 0)) { *__error_nid_postfix() = GUEST_EINVAL; return -1; } if (tv != nullptr) { char requested[48]; - std::snprintf(requested, sizeof(requested), "%lld.%06lld", static_cast(tv->tv_sec), static_cast(tv->tv_usec)); + std::snprintf(requested, sizeof(requested), "%lld.%06lld", static_cast(time.tv_sec), static_cast(time.tv_usec)); throw std::runtime_error(std::string("settimeofday: setting the time to ") + requested + " is not supported, the host clock is not changed"); } if (tz != nullptr) { std::lock_guard lock(guestTimezoneMutex); - guestTimezone = *tz; + guestTimezone = zone; } return 0; } diff --git a/core/libs/tests/GuestFreebsdClocks.cpp b/core/libs/tests/GuestFreebsdClocks.cpp index 1ae0f71258..17533a356f 100644 --- a/core/libs/tests/GuestFreebsdClocks.cpp +++ b/core/libs/tests/GuestFreebsdClocks.cpp @@ -246,6 +246,9 @@ static void SettimeValidatesLikeFreebsd() { const KernelTimespec valid{1767225600, 5}; RequireSettimeError(GUEST_CLOCK_REALTIME, nullptr, 14); RequireSettimeError(GUEST_CLOCK_MONOTONIC, nullptr, 14); + const auto unreadable = reinterpret_cast(1); + RequireSettimeError(GUEST_CLOCK_REALTIME, unreadable, 14); + RequireSettimeError(GUEST_CLOCK_MONOTONIC, unreadable, 14); for (const int clockId : {GUEST_CLOCK_VIRTUAL, GUEST_CLOCK_PROF, GUEST_CLOCK_MONOTONIC, GUEST_CLOCK_REALTIME_PRECISE, GUEST_CLOCK_SECOND, GUEST_CLOCK_THREAD_CPUTIME_ID, GUEST_CLOCK_PROCESS_CPUTIME_ID, -1, 16}) RequireSettimeError(clockId, &valid, 22); diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index 99ffc5a124..76504c9be8 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -384,10 +384,18 @@ void CheckSettimeofday() { Require(settimeofday_nid_postfix(nullptr, nullptr) == 0); Require(gettimeofday_nid_postfix(nullptr, &zone) == 0 && zone.tz_minuteswest == 0 && zone.tz_dsttime == 0); const KernelTimezone west{60, 1}; - for (const KernelTimeval invalid : {KernelTimeval{0, 1000000}, KernelTimeval{0, -1}, KernelTimeval{-1, 0}}) + const auto unreadableTime = reinterpret_cast(1); + const auto unreadableZone = reinterpret_cast(1); + Require(settimeofday_nid_postfix(unreadableTime, &west) == -1 && Error() == 14); + Require(settimeofday_nid_postfix(unreadableTime, unreadableZone) == -1 && Error() == 14); + Require(settimeofday_nid_postfix(nullptr, unreadableZone) == -1 && Error() == 14); + for (const KernelTimeval invalid : {KernelTimeval{0, 1000000}, KernelTimeval{0, -1}, KernelTimeval{-1, 0}}) { Require(settimeofday_nid_postfix(&invalid, &west) == -1 && Error() == 22); + Require(settimeofday_nid_postfix(&invalid, unreadableZone) == -1 && Error() == 14); + } Require(gettimeofday_nid_postfix(nullptr, &zone) == 0 && zone.tz_minuteswest == 0 && zone.tz_dsttime == 0); const KernelTimeval valid{1000000000, 0}; + Require(settimeofday_nid_postfix(&valid, unreadableZone) == -1 && Error() == 14); bool rejected = false; try { settimeofday_nid_postfix(&valid, &west); From 54fb58a5e4cd2c9f982b22d2be208fa697ee7f6b Mon Sep 17 00:00:00 2001 From: iAbdulrrahman Date: Mon, 26 Oct 2026 13:10:39 +0300 Subject: [PATCH 113/198] feat(libc): implement heapsort --- core/libs/prx/libc/include/Api.hpp | 1 + core/libs/prx/libc/src/FileAndHeap.cpp | 37 ++++++++++++++++++++++++++ core/libs/tests/GuestQsort.cpp | 21 +++++++++++++++ 3 files changed, 59 insertions(+) diff --git a/core/libs/prx/libc/include/Api.hpp b/core/libs/prx/libc/include/Api.hpp index 6e7907eeff..81bfd8131f 100644 --- a/core/libs/prx/libc/include/Api.hpp +++ b/core/libs/prx/libc/include/Api.hpp @@ -257,6 +257,7 @@ void* APS5_VABI calloc_nid_postfix(size_t count, size_t size); int APS5_VABI posix_memalign_nid_postfix(void** pointer, size_t alignment, size_t size); void* APS5_VABI bsearch_nid_postfix(const void* key, const void* base, size_t count, size_t size, int (APS5_VABI *compare)(const void*, const void*)); int APS5_VABI mergesort_nid_postfix(void* base, size_t count, size_t size, GuestCompare compare); +int APS5_VABI heapsort_nid_postfix(void* base, size_t count, size_t size, GuestCompare compare); void APS5_VABI qsort_nid_postfix(void* base, size_t count, size_t size, int (APS5_VABI *compare)(const void*, const void*)); int APS5_VABI qsort_s_nid_postfix(void* base, size_t count, size_t size, GuestContextCompare compare, void* context); int APS5_VABI access_nid_postfix(const char* path, int mode); diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index 51550e1dac..42c79b4255 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -752,6 +752,43 @@ int APS5_VABI mergesort_nid_postfix(void* base, size_t count, size_t size, Guest return 0; } +int APS5_VABI heapsort_nid_postfix(void* base, size_t count, size_t size, GuestCompare compare) { + if (size == 0) { + errno = 22; + return -1; + } + if (count <= 1) return 0; + if (!compare) throw std::invalid_argument("heapsort: null comparator"); + if (!base) throw std::invalid_argument("heapsort: null base"); + if (count > std::numeric_limits::max() / size) throw std::overflow_error("heapsort: array size overflow"); + + auto* const bytes = static_cast(base); + const auto siftDown = [bytes, size, compare](size_t root, size_t n) { + size_t current = root; + while (2 * current + 1 < n) { + size_t child = 2 * current + 1; + if (child + 1 < n && compare(bytes + (child + 1) * size, bytes + child * size) > 0) { + child = child + 1; + } + if (compare(bytes + child * size, bytes + current * size) <= 0) { + break; + } + SwapElements(bytes + current * size, bytes + child * size, size); + current = child; + } + }; + + for (size_t i = count / 2; i > 0; --i) { + siftDown(i - 1, count); + } + for (size_t i = count - 1; i > 0; --i) { + SwapElements(bytes, bytes + i * size, size); + siftDown(0, i); + } + + return 0; +} + void APS5_VABI qsort_nid_postfix(void* base, size_t count, size_t size, int (APS5_VABI *compare)(const void*, const void*)) { if (!compare) throw std::invalid_argument("qsort: null comparator"); if (size == 0) throw std::invalid_argument("qsort: zero element size"); diff --git a/core/libs/tests/GuestQsort.cpp b/core/libs/tests/GuestQsort.cpp index 00e7f72ef2..53e368003e 100644 --- a/core/libs/tests/GuestQsort.cpp +++ b/core/libs/tests/GuestQsort.cpp @@ -210,6 +210,26 @@ bool CheckMergesort() { if (!correct) std::fprintf(stderr, "mergesort failed\n"); return correct; } + +bool CheckHeapsort() { + bool correct = true; + for (const std::size_t count : {0, 1, 2, 3, 7, 16, 33, 1000}) { + std::vector elements; + for (std::size_t index = 0; index < count; ++index) elements.push_back({static_cast((index * 7919) % 5), static_cast(index)}); + correct &= heapsort_nid_postfix(elements.data(), count, sizeof(Element), CompareKeys) == 0; + for (std::size_t index = 1; index < count; ++index) { + const auto& before = elements[index - 1]; + const auto& after = elements[index]; + correct &= before.key <= after.key; + } + } + unsigned char bytes[3] = {3, 1, 2}; + *__error_nid_postfix() = 0; + correct &= heapsort_nid_postfix(bytes, 3, 0, CompareFirstByte) == -1 && *__error_nid_postfix() == 22 && bytes[0] == 3; + correct &= heapsort_nid_postfix(nullptr, 0, 4, CompareKeys) == 0; + if (!correct) std::fprintf(stderr, "heapsort failed\n"); + return correct; +} } int main() { @@ -229,5 +249,6 @@ int main() { for (const std::size_t count : {0, 1, 2, 6, 7, 8, 40, 41, 1000}) correct &= ExpectSorted(size, count); correct &= CheckMergesort(); + correct &= CheckHeapsort(); return correct ? 0 : 1; } From b999ea3c4348e40e0fa738331a250eb6fa4e4ea8 Mon Sep 17 00:00:00 2001 From: iAbdulrrahman Date: Mon, 26 Oct 2026 14:13:29 +0300 Subject: [PATCH 114/198] fix(libc): return success for heapsort with at most one element regardless of size --- core/libs/prx/libc/src/FileAndHeap.cpp | 2 +- core/libs/tests/GuestQsort.cpp | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index 42c79b4255..64082b9b9a 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -753,11 +753,11 @@ int APS5_VABI mergesort_nid_postfix(void* base, size_t count, size_t size, Guest } int APS5_VABI heapsort_nid_postfix(void* base, size_t count, size_t size, GuestCompare compare) { + if (count <= 1) return 0; if (size == 0) { errno = 22; return -1; } - if (count <= 1) return 0; if (!compare) throw std::invalid_argument("heapsort: null comparator"); if (!base) throw std::invalid_argument("heapsort: null base"); if (count > std::numeric_limits::max() / size) throw std::overflow_error("heapsort: array size overflow"); diff --git a/core/libs/tests/GuestQsort.cpp b/core/libs/tests/GuestQsort.cpp index 53e368003e..17a380e5c7 100644 --- a/core/libs/tests/GuestQsort.cpp +++ b/core/libs/tests/GuestQsort.cpp @@ -225,6 +225,9 @@ bool CheckHeapsort() { } unsigned char bytes[3] = {3, 1, 2}; *__error_nid_postfix() = 0; + correct &= heapsort_nid_postfix(nullptr, 0, 0, nullptr) == 0; + unsigned char single = 42; + correct &= heapsort_nid_postfix(&single, 1, 0, nullptr) == 0; correct &= heapsort_nid_postfix(bytes, 3, 0, CompareFirstByte) == -1 && *__error_nid_postfix() == 22 && bytes[0] == 3; correct &= heapsort_nid_postfix(nullptr, 0, 4, CompareKeys) == 0; if (!correct) std::fprintf(stderr, "heapsort failed\n"); From 3fc5c569f0da58fc45004dc971644ea8e5bfe2ee Mon Sep 17 00:00:00 2001 From: iAbdulrrahman Date: Mon, 26 Oct 2026 12:44:33 +0300 Subject: [PATCH 115/198] feat(libSceRtc): implement sceRtcGetCurrentClockUtc and conversion aliases --- core/libs/prx/libSceRtc/Export.cpp | 20 ++++++++++++++++++++ core/libs/prx/libSceRtc/include/Rtc.hpp | 5 +++++ core/libs/tests/GuestRtc.cpp | 8 ++++++++ 3 files changed, 33 insertions(+) diff --git a/core/libs/prx/libSceRtc/Export.cpp b/core/libs/prx/libSceRtc/Export.cpp index 91c6591395..d9603d3827 100644 --- a/core/libs/prx/libSceRtc/Export.cpp +++ b/core/libs/prx/libSceRtc/Export.cpp @@ -424,6 +424,14 @@ int APS5_VABI sceRtcSetTick(RtcDateTime* time, const RtcTick* tick) { return 0; } +int APS5_VABI sceRtcConvertDateTimeToTick(const RtcDateTime* time, RtcTick* tick) { + return sceRtcGetTick(time, tick); +} + +int APS5_VABI sceRtcConvertTickToDateTime(const RtcTick* tick, RtcDateTime* time) { + return sceRtcSetTick(time, tick); +} + int APS5_VABI sceRtcGetCurrentTick(RtcTick* tick) { if (!tick) return SCE_RTC_ERROR_INVALID_POINTER; tick->tick = currentTick(); @@ -453,6 +461,10 @@ int APS5_VABI sceRtcGetCurrentClockLocalTime(RtcDateTime* time) { return 0; } +int APS5_VABI sceRtcGetCurrentClockUtc(RtcDateTime* time) { + return sceRtcGetCurrentClock(time, 0); +} + int APS5_VABI sceRtcConvertUtcToLocalTime(const RtcTick* utc, RtcTick* local_time) { if (!utc || !local_time) return SCE_RTC_ERROR_INVALID_POINTER; if (utc->tick > MAX_TICK) return SCE_RTC_ERROR_INVALID_VALUE; @@ -491,6 +503,14 @@ int APS5_VABI sceRtcSetTime_t(RtcDateTime* time, int64_t seconds) { return 0; } +int APS5_VABI sceRtcConvertDateTimeToTime_t(const RtcDateTime* time, int64_t* seconds) { + return sceRtcGetTime_t(time, seconds); +} + +int APS5_VABI sceRtcConvertTime_tToDateTime(int64_t seconds, RtcDateTime* time) { + return sceRtcSetTime_t(time, seconds); +} + int APS5_VABI sceRtcGetWin32FileTime(const RtcDateTime* time, uint64_t* win32_time) { if (!win32_time) return SCE_RTC_ERROR_INVALID_POINTER; if (const int result = validate(time); result != 0) return result; diff --git a/core/libs/prx/libSceRtc/include/Rtc.hpp b/core/libs/prx/libSceRtc/include/Rtc.hpp index 7e84acf6b7..4fa86be6d1 100644 --- a/core/libs/prx/libSceRtc/include/Rtc.hpp +++ b/core/libs/prx/libSceRtc/include/Rtc.hpp @@ -11,14 +11,19 @@ int APS5_VABI sceRtcGetDayOfWeek(int year, int month, int day); int APS5_VABI sceRtcGetTickResolution(void); int APS5_VABI sceRtcGetTick(const RtcDateTime* time, RtcTick* tick); int APS5_VABI sceRtcSetTick(RtcDateTime* time, const RtcTick* tick); +int APS5_VABI sceRtcConvertDateTimeToTick(const RtcDateTime* time, RtcTick* tick); +int APS5_VABI sceRtcConvertTickToDateTime(const RtcTick* tick, RtcDateTime* time); int APS5_VABI sceRtcGetCurrentTick(RtcTick* tick); int APS5_VABI sceRtcGetCurrentNetworkTick(RtcTick* tick); int APS5_VABI sceRtcGetCurrentClock(RtcDateTime* time, int timeZoneMinutes); int APS5_VABI sceRtcGetCurrentClockLocalTime(RtcDateTime* time); +int APS5_VABI sceRtcGetCurrentClockUtc(RtcDateTime* time); int APS5_VABI sceRtcConvertUtcToLocalTime(const RtcTick* utc, RtcTick* localTime); int APS5_VABI sceRtcConvertLocalTimeToUtc(const RtcTick* localTime, RtcTick* utc); int APS5_VABI sceRtcGetTime_t(const RtcDateTime* time, int64_t* seconds); int APS5_VABI sceRtcSetTime_t(RtcDateTime* time, int64_t seconds); +int APS5_VABI sceRtcConvertDateTimeToTime_t(const RtcDateTime* time, int64_t* seconds); +int APS5_VABI sceRtcConvertTime_tToDateTime(int64_t seconds, RtcDateTime* time); int APS5_VABI sceRtcGetWin32FileTime(const RtcDateTime* time, uint64_t* win32Time); int APS5_VABI sceRtcSetWin32FileTime(RtcDateTime* time, uint64_t win32Time); int APS5_VABI sceRtcGetDosTime(const RtcDateTime* time, uint32_t* dosTime); diff --git a/core/libs/tests/GuestRtc.cpp b/core/libs/tests/GuestRtc.cpp index 8d9b302b49..c2253cf7e5 100644 --- a/core/libs/tests/GuestRtc.cpp +++ b/core/libs/tests/GuestRtc.cpp @@ -139,6 +139,10 @@ int main(int argc, char** argv) { Require(sceRtcSetTick(&converted, &tick) == 0 && Equal(converted, RtcDateTime{9999, 12, 31, 23, 59, 59, 999999})); tick.tick = maxTick + 1; Require(sceRtcSetTick(&converted, &tick) == 0 && Equal(converted, RtcDateTime{10000, 1, 1, 0, 0, 0, 0})); + RtcTick convertedTick{}; + Require(sceRtcConvertDateTimeToTick(&leapDay, &convertedTick) == 0 && convertedTick.tick == leapDayTick); + RtcDateTime fromConvertedTick{}; + Require(sceRtcConvertTickToDateTime(&tick, &fromConvertedTick) == 0 && Equal(fromConvertedTick, RtcDateTime{10000, 1, 1, 0, 0, 0, 0})); Require(sceRtcGetCurrentClock(nullptr, 0) == invalidPointer); Require(sceRtcGetCurrentClock(&converted, 0) == 0 && sceRtcCheckValid(&converted) == 0); @@ -146,6 +150,8 @@ int main(int argc, char** argv) { Require(sceRtcGetCurrentClock(&converted, -300) == 0 && sceRtcCheckValid(&converted) == 0); Require(sceRtcGetCurrentClockLocalTime(nullptr) == invalidPointer); Require(sceRtcGetCurrentClockLocalTime(&converted) == 0 && sceRtcCheckValid(&converted) == 0); + Require(sceRtcGetCurrentClockUtc(nullptr) == invalidPointer); + Require(sceRtcGetCurrentClockUtc(&converted) == 0 && sceRtcCheckValid(&converted) == 0); RtcDateTime epoch{1970, 1, 1, 0, 0, 0, 0}; std::int64_t seconds = -1; @@ -153,6 +159,8 @@ int main(int argc, char** argv) { RtcDateTime millennium{2000, 1, 1, 0, 0, 0, 0}; Require(sceRtcGetTime_t(&millennium, &seconds) == 0 && seconds == 946684800); Require(sceRtcSetTime_t(&converted, 946684800) == 0 && Equal(converted, millennium)); + Require(sceRtcConvertDateTimeToTime_t(&millennium, &seconds) == 0 && seconds == 946684800); + Require(sceRtcConvertTime_tToDateTime(946684800, &converted) == 0 && Equal(converted, millennium)); Require(sceRtcSetTime_t(&converted, -1) == 0 && Equal(converted, RtcDateTime{2106, 2, 7, 6, 28, 15, 0})); Require(sceRtcSetTime_t(&converted, 0x100000001ll) == 0 && Equal(converted, RtcDateTime{1970, 1, 1, 0, 0, 1, 0})); Require(sceRtcSetTime_t(&converted, -0x80000000ll) == 0 && Equal(converted, RtcDateTime{2038, 1, 19, 3, 14, 8, 0})); From 7d86f79ebe42d5751435d9802a9a556b9a8ca7b8 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Mon, 26 Oct 2026 05:04:51 -0700 Subject: [PATCH 116/198] fix(libSceFont): follow the module's frame order and leave vertical glyph renders unimplemented BunnyFl ran firmware 13.42's libSceFont natively against this PR's checks. With both set, the surface's style frame wins over the argument, so it is now taken first. A frame that sets no scale, zeroed or only initialised, is accepted and keeps the glyph's generation scale instead of returning SCE_FONT_ERROR_INVALID_PARAMETER. A valid sceFontGlyphRenderImageVertical call, and sceFontGlyphRenderImage on a vertical font, throw again: the module draws the char call's pixels there but reports a different advance and stride (40/40 against 90/90 at 40 px), and one case doesn't name the rule. The test drops the vertical comparison and the non-renderer pointer check, whose result in the module depends on memory past the pointer, and checks the frame order and the unscaled frames. --- core/libs/prx/libSceFont/src/Render.cpp | 26 +++++++++++------------ core/libs/tests/GuestSystemFont.cpp | 28 +++++++++++++++++++------ docs/dev/debt/libSceFont.md | 2 +- 3 files changed, 35 insertions(+), 21 deletions(-) diff --git a/core/libs/prx/libSceFont/src/Render.cpp b/core/libs/prx/libSceFont/src/Render.cpp index fd2f3918b5..af88e20e49 100644 --- a/core/libs/prx/libSceFont/src/Render.cpp +++ b/core/libs/prx/libSceFont/src/Render.cpp @@ -13,6 +13,7 @@ #include FT_FREETYPE_H #include "prx/libSceFont/include/FontInternal.hpp" +#include "prx/libc/include/General.hpp" namespace { @@ -129,8 +130,8 @@ int RenderGlyph(FontGlyph glyph, const FontStyleFrame* styleFrame, FontRenderer auto* generated = TryGetGeneratedGlyph(glyph); if (!generated) return fail(SCE_FONT_ERROR_INVALID_GLYPH); if (!renderer || static_cast(renderer)->magic != RENDERER_MAGIC) return fail(SCE_FONT_ERROR_INVALID_RENDERER); - if (styleFrame && !ValidStyleFrame(styleFrame)) return fail(SCE_FONT_ERROR_INVALID_PARAMETER); if (!surface || !metrics || !result) return fail(SCE_FONT_ERROR_INVALID_PARAMETER); + if (direction == 2) NotImplemented_nid_no_patch("sceFontGlyphRenderImageVertical"); const FontHandle owner = generated->owner; auto* font = TryGetState(owner) ? GetNativeFont(owner) : nullptr; std::uint32_t fontLock = 0; @@ -139,25 +140,22 @@ int RenderGlyph(FontGlyph glyph, const FontStyleFrame* styleFrame, FontRenderer glyphStyle.scale_w = generated->glyph.scale_x; glyphStyle.scale_h = generated->glyph.base_scale; StyleStateBlock framed{}; - const StyleStateBlock* style = ApplyStyleFrame(&glyphStyle, styleFrame ? styleFrame : SurfaceStyleFrame(surface), framed); - float xUsed = x; + const FontStyleFrame* surfaceFrame = SurfaceStyleFrame(surface); + const StyleStateBlock* style = ApplyStyleFrame(&glyphStyle, surfaceFrame ? surfaceFrame : styleFrame, framed); float yUsed = y; int rc = SCE_FONT_OK; if (direction == 0) { - const bool horizontal = static_cast(font->flags) >= 0; - if (horizontal) { - std::uint8_t layout[HORIZONTAL_LAYOUT_SIZE] = {}; - rc = ComputeHorizontalLayout(owner, style, layout); - yUsed = y + LoadFloat(layout, HORIZONTAL_BASELINE); - } else { - std::uint8_t layout[VERTICAL_LAYOUT_SIZE] = {}; - rc = ComputeVerticalLayout(owner, style, layout); - xUsed = x + LoadFloat(layout, VERTICAL_BASELINE_OFFSET_X); + if (static_cast(font->flags) < 0) { + ReleaseFontLock(font, fontLock); + NotImplemented_nid_no_patch("sceFontGlyphRenderImage: a vertical font"); } - direction = horizontal ? 1 : 2; + std::uint8_t layout[HORIZONTAL_LAYOUT_SIZE] = {}; + rc = ComputeHorizontalLayout(owner, style, layout); + yUsed = y + LoadFloat(layout, HORIZONTAL_BASELINE); + direction = 1; } CachedStyleSetDirectionWord(font->cached_style, direction); - if (rc == SCE_FONT_OK) rc = RenderCodeWithStyle(owner, generated->codepoint, style, surface, xUsed, yUsed, metrics, result); + if (rc == SCE_FONT_OK) rc = RenderCodeWithStyle(owner, generated->codepoint, style, surface, x, yUsed, metrics, result); ReleaseFontLock(font, fontLock); if (rc != SCE_FONT_OK) ClearRenderOutputs(metrics, result); return rc; diff --git a/core/libs/tests/GuestSystemFont.cpp b/core/libs/tests/GuestSystemFont.cpp index b09ed91645..d12bebc887 100644 --- a/core/libs/tests/GuestSystemFont.cpp +++ b/core/libs/tests/GuestSystemFont.cpp @@ -229,8 +229,6 @@ static void CheckGlyphImages(FontHandle font, FontRenderer renderer) { const auto horizontal = RenderInto([&](FontRenderSurface* surface, FontGlyphMetrics* metrics, FontRenderOutput* result) { return sceFontRenderCharGlyphImageHorizontal(font, 'A', surface, 10.0f, 100.0f, metrics, result); }); Require(SameImage(horizontal, RenderInto([&](FontRenderSurface* surface, FontGlyphMetrics* metrics, FontRenderOutput* result) { return sceFontGlyphRenderImageHorizontal(glyph, nullptr, renderer, surface, 10.0f, 100.0f, metrics, result); }))); - const auto vertical = RenderInto([&](FontRenderSurface* surface, FontGlyphMetrics* metrics, FontRenderOutput* result) { return sceFontRenderCharGlyphImageVertical(font, 'A', surface, 60.0f, 10.0f, metrics, result); }); - Require(SameImage(vertical, RenderInto([&](FontRenderSurface* surface, FontGlyphMetrics* metrics, FontRenderOutput* result) { return sceFontGlyphRenderImageVertical(glyph, nullptr, renderer, surface, 60.0f, 10.0f, metrics, result); }))); const auto written = RenderInto([&](FontRenderSurface* surface, FontGlyphMetrics* metrics, FontRenderOutput* result) { return sceFontRenderCharGlyphImage(font, 'A', surface, 10.0f, 20.0f, metrics, result); }); Require(SameImage(written, RenderInto([&](FontRenderSurface* surface, FontGlyphMetrics* metrics, FontRenderOutput* result) { return sceFontGlyphRenderImage(glyph, nullptr, renderer, surface, 10.0f, 20.0f, metrics, result); }))); @@ -246,6 +244,28 @@ static void CheckGlyphImages(FontHandle font, FontRenderer renderer) { Require(sceFontRenderSurfaceSetStyleFrame(surface, &frame) == SCE_FONT_OK); return sceFontGlyphRenderImageHorizontal(glyph, nullptr, renderer, surface, 10.0f, 100.0f, metrics, result); }))); + FontStyleFrame larger{}; + Require(sceFontStyleFrameInit(&larger) == SCE_FONT_OK && sceFontStyleFrameSetScalePixel(&larger, 30.0f, 30.0f) == SCE_FONT_OK); + const auto surfaceLarger = RenderInto([&](FontRenderSurface* surface, FontGlyphMetrics* metrics, FontRenderOutput* result) { + Require(sceFontRenderSurfaceSetStyleFrame(surface, &larger) == SCE_FONT_OK); + return sceFontGlyphRenderImageHorizontal(glyph, nullptr, renderer, surface, 10.0f, 100.0f, metrics, result); + }); + Require(surfaceLarger.rc == SCE_FONT_OK && surfaceLarger.pixels != smaller.pixels && surfaceLarger.pixels != horizontal.pixels); + Require(SameImage(surfaceLarger, RenderInto([&](FontRenderSurface* surface, FontGlyphMetrics* metrics, FontRenderOutput* result) { + Require(sceFontRenderSurfaceSetStyleFrame(surface, &larger) == SCE_FONT_OK); + return sceFontGlyphRenderImageHorizontal(glyph, &frame, renderer, surface, 10.0f, 100.0f, metrics, result); + }))); + Require(SameImage(smaller, RenderInto([&](FontRenderSurface* surface, FontGlyphMetrics* metrics, FontRenderOutput* result) { + Require(sceFontRenderSurfaceSetStyleFrame(surface, &frame) == SCE_FONT_OK); + return sceFontGlyphRenderImageHorizontal(glyph, &larger, renderer, surface, 10.0f, 100.0f, metrics, result); + }))); + FontStyleFrame notAFrame{}; + FontStyleFrame initialized{}; + Require(sceFontStyleFrameInit(&initialized) == SCE_FONT_OK); + for (const FontStyleFrame* unscaled : {¬AFrame, &initialized}) { + Require(SameImage(horizontal, RenderInto([&](FontRenderSurface* surface, FontGlyphMetrics* metrics, FontRenderOutput* result) { return sceFontGlyphRenderImageHorizontal(glyph, unscaled, renderer, surface, 10.0f, 100.0f, metrics, result); }))); + Require(SameImage(written, RenderInto([&](FontRenderSurface* surface, FontGlyphMetrics* metrics, FontRenderOutput* result) { return sceFontGlyphRenderImage(glyph, unscaled, renderer, surface, 10.0f, 20.0f, metrics, result); }))); + } Require(sceFontUnbindRenderer(font) == SCE_FONT_OK); Require(RenderInto([&](FontRenderSurface* surface, FontGlyphMetrics* metrics, FontRenderOutput* result) { return sceFontRenderCharGlyphImageHorizontal(font, 'A', surface, 10.0f, 100.0f, metrics, result); }).rc == SCE_FONT_ERROR_NOT_BOUND_RENDERER); @@ -256,8 +276,6 @@ static void CheckGlyphImages(FontHandle font, FontRenderer renderer) { std::memset(result, 0xA5, sizeof(*result)); }; FontGlyphOpaque notAGlyph{}; - FontStyleFrame notAFrame{}; - std::uint64_t notARenderer = 0; for (const FontGlyph wrong : {static_cast(nullptr), ¬AGlyph}) { for (const auto render : {sceFontGlyphRenderImage, sceFontGlyphRenderImageHorizontal, sceFontGlyphRenderImageVertical}) { Require(Cleared(RenderInto([&](FontRenderSurface* surface, FontGlyphMetrics* metrics, FontRenderOutput* result) { filled(metrics, result); return render(wrong, nullptr, renderer, surface, 10.0f, 100.0f, metrics, result); }), SCE_FONT_ERROR_INVALID_GLYPH)); @@ -268,8 +286,6 @@ static void CheckGlyphImages(FontHandle font, FontRenderer renderer) { Require(Cleared(RenderInto([&](FontRenderSurface* surface, FontGlyphMetrics*, FontRenderOutput* result) { std::memset(result, 0xA5, sizeof(*result)); return render(glyph, nullptr, renderer, surface, 10.0f, 100.0f, nullptr, result); }), SCE_FONT_ERROR_INVALID_PARAMETER)); Require(Cleared(RenderInto([&](FontRenderSurface* surface, FontGlyphMetrics* metrics, FontRenderOutput*) { std::memset(metrics, 0xA5, sizeof(*metrics)); return render(glyph, nullptr, renderer, surface, 10.0f, 100.0f, metrics, nullptr); }), SCE_FONT_ERROR_INVALID_PARAMETER)); Require(Cleared(RenderInto([&](FontRenderSurface* surface, FontGlyphMetrics* metrics, FontRenderOutput* result) { filled(metrics, result); return render(glyph, nullptr, nullptr, surface, 10.0f, 100.0f, metrics, result); }), SCE_FONT_ERROR_INVALID_RENDERER)); - Require(Cleared(RenderInto([&](FontRenderSurface* surface, FontGlyphMetrics* metrics, FontRenderOutput* result) { filled(metrics, result); return render(glyph, nullptr, ¬ARenderer, surface, 10.0f, 100.0f, metrics, result); }), SCE_FONT_ERROR_INVALID_RENDERER)); - Require(Cleared(RenderInto([&](FontRenderSurface* surface, FontGlyphMetrics* metrics, FontRenderOutput* result) { filled(metrics, result); return render(glyph, ¬AFrame, renderer, surface, 10.0f, 100.0f, metrics, result); }), SCE_FONT_ERROR_INVALID_PARAMETER)); } Require(sceFontDeleteGlyph(nullptr, &glyph) == SCE_FONT_OK && glyph == nullptr); } diff --git a/docs/dev/debt/libSceFont.md b/docs/dev/debt/libSceFont.md index ffdbf25290..303f048718 100644 --- a/docs/dev/debt/libSceFont.md +++ b/docs/dev/debt/libSceFont.md @@ -18,7 +18,7 @@ - [sceFontCharactersRefersTextCodes, sceFontTextCodesStepNext and sceFontTextCodesStepBack](../../../core/libs/prx/libSceFont/src/Text.cpp) (libSceFont) - the signatures follow the [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json#L679) IDA type database and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Font/SceFont.Engine.cs#L395-L406), and the 64-byte `FontTextCodes` with the order pointer at 0 and the code at 8 follows SharpProspero's [SceFontTextCodes](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Font/SceFontStructs.cs#L216-L228). The traversal follows the PS4 11.00 `libSceFont.sprx` disassembly posted in [#4050](https://github.com/boykopovar/AnyPS5/pull/4050#issuecomment-6114504202), not a PS5 measurement: the cursor keeps the current, start and term characters at +0x28, +0x30 and +0x38; `Refers` never compares the character with the term and returns null for a clustered or synthetic character; `StepNext` returns null at the term after visiting it; `StepBack` returns null when the current character is the start; both steps skip synthetic characters. Assumed and not covered by the report, which says `StepNext` returns null only at the term: `StepNext` also returns null at the end of the chain (for example with a null term), and `StepBack` at the head of the chain. A null cursor or output returns null as in [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/d2413fc2ebd91d1b7234197c2dd5bd48e1cb6a51/src/libs/libFont.cpp#L1696-L1740); a null text character throws, as its console result is unknown - [sceFontDefineAttribute, sceFontGetAttribute](../../../core/libs/prx/libSceFont/src/Style.cpp), [sceFontGlyphDefineAttribute, sceFontGlyphGetAttribute](../../../core/libs/prx/libSceFont/src/Render.cpp) (libSceFont) - the signatures `(handle, int attribute, int* oldAttribute)` and `(handle, int attribute, int* nowAttribute)`, and the same shape for a glyph, follow the [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFont.Engine.cs) bindings, matching the IDA type database of [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/d2413fc2ebd91d1b7234197c2dd5bd48e1cb6a51/src/libs/libFont.cpp#L1249-L1264), not confirmed on a title. Every handle and every glyph keeps one attribute, an opaque int whose meaning and valid values are unknown: define stores the new value and returns the previous one, and get returns the stored value with its `attribute` argument unused, as in KytyPS5. An attribute never set is 0. The null checks and the invalid-object errors follow the other libSceFont functions and diverge from [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/d2413fc2ebd91d1b7234197c2dd5bd48e1cb6a51/src/libs/libFont.cpp#L1251-L1265), which returns `OK` for a null handle or glyph after writing 0 to the old-attribute pointer: every other handle and glyph entry point in this library reports the invalid object instead, and a title that passes one is already failing. shadPS4 leaves both exports unimplemented, so there is no second implementation to compare. PPSA01325 calls the glyph define with 0x11 on a glyph from `sceFontGenerateCharGlyph` that it deletes unused and draws with `sceFontRenderCharGlyphImageHorizontal`; the three bindings all read the third argument as an out pointer, not a value, so this call sets the glyph's attribute to 0x11. The attribute changes neither shaping nor rendering. -- [sceFontGlyphRenderImage, sceFontGlyphRenderImageHorizontal, sceFontGlyphRenderImageVertical](../../../core/libs/prx/libSceFont/src/Render.cpp) (libSceFont) - unchecked on a console or a title, and [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/font/font.cpp) only stubs them. They draw the glyph's character from its owner font the way `sceFontRenderCharGlyphImage` and its directional forms do, at the scale the glyph was generated with, as `sceFontGlyphRefersOutline` uses it. The renderer argument stands in for the bound renderer and is checked as in `sceFontBindRenderer`; the style frame argument, else the surface's frame, is applied as a surface frame is in `sceFontRenderCharGlyphImage`, and an invalid one returns `SCE_FONT_ERROR_INVALID_PARAMETER`. Assumed: the argument frame wins over the surface's, the error order, `SCE_FONT_ERROR_INVALID_FONT_HANDLE` once the owner font is closed, and the generic form taking its direction from the owner font +- [sceFontGlyphRenderImage, sceFontGlyphRenderImageHorizontal, sceFontGlyphRenderImageVertical](../../../core/libs/prx/libSceFont/src/Render.cpp) (libSceFont) - follow firmware 13.42's module as BunnyFl ran it natively ([comment](https://github.com/boykopovar/AnyPS5/pull/10892#issuecomment-6415399922); one font, 'A', one pixel format, not a console): the glyph's character is drawn as `sceFontRenderCharGlyphImage` and its directional forms draw it, at the glyph's generation scale; the surface's style frame wins over the argument, and a frame that sets no scale keeps that scale. A valid `sceFontGlyphRenderImageVertical` call, and `sceFontGlyphRenderImage` on a vertical font, throw: the module reports advance and stride 40/40 there where the char call gives 90/90 at 40 px, and one case doesn't name the rule. A pointer that isn't a renderer, and `SCE_FONT_ERROR_INVALID_FONT_HANDLE` for a closed owner font, are not measured ### Functional From 44c6919b6420301adcd48f43a9e16c29c3fb09b5 Mon Sep 17 00:00:00 2001 From: Emanuele <118837721+imellag@users.noreply.github.com> Date: Mon, 26 Oct 2026 13:20:10 +0100 Subject: [PATCH 117/198] test(agc): expect the ring slice offset for the replaced moved-word descriptor Co-authored-by: SeaSunPT <24361469+SeaSunPT@users.noreply.github.com> --- core/libs/prx/libSceAgcDriver/tests/Recorder.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 7a8cc7bd50..f13433bd39 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -4235,8 +4235,10 @@ void drawDescriptorWriteTests(const Device& device, Recorder& recorder) { Require(actual.buffers.size() == original.buffers.size() && actual.images.size() == original.images.size(), "snapshot update omitted unchanged descriptors"); for (const auto& [key, before] : original.buffers) { const auto after = actual.buffers.at(key); - const auto expected = key.first == (allocation == 0 ? 2u : 7u) ? draw->snapshots.front().buffer->Handle() : before.buffer; - Require(after.buffer == expected && after.offset == before.offset && after.range == before.range, "snapshot update replaced the wrong buffer or changed its range"); + const bool replaced = key.first == (allocation == 0 ? 2u : 7u); + const auto expected = replaced ? draw->snapshots.front().buffer->Handle() : before.buffer; + const auto expectedOffset = replaced ? draw->snapshots.front().offset : before.offset; + Require(after.buffer == expected && after.offset == expectedOffset && after.range == before.range, "snapshot update replaced the wrong buffer or changed its range"); } for (const auto& [key, before] : original.images) { const auto after = actual.images.at(key); From 56047ebb9734172d7b8e9917b72337d15325c13d Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Mon, 26 Oct 2026 05:35:55 -0700 Subject: [PATCH 118/198] fix(libSceBluetoothHid): refuse a null parameter block as the module does BunnyFl ran firmware 13.42's libSceBluetoothHid natively. A null block given to sceBluetoothHidParamInitialize returns 0x81540001, so it now returns InvalidArgument instead of throwing, and the test checks that answer. The run also shows sceBluetoothHidRegisterCallback reading the event thread's priority and affinity through the stored value, which makes it the BluetoothHidThreadParam pointer the signature declares; the debt entry cites the run and its limits. --- core/libs/prx/libSceBluetoothHid/Export.cpp | 2 +- core/libs/tests/GuestBluetoothHid.cpp | 8 +------- docs/dev/debt/libSceBluetoothHid.md | 2 +- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceBluetoothHid/Export.cpp b/core/libs/prx/libSceBluetoothHid/Export.cpp index ba64feb919..61bcb98b50 100644 --- a/core/libs/prx/libSceBluetoothHid/Export.cpp +++ b/core/libs/prx/libSceBluetoothHid/Export.cpp @@ -119,7 +119,7 @@ int APS5_VABI sceBluetoothHidInterruptOutput(std::uint64_t deviceHandle, std::ui } int APS5_VABI sceBluetoothHidParamInitialize(BluetoothHidParam* param, const BluetoothHidThreadParam* threadParam) { - if (param == nullptr) throw std::invalid_argument("sceBluetoothHidParamInitialize: no source gives the answer for a null parameter block"); + if (param == nullptr) return InvalidArgument; param->threadParam = threadParam; param->reserved = 0; return 0; diff --git a/core/libs/tests/GuestBluetoothHid.cpp b/core/libs/tests/GuestBluetoothHid.cpp index 26a6ca3028..a4f33b185d 100644 --- a/core/libs/tests/GuestBluetoothHid.cpp +++ b/core/libs/tests/GuestBluetoothHid.cpp @@ -109,13 +109,7 @@ int main() { Require(bytes[12] == 0xa5 && bytes[13] == 0xa5 && bytes[14] == 0xa5 && bytes[15] == 0xa5, "bytes 12 to 15 of the parameter block changed"); std::memset(&filled, 0x5a, sizeof(filled)); Require(sceBluetoothHidParamInitialize(&filled, nullptr) == 0 && filled.threadParam == nullptr && filled.reserved == 0 && filled.padding == 0x5a5a5a5au, "a null thread parameter was not stored as given"); - threw = false; - try { - sceBluetoothHidParamInitialize(nullptr, &thread); - } catch (const std::invalid_argument&) { - threw = true; - } - Require(threw, "a null parameter block did not throw"); + Require(sceBluetoothHidParamInitialize(nullptr, &thread) == invalidArgument, "a null parameter block was not refused"); std::puts("BluetoothHid tests passed"); return 0; } diff --git a/docs/dev/debt/libSceBluetoothHid.md b/docs/dev/debt/libSceBluetoothHid.md index ebde90f39a..83461210f3 100644 --- a/docs/dev/debt/libSceBluetoothHid.md +++ b/docs/dev/debt/libSceBluetoothHid.md @@ -7,5 +7,5 @@ ### Unknown function info - [sceBluetoothHidGetDeviceName and sceBluetoothHidGetDeviceInfo](../../../core/libs/prx/libSceBluetoothHid/Export.cpp): output pointers are forwarded to device ioctls without accessing the buffers. Their output sizes and layouts remain unresolved. Report APIs use 64-bit device handles; device-info and disconnect APIs use 32-bit device IDs. -- [sceBluetoothHidParamInitialize](../../../core/libs/prx/libSceBluetoothHid/Export.cpp) follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Bluetooth/SceBluetoothHid.cs#L21-L49), whose signatures were recovered from the PS5 module: it writes the thread-parameter pointer at offset 0 of the 16-byte block, clears the 32-bit word at offset 8 and returns 0. The source names no other byte, so bytes 12 to 15 keep what the caller left there. A null block throws, as no source gives its answer. Not measured on a PS5 +- [sceBluetoothHidParamInitialize](../../../core/libs/prx/libSceBluetoothHid/Export.cpp) follows firmware 13.42's module as BunnyFl ran it natively ([comment](https://github.com/boykopovar/AnyPS5/pull/11124#issuecomment-6416053242); not a console, and neither the kernel side of `/dev/bt` nor the event thread's work is covered): it writes its second argument at offset 0 of the 16-byte block, clears the 32-bit word at offset 8, leaves bytes 12 to 15 and returns 0, and a null block returns `0x81540001`. [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Bluetooth/SceBluetoothHid.cs#L21-L49) types that argument as a 64-bit value; it is the address of a `BluetoothHidThreadParam`, since `sceBluetoothHidRegisterCallback` reads the event thread's priority and affinity through it. The same run covers `sceBluetoothHidThreadParamInitialize`, still unimplemented here - [sceBluetoothHidDebugGetVersion](../../../core/libs/prx/libSceBluetoothHid/Export.cpp): returns a constant without reading arguments, so the supplied implementation does not establish whether the public prototype has unused parameters. From a5128c9be7f987c2e4e817427a5240f649c1c30f Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Mon, 26 Oct 2026 05:41:11 -0700 Subject: [PATCH 119/198] fix(libScePad): follow the module's light bar resets, handle checks and forced vibration BunnyFl ran firmware 13.42's libScePad natively over a slot table he wrote, and oneandonlydean asked for the four calls to follow it. scePadIsValidHandle matches the handle's open port type against the argument, and answers false before scePadInit. scePadResetLightBarAllByPortType returns INVALID_ARG for any type but 0, 1 and 2 before the initialisation check, and otherwise resets each open handle of that type as scePadResetLightBar resets it; scePadResetLightBarAll is type 0. So a standard port with no open handle keeps its light bar, and types 3, 4 and 16 no longer throw. scePadSetVibrationForce returns NOT_INITIALIZED before scePadInit, then INVALID_ARG for a null parameter, without looking the handle up. There is no pad driver here: a special handle returns PAD_OK and a standard handle sets its port's motors, open or not. A handle with no pad port throws, as the driver's answer for it is not measured. The tests drop the closed and unknown handle checks that no longer hold and check the measured answers; the debt entries cite the run and say which parts are this model's. --- core/libs/prx/libScePad/Export.cpp | 36 ++++++++++++++++--------- core/libs/tests/GuestPad.cpp | 43 ++++++++++++++++-------------- docs/dev/debt/libScePad.md | 6 ++--- 3 files changed, 49 insertions(+), 36 deletions(-) diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index 480723f26e..8b22459680 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -83,6 +83,13 @@ bool RemoteHandle(int handle) { return g_remoteOpened && handle == PAD_REMOTE_HANDLE; } +int OpenPortType(int handle) { + if (SpecialHandle(handle)) return PAD_PORT_TYPE_SPECIAL; + if (RemoteHandle(handle)) return PAD_PORT_TYPE_REMOTE; + const int port = PortOf(handle); + return port >= 0 && g_opened[port] ? PAD_PORT_TYPE_STANDARD : -1; +} + int PortForUser(int userId, int type, int index) { if (index != 0) return -1; if (type == PAD_PORT_TYPE_STANDARD) { @@ -614,7 +621,13 @@ int APS5_VABI scePadShareOutputDataWithFunctionType(std::int32_t source, std::in } int APS5_VABI scePadSetVibrationForce(std::int32_t handle, const PadVibrationParam* param) { - return scePadSetVibration(handle, param); + if (!Pad::IsInitialized()) return PAD_ERROR_NOT_INITIALIZED; + if (param == nullptr) return PAD_ERROR_INVALID_ARG; + if (SpecialHandle(handle)) return PAD_OK; + const int port = PortOf(handle); + if (port < 0) NotImplemented_nid_no_patch("scePadSetVibrationForce: a handle with no pad port"); + Pad::SetVibration(port, param->large_motor, param->small_motor); + return PAD_OK; } int APS5_VABI scePadGetVersionInfo(std::int32_t handle, PadVersionInfo* info) { @@ -913,16 +926,16 @@ int APS5_VABI scePadGetInfoByPortType(std::int32_t type, PadPortInfo* info) { } int APS5_VABI scePadResetLightBarAll(void) { - for (int port = 0; port < PAD_MAX_CONTROLLERS; ++port) Pad::SetLightBar(port, false, 0, 0, 0); - return PAD_OK; + return scePadResetLightBarAllByPortType(PAD_PORT_TYPE_STANDARD); } int APS5_VABI scePadResetLightBarAllByPortType(std::uint32_t portType) { - if (portType == 3 || portType == 4 || portType == PAD_PORT_TYPE_REMOTE) - NotImplemented_nid_no_patch("scePadResetLightBarAllByPortType: port types 3, 4 and 16"); - if (portType == PAD_PORT_TYPE_SPECIAL) return PAD_OK; - if (portType != PAD_PORT_TYPE_STANDARD) return PAD_ERROR_INVALID_ARG; - return scePadResetLightBarAll(); + if (portType != PAD_PORT_TYPE_STANDARD && portType != 1 && portType != PAD_PORT_TYPE_SPECIAL) return PAD_ERROR_INVALID_ARG; + if (!Pad::IsInitialized()) return PAD_OK; + const int type = static_cast(portType); + for (int port = 0; port < PAD_MAX_CONTROLLERS; ++port) if (OpenPortType(PAD_HANDLE_BASE + port) == type) scePadResetLightBar(PAD_HANDLE_BASE + port); + for (int slot = 0; slot < PAD_SPECIAL_HANDLE_COUNT; ++slot) if (OpenPortType(PAD_SPECIAL_HANDLE + slot) == type) scePadResetLightBar(PAD_SPECIAL_HANDLE + slot); + return PAD_OK; } int APS5_VABI scePadSetLightBarForTracker(std::int32_t handle, const PadLightBarParam* color) { @@ -966,11 +979,8 @@ int APS5_VABI scePadGetSphereRadius(std::int32_t handle, float* radius) { } bool APS5_VABI scePadIsValidHandle(std::int32_t handle, std::int32_t portType) { - if (portType == PAD_PORT_TYPE_SPECIAL) return SpecialHandle(handle); - if (portType == PAD_PORT_TYPE_REMOTE) return RemoteHandle(handle); - if (portType != PAD_PORT_TYPE_STANDARD) return false; - const int port = PortOf(handle); - return port >= 0 && g_opened[port]; + const int type = OpenPortType(handle); + return Pad::IsInitialized() && type >= 0 && type == portType; } bool APS5_VABI scePadIsDS4Connected(std::int32_t handle) { diff --git a/core/libs/tests/GuestPad.cpp b/core/libs/tests/GuestPad.cpp index 7ebbb256a0..76e0cfaadf 100644 --- a/core/libs/tests/GuestPad.cpp +++ b/core/libs/tests/GuestPad.cpp @@ -8,7 +8,6 @@ #include #include #include -#include #include #include #include @@ -456,24 +455,26 @@ static bool LightBarIs(int port, bool valid, std::uint8_t r, std::uint8_t g, std return output.lightBarValid == valid && output.lightBar[0] == r && output.lightBar[1] == g && output.lightBar[2] == b; } -static void CheckLightBarResets(int handle) { +static void CheckLightBarResets(int user, int handle) { const PadLightBarParam color{10, 20, 30}; const auto setBoth = [&] { Require(scePadSetLightBar(handle, &color) == PAD_OK); Pad::SetLightBar(2, true, 40, 50, 60); Require(LightBarIs(0, true, 10, 20, 30) && LightBarIs(2, true, 40, 50, 60)); }; + const auto openPortReset = [] { return LightBarIs(0, false, 0, 0, 0) && LightBarIs(2, true, 40, 50, 60); }; setBoth(); - Require(scePadResetLightBarAll() == PAD_OK); - for (int port = 0; port < PAD_MAX_CONTROLLERS; ++port) Require(LightBarIs(port, false, 0, 0, 0)); - Require(scePadResetLightBarAll() == PAD_OK); - Require(LightBarIs(0, false, 0, 0, 0)); + Require(scePadResetLightBarAll() == PAD_OK && openPortReset()); + Require(scePadResetLightBarAll() == PAD_OK && openPortReset()); setBoth(); - Require(scePadResetLightBarAllByPortType(PAD_PORT_TYPE_SPECIAL) == PAD_OK); - for (const std::uint32_t type : {1u, 5u, 6u, 0xffu, 0xffffffffu}) Require(scePadResetLightBarAllByPortType(type) == PAD_ERROR_INVALID_ARG); + const int special = scePadOpen_nid_postfix(user, PAD_PORT_TYPE_SPECIAL, 0, nullptr); + Require(special >= PAD_SPECIAL_HANDLE); + for (const std::uint32_t type : {1u, 2u}) Require(scePadResetLightBarAllByPortType(type) == PAD_OK); + Require(scePadClose_nid_postfix(special) == PAD_OK); + for (const std::uint32_t type : {3u, 4u, 5u, 6u, 16u, 0xffu, 0xffffffffu}) Require(scePadResetLightBarAllByPortType(type) == PAD_ERROR_INVALID_ARG); Require(LightBarIs(0, true, 10, 20, 30) && LightBarIs(2, true, 40, 50, 60)); - Require(scePadResetLightBarAllByPortType(PAD_PORT_TYPE_STANDARD) == PAD_OK); - for (int port = 0; port < PAD_MAX_CONTROLLERS; ++port) Require(LightBarIs(port, false, 0, 0, 0)); + Require(scePadResetLightBarAllByPortType(PAD_PORT_TYPE_STANDARD) == PAD_OK && openPortReset()); + Pad::SetLightBar(2, false, 0, 0, 0); } static bool VibrationIs(int port, std::uint8_t large, std::uint8_t small) { @@ -488,14 +489,12 @@ static void CheckVibrationForce(int user, int handle) { Require(scePadSetVibration(handle, &weak) == PAD_OK && VibrationIs(0, 5, 6)); Require(scePadSetVibrationForce(handle, &weak) == scePadSetVibration(handle, &weak) && VibrationIs(0, 5, 6)); Require(scePadSetVibrationForce(handle, nullptr) == PAD_ERROR_INVALID_ARG && VibrationIs(0, 5, 6)); - Require(scePadSetVibrationForce(handle + 1, &strong) == PAD_ERROR_INVALID_HANDLE && VibrationIs(0, 5, 6)); - Require(scePadSetVibrationForce(-1, &strong) == PAD_ERROR_INVALID_HANDLE); + for (const int other : {handle + 1, 0x999, -1}) Require(scePadSetVibrationForce(other, nullptr) == PAD_ERROR_INVALID_ARG); const int special = scePadOpen_nid_postfix(user, PAD_PORT_TYPE_SPECIAL, 0, nullptr); Require(special >= PAD_SPECIAL_HANDLE); Require(scePadSetVibrationForce(special, &strong) == PAD_OK && VibrationIs(0, 5, 6)); Require(scePadSetVibrationForce(special, nullptr) == PAD_ERROR_INVALID_ARG); Require(scePadClose_nid_postfix(special) == PAD_OK); - Require(scePadSetVibrationForce(special, &strong) == PAD_ERROR_INVALID_HANDLE); const PadVibrationParam off{0, 0}; Require(scePadSetVibrationForce(handle, &off) == PAD_OK && VibrationIs(0, 0, 0)); } @@ -624,6 +623,13 @@ int main() { Require(scePadGetControllerType(PAD_HANDLE, &controllerType) == PAD_ERROR_NOT_INITIALIZED); Require(controllerType == -1); Require(scePadUnknown_n3kSX62fgNo() == static_cast(0x80920005)); + Require(!scePadIsValidHandle(PAD_HANDLE, PAD_PORT_TYPE_STANDARD)); + const PadVibrationParam earlyVibration{1, 2}; + for (const PadVibrationParam* param : {static_cast(nullptr), &earlyVibration}) Require(scePadSetVibrationForce(PAD_HANDLE, param) == PAD_ERROR_NOT_INITIALIZED); + Pad::SetLightBar(0, true, 1, 2, 3); + Require(scePadResetLightBarAll() == PAD_OK && LightBarIs(0, true, 1, 2, 3)); + Require(scePadResetLightBarAllByPortType(3) == PAD_ERROR_INVALID_ARG && scePadResetLightBarAllByPortType(PAD_PORT_TYPE_STANDARD) == PAD_OK && LightBarIs(0, true, 1, 2, 3)); + Pad::SetLightBar(0, false, 0, 0, 0); const auto path = std::filesystem::temp_directory_path() / ("anyps5-pad-state-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); struct Cleanup { std::filesystem::path path; @@ -677,6 +683,7 @@ int main() { Require(scePadGetHandle(user, 0, 0) == noHandle); const int earlySpecial = scePadOpen_nid_postfix(user, PAD_PORT_TYPE_SPECIAL, 0, nullptr); Require(earlySpecial > 0); + Require(!scePadIsValidHandle(earlySpecial, PAD_PORT_TYPE_SPECIAL)); Require(scePadGetHandle(user, 0, 0) == noHandle); Require(scePadGetHandle(user, PAD_PORT_TYPE_SPECIAL, 0) == earlySpecial); Require(scePadClose_nid_postfix(earlySpecial) == PAD_OK); @@ -710,7 +717,7 @@ int main() { CheckTrackerDevices(handle); CheckAccessibilityControllerState(handle); CheckIsValidHandle(user, handle); - CheckLightBarResets(handle); + CheckLightBarResets(user, handle); CheckVibrationForce(user, handle); CheckControllerType(handle); CheckInjectedState(commandPath); @@ -738,12 +745,8 @@ int main() { Require(scePadGetHandle(user, 0, 0) == noHandle); Require(!scePadIsValidHandle(handle, PAD_PORT_TYPE_STANDARD)); Require(scePadSetLightBar(handle, &lightBar) == PAD_OK && LightBarIs(0, true, 255, 128, 64)); - Require(scePadResetLightBarAll() == PAD_OK && LightBarIs(0, false, 0, 0, 0)); - const PadVibrationParam closedVibration{9, 10}; - Require(scePadSetVibrationForce(handle, &closedVibration) == scePadSetVibration(handle, &closedVibration) && VibrationIs(0, 9, 10)); - Require(scePadSetVibrationForce(handle + 1, &closedVibration) == PAD_ERROR_INVALID_HANDLE); - const PadVibrationParam closedOff{0, 0}; - Require(scePadSetVibrationForce(handle, &closedOff) == PAD_OK && VibrationIs(0, 0, 0)); + Require(scePadResetLightBarAll() == PAD_OK && LightBarIs(0, true, 255, 128, 64)); + Require(scePadResetLightBar(handle) == PAD_OK && LightBarIs(0, false, 0, 0, 0)); Require(scePadUnknown_n3kSX62fgNo() == PAD_OK); Require(scePadSetVibrationTriggerEffectWeakWhileEmbeddedMicInUse(true) == 0); Require(scePadSetAngularVelocityDeadbandState(handle, false) == 0); diff --git a/docs/dev/debt/libScePad.md b/docs/dev/debt/libScePad.md index 3bde917520..ea93c9c8bd 100644 --- a/docs/dev/debt/libScePad.md +++ b/docs/dev/debt/libScePad.md @@ -22,9 +22,9 @@ - [scePadSetVibrationMode](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - modes 1 to 3 and 5 are accepted and stored. On a PS5 (firmware 13.60) the call returns `0x80920005` before `scePadInit`, then rejects a mode of 0 or below with `INVALID_ARG` before it checks the handle, and returns `INVALID_HANDLE` for a bad handle with any higher mode; whether an open handle accepts modes above 3 was not measured, since the payload could not open a pad; 5 is accepted because PPSA25286 passes it on its open handle at start-up; the meanings of modes 3 and 5 are unknown (PPSA21480 calls it with 3 right after `scePadOpen` and aborts on failure; [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Pad/Pad.cs) names only 1 and 2). The stored mode has no other effect - [scePadIsRemoteController](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the name hashes to NID `fCWdlnmB1Ks`; the signature `(int handle, bool* remote)` comes from PPSA16064, which zeroes a byte, passes its address after `scePadGetControllerInformation` and ignores the result. The error codes follow the other pad calls. Controllers are local devices, so the answer is always false - [scePadIsDS4Connected, scePadIsBlasterConnected and scePadIsMoveConnected](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the `bool (int handle)` signatures are recovered; the library reports one DualSense to every title (see Touchpad resolution below), so no DualShock 4, Blaster or Move controller is ever behind a handle and each call answers false for any handle. The console's answer for an invalid handle is not measured -- [scePadIsValidHandle](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the `bool (int handle, int portType)` signature is recovered, and the answer follows the handle table `scePadGetHandle` reads. A standard handle is valid while its port is open, a special handle while its user's slot is open, and the remote handle while the system user's remote port is open, each only for its own port type (0, 2 or 16); any other port type answers false. A closed standard handle is not valid here, although the light bar, vibration and other setters keep accepting it. The console's answers are not measured -- [scePadResetLightBarAll and scePadResetLightBarAllByPortType](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the `(void)` and `(uint32_t portType)` signatures are recovered. Each resets every standard port's light bar to its default, as `scePadResetLightBar` does for one handle, whether or not a handle is open on it. Port type 2 returns `PAD_OK` without effect, as the special handles' light bar calls do. Types 3, 4 and 16 throw: `scePadOpen` cannot open ports of types 3 and 4, and the remote port has no light bar here. Any other type returns `INVALID_ARG`, as `scePadOpen` does. The console's answers are not measured -- [scePadSetVibrationForce](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the `(int handle, const PadVibrationParam*)` signature is recovered. The call returns what `scePadSetVibration` returns for the same arguments and sets the same motors. Its name suggests it overrides a system vibration-off setting, which is not modelled here (`scePadSetVibrationMode` only stores its mode), so there is nothing to override. Whether the console differs in any other way is not measured +- [scePadIsValidHandle](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the `bool (int handle, int portType)` signature is recovered. Firmware 13.42's module, as BunnyFl ran it natively over a slot table he wrote ([comment](https://github.com/boykopovar/AnyPS5/pull/10880#issuecomment-6415854351); the driver emulated, the table not built by `scePadOpen`), answers 1 only when a slot holds the handle with exactly that port type, and 0 while not initialised. Here it is the same match on each open handle's type: 0 for a standard handle while its port is open, 2 for a special handle while its user's slot is open, 16 for the remote handle while the system user's remote port is open, and no match before `scePadInit`. `scePadOpen` opens no type-1 handle here, so no handle matches type 1. A closed standard handle is not valid, although the light bar, vibration and other setters keep accepting it +- [scePadResetLightBarAll and scePadResetLightBarAllByPortType](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the `(void)` and `(uint32_t portType)` signatures are recovered, and both follow firmware 13.42's module as BunnyFl ran it natively ([comment](https://github.com/boykopovar/AnyPS5/pull/10880#issuecomment-6415854351); the driver emulated, port type 1 unknown, the table not built by `scePadOpen`). By port type, any type but 0, 1 and 2 returns `INVALID_ARG` before the initialisation check (3, 4, 16 and -1 measured). Otherwise each open handle of that type is reset as `scePadResetLightBar` resets it, and the call returns `PAD_OK`, also before `scePadInit`, when it does nothing; `scePadResetLightBarAll` is type 0. In this model a special handle's reset has no effect, as special handles have no light bar, and `scePadOpen` opens no type-1 handle +- [scePadSetVibrationForce](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the `(int handle, const PadVibrationParam*)` signature is recovered. Firmware 13.42's module, as BunnyFl ran it natively ([comment](https://github.com/boykopovar/AnyPS5/pull/10880#issuecomment-6415854351); the driver emulated), returns `0x80920005` before `scePadInit`, even for a null `param`, then `INVALID_ARG` for a null `param`; otherwise it sends `scePadSetVibration`'s request with a force byte set, without looking the handle up, and returns the driver's answer. There is no pad driver here: a special handle returns `PAD_OK` without effect, as with `scePadSetVibration`, and a standard handle sets its pad port's motors whether or not it is still open. A handle with no pad port throws, as the driver's answer for it is not measured. In this model the force byte has no effect, as there is no system vibration-off setting to override (`scePadSetVibrationMode` only stores its mode) - [scePadSetAngularVelocityBiasCorrectionState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - inferred signature `(int handle, bool enabled)` follows `scePadSetTiltCorrectionState` and `scePadSetAngularVelocityDeadbandState`; an invalid handle returns `PAD_ERROR_INVALID_HANDLE`, disabling is a no-op, and enabling throws because bias correction is not modelled - [scePadGetAccessibilityControllerState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the value a console reports for a DualSense is not measured; the emulated pad, which is not an Access controller, reports 0. The name hashes to NID `zBsgF0q8DIM`. In the OS 10.01 system module the `(int handle, uint32_t* state)` function returns `0x80920005` before `scePadInit`, then `INVALID_ARG` for a NULL `state` and `INVALID_HANDLE` for a bad handle, and otherwise asks the pad driver for a per-device value of the device behind the handle: 0 when no device is behind it, 1 when the driver has no answer, else the driver's value. What the values mean is unknown - [scePadGetControllerType](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the value a console reports for a DualSense is not measured; the emulated pad reports `PAD_CONTROLLER_TYPE_DUAL_SENSE` (2), following [duaLib](https://github.com/WujekFoliarz/duaLib/blob/master/src/include/duaLib.h)'s `s_SceControllerType` enum where `DUALSENSE = 2`. The signature `(int handle, int32_t* type)` follows duaLib, while the validation order (checking initialization, null pointer, then handle) follows the library's other pad queries; duaLib does not validate null pointers and returns `PAD_ERROR_DEVICE_NOT_CONNECTED` for an invalid pad. From b09af0dc734dcb9317fe665c64fc3062413be2b5 Mon Sep 17 00:00:00 2001 From: Federico Marras Date: Mon, 26 Oct 2026 14:29:51 +0100 Subject: [PATCH 120/198] test(libSceNgs2.native): stop requiring the unparsed waveform routes to throw --- .../libs/prx/libSceNgs2.native/tests/Ngs2Parse.cpp | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Parse.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Parse.cpp index 4db92c83a2..ddc412134c 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Parse.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Parse.cpp @@ -108,20 +108,9 @@ static void TestParsePcm() { Require(sceNgs2ParseWaveformData(noChannels.data(), noChannels.size(), &info) == SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT); } -static bool ParseThrowsFormat(const std::vector& file) { - Ngs2WaveformInfo info{}; - try { - sceNgs2ParseWaveformData(file.data(), file.size(), &info); - } catch (const std::runtime_error&) { - return true; - } - return false; -} - static void TestParseUnknownFormats() { const std::uint8_t pcmGuid[16] = {0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71}; const std::uint8_t unknownGuid[16] = {0xaa, 0xbb, 0xcc, 0xdd, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc}; - const std::uint8_t type43Guid[16] = {0x1c, 0xe0, 0x10, 0x9e, 0xcc, 0x8b, 0xd9, 0x47, 0x8f, 0x3b, 0xe3, 0x0b, 0x8b, 0xdc, 0x35, 0x29}; Ngs2WaveformInfo info{}; for (const std::uint32_t tag : {0x0u, 0x2u, 0x11u, 0x55u, 0x165u, 0xffffu}) { const auto wave = PcmFile(1, 44100, 16, 10, tag); @@ -137,9 +126,6 @@ static void TestParseUnknownFormats() { } const auto pcm = ExtensibleFile(0x34, pcmGuid); Require(sceNgs2ParseWaveformData(pcm.data(), pcm.size(), &info) == SCE_NGS2_OK && info.format.waveform_type == SCE_NGS2_WAVEFORM_TYPE_PCM_I16L && info.num_samples == 10); - Require(ParseThrowsFormat(ExtensibleFile(0x28, type43Guid))); - Require(ParseThrowsFormat(ExtensibleFile(0x34, type43Guid))); - Require(ParseThrowsFormat(ExtensibleFile(0x34, unknownGuid))); const auto wave = PcmFile(1, 44100, 16, 10, 0x55); const char* const guestPath = "/aps5_ngs2_parse_unknown.wav"; From c48ed84d47746c3586e7ff04551628b405ec7492 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Sun, 25 Oct 2026 15:21:45 -0400 Subject: [PATCH 121/198] fix(libc): preserve modf decomposition and floating-point status --- core/libs/prx/libc/src/Math.cpp | 39 ++++++++++- core/libs/tests/GuestMath.cpp | 118 ++++++++++++++++++++++++++++++++ docs/dev/debt/libc.md | 1 + 3 files changed, 156 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index d04aed2822..05b515e6ec 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -1338,8 +1338,43 @@ double APS5_VABI nan_nid_postfix(const char* tagp) { return std::bit_cast(0x7ff8000000000000ull); } -double APS5_VABI modf_nid_postfix(double x, double* integral) { return std::modf(x, integral); } -float APS5_VABI modff_nid_postfix(float x, float* integral) { return std::modf(x, integral); } +double APS5_VABI modf_nid_postfix(double x, double* integral) { + const auto bits = std::bit_cast(x); + const auto exponent = (bits >> 52u) & 0x7ffull; + const auto zero = std::bit_cast(bits & 0x8000000000000000ull); + if (exponent < 1023u) { + *integral = zero; + return x; + } + if (exponent >= 1075u) { + *integral = x; + return exponent == 0x7ffu && (bits & 0xfffffffffffffull) != 0 ? x : zero; + } + const auto mask = (std::uint64_t{1} << (1075u - exponent)) - 1u; + *integral = std::bit_cast(bits & ~mask); + return (bits & mask) == 0 ? zero : x - *integral; +} +float APS5_VABI modff_nid_postfix(float x, float* integral) { + const auto bits = std::bit_cast(x); + const auto exponent = (bits >> 23u) & 0xffu; + const auto zero = std::bit_cast(bits & 0x80000000u); + if (exponent < 127u) { + *integral = zero; + return x; + } + if (exponent >= 150u) { + *integral = x; + if (exponent == 0xffu && (bits & 0x7fffffu) != 0) { + volatile int unordered = _mm_ucomieq_ss(_mm_set_ss(x), _mm_setzero_ps()); + (void)unordered; + return x; + } + return zero; + } + const auto mask = (std::uint32_t{1} << (150u - exponent)) - 1u; + *integral = std::bit_cast(bits & ~mask); + return (bits & mask) == 0 ? zero : x - *integral; +} double APS5_VABI tanh_nid_postfix(double x) { static volatile double tiny = 1.0e-300; const double huge = 1.0e300; diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index f9cffe9b6e..3f2a1e064b 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -172,6 +172,123 @@ static void CheckFegetenv() { Require(threw); } +template +struct ModfCase { Bits input, fraction, integral; unsigned flags; }; + +template +static void CheckModfCases(const ModfCase (&cases)[Count], Call call) { + std::fenv_t saved; + std::fegetenv(&saved); + const auto savedCsr = _mm_getcsr(); + int* error = __error_nid_postfix(); + const int savedError = *error; + const int rounding[] = {FE_TONEAREST, FE_DOWNWARD, FE_UPWARD, FE_TOWARDZERO}; + for (unsigned mode = 0; mode < 4; ++mode) { + for (unsigned denorm = 0; denorm < 4; ++denorm) { + const unsigned control = 0x1f80u | (mode << 13u) | ((denorm & 1u) != 0 ? 0x40u : 0u) | ((denorm & 2u) != 0 ? 0x8000u : 0u); + for (const unsigned sticky : {0u, 5u, 32u}) { + for (const auto& item : cases) { + std::fesetenv(FE_DFL_ENV); + std::fesetround(rounding[mode]); + std::uint32_t environment[7]; + asm volatile("fnstenv %0" : "=m"(environment)); + environment[1] = (environment[1] & ~63u) | sticky; + asm volatile("fldenv %0" : : "m"(environment)); + _mm_setcsr(control | sticky); + const auto beforeControl = X87ControlWord(); + *error = 97; + Float integral = 77; + const auto fraction = std::bit_cast(call(std::bit_cast(item.input), &integral)); + const auto integralBits = std::bit_cast(integral); + const auto csr = _mm_getcsr(); + const auto x87Status = X87StatusWord(); + const auto x87Control = X87ControlWord(); + if (fraction != item.fraction || integralBits != item.integral || csr != (control | sticky | item.flags) || (x87Status & 63u) != sticky || x87Control != beforeControl || *error != 97) { + std::fprintf(stderr, "modf width=%zu input=%llx round=%u denorm=%u sticky=%u fraction=%llx integral=%llx csr=%x x87=%x\n", sizeof(Float), static_cast(item.input), mode, denorm, sticky, static_cast(fraction), static_cast(integralBits), csr, x87Status); + Require(false); + } + } + } + } + } + std::fesetenv(&saved); + _mm_setcsr(savedCsr); + *error = savedError; +} + +static void CheckModfDecomposition() { + constexpr ModfCase doubles[] = { + {0x00000000ull, 0x00000000ull, 0x0000000000000000ull, 0u}, + {0x8000000000000000ull, 0x8000000000000000ull, 0x8000000000000000ull, 0u}, + {0x00000001ull, 0x00000001ull, 0x0000000000000000ull, 0u}, + {0x8000000000000001ull, 0x8000000000000001ull, 0x8000000000000000ull, 0u}, + {0xfffffffffffffull, 0xfffffffffffffull, 0x0000000000000000ull, 0u}, + {0x800fffffffffffffull, 0x800fffffffffffffull, 0x8000000000000000ull, 0u}, + {0x10000000000000ull, 0x10000000000000ull, 0x0000000000000000ull, 0u}, + {0x8010000000000000ull, 0x8010000000000000ull, 0x8000000000000000ull, 0u}, + {0x3fe0000000000000ull, 0x3fe0000000000000ull, 0x0000000000000000ull, 0u}, + {0x3ff0000000000000ull, 0x00000000ull, 0x3ff0000000000000ull, 0u}, + {0xbff0000000000000ull, 0x8000000000000000ull, 0xbff0000000000000ull, 0u}, + {0x4028000000000000ull, 0x00000000ull, 0x4028000000000000ull, 0u}, + {0x7fefffffffffffffull, 0x00000000ull, 0x7fefffffffffffffull, 0u}, + {0xffefffffffffffffull, 0x8000000000000000ull, 0xffefffffffffffffull, 0u}, + {0x7ff0000000000000ull, 0x00000000ull, 0x7ff0000000000000ull, 0u}, + {0xfff0000000000000ull, 0x8000000000000000ull, 0xfff0000000000000ull, 0u}, + {0x7ff0000000000001ull, 0x7ff0000000000001ull, 0x7ff0000000000001ull, 0u}, + {0xfff0000000000001ull, 0xfff0000000000001ull, 0xfff0000000000001ull, 0u}, + {0x7ff8000000001234ull, 0x7ff8000000001234ull, 0x7ff8000000001234ull, 0u}, + {0xfff8000000001234ull, 0xfff8000000001234ull, 0xfff8000000001234ull, 0u}, + {0x3ff4000000000000ull, 0x3fd0000000000000ull, 0x3ff0000000000000ull, 0u}, + {0xbff4000000000000ull, 0xbfd0000000000000ull, 0xbff0000000000000ull, 0u}, + {0x3ff8000000000000ull, 0x3fe0000000000000ull, 0x3ff0000000000000ull, 0u}, + {0xbff8000000000000ull, 0xbfe0000000000000ull, 0xbff0000000000000ull, 0u}, + {0x4006000000000000ull, 0x3fe8000000000000ull, 0x4000000000000000ull, 0u}, + {0xc006000000000000ull, 0xbfe8000000000000ull, 0xc000000000000000ull, 0u}, + {0x3fe8000000000000ull, 0x3fe8000000000000ull, 0x0000000000000000ull, 0u}, + {0xbfe8000000000000ull, 0xbfe8000000000000ull, 0x8000000000000000ull, 0u}, + {0x3fffffffffffffffull, 0x3feffffffffffffeull, 0x3ff0000000000000ull, 0u}, + {0xbfffffffffffffffull, 0xbfeffffffffffffeull, 0xbff0000000000000ull, 0u}, + {0x4320000000000001ull, 0x3fe0000000000000ull, 0x4320000000000000ull, 0u}, + {0xc320000000000001ull, 0xbfe0000000000000ull, 0xc320000000000000ull, 0u}, + }; + constexpr ModfCase floats[] = { + {0x00000000u, 0x00000000u, 0x00000000u, 0u}, + {0x80000000u, 0x80000000u, 0x80000000u, 0u}, + {0x00000001u, 0x00000001u, 0x00000000u, 0u}, + {0x80000001u, 0x80000001u, 0x80000000u, 0u}, + {0x007fffffu, 0x007fffffu, 0x00000000u, 0u}, + {0x807fffffu, 0x807fffffu, 0x80000000u, 0u}, + {0x00800000u, 0x00800000u, 0x00000000u, 0u}, + {0x80800000u, 0x80800000u, 0x80000000u, 0u}, + {0x3f000000u, 0x3f000000u, 0x00000000u, 0u}, + {0x3f800000u, 0x00000000u, 0x3f800000u, 0u}, + {0xbf800000u, 0x80000000u, 0xbf800000u, 0u}, + {0x41400000u, 0x00000000u, 0x41400000u, 0u}, + {0x7f7fffffu, 0x00000000u, 0x7f7fffffu, 0u}, + {0xff7fffffu, 0x80000000u, 0xff7fffffu, 0u}, + {0x7f800000u, 0x00000000u, 0x7f800000u, 0u}, + {0xff800000u, 0x80000000u, 0xff800000u, 0u}, + {0x7f800001u, 0x7f800001u, 0x7f800001u, 1u}, + {0xff800001u, 0xff800001u, 0xff800001u, 1u}, + {0x7fc01234u, 0x7fc01234u, 0x7fc01234u, 0u}, + {0xffc01234u, 0xffc01234u, 0xffc01234u, 0u}, + {0x3fa00000u, 0x3e800000u, 0x3f800000u, 0u}, + {0xbfa00000u, 0xbe800000u, 0xbf800000u, 0u}, + {0x3fc00000u, 0x3f000000u, 0x3f800000u, 0u}, + {0xbfc00000u, 0xbf000000u, 0xbf800000u, 0u}, + {0x40300000u, 0x3f400000u, 0x40000000u, 0u}, + {0xc0300000u, 0xbf400000u, 0xc0000000u, 0u}, + {0x3f400000u, 0x3f400000u, 0x00000000u, 0u}, + {0xbf400000u, 0xbf400000u, 0x80000000u, 0u}, + {0x3fffffffu, 0x3f7ffffeu, 0x3f800000u, 0u}, + {0xbfffffffu, 0xbf7ffffeu, 0xbf800000u, 0u}, + {0x4a800001u, 0x3f000000u, 0x4a800000u, 0u}, + {0xca800001u, 0xbf000000u, 0xca800000u, 0u}, + }; + CheckModfCases(doubles, modf_nid_postfix); + CheckModfCases(floats, modff_nid_postfix); +} + static bool ExtendedBits(long double value, std::uint64_t mantissa, std::uint16_t signExponent) { unsigned char bytes[sizeof(long double)]{}; std::memcpy(bytes, &value, sizeof(value)); @@ -1704,6 +1821,7 @@ int main() { Require(*__error_nid_postfix() == 34); Require(strtold_nid_postfix("1.0000000000000000001!", &end) > 1.L && *end == '!'); CheckNanConversions(); + CheckModfDecomposition(); Require(fmodf_nid_postfix(5.5f, 2.f) == 1.5f); Require(fmodf_nid_postfix(-5.5f, 2.f) == -1.5f); Require(std::signbit(fmodf_nid_postfix(-4.f, 2.f))); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 6a2be3edf8..9e0f33c69c 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -76,6 +76,7 @@ - [std::thread::hardware_concurrency](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `CHrhwd8QSBs`) returns the host's `std::thread::hardware_concurrency()` (which may be 0 when unknown) instead of the console's processor count; no reference implements it and the value a title expects (PPSA34015 imports it) is unverified. - [fmodf](../../../core/libs/prx/libc/src/Math.cpp) (libc) - console `errno`, exception flags and NaN payloads remain unverified. The wrapper preserves `errno` like the existing double `fmod`, following FreeBSD 11.4 [`e_fmodf.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/e_fmodf.c); host `std::fmod` still supplies the result and exception flags. - [_LCosh and _LSinh](../../../core/libs/prx/libc/src/Math.cpp) (libc) follow the two-argument Dinkumware CRT helpers `y * cosh(x)` and `y * sinh(x)`, including scaled large-finite-input paths; the signatures are confirmed by Dinkumware's public `ymath.h`, but the exact console results, range-error thresholds and floating-point exception behavior are not verified. +- [modf and modff](../../../core/libs/prx/libc/src/Math.cpp) (libc) - native PS5 execution, Windows and unmasked exceptions remain unverified. Linux comparisons with bundled libraries from PPSA03766 and PPSA07923 cover result and integral bits, errno, SSE/x87 flags and control state. Signaling NaNs retain their payload and quiet bit; the measured float path raises SSE invalid while the double path leaves flags unchanged. - [_FSinh and _FCosh](../../../core/libs/prx/libc/src/Math.cpp) (libc) finite results are not verified bit for bit against the console: the scaled product uses the host's binary64 hyperbolic function before rounding to binary32, rather than the float rational and `_FExp` sequence in PRAGMATA's bundled `libc.prx`. NaN preference and payloads, signed zeros, infinite operands and the range-error thresholds follow that binary's instructions; the system `libSceLibcInternal` implementation and nondefault flush-to-zero modes are unverified. Against `libSceLibcInternal.sprx` on a PS5 (firmware 13.60), 12 of 36 measured results differ by one unit in the last place (`_FSinh(0.5, y)` and `_FCosh(1, y)`, `_FCosh(10, y)` for y of 0.5, 1 and 2); zeros, the overflow to infinity at 89 and 90 and the tiny-argument results matched - [_Divide, _FDivide, _Recip and _FRecip](../../../core/libs/prx/libc/src/DinkumwareInternals.cpp) (libc) follow the Dinkumware CRT division and reciprocal contracts with IEEE-754 signed-zero, infinity and NaN behavior. The exact console results, signaling-NaN handling and floating-point exception behavior are not verified. From 8ed27932f26399f4f8646cd057fb684681f2243b Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Mon, 26 Oct 2026 06:59:44 -0700 Subject: [PATCH 122/198] test: stop requiring the four unimplemented content and entitlement calls to throw The tests checked the exact "not implemented" message from sceContentDeleteByPath, sceContentDeleteById, sceNpEntitlementAccessAbortRequest and sceNpEntitlementAccessDeleteRequest. A test that requires a placeholder to throw fails as soon as the call is implemented, so those checks go, with the helper and catch they used. The calls still report themselves as not implemented, as their debt entries say, and the argument checks stay. --- core/libs/tests/GuestContentDelete.cpp | 5 ----- core/libs/tests/GuestEntitlements.cpp | 14 -------------- 2 files changed, 19 deletions(-) diff --git a/core/libs/tests/GuestContentDelete.cpp b/core/libs/tests/GuestContentDelete.cpp index ad6cfc6411..34fb9129d7 100644 --- a/core/libs/tests/GuestContentDelete.cpp +++ b/core/libs/tests/GuestContentDelete.cpp @@ -17,8 +17,6 @@ std::string LogicErrorMessage(TAction action) { return "invalid argument"; } catch (const std::logic_error& error) { return error.what(); - } catch (const std::runtime_error& error) { - return error.what(); } return "returned"; } @@ -42,9 +40,6 @@ int main() { Require(sceContentDeleteByPath("") == static_cast(0x809D5001u)); Require(LogicErrorMessage([] { sceContentDeleteByPath(nullptr); }) == "invalid argument"); - Require(LogicErrorMessage([] { sceContentDeleteByPath("/user/av_contents/photo/a.png"); }) == "sceContentDeleteByPath not implemented"); - Require(LogicErrorMessage([] { sceContentDeleteById(0x123456789); }) == "sceContentDeleteById not implemented"); - Require(LogicErrorMessage([] { sceContentDeleteById(-1); }) == "sceContentDeleteById not implemented"); Require(sceContentDeleteTerminate() == 0); Require(sceContentDeleteTerminate() == static_cast(0x809D5002u)); diff --git a/core/libs/tests/GuestEntitlements.cpp b/core/libs/tests/GuestEntitlements.cpp index 843f3fd89b..6960f9b349 100644 --- a/core/libs/tests/GuestEntitlements.cpp +++ b/core/libs/tests/GuestEntitlements.cpp @@ -14,16 +14,6 @@ static void Require(bool value) { if (!value) std::abort(); } -template -static bool NotImplemented(TCall call, const std::string& name) { - try { - call(); - } catch (const std::runtime_error& error) { - return std::string(error.what()) == name + " not implemented"; - } - return false; -} - static void SetEntitlements(const std::filesystem::path& file) { #ifdef _WIN32 Require(_wputenv_s(L"ANYPS5_ENTITLEMENTS", file.wstring().c_str()) == 0); @@ -60,10 +50,6 @@ int main(int argc, char** argv) { std::puts("Guest entitlements rejected a missing configured file"); return 0; } - for (std::int64_t requestId : {std::int64_t{0}, std::int64_t{1}, INT64_MAX}) { - Require(NotImplemented([requestId] { sceNpEntitlementAccessAbortRequest(requestId); }, "sceNpEntitlementAccessAbortRequest")); - Require(NotImplemented([requestId] { sceNpEntitlementAccessDeleteRequest(requestId); }, "sceNpEntitlementAccessDeleteRequest")); - } std::filesystem::create_directories(directory); { std::ofstream ini(file, std::ios::binary); From c867473002100bb4530d2f60712575ecf1714a54 Mon Sep 17 00:00:00 2001 From: janzezno Date: Sun, 25 Oct 2026 20:51:34 +0100 Subject: [PATCH 123/198] feat(agc): capture and replay frames on Linux --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 34 ++++++-- .../Execution/src/FrameRecorder.cpp | 79 +++++++++++++++++- .../prx/libSceAgcDriver/tests/FrameReplay.cpp | 16 +++- .../tests/FrameReplayState.cpp | 83 ++++++++++++++++++- .../prx/libSceAgcDriver/tools/FrameReplay.cpp | 61 +++++++++++++- .../include/specifics/linux/HostMapping.hpp | 80 ++++++++++++++---- docs/dev/BUILD.md | 2 +- docs/dev/debt/libSceAgcDriver.md | 2 +- 8 files changed, 321 insertions(+), 36 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 29669383a9..0c233403b1 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -3463,7 +3463,7 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() - if(WIN32) + if(WIN32 OR CMAKE_SYSTEM_NAME STREQUAL "Linux") add_test_executable(agc_driver_frame_capture_eop_tests tests/FrameCaptureEop.cpp Execution/src/FrameCapture.cpp) target_include_directories(agc_driver_frame_capture_eop_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR} ${ANYPS5_SPIRV_HEADERS_INCLUDE_DIR}) target_link_libraries(agc_driver_frame_capture_eop_tests PRIVATE libSceAgcDriver libkernel libc libzstd_static) @@ -3473,8 +3473,10 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_frame_capture_eop PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_FRAME_CAPTURE=${CMAKE_CURRENT_BINARY_DIR}/frame-capture-eop;APS5_FRAME_CAPTURE_AT_FLIP=1;APS5_FRAME_CAPTURE_FRAMES=3" TIMEOUT 120) - set_tests_properties(agc_driver_frame_capture_eop PROPERTIES ENVIRONMENT_MODIFICATION - "PATH=path_list_prepend:$") + if(WIN32) + set_tests_properties(agc_driver_frame_capture_eop PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() add_test_executable(agc_driver_frame_replay_state_tests tests/FrameReplayState.cpp Execution/src/FrameCapture.cpp) @@ -3483,9 +3485,15 @@ if(BUILD_TESTING) set_target_properties(agc_driver_frame_replay_state_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_driver_frame_replay_state_tests) set(agcFrameReplayModes order tokens blocking) - if(WIN32) + if(WIN32 OR CMAKE_SYSTEM_NAME STREQUAL "Linux") list(APPEND agcFrameReplayModes zero shader shader_host) endif() + if(WIN32 OR CMAKE_SYSTEM_NAME STREQUAL "Linux") + list(APPEND agcFrameReplayModes unwatched) + endif() + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + list(APPEND agcFrameReplayModes shared_page host_neighbors) + endif() foreach(mode IN LISTS agcFrameReplayModes) set(capture "${CMAKE_CURRENT_BINARY_DIR}/frame-replay-${mode}") add_test(NAME agc_driver_frame_replay_${mode} COMMAND agc_driver_frame_replay_state_tests ${mode} "${capture}") @@ -3494,13 +3502,17 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_frame_replay_${mode} PROPERTIES ENVIRONMENT "APS5_FRAME_CAPTURE=${capture};APS5_FRAME_CAPTURE_AT_FLIP=1;APS5_FRAME_CAPTURE_FRAMES=1") endif() + if(mode STREQUAL "unwatched") + set_tests_properties(agc_driver_frame_replay_${mode} PROPERTIES ENVIRONMENT + "APS5_NO_WRITE_WATCH=1;APS5_FRAME_CAPTURE=${capture};APS5_FRAME_CAPTURE_AT_FLIP=1;APS5_FRAME_CAPTURE_FRAMES=1") + endif() if(WIN32) set_tests_properties(agc_driver_frame_replay_${mode} PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endforeach() - if(WIN32) + if(WIN32 OR CMAKE_SYSTEM_NAME STREQUAL "Linux") add_test_executable(agc_driver_frame_replay_tests tests/FrameReplay.cpp Execution/src/FrameCapture.cpp) target_include_directories(agc_driver_frame_replay_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(agc_driver_frame_replay_tests PRIVATE libSceAgc libSceAgcDriver libkernel libc libzstd_static) @@ -3511,14 +3523,18 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_frame_replay PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_FRAME_CAPTURE=${CMAKE_CURRENT_BINARY_DIR}/frame-replay-capture;APS5_FRAME_CAPTURE_AT_FLIP=1;APS5_FRAME_CAPTURE_FRAMES=2" TIMEOUT 120) - set_tests_properties(agc_driver_frame_replay PROPERTIES ENVIRONMENT_MODIFICATION - "PATH=path_list_prepend:$") + if(WIN32) + set_tests_properties(agc_driver_frame_replay PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test(NAME agc_driver_frame_replay_concurrent COMMAND agc_driver_frame_replay_tests $ concurrent) set_tests_properties(agc_driver_frame_replay_concurrent PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_FRAME_CAPTURE=${CMAKE_CURRENT_BINARY_DIR}/frame-replay-concurrent-capture;APS5_FRAME_CAPTURE_AT_FLIP=1;APS5_FRAME_CAPTURE_FRAMES=8" TIMEOUT 120) - set_tests_properties(agc_driver_frame_replay_concurrent PROPERTIES ENVIRONMENT_MODIFICATION - "PATH=path_list_prepend:$") + if(WIN32) + set_tests_properties(agc_driver_frame_replay_concurrent PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() endif() diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/FrameRecorder.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/FrameRecorder.cpp index cdfdc9afbe..17475384ee 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/FrameRecorder.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/FrameRecorder.cpp @@ -8,6 +8,7 @@ #include "prx/libc/include/General.hpp" #include #include +#include #include #include #include @@ -25,6 +26,10 @@ #else #include #endif +#ifdef __linux__ +#include "prx/libc/include/specifics/linux/HostMapping.hpp" +#include +#endif namespace AgcDriver { namespace { @@ -88,6 +93,39 @@ std::uint64_t flipValue(std::int64_t value) { return static_cast(value); } +#ifdef __linux__ +std::uint32_t windowsProtection(const HostMapping::Mapping& mapping) { + if (mapping.executable) return mapping.writable ? 0x40u : mapping.readable ? 0x20u : 0x10u; + if (mapping.writable) return 0x04u; + return mapping.readable ? 0x02u : 0x01u; +} + +bool pageReadable(std::uint64_t address) { + char byte = 0; + iovec local{&byte, 1}; + iovec remote{reinterpret_cast(static_cast(address)), 1}; + const auto read = process_vm_readv(getpid(), &local, 1, &remote, 1, 0); + if (read == 1) return true; + if (read < 0 && errno == EFAULT) return false; + throw std::runtime_error(std::string("frame capture: cannot probe a file mapping with process_vm_readv: ") + (read < 0 ? std::strerror(errno) : "nothing was read")); +} + +std::uint64_t fileBackedEnd(const HostMapping::Mapping& mapping) { + if (mapping.inode == 0 || !mapping.readable) return mapping.end; + const auto page = static_cast(sysconf(_SC_PAGESIZE)); + if (pageReadable(mapping.end - page)) return mapping.end; + if (!pageReadable(mapping.start)) return mapping.start; + std::uint64_t readable = 0; + std::uint64_t unreadable = (mapping.end - mapping.start) / page - 1; + while (unreadable - readable > 1) { + const auto middle = readable + (unreadable - readable) / 2; + if (pageReadable(mapping.start + middle * page)) readable = middle; + else unreadable = middle; + } + return mapping.start + unreadable * page; +} +#endif + std::uint64_t contentHash(std::span bytes) { std::uint64_t hash = 0xcbf29ce484222325ull; for (const auto value : bytes) hash = (hash ^ static_cast(value)) * 0x100000001b3ull; @@ -478,6 +516,7 @@ void FrameRecorder::start() { } return; } + if (!GuestMemory::WriteWatched()) throw std::runtime_error("frame capture: the guest write watch is unavailable, so CPU stores during the frames cannot be recorded"); startedAt = std::chrono::steady_clock::now(); startBoundary = boundaries; allocationsGeneration = GuestAllocations::GuestAllocationsGeneration_nid_postfix(); @@ -597,8 +636,46 @@ std::vector FrameRecorder::committedRanges() { } } return result; +#elif defined(__linux__) + std::uintptr_t base = 0; + std::size_t size = 0; + GuestArena::GuestArenaRange_nid_postfix(&base, &size); + if (size == 0) throw std::runtime_error("frame capture: the guest arena is not reserved"); + std::vector result; + for (const auto& allocation : GuestAllocations::GuestAllocationsAcquireAll_nid_postfix()) { + if (!allocation->readable) continue; + const auto first = allocation->address; + const auto end = first + allocation->bytes; + const bool host = first < base || end > base + size; + const auto access = FrameRangeAccess::Readable | (allocation->writable ? FrameRangeAccess::Writable : 0u) | (allocation->gpu ? FrameRangeAccess::Gpu : 0u); + HostMapping::Walk(first, end, "frame capture", [&](const HostMapping::Mapping& mapping) { + if (!mapping.readable && !mapping.writable && !mapping.executable) return; + const auto add = [&](std::uint64_t address, std::uint64_t regionEnd, bool readable) { + if (address >= regionEnd) return; + Range range; + range.address = address; + range.bytes = regionEnd - address; + range.protection = windowsProtection(mapping); + range.access = access; + range.readable = readable; + if (host) range.kind = FrameRangeKind::Host; + else if (GuestMemory::Watched(range.address, static_cast(range.bytes))) range.kind = FrameRangeKind::Watched; + else if (!mapping.shared && mapping.inode == 0) range.kind = FrameRangeKind::Unwatched; + else range.kind = FrameRangeKind::Section; + auto& last = result.empty() ? range : result.back(); + if (!result.empty() && last.address + last.bytes == range.address && last.kind == range.kind && last.protection == range.protection && last.readable == range.readable && last.access == range.access) last.bytes += range.bytes; + else result.push_back(range); + }; + const auto address = std::max(first, mapping.start); + const auto regionEnd = std::min(end, mapping.end); + const auto backed = std::clamp(fileBackedEnd(mapping), address, regionEnd); + add(address, backed, mapping.readable); + add(backed, regionEnd, false); + }); + } + return result; #else - throw std::runtime_error("frame capture: guest memory capture is implemented on Windows only"); + throw std::runtime_error("frame capture: guest memory capture is implemented on Windows and Linux only"); #endif } diff --git a/core/libs/prx/libSceAgcDriver/tests/FrameReplay.cpp b/core/libs/prx/libSceAgcDriver/tests/FrameReplay.cpp index 23e6c21513..9b557243eb 100644 --- a/core/libs/prx/libSceAgcDriver/tests/FrameReplay.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/FrameReplay.cpp @@ -5,6 +5,7 @@ #include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" #include "SceTypes.hpp" #include #include @@ -80,6 +81,9 @@ Layout allocate() { auto* const block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(BlockBytes, 65536)); check(block != nullptr, "cannot allocate guest memory"); GuestArena::GuestArenaCommit_nid_postfix(block, BlockBytes, 0x04, BlockBytes); +#ifdef __linux__ + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, BlockBytes); +#endif { GuestAllocations::Mutation mutation; mutation.Add(block, BlockBytes, true, true, true); @@ -184,8 +188,11 @@ void testCaptureAndReplay(const std::filesystem::path& tool) { const auto firstDump = capture / "first.bin"; const auto secondDump = capture / "second.bin"; - const auto command = "\"\"" + tool.string() + "\" \"" + capture.string() + "\" --strict --dump " + std::to_string(at(layout.firstTarget)) + ":" + std::to_string(DataBytes) + ":\"" + firstDump.string() + - "\" --dump " + std::to_string(at(late.firstTarget)) + ":" + std::to_string(DataBytes) + ":\"" + secondDump.string() + "\"\""; + auto command = "\"" + tool.string() + "\" \"" + capture.string() + "\" --strict --dump " + std::to_string(at(layout.firstTarget)) + ":" + std::to_string(DataBytes) + ":\"" + firstDump.string() + + "\" --dump " + std::to_string(at(late.firstTarget)) + ":" + std::to_string(DataBytes) + ":\"" + secondDump.string() + "\""; +#ifdef _WIN32 + command = "\"" + command + "\""; +#endif const auto code = std::system(command.c_str()); check(code == 0, "agc_frame_replay exited with " + std::to_string(code)); check(readFile(firstDump) == first, "the replay did not reproduce the first copy"); @@ -241,7 +248,10 @@ void testConcurrentCopies(const std::filesystem::path& tool) { check(memory != 0, "the capture holds no memory deltas"); const auto dump = capture / "targets.bin"; - const auto command = "\"\"" + tool.string() + "\" \"" + capture.string() + "\" --strict --dump " + std::to_string(at(targets)) + ":" + std::to_string(Copies * DataBytes) + ":\"" + dump.string() + "\"\""; + auto command = "\"" + tool.string() + "\" \"" + capture.string() + "\" --strict --dump " + std::to_string(at(targets)) + ":" + std::to_string(Copies * DataBytes) + ":\"" + dump.string() + "\""; +#ifdef _WIN32 + command = "\"" + command + "\""; +#endif const auto code = std::system(command.c_str()); check(code == 0, "agc_frame_replay exited with " + std::to_string(code)); const auto replayed = readFile(dump); diff --git a/core/libs/prx/libSceAgcDriver/tests/FrameReplayState.cpp b/core/libs/prx/libSceAgcDriver/tests/FrameReplayState.cpp index c54b01466a..29c816ebcd 100644 --- a/core/libs/prx/libSceAgcDriver/tests/FrameReplayState.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/FrameReplayState.cpp @@ -2,12 +2,15 @@ #include "../tools/FrameReplay.cpp" #undef main #include "prx/libSceAgcDriver/Execution/include/FrameRecorder.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" #include namespace { using AgcDriver::FrameCaptureWriter; +constexpr std::uint32_t ReadWrite = 0x04; + void check(bool condition, const char* reason) { if (!condition) throw std::runtime_error(reason); } @@ -20,6 +23,9 @@ std::byte* allocate(std::size_t size, bool gpu = true) { auto* bytes = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(size, 65536)); check(bytes != nullptr, "guest allocation failed"); GuestArena::GuestArenaCommit_nid_postfix(bytes, size, 0x04, size); +#ifdef __linux__ + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(bytes, size); +#endif GuestAllocations::Mutation mutation; mutation.Add(bytes, size, true, true, gpu); return bytes; @@ -61,7 +67,7 @@ void testMemoryOrder(const std::filesystem::path& directory) { } void testReusedMemory(const std::filesystem::path& directory) { -#ifdef _WIN32 +#if defined(_WIN32) || defined(__linux__) constexpr std::size_t Size = 2 * FrameCaptureWriter::BlockBytes; auto* bytes = allocate(Size); auto* cpu = allocate(FrameCaptureWriter::BlockBytes, false); @@ -73,8 +79,8 @@ void testReusedMemory(const std::filesystem::path& directory) { std::size_t arenaSize = 0; GuestArena::GuestArenaRange_nid_postfix(&base, &arenaSize); FrameCaptureWriter writer(directory); - writer.WriteRange(address(bytes), Size, FrameRangeKind::Watched, PAGE_READWRITE, FrameRangeAccess::Readable | FrameRangeAccess::Writable | FrameRangeAccess::Gpu); - writer.WriteRange(address(cpu), FrameCaptureWriter::BlockBytes, FrameRangeKind::Unwatched, PAGE_READWRITE, FrameRangeAccess::Readable | FrameRangeAccess::Writable); + writer.WriteRange(address(bytes), Size, FrameRangeKind::Watched, ReadWrite, FrameRangeAccess::Readable | FrameRangeAccess::Writable | FrameRangeAccess::Gpu); + writer.WriteRange(address(cpu), FrameCaptureWriter::BlockBytes, FrameRangeKind::Unwatched, ReadWrite, FrameRangeAccess::Readable | FrameRangeAccess::Writable); check(writer.WriteSnapshot(address(bytes), expected) == FrameCaptureWriter::BlockBytes, "zero snapshot block was not elided"); writer.Finish("{\"arena_base\":" + std::to_string(base) + "}"); const FrameCaptureReader reader(directory); @@ -85,6 +91,67 @@ void testReusedMemory(const std::filesystem::path& directory) { #endif } +void testSharedPage(const std::filesystem::path& directory) { +#ifdef __linux__ + constexpr std::size_t Page = 65536; + auto* page = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(Page, Page)); + check(page != nullptr, "guest reservation failed"); + GuestArena::GuestArenaRelease_nid_postfix(page, Page); + const std::vector gpu(0x20, std::byte{0x3c}); + const std::vector cpu(0x40, std::byte{0x5a}); + std::uintptr_t base = 0; + std::size_t arenaSize = 0; + GuestArena::GuestArenaRange_nid_postfix(&base, &arenaSize); + FrameCaptureWriter writer(directory); + writer.WriteRange(address(page + 0x20), gpu.size(), FrameRangeKind::Watched, ReadWrite, FrameRangeAccess::Readable | FrameRangeAccess::Writable | FrameRangeAccess::Gpu); + writer.WriteRange(address(page + 0x40), cpu.size(), FrameRangeKind::Unwatched, ReadWrite, FrameRangeAccess::Readable | FrameRangeAccess::Writable); + writer.WriteSnapshot(address(page + 0x20), gpu); + writer.WriteSnapshot(address(page + 0x40), cpu); + writer.Finish("{\"arena_base\":" + std::to_string(base) + "}"); + const FrameCaptureReader reader(directory); + const auto skipped = restoreMemory(reader); + check(skipped.bytes == 0, "a range sharing a page with an earlier one was skipped"); + check(std::equal(gpu.begin(), gpu.end(), page + 0x20) && std::equal(cpu.begin(), cpu.end(), page + 0x40), "ranges sharing a page were not restored"); +#else + static_cast(directory); +#endif +} + +void testHostNeighbors(const std::filesystem::path& directory) { +#ifdef __linux__ + const auto page = static_cast(sysconf(_SC_PAGESIZE)); + void* mapped = mmap(nullptr, 2 * page, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + check(mapped != MAP_FAILED, "host allocation failed"); + auto* occupied = static_cast(mapped); + auto* available = occupied + page; + check(munmap(available, page) == 0, "host page release failed"); + std::uintptr_t base = 0; + std::size_t arenaSize = 0; + GuestArena::GuestArenaRange_nid_postfix(&base, &arenaSize); + FrameCaptureWriter writer(directory); + writer.WriteRange(address(occupied), page, FrameRangeKind::Host, ReadWrite, FrameRangeAccess::Readable | FrameRangeAccess::Writable); + writer.WriteRange(address(available), page, FrameRangeKind::Host, ReadWrite, FrameRangeAccess::Readable | FrameRangeAccess::Writable | FrameRangeAccess::Gpu); + writer.Finish("{\"arena_base\":" + std::to_string(base) + "}"); + const FrameCaptureReader reader(directory); + const auto skipped = restoreMemory(reader); + check(skipped.bytes == page, "an occupied CPU host range was not the only range skipped"); +#else + static_cast(directory); +#endif +} + +void testUnwatched() { + auto* recorder = AgcDriver::FrameRecorder::Active(); + check(recorder != nullptr, "frame recorder is disabled"); + try { + recorder->FrameBoundary(); + } catch (const std::runtime_error& error) { + check(std::string_view(error.what()).find("write watch is unavailable") != std::string_view::npos, "a capture without the write watch failed for another reason"); + return; + } + check(false, "a capture started without the write watch"); +} + void testEopTokens() { Frames frames; auto output = std::make_shared(frames); @@ -170,9 +237,14 @@ void testShaderCode(const std::filesystem::path& directory, bool host) { auto* shader = reinterpret_cast(bytes); auto* code = bytes + (1u << 19) + 256; constexpr std::size_t CodeBytes = 3 * FrameCaptureWriter::BlockBytes; -#ifdef _WIN32 +#if defined(_WIN32) || defined(__linux__) if (host) { +#ifdef _WIN32 auto* memory = static_cast(VirtualAlloc(nullptr, CodeBytes + 4096, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + void* mapped = mmap(nullptr, CodeBytes + 4096, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + auto* memory = mapped != MAP_FAILED ? static_cast(mapped) : nullptr; +#endif check(memory != nullptr, "host shader allocation failed"); GuestAllocations::Mutation mutation; mutation.Add(memory, CodeBytes + 4096, true, true, true); @@ -226,6 +298,9 @@ int main(int argc, char** argv) { const std::filesystem::path directory = argv[2]; if (mode == "order") testMemoryOrder(directory); else if (mode == "zero") testReusedMemory(directory); + else if (mode == "shared_page") testSharedPage(directory); + else if (mode == "host_neighbors") testHostNeighbors(directory); + else if (mode == "unwatched") testUnwatched(); else if (mode == "tokens") testEopTokens(); else if (mode == "blocking") testBlockingSubmission(directory); else if (mode == "shader" || mode == "shader_host") testShaderCode(directory, mode == "shader_host"); diff --git a/core/libs/prx/libSceAgcDriver/tools/FrameReplay.cpp b/core/libs/prx/libSceAgcDriver/tools/FrameReplay.cpp index 943f962b6e..0ddce118db 100644 --- a/core/libs/prx/libSceAgcDriver/tools/FrameReplay.cpp +++ b/core/libs/prx/libSceAgcDriver/tools/FrameReplay.cpp @@ -35,6 +35,11 @@ #ifdef _WIN32 #include #endif +#ifdef __linux__ +#include "prx/libc/include/specifics/linux/HostMapping.hpp" +#include +#include +#endif namespace { @@ -142,6 +147,13 @@ bool committedWritable(std::uint64_t address, std::uint64_t bytes) { at = reinterpret_cast(info.BaseAddress) + info.RegionSize; } return true; +#elif defined(__linux__) + auto cursor = address; + HostMapping::Walk(address, address + bytes, "frame replay", [&](const HostMapping::Mapping& mapping) { + if (mapping.start > cursor || !mapping.readable || !mapping.writable) return; + cursor = std::max(cursor, mapping.end); + }); + return cursor >= address + bytes; #else static_cast(address); static_cast(bytes); @@ -149,6 +161,21 @@ bool committedWritable(std::uint64_t address, std::uint64_t bytes) { #endif } +void commit(const FrameMemoryChunk& chunk) { + auto* const pointer = reinterpret_cast(static_cast(chunk.address)); + const auto bytes = static_cast(chunk.bytes); +#ifdef __linux__ + const auto page = static_cast(sysconf(_SC_PAGESIZE)); + const auto begin = chunk.address & ~(page - 1); + const auto end = (chunk.address + chunk.bytes + page - 1) & ~(page - 1); + if (begin != chunk.address || end != chunk.address + chunk.bytes) { + GuestArena::GuestArenaSetProtection_nid_postfix(static_cast(begin), static_cast(end - begin), 0x04); + return; + } +#endif + GuestArena::GuestArenaCommit_nid_postfix(pointer, bytes, 0x04, bytes); +} + std::pair progress(); struct Skipped { @@ -189,6 +216,31 @@ void mapHostRanges(const FrameCaptureReader& reader, Skipped& skipped) { if ((chunk->access & FrameRangeAccess::Gpu) != 0) throw std::runtime_error("frame replay: cannot map the GPU-visible host range at " + std::to_string(chunk->address)); skipped.Add(*chunk); } +#elif defined(__linux__) + const auto page = static_cast(sysconf(_SC_PAGESIZE)); + std::sort(host.begin(), host.end(), [](const auto* left, const auto* right) { return left->address < right->address; }); + std::vector> envelopes; + for (const auto* chunk : host) { + const auto begin = chunk->address & ~(page - 1); + const auto end = (chunk->address + chunk->bytes + page - 1) & ~(page - 1); + if (!envelopes.empty() && begin < envelopes.back().second) envelopes.back().second = std::max(envelopes.back().second, end); + else envelopes.emplace_back(begin, end); + } + std::vector> refused; + for (const auto& [begin, end] : envelopes) { + auto* const wanted = reinterpret_cast(static_cast(begin)); + const auto bytes = static_cast(end - begin); + void* const placed = mmap(wanted, bytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED_NOREPLACE, -1, 0); + if (placed == wanted) continue; + if (placed != MAP_FAILED) munmap(placed, bytes); + refused.emplace_back(begin, end); + } + for (const auto* chunk : host) { + const bool blocked = std::any_of(refused.begin(), refused.end(), [&](const auto& range) { return chunk->address < range.second && range.first < chunk->address + chunk->bytes; }); + if (!blocked) continue; + if ((chunk->access & FrameRangeAccess::Gpu) != 0) throw std::runtime_error("frame replay: cannot map the GPU-visible host range at " + std::to_string(chunk->address)); + skipped.Add(*chunk); + } #else for (const auto* chunk : host) skipped.Add(*chunk); #endif @@ -202,12 +254,15 @@ Skipped restoreMemory(const FrameCaptureReader& reader) { progress(); Skipped skipped; mapHostRanges(reader, skipped); + std::vector owned; + for (const auto& chunk : reader.Chunks()) owned.push_back(chunk.kind == FrameMemoryKind::Range && chunk.rangeKind != FrameRangeKind::Host && committedWritable(chunk.address, chunk.bytes)); { GuestAllocations::Mutation mutation; - for (const auto& chunk : reader.Chunks()) { + for (std::size_t index = 0; index < reader.Chunks().size(); ++index) { + const auto& chunk = reader.Chunks()[index]; if (chunk.kind != FrameMemoryKind::Range || chunk.rangeKind == FrameRangeKind::Host) continue; const bool gpu = (chunk.access & FrameRangeAccess::Gpu) != 0; - if (committedWritable(chunk.address, chunk.bytes)) { + if (owned[index]) { if (!gpu) skipped.Add(chunk); else std::memset(reinterpret_cast(static_cast(chunk.address)), 0, static_cast(chunk.bytes)); continue; @@ -221,7 +276,7 @@ Skipped restoreMemory(const FrameCaptureReader& reader) { skipped.Add(chunk); continue; } - GuestArena::GuestArenaCommit_nid_postfix(pointer, bytes, 0x04, bytes); + commit(chunk); mutation.Add(pointer, bytes, (chunk.access & FrameRangeAccess::Readable) != 0, (chunk.access & FrameRangeAccess::Writable) != 0, gpu); } } diff --git a/core/libs/prx/libc/include/specifics/linux/HostMapping.hpp b/core/libs/prx/libc/include/specifics/linux/HostMapping.hpp index 11c5e5cb45..7eed449734 100644 --- a/core/libs/prx/libc/include/specifics/linux/HostMapping.hpp +++ b/core/libs/prx/libc/include/specifics/linux/HostMapping.hpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -37,6 +38,8 @@ static_assert(sizeof(procmap_query) == 104); enum : std::uint64_t { PROCMAP_QUERY_VMA_READABLE = 0x01, PROCMAP_QUERY_VMA_WRITABLE = 0x02, + PROCMAP_QUERY_VMA_EXECUTABLE = 0x04, + PROCMAP_QUERY_VMA_SHARED = 0x08, PROCMAP_QUERY_COVERING_OR_NEXT_VMA = 0x10, }; #define PROCMAP_QUERY _IOWR('f', 17, struct procmap_query) @@ -48,6 +51,11 @@ struct Mapping { std::uint64_t start; std::uint64_t end; std::string path; + bool readable = false; + bool writable = false; + bool executable = false; + bool shared = false; + std::uint64_t inode = 0; }; inline int QueryFd() { @@ -65,28 +73,40 @@ inline int QueryFd() { return fd; } +inline std::optional ParseMapsLine(const std::string& line) { + std::uint64_t start = 0; + std::uint64_t end = 0; + char perms[8] = {}; + std::uint64_t offset = 0; + unsigned int devMajor = 0; + unsigned int devMinor = 0; + std::uint64_t inode = 0; + char path[4096] = {}; + const int parsed = std::sscanf(line.c_str(), "%llx-%llx %7s %llx %x:%x %llu %4095s", + (unsigned long long*)&start, (unsigned long long*)&end, perms, + (unsigned long long*)&offset, &devMajor, &devMinor, (unsigned long long*)&inode, path); + if (parsed < 7 || std::strlen(perms) < 4) return std::nullopt; + return Mapping{start, end, parsed >= 8 ? path : "", perms[0] == 'r', perms[1] == 'w', perms[2] == 'x', perms[3] == 's', inode}; +} + inline std::optional ScanMaps(std::uint64_t address, const char* caller) { std::ifstream maps("/proc/self/maps"); if (!maps) throw std::runtime_error(std::string(caller) + ": failed to open /proc/self/maps"); std::string line; while (std::getline(maps, line)) { - std::uint64_t start = 0; - std::uint64_t end = 0; - char perms[8] = {}; - std::uint64_t offset = 0; - unsigned int devMajor = 0; - unsigned int devMinor = 0; - std::uint64_t inode = 0; - char path[4096] = {}; - const int parsed = std::sscanf(line.c_str(), "%llx-%llx %7s %llx %x:%x %llu %4095s", - (unsigned long long*)&start, (unsigned long long*)&end, perms, - (unsigned long long*)&offset, &devMajor, &devMinor, (unsigned long long*)&inode, path); - if (parsed < 7 || address < start || address >= end) continue; - return Mapping{start, end, parsed >= 8 ? path : ""}; + auto mapping = ParseMapsLine(line); + if (!mapping || address < mapping->start || address >= mapping->end) continue; + return mapping; } return std::nullopt; } +inline Mapping FromQuery(const procmap_query& query, const char* name) { + return Mapping{query.vma_start, query.vma_end, query.vma_name_size != 0 ? name : "", (query.vma_flags & PROCMAP_QUERY_VMA_READABLE) != 0, + (query.vma_flags & PROCMAP_QUERY_VMA_WRITABLE) != 0, (query.vma_flags & PROCMAP_QUERY_VMA_EXECUTABLE) != 0, + (query.vma_flags & PROCMAP_QUERY_VMA_SHARED) != 0, query.inode}; +} + inline std::optional Find(std::uint64_t address, const char* caller) { const int fd = QueryFd(); if (fd < 0) return ScanMaps(address, caller); @@ -100,7 +120,39 @@ inline std::optional Find(std::uint64_t address, const char* caller) { if (errno == ENOENT) return std::nullopt; throw std::runtime_error(std::string(caller) + ": PROCMAP_QUERY failed"); } - return Mapping{query.vma_start, query.vma_end, query.vma_name_size != 0 ? name : ""}; + return FromQuery(query, name); +} + +template +void Walk(std::uint64_t begin, std::uint64_t end, const char* caller, Visit&& visit) { + if (begin >= end) return; + if (const int fd = QueryFd(); fd >= 0) { + for (auto cursor = begin; cursor < end;) { + procmap_query query{}; + query.size = sizeof(query); + query.query_flags = PROCMAP_QUERY_COVERING_OR_NEXT_VMA; + query.query_addr = cursor; + if (ioctl(fd, PROCMAP_QUERY, &query) != 0) { + if (errno == ENOENT) return; + throw std::runtime_error(std::string(caller) + ": PROCMAP_QUERY failed"); + } + if (query.vma_start >= end) return; + if (query.vma_end <= cursor) throw std::runtime_error(std::string(caller) + ": PROCMAP_QUERY went backwards"); + visit(FromQuery(query, "")); + cursor = query.vma_end; + } + return; + } + std::ifstream maps("/proc/self/maps"); + if (!maps) throw std::runtime_error(std::string(caller) + ": failed to open /proc/self/maps"); + std::string line; + while (std::getline(maps, line)) { + const auto mapping = ParseMapsLine(line); + if (!mapping) throw std::runtime_error(std::string(caller) + ": unexpected /proc/self/maps line: " + line); + if (mapping->end <= begin) continue; + if (mapping->start >= end) return; + visit(*mapping); + } } } diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index 377205720b..1e4dbfc46f 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -195,7 +195,7 @@ On Windows use `agc_pm4_replay.exe` and prepend `build/core/libs/libs/unpatched` ## Frame capture and replay -Set `APS5_FRAME_CAPTURE` to a directory to record a few frames of a title at the AGC driver boundary, then replay them with `agc_frame_replay` through any build of the driver with no title running. From the start of the process the driver keeps every shader registration and ABI resolution, video output registration and EOP flip reservation. At the frame boundary picked by `APS5_FRAME_CAPTURE_AT_FLIP` (default 1), or by the appearance of the file `APS5_FRAME_CAPTURE_TRIGGER` names (a number in it replaces the frame count), it waits for the driver to go idle and writes every committed, readable allocation of the guest allocation registry, the title's image included, as zstd chunks without their all-zero 64 KiB blocks. A frame ends at a CPU flip or at a submission that flips (a flip packet, or the EOP release of a reserved flip token); the reservation itself only records its token. For the next `APS5_FRAME_CAPTURE_FRAMES` (10) frames it records each submission (queue, command buffer address and size), flip and suspend point with the thread that made it and the accepted and completed serials, each preceded by the guest pages the write tracker saw the CPU write since the previous call that handed work to the GPU (`GuestMemory::CollectWrittenBlocks`, per 4 KiB where the tracker kept page stamps), read once the submissions already accepted have completed (a recorded submission, flip or suspend point reaches the driver before the recorder takes its next call, so that wait covers every call recorded before the delta); an allocation the registry gained after the snapshot is recorded there as a new range with its nonzero 64 KiB blocks. A submission ends a frame only when it flips itself, not when another thread's submission flipped while it was being submitted. Shader registrations and ABI resolutions only collect the blocks of the shader header and the arrays it points to, and the pages of the title's image only when their content changed. At the end it stores each display buffer the title presented, decoded, as a reference image. A capture is `calls.bin`, `memory.bin` and `capture.json` (commit, switches, sizes, warnings), and the recorder stops with a warning when its volume has less than 4 GiB free. A 20-frame capture in the Demon's Souls Nexus is 5.3 GiB of snapshot and 11.5 GiB of deltas before compression, 3.9 GiB on disk. +On Windows and Linux, set `APS5_FRAME_CAPTURE` to a directory to record a few frames of a title at the AGC driver boundary, then replay them with `agc_frame_replay` through any build of the driver with no title running. From the start of the process the driver keeps every shader registration and ABI resolution, video output registration and EOP flip reservation. At the frame boundary picked by `APS5_FRAME_CAPTURE_AT_FLIP` (default 1), or by the appearance of the file `APS5_FRAME_CAPTURE_TRIGGER` names (a number in it replaces the frame count), it waits for the driver to go idle and writes every committed, readable allocation of the guest allocation registry, the title's image included, as zstd chunks without their all-zero 64 KiB blocks. A frame ends at a CPU flip or at a submission that flips (a flip packet, or the EOP release of a reserved flip token); the reservation itself only records its token. For the next `APS5_FRAME_CAPTURE_FRAMES` (10) frames it records each submission (queue, command buffer address and size), flip and suspend point with the thread that made it and the accepted and completed serials, each preceded by the guest pages the write tracker saw the CPU write since the previous call that handed work to the GPU (`GuestMemory::CollectWrittenBlocks`, per 4 KiB where the tracker kept page stamps), read once the submissions already accepted have completed (a recorded submission, flip or suspend point reaches the driver before the recorder takes its next call, so that wait covers every call recorded before the delta); an allocation the registry gained after the snapshot is recorded there as a new range with its nonzero 64 KiB blocks. A submission ends a frame only when it flips itself, not when another thread's submission flipped while it was being submitted. Shader registrations and ABI resolutions only collect the blocks of the shader header and the arrays it points to, and the pages of the title's image only when their content changed. At the end it stores each display buffer the title presented, decoded, as a reference image. Without the guest write watch (`APS5_NO_WRITE_WATCH`, or no write watch on the host) the capture throws when it starts, since it could not record CPU stores. A capture is `calls.bin`, `memory.bin` and `capture.json` (commit, switches, sizes, warnings), and the recorder stops with a warning when its volume has less than 4 GiB free. A 20-frame capture in the Demon's Souls Nexus is 5.3 GiB of snapshot and 11.5 GiB of deltas before compression, 3.9 GiB on disk. `agc_frame_replay [--no-pace] [--no-release] [--release-after-ms ] [--no-synthesize] [--strict] [--dump
::]... [--frames ]` maps the captured ranges at their addresses and registers them with their access (a range the replaying process already maps writable, such as the driver's fixed memory, is only refilled, and a non-GPU range it uses itself is skipped and counted), repeats the registrations, then replays the calls of each captured thread on a thread of its own, started in captured order. Before a call that hands work to the GPU it waits until no more submissions are in flight than when the title made it (`--no-pace` turns this off). Titles release GPU waits from threads that never call the driver, so when the driver completes nothing for 3 ms the replay reads the waits its queues are blocked on (`AgcDriverAwaitedWaits`) and writes the next recorded CPU value of the awaited dword that satisfies the compare, else the last earlier one, else a value derived from the reference (`--no-synthesize` turns this last step off). It prints one JSON line with the frame count and times, the submissions, the pacing wait, the released waits and how many were derived, the submissions that never completed and, for captures with reference images, the share of identical pixels and the largest channel difference of the replayed display buffers (`--frames` writes both images). Effects that depend on timing (temporal antialiasing, particles) make pixels differ, so expect a visual match rather than identical frames. Replay times follow the GPU clock state: lock the clocks (`nvidia-smi -lgc`, `-lmc`) when comparing two drivers. diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 90920465e6..d8b77961b2 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -72,7 +72,7 @@ ### Functional - [Written staging window on discrete GPUs](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver) - a written buffer element of a dispatch that is not swept is staged device-local up to 8 MiB on a discrete GPU (`Context::discreteGpu`) and up to 2 MiB otherwise; `APS5_WRITTEN_SHADOW_MAX_KIB` replaces either bound. The 8 MiB figure comes from one title on one NVIDIA Windows system (PPSA09911: a 2.5 and a 3.3 MiB skin-cache output written in place cost 55 ms of GPU time per frame, 1.8 ms of extra staging copies once staged); the point where the two copies of a larger element cost more than its in-place stores, and whether AMD or Linux drivers move it, has not been measured. The window is per element and per use, so a kernel that streams once through an element inside the window pays the copy-in and the copy-back on every dispatch. -- [Frame capture](../../../core/libs/prx/libSceAgcDriver/Execution/src/FrameRecorder.cpp) (libSceAgcDriver) records guest memory on Windows only (a VirtualQuery walk of the registry allocations and the Windows write tracker); elsewhere a capture throws when it starts. Ranges are captured by allocation and protection, not by mapping: section views, aliases of one direct memory block and file views are snapshotted and replayed as separate private memory, so a store through one alias is not seen through another, and CPU stores to them during the captured frames are not recorded (only write-watched private memory is). Pages of the title's image are recorded during the frames only when a shader header or an array it points to lies in them and their content changed. A capture starts only at a frame boundary where the driver goes idle within 10 s, and gives up after five boundaries that do not. A capture also stops when GPU work in flight at a delta waits on a later CPU store. +- [Frame capture](../../../core/libs/prx/libSceAgcDriver/Execution/src/FrameRecorder.cpp) (libSceAgcDriver) throws on macOS. Section views, aliases of one direct memory block and file views replay as separate private memory, so a store through one alias is not seen through another. Only write-watched memory records CPU stores during the frames: private memory on Windows, where a store through one alias is recorded only at that alias; registered ranges on Linux, including direct memory unless mapped more than once (libkernel leaves those views unwatched, so no store through any of them is recorded). Title image pages are recorded only when changed and holding a shader header or its arrays. Linux omits no-access pages (Windows records them unreadable) and records file mapping pages past the file's end as unreadable. A capture starts only at a frame boundary where the driver idles within 10 s (five tries), and stops when GPU work in flight at a delta waits on a later CPU store. - [Frame replay](../../../core/libs/prx/libSceAgcDriver/tools/FrameReplay.cpp) (libSceAgcDriver) orders the calls of different captured threads only by their start (a call starts once every earlier call has started), so the driver sees a schedule the title could have produced, not necessarily the one it did. CPU stores a title made from threads that never call the driver are known only by the next recorded call, so the replay applies them early or late when a GPU wait needs them and derives a value from the wait's reference when no recorded store satisfies it (`of_them_synthesized`). Pacing compares the number of submissions in flight, not what they hold. Prologue registrations of shaders whose memory the title reused after registering them fail and are counted (`stale_registrations`). The title's image range must be free in the replaying process, so the tool is linked at a high image base on Windows. - [Workgroup count split](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (libSceAgcDriver) - a direct dispatch over `maxComputeWorkGroupCount` runs as `vkCmdDispatch` chunks, each bound to a pipeline variant whose workgroup offset specialization constants add the chunk's base to `WorkgroupId` and `GlobalInvocationId`. A variant is built when its chunk base is first recorded, not when the dispatch is prepared. A split of a shader with device-memory LDS throws: its slot base reads the chunk's `NumWorkgroups`, not the dispatch's. Chunks are ordered by a compute barrier, which the console does not have between workgroups of one dispatch. - [Experimental submission progress](../../../core/libs/prx/libSceAgcDriver/Execution/include/Driver/Queues/SubmissionProgress.hpp) (AGC driver) - the opt-in two-millisecond pacing budget is not derived from console hardware. Cross-title performance remains unvalidated; host scheduling can overshoot the timeout and pacing can reduce CPU/GPU overlap. From c63d98b6f14fd9dd6088e0bb8837297dca9a1413 Mon Sep 17 00:00:00 2001 From: AZLisme <8706522+AZLisme@users.noreply.github.com> Date: Fri, 23 Oct 2026 12:14:03 +0800 Subject: [PATCH 124/198] feat(libSceIcu): implement escape decoding --- core/libs/prx/libSceIcu/CMakeLists.txt | 15 +- core/libs/prx/libSceIcu/Unescape.cpp | 158 ++++++++++++++++++++ core/libs/prx/libSceIcu/tests/Unescape.cpp | 161 +++++++++++++++++++++ docs/dev/debt/libSceIcu.md | 6 + 4 files changed, 339 insertions(+), 1 deletion(-) create mode 100644 core/libs/prx/libSceIcu/Unescape.cpp create mode 100644 core/libs/prx/libSceIcu/tests/Unescape.cpp diff --git a/core/libs/prx/libSceIcu/CMakeLists.txt b/core/libs/prx/libSceIcu/CMakeLists.txt index 5fcc2e7744..14fb9a0761 100644 --- a/core/libs/prx/libSceIcu/CMakeLists.txt +++ b/core/libs/prx/libSceIcu/CMakeLists.txt @@ -1,4 +1,4 @@ -add_library(libSceIcu SHARED EXCLUDE_FROM_ALL Export.cpp Invariant.cpp Version.cpp) +add_library(libSceIcu SHARED EXCLUDE_FROM_ALL Export.cpp Invariant.cpp Version.cpp Unescape.cpp) target_include_directories(libSceIcu PRIVATE ${LIBS_INCLUDE_DIR}) configure_windows_unwind(libSceIcu) @@ -14,3 +14,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endif() + +if(BUILD_TESTING) + add_test_executable(guest_icu_unescape_tests ${CMAKE_CURRENT_SOURCE_DIR}/tests/Unescape.cpp) + target_include_directories(guest_icu_unescape_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_icu_unescape_tests PRIVATE libSceIcu) + configure_windows_unwind(guest_icu_unescape_tests) + add_test(NAME guest_icu_unescape COMMAND guest_icu_unescape_tests) + set_tests_properties(guest_icu_unescape PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_icu_unescape PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceIcu/Unescape.cpp b/core/libs/prx/libSceIcu/Unescape.cpp new file mode 100644 index 0000000000..e96a095892 --- /dev/null +++ b/core/libs/prx/libSceIcu/Unescape.cpp @@ -0,0 +1,158 @@ +#include "prx/libc/include/General.hpp" +#include +#include +#include +#include + +namespace { + +using CharacterAt = char16_t (APS5_VABI*)(std::int32_t, void*); + +bool IsLead(std::int32_t value) { + return value >= 0xd800 && value <= 0xdbff; +} + +bool IsTrail(std::int32_t value) { + return value >= 0xdc00 && value <= 0xdfff; +} + +std::int32_t Supplementary(std::int32_t lead, std::int32_t trail) { + return 0x10000 + ((lead - 0xd800) << 10) + trail - 0xdc00; +} + +int Digit(char16_t value, int base) { + int digit = value >= u'0' && value <= u'9' ? value - u'0' + : value >= u'a' && value <= u'f' ? value - u'a' + 10 + : value >= u'A' && value <= u'F' ? value - u'A' + 10 : -1; + return digit < base ? digit : -1; +} + +std::int32_t Decode(CharacterAt characterAt, std::int32_t& position, std::int32_t length, void* context) { + const auto start = position; + if (position < 0 || position >= length) return -1; + if (!characterAt) NotImplemented_nid_no_patch("u_unescapeAt: null callback"); + const auto initial = characterAt(position++, context); + bool braces = false; + int minimum = 0; + int maximum = 0; + int digits = 0; + int base = 16; + std::uint32_t value = 0; + switch (initial) { + case u'u': minimum = maximum = 4; break; + case u'U': minimum = maximum = 8; break; + case u'x': + minimum = 1; + braces = position < length && characterAt(position, context) == u'{'; + maximum = braces ? 8 : 2; + if (braces) ++position; + break; + default: + if (initial >= u'0' && initial <= u'7') { + minimum = 1; + maximum = 3; + digits = 1; + base = 8; + value = initial - u'0'; + } + break; + } + if (minimum) { + char16_t last = initial; + while (position < length && digits < maximum) { + last = characterAt(position, context); + const int digit = Digit(last, base); + if (digit < 0) break; + value = value * base + digit; + ++position; + ++digits; + } + if (digits < minimum || (braces && last != u'}') || value > 0x10ffff) { + position = start; + return -1; + } + if (braces) ++position; + if (IsLead(value) && position < length) { + auto ahead = position + 1; + std::int32_t tail = characterAt(position, context); + if (tail == u'\\' && ahead < length) { + const auto limit = ahead + std::min(11, length - ahead); + tail = Decode(characterAt, ahead, limit, context); + } + if (IsTrail(tail)) { + position = ahead; + return Supplementary(value, tail); + } + } + return static_cast(value); + } + switch (initial) { + case u'a': return 7; + case u'b': return 8; + case u'e': return 27; + case u'f': return 12; + case u'n': return 10; + case u'r': return 13; + case u't': return 9; + case u'v': return 11; + default: break; + } + const bool control = initial == u'c' && position < length; + std::int32_t result = control ? characterAt(position++, context) : initial; + if (IsLead(result) && position < length) { + const auto tail = characterAt(position, context); + if (IsTrail(tail)) { + ++position; + result = Supplementary(result, tail); + } + } + return control ? result & 0x1f : result; +} + +char16_t APS5_VABI ByteAt(std::int32_t position, void* context) { + return static_cast(context)[position]; +} + +} // namespace + +extern "C" { + +std::int32_t APS5_VABI u_unescapeAt_nid_postfix(CharacterAt characterAt, std::int32_t* offset, std::int32_t length, void* context) { + if (!offset) NotImplemented_nid_no_patch(__func__); + return Decode(characterAt, *offset, length, context); +} + +std::int32_t APS5_VABI u_unescape_nid_postfix(const char* source, char16_t* destination, std::int32_t capacity) { + if (!source || (destination && capacity < 0)) NotImplemented_nid_no_patch(__func__); + const auto bytes = std::strlen(source); + if (bytes > static_cast(std::numeric_limits::max())) + NotImplemented_nid_no_patch(__func__); + const auto length = static_cast(bytes); + std::int32_t written = 0; + for (std::int32_t position = 0; position < length;) { + const auto byte = static_cast(source[position++]); + if (byte != '\\') { + if (destination && written < capacity) destination[written] = byte; + ++written; + continue; + } + const auto value = Decode(ByteAt, position, length, const_cast(source)); + if (value < 0) { + if (destination && capacity > 0) destination[0] = 0; + return 0; + } + const int units = value > 0xffff ? 2 : 1; + if (destination && written <= capacity && units <= capacity - written) { + if (units == 1) destination[written] = static_cast(value); + else { + destination[written] = static_cast(0xd800 + ((value - 0x10000) >> 10)); + destination[written + 1] = static_cast(0xdc00 + ((value - 0x10000) & 0x3ff)); + } + } + written += units; + } + if (destination && written < capacity) destination[written] = 0; + return written; +} + +} diff --git a/core/libs/prx/libSceIcu/tests/Unescape.cpp b/core/libs/prx/libSceIcu/tests/Unescape.cpp new file mode 100644 index 0000000000..b0a22378d3 --- /dev/null +++ b/core/libs/prx/libSceIcu/tests/Unescape.cpp @@ -0,0 +1,161 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using CharacterAt = char16_t (APS5_VABI*)(std::int32_t, void*); + +extern "C" { +std::int32_t APS5_VABI u_unescape_nid_postfix(const char*, char16_t*, std::int32_t); +std::int32_t APS5_VABI u_unescapeAt_nid_postfix(CharacterAt, std::int32_t*, std::int32_t, void*); +} + +namespace { + +void Require(bool condition, std::source_location location = std::source_location::current()) { + if (condition) return; + std::fprintf(stderr, "ICU unescape check failed at line %u\n", location.line()); + std::abort(); +} + +template +void RequireThrows(TAction action) { + try { action(); } catch (const std::exception&) { return; } + Require(false); +} + +struct Input { + std::u16string text; + std::vector positions; +}; + +char16_t APS5_VABI Read_nid_no_patch(std::int32_t position, void* context) { + auto& input = *static_cast(context); + Require(position >= 0 && static_cast(position) < input.text.size()); + input.positions.push_back(position); + return input.text[position]; +} + +void At(std::u16string text, std::int32_t expected, std::int32_t consumed) { + Input input{std::move(text), {}}; + std::int32_t offset = 0; + Require(u_unescapeAt_nid_postfix(Read_nid_no_patch, &offset, input.text.size(), &input) == expected); + Require(offset == consumed); +} + +void SingleEscapes() { + At(u"u0041!", 0x41, 5); + At(u"U0010ffff!", 0x10ffff, 9); + At(u"xF!", 15, 2); + At(u"xFF0", 255, 3); + At(u"x{10FfFf}!", 0x10ffff, 9); + At(u"x{010ffff}!", 0x10ffff, 10); + At(u"x{0010ffff}!", -1, 0); + At(u"7777", 511, 3); + At(u"078", 7, 2); + At(u"8", u'8', 1); + At(u"cA", 1, 2); + At(u"c", u'c', 1); + At(u"c\U0001f600", 0, 3); + At(u"\U0001f600", 0x1f600, 2); + At(u"uD83D\\uDE00!", 0x1f600, 11); + At(std::u16string(u"uD83D") + char16_t{0xde00} + u"!", 0x1f600, 6); + At(u"uD83D\\x{0DE00}!", 0x1f600, 14); + At(u"uD800\\uD801\\uDC00", 0xd800, 5); + At(u"uD800\\u12", 0xd800, 5); + At(u"uDC00", 0xdc00, 5); + At(u"\\", u'\\', 1); + At(u"-", u'-', 1); + At(u"a", 7, 1); + At(u"b", 8, 1); + At(u"e", 27, 1); + At(u"f", 12, 1); + At(u"n", 10, 1); + At(u"r", 13, 1); + At(u"t", 9, 1); + At(u"v", 11, 1); + for (const auto* text : {u"", u"u", u"u123", u"U0000000", u"U00110000", u"Uffffffff", u"x", u"x{}", u"x{1", u"x{110000}"}) + At(text, -1, 0); + Input input{u"prefixu0041suffix", {}}; + std::int32_t offset = 6; + Require(u_unescapeAt_nid_postfix(Read_nid_no_patch, &offset, input.text.size(), &input) == 0x41 && offset == 11); + for (const auto invalid : {-1, 17, std::numeric_limits::max()}) { + offset = invalid; + const auto calls = input.positions.size(); + Require(u_unescapeAt_nid_postfix(Read_nid_no_patch, &offset, input.text.size(), &input) == -1); + Require(offset == invalid && input.positions.size() == calls); + } + offset = 0; + Require(u_unescapeAt_nid_postfix(nullptr, &offset, -1, nullptr) == -1 && offset == 0); + RequireThrows([&] { u_unescapeAt_nid_postfix(nullptr, &offset, 1, nullptr); }); + RequireThrows([] { u_unescapeAt_nid_postfix(Read_nid_no_patch, nullptr, 1, nullptr); }); +} + +void String(const char* escaped, std::u16string_view expected) { + Require(u_unescape_nid_postfix(escaped, nullptr, std::numeric_limits::min()) == static_cast(expected.size())); + for (std::int32_t capacity = 0; capacity <= static_cast(expected.size()) + 2; ++capacity) { + std::vector output(expected.size() + 4, 0xa55a); + Require(u_unescape_nid_postfix(escaped, output.data() + 1, capacity) == static_cast(expected.size())); + if (capacity >= static_cast(expected.size())) + Require(std::equal(expected.begin(), expected.end(), output.begin() + 1)); + if (capacity > static_cast(expected.size())) Require(output[expected.size() + 1] == 0); + Require(output.front() == 0xa55a); + for (std::size_t index = capacity + 1; index < output.size(); ++index) Require(output[index] == 0xa55a); + } +} + +void Strings() { + String("", u""); + String("plain text", u"plain text"); + String("\\a\\b\\t\\n\\v\\f\\r\\e\\\"\\'\\?\\\\", u"\a\b\t\n\v\f\r\x1b\"'?\\"); + String("[a\\-z]\\cA\\777", u"[a-z]\x01\u01ff"); + String("A\\u0042\\U0001F600Z", u"AB\U0001f600Z"); + String("\\uD83D\\uDE00", u"\U0001f600"); + const char16_t unpaired[] = {0xd800, 0xd801, 0xdc00}; + String("\\uD800\\uD801\\uDC00", std::u16string_view(unpaired, 3)); + String("\\x{10ffff}\\xA\\123", u"\U0010ffff\nS"); + const char16_t embedded[] = {u'A', 0, u'B'}; + String("A\\0B", std::u16string_view(embedded, 3)); + for (unsigned int byte = 1; byte <= 255; ++byte) { + if (byte == '\\') continue; + const char source[] = {static_cast(byte), 0}; + const char16_t expected[] = {static_cast(byte)}; + String(source, std::u16string_view(expected, 1)); + } + char16_t destination[] = {0xa55a, 0xa55a, 0xa55a}; + Require(u_unescape_nid_postfix("\\U0001f600Z", destination, 1) == 3); + Require(destination[0] == 0xa55a && destination[1] == 0xa55a); + Require(u_unescape_nid_postfix("a\\u12", destination, 3) == 0 && destination[0] == 0 && destination[1] == 0xa55a); + destination[0] = 0xa55a; + Require(u_unescape_nid_postfix("abc\\u12", destination, 3) == 0 && destination[0] == 0 && destination[1] == u'b' && destination[2] == u'c'); + for (auto capacity : {-1, std::numeric_limits::min()}) { + std::fill(std::begin(destination), std::end(destination), 0xa55a); + RequireThrows([&] { u_unescape_nid_postfix("a\\U0001f600", destination, capacity); }); + Require(destination[0] == 0xa55a && destination[1] == 0xa55a && destination[2] == 0xa55a); + } + RequireThrows([] { u_unescape_nid_postfix(nullptr, nullptr, 0); }); + for (std::uint32_t value = 0; value <= 0x10ffff; value += 17) { + char escaped[16]; + std::snprintf(escaped, sizeof(escaped), "\\U%08x", value); + std::array output{}; + const auto size = u_unescape_nid_postfix(escaped, output.data(), output.size()); + if (value <= 0xffff) Require(size == 1 && output[0] == value && output[1] == 0); + else Require(size == 2 && output[0] == 0xd800 + ((value - 0x10000) >> 10) && output[1] == 0xdc00 + ((value - 0x10000) & 0x3ff) && output[2] == 0); + } +} + +} // namespace + +int main() { + SingleEscapes(); + Strings(); + std::puts("ICU escape decoding tests passed"); +} diff --git a/docs/dev/debt/libSceIcu.md b/docs/dev/debt/libSceIcu.md index dcf66e6027..f22d70df64 100644 --- a/docs/dev/debt/libSceIcu.md +++ b/docs/dev/debt/libSceIcu.md @@ -1,5 +1,11 @@ # libSceIcu technical debt +### Unknown function info + +- [ICU exports](../../../core/libs/prx/libSceIcu/Unescape.cpp) implement only `u_unescape` and `u_unescapeAt` from the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceIcu.c); the other 659 named exports are not implemented here. Console ICU version, signatures and behaviour have not been verified. + ### Functional +- [Escape decoding](../../../core/libs/prx/libSceIcu/Unescape.cpp) has no PS5 verification. It follows ICU 78.1's [ustring.cpp](https://github.com/unicode-org/icu/blob/049e0d6a420629ac7db77256987d083a563287b5/icu4c/source/common/ustring.cpp), including rejection of eight-digit braced hexadecimal escapes although its public header advertises up to eight digits. Null source or offset pointers, null callbacks with an in-range offset, negative capacity with a nonnull destination and source lengths exceeding `INT32_MAX` throw. Null-destination preflight ignores capacity. Closes when: compared with the console, including callback ABI, byte conversion, capacity limits and braced hexadecimal parsing. + - [Invariant character and version helpers](../../../core/libs/prx/libSceIcu/Version.cpp) are unverified on a console. `u_charsToUChars`, `u_UCharsToChars`, `u_versionFromString`, `u_versionFromUString` and `u_versionToString` follow ICU 78.1 [uinvchar.cpp](https://github.com/unicode-org/icu/blob/release-78.1/icu4c/source/common/uinvchar.cpp) and [putil.cpp](https://github.com/unicode-org/icu/blob/release-78.1/icu4c/source/common/putil.cpp), unchanged since 65.1. Firmware 13.42 reports ICU 78.1 and exports none of them ([#11013](https://github.com/boykopovar/AnyPS5/pull/11013#issuecomment-6400470698)); the [3.20 table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceIcu.c) has all five. Version parts use the guest `strtoul`, truncated to eight bits; `u_versionToString` keeps ICU's dropped zero tens digit (105 becomes `15`). A null buffer with a positive conversion length throws; ICU dereferences it. Closes when: compared with the console. From c1183ca65b25bb00254fd3d6edf42abcba336980 Mon Sep 17 00:00:00 2001 From: AZLisme <8706522+AZLisme@users.noreply.github.com> Date: Sun, 25 Oct 2026 18:21:15 +0800 Subject: [PATCH 125/198] refactor(libSceIcu): expose escape decoding through public headers --- core/libs/prx/libSceIcu/Unescape.cpp | 7 +++---- core/libs/prx/libSceIcu/Unescape.hpp | 12 ++++++++++++ core/libs/prx/libSceIcu/libSceIcu.hpp | 1 + core/libs/prx/libSceIcu/tests/Unescape.cpp | 8 +------- 4 files changed, 17 insertions(+), 11 deletions(-) create mode 100644 core/libs/prx/libSceIcu/Unescape.hpp diff --git a/core/libs/prx/libSceIcu/Unescape.cpp b/core/libs/prx/libSceIcu/Unescape.cpp index e96a095892..acf0b2abaf 100644 --- a/core/libs/prx/libSceIcu/Unescape.cpp +++ b/core/libs/prx/libSceIcu/Unescape.cpp @@ -1,3 +1,4 @@ +#include "prx/libSceIcu/libSceIcu.hpp" #include "prx/libc/include/General.hpp" #include #include @@ -6,8 +7,6 @@ namespace { -using CharacterAt = char16_t (APS5_VABI*)(std::int32_t, void*); - bool IsLead(std::int32_t value) { return value >= 0xd800 && value <= 0xdbff; } @@ -27,7 +26,7 @@ int Digit(char16_t value, int base) { return digit < base ? digit : -1; } -std::int32_t Decode(CharacterAt characterAt, std::int32_t& position, std::int32_t length, void* context) { +std::int32_t Decode(Icu::UnescapeCharacterAt characterAt, std::int32_t& position, std::int32_t length, void* context) { const auto start = position; if (position < 0 || position >= length) return -1; if (!characterAt) NotImplemented_nid_no_patch("u_unescapeAt: null callback"); @@ -117,7 +116,7 @@ char16_t APS5_VABI ByteAt(std::int32_t position, void* context) { extern "C" { -std::int32_t APS5_VABI u_unescapeAt_nid_postfix(CharacterAt characterAt, std::int32_t* offset, std::int32_t length, void* context) { +std::int32_t APS5_VABI u_unescapeAt_nid_postfix(Icu::UnescapeCharacterAt characterAt, std::int32_t* offset, std::int32_t length, void* context) { if (!offset) NotImplemented_nid_no_patch(__func__); return Decode(characterAt, *offset, length, context); } diff --git a/core/libs/prx/libSceIcu/Unescape.hpp b/core/libs/prx/libSceIcu/Unescape.hpp new file mode 100644 index 0000000000..a91eb17ed6 --- /dev/null +++ b/core/libs/prx/libSceIcu/Unescape.hpp @@ -0,0 +1,12 @@ +#pragma once +#include "prx/libc/include/general/VabiMacros.hpp" +#include + +namespace Icu { +using UnescapeCharacterAt = char16_t (APS5_VABI*)(std::int32_t, void*); +} // namespace Icu + +extern "C" { +std::int32_t APS5_VABI u_unescape_nid_postfix(const char*, char16_t*, std::int32_t); +std::int32_t APS5_VABI u_unescapeAt_nid_postfix(Icu::UnescapeCharacterAt, std::int32_t*, std::int32_t, void*); +} diff --git a/core/libs/prx/libSceIcu/libSceIcu.hpp b/core/libs/prx/libSceIcu/libSceIcu.hpp index 7b35c6d5fb..d8c66857ef 100644 --- a/core/libs/prx/libSceIcu/libSceIcu.hpp +++ b/core/libs/prx/libSceIcu/libSceIcu.hpp @@ -1,3 +1,4 @@ #pragma once #include "Invariant.hpp" #include "Version.hpp" +#include "Unescape.hpp" diff --git a/core/libs/prx/libSceIcu/tests/Unescape.cpp b/core/libs/prx/libSceIcu/tests/Unescape.cpp index b0a22378d3..057cd75f94 100644 --- a/core/libs/prx/libSceIcu/tests/Unescape.cpp +++ b/core/libs/prx/libSceIcu/tests/Unescape.cpp @@ -1,4 +1,4 @@ -#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libSceIcu/libSceIcu.hpp" #include #include #include @@ -11,12 +11,6 @@ #include #include -using CharacterAt = char16_t (APS5_VABI*)(std::int32_t, void*); - -extern "C" { -std::int32_t APS5_VABI u_unescape_nid_postfix(const char*, char16_t*, std::int32_t); -std::int32_t APS5_VABI u_unescapeAt_nid_postfix(CharacterAt, std::int32_t*, std::int32_t, void*); -} namespace { From 11d4dd44bf5d95d0dcf9085f195e1e2a8ba6beba Mon Sep 17 00:00:00 2001 From: AZLisme <8706522+AZLisme@users.noreply.github.com> Date: Sun, 25 Oct 2026 21:10:57 +0800 Subject: [PATCH 126/198] test(libSceIcu): strengthen unescape output and callback checks --- core/libs/prx/libSceIcu/tests/Unescape.cpp | 44 +++++++++++++++++++--- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceIcu/tests/Unescape.cpp b/core/libs/prx/libSceIcu/tests/Unescape.cpp index 057cd75f94..1dc7152940 100644 --- a/core/libs/prx/libSceIcu/tests/Unescape.cpp +++ b/core/libs/prx/libSceIcu/tests/Unescape.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include @@ -29,13 +30,18 @@ void RequireThrows(TAction action) { struct Input { std::u16string text; std::vector positions; + std::int32_t* offset = nullptr; + std::int32_t base = 0; + std::vector> observations; }; char16_t APS5_VABI Read_nid_no_patch(std::int32_t position, void* context) { auto& input = *static_cast(context); - Require(position >= 0 && static_cast(position) < input.text.size()); + const auto relative = static_cast(position) - input.base; + Require(relative >= 0 && static_cast(relative) < input.text.size()); input.positions.push_back(position); - return input.text[position]; + if (input.offset) input.observations.emplace_back(static_cast(relative), *input.offset - input.base); + return input.text[relative]; } void At(std::u16string text, std::int32_t expected, std::int32_t consumed) { @@ -45,6 +51,27 @@ void At(std::u16string text, std::int32_t expected, std::int32_t consumed) { Require(offset == consumed); } +void CallbackOrder() { + const auto check = [](std::u16string text, std::int32_t expected, std::int32_t consumed, + std::vector> observations, std::int32_t base = 0) { + auto offset = base; + Input input{std::move(text), {}, &offset, base, {}}; + const auto length = static_cast(static_cast(base) + input.text.size()); + Require(u_unescapeAt_nid_postfix(Read_nid_no_patch, &offset, length, &input) == expected); + Require(offset - base == consumed && input.observations == observations); + }; + check(u"u0041!", 0x41, 5, {{0, 1}, {1, 1}, {2, 2}, {3, 3}, {4, 4}}); + check(u"x{41}", 0x41, 5, {{0, 1}, {1, 1}, {2, 2}, {3, 3}, {4, 4}}); + check(u"u12", -1, 0, {{0, 1}, {1, 1}, {2, 2}}); + check(u"uD83D\\uDE00", 0x1f600, 11, + {{0, 1}, {1, 1}, {2, 2}, {3, 3}, {4, 4}, {5, 5}, {6, 5}, {7, 5}, {8, 5}, {9, 5}, {10, 5}}); + check(u"uD800\\u12", 0xd800, 5, + {{0, 1}, {1, 1}, {2, 2}, {3, 3}, {4, 4}, {5, 5}, {6, 5}, {7, 5}, {8, 5}}); + check(std::u16string(u"uD83D") + char16_t{0xde00}, 0x1f600, 6, + {{0, 1}, {1, 1}, {2, 2}, {3, 3}, {4, 4}, {5, 5}}, std::numeric_limits::max() - 6); + check(u"u", -1, 0, {{0, 1}}, std::numeric_limits::max() - 1); +} + void SingleEscapes() { At(u"u0041!", 0x41, 5); At(u"U0010ffff!", 0x10ffff, 9); @@ -98,8 +125,12 @@ void String(const char* escaped, std::u16string_view expected) { for (std::int32_t capacity = 0; capacity <= static_cast(expected.size()) + 2; ++capacity) { std::vector output(expected.size() + 4, 0xa55a); Require(u_unescape_nid_postfix(escaped, output.data() + 1, capacity) == static_cast(expected.size())); - if (capacity >= static_cast(expected.size())) - Require(std::equal(expected.begin(), expected.end(), output.begin() + 1)); + auto prefix = std::min(capacity, expected.size()); + if (prefix > 0 && prefix < expected.size() && expected[prefix - 1] >= 0xd800 && expected[prefix - 1] <= 0xdbff + && expected[prefix] >= 0xdc00 && expected[prefix] <= 0xdfff) --prefix; + Require(std::equal(expected.begin(), expected.begin() + prefix, output.begin() + 1)); + for (auto index = prefix; index < std::min(capacity, expected.size()); ++index) + Require(output[index + 1] == 0xa55a); if (capacity > static_cast(expected.size())) Require(output[expected.size() + 1] == 0); Require(output.front() == 0xa55a); for (std::size_t index = capacity + 1; index < output.size(); ++index) Require(output[index] == 0xa55a); @@ -109,6 +140,7 @@ void String(const char* escaped, std::u16string_view expected) { void Strings() { String("", u""); String("plain text", u"plain text"); + String("A\\u0042", u"AB"); String("\\a\\b\\t\\n\\v\\f\\r\\e\\\"\\'\\?\\\\", u"\a\b\t\n\v\f\r\x1b\"'?\\"); String("[a\\-z]\\cA\\777", u"[a-z]\x01\u01ff"); String("A\\u0042\\U0001F600Z", u"AB\U0001f600Z"); @@ -139,7 +171,8 @@ void Strings() { for (std::uint32_t value = 0; value <= 0x10ffff; value += 17) { char escaped[16]; std::snprintf(escaped, sizeof(escaped), "\\U%08x", value); - std::array output{}; + std::array output; + output.fill(0xa55a); const auto size = u_unescape_nid_postfix(escaped, output.data(), output.size()); if (value <= 0xffff) Require(size == 1 && output[0] == value && output[1] == 0); else Require(size == 2 && output[0] == 0xd800 + ((value - 0x10000) >> 10) && output[1] == 0xdc00 + ((value - 0x10000) & 0x3ff) && output[2] == 0); @@ -150,6 +183,7 @@ void Strings() { int main() { SingleEscapes(); + CallbackOrder(); Strings(); std::puts("ICU escape decoding tests passed"); } From 844e39236f0ea90203751894aad259bbc703748a Mon Sep 17 00:00:00 2001 From: AZLisme <8706522+AZLisme@users.noreply.github.com> Date: Mon, 26 Oct 2026 00:26:45 +0800 Subject: [PATCH 127/198] docs(libSceIcu): record escape export firmware evidence --- docs/dev/debt/libSceIcu.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/dev/debt/libSceIcu.md b/docs/dev/debt/libSceIcu.md index f22d70df64..3623dfec51 100644 --- a/docs/dev/debt/libSceIcu.md +++ b/docs/dev/debt/libSceIcu.md @@ -2,10 +2,10 @@ ### Unknown function info -- [ICU exports](../../../core/libs/prx/libSceIcu/Unescape.cpp) implement only `u_unescape` and `u_unescapeAt` from the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceIcu.c); the other 659 named exports are not implemented here. Console ICU version, signatures and behaviour have not been verified. +- [ICU export availability](../../../core/libs/prx/libSceIcu/Unescape.cpp) across firmware versions remains unproven. The [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceIcu.c) includes the implemented escape, invariant-character and version helpers. [BunnyFl’s firmware 13.42 report](https://github.com/boykopovar/AnyPS5/pull/10961#issuecomment-6400071863) records `u_getVersion` returning `78.1.0.0` under an emulator, but neither escape function is in the checked native `system_ex/common_ex/lib/libSceIcu.sprx` export table. Only 104 of the 661 inventory NIDs appear among that module’s 299 exports. The exports for compatibility with older firmware are retained; availability and routing across firmware versions remain unproven. ### Functional -- [Escape decoding](../../../core/libs/prx/libSceIcu/Unescape.cpp) has no PS5 verification. It follows ICU 78.1's [ustring.cpp](https://github.com/unicode-org/icu/blob/049e0d6a420629ac7db77256987d083a563287b5/icu4c/source/common/ustring.cpp), including rejection of eight-digit braced hexadecimal escapes although its public header advertises up to eight digits. Null source or offset pointers, null callbacks with an in-range offset, negative capacity with a nonnull destination and source lengths exceeding `INT32_MAX` throw. Null-destination preflight ignores capacity. Closes when: compared with the console, including callback ABI, byte conversion, capacity limits and braced hexadecimal parsing. +- [Escape decoding](../../../core/libs/prx/libSceIcu/Unescape.cpp) has no behavior or ABI verification on console hardware. It follows ICU 78.1's [ustring.cpp](https://github.com/unicode-org/icu/blob/049e0d6a420629ac7db77256987d083a563287b5/icu4c/source/common/ustring.cpp), including rejection of eight-digit braced hexadecimal escapes although its public header advertises up to eight digits. Null source or offset pointers, null callbacks with an in-range offset, negative capacity with a nonnull destination and source lengths exceeding `INT32_MAX` throw. Null-destination preflight ignores capacity. Closes when: compared with the console, including callback ABI, byte conversion, capacity limits and braced hexadecimal parsing. - [Invariant character and version helpers](../../../core/libs/prx/libSceIcu/Version.cpp) are unverified on a console. `u_charsToUChars`, `u_UCharsToChars`, `u_versionFromString`, `u_versionFromUString` and `u_versionToString` follow ICU 78.1 [uinvchar.cpp](https://github.com/unicode-org/icu/blob/release-78.1/icu4c/source/common/uinvchar.cpp) and [putil.cpp](https://github.com/unicode-org/icu/blob/release-78.1/icu4c/source/common/putil.cpp), unchanged since 65.1. Firmware 13.42 reports ICU 78.1 and exports none of them ([#11013](https://github.com/boykopovar/AnyPS5/pull/11013#issuecomment-6400470698)); the [3.20 table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceIcu.c) has all five. Version parts use the guest `strtoul`, truncated to eight bits; `u_versionToString` keeps ICU's dropped zero tens digit (105 becomes `15`). A null buffer with a positive conversion length throws; ICU dereferences it. Closes when: compared with the console. From d21fb108e4fd456b6a487f3adf072c73ba5291fd Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Mon, 26 Oct 2026 16:12:36 +0000 Subject: [PATCH 128/198] build(vorbis): update libvorbis to the commit that stops shifting a negative bark bound --- 3rdparty/vorbis | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/3rdparty/vorbis b/3rdparty/vorbis index 0657aee69d..c2aa86b05e 160000 --- a/3rdparty/vorbis +++ b/3rdparty/vorbis @@ -1 +1 @@ -Subproject commit 0657aee69dec8508a0011f47f3b69d7538e9d262 +Subproject commit c2aa86b05e981c96bf381fc6aa11cdd03eccc2fb From 0fd587337a9a6f0aa52821283d8a2caa0ae0bbce Mon Sep 17 00:00:00 2001 From: Santhosh Kumar Date: Mon, 26 Oct 2026 22:42:50 +0530 Subject: [PATCH 129/198] fix(libSceFreeTypeOt): declare named import-resolution helpers --- core/libs/prx/libSceFreeType/include/Api.hpp | 2 ++ core/libs/prx/libSceFreeTypeOt/Export.cpp | 1 + 2 files changed, 3 insertions(+) diff --git a/core/libs/prx/libSceFreeType/include/Api.hpp b/core/libs/prx/libSceFreeType/include/Api.hpp index bdac73fc76..ec0b54a189 100644 --- a/core/libs/prx/libSceFreeType/include/Api.hpp +++ b/core/libs/prx/libSceFreeType/include/Api.hpp @@ -64,7 +64,9 @@ void APS5_VABI FT_Outline_Translate_nid_postfix(const FreeType::GuestOutline* ou void APS5_VABI FT_Outline_Reverse_nid_postfix(FreeType::GuestOutline* outline); int APS5_VABI FT_Request_Size_nid_postfix(FreeType::GuestFaceRec* face, const FreeType::GuestSizeRequestRec* request); int APS5_VABI FT_Property_Get_nid_postfix(FreeType::GuestLibrary* library, const char* moduleName, const char* propertyName, void* value); +int APS5_VABI sceFreeTypeOtPropertyGet_nid_no_patch(); int APS5_VABI FT_Property_Set_nid_postfix(FreeType::GuestLibrary* library, const char* moduleName, const char* propertyName, const void* value); +int APS5_VABI sceFreeTypeOtPropertySet_nid_no_patch(); void APS5_VABI FT_Bitmap_New_nid_postfix(FreeType::GuestBitmap* bitmap); int APS5_VABI FT_Bitmap_Copy_nid_postfix(FreeType::GuestLibrary* library, const FreeType::GuestBitmap* source, FreeType::GuestBitmap* target); diff --git a/core/libs/prx/libSceFreeTypeOt/Export.cpp b/core/libs/prx/libSceFreeTypeOt/Export.cpp index ad725720b0..c8e86e63db 100644 --- a/core/libs/prx/libSceFreeTypeOt/Export.cpp +++ b/core/libs/prx/libSceFreeTypeOt/Export.cpp @@ -1,3 +1,4 @@ +#include "prx/libSceFreeType/include/Api.hpp" #include #include #include "SceTypes.hpp" From dc6395595c3a9589384786675c41e0ed185a3b7d Mon Sep 17 00:00:00 2001 From: Santhosh Kumar Date: Mon, 26 Oct 2026 22:42:51 +0530 Subject: [PATCH 130/198] fix(libSceFreeTypeOt): declare named import-resolution helpers --- core/libs/prx/libSceFreeType/include/Api.hpp | 1 + core/libs/prx/libSceFreeTypeOt/Export.cpp | 1 + 2 files changed, 2 insertions(+) diff --git a/core/libs/prx/libSceFreeType/include/Api.hpp b/core/libs/prx/libSceFreeType/include/Api.hpp index 1f876898ea..dddd6b06d2 100644 --- a/core/libs/prx/libSceFreeType/include/Api.hpp +++ b/core/libs/prx/libSceFreeType/include/Api.hpp @@ -61,6 +61,7 @@ int APS5_VABI FT_Get_Sfnt_Name_nid_postfix(FreeType::GuestFaceRec* face, std::ui int APS5_VABI FT_Outline_Check_nid_postfix(FreeType::GuestOutline* outline); void APS5_VABI FT_Outline_Get_CBox_nid_postfix(const FreeType::GuestOutline* outline, FreeType::GuestBBox* box); int APS5_VABI FT_Outline_Get_BBox_nid_postfix(FreeType::GuestOutline* outline, FreeType::GuestBBox* boundingBox); +int APS5_VABI sceFreeTypeOtOutlineGetBBox_nid_no_patch(); void APS5_VABI FT_Outline_Translate_nid_postfix(const FreeType::GuestOutline* outline, std::int64_t xOffset, std::int64_t yOffset); void APS5_VABI FT_Outline_Reverse_nid_postfix(FreeType::GuestOutline* outline); int APS5_VABI FT_Request_Size_nid_postfix(FreeType::GuestFaceRec* face, const FreeType::GuestSizeRequestRec* request); diff --git a/core/libs/prx/libSceFreeTypeOt/Export.cpp b/core/libs/prx/libSceFreeTypeOt/Export.cpp index e4d923afc8..3bb7f5d3f4 100644 --- a/core/libs/prx/libSceFreeTypeOt/Export.cpp +++ b/core/libs/prx/libSceFreeTypeOt/Export.cpp @@ -1,3 +1,4 @@ +#include "prx/libSceFreeType/include/Api.hpp" #include #include #include "SceTypes.hpp" From 9c9c33821f2ab94595b8ef7d6728c966341d6860 Mon Sep 17 00:00:00 2001 From: Michael Krumland-Dunning Date: Sat, 24 Oct 2026 17:08:50 -0700 Subject: [PATCH 131/198] docs(shader): record the VCC descriptor tuples measured on RDNA2 Buffer resources and MIMG samplers at register field 26 read s104, s105, vcc_lo and vcc_hi, as measured with the hardware oracle on RDNA2 (gfx1036) in wave32 and wave64 with raw encodings, which LLVM's assembler refuses. Loads, format conversion, bounds and stores through the buffer tuple, and sampling through the sampler tuple, act as the same words in ordinary SGPRs do, and a change to each of the four words shows in the result. Both debt entries now keep gfx1013 as open. Co-Authored-By: Claude Opus 5.5 --- docs/dev/debt/shader.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index e7aba24b21..f4395088cd 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -10,10 +10,10 @@ ### Unknown function info -- [VCC-backed MIMG samplers](../../../core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp): the s104/s105/vcc_lo/vcc_hi tuple follows LLVM's [SGPR register classes](https://github.com/llvm/llvm-project/blob/d8219b47422f7322db252ad29ca8ea87993fb433/llvm/lib/Target/AMDGPU/SIRegisterInfo.td#L452) and [gfx10 sampler operand](https://github.com/llvm/llvm-project/blob/d8219b47422f7322db252ad29ca8ea87993fb433/llvm/lib/Target/AMDGPU/MIMGInstructions.td#L1379), using the existing VCC word reads. Recompiled wave32/wave64 reads are checked by [bilinear and white-border cases](../../../core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleVccSampler.cpp); raw RDNA execution of this tuple on gfx1013 or gfx1036 is unmeasured. +- [VCC-backed MIMG samplers](../../../core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp) - the PS5's gfx1013 is not measured. The sampler at field 26 is s104, s105, vcc_lo and vcc_hi. Measured on RDNA2 (gfx1036, AMD Windows driver 32.0.21030.31) through the [hardware oracle](../HW_ORACLE.md) with raw `image_sample_lz` encodings in wave32 and wave64 on a two-level texture, it samples as the same S# in s[16:19] does, and a border clamp in s104, a `MIN_LOD` of 1.0 in s105, point filtering in vcc_lo and a transparent black border in vcc_hi each change the result. Recompiled reads are checked by [bilinear and white-border cases](../../../core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleVccSampler.cpp). - [MSAA-address image loads on 2D textures](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) - 32-bit `image_load` with DIM 2D_MSAA reads a non-multisampled 2D T# as the same DIM 2D load, ignoring the sample index. Measured on RX 6650 XT (gfx1032), wave32 and wave64, for R32_UINT, R32_FLOAT, RGBA8_UNORM and RGBA8_SRGB, sample indices 0/1/2/7/31/0xffffffff and in-range/out-of-range coordinates; IEEE 0, DX10 clamp 1, denorm32 0, denorm16 3, round32/16 0, fp16 overflow 0. Pinned in ImageMsaaAddressOnPlane.cpp. Not measured on gfx1013. Stores, atomics, mip-bearing or A16 addresses, packed accesses, converted formats, depth-bit views, array-MSAA addresses and image-table accesses are not enabled by this compatibility mode. -- [MUBUF/MTBUF descriptors ending in VCC](../../../core/shader/recompiler/RdnaDecoder/src/RdnaMemoryOpDecoder.cpp): the four dword descriptor at s104 is s104, s105, vcc_lo and vcc_hi, as LLVM's [VCCTuple/SReg_128](https://github.com/llvm/llvm-project/blob/d8219b47422f7322db252ad29ca8ea87993fb433/llvm/lib/Target/AMDGPU/SIRegisterInfo.td#L422) and [buffer resource operands](https://github.com/llvm/llvm-project/blob/d8219b47422f7322db252ad29ca8ea87993fb433/llvm/lib/Target/AMDGPU/BUFInstructions.td) describe. Existing translation maps scalar codes106/107 to the raw VCC words. Recompiled loads/stores, both words, bounds, formats and wave32/wave64 execute on NVIDIA and AMD Vulkan; raw gfx10/gfx1013 hardware is not measured. Higher descriptor bases remain refused; the normal SGPR bound is unchanged. +- [MUBUF/MTBUF descriptors ending in VCC](../../../core/shader/recompiler/RdnaDecoder/src/RdnaMemoryOpDecoder.cpp) - the PS5's gfx1013 is not measured; higher descriptor bases remain refused. The descriptor at resource field 26 is s104, s105, vcc_lo and vcc_hi. Measured on RDNA2 (gfx1036, AMD Windows driver 32.0.21030.31) through the [hardware oracle](../HW_ORACLE.md) with raw encodings, which LLVM's assembler refuses, in wave32 and wave64: `buffer_load_dword`, `buffer_load_format_x` with a format in vcc_hi, `tbuffer_load_format_x`, `buffer_store_dword` and `tbuffer_store_format_x` through it act as through the same words in s[8:11], including a nonzero base high half in s105 and a 12-byte `NUM_RECORDS` in vcc_lo. Recompiled accesses are checked by [BufferResourceVcc](../../../core/libs/prx/libSceAgcDriver/tests/execution/BufferResourceVcc.cpp). - [Packed pixel ancillary word](../../../core/shader/recompiler/Optimization/src/ConstantFolder.cpp) (shader recompiler) - the sample index at bits 8-11 and the render target index at bits 16-28 follow #295. The sample index is the SampleId builtin in a pixel shader the guest iterates per sample (`PS_ITER_SAMPLE` on a multisampled draw), an equality inferred, not measured. In any other pixel shader it is 0, the iterated sample number of a pixel the hardware does not iterate (also inferred), so reading it does not make the host shade per sample. The render target index is the Layer builtin when the draw exports it (a layered draw, `PA_CL_VS_OUT_CNTL` bit 18; the pipeline specialization constant `LayerExported`) and 0 otherwise, the first layer Vulkan renders when no pre-rasterization stage writes Layer. Reading the unwritten Layer builtin instead makes AMD's Windows driver drop the fragment's writes (#4800); with the constant at 0 the load is folded away. Mesa reads both fields from those bits, with the layer 14 bits wide only from GFX12 on ([ac_nir_lower_intrinsics_to_args.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/bffd91947efc1932da99aca69fc540d6d4201b0c/src/amd/common/nir/ac_nir_lower_intrinsics_to_args.c#L342-L368)). A value passed through a select, move or phi whose every read is an extract inside one of those fields is rebuilt from the two fields alone. Any other read the masked-select pass cannot drop gets the whole word: the primitive class at bits 0-1 (0 point, 1 line, 2 triangle, 3 rectangle), which the draw sets as a pipeline specialization constant ([RasterPrimitiveOf](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp)) in the shaders that read it, plus the two fields, with every other bit zero. That layout comes from the RDNA 3 and 3.5 ISA guides (prim type 1:0, VRS rates 3:2 and 5:4, eye id 7, sample 11:8, render target index 28:16). The GCN register guides give the same prim type and sample fields and put the render target index at 26:16, so the two sources agree for layers below 2048. AMD's LLPC reads the primitive class from bits 1:0 with the same encoding ([LowerGlCompatibility.cpp](https://github.com/GPUOpen-Drivers/llpc/blob/40cb8d95ad8d6f7f1652e3fd47d39667594cce08/llpc/lowering/LowerGlCompatibility.cpp#L830-L836)) and without a GFX-level check ([getPrimType](https://github.com/GPUOpen-Drivers/llpc/blob/40cb8d95ad8d6f7f1652e3fd47d39667594cce08/lgc/lowering/LowerInOut.cpp#L5307-L5315)). Nobody has checked it on gfx1013. In the whole word only bits 0-1, 8-11 and 16-28 have a source. Bits 2-7, 12-15 and 29-31 are assumed zero, not measured: the VRS rates (2-5) and the eye id (7) because the driver supports neither variable-rate shading nor multiview, so a title that enables either would read other bits on the console, and bits 6, 12-15 and 29-31 because no source gives them a meaning. The sample field reads SampleId only in iterated shaders. The whole word has the same render target index at bits 16-28 - [Front-face input with `FRONT_FACE_ALL_BITS`](../../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) (shader recompiler) - with `SPI_BARYC_CNTL.FRONT_FACE_ALL_BITS` set, a back face reads 0 and a front face 0xffffffff. Mesa 22.0 RADV sets the bit ([radv_pipeline.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-22.0.0/src/amd/vulkan/radv_pipeline.c#L5953)) and ACO tests the input with `v_cmp_lg_u32 0` ([aco_instruction_selection.cpp](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-22.0.0/src/amd/compiler/aco_instruction_selection.cpp#L8123-L8128)), so only back = 0 and front ≠ 0 are sourced; PPSA05684's two-sided cloth shaders test it with `v_cmp_lt_u32 0`. The exact front value is not measured. Without the bit the input is ±1.0, as current Mesa reads it (`fgt 0`, `fsign`; [ac_nir_lower_intrinsics_to_args.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/bffd91947efc1932da99aca69fc540d6d4201b0c/src/amd/common/nir/ac_nir_lower_intrinsics_to_args.c)) - [Color comparison descriptor forms](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp): for formats AnyPS5 can sample as floating-point from GFX10 codes 1–77, `image_sample_c*` returns 0 for every comparison function except codes 7 (`16_UNORM`) and 22 (`32_FLOAT`), which use the native comparison path. The split was measured with point sampling on RDNA2 (gfx1031 and gfx1036) and with point and bilinear sampling on RDNA3 (gfx1100); the PS5's gfx1013 and 2D-array/cube comparisons across these formats remain unverified. Codes 128–130 (8-bit sRGB) also return 0, measured on gfx1035 over references -0.5..1.5 and texel codes 0..255. Scaled, integer and formats requiring converted sampling remain unsupported, although the tested hardware formats also return 0 for them. Anisotropic comparison of the remaining formats has not been measured. From 39f6f45487033c17eb8d6218fa7c5f1e2a3d03c1 Mon Sep 17 00:00:00 2001 From: Michael Krumland-Dunning Date: Sat, 24 Oct 2026 17:20:04 -0700 Subject: [PATCH 132/198] docs(shader): record 2D-array comparison samples measured on RDNA2 A comparison or plain sample with a 2D-array address on a 2D T# returns what the same sample with a 2D address returns, whatever the slice, as measured with the hardware oracle on RDNA2 (gfx1036) for formats 1, 7, 22 and 56 with point and bilinear filtering, while a four-layer 2D-array T# over the same memory returns a different result per slice. Codes 1 and 56 also compare to 0 with 2D-array addresses. The three comparison debt entries keep gfx1013, cube comparisons and the other formats' array comparisons as open. Co-Authored-By: Claude Opus 5.5 --- docs/dev/debt/shader.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index e7aba24b21..066ee060ea 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -16,7 +16,7 @@ - [MUBUF/MTBUF descriptors ending in VCC](../../../core/shader/recompiler/RdnaDecoder/src/RdnaMemoryOpDecoder.cpp): the four dword descriptor at s104 is s104, s105, vcc_lo and vcc_hi, as LLVM's [VCCTuple/SReg_128](https://github.com/llvm/llvm-project/blob/d8219b47422f7322db252ad29ca8ea87993fb433/llvm/lib/Target/AMDGPU/SIRegisterInfo.td#L422) and [buffer resource operands](https://github.com/llvm/llvm-project/blob/d8219b47422f7322db252ad29ca8ea87993fb433/llvm/lib/Target/AMDGPU/BUFInstructions.td) describe. Existing translation maps scalar codes106/107 to the raw VCC words. Recompiled loads/stores, both words, bounds, formats and wave32/wave64 execute on NVIDIA and AMD Vulkan; raw gfx10/gfx1013 hardware is not measured. Higher descriptor bases remain refused; the normal SGPR bound is unchanged. - [Packed pixel ancillary word](../../../core/shader/recompiler/Optimization/src/ConstantFolder.cpp) (shader recompiler) - the sample index at bits 8-11 and the render target index at bits 16-28 follow #295. The sample index is the SampleId builtin in a pixel shader the guest iterates per sample (`PS_ITER_SAMPLE` on a multisampled draw), an equality inferred, not measured. In any other pixel shader it is 0, the iterated sample number of a pixel the hardware does not iterate (also inferred), so reading it does not make the host shade per sample. The render target index is the Layer builtin when the draw exports it (a layered draw, `PA_CL_VS_OUT_CNTL` bit 18; the pipeline specialization constant `LayerExported`) and 0 otherwise, the first layer Vulkan renders when no pre-rasterization stage writes Layer. Reading the unwritten Layer builtin instead makes AMD's Windows driver drop the fragment's writes (#4800); with the constant at 0 the load is folded away. Mesa reads both fields from those bits, with the layer 14 bits wide only from GFX12 on ([ac_nir_lower_intrinsics_to_args.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/bffd91947efc1932da99aca69fc540d6d4201b0c/src/amd/common/nir/ac_nir_lower_intrinsics_to_args.c#L342-L368)). A value passed through a select, move or phi whose every read is an extract inside one of those fields is rebuilt from the two fields alone. Any other read the masked-select pass cannot drop gets the whole word: the primitive class at bits 0-1 (0 point, 1 line, 2 triangle, 3 rectangle), which the draw sets as a pipeline specialization constant ([RasterPrimitiveOf](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp)) in the shaders that read it, plus the two fields, with every other bit zero. That layout comes from the RDNA 3 and 3.5 ISA guides (prim type 1:0, VRS rates 3:2 and 5:4, eye id 7, sample 11:8, render target index 28:16). The GCN register guides give the same prim type and sample fields and put the render target index at 26:16, so the two sources agree for layers below 2048. AMD's LLPC reads the primitive class from bits 1:0 with the same encoding ([LowerGlCompatibility.cpp](https://github.com/GPUOpen-Drivers/llpc/blob/40cb8d95ad8d6f7f1652e3fd47d39667594cce08/llpc/lowering/LowerGlCompatibility.cpp#L830-L836)) and without a GFX-level check ([getPrimType](https://github.com/GPUOpen-Drivers/llpc/blob/40cb8d95ad8d6f7f1652e3fd47d39667594cce08/lgc/lowering/LowerInOut.cpp#L5307-L5315)). Nobody has checked it on gfx1013. In the whole word only bits 0-1, 8-11 and 16-28 have a source. Bits 2-7, 12-15 and 29-31 are assumed zero, not measured: the VRS rates (2-5) and the eye id (7) because the driver supports neither variable-rate shading nor multiview, so a title that enables either would read other bits on the console, and bits 6, 12-15 and 29-31 because no source gives them a meaning. The sample field reads SampleId only in iterated shaders. The whole word has the same render target index at bits 16-28 - [Front-face input with `FRONT_FACE_ALL_BITS`](../../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) (shader recompiler) - with `SPI_BARYC_CNTL.FRONT_FACE_ALL_BITS` set, a back face reads 0 and a front face 0xffffffff. Mesa 22.0 RADV sets the bit ([radv_pipeline.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-22.0.0/src/amd/vulkan/radv_pipeline.c#L5953)) and ACO tests the input with `v_cmp_lg_u32 0` ([aco_instruction_selection.cpp](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-22.0.0/src/amd/compiler/aco_instruction_selection.cpp#L8123-L8128)), so only back = 0 and front ≠ 0 are sourced; PPSA05684's two-sided cloth shaders test it with `v_cmp_lt_u32 0`. The exact front value is not measured. Without the bit the input is ±1.0, as current Mesa reads it (`fgt 0`, `fsign`; [ac_nir_lower_intrinsics_to_args.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/bffd91947efc1932da99aca69fc540d6d4201b0c/src/amd/common/nir/ac_nir_lower_intrinsics_to_args.c)) -- [Color comparison descriptor forms](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp): for formats AnyPS5 can sample as floating-point from GFX10 codes 1–77, `image_sample_c*` returns 0 for every comparison function except codes 7 (`16_UNORM`) and 22 (`32_FLOAT`), which use the native comparison path. The split was measured with point sampling on RDNA2 (gfx1031 and gfx1036) and with point and bilinear sampling on RDNA3 (gfx1100); the PS5's gfx1013 and 2D-array/cube comparisons across these formats remain unverified. Codes 128–130 (8-bit sRGB) also return 0, measured on gfx1035 over references -0.5..1.5 and texel codes 0..255. Scaled, integer and formats requiring converted sampling remain unsupported, although the tested hardware formats also return 0 for them. Anisotropic comparison of the remaining formats has not been measured. +- [Color comparison descriptor forms](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp): for formats AnyPS5 can sample as floating-point from GFX10 codes 1–77, `image_sample_c*` returns 0 for every comparison function except codes 7 (`16_UNORM`) and 22 (`32_FLOAT`), which use the native comparison path. The split was measured with point sampling on RDNA2 (gfx1031 and gfx1036) and with point and bilinear sampling on RDNA3 (gfx1100); the PS5's gfx1013, cube comparisons and 2D-array comparisons of formats other than 1, 7, 22 and 56 remain unverified (on gfx1036, codes 1 and 56 return 0 with DIM 2D_ARRAY through 2D-array and 2D T#s too). Codes 128–130 (8-bit sRGB) also return 0, measured on gfx1035 over references -0.5..1.5 and texel codes 0..255. Scaled, integer and formats requiring converted sampling remain unsupported, although the tested hardware formats also return 0 for them. Anisotropic comparison of the remaining formats has not been measured. - [s_setkill](../../../core/shader/recompiler/RdnaDecoder/src/RdnaScalarOpDecoder.cpp) (core/shader/recompiler) - decoded as `s_endpgm` for any immediate: measured on RDNA2 hardware (gfx1035), a store before it is visible and nothing after it runs ([#771](https://github.com/boykopovar/AnyPS5/pull/771)); whether the KILL status bit it sets is observable differently from `s_endpgm` is unverified - [s_sethalt](../../../core/shader/recompiler/Translation/src/ScalarInstructions.cpp) (core/shader/recompiler) - with SIMM16[0] clear it is a no-op: the RDNA 2 ISA guide describes that value as resuming a halted wave, and a wave that executes it is not halted; measured on a gfx1035 (RDNA 2) wave: `s_sethalt 0` followed by a VALU op completes and gives the normal result. Only SIMM16[0] is read; with it set the instruction throws - [s_subvector_loop_begin and s_subvector_loop_end](../../../core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp) (core/shader/recompiler) - follow AMD's RDNA 2 instruction set reference and a gfx1036 (wave64) measurement, pinned in [SubvectorLoop.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/SubvectorLoop.cpp); `S0` in the end instruction's `elif(S0 == 0)` is read as its SGPR operand. A wave32 shader and a destination other than an SGPR throw because neither is measured. The begin instruction tests for an empty mask as `(exec_lo | exec_hi) == 0`: AMD's Windows driver (32.0.21045.5002 and 32.0.21042.62) compiled `exec_lo == 0 && exec_hi == 0` as a test of the whole 64-bit mask when guest loads use selects, as they do with `robustBufferAccess2` (#7376, #7623). Measured on PC RDNA2 (Radeon 610M), not on a PS5. The [specialization pass](../../../core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp) keeps a constant-condition branch whose untaken target is a loop header, since folding it removes the loop's back edge, which SPIR-V requires exactly once per loop header (SPIRV-Tools rejects the module and the NVIDIA driver 615.78.08 crashes compiling it); a loop whose back-edge block becomes unreachable in another way is not handled @@ -96,7 +96,7 @@ - A [V#, T# or S#](../../../core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp) loaded through a null or unmapped pointer read from memory is zero when the work is recorded, and the capture throws if no flat SRT slot records that read. - [2D accesses through a 1D T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) read the texel the 1D addressing selects and ignore the second coordinate, as simonbbby measured on RDNA2 (gfx1036) in #2237 for `image_sample_lz` and mip-less `image_load` with point filtering and wrap, clamp and border addressing; `image_sample_l` was measured the same way on gfx1036 against `image_sample_l` with DIM 1D through the same T#, for u inside, on and past the edges, v of 0, 0.5, 1, -0.25 and 3, LOD -1 to 6 on a 5-level R32_FLOAT texture, point, linear and linear-mip filtering, wrap, clamp and border addressing and HEIGHT or DEPTH 3: all 4725 rows matched. The PS5's gfx1013 is not measured. A plain `image_sample` (implicit LOD) through such a pair takes its LOD from the derivatives of the first coordinate alone, measured on RDNA2 (gfx1036) with the [hardware oracle](../HW_ORACLE.md) on the quads of a compute kernel (wave32 and wave64, IEEE off, DX10 clamp on, denormals preserved, round to nearest even): on a 16-texel, 5-mip 32_UINT T# with point filters it picks the same mip and texel as DIM 1D for first-coordinate steps of 0, 1, 2, 4 and 8 texels per lane along x or along y, whatever the second coordinate does (normalised steps up to 16 per lane), while the same rows on a 16x16 2D T# move the mip with the second coordinate. Outside the pixel stage a plain `image_sample` is emitted with LOD 0 for every image type, so this pair reads mip 0 there, where the kernel measurement shows the quad's derivatives being used. `image_sample_c_lz` and plain `image_sample_c` through such a pair compare against the texel the DIM 1D comparison with the same reference and first coordinate reads, measured on RDNA1 (gfx1010, RX 5700 XT) with the [hardware oracle](../HW_ORACLE.md) template run through the Windows driver's comgr and HIP runtime (wave32 and wave64, IEEE off, DX10 clamp on, denormals preserved, round to nearest even): 8-texel 32_FLOAT and 16_UNORM T#s, also with HEIGHT 3, point and bilinear filters, wrap, clamp and opaque-white border addressing, less and gequal functions, second coordinates 0, 0.5, 1, 1.5, -0.25 and 3; on a 64-texel, 7-mip 32_FLOAT T# `image_sample_c` picks its LOD from the first coordinate's steps alone (1, 4 and 16 texels per lane give mips 0, 2 and 4; second-coordinate steps up to 64 texels leave it unchanged). Only comparisons the runtime does natively (32_FLOAT, 16_UNORM) reach the 1D view; comparisons the runtime emulates for other color formats still throw through such a pair. 2D-array `image_sample_l` and `image_sample_lz` through a 1D T# read as DIM 1D too, ignoring v and the slice: measured on gfx1036 with the hardware oracle (template assembled by the AMD Windows driver's `amd_comgr_2.dll` and launched through its `amdhip64_6.dll`) against the same sample with DIM 1D on a 16-texel 8_8_8_8 UNORM T# with 1 or 5 levels, HEIGHT or DEPTH 3, for u inside, on and past the edges, v of 0, 0.5, 1, -0.25 and 3, slices 0, 1, 2.7, -1 and 5, LOD -1 to 6, and the five filter and address cases above: all 48000 rows matched. A 2D-array `image_load` through a 1D T# is not the same: on gfx1036 it returns the DIM 1D texel only at slice 0, and zeros at slices 1, 2, 5 and -1 even with DEPTH 3, so it, the plain `image_sample` and the other 2D-array accesses still throw. Other 2D accesses through a 1D T# (storage, depth, packed, BY loads, mips other than explicit LOD, explicit LOD with comparison, bias, offsets, derivatives) still throw. - [3D image accesses bound to a 1D T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) follow a [hardware oracle](../HW_ORACLE.md) measurement on RDNA1 (RX 5700 XT, gfx1010; wave32 and wave64, IEEE off, DX10 clamp on, denormals preserved, round to nearest even), not the PS5's gfx1013, on 8x1 R8G8B8A8_UNORM linear 1D T#s with HEIGHT and DEPTH 0 or 3 and point filters. `image_sample_lz` reads the texel u selects for v and r in {0, 0.5, 1, 1.5, -0.25, 3}, with wrap, clamp and border addressing, and v and r never produce the border; u wraps, clamps and borders as with DIM 1D. Mip-less `image_load` reads the x texel for any y in {0, 1, 2, 3, -1} when z is 0, returns zero for z in {1, 2, 3, -1}, whatever DEPTH says, and returns zero for x past the width. A plain `image_sample` (implicit LOD) on the quads of a compute kernel takes its texel and LOD from u alone: on a 64-texel, 7-mip 1D T# steps of 1, 4 and 16 texels per lane pick mips 0, 2 and 4, as DIM 1D `image_sample_l` does, and steps in v or r alone stay at mip 0. Outside the pixel stage a plain `image_sample` is emitted with LOD 0, as for 2D accesses through a 1D T#. Every other 3D access of such an image (`image_load` with mips, storage, explicit LOD, bias, derivatives, offsets, comparison, gathers, queries) keeps throwing. SONIC SUPERSTARS (PPSA06888) samples a 1x1 1D T# this way with plain `image_sample` and `image_load`. -- [Emulated comparison of a 2D-array access through a 2D T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) samples the 2D image and ignores the slice, as the native modes of a 2D-array access through a 2D T# already do. This follows the T# type deciding the addressing; it is not measured on hardware for comparison samples +- [Emulated comparison of a 2D-array access through a 2D T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) samples the 2D image and ignores the slice; the PS5's gfx1013 is not measured. On RDNA2 (gfx1036, AMD Windows driver 32.0.21030.31, [hardware oracle](../HW_ORACLE.md)), `image_sample_c_lz` and `image_sample_lz` with DIM 2D_ARRAY on linear 2D T#s of formats 1, 7, 22 and 56 return what the same samples with DIM 2D return, for slices 0, 1, 2, 3, 0.6, 1.4, -1 and 5, point and bilinear filtering and `DEPTH` 0 and 3, while a four-layer 2D-array T# over the same memory returns a different result per slice - [Flat aperture bases](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (core/shader/recompiler) - the layout follows the gfx9+ one in the AMD KFD driver's `kfd_flat_memory.c` (`MAKE_LDS_APP_BASE_V9`, `MAKE_LDS_APP_LIMIT`): each aperture is a 4 GB window whose base is the high dword, so a flat address is routed by its whole high dword, as measured on RDNA2 (gfx1035). How the console routes a flat address was not measured. Its bases, high dwords 0x80000000 (shared, `SharedApertureHigh`) and 0x70000000 (private, `PrivateApertureHigh`), first inferred from PPSA04263's traversal addresses, are measured: on a PS5 (13.40, measured 2026-10-19 by a gfx1013 kernel run from inside a retail title) `src_shared_base` / `src_shared_limit` read 0x80000000_00000000 / 0x80000000_FFFFFFFF and `src_private_base` / `src_private_limit` 0x70000000_00000000 / 0x70000000_FFFFFFFF. The scalar aperture sources read the same two values - [Flat accesses outside the aperture routing](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (core/shader/recompiler) - only compute stages route flat accesses, so graphics-stage flat accesses to the shared or private aperture still go to BDA. In compute, a 64-bit flat atomic whose address is in the shared aperture takes the workgroup lock (`ldsLock`) for its two-dword update, and every such atomic requests that lock, so a shader with one pays for it even on global addresses (the path is exercised by `agc_driver_global_atomics_float`). Inferred, not measured on LDS or scratch: an aperture access that is not inside one dword is done byte by byte, an unaligned aperture atomic records the BDA unaligned fault, and LDS or scratch accesses past their end are dropped or read zero. - On a host subgroup narrower than 32 lanes, [`threadBit`](../../../core/shader/recompiler/Translation/src/TranslationContext/ExecMask.cpp) reads an EXEC written as a value (`s_mov_b32`/`s_mov_b64`) at bit (subgroup invocation id) of EXEC_LO, so lanes at or above the subgroup size read a lower lane's bit. @@ -157,7 +157,7 @@ - [FP16_OVFL](../../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) follows the shader's float mode as measured on RDNA2 (gfx1036) at IEEE=0 and IEEE=1 with f16 denormals kept and flushed: with `FP16_OVFL`=1 an f16 result that rounds to ±infinity is written as ±0x7bff unless a source operand is infinite, so `v_rcp_f16`, `v_rsq_f16` and `v_log_f16` of ±0 and `v_exp_f16` of 16 or more are clamped while an infinite input passes through, and `v_div_fixup_f16` keeps the infinity it writes for a zero denominator or an infinite numerator and clamps any other infinite quotient, a NaN one included. `v_cvt_pkrtz_f16_f32` rounds toward zero and never overflows. The sources are checked as the f32 values the translation computes with; the rule is applied to `v_fma_mix*_f16` with f32 sources and to `v_interp_p2_f16` without a measurement of infinite f32 sources or parameters. Round modes other than nearest even are not modelled, so the overflow threshold is the nearest-even one (65520). - [Directed f16 rounding](../../../core/shader/recompiler/Translation/src/TranslationContext/SubwordPacking.cpp) for `v_cvt_f16_f32`, `v_cvt_f16_i16`, `v_cvt_f16_u16` and `v_ldexp_f16` follows `FLOAT_MODE` bits 3:2 in every f16 denormal mode, measured on RDNA2 (gfx1036 with f16 denormals preserved, gfx1030 in all sixteen combinations of f16 rounding and denormal mode). When bit 7 flushes f16 results, `v_cvt_f16_f32`, `v_cvt_f16_i16` and `v_cvt_f16_u16` write a zero of the result's sign for a value that, rounded in that mode with an unbounded exponent, is below 2^-14, so a value just below 2^-14 that rounds up to it stays 0x0400; `v_ldexp_f16` keeps its rule above. The `clamp` modifier is applied before the rounding. On gfx1030 every row matches: 8,996 f32 inputs around the half denormal, normal and overflow boundaries with and without `clamp`, every 16-bit integer, and 2,470 `v_ldexp_f16` operands. - [v_cvt_pkrtz_f16_f32 and v_pack_b32_f16 denormals](../../../core/shader/recompiler/Translation/src/ConvertInstructions.cpp) follow the `FLOAT_MODE` f16 denormal field: `v_cvt_pkrtz_f16_f32` flushes each denormal half result to its signed zero when bit 1 is clear (0 and 1), and `v_pack_b32_f16` flushes each denormal half input to its signed zero when bit 0 is clear (0 and 2). Measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) at IEEE=0 and IEEE=1 with denorm16 0..3 and f32 denormals kept, over 256 pairs of 16 f32 values around the f16 denormal range for `v_cvt_pkrtz_f16_f32` and 256 pairs of 16 halves (zeros, denormals, minimum normals, ±1, the largest finite value, infinities and quiet and signaling NaNs) for `v_pack_b32_f16`. Shaders without a known float mode keep the previous lowering. The PS5's gfx1013 is not measured. -- [Comparison sampling of a color texture](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (`image_sample_c`/`image_sample_c_lz` on a texture that is not R32 float or R16 unorm) returns 0 for every comparison function on formats AnyPS5 can sample as floating-point measured on GFX10 (codes 1–77 except native comparison codes 7 and 22) and on the corresponding GFX11 formats measured on gfx1100. The current Vulkan path still lowers these formats through the shader; the compare result is constant zero. Scaled, integer and formats requiring converted sampling remain refused. Only 2D, 2D-array and cube views are accepted; image tables, min/max reduction, unnormalized coordinates and unsupported addressing remain refused. Point and bilinear results match on gfx1100. The PS5's gfx1013, array/cube behavior for the measured formats, and anisotropic comparisons remain unverified. Bilinear weights are full float, not the hardware's fixed-point subtexel weights +- [Comparison sampling of a color texture](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (`image_sample_c`/`image_sample_c_lz` on a texture that is not R32 float or R16 unorm) returns 0 for every comparison function on formats AnyPS5 can sample as floating-point measured on GFX10 (codes 1–77 except native comparison codes 7 and 22) and on the corresponding GFX11 formats measured on gfx1100. The current Vulkan path still lowers these formats through the shader; the compare result is constant zero. Scaled, integer and formats requiring converted sampling remain refused. Only 2D, 2D-array and cube views are accepted; image tables, min/max reduction, unnormalized coordinates and unsupported addressing remain refused. Point and bilinear results match on gfx1100. The PS5's gfx1013, cube behavior, array behavior except codes 1 and 56, and anisotropic comparisons remain unverified. Bilinear weights are full float, not the hardware's fixed-point subtexel weights - [Negative color-texture comparison wrap indices](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (libSceAgcDriver) use explicit `OpSRem` normalization to implement modulo; the negative-offset point-wrap regression fails with direct `OpSMod` and passes with normalization on Mesa 22.3.6 llvmpipe (LLVM 15.0.6), but negative-index wrap has not been measured on PS5 hardware. - [Converted unorm images](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (10_11_11_UNORM, read and written through an R32_UINT view) are sampled in the shader, the way color comparisons are emulated: `image_sample` of a 2D or 2D array view fetches the one or four texels at LOD 0, decodes them and blends them, for point or bilinear filtering with equal minification and magnification filters and wrap or clamp-to-edge addressing. A shader gets these sample modes only once a converted descriptor reaches one of its sampled images: that capture throws `ConvertedSampleRequired`, the driver records the program and prepares it again with `GuestContext::convertedSamples`, so such a shader is prepared twice per run and its first capture is repeated. The record lives with the registered program and is not kept across runs. Bilinear weights are rounded to the hardware's 8-bit grid (`roundEven(fraction * 256) / 256`, ties to even), as measured with `image_sample_lz` on RDNA2 (gfx1035) for 10_11_11 UNORM texels by adriadam10 in #5958; point sampling, decoding, swizzles and wrap and clamp-to-edge addressing matched the same measurement. A sample that is not `_lz` needs an image with one mip level; gathers, LOD queries, clamp-to-border and mirrored addressing, other filters, min/max reduction, unnormalized coordinates, images selected at run time, 16-bit data and a DST_SEL of the missing W channel throw. - [64-bit LDS atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_*_u64`, `ds_*_b64`, `ds_*_f64`) read and write the two dwords under a workgroup lock stored after the guest LDS, as LDS is a 32-bit array without 64-bit atomics. On RDNA2 (gfx1036) the LDS serialises a 32-bit atomic on a slot's high dword and a 64-bit atomic on that slot: 128 lanes of `ds_add_u32` and 128 lanes of `ds_add_u64` on one slot sum exactly. Only that pair was measured. Every 32-bit LDS atomic of a program that contains a 64-bit LDS atomic therefore takes the same lock, including a 32-bit `flat_atomic_*` whose address lands in the LDS aperture (a 64-bit one takes it already); the flat forms were not measured against a 64-bit atomic either (the other 32-bit LDS atomics, `ds_min/max`, `ds_cmpst`, the float forms, `ds_inc/dec`, `ds_mskor` and `ds_wrxchg`, are extrapolated from `ds_add_u32` and were not measured against a 64-bit atomic); a program without a 64-bit LDS atomic keeps its native 32-bit atomics. `ds_condxchg32_rtn_b64` is two `ds_mskor` halves, each under the lock but not atomic as a pair, so a 64-bit atomic between the two halves is not excluded. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms) and plain `ds_read_*`, `ds_write_*`, `ds_append_u32` and `ds_consume_u32` are not taken under the lock, so a plain access racing a 64-bit atomic on the same dwords can see or overwrite one half of it; whether the hardware orders them was not measured. The lock serialises the workgroup's LDS atomics of such a program; the throughput cost was not measured. `VK_KHR_workgroup_memory_explicit_layout` with `shaderSharedInt64Atomics` would make both widths native and is not used. A 64-bit atomic at an address that is 4-byte but not 8-byte aligned faults the wave on gfx1036, as on gfx1035; with the workgroup lock, in device memory and in the invocation-private LDS of other stages alike, the recompiler records the `Unaligned` shader fault with the LDS byte address, leaves both dwords unchanged and returns 0, but the lane and the rest of its wave keep running. When the LDS lives in device memory (compute LDS beyond the device's shared-memory limit) and the device has `shaderBufferInt64Atomics`, no lock is used: a 64-bit atomic is a compare-exchange loop on a 64-bit view of the storage buffer and a 32-bit atomic is a native buffer atomic, so a mixed-width pair on one slot relies on the device ordering both widths on one buffer address; only `ds_add_u32` against `ds_add_u64` was measured, on gfx1036 (AMD Windows driver 32.0.21042.62) and an RTX 4070 SUPER. The lock is not used there because AMD's Windows driver compiles an acquire or release on a storage buffer, at workgroup scope or wider, to `s_barrier`, and those barriers inside the lock's divergent loop pair with the program's own `s_barrier`s. The fence after each LDS atomic compiles to `s_barrier` on that driver as well, so a wave that skips an LDS atomic that other waves of its workgroup execute misses a barrier they reach; its effect was not measured. From 854be22d20dde59492f17d52896573b969ecefec Mon Sep 17 00:00:00 2001 From: Michael Krumland-Dunning Date: Sun, 25 Oct 2026 16:00:41 -0700 Subject: [PATCH 133/198] docs(shader): extend the 2D-array comparison measurement to every compare function The 2D-array comparison results were measured with LESSEQUAL only. A second gfx1036 run over all eight DEPTH_COMPARE_FUNC values (128 kernels) gives the same: a 2D-array address on a 2D T# ignores the slice, and codes 1 and 56 return 0 under every function. Both entries now say so. Co-Authored-By: Claude Opus 5.5 --- docs/dev/debt/shader.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 066ee060ea..4d31e1ae92 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -16,7 +16,7 @@ - [MUBUF/MTBUF descriptors ending in VCC](../../../core/shader/recompiler/RdnaDecoder/src/RdnaMemoryOpDecoder.cpp): the four dword descriptor at s104 is s104, s105, vcc_lo and vcc_hi, as LLVM's [VCCTuple/SReg_128](https://github.com/llvm/llvm-project/blob/d8219b47422f7322db252ad29ca8ea87993fb433/llvm/lib/Target/AMDGPU/SIRegisterInfo.td#L422) and [buffer resource operands](https://github.com/llvm/llvm-project/blob/d8219b47422f7322db252ad29ca8ea87993fb433/llvm/lib/Target/AMDGPU/BUFInstructions.td) describe. Existing translation maps scalar codes106/107 to the raw VCC words. Recompiled loads/stores, both words, bounds, formats and wave32/wave64 execute on NVIDIA and AMD Vulkan; raw gfx10/gfx1013 hardware is not measured. Higher descriptor bases remain refused; the normal SGPR bound is unchanged. - [Packed pixel ancillary word](../../../core/shader/recompiler/Optimization/src/ConstantFolder.cpp) (shader recompiler) - the sample index at bits 8-11 and the render target index at bits 16-28 follow #295. The sample index is the SampleId builtin in a pixel shader the guest iterates per sample (`PS_ITER_SAMPLE` on a multisampled draw), an equality inferred, not measured. In any other pixel shader it is 0, the iterated sample number of a pixel the hardware does not iterate (also inferred), so reading it does not make the host shade per sample. The render target index is the Layer builtin when the draw exports it (a layered draw, `PA_CL_VS_OUT_CNTL` bit 18; the pipeline specialization constant `LayerExported`) and 0 otherwise, the first layer Vulkan renders when no pre-rasterization stage writes Layer. Reading the unwritten Layer builtin instead makes AMD's Windows driver drop the fragment's writes (#4800); with the constant at 0 the load is folded away. Mesa reads both fields from those bits, with the layer 14 bits wide only from GFX12 on ([ac_nir_lower_intrinsics_to_args.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/bffd91947efc1932da99aca69fc540d6d4201b0c/src/amd/common/nir/ac_nir_lower_intrinsics_to_args.c#L342-L368)). A value passed through a select, move or phi whose every read is an extract inside one of those fields is rebuilt from the two fields alone. Any other read the masked-select pass cannot drop gets the whole word: the primitive class at bits 0-1 (0 point, 1 line, 2 triangle, 3 rectangle), which the draw sets as a pipeline specialization constant ([RasterPrimitiveOf](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp)) in the shaders that read it, plus the two fields, with every other bit zero. That layout comes from the RDNA 3 and 3.5 ISA guides (prim type 1:0, VRS rates 3:2 and 5:4, eye id 7, sample 11:8, render target index 28:16). The GCN register guides give the same prim type and sample fields and put the render target index at 26:16, so the two sources agree for layers below 2048. AMD's LLPC reads the primitive class from bits 1:0 with the same encoding ([LowerGlCompatibility.cpp](https://github.com/GPUOpen-Drivers/llpc/blob/40cb8d95ad8d6f7f1652e3fd47d39667594cce08/llpc/lowering/LowerGlCompatibility.cpp#L830-L836)) and without a GFX-level check ([getPrimType](https://github.com/GPUOpen-Drivers/llpc/blob/40cb8d95ad8d6f7f1652e3fd47d39667594cce08/lgc/lowering/LowerInOut.cpp#L5307-L5315)). Nobody has checked it on gfx1013. In the whole word only bits 0-1, 8-11 and 16-28 have a source. Bits 2-7, 12-15 and 29-31 are assumed zero, not measured: the VRS rates (2-5) and the eye id (7) because the driver supports neither variable-rate shading nor multiview, so a title that enables either would read other bits on the console, and bits 6, 12-15 and 29-31 because no source gives them a meaning. The sample field reads SampleId only in iterated shaders. The whole word has the same render target index at bits 16-28 - [Front-face input with `FRONT_FACE_ALL_BITS`](../../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) (shader recompiler) - with `SPI_BARYC_CNTL.FRONT_FACE_ALL_BITS` set, a back face reads 0 and a front face 0xffffffff. Mesa 22.0 RADV sets the bit ([radv_pipeline.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-22.0.0/src/amd/vulkan/radv_pipeline.c#L5953)) and ACO tests the input with `v_cmp_lg_u32 0` ([aco_instruction_selection.cpp](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-22.0.0/src/amd/compiler/aco_instruction_selection.cpp#L8123-L8128)), so only back = 0 and front ≠ 0 are sourced; PPSA05684's two-sided cloth shaders test it with `v_cmp_lt_u32 0`. The exact front value is not measured. Without the bit the input is ±1.0, as current Mesa reads it (`fgt 0`, `fsign`; [ac_nir_lower_intrinsics_to_args.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/bffd91947efc1932da99aca69fc540d6d4201b0c/src/amd/common/nir/ac_nir_lower_intrinsics_to_args.c)) -- [Color comparison descriptor forms](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp): for formats AnyPS5 can sample as floating-point from GFX10 codes 1–77, `image_sample_c*` returns 0 for every comparison function except codes 7 (`16_UNORM`) and 22 (`32_FLOAT`), which use the native comparison path. The split was measured with point sampling on RDNA2 (gfx1031 and gfx1036) and with point and bilinear sampling on RDNA3 (gfx1100); the PS5's gfx1013, cube comparisons and 2D-array comparisons of formats other than 1, 7, 22 and 56 remain unverified (on gfx1036, codes 1 and 56 return 0 with DIM 2D_ARRAY through 2D-array and 2D T#s too). Codes 128–130 (8-bit sRGB) also return 0, measured on gfx1035 over references -0.5..1.5 and texel codes 0..255. Scaled, integer and formats requiring converted sampling remain unsupported, although the tested hardware formats also return 0 for them. Anisotropic comparison of the remaining formats has not been measured. +- [Color comparison descriptor forms](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp): for formats AnyPS5 can sample as floating-point from GFX10 codes 1–77, `image_sample_c*` returns 0 for every comparison function except codes 7 (`16_UNORM`) and 22 (`32_FLOAT`), which use the native comparison path. The split was measured with point sampling on RDNA2 (gfx1031 and gfx1036) and with point and bilinear sampling on RDNA3 (gfx1100); the PS5's gfx1013, cube comparisons and 2D-array comparisons of formats other than 1, 7, 22 and 56 remain unverified (on gfx1036, 1 and 56 return 0 for all eight functions with DIM 2D_ARRAY on 2D-array and 2D T#s). Codes 128–130 (8-bit sRGB) also return 0, measured on gfx1035 over references -0.5..1.5 and texel codes 0..255. Scaled, integer and formats requiring converted sampling remain unsupported, although the tested hardware formats also return 0 for them. Anisotropic comparison of the remaining formats has not been measured. - [s_setkill](../../../core/shader/recompiler/RdnaDecoder/src/RdnaScalarOpDecoder.cpp) (core/shader/recompiler) - decoded as `s_endpgm` for any immediate: measured on RDNA2 hardware (gfx1035), a store before it is visible and nothing after it runs ([#771](https://github.com/boykopovar/AnyPS5/pull/771)); whether the KILL status bit it sets is observable differently from `s_endpgm` is unverified - [s_sethalt](../../../core/shader/recompiler/Translation/src/ScalarInstructions.cpp) (core/shader/recompiler) - with SIMM16[0] clear it is a no-op: the RDNA 2 ISA guide describes that value as resuming a halted wave, and a wave that executes it is not halted; measured on a gfx1035 (RDNA 2) wave: `s_sethalt 0` followed by a VALU op completes and gives the normal result. Only SIMM16[0] is read; with it set the instruction throws - [s_subvector_loop_begin and s_subvector_loop_end](../../../core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp) (core/shader/recompiler) - follow AMD's RDNA 2 instruction set reference and a gfx1036 (wave64) measurement, pinned in [SubvectorLoop.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/SubvectorLoop.cpp); `S0` in the end instruction's `elif(S0 == 0)` is read as its SGPR operand. A wave32 shader and a destination other than an SGPR throw because neither is measured. The begin instruction tests for an empty mask as `(exec_lo | exec_hi) == 0`: AMD's Windows driver (32.0.21045.5002 and 32.0.21042.62) compiled `exec_lo == 0 && exec_hi == 0` as a test of the whole 64-bit mask when guest loads use selects, as they do with `robustBufferAccess2` (#7376, #7623). Measured on PC RDNA2 (Radeon 610M), not on a PS5. The [specialization pass](../../../core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp) keeps a constant-condition branch whose untaken target is a loop header, since folding it removes the loop's back edge, which SPIR-V requires exactly once per loop header (SPIRV-Tools rejects the module and the NVIDIA driver 615.78.08 crashes compiling it); a loop whose back-edge block becomes unreachable in another way is not handled @@ -96,7 +96,7 @@ - A [V#, T# or S#](../../../core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp) loaded through a null or unmapped pointer read from memory is zero when the work is recorded, and the capture throws if no flat SRT slot records that read. - [2D accesses through a 1D T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) read the texel the 1D addressing selects and ignore the second coordinate, as simonbbby measured on RDNA2 (gfx1036) in #2237 for `image_sample_lz` and mip-less `image_load` with point filtering and wrap, clamp and border addressing; `image_sample_l` was measured the same way on gfx1036 against `image_sample_l` with DIM 1D through the same T#, for u inside, on and past the edges, v of 0, 0.5, 1, -0.25 and 3, LOD -1 to 6 on a 5-level R32_FLOAT texture, point, linear and linear-mip filtering, wrap, clamp and border addressing and HEIGHT or DEPTH 3: all 4725 rows matched. The PS5's gfx1013 is not measured. A plain `image_sample` (implicit LOD) through such a pair takes its LOD from the derivatives of the first coordinate alone, measured on RDNA2 (gfx1036) with the [hardware oracle](../HW_ORACLE.md) on the quads of a compute kernel (wave32 and wave64, IEEE off, DX10 clamp on, denormals preserved, round to nearest even): on a 16-texel, 5-mip 32_UINT T# with point filters it picks the same mip and texel as DIM 1D for first-coordinate steps of 0, 1, 2, 4 and 8 texels per lane along x or along y, whatever the second coordinate does (normalised steps up to 16 per lane), while the same rows on a 16x16 2D T# move the mip with the second coordinate. Outside the pixel stage a plain `image_sample` is emitted with LOD 0 for every image type, so this pair reads mip 0 there, where the kernel measurement shows the quad's derivatives being used. `image_sample_c_lz` and plain `image_sample_c` through such a pair compare against the texel the DIM 1D comparison with the same reference and first coordinate reads, measured on RDNA1 (gfx1010, RX 5700 XT) with the [hardware oracle](../HW_ORACLE.md) template run through the Windows driver's comgr and HIP runtime (wave32 and wave64, IEEE off, DX10 clamp on, denormals preserved, round to nearest even): 8-texel 32_FLOAT and 16_UNORM T#s, also with HEIGHT 3, point and bilinear filters, wrap, clamp and opaque-white border addressing, less and gequal functions, second coordinates 0, 0.5, 1, 1.5, -0.25 and 3; on a 64-texel, 7-mip 32_FLOAT T# `image_sample_c` picks its LOD from the first coordinate's steps alone (1, 4 and 16 texels per lane give mips 0, 2 and 4; second-coordinate steps up to 64 texels leave it unchanged). Only comparisons the runtime does natively (32_FLOAT, 16_UNORM) reach the 1D view; comparisons the runtime emulates for other color formats still throw through such a pair. 2D-array `image_sample_l` and `image_sample_lz` through a 1D T# read as DIM 1D too, ignoring v and the slice: measured on gfx1036 with the hardware oracle (template assembled by the AMD Windows driver's `amd_comgr_2.dll` and launched through its `amdhip64_6.dll`) against the same sample with DIM 1D on a 16-texel 8_8_8_8 UNORM T# with 1 or 5 levels, HEIGHT or DEPTH 3, for u inside, on and past the edges, v of 0, 0.5, 1, -0.25 and 3, slices 0, 1, 2.7, -1 and 5, LOD -1 to 6, and the five filter and address cases above: all 48000 rows matched. A 2D-array `image_load` through a 1D T# is not the same: on gfx1036 it returns the DIM 1D texel only at slice 0, and zeros at slices 1, 2, 5 and -1 even with DEPTH 3, so it, the plain `image_sample` and the other 2D-array accesses still throw. Other 2D accesses through a 1D T# (storage, depth, packed, BY loads, mips other than explicit LOD, explicit LOD with comparison, bias, offsets, derivatives) still throw. - [3D image accesses bound to a 1D T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) follow a [hardware oracle](../HW_ORACLE.md) measurement on RDNA1 (RX 5700 XT, gfx1010; wave32 and wave64, IEEE off, DX10 clamp on, denormals preserved, round to nearest even), not the PS5's gfx1013, on 8x1 R8G8B8A8_UNORM linear 1D T#s with HEIGHT and DEPTH 0 or 3 and point filters. `image_sample_lz` reads the texel u selects for v and r in {0, 0.5, 1, 1.5, -0.25, 3}, with wrap, clamp and border addressing, and v and r never produce the border; u wraps, clamps and borders as with DIM 1D. Mip-less `image_load` reads the x texel for any y in {0, 1, 2, 3, -1} when z is 0, returns zero for z in {1, 2, 3, -1}, whatever DEPTH says, and returns zero for x past the width. A plain `image_sample` (implicit LOD) on the quads of a compute kernel takes its texel and LOD from u alone: on a 64-texel, 7-mip 1D T# steps of 1, 4 and 16 texels per lane pick mips 0, 2 and 4, as DIM 1D `image_sample_l` does, and steps in v or r alone stay at mip 0. Outside the pixel stage a plain `image_sample` is emitted with LOD 0, as for 2D accesses through a 1D T#. Every other 3D access of such an image (`image_load` with mips, storage, explicit LOD, bias, derivatives, offsets, comparison, gathers, queries) keeps throwing. SONIC SUPERSTARS (PPSA06888) samples a 1x1 1D T# this way with plain `image_sample` and `image_load`. -- [Emulated comparison of a 2D-array access through a 2D T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) samples the 2D image and ignores the slice; the PS5's gfx1013 is not measured. On RDNA2 (gfx1036, AMD Windows driver 32.0.21030.31, [hardware oracle](../HW_ORACLE.md)), `image_sample_c_lz` and `image_sample_lz` with DIM 2D_ARRAY on linear 2D T#s of formats 1, 7, 22 and 56 return what the same samples with DIM 2D return, for slices 0, 1, 2, 3, 0.6, 1.4, -1 and 5, point and bilinear filtering and `DEPTH` 0 and 3, while a four-layer 2D-array T# over the same memory returns a different result per slice +- [Emulated comparison of a 2D-array access through a 2D T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) samples the 2D image and ignores the slice; the PS5's gfx1013 is not measured. On RDNA2 (gfx1036, AMD Windows driver 32.0.21030.31, [hardware oracle](../HW_ORACLE.md)), `image_sample_c_lz` and `image_sample_lz` with DIM 2D_ARRAY on linear 2D T#s of formats 1, 7, 22 and 56 return what the same samples with DIM 2D return, for slices 0, 1, 2, 3, 0.6, 1.4, -1 and 5, all eight comparison functions, point and bilinear filtering and `DEPTH` 0 and 3, while a four-layer 2D-array T# over the same memory returns a different result per slice - [Flat aperture bases](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (core/shader/recompiler) - the layout follows the gfx9+ one in the AMD KFD driver's `kfd_flat_memory.c` (`MAKE_LDS_APP_BASE_V9`, `MAKE_LDS_APP_LIMIT`): each aperture is a 4 GB window whose base is the high dword, so a flat address is routed by its whole high dword, as measured on RDNA2 (gfx1035). How the console routes a flat address was not measured. Its bases, high dwords 0x80000000 (shared, `SharedApertureHigh`) and 0x70000000 (private, `PrivateApertureHigh`), first inferred from PPSA04263's traversal addresses, are measured: on a PS5 (13.40, measured 2026-10-19 by a gfx1013 kernel run from inside a retail title) `src_shared_base` / `src_shared_limit` read 0x80000000_00000000 / 0x80000000_FFFFFFFF and `src_private_base` / `src_private_limit` 0x70000000_00000000 / 0x70000000_FFFFFFFF. The scalar aperture sources read the same two values - [Flat accesses outside the aperture routing](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (core/shader/recompiler) - only compute stages route flat accesses, so graphics-stage flat accesses to the shared or private aperture still go to BDA. In compute, a 64-bit flat atomic whose address is in the shared aperture takes the workgroup lock (`ldsLock`) for its two-dword update, and every such atomic requests that lock, so a shader with one pays for it even on global addresses (the path is exercised by `agc_driver_global_atomics_float`). Inferred, not measured on LDS or scratch: an aperture access that is not inside one dword is done byte by byte, an unaligned aperture atomic records the BDA unaligned fault, and LDS or scratch accesses past their end are dropped or read zero. - On a host subgroup narrower than 32 lanes, [`threadBit`](../../../core/shader/recompiler/Translation/src/TranslationContext/ExecMask.cpp) reads an EXEC written as a value (`s_mov_b32`/`s_mov_b64`) at bit (subgroup invocation id) of EXEC_LO, so lanes at or above the subgroup size read a lower lane's bit. From edf98688c8b620fbd1fb176a883f042f85773926 Mon Sep 17 00:00:00 2001 From: slopgobblercobbler Date: Sun, 25 Oct 2026 18:14:38 +0100 Subject: [PATCH 134/198] fix(libSceSaveData.native): validate null arguments in sceSaveDataDirNameSearchPs4 and sceSaveDataMount --- core/libs/prx/libSceSaveData.native/Export.cpp | 6 +++--- core/libs/tests/GuestSaveDataPs4.cpp | 14 ++------------ docs/dev/debt/libSceSaveData.native.md | 2 +- 3 files changed, 6 insertions(+), 16 deletions(-) diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index 0627767b30..e32d58bb14 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -925,7 +925,7 @@ int APS5_VABI sceSaveDataTransferringMountPs4(const SaveDataTransferringMount* m static int dirNameSearchPs4(const SaveDataDirNameSearchCond* cond, SaveDataDirNameSearchResult* result) { if (cond == nullptr || result == nullptr) { - throw std::runtime_error("sceSaveDataDirNameSearchPs4: null argument"); + return SAVE_DATA_ERROR_PARAMETER; } result->hit_num = 0; result->set_num = 0; @@ -934,7 +934,7 @@ static int dirNameSearchPs4(const SaveDataDirNameSearchCond* cond, SaveDataDirNa int APS5_VABI sceSaveDataDirNameSearchPs4(const SaveDataDirNameSearchCond* cond, SaveDataDirNameSearchResult* result) { const int rc = dirNameSearchPs4(cond, result); - SAVEDATA_TRACE("dirNameSearchPs4 user=%d -> 0x%08x", cond->user_id, static_cast(rc)); + SAVEDATA_TRACE("dirNameSearchPs4 user=%d -> 0x%08x", cond != nullptr ? cond->user_id : -1, static_cast(rc)); return rc; } @@ -1053,7 +1053,7 @@ int APS5_VABI sceSaveDataInitialize(const void* init) { } int APS5_VABI sceSaveDataMount(const SaveDataMountPs4* mount, SaveDataMountResult* mount_result) { - if (mount == nullptr || mount->dir_name == nullptr) return SAVE_DATA_ERROR_PARAMETER; + if (mount == nullptr || mount_result == nullptr || mount->dir_name == nullptr) return SAVE_DATA_ERROR_PARAMETER; if (mount->title_id != nullptr) NotImplemented_nid_no_patch("sceSaveDataMount of another title's save data"); SaveDataMount3 converted{}; converted.user_id = mount->user_id; diff --git a/core/libs/tests/GuestSaveDataPs4.cpp b/core/libs/tests/GuestSaveDataPs4.cpp index 0c4a45540f..d35c27146d 100644 --- a/core/libs/tests/GuestSaveDataPs4.cpp +++ b/core/libs/tests/GuestSaveDataPs4.cpp @@ -26,16 +26,6 @@ void Check(bool condition, const std::string& what) { ++failures; } -template -bool ThrowsRuntimeError(TFunction function) { - try { - function(); - } catch (const std::runtime_error&) { - return true; - } - return false; -} - void TestTransferringMountPs4() { SceSaveDataTitleId title{}; std::strcpy(title.data, "CUSA00001"); @@ -77,8 +67,8 @@ void TestDirNameSearchPs4() { Check(result.set_num == 0, "DirNameSearchPs4 writes zero set entries"); Check(static_cast(names[0].data[0]) == 0x5A, "DirNameSearchPs4 leaves the name buffer untouched"); - Check(ThrowsRuntimeError([&] { sceSaveDataDirNameSearchPs4(nullptr, &result); }), "null cond throws"); - Check(ThrowsRuntimeError([&] { sceSaveDataDirNameSearchPs4(&cond, nullptr); }), "null result throws"); + Check(sceSaveDataDirNameSearchPs4(nullptr, &result) == SaveDataErrorParameter, "null cond returns PARAMETER in sceSaveDataDirNameSearchPs4"); + Check(sceSaveDataDirNameSearchPs4(&cond, nullptr) == SaveDataErrorParameter, "null result returns PARAMETER in sceSaveDataDirNameSearchPs4"); } void TestSaveDataMemoryV1() { diff --git a/docs/dev/debt/libSceSaveData.native.md b/docs/dev/debt/libSceSaveData.native.md index b022421794..df1663160a 100644 --- a/docs/dev/debt/libSceSaveData.native.md +++ b/docs/dev/debt/libSceSaveData.native.md @@ -19,6 +19,6 @@ - [sceSaveDataDirNameSearch2](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) runs the same search as `sceSaveDataDirNameSearch` with the same condition and result structures, as [SharpEmu](https://github.com/sharpemu/sharpemu/blob/main/src/SharpEmu.Libs/SaveData/SaveDataExports.cs) does, its only source; what distinguishes it on the console is unknown. One title (PPSA35669, in a plugin) disassembled for this entry has one call site (0x2eda71); it clears 56 bytes for the result structure first (the size of the structure used here), puts a value returned by a call with the argument 0x8000 at offset 8 and 0x400 at offset 0x10, and afterwards branches on a negative result and reads the 32-bit field at offset 0, skipping its loop when it is 0. That agrees with the result structure of `sceSaveDataDirNameSearch`. The condition structure was not read. - [sceSaveDataMount3](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - a failed mount (BUSY, EXISTS, NOT_FOUND, MOUNT_FULL) leaves the result as it was, as shadPS4 does; not checked on hardware. Animal Well (PPSA22520) depends on it: it checks for its save with a mount it never unmounts, then saves with a second mount of the same directory, ignores its BUSY and keeps using the first mount point for `sceSaveDataPrepare`, its file and `sceSaveDataUmount2` - [sceSaveDataMount3](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - an unterminated or empty directory name, `.`, `..`, or a name containing `/`, `\` or `:` returns `SAVE_DATA_ERROR_PARAMETER`, which the [SDK 12.000 reference](https://github.com/peterpuppy/bm/blob/3f9d1ba6b896a53080460c903f5c15af8affb80d/features/ps5/document/output/psn_12/System/Save_Data/SaveData-Reference/sce-save-data-mount3.md#L196-L204) lists for an invalid parameter, and leaves the result as it was, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/569ec04d1a77127c601eed522ad5bba187173f9d/src/libs/libSaveData.cpp#L652-L658) does. The [SDK](https://github.com/peterpuppy/bm/blob/3f9d1ba6b896a53080460c903f5c15af8affb80d/features/ps5/document/output/psn_12/System/Save_Data/SaveData-Reference/sce-save-data-dir-name.md#L32-L36) lets an application use only letters, digits, `-`, `.` and `@` in a name, and KytyPS5 also accepts `_` ([`valid_path_component`](https://github.com/KytyPS5/KytyPS5/blob/569ec04d1a77127c601eed522ad5bba187173f9d/src/libs/libSaveData.cpp#L276-L287)); which other names the console's mount rejects is not checked on hardware -- [sceSaveDataBackup, sceSaveDataDelete, sceSaveDataDirNameSearch, sceSaveDataGetEventResult, sceSaveDataGetMountInfo, sceSaveDataGetParam, sceSaveDataLoadIcon, sceSaveDataMount3, sceSaveDataSaveIcon, sceSaveDataSetParam, sceSaveDataUmount2](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - null pointer arguments return `SAVE_DATA_ERROR_PARAMETER` instead of throwing C++ exceptions, an implementation assumption matching guest error expectations not checked on hardware; unmounted targets return `SAVE_DATA_ERROR_NOT_MOUNTED`, and parameter type or buffer size violations return `SAVE_DATA_ERROR_PARAMETER` after slot lookup +- [sceSaveDataBackup, sceSaveDataDelete, sceSaveDataDirNameSearch, sceSaveDataDirNameSearchPs4, sceSaveDataGetEventResult, sceSaveDataGetMountInfo, sceSaveDataGetParam, sceSaveDataLoadIcon, sceSaveDataMount, sceSaveDataMount3, sceSaveDataSaveIcon, sceSaveDataSetParam, sceSaveDataUmount2](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - null pointer arguments return `SAVE_DATA_ERROR_PARAMETER` instead of throwing C++ exceptions, an implementation assumption matching guest error expectations not checked on hardware; unmounted targets return `SAVE_DATA_ERROR_NOT_MOUNTED`, and parameter type or buffer size violations return `SAVE_DATA_ERROR_PARAMETER` after slot lookup - [sceSaveDataSyncSaveDataMemory](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - validates library initialization and parameters; since in-memory saves flush synchronously on write, sync confirms existence and readiness of the memory blob From 46ac6233e9d813412a7b3c042f6c4fef51f3ec16 Mon Sep 17 00:00:00 2001 From: plexerPK <160128888+plexerPK@users.noreply.github.com> Date: Mon, 26 Oct 2026 09:25:54 +0100 Subject: [PATCH 135/198] fix(ci): preserve changed paths in test selection --- .github/workflows/build.yml | 4 +-- tools/select_tests.py | 15 ++++++++--- tools/tests/test_select_tests.py | 43 ++++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 69f6ed8c96..3659d69bdf 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -522,7 +522,7 @@ jobs: env: BASE: ${{ needs.eligibility.outputs.sha }} run: | - git diff --name-only "$BASE" HEAD > changed.txt + git diff --name-only -z "$BASE" HEAD -- > changed.txt python3 tools/select_tests.py select build test-graph.json changed.txt - id: build run: | @@ -674,7 +674,7 @@ jobs: BASE: ${{ needs.eligibility.outputs.sha }} shell: cmd run: | - git diff --name-only %BASE% HEAD > changed.txt + git diff --name-only -z %BASE% HEAD -- > changed.txt python tools/select_tests.py select build test-graph.json changed.txt - id: build shell: cmd diff --git a/tools/select_tests.py b/tools/select_tests.py index 1fa013a3a9..73febf0e7f 100644 --- a/tools/select_tests.py +++ b/tools/select_tests.py @@ -154,6 +154,13 @@ def paths(arguments): yield value +def changed_paths(path): + text = Path(path).read_bytes().decode("utf-8-sig") + if "\0" in text: + return [name for name in text.split("\0") if name] + return [line.strip() for line in text.splitlines() if line.strip()] + + def chunks(names): batch, size = [], 0 for name in names: @@ -305,10 +312,10 @@ def main(argv=None): s = sub.add_parser("select", help="write a ninja file and a ctest index list for a configured tree") s.add_argument("build") s.add_argument("graph") - s.add_argument("changed", help="file with one changed path per line") + s.add_argument("changed", help="file with newline-separated or Git -z NUL-separated changed paths") e = sub.add_parser("explain", help="explain saved-graph selections without a configured build; CTest may add fixtures") e.add_argument("graph", type=Path) - e.add_argument("changed", type=Path, help="file with one changed path per line") + e.add_argument("changed", type=Path, help="file with newline-separated or Git -z NUL-separated changed paths") c = sub.add_parser("ctest", help="run ctest on a tree, limited to the selected tests if select wrote any") c.add_argument("build") c.add_argument("arguments", nargs=argparse.REMAINDER, help="ctest arguments") @@ -320,7 +327,7 @@ def main(argv=None): if args.command == "explain": try: graph = load_graph(args.graph) - changed = [line.strip() for line in args.changed.read_text(encoding="utf-8-sig").splitlines() if line.strip()] + changed = changed_paths(args.changed) reasons = {} picked, full = select(graph, changed, reasons) except (OSError, ValueError) as error: @@ -343,7 +350,7 @@ def main(argv=None): return 0 try: graph = load_graph(args.graph) - changed = [line.strip() for line in Path(args.changed).read_text(encoding="utf-8-sig").splitlines() if line.strip()] + changed = changed_paths(args.changed) except (OSError, ValueError) as error: parser.error(str(error)) picked, full = select(graph, changed) diff --git a/tools/tests/test_select_tests.py b/tools/tests/test_select_tests.py index cbe8248990..a8769c9c11 100644 --- a/tools/tests/test_select_tests.py +++ b/tools/tests/test_select_tests.py @@ -108,6 +108,15 @@ class SelectTests(unittest.TestCase): self.assertIn("CMakeLists.txt", output) self.assertFalse((self.build / "select.tests").exists()) + def test_nul_changed_paths_select_from_the_configured_build(self): + changed = self.build / "changed-nul.txt" + changed.write_bytes(b"alpha/alpha.h\0") + self.tool("select", self.build, self.graph, changed) + indices = (self.build / "select.tests").read_text(encoding="utf-8").strip() + shown = subprocess.run(["ctest", "--test-dir", str(self.build), "-N", "-I", indices], + check=True, capture_output=True, text=True).stdout + self.assertEqual(set(re.findall(r"Test +#\d+: (\S+)", shown)), {"alpha", "alpha_data", "script"}) + def test_missing_graph_runs_everything(self): self.assertIsNone(self.select("alpha/alpha.h", graph=self.build / "missing.json")[1]) @@ -423,6 +432,40 @@ class BuildErrorTests(unittest.TestCase): self.assertEqual(data["tests"], ["unit"]) +class GitChangedPathTests(unittest.TestCase): + @unittest.skipUnless(shutil.which("git"), "needs git") + def test_git_nul_paths_select_the_changed_headers(self): + names = ["größe.h", " leading.h", "with space.h"] + if os.name != "nt": + names += ['quote".h', "tab\t.h", "line\n.h", "carriage\r.h", "trailing.h "] + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + subprocess.run(["git", "init", "-q", str(root)], check=True, capture_output=True) + source = root / "src" + source.mkdir() + for name in names: + (source / name).write_text("before\n", encoding="utf-8") + subprocess.run(["git", "add", "."], cwd=root, check=True, capture_output=True) + for name in names: + (source / name).write_text("after\n", encoding="utf-8") + changed = root / "changed" + changed.write_bytes(subprocess.check_output(["git", "diff", "--name-only", "-z", "--"], cwd=root)) + graph = {"closures": {}, "tests": {}} + for index, name in enumerate(names): + target = f"unit_{index}" + graph["closures"][target] = ["src/" + name] + graph["tests"][target] = {"target": target, "registered": ["src/CMakeLists.txt"], + "arguments": [], "requires": [], "sets": [], "depends": []} + graph_path = root / "graph.json" + graph_path.write_text(json.dumps(graph), encoding="utf-8") + result = subprocess.run([sys.executable, str(TOOL), "explain", str(graph_path), str(changed)], + check=True, capture_output=True, text=True) + report = json.loads(result.stdout) + self.assertEqual(report["tests"], sorted(graph["tests"])) + for index, name in enumerate(names): + self.assertEqual(report["reasons"][f"unit_{index}"], ["input: src/" + name]) + + class CaseInsensitivePathTests(unittest.TestCase): def build(self): build = object.__new__(select_tests.Build) From 8608bd21713b5a007a324c5148dd30a571657295 Mon Sep 17 00:00:00 2001 From: plexerPK <160128888+plexerPK@users.noreply.github.com> Date: Mon, 26 Oct 2026 18:10:37 +0100 Subject: [PATCH 136/198] fix(ci): retain leading BOM characters in Git paths --- tools/select_tests.py | 7 ++++--- tools/tests/test_select_tests.py | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/tools/select_tests.py b/tools/select_tests.py index 73febf0e7f..587684ed8a 100644 --- a/tools/select_tests.py +++ b/tools/select_tests.py @@ -155,9 +155,10 @@ def paths(arguments): def changed_paths(path): - text = Path(path).read_bytes().decode("utf-8-sig") - if "\0" in text: - return [name for name in text.split("\0") if name] + data = Path(path).read_bytes() + if b"\0" in data: + return [name for name in data.decode("utf-8").split("\0") if name] + text = data.decode("utf-8-sig") return [line.strip() for line in text.splitlines() if line.strip()] diff --git a/tools/tests/test_select_tests.py b/tools/tests/test_select_tests.py index a8769c9c11..9362bf40de 100644 --- a/tools/tests/test_select_tests.py +++ b/tools/tests/test_select_tests.py @@ -433,6 +433,20 @@ class BuildErrorTests(unittest.TestCase): class GitChangedPathTests(unittest.TestCase): + @unittest.skipUnless(shutil.which("git"), "needs git") + def test_git_nul_preserves_a_leading_bom_in_the_first_filename(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + subprocess.run(["git", "init", "-q", str(root)], check=True, capture_output=True) + name = "\ufeffleading.h" + source = root / name + source.write_text("before\n", encoding="utf-8") + subprocess.run(["git", "add", "."], cwd=root, check=True, capture_output=True) + source.write_text("after\n", encoding="utf-8") + changed = root / "changed" + changed.write_bytes(subprocess.check_output(["git", "diff", "--name-only", "-z", "--"], cwd=root)) + self.assertEqual(select_tests.changed_paths(changed), [name]) + @unittest.skipUnless(shutil.which("git"), "needs git") def test_git_nul_paths_select_the_changed_headers(self): names = ["größe.h", " leading.h", "with space.h"] From 728df42165aad444ece5b33e39422016f36703f4 Mon Sep 17 00:00:00 2001 From: Morten Holm Date: Mon, 26 Oct 2026 18:44:05 +0100 Subject: [PATCH 137/198] test(agc): name the fields of the mesh limit test configuration Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index db00c19d58..8fdd0b78bd 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -11141,10 +11141,17 @@ void meshLimitTests() { context.meshLimits.maxMeshOutputVertices = 256; context.meshLimits.maxMeshOutputPrimitives = 512; context.meshLimits.maxMeshSharedMemorySize = 12284; - ShaderRecompiler::MeshConfiguration mesh{4u, 32u, 96u, 96u, 32u, 128u, 2048u, 0u, 4u}; - mesh.maxVertices = 256; - mesh.maxPrimitives = 512; - mesh.ldsSizeDwords = 3071; + ShaderRecompiler::MeshConfiguration mesh{ + .inputPrimitive = 4u, + .primitivesPerGroup = 32u, + .verticesPerGroup = 96u, + .maxVertices = 256u, + .maxPrimitives = 512u, + .threadsPerGroup = 128u, + .ldsSizeDwords = 3071u, + .provokingVertex = 0u, + .esgsItemSize = 4u, + }; AgcDriver::Graphics::ValidateMeshLimits(context, mesh, 96); expectFailure([&] { AgcDriver::Graphics::ValidateMeshLimits(context, mesh, 97); }, "mesh workgroup of 97 invocations exceeds device limits (128 invocations, 96 wide)"); mesh.maxVertices = 257; From 93eb9d304a4416df65082761cb3e9bc1c71543f5 Mon Sep 17 00:00:00 2001 From: Leonardo Date: Mon, 26 Oct 2026 18:46:40 +0100 Subject: [PATCH 138/198] feat(libSceNetCtl): classify NAT type from multi-endpoint STUN probes --- core/libs/prx/libSceNetCtl/Export.cpp | 97 +++++++++++++++++++++++---- core/libs/tests/GuestNetCtl.cpp | 97 ++++++++++++++++++++------- docs/dev/debt/libSceNetCtl.md | 2 +- 3 files changed, 157 insertions(+), 39 deletions(-) diff --git a/core/libs/prx/libSceNetCtl/Export.cpp b/core/libs/prx/libSceNetCtl/Export.cpp index 385d0fc44e..2fcb58f748 100644 --- a/core/libs/prx/libSceNetCtl/Export.cpp +++ b/core/libs/prx/libSceNetCtl/Export.cpp @@ -56,12 +56,17 @@ static constexpr int NET_CTL_EVENT_IPOBTAINED = 3; static constexpr int NET_CTL_STUN_STATUS_FAILED = 1; static constexpr int NET_CTL_STUN_STATUS_OK = 2; static constexpr int NET_CTL_NAT_TYPE_NONE = 0; +static constexpr int NET_CTL_NAT_TYPE_1 = 1; +static constexpr int NET_CTL_NAT_TYPE_2 = 2; +static constexpr int NET_CTL_NAT_TYPE_3 = 3; static constexpr std::uint16_t STUN_BINDING_REQUEST = 0x0001; static constexpr std::uint16_t STUN_BINDING_RESPONSE = 0x0101; static constexpr std::uint32_t STUN_MAGIC_COOKIE = 0x2112A442u; static constexpr std::uint16_t STUN_ATTR_MAPPED_ADDRESS = 0x0001; +static constexpr std::uint16_t STUN_ATTR_CHANGED_ADDRESS = 0x0005; static constexpr std::uint16_t STUN_ATTR_XOR_MAPPED_ADDRESS = 0x0020; static constexpr std::uint16_t STUN_ATTR_XOR_MAPPED_ADDRESS_LEGACY = 0x8020; +static constexpr std::uint16_t STUN_ATTR_OTHER_ADDRESS = 0x802b; static constexpr std::size_t STUN_HEADER_SIZE = 20; static constexpr std::size_t STUN_TRANSACTION_ID_SIZE = 12; static constexpr int STUN_DEFAULT_TIMEOUT_MS = 300; @@ -497,6 +502,11 @@ struct StunEndpoint { bool valid = false; }; +struct StunProbeResponse { + StunEndpoint mapped; + StunEndpoint other; +}; + struct StunConfig { std::mutex lock; bool overrideActive = false; @@ -590,7 +600,7 @@ static StunEndpoint DecodeStunIpv4Endpoint(const std::uint8_t* value, std::uint1 return endpoint; } -static bool ParseStunBindingResponse(const std::uint8_t* packet, std::size_t size, const std::uint8_t* transactionId, StunEndpoint& mapped) { +static bool ParseStunBindingResponse(const std::uint8_t* packet, std::size_t size, const std::uint8_t* transactionId, StunProbeResponse& response) { if (size < STUN_HEADER_SIZE) return false; const auto type = static_cast((static_cast(packet[0]) << 8) | packet[1]); const auto length = static_cast((static_cast(packet[2]) << 8) | packet[3]); @@ -598,7 +608,7 @@ static bool ParseStunBindingResponse(const std::uint8_t* packet, std::size_t siz const std::uint32_t expectedCookie = htonl(STUN_MAGIC_COOKIE); if (std::memcmp(packet + 4, &expectedCookie, sizeof(expectedCookie)) != 0) return false; if (std::memcmp(packet + 8, transactionId, STUN_TRANSACTION_ID_SIZE) != 0) return false; - mapped = {}; + response = {}; bool hasXorMapped = false; const std::size_t end = STUN_HEADER_SIZE + length; for (std::size_t offset = STUN_HEADER_SIZE; offset < end;) { @@ -611,19 +621,22 @@ static bool ParseStunBindingResponse(const std::uint8_t* packet, std::size_t siz if (attrType == STUN_ATTR_XOR_MAPPED_ADDRESS || attrType == STUN_ATTR_XOR_MAPPED_ADDRESS_LEGACY) { const auto endpoint = DecodeStunIpv4Endpoint(value, attrLen, true); if (endpoint.valid) { - mapped = endpoint; + response.mapped = endpoint; hasXorMapped = true; } } else if (attrType == STUN_ATTR_MAPPED_ADDRESS && !hasXorMapped) { const auto endpoint = DecodeStunIpv4Endpoint(value, attrLen, false); - if (endpoint.valid) mapped = endpoint; + if (endpoint.valid) response.mapped = endpoint; + } else if (attrType == STUN_ATTR_OTHER_ADDRESS || attrType == STUN_ATTR_CHANGED_ADDRESS) { + const auto endpoint = DecodeStunIpv4Endpoint(value, attrLen, false); + if (endpoint.valid) response.other = endpoint; } offset = valueOffset + ((static_cast(attrLen) + 3u) & ~std::size_t(3u)); } - return mapped.valid; + return response.mapped.valid; } -static bool ProbeStunServer(StunSocket descriptor, const sockaddr_in& server, int timeoutMs, StunEndpoint& mapped) { +static bool ProbeStunServer(StunSocket descriptor, const sockaddr_in& server, int timeoutMs, StunProbeResponse& response) { std::uint8_t request[STUN_HEADER_SIZE] = {}; request[0] = static_cast(STUN_BINDING_REQUEST >> 8); request[1] = static_cast(STUN_BINDING_REQUEST & 0xFF); @@ -669,7 +682,7 @@ static bool ProbeStunServer(StunSocket descriptor, const sockaddr_in& server, in if (received < 0) return false; #endif if (received == 0) return false; - if (ParseStunBindingResponse(buffer, static_cast(received), request + 8, mapped)) return true; + if (ParseStunBindingResponse(buffer, static_cast(received), request + 8, response)) return true; } } @@ -703,13 +716,73 @@ static NetCtlNatInfo DiscoverNatInfo(in_addr localAddress) { return result; } - for (const auto& server : servers) { - StunEndpoint mapped{}; - if (ProbeStunServer(descriptor, server, timeoutMs, mapped)) { + sockaddr_in boundAddr{}; +#ifdef _WIN32 + int boundLen = sizeof(boundAddr); +#else + socklen_t boundLen = sizeof(boundAddr); +#endif + if (getsockname(descriptor, reinterpret_cast(&boundAddr), &boundLen) != 0) { + CloseStunSocket(descriptor); + return result; + } + const std::uint16_t localPort = ntohs(boundAddr.sin_port); + + StunProbeResponse firstResponse{}; + sockaddr_in primaryServer{}; + std::size_t primaryIndex = servers.size(); + for (std::size_t i = 0; i < servers.size(); ++i) { + if (ProbeStunServer(descriptor, servers[i], timeoutMs, firstResponse)) { + primaryServer = servers[i]; + primaryIndex = i; + break; + } + } + if (primaryIndex == servers.size()) { + CloseStunSocket(descriptor); + return result; + } + + if (firstResponse.mapped.address == localAddress.s_addr && firstResponse.mapped.port == localPort) { + CloseStunSocket(descriptor); + result.stun_status = NET_CTL_STUN_STATUS_OK; + result.nat_type = NET_CTL_NAT_TYPE_1; + result.mapped_addr = firstResponse.mapped.address; + return result; + } + + std::vector secondaryServers; + if (firstResponse.other.valid) { + sockaddr_in otherServer{}; + otherServer.sin_family = AF_INET; + otherServer.sin_addr.s_addr = firstResponse.other.address; + otherServer.sin_port = htons(firstResponse.other.port); + if (otherServer.sin_addr.s_addr != primaryServer.sin_addr.s_addr || otherServer.sin_port != primaryServer.sin_port) { + AppendUniqueStunServer(secondaryServers, otherServer); + } + if (otherServer.sin_port != primaryServer.sin_port) { + sockaddr_in sameIpOtherPort = primaryServer; + sameIpOtherPort.sin_port = otherServer.sin_port; + AppendUniqueStunServer(secondaryServers, sameIpOtherPort); + } + } + for (std::size_t i = 0; i < servers.size(); ++i) { + if (i == primaryIndex) continue; + if (servers[i].sin_addr.s_addr != primaryServer.sin_addr.s_addr || servers[i].sin_port != primaryServer.sin_port) { + AppendUniqueStunServer(secondaryServers, servers[i]); + } + } + + for (const auto& secondary : secondaryServers) { + StunProbeResponse secondResponse{}; + if (ProbeStunServer(descriptor, secondary, timeoutMs, secondResponse)) { CloseStunSocket(descriptor); result.stun_status = NET_CTL_STUN_STATUS_OK; - result.nat_type = NET_CTL_NAT_TYPE_NONE; - result.mapped_addr = mapped.address; + result.nat_type = (secondResponse.mapped.address == firstResponse.mapped.address && + secondResponse.mapped.port == firstResponse.mapped.port) + ? NET_CTL_NAT_TYPE_2 + : NET_CTL_NAT_TYPE_3; + result.mapped_addr = firstResponse.mapped.address; return result; } } diff --git a/core/libs/tests/GuestNetCtl.cpp b/core/libs/tests/GuestNetCtl.cpp index 0029389fe4..4a495b3564 100644 --- a/core/libs/tests/GuestNetCtl.cpp +++ b/core/libs/tests/GuestNetCtl.cpp @@ -280,7 +280,7 @@ StunTxId ReceiveStunBindingRequest(TestSocket serverSocket, sockaddr_in& client) return txId; } -void SendStunBindingResponse(TestSocket serverSocket, const sockaddr_in& client, const StunTxId& txId, std::uint32_t mappedAddr, std::uint16_t mappedHostPort, bool xorMapped) { +void SendStunBindingResponse(TestSocket serverSocket, const sockaddr_in& client, const StunTxId& txId, std::uint32_t mappedAddr, std::uint16_t mappedHostPort, bool xorMapped, const sockaddr_in* otherServer = nullptr) { const std::uint16_t effectivePort = mappedHostPort != 0 ? mappedHostPort : ntohs(client.sin_port); const std::uint32_t effectiveAddr = mappedAddr != 0 ? mappedAddr : client.sin_addr.s_addr; std::vector response(20, 0); @@ -290,6 +290,9 @@ void SendStunBindingResponse(TestSocket serverSocket, const sockaddr_in& client, std::memcpy(response.data() + 4, &cookie, sizeof(cookie)); std::memcpy(response.data() + 8, txId.data(), txId.size()); AppendStunAddressAttribute(response, xorMapped ? 0x0020 : 0x0001, effectiveAddr, effectivePort, xorMapped); + if (otherServer) { + AppendStunAddressAttribute(response, 0x0005, otherServer->sin_addr.s_addr, ntohs(otherServer->sin_port), false); + } const auto attrLen = static_cast(response.size() - 20); response[2] = static_cast(attrLen >> 8); response[3] = static_cast(attrLen & 0xFF); @@ -352,49 +355,91 @@ void CheckNatDiscovery(in_addr address) { Require(natInfo.nat_type == 0); Require(natInfo.mapped_addr == 0); - const sockaddr_in bothServers[2] = {server1Addr, server2Addr}; - NetCtlSetStunServers_nid_no_patch(bothServers, 2, 40); - StunTxId droppedTx{}; - StunTxId fallbackTx{}; - std::thread fallbackWorker([&] { - sockaddr_in droppedClient{}; - droppedTx = ReceiveStunBindingRequest(server1, droppedClient); + NetCtlSetStunServers_nid_no_patch(&server1Addr, 1, 1000); + StunTxId openTx{}; + std::thread openWorker([&] { sockaddr_in client{}; - fallbackTx = ReceiveStunBindingRequest(server2, client); - StunTxId wrongTx = fallbackTx; - wrongTx[11] ^= 0xA5u; - SendStunBindingResponse(server2, client, wrongTx, spoofedIp, 54321, true); - SendStunBindingResponse(server2, client, fallbackTx, externalIp, 54321, true); + openTx = ReceiveStunBindingRequest(server1, client); + SendStunBindingResponse(server1, client, openTx, address.s_addr, 0, true); }); natInfo = {sizeof(natInfo), 99, 99, 0}; Require(sceNetCtlGetNatInfo(&natInfo) == 0); - fallbackWorker.join(); - RecordUniqueTransactionId(seenIds, droppedTx); - RecordUniqueTransactionId(seenIds, fallbackTx); + openWorker.join(); + RecordUniqueTransactionId(seenIds, openTx); Require(natInfo.size == sizeof(natInfo)); Require(natInfo.stun_status == 2); + Require(natInfo.nat_type == 1); + Require(natInfo.mapped_addr == address.s_addr); + + NetCtlSetStunServers_nid_no_patch(&server1Addr, 1, 1000); + StunTxId incompleteTx{}; + std::thread incompleteWorker([&] { + sockaddr_in client{}; + incompleteTx = ReceiveStunBindingRequest(server1, client); + SendStunBindingResponse(server1, client, incompleteTx, externalIp, 54321, true); + }); + natInfo = {sizeof(natInfo), 99, 99, 0xFFFFFFFFu}; + Require(sceNetCtlGetNatInfo(&natInfo) == 0); + incompleteWorker.join(); + RecordUniqueTransactionId(seenIds, incompleteTx); + Require(natInfo.size == sizeof(natInfo)); + Require(natInfo.stun_status == 1); Require(natInfo.nat_type == 0); + Require(natInfo.mapped_addr == 0); + + const sockaddr_in bothServers[2] = {server1Addr, server2Addr}; + NetCtlSetStunServers_nid_no_patch(bothServers, 2, 1000); + StunTxId type2Tx1{}; + StunTxId type2Tx2{}; + std::thread type2Worker([&] { + sockaddr_in firstClient{}; + type2Tx1 = ReceiveStunBindingRequest(server1, firstClient); + SendStunBindingResponse(server1, firstClient, type2Tx1, externalIp, 54321, true); + sockaddr_in secondClient{}; + type2Tx2 = ReceiveStunBindingRequest(server2, secondClient); + Require(firstClient.sin_addr.s_addr == secondClient.sin_addr.s_addr); + Require(firstClient.sin_port == secondClient.sin_port); + StunTxId wrongTx = type2Tx2; + wrongTx[11] ^= 0xA5u; + SendStunBindingResponse(server2, secondClient, wrongTx, spoofedIp, 59999, true); + SendStunBindingResponse(server2, secondClient, type2Tx2, externalIp, 54321, true); + }); + natInfo = {sizeof(natInfo), 99, 99, 0}; + Require(sceNetCtlGetNatInfo(&natInfo) == 0); + type2Worker.join(); + RecordUniqueTransactionId(seenIds, type2Tx1); + RecordUniqueTransactionId(seenIds, type2Tx2); + Require(natInfo.size == sizeof(natInfo)); + Require(natInfo.stun_status == 2); + Require(natInfo.nat_type == 2); Require(natInfo.mapped_addr == externalIp); NetCtlNatInfo cachedInfo{sizeof(cachedInfo), 99, 99, 0}; Require(sceNetCtlGetNatInfo(&cachedInfo) == 0); - Require(cachedInfo.stun_status == 2 && cachedInfo.nat_type == 0 && cachedInfo.mapped_addr == externalIp); + Require(cachedInfo.stun_status == 2 && cachedInfo.nat_type == 2 && cachedInfo.mapped_addr == externalIp); NetCtlSetStunServers_nid_no_patch(&server1Addr, 1, 1000); - StunTxId legacyTx{}; - std::thread legacyWorker([&] { - sockaddr_in client{}; - legacyTx = ReceiveStunBindingRequest(server1, client); - SendStunBindingResponse(server1, client, legacyTx, address.s_addr, 0, false); + StunTxId type3Tx1{}; + StunTxId type3Tx2{}; + std::thread type3Worker([&] { + sockaddr_in firstClient{}; + type3Tx1 = ReceiveStunBindingRequest(server1, firstClient); + SendStunBindingResponse(server1, firstClient, type3Tx1, externalIp, 54321, false, &server2Addr); + sockaddr_in secondClient{}; + type3Tx2 = ReceiveStunBindingRequest(server2, secondClient); + Require(firstClient.sin_addr.s_addr == secondClient.sin_addr.s_addr); + Require(firstClient.sin_port == secondClient.sin_port); + SendStunBindingResponse(server2, secondClient, type3Tx2, externalIp, 54322, false); }); natInfo = {sizeof(natInfo), 99, 99, 0}; Require(sceNetCtlGetNatInfo(&natInfo) == 0); - legacyWorker.join(); - RecordUniqueTransactionId(seenIds, legacyTx); + type3Worker.join(); + RecordUniqueTransactionId(seenIds, type3Tx1); + RecordUniqueTransactionId(seenIds, type3Tx2); Require(natInfo.size == sizeof(natInfo)); Require(natInfo.stun_status == 2); - Require(natInfo.nat_type == 0); - Require(natInfo.mapped_addr == address.s_addr); + Require(natInfo.nat_type == 3); + Require(natInfo.mapped_addr == externalIp); NetCtlSetStunServers_nid_no_patch(nullptr, 0, 0); CloseTestSocket(server1); diff --git a/docs/dev/debt/libSceNetCtl.md b/docs/dev/debt/libSceNetCtl.md index 1bc4e74831..8c4dbecb3e 100644 --- a/docs/dev/debt/libSceNetCtl.md +++ b/docs/dev/debt/libSceNetCtl.md @@ -18,4 +18,4 @@ - [sceNetCtlGetInfo](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) finds the host's default route in `/proc/net/route` on Linux and, on macOS, in the routing table read with `sysctl(NET_RT_FLAGS, RTF_GATEWAY)`, where an interface-scoped default route ranks after the unscoped one - [sceNetCtlGetIfStat](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) throws on macOS: an unprivileged process reads the interface byte counters (`getifaddrs` and `sysctl(NET_RT_IFLIST2)` alike) reduced to their low 32 bits, so a total would wrap every 4 GiB - [Host interface cache](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - on Windows, `sceNetCtlGetState`, `sceNetCtlGetStateV6`, `sceNetCtlCheckCallback`, `sceNetCtlGetIfStat` and `sceNetCtlGetInfo` reuse the last host interface query until `NotifyUnicastIpAddressChange`, `NotifyIpInterfaceChange` or `NotifyRouteChange2` reports a change, instead of calling `GetAdaptersAddresses` on every call. A DNS server change with no address, interface or route change is seen only at the next such change. If the notifications cannot be registered, every call queries the host as before. -- [sceNetCtlGetNatInfo](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - performs RFC 5389 STUN binding probes from the active host IPv4 interface against `stun.playstation.net` and fallback public STUN servers (`stun.cloudflare.com`, `stun.l.google.com`, `stun1.l.google.com`) instead of reading cached state from the console's background `netctl` service. Reports STUN status 2 and the discovered external IPv4 address in `mapped_addr` on a valid binding response, or STUN status 1 (`FAILED`) and `mapped_addr` 0 when no server responds; `nat_type` is left at 0 because PlayStation's NAT classification procedure and field semantics are unverified on hardware. +- [sceNetCtlGetNatInfo](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - performs RFC 5389 STUN binding probes from the active host IPv4 interface against `stun.playstation.net` and fallback public STUN servers (`stun.cloudflare.com`, `stun.l.google.com`, `stun1.l.google.com`) instead of reading cached state from the console's background `netctl` service, which probes `*.np.stun.playstation.net` with authenticated Classic STUN (RFC 3489). Console captures confirm NAT type 2 when mapped address and port match across STUN endpoints, NAT type 3 when the mapped port changes across endpoints, and STUN failure when no server responds; NAT type 1 when the mapped endpoint matches the local socket is not measured on a console with a direct public IP. From a7b52084ecf4c084b6421dbddfe42f671726d1a1 Mon Sep 17 00:00:00 2001 From: Evrim Cicek Date: Mon, 26 Oct 2026 21:25:56 +0300 Subject: [PATCH 139/198] fix(libc): set up streams for writing after a read as FreeBSD does on Windows On Windows the UCRT drops a write that directly follows a read on an r+, w+ or a+ stream: fputc and fprintf report success, fwrite returns 0 and fputs returns EOF, all without the error indicator, and the file is left unchanged, also after an fflush. On a stream open only for reading, fputc after a read reports success as well. Writes now first do what FreeBSD 11.4's __swsetup does: a stream open only for reading fails with EBADF and sets the error indicator, and an update stream that is reading drops its read buffer and ungetc data, clears end of file and writes at the descriptor's offset. This covers fwrite, fputs, fputc, putc_unlocked, fprintf, fprintf_s, fputwc, fputws, fwprintf, fwprintf_s, puts and wprintf, and printf, vprintf and printf_s, which write to standard output through PrintWindows and now return -1 there instead of throwing. Linux is unchanged: glibc writes at the stream position, which the libc technical debt now records. --- core/libs/prx/libc/CMakeLists.txt | 5 ++ core/libs/prx/libc/include/FileStream.hpp | 37 ++++++++++ .../prx/libc/include/WindowsFormatting.hpp | 5 +- core/libs/prx/libc/src/FileAndHeap.cpp | 5 +- core/libs/prx/libc/src/Formatting.cpp | 3 + core/libs/prx/libc/src/FormattingWide.cpp | 5 ++ core/libs/prx/libc/src/RuntimeSupport.cpp | 5 ++ core/libs/prx/libc/src/StandardStreams.cpp | 10 ++- core/libs/tests/FileStreams.cpp | 69 +++++++++++++++++++ core/libs/tests/GuestStandardStreams.cpp | 35 ++++++++++ docs/dev/debt/libc.md | 2 +- 11 files changed, 175 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index 2fb827e104..3d26b2248b 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -737,6 +737,11 @@ if(BUILD_TESTING) set_tests_properties(guest_standard_streams guest_standard_streams_closed_stdout guest_standard_streams_exclusive_stdout PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test(NAME guest_standard_streams_update_stdout COMMAND guest_standard_stream_tests --update-stdout) + if(WIN32) + set_tests_properties(guest_standard_streams_update_stdout PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_gets_tests ${LIBS_INCLUDE_DIR}/tests/GuestGets.cpp) target_include_directories(guest_gets_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_gets_tests PRIVATE libc ${CMAKE_DL_LIBS}) diff --git a/core/libs/prx/libc/include/FileStream.hpp b/core/libs/prx/libc/include/FileStream.hpp index a97f626f49..b624bc9433 100644 --- a/core/libs/prx/libc/include/FileStream.hpp +++ b/core/libs/prx/libc/include/FileStream.hpp @@ -281,6 +281,43 @@ public: #endif } + bool SwitchToWriting() { +#ifdef _WIN32 + ::_lock_file(GetHandle()); + const bool switched = SwitchToWritingUnlocked(); + ::_unlock_file(GetHandle()); + return switched; +#else + return true; +#endif + } + + bool SwitchToWritingUnlocked() { +#ifdef _WIN32 + constexpr long UcrtReading = 0x0001; + constexpr long UcrtWriting = 0x0002; + constexpr long UcrtUpdate = 0x0004; + constexpr long UcrtEndOfFile = 0x0008; + if (randomDevice) return true; + auto* native = reinterpret_cast(GetHandle()); + std::atomic_ref state(native->flags); + if ((state.load() & (UcrtWriting | UcrtUpdate)) == 0) { + errno = 9; + guestError = true; + SyncUnlockedStatus(); + return false; + } + if ((state.load() & (UcrtReading | UcrtUpdate)) != (UcrtReading | UcrtUpdate)) return true; + native->count = 0; + native->pointer = native->base; + state.fetch_and(~(UcrtReading | UcrtEndOfFile)); + SyncUnlockedStatus(); + return true; +#else + return true; +#endif + } + void KeepDirectoryReadError(bool failed) { if (failed && directory && (_guest.flags & 0x40) != 0) errno = 21; } diff --git a/core/libs/prx/libc/include/WindowsFormatting.hpp b/core/libs/prx/libc/include/WindowsFormatting.hpp index ad07f41a7b..e3b9731782 100644 --- a/core/libs/prx/libc/include/WindowsFormatting.hpp +++ b/core/libs/prx/libc/include/WindowsFormatting.hpp @@ -18,6 +18,7 @@ #include extern "C" std::FILE* GuestStandardOutputFile_nid_no_patch(); +extern "C" std::FILE* GuestStandardOutputForWriting_nid_no_patch(); namespace LibcDetail { @@ -729,9 +730,11 @@ inline int FormatWindows(char* buffer, size_t size, const char* format, const vo } inline int PrintWindows(const char* format, const void* args) { + auto* output = GuestStandardOutputForWriting_nid_no_patch(); + if (!output) return -1; std::string buffer; const int size = FormatWindows(nullptr, 0, format, args, &buffer); - if (std::fwrite(buffer.data(), 1, static_cast(size), GuestStandardOutputFile_nid_no_patch()) != static_cast(size)) + if (std::fwrite(buffer.data(), 1, static_cast(size), output) != static_cast(size)) throw std::runtime_error("Formatted output write failed"); return size; } diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index e023b0eaf4..9a04f9490c 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -536,6 +536,7 @@ size_t APS5_VABI fwrite_nid_postfix(const void* buffer, size_t size, size_t coun if (count > std::numeric_limits::max() / size) throw std::overflow_error("fwrite: buffer size overflow"); const auto bytes = size * count; if (bytes > std::numeric_limits::max() - reinterpret_cast(buffer)) throw std::overflow_error("fwrite: buffer address overflow"); + if (!stream->SwitchToWriting()) return 0; GuestArena::GuestArenaPrepareHostRead_nid_no_patch(buffer, bytes); const auto result = std::fwrite(buffer, size, count, handle); stream->SyncStatus(); @@ -601,7 +602,9 @@ void APS5_VABI rewind_nid_postfix(FileStream* stream) { int APS5_VABI fputs_nid_postfix(const char* str, FileStream* stream) { if (!str) throw std::runtime_error("fputs: null string"); if (stream && !stream->IsOpen()) { errno = 9; return -1; } - const int result = std::fputs(str, GetNativeStream(stream)); + auto* native = GetNativeStream(stream); + if (*str != '\0' && !stream->SwitchToWriting()) return -1; + const int result = std::fputs(str, native); stream->SyncStatus(); return result; } diff --git a/core/libs/prx/libc/src/Formatting.cpp b/core/libs/prx/libc/src/Formatting.cpp index a67a0b877e..cec4ee15e7 100644 --- a/core/libs/prx/libc/src/Formatting.cpp +++ b/core/libs/prx/libc/src/Formatting.cpp @@ -409,6 +409,7 @@ int APS5_VABI vasprintf_nid_postfix(char** destination, const char* format, VaLi int APS5_VABI vfprintf_nid_postfix(FileStream* stream, const char* format, VaList* args) { if (stream && !stream->IsOpen()) { errno = 9; return -1; } auto* native = GetNativeStream(stream); + if (!stream->SwitchToWriting()) return -1; #ifdef _WIN32 std::string buffer; const int count = LibcDetail::FormatWindows(nullptr, 0, format, args, &buffer); @@ -661,6 +662,7 @@ static int WriteSecure(FileStream* stream, const char* format, const void* args) if (stream == nullptr || format == nullptr || args == nullptr) return -1; if (!stream->IsOpen()) { errno = 9; return -1; } auto* native = GetNativeStream(stream); + if (!stream->SwitchToWriting()) return -1; std::string text; int count; try { @@ -766,6 +768,7 @@ int APS5_VABI printf_s_nid_postfix(const char* format, ...) { int APS5_VABI puts_nid_postfix(const char* s) { if (!_Stdout_nid_postfix.IsOpen()) { errno = 9; return -1; } auto* native = GetNativeStream(&_Stdout_nid_postfix); + if (!_Stdout_nid_postfix.SwitchToWriting()) return EOF; if (std::fputs(s, native) < 0 || std::fputc('\n', native) == EOF) return EOF; return 0; } diff --git a/core/libs/prx/libc/src/FormattingWide.cpp b/core/libs/prx/libc/src/FormattingWide.cpp index d233dd8faa..a65ccba071 100644 --- a/core/libs/prx/libc/src/FormattingWide.cpp +++ b/core/libs/prx/libc/src/FormattingWide.cpp @@ -338,6 +338,7 @@ int APS5_VABI vsnwprintf_s_nid_postfix(char16_t* buffer, std::size_t size, const int APS5_VABI wprintf_nid_postfix(const char16_t* format, ...) { if (!__stdoutp_nid_postfix->IsOpen()) { errno = 9; return -1; } + if (!__stdoutp_nid_postfix->SwitchToWriting()) return -1; APS5_VA_BEGIN(format); int result = -1; try { @@ -354,6 +355,7 @@ int APS5_VABI wprintf_nid_postfix(const char16_t* format, ...) { int APS5_VABI vfwprintf_nid_postfix(FileStream* stream, const char16_t* format, VaList* args) { if (stream == nullptr || format == nullptr) { errno = 22; return -1; } if (!stream->IsOpen()) { errno = 9; return -1; } + if (!stream->SwitchToWriting()) return -1; int result = -1; try { const std::u16string text = FormatWide(format, args); @@ -380,6 +382,7 @@ int APS5_VABI vwprintf_nid_postfix(const char16_t* format, VaList* args) { int APS5_VABI vfwprintf_s_nid_postfix(FileStream* stream, const char16_t* format, VaList* args) { if (stream == nullptr || format == nullptr || args == nullptr) return -1; if (!stream->IsOpen()) { errno = 9; return -1; } + if (!stream->SwitchToWriting()) return -1; int result = -1; try { const std::u16string text = FormatWide(format, args, true); @@ -416,6 +419,7 @@ int APS5_VABI fputwc_nid_postfix(char16_t value, FileStream* stream) { try { const std::string bytes = ToUtf8(std::u16string(1, value)); auto* native = GetNativeStream(stream); + if (!stream->SwitchToWriting()) return -1; if (std::fwrite(bytes.data(), 1, bytes.size(), native) != bytes.size()) throw std::runtime_error("stream write failed"); stream->SyncStatus(); return value; @@ -432,6 +436,7 @@ int APS5_VABI fputws_nid_postfix(const char16_t* str, FileStream* stream) { const std::u16string text(str); const std::string bytes = ToUtf8(text); auto* native = GetNativeStream(stream); + if (!stream->SwitchToWriting()) return -1; if (std::fwrite(bytes.data(), 1, bytes.size(), native) != bytes.size()) throw std::runtime_error("stream write failed"); stream->SyncStatus(); return static_cast(text.size()); diff --git a/core/libs/prx/libc/src/RuntimeSupport.cpp b/core/libs/prx/libc/src/RuntimeSupport.cpp index dbaf710627..0b95021e58 100644 --- a/core/libs/prx/libc/src/RuntimeSupport.cpp +++ b/core/libs/prx/libc/src/RuntimeSupport.cpp @@ -62,6 +62,11 @@ std::FILE* GuestStandardOutputFile_nid_no_patch() { return GetNativeStream(&_Stdout_nid_postfix); } +std::FILE* GuestStandardOutputForWriting_nid_no_patch() { + auto* native = GetNativeStream(&_Stdout_nid_postfix); + return _Stdout_nid_postfix.SwitchToWriting() ? native : nullptr; +} + int APS5_VABI __cxa_atexit_nid_postfix(void (APS5_VABI *func)(void*), void* arg, void* dsoHandle) { std::lock_guard lock(g_exitMutex); g_exitDestructors.push_back({func, arg, dsoHandle}); diff --git a/core/libs/prx/libc/src/StandardStreams.cpp b/core/libs/prx/libc/src/StandardStreams.cpp index b40f822753..de2549b946 100644 --- a/core/libs/prx/libc/src/StandardStreams.cpp +++ b/core/libs/prx/libc/src/StandardStreams.cpp @@ -184,17 +184,21 @@ std::int32_t APS5_VABI getwchar_nid_postfix() { return fgetwc_nid_postfix(__stdi int APS5_VABI getchar_unlocked_nid_postfix() { return getc_unlocked_nid_postfix(__stdinp_nid_postfix); } int APS5_VABI fputc_nid_postfix(int value, FileStream* stream) { if (stream && !stream->IsOpen()) { errno = 9; return -1; } - const int result = std::fputc(value, GetNativeStream(stream)); + auto* native = GetNativeStream(stream); + if (!stream->SwitchToWriting()) return -1; + const int result = std::fputc(value, native); stream->SyncStatus(); return result; } int APS5_VABI putc_nid_postfix(int value, FileStream* stream) { return fputc_nid_postfix(value, stream); } int APS5_VABI putc_unlocked_nid_postfix(int value, FileStream* stream) { if (stream && !stream->IsOpen()) { errno = 9; return -1; } + auto* native = GetNativeStream(stream); + if (!stream->SwitchToWritingUnlocked()) return -1; #ifdef _WIN32 - const int result = _fputc_nolock(value, GetNativeStream(stream)); + const int result = _fputc_nolock(value, native); #else - const int result = putc_unlocked(value, GetNativeStream(stream)); + const int result = putc_unlocked(value, native); #endif stream->SyncUnlockedStatus(); return result; diff --git a/core/libs/tests/FileStreams.cpp b/core/libs/tests/FileStreams.cpp index fc8af8e721..7bbec0c6a4 100644 --- a/core/libs/tests/FileStreams.cpp +++ b/core/libs/tests/FileStreams.cpp @@ -198,10 +198,79 @@ static void CheckUpdateStreamReadsAfterWrites(const std::string& base) { Require(std::remove(path.c_str()) == 0); } +template +static void CheckWriteAfterRead(const std::string& path, const char* mode, const char* expected, TAccess access) { + auto* stream = fopen_nid_postfix(path.c_str(), "wb"); + Require(stream != nullptr && fputs_nid_postfix("0123456789", stream) >= 0 && fclose_nid_postfix(stream) == 0, "Creating the written file"); + stream = fopen_nid_postfix(path.c_str(), mode); + Require(stream != nullptr, "fopen of the written file"); + access(stream); + Require(fclose_nid_postfix(stream) == 0, "fclose after a write after read"); + Require(ReadWhole(path) == expected, "Write after read wrote other bytes"); +} + +static void CheckStreamWritesAfterReads(const std::string& base) { + const std::string path = base + ".written"; + constexpr char appended[] = "0123456789XY"; +#if defined(__GLIBC__) + constexpr char afterReadAhead[] = "0XY3456789"; + constexpr char afterPushBack[] = "XY23456789"; +#else + constexpr char afterReadAhead[] = "0123456789XY"; + constexpr char afterPushBack[] = "0123456789XY"; +#endif + using Write = void (*)(FileStream*); + for (const Write write : { + +[](FileStream* stream) { Require(fwrite_nid_postfix("XY", 1, 2, stream) == 2, "fwrite after fgetc on an update stream"); }, + +[](FileStream* stream) { Require(fputs_nid_postfix("XY", stream) >= 0, "fputs after fgetc on an update stream"); }, + +[](FileStream* stream) { Require(fputc_nid_postfix('X', stream) == 'X' && fputc_nid_postfix('Y', stream) == 'Y', "fputc after fgetc on an update stream"); }, + +[](FileStream* stream) { Require(putc_unlocked_nid_postfix('X', stream) == 'X' && putc_unlocked_nid_postfix('Y', stream) == 'Y', "putc_unlocked after fgetc on an update stream"); }, + +[](FileStream* stream) { Require(fprintf_nid_postfix(stream, "%s", "XY") == 2, "fprintf after fgetc on an update stream"); }, + +[](FileStream* stream) { Require(fprintf_s_nid_postfix(stream, "%s", "XY") == 2, "fprintf_s after fgetc on an update stream"); }, + +[](FileStream* stream) { Require(fputwc_nid_postfix(u'X', stream) == u'X' && fputwc_nid_postfix(u'Y', stream) == u'Y', "fputwc after fgetc on an update stream"); }, + +[](FileStream* stream) { Require(fputws_nid_postfix(u"XY", stream) >= 0, "fputws after fgetc on an update stream"); }, + +[](FileStream* stream) { Require(fwprintf_nid_postfix(stream, u"XY") == 2, "fwprintf after fgetc on an update stream"); }, + }) { + for (const char* mode : {"r+b", "a+b"}) { + CheckWriteAfterRead(path, mode, std::string_view(mode) == "a+b" ? appended : afterReadAhead, [write](FileStream* stream) { + Require(fseek_nid_postfix(stream, 0, SEEK_SET) == 0 && fgetc_nid_postfix(stream) == '0', "fgetc on an update stream"); + write(stream); + Require(ferror_nid_postfix(stream) == 0, "Write after fgetc on an update stream set the error indicator"); + }); + } + } + CheckWriteAfterRead(path, "r+b", appended, [](FileStream* stream) { + std::array buffer{}; + Require(fread_nid_postfix(buffer.data(), 1, buffer.size(), stream) == buffer.size() && feof_nid_postfix(stream) == 0, "fread of every byte of an r+ stream"); + Require(fwrite_nid_postfix("XY", 1, 2, stream) == 2 && ferror_nid_postfix(stream) == 0, "fwrite after reading every byte of an r+ stream"); + }); + CheckWriteAfterRead(path, "r+b", afterReadAhead, [](FileStream* stream) { + Require(fgetc_nid_postfix(stream) == '0' && fflush_nid_postfix(stream) == 0, "fflush after fgetc on an r+ stream"); + Require(fputs_nid_postfix("XY", stream) >= 0 && ferror_nid_postfix(stream) == 0, "fputs after fgetc and fflush on an r+ stream"); + }); + CheckWriteAfterRead(path, "r+b", afterPushBack, [](FileStream* stream) { + Require(fgetc_nid_postfix(stream) == '0' && ungetc_nid_postfix('Z', stream) == 'Z', "ungetc after fgetc on an r+ stream"); + Require(fputs_nid_postfix("XY", stream) >= 0 && ferror_nid_postfix(stream) == 0, "fputs after ungetc on an r+ stream"); + }); + CheckWriteAfterRead(path, "rb", "0123456789", [](FileStream* stream) { + Require(fgetc_nid_postfix(stream) == '0', "fgetc on a read-only stream"); + errno = 0; + Require(fputc_nid_postfix('X', stream) == EOF && errno == EBADF && ferror_nid_postfix(stream) != 0, "fputc after fgetc on a read-only stream"); + clearerr_nid_postfix(stream); + errno = 0; + Require(fwrite_nid_postfix("XY", 1, 2, stream) == 0 && errno == EBADF && ferror_nid_postfix(stream) != 0, "fwrite after fgetc on a read-only stream"); + clearerr_nid_postfix(stream); + errno = 0; + Require(fprintf_nid_postfix(stream, "%s", "XY") == -1 && errno == EBADF && ferror_nid_postfix(stream) != 0, "fprintf after fgetc on a read-only stream"); + }); + Require(std::remove(path.c_str()) == 0); +} + int main(int argc, char** argv) { Require(argc == 2); CheckExclusiveCppOpen(argv[1]); CheckUpdateStreamReadsAfterWrites(argv[1]); + CheckStreamWritesAfterReads(argv[1]); Require(_Stdout_nid_postfix.GetHandle() == stdout); Require(_Stderr_nid_postfix.GetHandle() == stderr); Require(fputs_nid_postfix("stdout object works\n", &_Stdout_nid_postfix) >= 0); diff --git a/core/libs/tests/GuestStandardStreams.cpp b/core/libs/tests/GuestStandardStreams.cpp index 3b0dd20541..b750cd5ebd 100644 --- a/core/libs/tests/GuestStandardStreams.cpp +++ b/core/libs/tests/GuestStandardStreams.cpp @@ -596,6 +596,40 @@ static int CheckExclusiveStandardOutput() { return 0; } +static int CheckUpdateStandardOutput() { + const auto filename = "anyps5-update-stdout-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); + auto* const stream = __stdoutp_nid_postfix; + Require(freopen_nid_postfix(filename.c_str(), "a+b", stream) == stream); + Require(fputs_nid_postfix("0123456789", stream) >= 0); + const auto readFirst = [stream] { Require(fseek_nid_postfix(stream, 0, SEEK_SET) == 0 && fgetc_nid_postfix(stream) == '0'); }; + readFirst(); + Require(puts_nid_postfix("A") != EOF); + readFirst(); + Require(printf_nid_postfix("%s", "B") == 1); + readFirst(); + Require(PrintStandard("%s", "C") == 1); + readFirst(); + Require(printf_s_nid_postfix("%s", "D") == 1); + readFirst(); + Require(wprintf_nid_postfix(u"E") == 1); + Require(ferror_nid_postfix(stream) == 0); + Require(freopen_nid_postfix(filename.c_str(), "rb", stream) == stream && fgetc_nid_postfix(stream) == '0'); + *__error_nid_postfix() = 0; + Require(puts_nid_postfix("A") == EOF && *__error_nid_postfix() == 9); + *__error_nid_postfix() = 0; + Require(printf_nid_postfix("%s", "B") == -1 && *__error_nid_postfix() == 9); + *__error_nid_postfix() = 0; + Require(PrintStandard("%s", "C") == -1 && *__error_nid_postfix() == 9); + *__error_nid_postfix() = 0; + Require(printf_s_nid_postfix("%s", "D") == -1 && *__error_nid_postfix() == 9); + *__error_nid_postfix() = 0; + Require(wprintf_nid_postfix(u"E") == -1 && *__error_nid_postfix() == 9); + Require(ferror_nid_postfix(stream) != 0 && fclose_nid_postfix(stream) == 0); + Require(CheckFileBytes(filename, "0123456789A\nBCDE", "a+b", true)); + Require(std::filesystem::remove(filename)); + return 0; +} + #ifdef _WIN32 static void CheckStandardStreamDeleteSharing() { int index = 0; @@ -617,6 +651,7 @@ static void CheckStandardStreamDeleteSharing() { #endif int main(int argc, char** argv) { + if (argc > 1 && std::string_view(argv[1]) == "--update-stdout") return CheckUpdateStandardOutput(); if (argc > 1 && std::string_view(argv[1]) == "--closed-stdout") return CheckClosedStandardOutput(); if (argc > 1 && std::string_view(argv[1]) == "--exclusive-stdout") return CheckExclusiveStandardOutput(); CheckClosedStreamWrites(); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index ae8a6c9018..253757cd11 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -138,7 +138,7 @@ - [sceKernelSync](../../../core/libs/prx/libc/src/General.cpp) (libkernel) on Windows skips a recorded file it cannot open for writing (read-only, or held open by a handle that denies write sharing), since `FlushFileBuffers` needs a writable handle. On Linux, it skips a recorded file it cannot open for reading. Such a file's data may not reach the disk before a host crash; the guest reads the same data either way - [`fopen` and `freopen`](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) on Windows: `freopen` onto a directory in a read mode fails with `EACCES`, where FreeBSD opens it. Reading a directory stream from `fopen` fails with `EISDIR` and sets the error indicator on every host, as FreeBSD 11 tmpfs does; FreeBSD 11 FFS returns the directory's raw blocks instead, and what the console's filesystems return is unverified - [readdir and readdir_r](../../../core/libs/prx/libc/src/Directory.cpp) (libc) on Windows report, on a file system whose directory listing carries no file ID (the entry's ID is 0, or the volume refuses `FileIdBothDirectoryInfo`), a nonzero `d_fileno` hashed from the entry name and the directory's file index, which differs from `lstat`'s `st_ino`. Not reproduced: NTFS lists an ID for every entry -- [Writes on an update stream](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) on Windows go through the UCRT, which drops a write that directly follows a read without `fflush`, `fseek`, `fsetpos` or `rewind` unless the read reached end of file: `fwrite` returns 0, and `fputc` and `fprintf` report success, without the error indicator. FreeBSD 11.4's [`__swsetup`](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libc/stdio/wsetup.c#L59-L74) discards the read buffer and writes at the descriptor's offset, past a read-ahead of `st_blksize` bytes for a regular file, so where the console writes depends on its block size. Reads directly after writes follow FreeBSD's `__srefill` +- [Writes on an update stream](../../../core/libs/prx/libc/include/FileStream.hpp) (libc) directly after a read differ from the console in where they land. Windows follows FreeBSD 11.4's [`__swsetup`](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libc/stdio/wsetup.c#L59-L74): the read buffer and `ungetc` data are dropped and the write goes to the descriptor's offset, but past the UCRT's read-ahead (4096 bytes by default, 2 unbuffered) where the console reads `st_blksize` bytes (1 unbuffered), so the two agree while the rest of the file fits in both. Linux glibc writes at the stream position: after `fgetc` on an `r+` stream over `0123456789`, `fputs("XY")` gives `0XY3456789` where FreeBSD gives `0123456789XY`; matching FreeBSD there needs the stream to track whether it was positioned since its last read. Reads directly after writes follow FreeBSD's `__srefill` - [`fopen` of /dev/random and /dev/urandom](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) in a read mode returns a stream over the host null device whose `fread` fills the buffer with host OS entropy, as the libkernel [random device](../../../core/libs/prx/libkernel/File/src/RandomDevice.cpp) descriptors do. Only the exact paths are recognised, the stream has no descriptor, `setvbuf`, `feof`, `ferror`, `clearerr`, `flockfile`, `ftrylockfile`, `funlockfile` and `fclose` work on it, `fflush` returns 0 as on a FreeBSD read-only stream, and every other stream function throws, as does opening the paths for writing. `freopen` onto these paths still fails with `ENOENT`. ProsperoTV (PPSA99003) reads its phone pairing code this way - [RemoveTree](../../../core/libs/prx/libc/include/RemoveTree.hpp) (libc) removes a host tree itself on Windows instead of calling `std::filesystem::remove_all`: MinGW libstdc++ up to GCC 16 does not treat a reparse point as a link, so `remove_all` recurses through it and never terminates, and any tree that holds a symlink hangs. It builds the entry list from `GetFileAttributesW`, removes a reparse point as the link it is rather than its target, clears the read-only attribute and retries as `remove_all` does, and reports the first native error. The libstdc++ fix is in GCC 17 (commit 88856ad855ae); other hosts call `remove_all`. - The [`_Unwind_Context`](../../../core/libs/prx/libc/include/exceptions/Unwind.hpp) that `_Unwind_Backtrace` passes to its callback starts with a table laid out like LLVM libunwind's `AbstractUnwindCursor` virtual table, because homebrew built with the ps5-payload-dev SDK links libunwind statically and reads the frames with its own `_Unwind_GetIP` (`__unw_get_reg`), which calls through that table. Only register reads and writes (`validReg`, `getReg`, `setReg`, with `UNW_REG_IP` and `UNW_REG_SP`), `validFloatReg` (always false) and `isSignalFrame` are answered; the other slots throw. The layout follows libunwind's `UnwindCursor.hpp`, not the console's own `libSceLibcInternal`, which is not checked From 61e16da2c545c7a04dbd4f36309e14d5b4c3f56e Mon Sep 17 00:00:00 2001 From: AZLisme <8706522+AZLisme@users.noreply.github.com> Date: Fri, 23 Oct 2026 12:21:13 +0800 Subject: [PATCH 140/198] feat(libSceNetCtl): deliver IPv6 state callbacks --- core/libs/prx/libSceNetCtl/CMakeLists.txt | 10 ++ core/libs/prx/libSceNetCtl/Export.cpp | 35 +++-- .../prx/libSceNetCtl/tests/V6Callbacks.cpp | 127 ++++++++++++++++++ core/libs/tests/GuestNetCtl.cpp | 8 +- docs/dev/debt/libSceNetCtl.md | 6 +- 5 files changed, 172 insertions(+), 14 deletions(-) create mode 100644 core/libs/prx/libSceNetCtl/tests/V6Callbacks.cpp diff --git a/core/libs/prx/libSceNetCtl/CMakeLists.txt b/core/libs/prx/libSceNetCtl/CMakeLists.txt index 0083a712af..6907cd9fb7 100644 --- a/core/libs/prx/libSceNetCtl/CMakeLists.txt +++ b/core/libs/prx/libSceNetCtl/CMakeLists.txt @@ -3,6 +3,16 @@ target_include_directories(libSceNetCtl PRIVATE ${LIBS_INCLUDE_DIR}) configure_windows_unwind(libSceNetCtl) if(BUILD_TESTING) + add_test_executable(guest_net_ctl_v6_callbacks_tests tests/V6Callbacks.cpp) + target_include_directories(guest_net_ctl_v6_callbacks_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_net_ctl_v6_callbacks_tests PRIVATE libSceNetCtl libc) + configure_windows_unwind(guest_net_ctl_v6_callbacks_tests) + add_test(NAME guest_net_ctl_v6_callbacks COMMAND guest_net_ctl_v6_callbacks_tests) + set_tests_properties(guest_net_ctl_v6_callbacks PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_net_ctl_v6_callbacks PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_net_ctl_tests ${LIBS_INCLUDE_DIR}/tests/GuestNetCtl.cpp) target_include_directories(guest_net_ctl_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_net_ctl_tests PRIVATE libSceNetCtl libc) diff --git a/core/libs/prx/libSceNetCtl/Export.cpp b/core/libs/prx/libSceNetCtl/Export.cpp index a5e469dfdd..a3ddc3395d 100644 --- a/core/libs/prx/libSceNetCtl/Export.cpp +++ b/core/libs/prx/libSceNetCtl/Export.cpp @@ -504,28 +504,41 @@ static void HostTraffic(std::uint64_t& sent, std::uint64_t& received) { #endif } -static int CheckCallbacks(CallbackSlot (&callbacks)[MAX_CALLBACKS]) { +static int CheckCallbacks(CallbackSlot (&callbacks)[MAX_CALLBACKS], bool includeV6 = false) { if (!g_initialized.load()) return SCE_NET_CTL_ERROR_NOT_INITIALIZED; - if (!HasCallback(callbacks)) return 0; - const int event = HostHasAddress(AF_UNSPEC) ? NET_CTL_EVENT_IPOBTAINED : NET_CTL_EVENT_DISCONNECTED; - CallbackSlot due[MAX_CALLBACKS]; + const bool primary = HasCallback(callbacks); + const bool ipv6 = includeV6 && HasCallback(g_callbacksV6); + if (!primary && !ipv6) return 0; + const int primaryEvent = primary ? (HostHasAddress(AF_UNSPEC) ? NET_CTL_EVENT_IPOBTAINED : NET_CTL_EVENT_DISCONNECTED) : 0; + const int ipv6Event = ipv6 ? (HostHasAddress(AF_INET6) ? NET_CTL_EVENT_IPOBTAINED : NET_CTL_EVENT_DISCONNECTED) : 0; + struct Delivery { + CallbackSlot callback; + int event; + }; + Delivery due[2 * MAX_CALLBACKS]; int count = 0; { std::lock_guard lock(g_callbackLock); - for (auto& slot : callbacks) { - if (!slot.func || slot.lastEvent == event) continue; - slot.lastEvent = event; - due[count++] = slot; - } + const auto collect = [&](CallbackSlot (&slots)[MAX_CALLBACKS], int event) { + if (event == 0) return; + for (auto& slot : slots) { + if (!slot.func || slot.lastEvent == event) continue; + slot.lastEvent = event; + due[count++] = {slot, event}; + } + }; + collect(callbacks, primaryEvent); + if (includeV6) collect(g_callbacksV6, ipv6Event); } - for (int index = 0; index < count; ++index) due[index].func(event, due[index].arg); + for (int index = 0; index < count; ++index) + due[index].callback.func(due[index].event, due[index].callback.arg); return 0; } extern "C" { int APS5_VABI sceNetCtlCheckCallback(void) { - return CheckCallbacks(g_callbacks); + return CheckCallbacks(g_callbacks, true); } int APS5_VABI sceNetCtlGetIfStat(NetCtlIfStat* stat) { diff --git a/core/libs/prx/libSceNetCtl/tests/V6Callbacks.cpp b/core/libs/prx/libSceNetCtl/tests/V6Callbacks.cpp new file mode 100644 index 0000000000..536e70a3a7 --- /dev/null +++ b/core/libs/prx/libSceNetCtl/tests/V6Callbacks.cpp @@ -0,0 +1,127 @@ +#include "prx/libSceNetCtl/libSceNetCtl.hpp" +#include "SceTypes.hpp" +#include +#include +#include +#include +#include + + +static void Require(bool value, std::source_location location = std::source_location::current()) { + if (value) return; + std::fprintf(stderr, "NetCtl IPv6 callback check failed at line %u\n", location.line()); + std::abort(); +} + +struct Probe { + int calls = 0; + int event = 0; + std::thread::id thread; +}; + +static void APS5_VABI Record(int event, void* argument) { + auto& probe = *static_cast(argument); + ++probe.calls; + probe.event = event; + probe.thread = std::this_thread::get_id(); +} + +static int ExpectedEvent(bool ipv6) { + int state = -1; + Require((ipv6 ? sceNetCtlGetStateV6(&state) : sceNetCtlGetState(&state)) == 0); + return state == 3 ? 3 : 1; +} + +struct RegisterDuringCheck { + Probe current; + Probe later; + int laterId = -1; +}; + +static void APS5_VABI RegisterV6(int event, void* argument) { + auto& request = *static_cast(argument); + Record(event, &request.current); + Require(sceNetCtlRegisterCallbackV6(Record, &request.later, &request.laterId) == 0); +} + +struct Reentrant { + Probe probe; + int id = -1; + int unregisterResult = -1; + int checkResult = -1; +}; + +static void APS5_VABI UnregisterAndReenter(int event, void* argument) { + auto& request = *static_cast(argument); + Record(event, &request.probe); + request.unregisterResult = sceNetCtlUnregisterCallbackV6(request.id); + request.checkResult = sceNetCtlCheckCallback(); +} + +int main() { + Require(sceNetCtlCheckCallback() == 0); + const auto checkingThread = std::this_thread::get_id(); + const int ipv6Event = ExpectedEvent(true); + Probe v6; + int v6Id = -1; + Require(sceNetCtlRegisterCallbackV6(Record, &v6, &v6Id) == 0); + Require(v6.calls == 0); + Require(sceNetCtlCheckCallback() == 0); + Require(v6.calls == 1 && v6.event == ipv6Event && v6.thread == checkingThread); + Require(sceNetCtlCheckCallback() == 0 && v6.calls == 1); + Require(sceNetCtlUnregisterCallbackV6(v6Id) == 0); + Require(sceNetCtlCheckCallback() == 0 && v6.calls == 1); + + RegisterDuringCheck registration; + int v4Id = -1; + Require(sceNetCtlRegisterCallback(RegisterV6, ®istration, &v4Id) == 0); + Require(sceNetCtlRegisterCallbackV6(Record, &v6, &v6Id) == 0); + Require(sceNetCtlCheckCallback() == 0); + Require(registration.current.calls == 1 && registration.current.event == ExpectedEvent(false)); + Require(registration.later.calls == 0 && v6.calls == 2); + Require(sceNetCtlCheckCallback() == 0); + Require(registration.later.calls == 1 && registration.later.event == ipv6Event && registration.later.thread == checkingThread); + Require(v6.calls == 2 && registration.current.calls == 1); + Require(sceNetCtlUnregisterCallback(v4Id) == 0); + Require(sceNetCtlUnregisterCallbackV6(v6Id) == 0); + Require(sceNetCtlUnregisterCallbackV6(registration.laterId) == 0); + + Reentrant reentrant; + Probe other; + Require(sceNetCtlRegisterCallbackV6(UnregisterAndReenter, &reentrant, &reentrant.id) == 0); + Require(sceNetCtlRegisterCallbackV6(Record, &other, &v6Id) == 0); + Require(sceNetCtlCheckCallback() == 0); + Require(reentrant.probe.calls == 1 && reentrant.probe.event == ipv6Event); + Require(reentrant.unregisterResult == 0 && reentrant.checkResult == 0); + Require(other.calls == 1 && other.event == ipv6Event); + Require(sceNetCtlCheckCallback() == 0 && other.calls == 1); + Require(sceNetCtlUnregisterCallbackV6(v6Id) == 0); + + std::array probes{}; + std::array ids{}; + std::array ipv4Probes{}; + std::array ipv4Ids{}; + for (std::size_t index = 0; index < probes.size(); ++index) { + Require(sceNetCtlRegisterCallbackV6(Record, &probes[index], &ids[index]) == 0); + Require(sceNetCtlRegisterCallback(Record, &ipv4Probes[index], &ipv4Ids[index]) == 0); + } + Require(sceNetCtlCheckCallback() == 0); + for (std::size_t index = 0; index < probes.size(); ++index) { + Require(probes[index].calls == 1 && probes[index].event == ipv6Event && probes[index].thread == checkingThread); + Require(ipv4Probes[index].calls == 1 && ipv4Probes[index].event == ExpectedEvent(false) && ipv4Probes[index].thread == checkingThread); + Require(sceNetCtlUnregisterCallbackV6(ids[index]) == 0); + Require(sceNetCtlUnregisterCallback(ipv4Ids[index]) == 0); + } + Probe toolkit, isolatedV6; + int toolkitId = -1; + Require(sceNetCtlRegisterCallbackForNpToolkit(Record, &toolkit, &toolkitId) == 0); + Require(sceNetCtlRegisterCallbackV6(Record, &isolatedV6, &v6Id) == 0); + Require(sceNetCtlCheckCallbackForNpToolkit() == 0); + Require(toolkit.calls == 1 && toolkit.event == ExpectedEvent(false) && isolatedV6.calls == 0); + Require(sceNetCtlCheckCallback() == 0); + Require(toolkit.calls == 1 && isolatedV6.calls == 1 && isolatedV6.event == ipv6Event); + Require(sceNetCtlCheckCallbackForNpToolkit() == 0 && toolkit.calls == 1); + Require(sceNetCtlUnregisterCallbackForNpToolkit(toolkitId) == 0); + Require(sceNetCtlUnregisterCallbackV6(v6Id) == 0); + +} diff --git a/core/libs/tests/GuestNetCtl.cpp b/core/libs/tests/GuestNetCtl.cpp index fa070adf54..d43e18cc23 100644 --- a/core/libs/tests/GuestNetCtl.cpp +++ b/core/libs/tests/GuestNetCtl.cpp @@ -102,10 +102,15 @@ void CheckCallbacks() { int v6 = -1; Require(sceNetCtlRegisterCallbackV6(RecordV6, &v6Probe, &v6) == 0 && v6 == 0); + int stateV6 = -1; + Require(sceNetCtlGetStateV6(&stateV6) == 0); + const int eventV6 = stateV6 == kStateIpObtained ? kEventIpObtained : kEventDisconnected; + int first = -1; Require(sceNetCtlRegisterCallback(RecordFirst, &firstProbe, &first) == 0 && first == 0); Require(sceNetCtlCheckCallback() == 0); Require(g_first.calls == 1 && g_first.event == event && g_first.arg == &firstProbe); + Require(g_v6.calls == 1 && g_v6.event == eventV6 && g_v6.arg == &v6Probe); Require(sceNetCtlCheckCallback() == 0); Require(g_first.calls == 1); @@ -144,7 +149,8 @@ void CheckCallbacks() { Require(sceNetCtlRegisterCallback(RecordFirst, &secondProbe, &first) == 0 && first == 0); Require(sceNetCtlCheckCallback() == 0); Require(g_first.calls == 2 && g_first.event == event && g_first.arg == &secondProbe); - Require(g_second.calls == 2 && g_self.calls == 1 && g_v6.calls == 0); + Require(g_second.calls == 2 && g_self.calls == 1 && g_v6.calls == 2); + Require(g_v6.event == eventV6 && g_v6.arg == &v6Probe); Require(sceNetCtlUnregisterCallback(first) == 0); Require(sceNetCtlUnregisterCallbackV6(v6) == 0); } diff --git a/docs/dev/debt/libSceNetCtl.md b/docs/dev/debt/libSceNetCtl.md index ef536e17eb..82f2498c8a 100644 --- a/docs/dev/debt/libSceNetCtl.md +++ b/docs/dev/debt/libSceNetCtl.md @@ -11,15 +11,17 @@ - [sceNetCtlGetInfo](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - answers from the host interface with an IPv4 address, preferring the default-route one: its address, netmask, gateway, MTU and first two IPv4 DNS servers. The device is always reported as wired, the link as connected, the IP configuration as DHCP and the HTTP proxy as off, and the Ethernet address is the one from `sceNetGetMacAddress`. A missing gateway or DNS server is `0.0.0.0`; the console value is unknown. The DHCP host name (12), PPPoE user name (13), HTTP proxy server (20) and Wi-Fi BSSID and SSID (5, 6) are empty strings and the other Wi-Fi codes (7-10) and the proxy port (21) are 0, as that configuration sets none of them; this follows [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/network.cpp) and is not measured on hardware. One title (PPSA02225) calls code 12 at 0x1056f0f, 0xa0387f and 0xa26de5 and code 14 at 0x1056fa1; the meaning of the flag byte it checks is not shown. - [sceNetCtlGetInfoV6](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl, NID `Jy1EO5GdlcM`) - imported by PPSA08587. `(int code, SceNetCtlInfoV6* info)` with codes 1-5 (IP address, link-local address, default route, primary and secondary DNS) comes only from [SharpProspero](https://github.com/SvenGDK/SharpProspero), whose source is unknown; shadPS4 and fpPS4 only stub it. A null `info` answers `INVALID_ADDR`, a code outside 1-5 `INVALID_CODE`, and a host without an IPv6 address `NOT_CONNECTED`, as `sceNetCtlGetStateV6` reports it. The layout of each answer is unverified, so every code throws on a host with an IPv6 address - [sceNetCtlGetResult](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - `(int eventType, int* errorCode)` reports no error: event types 1-3 (disconnected to IP obtained, the numbering `sceNetCtlCheckCallback` delivers) write 0 to `errorCode`, as `sceNetCtlGetResultV6` does. The error code a console reports for each event, a disconnection's cause in particular, is unconfirmed. Callers found: PPSA20052 (Worms Armageddon: Anniversary Edition 01.003.000) calls it 2 s into boot, and PPSA24191 (Monster Hunter Wilds) without a network, with event 1 (disconnected) as its first call. A null `errorCode` answers `INVALID_ADDR` and other event types throw, as their error code on a console is unknown -- [sceNetCtlGetResultV6](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl, NID `H5yARg37U5g`) - `(int eventType, int* errorCode)` comes only from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/NetCtl.cs#L246-L251), whose source is unknown; shadPS4 only stubs it. Callbacks registered with `sceNetCtlRegisterCallbackV6` receive no event, so event types 1-3 write 0 to `errorCode`; what a console writes before the first event is unmeasured. A null `errorCode` answers `INVALID_ADDR`, as `sceNetCtlGetResult` does, and other event types throw, as their error code on a console is unknown +- [sceNetCtlGetResultV6](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl, NID `H5yARg37U5g`) - `(int eventType, int* errorCode)` comes only from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/NetCtl.cs#L246-L251), whose source is unknown; shadPS4 only stubs it. The host backend has no IPv6 service errors, so event types 1-3 write 0 to `errorCode`; what a console writes before the first event is unmeasured. A null `errorCode` answers `INVALID_ADDR`, as `sceNetCtlGetResult` does, and other event types throw, as their error code on a console is unknown - [sceNetCtlRegisterCallbackForNpToolkit, sceNetCtlUnregisterCallbackForNpToolkit, sceNetCtlCheckCallbackForNpToolkit](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - a table of eight callbacks apart from the IPv4 and IPv6 ones, with the `(int eventType, void* arg)` callback, follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/net_ctl_obj.h), whose NpToolkit check delivers the same events to that table. The three share the register, unregister and dispatch of `sceNetCtlRegisterCallback`, `sceNetCtlUnregisterCallback` and `sceNetCtlCheckCallback`, so they return the same initialization, address, slot-limit and ID errors, and the check reports the state `sceNetCtlCheckCallback` does. Whether the console keeps this table apart, and what `sceNetCtlCheckCallback` does with it, is unverified on hardware - [sceNetCtlGetStateIpcInt, sceNetCtlGetStateV6IpcInt, sceNetCtlGetResultIpcInt, sceNetCtlGetResultV6IpcInt, sceNetCtlGetInfoIpcInt, sceNetCtlGetInfoV6IpcInt, sceNetCtlRegisterCallbackIpcInt, sceNetCtlUnregisterCallbackIpcInt, sceNetCtlRegisterCallbackV6IpcInt and sceNetCtlUnregisterCallbackV6IpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - answer as their public twins, whose recovered argument registers they share: the getters return and write what `sceNetCtlGetState`, `GetStateV6`, `GetResult`, `GetResultV6`, `GetInfo` and `GetInfoV6` do, throws included, and the callback forms use the IPv4 and IPv6 tables, as no IpcInt check is exported to deliver a table of their own. This rests on the matching call shapes, not on a measurement; whether the internal client answers a null pointer or a bad code before its service does is unknown - [ApRp exports](../../../core/libs/prx/libSceNetCtl/Export.cpp) are constant-success functions in the supplied native module and consume no arguments. No callers were found in the supplied system-library decompilations, so unused callback, output and configuration argument types remain unresolved. Protected stub bodies are unchanged. ### Functional -- [sceNetCtlCheckCallback](../../../core/libs/prx/libSceNetCtl/Export.cpp) - host address changes stand in for the native service event queues. IPv4 callbacks receive connected/disconnected transitions and an initial event; intermediate connection states and errors are not represented. Native CheckCallback drains both IPv4 and IPv6 queues, but the host backend never delivers IPv6 events. Dispatch copies slots before invoking callbacks, so unregistering concurrently may leave one callback in flight; native dispatch holds its registration lock. Slot limits, invalid/deleted IDs and the no-op Term follow the supplied module. +- [sceNetCtlCheckCallback](../../../core/libs/prx/libSceNetCtl/Export.cpp) - host address changes stand in for native service events, following the existing [KytyPS5 state-transition model](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/network.cpp#L3648-L3661). Each IPv4 or IPv6 callback receives an initial connected/disconnected event and changes matching `sceNetCtlGetState` or `sceNetCtlGetStateV6`; intermediate connection states and errors are not represented, and console timing remains unverified. Native CheckCallback drains both service queues, as recorded in [the recovered-contract correction](https://github.com/boykopovar/AnyPS5/commit/ea086b23745cb167780f5aed0a3b9600a4e8dd0a). Dispatch snapshots both tables before invoking any callback, so a registration made during a callback waits for another check and unregistering concurrently may leave one callback in flight; native dispatch holds its registration lock. - [libSceNetCtl initialization](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - the library starts initialized, as titles find it: it is the internal sysmodule `0x80000009`, and PPSA11386, PPSA20049, PPSA02530, PPSA14632 and PPSA15878 register and check NetCtl callbacks without importing `sceNetCtlInit`. PPSA11386 executes `int 0x42` at start-up unless `sceNetCtlRegisterCallback` returns 0. So the supplied module's `SCE_NET_CTL_ERROR_NOT_INITIALIZED` answers are unreachable, since Term is a no-op. Whether the console's process start or the module's start sets that state is not measured. - [sceNetCtlGetInfo](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) finds the host's default route in `/proc/net/route` on Linux and, on macOS, in the routing table read with `sysctl(NET_RT_FLAGS, RTF_GATEWAY)`, where an interface-scoped default route ranks after the unscoped one - [sceNetCtlGetIfStat](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) throws on macOS: an unprivileged process reads the interface byte counters (`getifaddrs` and `sysctl(NET_RT_IFLIST2)` alike) reduced to their low 32 bits, so a total would wrap every 4 GiB - [Host interface cache](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - on Windows, `sceNetCtlGetState`, `sceNetCtlGetStateV6`, `sceNetCtlCheckCallback`, `sceNetCtlGetIfStat` and `sceNetCtlGetInfo` reuse the last host interface query until `NotifyUnicastIpAddressChange`, `NotifyIpInterfaceChange` or `NotifyRouteChange2` reports a change, instead of calling `GetAdaptersAddresses` on every call. A DNS server change with no address, interface or route change is seen only at the next such change. If the notifications cannot be registered, every call queries the host as before. + + From 0d6de39b3d66565e5cdbf897541ce4c120dfb157 Mon Sep 17 00:00:00 2001 From: AZLisme <8706522+AZLisme@users.noreply.github.com> Date: Sun, 25 Oct 2026 21:07:52 +0800 Subject: [PATCH 141/198] docs(libSceNetCtl): remove trailing blank lines --- docs/dev/debt/libSceNetCtl.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/dev/debt/libSceNetCtl.md b/docs/dev/debt/libSceNetCtl.md index 82f2498c8a..210982cb70 100644 --- a/docs/dev/debt/libSceNetCtl.md +++ b/docs/dev/debt/libSceNetCtl.md @@ -23,5 +23,3 @@ - [sceNetCtlGetInfo](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) finds the host's default route in `/proc/net/route` on Linux and, on macOS, in the routing table read with `sysctl(NET_RT_FLAGS, RTF_GATEWAY)`, where an interface-scoped default route ranks after the unscoped one - [sceNetCtlGetIfStat](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) throws on macOS: an unprivileged process reads the interface byte counters (`getifaddrs` and `sysctl(NET_RT_IFLIST2)` alike) reduced to their low 32 bits, so a total would wrap every 4 GiB - [Host interface cache](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - on Windows, `sceNetCtlGetState`, `sceNetCtlGetStateV6`, `sceNetCtlCheckCallback`, `sceNetCtlGetIfStat` and `sceNetCtlGetInfo` reuse the last host interface query until `NotifyUnicastIpAddressChange`, `NotifyIpInterfaceChange` or `NotifyRouteChange2` reports a change, instead of calling `GetAdaptersAddresses` on every call. A DNS server change with no address, interface or route change is seen only at the next such change. If the notifications cannot be registered, every call queries the host as before. - - From 9b34409537e44f2f0b25292133e8f97dd555be41 Mon Sep 17 00:00:00 2001 From: AZLisme <8706522+AZLisme@users.noreply.github.com> Date: Tue, 27 Oct 2026 03:31:05 +0800 Subject: [PATCH 142/198] test(libSceNetCtl): expect IPv6 IPC callback delivery --- core/libs/tests/GuestNetCtl.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/core/libs/tests/GuestNetCtl.cpp b/core/libs/tests/GuestNetCtl.cpp index d43e18cc23..8141059c78 100644 --- a/core/libs/tests/GuestNetCtl.cpp +++ b/core/libs/tests/GuestNetCtl.cpp @@ -232,12 +232,17 @@ void CheckIpcIntCallbacks() { Require(sceNetCtlUnregisterCallback(internal) == 0); Require(sceNetCtlUnregisterCallbackIpcInt(internal) == idNotFound); + int stateV6 = -1; + Require(sceNetCtlGetStateV6(&stateV6) == 0); + const int eventV6 = stateV6 == kStateIpObtained ? kEventIpObtained : kEventDisconnected; int internalV6 = -1; Require(sceNetCtlRegisterCallbackV6IpcInt(RecordIpcInt, &probe, &internalV6) == 0 && internalV6 == 0); int sharedV6 = -1; Require(sceNetCtlRegisterCallbackV6(RecordIpcInt, &probe, &sharedV6) == 0 && sharedV6 == 1); Require(sceNetCtlCheckCallback() == 0); - Require(g_ipcInt.calls == 2); + Require(g_ipcInt.calls == 4 && g_ipcInt.event == eventV6 && g_ipcInt.arg == &probe); + Require(sceNetCtlCheckCallback() == 0); + Require(g_ipcInt.calls == 4 && g_ipcInt.event == eventV6 && g_ipcInt.arg == &probe); Require(sceNetCtlUnregisterCallbackV6IpcInt(sharedV6) == 0); Require(sceNetCtlUnregisterCallbackV6(internalV6) == 0); Require(sceNetCtlUnregisterCallbackV6IpcInt(internalV6) == idNotFound); From 313a63f293a6b6f81e74236305fdb773da93dea3 Mon Sep 17 00:00:00 2001 From: simnJS Date: Mon, 26 Oct 2026 22:07:52 +0100 Subject: [PATCH 143/198] fix(libSceUsbStorage): follow a moved volume, stop unregistered callbacks and check IsExist's directory --- core/libs/prx/libSceUsbStorage/Export.cpp | 62 ++++++++++++++------ core/libs/tests/GuestUsbStorage.cpp | 71 +++++++++++++---------- docs/dev/debt/libSceUsbStorage.md | 6 +- 3 files changed, 88 insertions(+), 51 deletions(-) diff --git a/core/libs/prx/libSceUsbStorage/Export.cpp b/core/libs/prx/libSceUsbStorage/Export.cpp index 3f3cccc814..ef68706db3 100644 --- a/core/libs/prx/libSceUsbStorage/Export.cpp +++ b/core/libs/prx/libSceUsbStorage/Export.cpp @@ -71,6 +71,7 @@ struct DirectoryScan { struct Service { std::mutex mutex; + std::mutex dispatch; std::condition_variable wake; bool initialized = false; bool stopping = false; @@ -111,6 +112,7 @@ void Update(Service& service, std::vector volumes, std:: if (!device) continue; const auto found = std::find_if(volumes.begin(), volumes.end(), [&](const auto& volume) { return volume.key == device->volume.key; }); if (found != volumes.end()) { + if (found->root != device->volume.root) AddPathAlias_nid_no_patch(MountPoint(slot).c_str(), found->root.string().c_str()); device->volume = std::move(*found); volumes.erase(found); continue; @@ -129,10 +131,29 @@ void Update(Service& service, std::vector volumes, std:: } } -void Dispatch(const std::vector& events, const std::vector& registrations) { +std::vector::iterator Registered(Service& service, std::int32_t eventType, UsbStorageCallback callback) { + return std::find_if(service.registrations.begin(), service.registrations.end(), + [&](const auto& registration) { return registration.eventType == eventType && registration.callback == callback; }); +} + +void Dispatch(Service& service, const std::vector& events) { for (const auto& event : events) { + std::vector registrations; + { + std::lock_guard lock(service.mutex); + registrations = service.registrations; + } for (const auto& registration : registrations) { - if (registration.eventType == static_cast(event.event)) registration.callback(&event, registration.arg); + if (registration.eventType != static_cast(event.event)) continue; + std::lock_guard calling(service.dispatch); + void* arg = nullptr; + { + std::lock_guard lock(service.mutex); + const auto current = Registered(service, registration.eventType, registration.callback); + if (current == service.registrations.end()) continue; + arg = current->arg; + } + registration.callback(&event, arg); } } } @@ -147,19 +168,13 @@ void* APS5_VABI Monitor(void*) { if (service.stopping) break; std::vector events; Update(service, std::move(volumes), events); - const auto registrations = service.registrations; lock.unlock(); - Dispatch(events, registrations); + Dispatch(service, events); lock.lock(); } return nullptr; } -std::vector::iterator Registered(Service& service, std::int32_t eventType, UsbStorageCallback callback) { - return std::find_if(service.registrations.begin(), service.registrations.end(), - [&](const auto& registration) { return registration.eventType == eventType && registration.callback == callback; }); -} - std::filesystem::path HostName(std::string_view name) { return std::filesystem::path(std::u8string(name.begin(), name.end())); } @@ -181,15 +196,20 @@ std::vector DirectoryNames(const char* directory, const char* funct return names; } -std::string Map(Device& device, std::uint32_t slot, const char* directory, const char* function) { +std::filesystem::path HostDirectory(const Device& device, const std::vector& names) { auto host = device.volume.root; + for (const auto& name : names) host /= HostName(name); + return host; +} + +std::string Map(Device& device, std::uint32_t slot, const char* directory, const char* function) { + const auto names = DirectoryNames(directory, function); auto path = MountPoint(slot); - for (const auto& name : DirectoryNames(directory, function)) { - host /= HostName(name); - path += "/" + name; - } + for (const auto& name : names) path += "/" + name; std::error_code error; - if (!std::filesystem::is_directory(host, error)) throw std::runtime_error(std::string(function) + ": mapping a directory the device does not hold is not implemented"); + if (!std::filesystem::is_directory(HostDirectory(device, names), error)) { + throw std::runtime_error(std::string(function) + ": mapping a directory the device does not hold is not implemented"); + } if (path.size() >= UsbStorage::MountPointSize) throw std::runtime_error(std::string(function) + ": a mount point longer than 127 bytes is not implemented"); if (std::find(device.maps.begin(), device.maps.end(), path) != device.maps.end()) { throw std::runtime_error(std::string(function) + ": mapping a mapped directory again is not implemented"); @@ -286,6 +306,13 @@ std::int32_t APS5_VABI sceUsbStorageUnregisterCallback(std::int32_t eventType, U return UsbStorage::InvalidArgument; } auto& service = State(); + bool fromCallback = false; + { + std::lock_guard lock(service.mutex); + fromCallback = service.monitor != nullptr && scePthreadEqual(scePthreadSelf(), service.monitor) != 0; + } + std::unique_lock calling(service.dispatch, std::defer_lock); + if (!fromCallback) calling.lock(); std::lock_guard lock(service.mutex); const auto found = Registered(service, eventType, callback); if (found != service.registrations.end()) { @@ -383,11 +410,12 @@ int APS5_VABI sceUsbStorageGetMountPointOfShellCore(uint32_t device, char* mount } bool APS5_VABI sceUsbStorageIsExist(std::uint32_t device, const char* directory, std::uint8_t* exists) { - (void)directory; if (exists == nullptr) throw std::runtime_error("sceUsbStorageIsExist: a null output is not implemented"); auto& service = State(); std::lock_guard lock(service.mutex); - *exists = Find(service, device) != nullptr ? 1 : 0; + const auto* found = Find(service, device); + std::error_code error; + *exists = found != nullptr && std::filesystem::is_directory(HostDirectory(*found, DirectoryNames(directory, __func__)), error) ? 1 : 0; return true; } diff --git a/core/libs/tests/GuestUsbStorage.cpp b/core/libs/tests/GuestUsbStorage.cpp index 07d66238fc..d526dcf8aa 100644 --- a/core/libs/tests/GuestUsbStorage.cpp +++ b/core/libs/tests/GuestUsbStorage.cpp @@ -88,6 +88,7 @@ struct FakeHost { std::filesystem::path sys; std::filesystem::path firstMount; std::filesystem::path secondMount; + std::filesystem::path movedMount; }; void WriteFile(const std::filesystem::path& path, std::string_view text) { @@ -142,9 +143,9 @@ std::string Utf8(const std::filesystem::path& path) { return std::string(text.begin(), text.end()); } -void Inject(const FakeHost& host, bool first, bool second) { +void Inject(const FakeHost& host, bool first, bool second, bool firstMoved = false) { static const UsbStorageTestVolume none{}; - const auto firstRoot = Utf8(host.firstMount); + const auto firstRoot = Utf8(firstMoved ? host.movedMount : host.firstMount); const auto secondRoot = Utf8(host.secondMount); std::vector volumes; if (first) volumes.push_back({"first", firstRoot.c_str(), FirstVolumeSize, 0x0781, 0x5581, 0x0100, "SanDisk", "Ultra Fit", "4C530001", false, false}); @@ -159,7 +160,9 @@ FakeHost CreateHost() { host.sys = host.root / "sys"; host.firstMount = host.root / "media" / "USB DRIVE"; host.secondMount = host.root / "media" / "second"; + host.movedMount = host.root / "media" / "moved"; WriteFile(host.firstMount / "song.txt", "song"); + WriteFile(host.movedMount / "moved.txt", "moved"); WriteFile(host.firstMount / ".hidden", "hidden"); std::filesystem::create_directories(host.firstMount / "Music"); WriteFile(host.secondMount / "Readme.txt", "readme"); @@ -207,8 +210,6 @@ void TestInitializeAndTerminate() { Require(sceUsbStorageTerm() == 0, "terminate before initialize"); Require(sceUsbStorageInit(nullptr) == 0, "initialize with a null attribute"); Require(sceUsbStorageInit(nullptr) == 0, "initialize again"); - Require(ThrowsWith([] { sceUsbStorageInit(&g_attr); }, "sceUsbStorageInit: a thread attribute is not implemented"), - "initialize with an attribute did not throw"); Require(sceUsbStorageTerm() == 0, "terminate"); Require(sceUsbStorageTerm() == 0, "terminate again"); Require(sceUsbStorageInit(nullptr) == 0, "initialize after terminate"); @@ -244,24 +245,12 @@ void TestRegisterCallbacks() { } void TestMapCallbacks() { - Require(ThrowsWith([] { sceUsbStorageRegisterCallbackForMapAvailable(0, "/data", Callback(), nullptr); }, - "sceUsbStorageRegisterCallbackForMapAvailable not implemented"), - "register map-available callback did not throw its marker"); Require(sceUsbStorageRegisterCallbackForMapAvailable(0, nullptr, Callback(), nullptr) == UsbStorage::InvalidArgument, "register map-available callback with a null directory"); Require(sceUsbStorageRegisterCallbackForMapAvailable(0, "/data", nullptr, nullptr) == UsbStorage::InvalidArgument, "register a null map-available callback"); - Require(ThrowsWith([] { sceUsbStorageUnregisterCallbackForMapAvailable(0, "/data", Callback()); }, - "sceUsbStorageUnregisterCallbackForMapAvailable not implemented"), - "unregister map-available callback did not throw its marker"); Require(sceUsbStorageUnregisterCallbackForMapAvailable(0, nullptr, Callback()) == UsbStorage::InvalidArgument, "unregister map-available callback with a null directory"); Require(sceUsbStorageUnregisterCallbackForMapAvailable(0, "/data", nullptr) == UsbStorage::InvalidArgument, "unregister a null map-available callback"); - Require(ThrowsWith([] { sceUsbStorageRegisterCallbackForMapUnavailable(0, "/data", Callback(), nullptr); }, - "sceUsbStorageRegisterCallbackForMapUnavailable not implemented"), - "register map-unavailable callback did not throw its marker"); Require(sceUsbStorageRegisterCallbackForMapUnavailable(0, nullptr, Callback(), nullptr) == UsbStorage::InvalidArgument, "register map-unavailable callback with a null directory"); Require(sceUsbStorageRegisterCallbackForMapUnavailable(0, "/data", nullptr, nullptr) == UsbStorage::InvalidArgument, "register a null map-unavailable callback"); - Require(ThrowsWith([] { sceUsbStorageUnregisterCallbackForMapUnavailable(0, "/data", Callback()); }, - "sceUsbStorageUnregisterCallbackForMapUnavailable not implemented"), - "unregister map-unavailable callback did not throw its marker"); Require(sceUsbStorageUnregisterCallbackForMapUnavailable(0, nullptr, Callback()) == UsbStorage::InvalidArgument, "unregister map-unavailable callback with a null directory"); Require(sceUsbStorageUnregisterCallbackForMapUnavailable(0, "/data", nullptr) == UsbStorage::InvalidArgument, "unregister a null map-unavailable callback"); } @@ -371,9 +360,19 @@ void TestDeviceInfo(const FakeHost& host) { std::memset(mountPoint, 0x5a, sizeof(mountPoint)); Require(sceUsbStorageGetMountPointOfShellCore(0, mountPoint) == 0 && std::string_view(mountPoint) == "/mnt/usb0", "the device is mounted at /mnt/usb0"); std::uint8_t exists = 0; - Require(sceUsbStorageIsExist(0, "/mnt/usb0", &exists) == true && exists == 1, "the attached device exists"); + Require(sceUsbStorageIsExist(0, "/", &exists) == true && exists == 1, "the root of the attached device exists"); + exists = 0; + Require(sceUsbStorageIsExist(0, nullptr, &exists) == true && exists == 1, "a null directory is the root of the attached device"); + exists = 0; + Require(sceUsbStorageIsExist(0, "Music/", &exists) == true && exists == 1, "a directory of the attached device exists"); exists = 1; - Require(sceUsbStorageIsExist(1, "/mnt/usb1", &exists) == true && exists == 0, "device 1 does not exist"); + Require(sceUsbStorageIsExist(0, "/Missing", &exists) == true && exists == 0, "a missing directory does not exist"); + exists = 1; + Require(sceUsbStorageIsExist(0, "/song.txt", &exists) == true && exists == 0, "a file is not a directory"); + Require(ThrowsWith([&] { sceUsbStorageIsExist(0, "/Music/../Music", &exists); }, "sceUsbStorageIsExist: a directory with . or .. components is not implemented"), + "an existence query with a .. component did not throw"); + exists = 1; + Require(sceUsbStorageIsExist(1, "/", &exists) == true && exists == 0, "device 1 does not exist"); const auto resolved = ResolvePath_nid_no_patch("/mnt/usb0/song.txt"); std::error_code error; Require(std::filesystem::equivalent(resolved, host.firstMount / "song.txt", error), "the mount point resolves to the host volume"); @@ -386,9 +385,6 @@ void TestDeviceMaps() { Require(std::string_view(mountPoint) == "/mnt/usb0" && mountId == 0, "the root is mapped at the mount point"); Require(sceUsbStorageRequestMap(0, "Music/", 0, 0, mountPoint, nullptr) == 0, "map a directory of the device"); Require(std::string_view(mountPoint) == "/mnt/usb0/Music", "the directory is mapped below the mount point"); - Require(ThrowsWith([&] { sceUsbStorageRequestMap(0, "/Music", 0, 0, mountPoint, nullptr); }, - "sceUsbStorageRequestMap: mapping a mapped directory again is not implemented"), - "map a mapped directory again did not throw"); Require(ThrowsWith([&] { sceUsbStorageRequestMap(0, "/Missing", 0, 0, mountPoint, nullptr); }, "sceUsbStorageRequestMap: mapping a directory the device does not hold is not implemented"), "map a missing directory did not throw"); @@ -410,9 +406,6 @@ void TestDeviceMaps() { Require(ThrowsWith([&] { sceUsbStorageRequestMap(0, "Music\\..", 0, 0, mountPoint, nullptr); }, "sceUsbStorageRequestMap: a directory with a backslash is not implemented"), "map a directory with a backslash did not throw"); - Require(ThrowsWith([&] { sceUsbStorageRequestMap(0, "/", 1, 0, mountPoint, nullptr); }, - "sceUsbStorageRequestMap: nonzero flags or a volume size are not implemented"), - "map with flags did not throw"); Require(sceUsbStorageRequestUnmap(0, "/mnt/usb0/Music") == 0, "unmap the directory"); Require(ThrowsWith([] { sceUsbStorageRequestUnmap(0, "/mnt/usb0/Music"); }, "sceUsbStorageRequestUnmap not implemented"), "unmap an unmapped directory did not throw its marker"); @@ -420,9 +413,6 @@ void TestDeviceMaps() { std::int32_t status = 0x5a5a5a5a; Require(sceUsbStorageRequestMapWSB(0, nullptr, 0, 0, mountPoint, &status) == 0, "map the root through the WSB request"); Require(std::string_view(mountPoint) == "/mnt/usb0" && status == 0, "the WSB request mapped the root at the mount point"); - Require(ThrowsWith([&] { sceUsbStorageRequestMapWSB(0, "/Music", 0, 1, mountPoint, &status); }, - "sceUsbStorageRequestMapWSB: nonzero flags or a nonzero fourth argument are not implemented"), - "a WSB request with a fourth argument did not throw"); Require(sceUsbStorageRequestUnmap(0, "/mnt/usb0") == 0, "unmap the WSB root"); } @@ -430,11 +420,7 @@ void TestDeviceGetdents() { std::int32_t fd = -1; std::uint64_t bufferSize = 0; Require(sceUsbStorageGetdentsOpen(0, &fd, &bufferSize) == 0 && fd > 0 && bufferSize == 0x8000, "open a getdents scan of the device"); - std::uint8_t small[8]; std::uint64_t bytesRead = 0; - Require(ThrowsWith([&] { sceUsbStorageGetdentsRead(fd, small, sizeof(small), &bytesRead); }, - "sceUsbStorageGetdentsRead: a buffer smaller than the next entry is not implemented"), - "read into a buffer smaller than an entry did not throw"); std::vector buffer(0x8000, 0x5a); Require(sceUsbStorageGetdentsRead(fd, buffer.data(), buffer.size(), &bytesRead) == 0, "read the getdents scan"); std::set> entries; @@ -507,6 +493,28 @@ void TestHotplug(const FakeHost& host) { Require(Events().events.size() == 3, "hotplug raised no other event"); } +void TestRootChange(const FakeHost& host) { + Inject(host, true, false); + Require(sceUsbStorageInit(nullptr) == 0, "initialize for a root change"); + std::error_code error; + Require(std::filesystem::equivalent(ResolvePath_nid_no_patch("/mnt/usb0/song.txt"), host.firstMount / "song.txt", error), "the volume starts at its first root"); + Require(sceUsbStorageRegisterCallback(UsbStorage::MountEvent, &Record, &g_mountTag) == 0, "register the mount recorder for a root change"); + std::size_t seen = 0; + { + std::lock_guard lock(Events().mutex); + seen = Events().events.size(); + } + Inject(host, true, true, true); + Require(NextEvent(seen).event.device == 1, "the scan that moved the volume mounted the second one"); + Require(std::filesystem::equivalent(ResolvePath_nid_no_patch("/mnt/usb0/moved.txt"), host.movedMount / "moved.txt", error), + "the mount point follows a volume whose key stayed while its root changed"); + std::uint32_t devices[UsbStorage::MaxDevices]{}; + std::int32_t count = -1; + Require(sceUsbStorageGetDeviceList(devices, &count) == 0 && count == 2 && devices[0] == 0, "the moved volume keeps its id"); + Require(sceUsbStorageUnregisterCallback(UsbStorage::MountEvent, &Record) == 0, "unregister the mount recorder after a root change"); + Require(sceUsbStorageTerm() == 0, "terminate after a root change"); +} + void TestSysfsScan(const FakeHost& host) { WriteMounts(host, FirstLine(host) + SecondLine(host) + MountLine("8_1", "/", host.root / "media" / "efi", "rw", "vfat") + MountLine("8_49", "/", host.root / "media", "rw", "ext4") + MountLine("8_17", "/Music", host.root / "media" / "bound", "rw", "vfat")); @@ -543,6 +551,7 @@ int main() { TestThrowingExports(); TestDevices(host); TestHotplug(host); + TestRootChange(host); if (CreateSysfs(host)) { TestSysfsScan(host); } else { diff --git a/docs/dev/debt/libSceUsbStorage.md b/docs/dev/debt/libSceUsbStorage.md index 585966d957..5c8ff4a7e9 100644 --- a/docs/dev/debt/libSceUsbStorage.md +++ b/docs/dev/debt/libSceUsbStorage.md @@ -2,7 +2,7 @@ ### Unknown function info -- [sceUsbStorageRegisterCallback, sceUsbStorageUnregisterCallback](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - the answers for a registration before `sceUsbStorageInit`, a second registration of the same callback for the same event type and the unregistration of a callback that is not registered are unknown, so the first two throw and the third keeps the marker. Whether the console raises mount events for devices attached before the registration is unknown; none are raised. A registration keeps its user data and receives [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Platform/UsbStorage.cs#L186-L199)'s 16-byte event (1 for a mount and 2 for an unmount, the device id, the device flags) for its event type only, on a service thread that rescans the host once a second instead of the automounter's IPC thread. Closes when: measured on a PS5 +- [sceUsbStorageRegisterCallback, sceUsbStorageUnregisterCallback](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - the answers for a registration before `sceUsbStorageInit`, a second registration of the same callback for the same event type and the unregistration of a callback that is not registered are unknown, so the first two throw and the third keeps the marker. Whether the console raises mount events for devices attached before the registration is unknown; none are raised. A registration keeps its user data and receives [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Platform/UsbStorage.cs#L186-L199)'s 16-byte event (1 for a mount and 2 for an unmount, the device id, the device flags) for its event type only, on a service thread that rescans the host once a second instead of the automounter's IPC thread; an unregistered callback is not called again. Closes when: measured on a PS5 - [sceUsbStorageRegisterCallbackForMapAvailable, sceUsbStorageUnregisterCallbackForMapAvailable, sceUsbStorageRegisterCallbackForMapUnavailable, sceUsbStorageUnregisterCallbackForMapUnavailable](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - what a successful registration stores and when the callbacks fire is unverified: [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/usbd/usbstorage.cpp#L128-L174) logs all four as `(STUBBED)` and returns 0 without storing anything, the success-without-a-reference pattern #6288 was closed for. The argument validation is implemented (0x80f40002 for a null callback or a null directory); past it they keep the marker until a reference or a measurement says what success does. - [sceUsbStorageRequestUnmap, sceUsbStorageGetdentsClose](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - what the console answers for a mount point that is not mapped and a getdents descriptor that is not open is unknown: shadPS4's [unmap](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/usbd/usbstorage.cpp#L281-L288) only logs without keeping state and its [close](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/usbd/usbstorage.cpp#L378-L383) succeeds only because `erase` ignores a missing key. Both keep the marker on that path, after their argument validation; unmapping a mount point a map request returned, as [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Platform/UsbStorage.cs#L386-L398) does, and closing an open scan succeed. - [sceUsbStorageClearFakeWriteProtection, sceUsbStorageSetFakeMapLockForDebug, sceUsbStorageSetFakeWriteProtection](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceUsbStorage.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim. @@ -11,11 +11,11 @@ ### Functional -- [the host passthrough](../../../core/libs/prx/libSceUsbStorage/src/HostVolumes.cpp) (libSceUsbStorage) - the devices are the FAT and exFAT volumes the host mounted from USB mass-storage devices: on Linux the whole-file-system `vfat`, `msdos` and `exfat` entries of `/proc/self/mountinfo` whose block device has a USB interface of class 08 among its sysfs ancestors, on [Windows](../../../core/libs/prx/libSceUsbStorage/src/HostVolumesWindows.cpp) the drive letters whose FAT, FAT32 or exFAT volume reports `BusTypeUsb`, and none on macOS. NTFS and a FUSE exFAT (`fuseblk`) are not listed. `sceUsbStorageInit` scans the host and a service thread rescans it once a second; a volume keeps its id, the lowest free of eight slots, while it stays mounted, and `/mnt/usb` resolves to it until a scan drops it or `sceUsbStorageTerm`, with or without a map request. How the console numbers its devices is unknown. Closes when: measured on a PS5 +- [the host passthrough](../../../core/libs/prx/libSceUsbStorage/src/HostVolumes.cpp) (libSceUsbStorage) - the devices are the FAT and exFAT volumes the host mounted from USB mass-storage devices: on Linux the whole-file-system `vfat`, `msdos` and `exfat` entries of `/proc/self/mountinfo` whose block device has a USB interface of class 08 among its sysfs ancestors, on [Windows](../../../core/libs/prx/libSceUsbStorage/src/HostVolumesWindows.cpp) the drive letters whose FAT, FAT32 or exFAT volume reports `BusTypeUsb`, and none on macOS. NTFS and a FUSE exFAT (`fuseblk`) are not listed. `sceUsbStorageInit` scans the host and a service thread rescans it once a second; a volume keeps its id, the lowest free of eight slots, while it stays mounted, and `/mnt/usb` resolves to its current root until a scan drops it or `sceUsbStorageTerm`, with or without a map request. How the console numbers its devices is unknown. Closes when: measured on a PS5 - [the device-info record](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - the 0x338-byte record is unverified on hardware; its layout follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Platform/UsbStorage.cs#L34-L45), which recovered it from the module's own export: the u64 volume size at offset 0, the vendor, product and serial strings at 0x16, 0x120 and 0x228, and the flags the service masks with 0xd0 at 0x330. The vendor id, product id and release at 0x10, 0x12 and 0x14, the string lengths at 0x118, 0x220 and 0x328 and flag bits 4 (read-only), 6 (FAT) and 7 (exFAT) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/usbd/usbstorage.cpp#L28-L55), whose record otherwise starts with a device id where a title reads the volume size. Closes when: measured on a PS5 - [the device-info values](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - which strings and which size the console reports is unknown. The volume size is the capacity of the mounted file system, not of the partition; the vendor and product are the SCSI inquiry strings (sysfs `vendor` and `model`, or the storage device descriptor on Windows) without trailing spaces, and the serial number is the USB one, empty when the device has none. A string longer than 255 bytes throws. Closes when: measured on a PS5 - [the map requests](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - `sceUsbStorageRequestMap` and `sceUsbStorageRequestMapWSB` follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/usbd/usbstorage.cpp#L245-L279): the mount point is `/mnt/usb` followed by the requested directory (the device root for a null or empty one), the last argument receives 0 and null outputs are skipped. The directory must exist on the device; a missing one, a `.` or `..` component, a component with a root name (a drive letter on Windows), a backslash, a mount point longer than 127 bytes, a second map of the same directory, nonzero flags or volume size, and a nonzero fourth `RequestMapWSB` argument throw, as their answers are unknown. Unlike shadPS4, a directory that starts with `/mnt/usb` is not returned unchanged. The mapping limit SharpProspero names (0x80f40015) is not enforced. Closes when: measured on a PS5 -- [sceUsbStorageIsExist](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - it reports whether the device is attached and ignores the directory, as shadPS4's [one-argument version](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/usbd/usbstorage.cpp#L237-L243) has none; what the console does with the directory or a null output, and whether the bool result reports success or failure, is unknown; a null output throws. Closes when: measured on a PS5 +- [sceUsbStorageIsExist](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - it reports whether the attached device holds the directory, resolved and refused as for a map request (the device root for a null or empty one), where shadPS4's [one-argument version](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/usbd/usbstorage.cpp#L237-L243) has no directory; what the console does with the directory or a null output, and whether the bool result reports success or failure, is unknown; a null output throws. Closes when: measured on a PS5 - [the getdents scan](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - `sceUsbStorageGetdentsOpen` lists the root of the device into descriptors numbered from 1 with a 0x8000-byte buffer size, skipping names that start with a dot, and `sceUsbStorageGetdentsRead` writes shadPS4's [packed dirent](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/usbd/usbstorage.cpp#L290-L376) (u32 file number from 1, u16 record length rounded to 8, type 4 for a directory and 8 otherwise, u8 name length, NUL-terminated name) until the buffer is full and zero bytes at the end. A name longer than 255 bytes, a host listing error and a buffer too small for the next record throw where shadPS4 truncates, ignores or answers zero bytes. None of this is measured on a console. Closes when: measured on a PS5 - [the argument validation](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - the null-pointer, null-callback, event-type and null-directory refusals with 0x80f40002, the answers 0x80f40004 for a device that is not attached, and the getdents read of a descriptor that is not open answering 0x80f40005 with zero bytes, follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/usbd/usbstorage.cpp#L176-L383); the validation runs before the markers the exports keep above. None of this is measured on a console. - [the prototypes of sceUsbStorageRequestMap and sceUsbStorageGetDeviceList](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - the signatures follow the ABI restored on `main` (the firmware-database sweep of 2026-10-21), which matches [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/usbd/usbstorage.cpp#L176-L383): `RequestMap`'s six parameters, the typed `UsbStorageCallback` of the register/unregister family, `IsExist`'s three-argument bool form and `GetDeviceList`'s `std::int32_t*` count (the count also follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Platform/UsbStorage.cs#L233-L235), whose earlier eight-parameter `RequestMap` reading carried the trailing reserved pair the restored ABI drops). From 93738dfbe9c62bd01385cc2c07d2d25d3aae69f4 Mon Sep 17 00:00:00 2001 From: Alvaro Date: Fri, 23 Oct 2026 23:25:16 +0200 Subject: [PATCH 144/198] feat(libSceLibreSsl): implement EVP_PKEY_type --- core/libs/prx/libSceLibreSsl/CMakeLists.txt | 13 ++++- core/libs/prx/libSceLibreSsl/Export.cpp | 5 -- core/libs/prx/libSceLibreSsl/src/Pkey.cpp | 53 +++++++++++++++++++++ core/libs/tests/GuestLibreSslPkeyType.cpp | 32 +++++++++++++ docs/dev/debt/libSceLibreSsl.md | 3 +- 5 files changed, 99 insertions(+), 7 deletions(-) create mode 100644 core/libs/prx/libSceLibreSsl/src/Pkey.cpp create mode 100644 core/libs/tests/GuestLibreSslPkeyType.cpp diff --git a/core/libs/prx/libSceLibreSsl/CMakeLists.txt b/core/libs/prx/libSceLibreSsl/CMakeLists.txt index 3621baad39..7f27debea6 100644 --- a/core/libs/prx/libSceLibreSsl/CMakeLists.txt +++ b/core/libs/prx/libSceLibreSsl/CMakeLists.txt @@ -1,4 +1,4 @@ -add_library(libSceLibreSsl SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Digest.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Runtime.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Aes.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Error.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Ocsp.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Stack.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Ui.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Rsa.cpp) +add_library(libSceLibreSsl SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Digest.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Runtime.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Aes.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Error.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Ocsp.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Stack.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Ui.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Rsa.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Pkey.cpp) target_include_directories(libSceLibreSsl PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(libSceLibreSsl PRIVATE mbedcrypto) configure_windows_unwind(libSceLibreSsl) @@ -121,6 +121,17 @@ if(BUILD_TESTING) set_tests_properties(guest_libressl_rsa PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + + add_test_executable(guest_libressl_pkey_type_tests ${LIBS_INCLUDE_DIR}/tests/GuestLibreSslPkeyType.cpp) + target_include_directories(guest_libressl_pkey_type_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_libressl_pkey_type_tests PRIVATE libSceLibreSsl libc) + configure_windows_unwind(guest_libressl_pkey_type_tests) + add_test(NAME guest_libressl_pkey_type COMMAND guest_libressl_pkey_type_tests) + set_tests_properties(guest_libressl_pkey_type PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_libressl_pkey_type PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() target_sources(libSceLibreSsl PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/Cipher.cpp) diff --git a/core/libs/prx/libSceLibreSsl/Export.cpp b/core/libs/prx/libSceLibreSsl/Export.cpp index 00ace935b0..70b83b065a 100644 --- a/core/libs/prx/libSceLibreSsl/Export.cpp +++ b/core/libs/prx/libSceLibreSsl/Export.cpp @@ -639,11 +639,6 @@ int APS5_VABI EVP_PKEY_set1_RSA_nid_postfix(void) { return 0; } -int APS5_VABI EVP_PKEY_type_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI EVP_aes_128_cbc_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceLibreSsl/src/Pkey.cpp b/core/libs/prx/libSceLibreSsl/src/Pkey.cpp new file mode 100644 index 0000000000..cf4d74d85f --- /dev/null +++ b/core/libs/prx/libSceLibreSsl/src/Pkey.cpp @@ -0,0 +1,53 @@ +#include +#include +#include "prx/libc/include/General.hpp" + +namespace { + +constexpr int NidUndef = 0; +constexpr int NidRsaEncryption = 6; +constexpr int NidRsa = 19; +constexpr int NidDhKeyAgreement = 28; +constexpr int NidDsaWithSha = 66; +constexpr int NidDsa2 = 67; +constexpr int NidDsaWithSha1Old = 70; +constexpr int NidDsaWithSha1 = 113; +constexpr int NidDsa = 116; +constexpr int NidEcPublicKey = 408; +constexpr int NidGostR34102001 = 811; +constexpr int NidGost2814789Mac = 815; +constexpr int NidHmac = 855; +constexpr int NidCmac = 894; +constexpr int NidGostR34102012With256 = 946; +constexpr int NidGostR34102012With512 = 947; + +} + +extern "C" { + +int APS5_VABI EVP_PKEY_type_nid_postfix(int type) { + switch (type) { + case NidRsaEncryption: + case NidRsa: + return NidRsaEncryption; + case NidEcPublicKey: + case NidHmac: + case NidCmac: + return type; + case NidDhKeyAgreement: + case NidDsaWithSha: + case NidDsa2: + case NidDsaWithSha1Old: + case NidDsaWithSha1: + case NidDsa: + case NidGostR34102001: + case NidGost2814789Mac: + case NidGostR34102012With256: + case NidGostR34102012With512: + throw std::runtime_error(std::string(__func__) + ": unverified key type " + std::to_string(type)); + default: + return NidUndef; + } +} + +} diff --git a/core/libs/tests/GuestLibreSslPkeyType.cpp b/core/libs/tests/GuestLibreSslPkeyType.cpp new file mode 100644 index 0000000000..592dd735b3 --- /dev/null +++ b/core/libs/tests/GuestLibreSslPkeyType.cpp @@ -0,0 +1,32 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI EVP_PKEY_type_nid_postfix(int); +} + +static void Require(bool condition, std::source_location location = std::source_location::current()) { + if (condition) return; + std::fprintf(stderr, "LibreSSL EVP_PKEY_type check failed at line %u\n", location.line()); + std::abort(); +} + +static void RequireThrows(int type, std::source_location location = std::source_location::current()) { + try { EVP_PKEY_type_nid_postfix(type); } catch (const std::exception&) { return; } + Require(false, location); +} + +int main() { + Require(EVP_PKEY_type_nid_postfix(6) == 6); + Require(EVP_PKEY_type_nid_postfix(19) == 6); + Require(EVP_PKEY_type_nid_postfix(408) == 408); + Require(EVP_PKEY_type_nid_postfix(855) == 855); + Require(EVP_PKEY_type_nid_postfix(894) == 894); + for (const int type : {28, 66, 67, 70, 113, 116, 811, 815, 946, 947}) RequireThrows(type); + for (const int type : {0, -1, 1, 5, 7, 64, 65, 117, 407, 409, 672, 854, 856, 893, 895, 948, INT_MIN, INT_MAX}) Require(EVP_PKEY_type_nid_postfix(type) == 0); + std::puts("libSceLibreSsl EVP_PKEY_type tests passed"); +} diff --git a/docs/dev/debt/libSceLibreSsl.md b/docs/dev/debt/libSceLibreSsl.md index 723c8f3d9d..91281803c4 100644 --- a/docs/dev/debt/libSceLibreSsl.md +++ b/docs/dev/debt/libSceLibreSsl.md @@ -2,7 +2,7 @@ ### Unknown function info -- [libSceLibreSsl pending exports](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - 319 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibreSsl.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. +- [libSceLibreSsl pending exports](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - 318 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibreSsl.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. - [MD4, MD5, SHA1, SHA224, SHA256, SHA384 and SHA512 Init/Update/Final and SHA256](../../../core/libs/prx/libSceLibreSsl/src/Digest.cpp) (libSceLibreSsl) - signatures and context layouts follow the [LibreSSL headers](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/sha/sha.h), and buffering and `Final` follow its [sources](https://github.com/libressl/openbsd/tree/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/sha); nothing is checked on the console. Null contexts, null input with a nonzero length, null digest buffers in the 32-bit `Final` functions and a buffered byte count that is not below the block size, which LibreSSL does not check, throw - [DES_set_key, DES_ecb_encrypt, DES_set_odd_parity](../../../core/libs/prx/libSceLibreSsl/Export.cpp) (libSceLibreSsl) - the 128-byte `DES_key_schedule` holds the 8-byte key followed by zeros instead of LibreSSL's precomputed subkeys, so a title reading its contents sees different bytes; `DES_ecb_encrypt` derives the subkeys through mbedtls on each call. Not compared with the console. Results follow LibreSSL [des_key.c](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/des/des_key.c) and [des.c](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/des/des.c): `DES_set_key` skips the parity and weak-key checks and returns 0, as it does while `DES_check_key` is 0 (that variable is not in the PS5 export inventory), a nonzero `enc` encrypts, and `DES_set_odd_parity` sets each byte's low bit for odd parity. Null pointers throw where LibreSSL faults - [X509_verify_cert_error_string](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - no console call is verified. The 64-bit argument, low-32-bit lookup, messages and shared numeric fallback follow [LibreSSL 2.5.4](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/x509/x509_txt.c), the version identified in [published firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214). Console modifications and other firmware versions remain unverified. Unknown-code results share an overwritten static buffer and are not safe for concurrent use, as in that release; code 1 and codes above 54 use the numeric fallback. @@ -15,6 +15,7 @@ - [SSL_CIPHER_get_bits, SSL_CIPHER_get_name and SSL_CIPHER_get_version](../../../core/libs/prx/libSceLibreSsl/src/Cipher.cpp) - the 88-byte guest cipher layout and these calls are not console-verified. They follow [LibreSSL 2.5.4 ssl.h](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libssl/ssl.h) and [ssl_ciph.c](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libssl/ssl_ciph.c), matching the version in [published firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214). Console modifications and other firmware layouts remain unverified. Protocol labels use the full 64-bit cipher ID, not the protocol flags; TLS 1.2 ciphers still report `TLSv1/SSLv3`, as in that release. - [sk_num, sk_value, sk_pop, sk_pop_free](../../../core/libs/prx/libSceLibreSsl/src/Stack.cpp) (libSceLibreSsl) - the 32-byte `_STACK` layout (`num`, `data`, `sorted`, `num_alloc`, `comp`) and the results follow [LibreSSL 2.5.4](https://github.com/libressl/openbsd/tree/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/stack) `stack.h` and `stack.c`, the version identified in [published firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214); nothing is checked on the console. `sk_pop_free` releases the element array and the stack with the guest `free`, as the module would through libc. No export of this module creates a stack yet, so only stacks the caller builds reach them. A null element array that LibreSSL would read through, and a null free function with a non-null element, throw where LibreSSL faults - [RSA_flags, RSA_size](../../../core/libs/prx/libSceLibreSsl/src/Rsa.cpp) (libSceLibreSsl) - nothing is checked on the console. Layouts and results follow [LibreSSL 2.5.4](https://github.com/libressl/openbsd/tree/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto) `rsa.h`, `rsa_crpt.c` and `bn_lib.c`, the version in the [firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214). No export of this module creates a key yet, so only keys the caller builds reach them. `RSA_size` counts the modulus bytes from its top word, as `BN_num_bytes`, so a zero top word is not skipped. A null method, a null key or modulus, and a positive word count with null words, which LibreSSL dereferences, throw, as do a negative word count and a bit count that overflows `int` +- [EVP_PKEY_type](../../../core/libs/prx/libSceLibreSsl/src/Pkey.cpp) (libSceLibreSsl) - follows the method table of [LibreSSL 2.5.4](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/asn1/ameth_lib.c#L83-L111) `ameth_lib.c`, the version identified in [published firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214), with no engine (every `ENGINE_` export throws): RSA (6) and its alias 19 give 6, EC (408), HMAC (855) and CMAC (894) give themselves, and a type in no build's table gives 0. Which method families the console's build compiles in is unknown: the module exports RSA, EC, HMAC and CMAC functions, but nothing shows DH (28), DSA (66, 67, 70, 113, 116) or GOST (811, 815, 946, 947), so those types throw (a default build gives 28, 116, 811 and 815). Nothing is checked on the console ### Functional From 5ecedd2d01484abbc0564cda017fa14192d6d321 Mon Sep 17 00:00:00 2001 From: Alvaro Date: Sun, 25 Oct 2026 05:59:36 +0100 Subject: [PATCH 145/198] refactor(libSceLibreSsl): declare the EVP_PKEY_type exports in the public header --- core/libs/prx/libSceLibreSsl/include/Api.hpp | 2 +- core/libs/prx/libSceLibreSsl/src/Pkey.cpp | 1 + core/libs/tests/GuestLibreSslPkeyType.cpp | 5 +---- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceLibreSsl/include/Api.hpp b/core/libs/prx/libSceLibreSsl/include/Api.hpp index a114265d0c..46f54dc043 100644 --- a/core/libs/prx/libSceLibreSsl/include/Api.hpp +++ b/core/libs/prx/libSceLibreSsl/include/Api.hpp @@ -246,7 +246,7 @@ int APS5_VABI EVP_PKEY_get1_RSA_nid_postfix(void); int APS5_VABI EVP_PKEY_id_nid_postfix(void); int APS5_VABI EVP_PKEY_new_nid_postfix(void); int APS5_VABI EVP_PKEY_set1_RSA_nid_postfix(void); -int APS5_VABI EVP_PKEY_type_nid_postfix(void); +int APS5_VABI EVP_PKEY_type_nid_postfix(int type); int APS5_VABI EVP_aes_128_cbc_nid_postfix(void); int APS5_VABI EVP_aes_128_cfb8_nid_postfix(void); int APS5_VABI EVP_aes_128_ctr_nid_postfix(void); diff --git a/core/libs/prx/libSceLibreSsl/src/Pkey.cpp b/core/libs/prx/libSceLibreSsl/src/Pkey.cpp index cf4d74d85f..03792e2661 100644 --- a/core/libs/prx/libSceLibreSsl/src/Pkey.cpp +++ b/core/libs/prx/libSceLibreSsl/src/Pkey.cpp @@ -1,3 +1,4 @@ +#include "prx/libSceLibreSsl/include/Api.hpp" #include #include #include "prx/libc/include/General.hpp" diff --git a/core/libs/tests/GuestLibreSslPkeyType.cpp b/core/libs/tests/GuestLibreSslPkeyType.cpp index 592dd735b3..4787e20219 100644 --- a/core/libs/tests/GuestLibreSslPkeyType.cpp +++ b/core/libs/tests/GuestLibreSslPkeyType.cpp @@ -1,3 +1,4 @@ +#include "prx/libSceLibreSsl/include/Api.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include #include @@ -5,10 +6,6 @@ #include #include -extern "C" { -int APS5_VABI EVP_PKEY_type_nid_postfix(int); -} - static void Require(bool condition, std::source_location location = std::source_location::current()) { if (condition) return; std::fprintf(stderr, "LibreSSL EVP_PKEY_type check failed at line %u\n", location.line()); From 1e88908181490f522cb638d54edd62273a8115d7 Mon Sep 17 00:00:00 2001 From: Alvaro Date: Mon, 26 Oct 2026 07:13:53 +0100 Subject: [PATCH 146/198] fix(libSceLibreSsl): resolve the DH, DSA and GOST types in EVP_PKEY_type --- core/libs/prx/libSceLibreSsl/src/Pkey.cpp | 14 ++------------ core/libs/tests/GuestLibreSslPkeyType.cpp | 12 ++++-------- docs/dev/debt/libSceLibreSsl.md | 2 +- 3 files changed, 7 insertions(+), 21 deletions(-) diff --git a/core/libs/prx/libSceLibreSsl/src/Pkey.cpp b/core/libs/prx/libSceLibreSsl/src/Pkey.cpp index 03792e2661..244f5e5d36 100644 --- a/core/libs/prx/libSceLibreSsl/src/Pkey.cpp +++ b/core/libs/prx/libSceLibreSsl/src/Pkey.cpp @@ -1,6 +1,4 @@ #include "prx/libSceLibreSsl/include/Api.hpp" -#include -#include #include "prx/libc/include/General.hpp" namespace { @@ -15,12 +13,8 @@ constexpr int NidDsaWithSha1Old = 70; constexpr int NidDsaWithSha1 = 113; constexpr int NidDsa = 116; constexpr int NidEcPublicKey = 408; -constexpr int NidGostR34102001 = 811; -constexpr int NidGost2814789Mac = 815; constexpr int NidHmac = 855; constexpr int NidCmac = 894; -constexpr int NidGostR34102012With256 = 946; -constexpr int NidGostR34102012With512 = 947; } @@ -31,21 +25,17 @@ int APS5_VABI EVP_PKEY_type_nid_postfix(int type) { case NidRsaEncryption: case NidRsa: return NidRsaEncryption; + case NidDhKeyAgreement: case NidEcPublicKey: case NidHmac: case NidCmac: return type; - case NidDhKeyAgreement: case NidDsaWithSha: case NidDsa2: case NidDsaWithSha1Old: case NidDsaWithSha1: case NidDsa: - case NidGostR34102001: - case NidGost2814789Mac: - case NidGostR34102012With256: - case NidGostR34102012With512: - throw std::runtime_error(std::string(__func__) + ": unverified key type " + std::to_string(type)); + return NidDsa; default: return NidUndef; } diff --git a/core/libs/tests/GuestLibreSslPkeyType.cpp b/core/libs/tests/GuestLibreSslPkeyType.cpp index 4787e20219..a8ba0075e1 100644 --- a/core/libs/tests/GuestLibreSslPkeyType.cpp +++ b/core/libs/tests/GuestLibreSslPkeyType.cpp @@ -3,8 +3,8 @@ #include #include #include +#include #include -#include static void Require(bool condition, std::source_location location = std::source_location::current()) { if (condition) return; @@ -12,18 +12,14 @@ static void Require(bool condition, std::source_location location = std::source_ std::abort(); } -static void RequireThrows(int type, std::source_location location = std::source_location::current()) { - try { EVP_PKEY_type_nid_postfix(type); } catch (const std::exception&) { return; } - Require(false, location); -} - int main() { Require(EVP_PKEY_type_nid_postfix(6) == 6); Require(EVP_PKEY_type_nid_postfix(19) == 6); Require(EVP_PKEY_type_nid_postfix(408) == 408); Require(EVP_PKEY_type_nid_postfix(855) == 855); Require(EVP_PKEY_type_nid_postfix(894) == 894); - for (const int type : {28, 66, 67, 70, 113, 116, 811, 815, 946, 947}) RequireThrows(type); - for (const int type : {0, -1, 1, 5, 7, 64, 65, 117, 407, 409, 672, 854, 856, 893, 895, 948, INT_MIN, INT_MAX}) Require(EVP_PKEY_type_nid_postfix(type) == 0); + Require(EVP_PKEY_type_nid_postfix(28) == 28); + for (const int type : {66, 67, 70, 113, 116}) Require(EVP_PKEY_type_nid_postfix(type) == 116); + for (const int type : {0, -1, 1, 5, 7, 64, 65, 117, 407, 409, 672, 854, 856, 811, 815, 893, 895, 946, 947, 948, INT_MIN, INT_MAX}) Require(EVP_PKEY_type_nid_postfix(type) == 0); std::puts("libSceLibreSsl EVP_PKEY_type tests passed"); } diff --git a/docs/dev/debt/libSceLibreSsl.md b/docs/dev/debt/libSceLibreSsl.md index 91281803c4..53442e0464 100644 --- a/docs/dev/debt/libSceLibreSsl.md +++ b/docs/dev/debt/libSceLibreSsl.md @@ -15,7 +15,7 @@ - [SSL_CIPHER_get_bits, SSL_CIPHER_get_name and SSL_CIPHER_get_version](../../../core/libs/prx/libSceLibreSsl/src/Cipher.cpp) - the 88-byte guest cipher layout and these calls are not console-verified. They follow [LibreSSL 2.5.4 ssl.h](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libssl/ssl.h) and [ssl_ciph.c](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libssl/ssl_ciph.c), matching the version in [published firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214). Console modifications and other firmware layouts remain unverified. Protocol labels use the full 64-bit cipher ID, not the protocol flags; TLS 1.2 ciphers still report `TLSv1/SSLv3`, as in that release. - [sk_num, sk_value, sk_pop, sk_pop_free](../../../core/libs/prx/libSceLibreSsl/src/Stack.cpp) (libSceLibreSsl) - the 32-byte `_STACK` layout (`num`, `data`, `sorted`, `num_alloc`, `comp`) and the results follow [LibreSSL 2.5.4](https://github.com/libressl/openbsd/tree/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/stack) `stack.h` and `stack.c`, the version identified in [published firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214); nothing is checked on the console. `sk_pop_free` releases the element array and the stack with the guest `free`, as the module would through libc. No export of this module creates a stack yet, so only stacks the caller builds reach them. A null element array that LibreSSL would read through, and a null free function with a non-null element, throw where LibreSSL faults - [RSA_flags, RSA_size](../../../core/libs/prx/libSceLibreSsl/src/Rsa.cpp) (libSceLibreSsl) - nothing is checked on the console. Layouts and results follow [LibreSSL 2.5.4](https://github.com/libressl/openbsd/tree/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto) `rsa.h`, `rsa_crpt.c` and `bn_lib.c`, the version in the [firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214). No export of this module creates a key yet, so only keys the caller builds reach them. `RSA_size` counts the modulus bytes from its top word, as `BN_num_bytes`, so a zero top word is not skipped. A null method, a null key or modulus, and a positive word count with null words, which LibreSSL dereferences, throw, as do a negative word count and a bit count that overflows `int` -- [EVP_PKEY_type](../../../core/libs/prx/libSceLibreSsl/src/Pkey.cpp) (libSceLibreSsl) - follows the method table of [LibreSSL 2.5.4](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/asn1/ameth_lib.c#L83-L111) `ameth_lib.c`, the version identified in [published firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214), with no engine (every `ENGINE_` export throws): RSA (6) and its alias 19 give 6, EC (408), HMAC (855) and CMAC (894) give themselves, and a type in no build's table gives 0. Which method families the console's build compiles in is unknown: the module exports RSA, EC, HMAC and CMAC functions, but nothing shows DH (28), DSA (66, 67, 70, 113, 116) or GOST (811, 815, 946, 947), so those types throw (a default build gives 28, 116, 811 and 815). Nothing is checked on the console +- [EVP_PKEY_type](../../../core/libs/prx/libSceLibreSsl/src/Pkey.cpp) (libSceLibreSsl) - follows the method table of [LibreSSL 2.5.4](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/asn1/ameth_lib.c#L83-L111) `ameth_lib.c` built without GOST and with no engine (every `ENGINE_` export throws): RSA (6) and its alias 19 give 6, the DSA types 66, 67, 70, 113 and 116 give 116, DH (28), EC (408), HMAC (855) and CMAC (894) give themselves, and every other type, the GOST types 811, 815, 946 and 947 included, gives 0. These results match [firmware 13.42's module run natively on a PC](https://github.com/boykopovar/AnyPS5/pull/10398#issuecomment-6406072913) for every type from -1000 to 5000 and the `int` extremes; nothing is checked on the console ### Functional From 3b210875a88e078322cfc85f02475f3edcd4bec4 Mon Sep 17 00:00:00 2001 From: p3nicillin Date: Mon, 26 Oct 2026 13:18:08 +0000 Subject: [PATCH 147/198] fix(libkernel): refuse to map direct memory with execute PS5_PayloadSDK measured in a title (firmware 12.09) that sceKernelMapDirectMemory with read-execute or read-write-execute returns 0x80020016, and that sceKernelMprotect then grants execute to a read-write mapping. The map granted execute directly. Co-Authored-By: Claude Opus 5.5 --- .../libkernel/DirectMemory/DirectMemory.cpp | 2 ++ core/libs/tests/GuestMemory.cpp | 20 +++++++++++++++++++ docs/dev/debt/libkernel.md | 1 + 3 files changed, 23 insertions(+) diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp index 4247868360..d433efa405 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp @@ -207,6 +207,7 @@ namespace { constexpr int GuestMapFixedFlag = 0x10; constexpr int GuestProtGpuReadWrite = 0x30; +constexpr int GuestProtCpuExecute = 0x4; #if defined(__linux__) void* MapAtOrAbove(std::uintptr_t start, size_t len, int prot, size_t alignment) { @@ -1018,6 +1019,7 @@ int DoMapDirect(void** addr, size_t len, int prot, int flags, int64_t physStart, if (physStart < 0 || (static_cast(physStart) & (PS5_PAGE_SIZE - 1)) != 0 || static_cast(physStart) >= DIRECT_MEMORY_SIZE || len > DIRECT_MEMORY_SIZE - static_cast(physStart)) { return SCE_KERNEL_ERROR_EINVAL; } + if ((prot & ~0x3F7) == 0 && (prot & GuestProtCpuExecute) != 0) return SCE_KERNEL_ERROR_EINVAL; GuestAllocations::Mutation mutation; flags = PlaceHintInReservation(*addr, len, flags); if (FixedNoOverwriteConflict(mutation, *addr, len, flags)) return SCE_KERNEL_ERROR_ENOMEM; diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index 3b28008f19..e02c30a064 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -231,6 +231,25 @@ static void CheckMprotectIgnoresUndefinedBits() { Require(sceKernelReleaseDirectMemory(physical, length) == 0); } +static void CheckDirectMapRefusesExecute() { + constexpr std::size_t length = 0x4000; + constexpr int invalid = static_cast(0x80020016u); + std::int64_t physical = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, length, 0, 0, &physical) == 0); + for (const int protection : {5, 7, 0x35, 0x37}) { + void* refused = nullptr; + Require(sceKernelMapDirectMemory(&refused, length, protection, 0, physical, 0) == invalid); + Require(refused == nullptr); + } + void* mapping = nullptr; + Require(sceKernelMapDirectMemory(&mapping, length, 3, 0, physical, 0) == 0); + Require(sceKernelMprotect(mapping, length, 7) == 0); + VirtualQueryInfo info{}; + Require(sceKernelVirtualQuery(mapping, 0, &info, sizeof(info)) == 0 && info.protection == 7); + Require(sceKernelMunmap(reinterpret_cast(mapping), length) == 0); + Require(sceKernelReleaseDirectMemory(physical, length) == 0); +} + static void CheckStrictMemoryProtectionBits() { constexpr std::size_t length = 0x4000; std::int64_t physical = 0; @@ -3342,6 +3361,7 @@ int main(int argc, char** argv) { CheckAudioCoprocessorProtection(); CheckMprotectIgnoresUndefinedBits(); CheckStrictMemoryProtectionBits(); + CheckDirectMapRefusesExecute(); CheckPosixMprotectIgnoresUndefinedBits(); CheckDirectMemoryFollowsPhysicalPages(); CheckSparseDirectMemoryBacking(); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 82fd796372..95c74d39ab 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -157,6 +157,7 @@ - [Windows file identity](../../../core/libs/prx/libkernel/File/src/NativeStat.cpp) (libkernel) - `stat` and `fstat` report the volume serial number as `st_dev` and the 64-bit file index folded into the guest's 32-bit `st_ino` (never 0), so two different files can in principle share an `st_ino`; the fold is not taken from a reference. `st_nlink` is clamped to 65535, the width of the guest field. The call keeps the `_wstat64` values when `GetFileInformationByHandle` fails - [lstat of a Windows symbolic link](../../../core/libs/prx/libkernel/File/src/NativeStat.cpp) (libkernel) reports a link whose target is relative, as `symlink` creates them, with the link's own times, `st_dev` and `st_ino`, mode `S_IFLNK | 0777` and the length of the target `readlink` returns as `st_size`. FreeBSD gives a new link `0777` less the creating process's umask, which the host does not keep, so the permission bits are not the console's. A link with an absolute target is reported the same way, using its print name, or its substitute name when the print name is empty, without a `\??\` or `\\?\` prefix. A directory junction is treated as a link only when `ReadJunction` in [WindowsSymbolicLink.hpp](../../../core/libs/prx/libkernel/File/include/WindowsSymbolicLink.hpp) accepts a drive-path target (`X:\...`) after stripping `\??\` / `\\?\`; volume mounts and other non-drive substitutes are not. Consumers of that drive-target path: [NativeStat.cpp](../../../core/libs/prx/libkernel/File/src/NativeStat.cpp) (`lstat`/`readlink`), [Open.cpp](../../../core/libs/prx/libkernel/File/src/Open.cpp) (`O_NOFOLLOW` → `EMLINK`, and `unlink` removing the junction), [Stdio.cpp](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (`rmdir` → `ENOTDIR`), and [Export.cpp](../../../core/libs/prx/libkernel_sys/Export.cpp) (`unlinkat` with and without `AT_REMOVEDIR`). `unlink` / plain `unlinkat` leave the target, including when it is not empty - [lstat of a Windows reparse point that is not a link](../../../core/libs/prx/libkernel/File/src/NativeStat.cpp) (libkernel) reports a mounted volume (`\??\Volume{GUID}\`) or another name surrogate as the reparse point itself, not followed, with the mode of its attributes, as CPython's `os.lstat` does, and a reparse point without the name surrogate bit (deduplication, cloud files) as `stat` reports it +- [sceKernelMapDirectMemory with execute](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - a protection with CPU execute (bit 0x4) returns `EINVAL` (0x80020016) and maps nothing, and `sceKernelMprotect` grants execute afterwards, as [PS5_PayloadSDK measured](https://github.com/mihawk-99/PS5_PayloadSDK/blob/3e31e0b1b68ada28beca68adc480cf0c3d17b9df/platform/docs/PROBE.md#L28-L34) in a title on firmware 12.09 for read-execute and read-write-execute. `sceKernelMapDirectMemory2` and `sceKernelMapNamedDirectMemory` share the check here, which is not measured for them, nor for a title with more privileges than a homebrew one. - [setitimer](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) follows FreeBSD's `sys_setitimer` and `kern_setitimer`: `EINVAL` for timers other than `ITIMER_REAL`, `ITIMER_VIRTUAL` and `ITIMER_PROF`, and for a value, or the interval of a non-zero value, with a negative field, `tv_usec` of 1000000 or more, or more than `INT32_MAX / 2` seconds; a null value reads the timer as `getitimer` does, with `EFAULT` when the output is null too. `ITIMER_REAL` shares the timer thread of `alarm`, which clears its interval: a zero value disarms it, the previous value is returned, and each expiry re-arms it by `it_interval`. With `SIG_IGN` an expiry is dropped. With `SIGALRM` blocked it is left on the pending set of `sigtimedwait`, as `kern_psignal` does, where `sigwait`, `sigtimedwait` and `sigwaitinfo` take it; unblocking it runs a guest handler on the unblocking thread, and with the default disposition exits the process with status 142. Unblocked, the default disposition exits the process with status 142 and a handler throws, since guest `SIGALRM` handlers are never run from the timer thread. The value is not raised to `tick / 16` as `itimerfix` does, and an interval below 1 ms throws, since the console's `hz`, and so that rounding, is unknown (1 ms covers any `hz` of 63 or more). `ITIMER_VIRTUAL` and `ITIMER_PROF` throw when armed, as process CPU time is not accounted, and read and disarm as zero. PPSA99420 imports it and fails to load without it - On Linux, [direct memory](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) lives in one sparse memfd at file offset = physical address, so the [GPU imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) can share one udmabuf buffer object per 64 MiB of it. Released direct memory is not punched out of the file, because a live import of its chunk pins the pages and the GPU would keep reading the old ones: its pages stay committed (the process keeps the high-water mark of the direct memory it used) and are zeroed when the range is allocated again. - [system](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) runs no command. It follows FreeBSD 11's `lib/libc/stdlib/system.c` for a sandbox without `/bin/sh`: a null command returns 1 unconditionally, as that version does, and any command returns the wait status of a child that could not exec the shell and called `_exit(127)` (`0x7f00`). Whether the console's application sandbox has no shell, and whether `vfork` succeeds there (FreeBSD returns -1 when it fails), is unverified From 1ac38cc5c4b7e29d16a12ed8b4ecf4496fb5c379 Mon Sep 17 00:00:00 2001 From: p3nicillin Date: Mon, 26 Oct 2026 21:40:48 +0000 Subject: [PATCH 148/198] docs(libkernel): drop the closed execute gap and name the batch map callers Co-Authored-By: Claude Opus 5.5 --- docs/dev/debt/libkernel.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 95c74d39ab..924bbfb8b9 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -143,7 +143,7 @@ - [Linux runtime module initialization](../../../core/libs/prx/libkernel/Module/src/DynamicLoader.cpp) (libkernel) - modules in explicit runtime directories can be mapped for bindings before an explicit guest load runs their entry and init array once with the caller arguments. Finalizers run only after initialization completed. Console dependency-start and finalization ordering are unmeasured. Windows and macOS use their existing lifecycle paths. - [sceKernelMprotect, sceKernelMtypeprotect](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) over a range with a gap that is not registered in the allocation tracker protect the registered parts and skip the gap, and return 0, as FreeBSD 11.4's [`vm_map_protect`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/vm/vm_map.c) does (it has no check for holes); a range whose start is unmapped (also when later pages are mapped) returns `SCE_KERNEL_ERROR_EINVAL` (`mprotect`: `EINVAL`) because an existing test pins that for an unmapped start; FreeBSD answers 0 there too (`vm_map_protect` takes the next entry), so this errno is not taken from the source and was not changed. `sceKernelMprotect` threw for a gap before, which ended the title. Every part is checked for a GPU or image pin before the first one is protected; the parts are then protected one after the other, so a host protection failure in a later part leaves the earlier parts changed (FreeBSD's `vm_map_protect` cannot fail in its second pass). The behaviour on the console is not known. None of this was measured on a console. -- [sceKernelMprotect protection bits](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - bits outside `0x3F7` are dropped instead of refused, in the POSIX `mprotect` as well (FreeBSD 11.4 masks the argument, `sys/vm/vm_mmap.c:613`; the console was measured only through `sceKernelMprotect`), as a PS5 (firmware 13.60) drops them: Onimusha: Way of the Sword (PPSA27836 01.000.001) maps direct memory with `0xf2` and then protects it with `0xfffffff2`, which the console reports as `0xf3` and then `0x3f3`. In a payload process the console also drops the CPU execute bit (`0x4`) of flexible, direct and JIT memory, and POSIX `mprotect` with write and execute reports write only (`0x2`). A title process may keep execute, so it is kept here. The map calls and `sceKernelMtypeprotect` refuse undefined bits on the console with `EINVAL` (`0x80020016`), and the direct map calls refuse execute; here both throw for undefined bits, and the direct map calls map execute +- [sceKernelMprotect protection bits](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - bits outside `0x3F7` are dropped instead of refused, in the POSIX `mprotect` as well (FreeBSD 11.4 masks the argument, `sys/vm/vm_mmap.c:613`; the console was measured only through `sceKernelMprotect`), as a PS5 (firmware 13.60) drops them: Onimusha: Way of the Sword (PPSA27836 01.000.001) maps direct memory with `0xf2` and then protects it with `0xfffffff2`, which the console reports as `0xf3` and then `0x3f3`. In a payload process the console also drops the CPU execute bit (`0x4`) of flexible, direct and JIT memory, and POSIX `mprotect` with write and execute reports write only (`0x2`). A title process may keep execute, so it is kept here. The map calls and `sceKernelMtypeprotect` refuse undefined bits on the console with `EINVAL` (`0x80020016`); here both throw for undefined bits - [JIT shared memory on macOS](../../../core/libs/prx/libkernel/DirectMemory/Jit.cpp) (libkernel) - the object is an unlinked `mkstemp` file in the temporary directory, as `shm_open` makes its anonymous objects. macOS refuses an executable shared file mapping (`EPERM`), so an executable view is mapped without execute and then given it with `mprotect`. Under Rosetta 2 (Apple M2, macOS 26), code rewritten through one view after it ran from another keeps its old translation until the executable view's protection changes; first writes and a single read-write-execute view, as XPSemu PPSA97358 and VLC PPSA85300 map, are seen. `mmap` of a JIT descriptor is not supported, as file-backed guest `mmap` throws on macOS. - [JIT shared memory](../../../core/libs/prx/libkernel/DirectMemory/Jit.cpp) (libkernel) - `sceKernelJitCreateSharedMemory`, `sceKernelJitCreateAliasOfSharedMemory` and `sceKernelJitMapSharedMemory` take the signatures of [Orbital](https://github.com/AlexAltea/orbital)'s `tools/ida/db_types.json` (name, length, maximum protection, descriptor out; descriptor, maximum protection, descriptor out; descriptor, protection, address in and out), which ps4sdk, ps5-jar-loader and the call sites in RetroArch PPSA99169, XPSemu PPSA97358 and VLC PPSA85300 match; SharpProspero declares the alias as returning the descriptor. The object is a host anonymous file shared by its descriptors (a `memfd` on Linux, a delete-on-close temporary file mapped as an executable section on Windows), the alias is a duplicate descriptor, and every map maps the whole object, so writes through one view show through the others; a non-null address is a hint, as in `mmap` without `MAP_FIXED`. Not measured on the console: a length that is zero or not a multiple of 16 KiB and protection bits other than CPU read, write and execute are `EINVAL`; an alias or a map asking for more than the descriptor's maximum protection (write and execute counting as read) is `EACCES`, as FreeBSD 11's `vm_map_protect` treats a protection past `max_protection`; a descriptor that is not a JIT object is `EBADF`. The name is ignored, the objects do not count against the flexible memory budget, `mprotect` of a view is not limited to the maximum protection, and `mmap` of the descriptors maps the object as a [regular file](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp), so its views and the JIT views see each other's writes. On Linux the `memfd` is created without `MFD_EXEC`, which Linux 6.3 added and older kernels reject with `EINVAL`, and relies on the protection of the mapping: the flag only sets the file's execute permission bits, and the direct memory `memfd` is mapped executable the same way; a host with `vm.memfd_noexec` set to 2 refuses the creation and the call throws - [kqueue and kevent](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) follow FreeBSD 11's [`kern_event.c`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/kern_event.c) (`kqueue_kevent`, `kqueue_register`, `kqueue_scan`, the timer and user filters) over the event queue; nothing here is taken from a console except what the kevent measurements entry lists. The descriptor is a host null device registered as the kqueue: `close` and `sceKernelClose` close the queue and `dup`/`dup2` of it throw, but `read`, `write` and `fstat` act on the null device instead of failing with `EOPNOTSUPP` or reporting `S_IFIFO`. Read and write events are supported on guest sockets only, plus `EVFILT_READ` on the read end and `EVFILT_WRITE` on the write end of a pipe (see the kevent on a pipe entry) and the timer, user and [vnode](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) filters and a read event on the host stream on descriptor 0, which never triggers, as `select` never reports descriptor 0 readable; PPSA34015 registers descriptor 0 for reading in a new kqueue at start-up and does not check the result; other filters except `EVFILT_AIO`, `EVFILT_LIO`, `EVFILT_SENDFILE` and `EVFILT_PROCDESC` (see the kevent measurements entry), `EV_CLEAR` on a socket or a pipe off Linux (see the kevent EV_CLEAR entry) and every change to an existing timer except `EV_DELETE` and the `EV_ADD` re-adds that the kevent measurements entry describes throw. A `NOTE_NSECONDS` period above one second with a fraction is kept to the nanosecond, where 11.0 converts the fraction with the microsecond macro. `EV_DISABLE`, `EV_DISPATCH` and `EV_FORCEONESHOT` follow kern_event.c: a disabled knote stays registered, stops polling or firing and is not reported, a trigger raised while it is disabled is kept and reported after `EV_ENABLE`, where `knote` would drop it, `EV_DISPATCH` reports once and then disables the knote, and `EV_FORCEONESHOT` is ignored on a new knote while on an existing one it sets `EV_ONESHOT` and fires the knote; a negative `nevents` fails with `EINVAL`. A new user event keeps its `fflags` unmasked and is not triggered by `NOTE_TRIGGER`, because `filt_userattach` reads the already cleared `kn_fflags`. The timer limit `kq_calloutmax` (`ENOMEM`) is not enforced, and closing a kqueue wakes a thread waiting on it in `kevent` with `EBADF`, where `kqueue_drain` makes the close wait for that thread. A level-triggered event is reported once per call and moved behind the other events, as in `kqueue_scan`; the `sceKernel*Equeue` calls keep their own collection loop. `NOTE_LOWAT` on a socket sets the low-water mark as FreeBSD 11's [`filt_soread` and `filt_sowrite`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/uipc_socket.c) do, and a later change of the knote replaces it as `kqueue_register` replaces `kn_sdata`: a read event fires once the buffered bytes reach the mark, or at end of file or on a listening socket. A read mark above 1 is taken on stream sockets only: on a datagram socket Linux's `FIONREAD` reports the next datagram where FreeBSD's `sbavail` counts every queued byte, so it throws. A write event fires once the send buffer's free space reaches the mark, as `filt_sowrite` does with `sbspace`; the host reports that space as `SO_SNDBUF` less `SIOCOUTQ` on Linux and as `SO_SNDBUF` alone elsewhere, so off Linux it does not shrink while data is queued. Marks below 1, which make FreeBSD's read filter always fire, throw @@ -157,7 +157,7 @@ - [Windows file identity](../../../core/libs/prx/libkernel/File/src/NativeStat.cpp) (libkernel) - `stat` and `fstat` report the volume serial number as `st_dev` and the 64-bit file index folded into the guest's 32-bit `st_ino` (never 0), so two different files can in principle share an `st_ino`; the fold is not taken from a reference. `st_nlink` is clamped to 65535, the width of the guest field. The call keeps the `_wstat64` values when `GetFileInformationByHandle` fails - [lstat of a Windows symbolic link](../../../core/libs/prx/libkernel/File/src/NativeStat.cpp) (libkernel) reports a link whose target is relative, as `symlink` creates them, with the link's own times, `st_dev` and `st_ino`, mode `S_IFLNK | 0777` and the length of the target `readlink` returns as `st_size`. FreeBSD gives a new link `0777` less the creating process's umask, which the host does not keep, so the permission bits are not the console's. A link with an absolute target is reported the same way, using its print name, or its substitute name when the print name is empty, without a `\??\` or `\\?\` prefix. A directory junction is treated as a link only when `ReadJunction` in [WindowsSymbolicLink.hpp](../../../core/libs/prx/libkernel/File/include/WindowsSymbolicLink.hpp) accepts a drive-path target (`X:\...`) after stripping `\??\` / `\\?\`; volume mounts and other non-drive substitutes are not. Consumers of that drive-target path: [NativeStat.cpp](../../../core/libs/prx/libkernel/File/src/NativeStat.cpp) (`lstat`/`readlink`), [Open.cpp](../../../core/libs/prx/libkernel/File/src/Open.cpp) (`O_NOFOLLOW` → `EMLINK`, and `unlink` removing the junction), [Stdio.cpp](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (`rmdir` → `ENOTDIR`), and [Export.cpp](../../../core/libs/prx/libkernel_sys/Export.cpp) (`unlinkat` with and without `AT_REMOVEDIR`). `unlink` / plain `unlinkat` leave the target, including when it is not empty - [lstat of a Windows reparse point that is not a link](../../../core/libs/prx/libkernel/File/src/NativeStat.cpp) (libkernel) reports a mounted volume (`\??\Volume{GUID}\`) or another name surrogate as the reparse point itself, not followed, with the mode of its attributes, as CPython's `os.lstat` does, and a reparse point without the name surrogate bit (deduplication, cloud files) as `stat` reports it -- [sceKernelMapDirectMemory with execute](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - a protection with CPU execute (bit 0x4) returns `EINVAL` (0x80020016) and maps nothing, and `sceKernelMprotect` grants execute afterwards, as [PS5_PayloadSDK measured](https://github.com/mihawk-99/PS5_PayloadSDK/blob/3e31e0b1b68ada28beca68adc480cf0c3d17b9df/platform/docs/PROBE.md#L28-L34) in a title on firmware 12.09 for read-execute and read-write-execute. `sceKernelMapDirectMemory2` and `sceKernelMapNamedDirectMemory` share the check here, which is not measured for them, nor for a title with more privileges than a homebrew one. +- [sceKernelMapDirectMemory with execute](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - a protection with CPU execute (bit 0x4) returns `EINVAL` (0x80020016) and maps nothing, and `sceKernelMprotect` grants execute afterwards, as [PS5_PayloadSDK measured](https://github.com/mihawk-99/PS5_PayloadSDK/blob/3e31e0b1b68ada28beca68adc480cf0c3d17b9df/platform/docs/PROBE.md#L28-L34) in a title on firmware 12.09 for read-execute and read-write-execute. `sceKernelMapDirectMemory2`, `sceKernelMapNamedDirectMemory`, and `sceKernelBatchMap` and `sceKernelBatchMap2` with operation 0 share the check here, which is not measured for them, nor for a title with more privileges than a homebrew one. - [setitimer](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) follows FreeBSD's `sys_setitimer` and `kern_setitimer`: `EINVAL` for timers other than `ITIMER_REAL`, `ITIMER_VIRTUAL` and `ITIMER_PROF`, and for a value, or the interval of a non-zero value, with a negative field, `tv_usec` of 1000000 or more, or more than `INT32_MAX / 2` seconds; a null value reads the timer as `getitimer` does, with `EFAULT` when the output is null too. `ITIMER_REAL` shares the timer thread of `alarm`, which clears its interval: a zero value disarms it, the previous value is returned, and each expiry re-arms it by `it_interval`. With `SIG_IGN` an expiry is dropped. With `SIGALRM` blocked it is left on the pending set of `sigtimedwait`, as `kern_psignal` does, where `sigwait`, `sigtimedwait` and `sigwaitinfo` take it; unblocking it runs a guest handler on the unblocking thread, and with the default disposition exits the process with status 142. Unblocked, the default disposition exits the process with status 142 and a handler throws, since guest `SIGALRM` handlers are never run from the timer thread. The value is not raised to `tick / 16` as `itimerfix` does, and an interval below 1 ms throws, since the console's `hz`, and so that rounding, is unknown (1 ms covers any `hz` of 63 or more). `ITIMER_VIRTUAL` and `ITIMER_PROF` throw when armed, as process CPU time is not accounted, and read and disarm as zero. PPSA99420 imports it and fails to load without it - On Linux, [direct memory](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) lives in one sparse memfd at file offset = physical address, so the [GPU imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) can share one udmabuf buffer object per 64 MiB of it. Released direct memory is not punched out of the file, because a live import of its chunk pins the pages and the GPU would keep reading the old ones: its pages stay committed (the process keeps the high-water mark of the direct memory it used) and are zeroed when the range is allocated again. - [system](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) runs no command. It follows FreeBSD 11's `lib/libc/stdlib/system.c` for a sandbox without `/bin/sh`: a null command returns 1 unconditionally, as that version does, and any command returns the wait status of a child that could not exec the shell and called `_exit(127)` (`0x7f00`). Whether the console's application sandbox has no shell, and whether `vfork` succeeds there (FreeBSD returns -1 when it fails), is unverified From 4023b8957f98e5ecfc92ceb2b7d56defa616e766 Mon Sep 17 00:00:00 2001 From: simnJS Date: Mon, 26 Oct 2026 22:48:51 +0100 Subject: [PATCH 149/198] test(libSceUsbStorage): cover unregistering from and during a callback --- core/libs/tests/GuestUsbStorage.cpp | 90 +++++++++++++++++++++++++++++ docs/dev/debt/libSceUsbStorage.md | 1 + 2 files changed, 91 insertions(+) diff --git a/core/libs/tests/GuestUsbStorage.cpp b/core/libs/tests/GuestUsbStorage.cpp index d526dcf8aa..e47a92b4dc 100644 --- a/core/libs/tests/GuestUsbStorage.cpp +++ b/core/libs/tests/GuestUsbStorage.cpp @@ -2,6 +2,7 @@ #include "prx/libSceUsbStorage/include/HostVolumes.hpp" #include "prx/libc/include/General.hpp" +#include #include #include #include @@ -82,6 +83,42 @@ RecordedEvent NextEvent(std::size_t index) { return recorder.events[index]; } +std::size_t RecordedCount() { + auto& recorder = Events(); + std::lock_guard lock(recorder.mutex); + return recorder.events.size(); +} + +std::atomic g_selfRemovalCalls{0}; +std::atomic g_selfRemovalResult{-1}; + +void APS5_VABI RemoveSelf(const UsbStorageEvent*, void*) { + ++g_selfRemovalCalls; + g_selfRemovalResult = sceUsbStorageUnregisterCallback(UsbStorage::MountEvent, &RemoveSelf); +} + +struct Blocker { + std::mutex mutex; + std::condition_variable changed; + int calls = 0; + bool entered = false; + bool released = false; +}; + +Blocker& Blocked() { + static Blocker blocker; + return blocker; +} + +void APS5_VABI BlockInside(const UsbStorageEvent*, void*) { + auto& blocker = Blocked(); + std::unique_lock lock(blocker.mutex); + ++blocker.calls; + blocker.entered = true; + blocker.changed.notify_all(); + blocker.changed.wait_for(lock, std::chrono::seconds(10), [&] { return blocker.released; }); +} + struct FakeHost { std::filesystem::path root; std::filesystem::path mountInfo; @@ -515,6 +552,58 @@ void TestRootChange(const FakeHost& host) { Require(sceUsbStorageTerm() == 0, "terminate after a root change"); } +void TestUnregisterDuringDispatch(const FakeHost& host) { + Inject(host, false, false); + Require(sceUsbStorageInit(nullptr) == 0, "initialize for unregistrations during dispatch"); + Require(sceUsbStorageRegisterCallback(UsbStorage::MountEvent, &RemoveSelf, nullptr) == 0, "register a callback that unregisters itself"); + Require(sceUsbStorageRegisterCallback(UsbStorage::MountEvent, &Record, &g_mountTag) == 0, "register the mount recorder after it"); + Require(sceUsbStorageRegisterCallback(UsbStorage::UnmountEvent, &BlockInside, nullptr) == 0, "register a callback that blocks"); + Require(sceUsbStorageRegisterCallback(UsbStorage::UnmountEvent, &Record, &g_unmountTag) == 0, "register the unmount recorder after it"); + const auto seen = RecordedCount(); + + Inject(host, true, false); + Require(NextEvent(seen).event.event == 1, "the first mount reached the recorder"); + Require(g_selfRemovalCalls == 1 && g_selfRemovalResult == 0, "a callback unregistered itself without deadlocking"); + Inject(host, true, true); + Require(NextEvent(seen + 1).event.device == 1, "the second mount reached the recorder"); + Require(g_selfRemovalCalls == 1, "a callback that unregistered itself was called again"); + + Inject(host, false, true); + auto& blocker = Blocked(); + { + std::unique_lock lock(blocker.mutex); + Require(blocker.changed.wait_for(lock, std::chrono::seconds(10), [&] { return blocker.entered; }), "the blocking callback was not called"); + } + std::atomic unregistering{false}; + std::atomic returned{false}; + std::atomic result{-1}; + std::thread other([&] { + unregistering = true; + result = sceUsbStorageUnregisterCallback(UsbStorage::UnmountEvent, &BlockInside); + returned = true; + }); + while (!unregistering) std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + Require(!returned, "an unregistration returned while its callback was still running"); + { + std::lock_guard lock(blocker.mutex); + blocker.released = true; + } + blocker.changed.notify_all(); + other.join(); + Require(returned && result == 0, "the unregistration returned after its callback"); + Require(NextEvent(seen + 2).event.device == 0, "the first unmount reached the recorder"); + Inject(host, false, false); + Require(NextEvent(seen + 3).event.device == 1, "the second unmount reached the recorder"); + { + std::lock_guard lock(blocker.mutex); + Require(blocker.calls == 1, "a callback was called after its unregistration returned"); + } + Require(sceUsbStorageUnregisterCallback(UsbStorage::MountEvent, &Record) == 0, "unregister the mount recorder after dispatch"); + Require(sceUsbStorageUnregisterCallback(UsbStorage::UnmountEvent, &Record) == 0, "unregister the unmount recorder after dispatch"); + Require(sceUsbStorageTerm() == 0, "terminate after unregistrations during dispatch"); +} + void TestSysfsScan(const FakeHost& host) { WriteMounts(host, FirstLine(host) + SecondLine(host) + MountLine("8_1", "/", host.root / "media" / "efi", "rw", "vfat") + MountLine("8_49", "/", host.root / "media", "rw", "ext4") + MountLine("8_17", "/Music", host.root / "media" / "bound", "rw", "vfat")); @@ -552,6 +641,7 @@ int main() { TestDevices(host); TestHotplug(host); TestRootChange(host); + TestUnregisterDuringDispatch(host); if (CreateSysfs(host)) { TestSysfsScan(host); } else { diff --git a/docs/dev/debt/libSceUsbStorage.md b/docs/dev/debt/libSceUsbStorage.md index 5c8ff4a7e9..8d4954d45e 100644 --- a/docs/dev/debt/libSceUsbStorage.md +++ b/docs/dev/debt/libSceUsbStorage.md @@ -12,6 +12,7 @@ ### Functional - [the host passthrough](../../../core/libs/prx/libSceUsbStorage/src/HostVolumes.cpp) (libSceUsbStorage) - the devices are the FAT and exFAT volumes the host mounted from USB mass-storage devices: on Linux the whole-file-system `vfat`, `msdos` and `exfat` entries of `/proc/self/mountinfo` whose block device has a USB interface of class 08 among its sysfs ancestors, on [Windows](../../../core/libs/prx/libSceUsbStorage/src/HostVolumesWindows.cpp) the drive letters whose FAT, FAT32 or exFAT volume reports `BusTypeUsb`, and none on macOS. NTFS and a FUSE exFAT (`fuseblk`) are not listed. `sceUsbStorageInit` scans the host and a service thread rescans it once a second; a volume keeps its id, the lowest free of eight slots, while it stays mounted, and `/mnt/usb` resolves to its current root until a scan drops it or `sceUsbStorageTerm`, with or without a map request. How the console numbers its devices is unknown. Closes when: measured on a PS5 +- [sceUsbStorageUnregisterCallback](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - it waits for a callback that is running, except when called from inside a callback, so a callback must not wait on a thread that is unregistering. Whether the console's unregistration waits is unknown. Closes when: measured on a PS5 - [the device-info record](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - the 0x338-byte record is unverified on hardware; its layout follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Platform/UsbStorage.cs#L34-L45), which recovered it from the module's own export: the u64 volume size at offset 0, the vendor, product and serial strings at 0x16, 0x120 and 0x228, and the flags the service masks with 0xd0 at 0x330. The vendor id, product id and release at 0x10, 0x12 and 0x14, the string lengths at 0x118, 0x220 and 0x328 and flag bits 4 (read-only), 6 (FAT) and 7 (exFAT) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/usbd/usbstorage.cpp#L28-L55), whose record otherwise starts with a device id where a title reads the volume size. Closes when: measured on a PS5 - [the device-info values](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - which strings and which size the console reports is unknown. The volume size is the capacity of the mounted file system, not of the partition; the vendor and product are the SCSI inquiry strings (sysfs `vendor` and `model`, or the storage device descriptor on Windows) without trailing spaces, and the serial number is the USB one, empty when the device has none. A string longer than 255 bytes throws. Closes when: measured on a PS5 - [the map requests](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - `sceUsbStorageRequestMap` and `sceUsbStorageRequestMapWSB` follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/usbd/usbstorage.cpp#L245-L279): the mount point is `/mnt/usb` followed by the requested directory (the device root for a null or empty one), the last argument receives 0 and null outputs are skipped. The directory must exist on the device; a missing one, a `.` or `..` component, a component with a root name (a drive letter on Windows), a backslash, a mount point longer than 127 bytes, a second map of the same directory, nonzero flags or volume size, and a nonzero fourth `RequestMapWSB` argument throw, as their answers are unknown. Unlike shadPS4, a directory that starts with `/mnt/usb` is not returned unchanged. The mapping limit SharpProspero names (0x80f40015) is not enforced. Closes when: measured on a PS5 From 5255577954c2ff77aab20e4f715edf16f2c40c4a Mon Sep 17 00:00:00 2001 From: atharvjairath <54663702+atharvjairath@users.noreply.github.com> Date: Mon, 26 Oct 2026 01:27:59 +0530 Subject: [PATCH 150/198] feat(libressl3): retain EVP and EC key references --- core/libs/prx/libSceLibreSsl3/CMakeLists.txt | 10 ++- core/libs/prx/libSceLibreSsl3/Export.cpp | 10 --- core/libs/prx/libSceLibreSsl3/include/Api.hpp | 5 +- core/libs/prx/libSceLibreSsl3/include/Key.hpp | 30 ++++++++ core/libs/prx/libSceLibreSsl3/src/Key.cpp | 38 +++++++++++ .../libs/tests/GuestLibreSsl3KeyReference.cpp | 68 +++++++++++++++++++ docs/dev/debt/libSceLibreSsl3.md | 3 +- 7 files changed, 149 insertions(+), 15 deletions(-) create mode 100644 core/libs/prx/libSceLibreSsl3/include/Key.hpp create mode 100644 core/libs/prx/libSceLibreSsl3/src/Key.cpp create mode 100644 core/libs/tests/GuestLibreSsl3KeyReference.cpp diff --git a/core/libs/prx/libSceLibreSsl3/CMakeLists.txt b/core/libs/prx/libSceLibreSsl3/CMakeLists.txt index 9f5ecac040..c787bccfea 100644 --- a/core/libs/prx/libSceLibreSsl3/CMakeLists.txt +++ b/core/libs/prx/libSceLibreSsl3/CMakeLists.txt @@ -1,4 +1,4 @@ -add_library(libSceLibreSsl3 SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Digest.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/IntegerDecoder.cpp) +add_library(libSceLibreSsl3 SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Digest.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/IntegerDecoder.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Key.cpp) target_include_directories(libSceLibreSsl3 PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(libSceLibreSsl3 PRIVATE libSceLibreSsl libc) configure_windows_unwind(libSceLibreSsl3) @@ -21,8 +21,14 @@ if(BUILD_TESTING) configure_windows_unwind(guest_libressl3_asn1_tests) add_test(NAME guest_libressl3_asn1 COMMAND guest_libressl3_asn1_tests) set_tests_properties(guest_libressl3_asn1 PROPERTIES TIMEOUT 30) + add_test_executable(guest_libressl3_key_reference_tests ${LIBS_INCLUDE_DIR}/tests/GuestLibreSsl3KeyReference.cpp) + target_include_directories(guest_libressl3_key_reference_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_libressl3_key_reference_tests PRIVATE libSceLibreSsl3) + configure_windows_unwind(guest_libressl3_key_reference_tests) + add_test(NAME guest_libressl3_key_reference COMMAND guest_libressl3_key_reference_tests) + set_tests_properties(guest_libressl3_key_reference PROPERTIES TIMEOUT 20) if(WIN32) - set_tests_properties(guest_crypto_memcmp guest_libressl3_digest guest_libressl3_asn1 PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_crypto_memcmp guest_libressl3_digest guest_libressl3_asn1 guest_libressl3_key_reference PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libSceLibreSsl3/Export.cpp b/core/libs/prx/libSceLibreSsl3/Export.cpp index e2fa6b0ef4..0f7cb0e894 100644 --- a/core/libs/prx/libSceLibreSsl3/Export.cpp +++ b/core/libs/prx/libSceLibreSsl3/Export.cpp @@ -61,11 +61,6 @@ int APS5_VABI DSA_get0_pqg_nid_postfix(void) { return 0; } -int APS5_VABI EC_KEY_up_ref_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI ERR_load_crypto_strings_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -181,11 +176,6 @@ int APS5_VABI EVP_PKEY_size_nid_postfix(void) { return 0; } -int APS5_VABI EVP_PKEY_up_ref_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI EVP_SignFinal_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceLibreSsl3/include/Api.hpp b/core/libs/prx/libSceLibreSsl3/include/Api.hpp index 396e514cae..49ba411188 100644 --- a/core/libs/prx/libSceLibreSsl3/include/Api.hpp +++ b/core/libs/prx/libSceLibreSsl3/include/Api.hpp @@ -1,6 +1,7 @@ #pragma once #include "SceTypes.hpp" +#include "prx/libSceLibreSsl3/include/Key.hpp" #include "prx/libSceLibreSsl/include/Api.hpp" extern "C" { @@ -15,7 +16,7 @@ int APS5_VABI DH_get0_key_nid_postfix(void); int APS5_VABI DH_get0_pqg_nid_postfix(void); int APS5_VABI DSA_get0_key_nid_postfix(void); int APS5_VABI DSA_get0_pqg_nid_postfix(void); -int APS5_VABI EC_KEY_up_ref_nid_postfix(void); +int APS5_VABI EC_KEY_up_ref_nid_postfix(LibreSsl3EcKey* key); int APS5_VABI ERR_load_crypto_strings_nid_postfix(void); int APS5_VABI ERR_peek_last_error_nid_postfix(void); int APS5_VABI ERR_print_errors_cb_nid_postfix(void); @@ -39,7 +40,7 @@ int APS5_VABI EVP_PKEY_get0_RSA_nid_postfix(void); int APS5_VABI EVP_PKEY_new_mac_key_nid_postfix(void); int APS5_VABI EVP_PKEY_set1_EC_KEY_nid_postfix(void); int APS5_VABI EVP_PKEY_size_nid_postfix(void); -int APS5_VABI EVP_PKEY_up_ref_nid_postfix(void); +int APS5_VABI EVP_PKEY_up_ref_nid_postfix(LibreSsl3EvpPkey* key); int APS5_VABI EVP_SignFinal_nid_postfix(void); int APS5_VABI EVP_VerifyFinal_nid_postfix(void); int APS5_VABI EVP_aes_128_wrap_nid_postfix(void); diff --git a/core/libs/prx/libSceLibreSsl3/include/Key.hpp b/core/libs/prx/libSceLibreSsl3/include/Key.hpp new file mode 100644 index 0000000000..bc0a301686 --- /dev/null +++ b/core/libs/prx/libSceLibreSsl3/include/Key.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include +#include + +struct LibreSsl3EvpPkey { + std::int32_t type; + std::int32_t references; + const void* method; + void* key; + std::int32_t saveParameters; +}; + +struct LibreSsl3EcKey { + const void* method; + std::int32_t version; + void* group; + void* publicKey; + void* privateKey; + std::uint32_t encodingFlags; + std::int32_t conversionForm; + std::int32_t references; + std::int32_t flags; + void* extraData; +}; + +static_assert(sizeof(LibreSsl3EvpPkey) == 32); +static_assert(offsetof(LibreSsl3EvpPkey, references) == 4); +static_assert(sizeof(LibreSsl3EcKey) == 64); +static_assert(offsetof(LibreSsl3EcKey, references) == 48); diff --git a/core/libs/prx/libSceLibreSsl3/src/Key.cpp b/core/libs/prx/libSceLibreSsl3/src/Key.cpp new file mode 100644 index 0000000000..d5c4b7a856 --- /dev/null +++ b/core/libs/prx/libSceLibreSsl3/src/Key.cpp @@ -0,0 +1,38 @@ +#include "prx/libSceLibreSsl3/libSceLibreSsl3.hpp" + +#include +#include +#include + +namespace { + +static_assert(std::atomic_ref::required_alignment <= alignof(std::int32_t)); + +int Retain_nid_no_patch(std::int32_t& references) { + std::atomic_ref count(references); + auto previous = count.load(); + for (;;) { + if (previous == std::numeric_limits::max()) + throw std::overflow_error("LibreSSL3 key reference count overflow"); + if (count.compare_exchange_weak(previous, previous + 1)) + return previous > 0; + } +} + +} // namespace + +extern "C" { + +int APS5_VABI EVP_PKEY_up_ref_nid_postfix(LibreSsl3EvpPkey* key) { + if (!key) + throw std::invalid_argument("EVP_PKEY_up_ref requires a key"); + return Retain_nid_no_patch(key->references); +} + +int APS5_VABI EC_KEY_up_ref_nid_postfix(LibreSsl3EcKey* key) { + if (!key) + throw std::invalid_argument("EC_KEY_up_ref requires a key"); + return Retain_nid_no_patch(key->references); +} + +} diff --git a/core/libs/tests/GuestLibreSsl3KeyReference.cpp b/core/libs/tests/GuestLibreSsl3KeyReference.cpp new file mode 100644 index 0000000000..ca69aceb4e --- /dev/null +++ b/core/libs/tests/GuestLibreSsl3KeyReference.cpp @@ -0,0 +1,68 @@ +#include "prx/libSceLibreSsl3/libSceLibreSsl3.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +static void Require(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "%s\n", message); + std::abort(); + } +} + +template +static void CheckKey(TRetain retain, std::size_t referenceOffset) { + struct GuardedKey { + std::array before; + TKey key; + std::array after; + } guarded; + for (std::int32_t initial : {-2, 0, 1, 37, std::numeric_limits::max() - 1}) { + std::memset(&guarded, 0xA5, sizeof(guarded)); + guarded.key.references = initial; + std::array expected; + std::memcpy(expected.data(), &guarded, sizeof(guarded)); + const std::int32_t next = initial + 1; + std::memcpy(expected.data() + 16 + referenceOffset, &next, sizeof(next)); + Require(retain(&guarded.key) == (next > 1), "retain return value"); + Require(std::memcmp(&guarded, expected.data(), sizeof(guarded)) == 0, "retain changed bytes outside reference count"); + } + std::memset(&guarded, 0x5A, sizeof(guarded)); + guarded.key.references = 1; + std::array expected; + std::memcpy(expected.data(), &guarded, sizeof(guarded)); + for (int index = 0; index < 32; ++index) + Require(retain(&guarded.key) == 1, "repeated retain failed"); + Require(guarded.key.references == 33, "repeated retain count"); + guarded.key.references = 1; + std::atomic start = false; + std::array threads; + for (auto& thread : threads) { + thread = std::thread([&] { + while (!start.load()) + std::this_thread::yield(); + for (int index = 0; index < 1000; ++index) + Require(retain(&guarded.key) == 1, "concurrent retain failed"); + }); + } + start = true; + for (auto& thread : threads) + thread.join(); + const std::int32_t finalCount = 4001; + std::memcpy(expected.data() + 16 + referenceOffset, &finalCount, sizeof(finalCount)); + Require(std::memcmp(&guarded, expected.data(), sizeof(guarded)) == 0, "concurrent count or adjacent bytes changed"); +} + +int main(int argc, char** argv) { + if (argc == 1 || std::strcmp(argv[1], "evp") == 0) + CheckKey(EVP_PKEY_up_ref_nid_postfix, 4); + if (argc == 1 || std::strcmp(argv[1], "ec") == 0) + CheckKey(EC_KEY_up_ref_nid_postfix, 48); + return 0; +} diff --git a/docs/dev/debt/libSceLibreSsl3.md b/docs/dev/debt/libSceLibreSsl3.md index fb9e7fe201..6607b4c8c5 100644 --- a/docs/dev/debt/libSceLibreSsl3.md +++ b/docs/dev/debt/libSceLibreSsl3.md @@ -2,10 +2,11 @@ ### Unknown function info -- [libSceLibreSsl3 pending exports](../../../core/libs/prx/libSceLibreSsl3/Export.cpp) - 83 named callable exports, excluding `CRYPTO_memcmp`, `MD5`, `SHA1` and `d2i_ASN1_INTEGER`, remain unimplemented from the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibreSsl3.c), cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. +- [libSceLibreSsl3 pending exports](../../../core/libs/prx/libSceLibreSsl3/Export.cpp) - 81 named callable exports, excluding `CRYPTO_memcmp`, `MD5`, `SHA1`, `d2i_ASN1_INTEGER`, `EVP_PKEY_up_ref` and `EC_KEY_up_ref`, remain unimplemented from the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibreSsl3.c), cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. ### Functional - [ASN.1 integer decoding](../../../core/libs/prx/libSceLibreSsl3/src/IntegerDecoder.cpp) throws on rejected encodings/allocation failures instead of matching native NULL/error handling. Redundant-sign/empty-content rejection retains old output that the module frees and clears. The decoder does not populate the legacy queue; remaining LibreSSL3 ERR exports throw. [13.42 native-PC evidence](https://github.com/boykopovar/AnyPS5/pull/10948#issuecomment-6412137728), using a recording allocator, confirms tested replacement/free order, allocation cleanup and negative-length NULL returns. Nonnegative null input faults natively and throws here. Console/application-heap ABI and untested error-precedence/overflow combinations remain unverified. Contracts follow [LibreSSL 4.0.0](https://github.com/libressl/openbsd/tree/ce488b78e0bd994780569df718596b9776ff681a/src/lib/libcrypto/asn1). Closes when: error/replacement semantics and console/allocator interoperability are verified. - [CRYPTO_memcmp](../../../core/libs/prx/libSceLibreSsl3/Export.cpp) follows the public [LibreSSL signature and equality contract](https://man.openbsd.org/CRYPTO_memcmp.3), without PS5 verification. The exact nonzero result for unequal buffers is unspecified by that contract. Content-independent execution of its volatile byte reads still depends on the compiler; inspect optimized code when changing toolchains. Closes when: measured on a PS5 and the supported toolchains' generated code is checked. - [MD5 and SHA1](../../../core/libs/prx/libSceLibreSsl3/src/Digest.cpp) (libSceLibreSsl3) - follow LibreSSL's [md5.c, sha1.c](https://github.com/libressl/openbsd/tree/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto) and hash through libSceLibreSsl's `MD5_*` and `SHA1_*`. A null digest buffer, which LibreSSL 4.0.0 does not check, throws. Nothing is checked on the console +- [EVP_PKEY_up_ref and EC_KEY_up_ref](../../../core/libs/prx/libSceLibreSsl3/src/Key.cpp) - console layouts and retain/release interoperability are unverified. The 32-byte EVP and 64-byte EC layouts, with counters at offsets 4 and 48, follow [LibreSSL 4.0.0](https://github.com/libressl/openbsd/tree/ce488b78e0bd994780569df718596b9776ff681a/src/lib/libcrypto) `evp/evp_local.h`, `evp/p_lib.c`, `ec/ec_local.h` and `ec/ec_key.c`. Atomic retains replace its internal mutex; future releases must synchronize with the same counters. With no key constructors/destructors in this module, tests use caller-owned objects without validating allocator or destruction compatibility. Null keys and signed-counter overflow, undefined upstream, throw without mutation. Closes when: console layouts and lifetime interoperability are measured. From 03e232ed5ce50f6184cddb5a966fe66c2ca3b3c0 Mon Sep 17 00:00:00 2001 From: SnekPolylepis <100386333+SnekPolylepis@users.noreply.github.com> Date: Thu, 22 Oct 2026 12:44:17 -0500 Subject: [PATCH 151/198] feat(prx): declare verified missing system exports --- core/libs/prx/libSceAppInstUtil/Export.cpp | 35 ++ core/libs/prx/libSceAvSetting/Export.cpp | 522 +++++++++++++++++- core/libs/prx/libSceCdlgUtilServer/Export.cpp | 55 +- core/libs/prx/libSceGvMp4Parser/Export.cpp | 25 +- core/libs/prx/libSceHidControl/Export.cpp | 117 +++- core/libs/prx/libSceJpegParser/Export.cpp | 25 +- .../prx/libSceMetadataReaderWriter/Export.cpp | 195 ++++++- core/libs/prx/libScePsm/Export.cpp | 325 ++++++++++- docs/dev/debt/libSceAppInstUtil.md | 3 + docs/dev/debt/libSceAvSetting.md | 22 + docs/dev/debt/libSceCdlgUtilServer.md | 7 + docs/dev/debt/libSceGvMp4Parser.md | 5 + docs/dev/debt/libSceHidControl.md | 6 + docs/dev/debt/libSceJpegParser.md | 5 + docs/dev/debt/libSceMetadataReaderWriter.md | 12 + docs/dev/debt/libScePsm.md | 17 + 16 files changed, 1367 insertions(+), 9 deletions(-) create mode 100644 docs/dev/debt/libSceCdlgUtilServer.md create mode 100644 docs/dev/debt/libSceGvMp4Parser.md create mode 100644 docs/dev/debt/libSceJpegParser.md create mode 100644 docs/dev/debt/libSceMetadataReaderWriter.md create mode 100644 docs/dev/debt/libScePsm.md diff --git a/core/libs/prx/libSceAppInstUtil/Export.cpp b/core/libs/prx/libSceAppInstUtil/Export.cpp index 51721067a8..e5481ca706 100644 --- a/core/libs/prx/libSceAppInstUtil/Export.cpp +++ b/core/libs/prx/libSceAppInstUtil/Export.cpp @@ -573,4 +573,39 @@ int APS5_VABI sceAppInstUtilInstallAddons(void) { return 0; } +int APS5_VABI sceAppInstUtilAppConvertAppDiscToDownload(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppInstUtilAppGetCustomThemeInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppInstUtilAppGetInstallAddcontFromDiscStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppInstUtilAppInstallCloudGame(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppInstUtilAppInstallMediaPlayer(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppInstUtilAppInstallRequestAddcontFromDisc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppInstUtilAppSetSavedataUploadStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceAvSetting/Export.cpp b/core/libs/prx/libSceAvSetting/Export.cpp index 1861a92646..64d53713fc 100644 --- a/core/libs/prx/libSceAvSetting/Export.cpp +++ b/core/libs/prx/libSceAvSetting/Export.cpp @@ -6,8 +6,6 @@ extern "C" { -APS5_DUMMY_FUN - int APS5_VABI sceAvControlGetPuTypeByMonitorInfo(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -128,4 +126,524 @@ int APS5_VABI sceAvControlDebugAddCallbacks(void) { return 0; } +int APS5_VABI sceAvControlChangeOutputLayout() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlChangeOutputMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlChangeOutputModeForDiag() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlChangeProcessAttribute() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlChangeScanInMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlGetCurrentDeviceId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlGetCurrentOutputMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlGetCurrentOutputModeWithSync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlGetDetailHdcpStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlGetDeviceInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlGetHdcpStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlGetMonitorInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlGetNativeMonitorInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlGetPuStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlGetPuType() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlIsAudioOutModeSupportedByMonitorInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlIsColorimetryHdr() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlIsDeepColorSupportedByMonitorInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlIsDeviceConnected() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlIsHdrSupportedByMonitorInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlIsModeGen1Vr() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlIsModeSystemDefault() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlIsSuspendedProcessOutputModeAvailable() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlIsVideoOutModeSupportedByMonitorInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlSet2dVrMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlSetAudioMuteForShutdown() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlSetAvOutputMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlSetColorEffect() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlSetDefaultAudioOutMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlSetDefaultVideoOutMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlSetDispclk() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlSetGamutMetadata() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlSetHdcpEncryption() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlSetHdcpStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlSetHdmiGamutMetadata() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlSetHdrMetadata() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlSetVideoOutSource() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlSimulateProcessOutputModeArbitration() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingAddCallbacks() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingAddCallbacksForLnc() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingAddCallbacksForSocialScreen() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingCallbackFuncsInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingCallbackFuncsInitForLnc() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingCallbackFuncsInitForLnc_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingCallbackFuncsInit_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingChangeOutputMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingChangeOutputMode2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingChangeOutputMode3() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingChangeOutputModeForDiag() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingChangeProcessAttribute() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingCheckCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingControlHdcpEncryption() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingDebugAddCallbacks() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingDebugClearDiagCommand() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingDebugSetDiagState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingDebugSetHdmiMonitorInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingDebugSetProcessAttribute() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingDriverChangeConnectionStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingDriverUpdateStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingEnterAudioMuteForShutdown() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingGetCurrentDeviceInfo_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingGetCurrentHdmiDeviceId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingGetCurrentOutputMode2_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingGetCurrentOutputMode_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingGetDetailedHdcpStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingGetDeviceInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingGetHdcpStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingGetHdmiConnectDisconnectNum() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingGetHdmiKsvList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingGetHdmiMonitorInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingGetHdmiRawEdid() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingGetMorpheusPuStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingGetNativeHdmiMonitorInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingGetRawHdmiMonitorInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingIsSupportedAudioOutModeByHdmiMonitorInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingIsSupportedHdcpVersionByHdmiMonitorInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingIsSupportedVideoOutModeByHdmiMonitorInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingIsSuspendedProcessOutputModeAvailable() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingIsSuspendedProcessOutputModeAvailable2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingIsVrSupportedByHdmiMonitorInfo2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingNotifyAudioOutMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingNotifyResponseForSocialScreen() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingNotifyUmdEvent() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingNotifyVideoOutMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingRemoveCallbacks() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingRemoveProcessOutputMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingSet2dVrMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingSetAudioOutModeAny_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingSetAudioOutModeInvalid_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingSetAvOutputMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingSetDispclk() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingSetDisplayBlackLevel() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingSetHdcpMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingSetHdcpStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingSetHdmiGamutMetadata_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingSetVideoOutModeAny_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingSetVideoOutModeInvalid_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingSetVideoOutSource() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingSetVideoOutputColorEffect() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingSimulateProcessOutputModeArbitration_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingTerm() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingWaitCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceCdlgUtilServer/Export.cpp b/core/libs/prx/libSceCdlgUtilServer/Export.cpp index d81f4a5e32..a7fac71365 100644 --- a/core/libs/prx/libSceCdlgUtilServer/Export.cpp +++ b/core/libs/prx/libSceCdlgUtilServer/Export.cpp @@ -5,6 +5,59 @@ extern "C" { -APS5_DUMMY_FUN +int APS5_VABI _ZN3sce16CommonDialogUtil6Server13setFinishDataEPvm() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce16CommonDialogUtil6Server6setRunEPvm() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce16CommonDialogUtil6Server9setFinishEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce16CommonDialogUtil6Server9setResultEi() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce16CommonDialogUtil6ServerC2EPKcmmis() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce16CommonDialogUtil6ServerD0Ev() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce16CommonDialogUtil6ServerD1Ev() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce16CommonDialogUtil6ServerD2Ev() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce16CommonDialogUtil6Server10getVersionERj() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce16CommonDialogUtil6Server10isCloseReqEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce16CommonDialogUtil6Server14getServerStateEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/core/libs/prx/libSceGvMp4Parser/Export.cpp b/core/libs/prx/libSceGvMp4Parser/Export.cpp index d81f4a5e32..d1151c8ed6 100644 --- a/core/libs/prx/libSceGvMp4Parser/Export.cpp +++ b/core/libs/prx/libSceGvMp4Parser/Export.cpp @@ -5,6 +5,29 @@ extern "C" { -APS5_DUMMY_FUN +int APS5_VABI _ZN23sceMetadataReaderWriter11gvMp4Parser19gvMp4ParserFinalizeEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter11gvMp4Parser21gvMp4ParserInitializeEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter11gvMp4Parser23gvMp4ParserReadMetadataERKSsRKNS_8MetadataERS3_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter11gvMp4Parser24gvMp4ParserWriteMetadataERKSsRKNS_8MetadataE() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter11gvMp4Parser27gvMp4ParserExtractThumbnailERKSsRNS_9ThumbnailE() { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/core/libs/prx/libSceHidControl/Export.cpp b/core/libs/prx/libSceHidControl/Export.cpp index bb7329027a..e53100bbdf 100644 --- a/core/libs/prx/libSceHidControl/Export.cpp +++ b/core/libs/prx/libSceHidControl/Export.cpp @@ -6,8 +6,6 @@ extern "C" { -APS5_DUMMY_FUN - int APS5_VABI sceHidControlEnableMicBeamForming(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -208,4 +206,119 @@ int APS5_VABI sceHidControlGetPswDeviceIdFromIdxAdd(void) { return 0; } +int APS5_VABI sceHidControlConnectHandle() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlConnectPort() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlDisconnectDevice() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlDisconnectHandle() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlDisconnectPort() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlGetAuthInformation() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlGetBatteryState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlGetDeviceId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlGetDeviceInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlGetDeviceName() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlGetJediExtensionUnitInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlGetJediState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlGetSparkState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlGetVersionInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlSetApplicationFocus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlSetHandleState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlSetJediAudioOutPath() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlSetJediVolumeGain() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlSetProcessFocus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlSetRevokeList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlSetRevokeList2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlSetUserColor() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceJpegParser/Export.cpp b/core/libs/prx/libSceJpegParser/Export.cpp index d81f4a5e32..575a1af715 100644 --- a/core/libs/prx/libSceJpegParser/Export.cpp +++ b/core/libs/prx/libSceJpegParser/Export.cpp @@ -5,6 +5,29 @@ extern "C" { -APS5_DUMMY_FUN +int APS5_VABI _ZN23sceMetadataReaderWriter10jpegParser18jpegParserFinalizeEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter10jpegParser20jpegParserInitializeEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter10jpegParser22jpegParserReadMetadataERKSsRKNS_8MetadataERS3_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter10jpegParser23jpegParserWriteMetadataERKSsRKNS_8MetadataE() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter10jpegParser26jpegParserExtractThumbnailERKSsRNS_9ThumbnailE() { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/core/libs/prx/libSceMetadataReaderWriter/Export.cpp b/core/libs/prx/libSceMetadataReaderWriter/Export.cpp index d81f4a5e32..4a5ec0048e 100644 --- a/core/libs/prx/libSceMetadataReaderWriter/Export.cpp +++ b/core/libs/prx/libSceMetadataReaderWriter/Export.cpp @@ -5,6 +5,199 @@ extern "C" { -APS5_DUMMY_FUN +int APS5_VABI _ZN23sceMetadataReaderWriter10initializeEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter11getMimeTypeERKSsRSt6vectorIjSaIjEE() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter12getMimeTypesERKSsiPi() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter12readMetadataERKSsRKNS_8MetadataEjRS2_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter13writeMetadataERKSsRKNS_8MetadataEj() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter13writeMetadataERKSsRKNS_8MetadataEjPS2_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter15ParserInterfaceC1ERKS0_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter15ParserInterfaceC2ERKS0_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter16extractThumbnailERKSsjRNS_9ThumbnailE() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter16getNumOfMimeTypeERKSsPi() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter17getParserInfoListERSt6vectorINS_10ParserInfoESaIS1_EE() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter18registerParserInfoERKSt6vectorINS_10ParserInfoESaIS1_EE() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter20unregisterParserInfoEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter23registerParserInterfaceEjRKNS_15ParserInterfaceE() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter5ValueaSERS0_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter8Metadata11DeserializeERN3sce4Json5ValueERS0_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter8Metadata11deleteFieldERKSs() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter8Metadata15iterateAllFieldEPFiRKSsRKNS_5ValueEbE() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter8Metadata8getFieldEiRSsRNS_5ValueERb() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter8Metadata8setFieldERKSs() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter8Metadata8setFieldERKSsRKNS_5ValueE() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter8Metadata9SerializeERS0_RN3sce4Json5ValueE() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter8MetadataC1EPFPvmEPFvS1_E() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter8MetadataC1ERKS0_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter8MetadataC1ERKS0_PFPvmEPFvS3_E() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter8MetadataC1Ev() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter8MetadataC2EPFPvmEPFvS1_E() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter8MetadataC2ERKS0_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter8MetadataC2ERKS0_PFPvmEPFvS3_E() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter8MetadataC2Ev() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter8MetadataD0Ev() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter8MetadataD1Ev() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter8MetadataD2Ev() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter8MetadataaSERKS0_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter8finalizeEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK23sceMetadataReaderWriter8Metadata13getFieldCountEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK23sceMetadataReaderWriter8Metadata8getFieldEiRSsRNS_5ValueE() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK23sceMetadataReaderWriter8Metadata8getValueERKSsRNS_5ValueE() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK23sceMetadataReaderWriter8Metadata9dumpValueEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/core/libs/prx/libScePsm/Export.cpp b/core/libs/prx/libScePsm/Export.cpp index fb34206c21..1af3dc531d 100644 --- a/core/libs/prx/libScePsm/Export.cpp +++ b/core/libs/prx/libScePsm/Export.cpp @@ -5,6 +5,329 @@ extern "C" { -APS5_DUMMY_FUN +int APS5_VABI ScePsmMonoArrayAddrWithSize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ScePsmMonoArrayLength() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ScePsmMonoDomainGet() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ScePsmMonoFree() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ScePsmMonoGcWbarrierGenericStore() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ScePsmMonoStringChars() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ScePsmMonoStringFromUtf16() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ScePsmMonoStringLength() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ScePsmMonoStringNew() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ScePsmMonoStringNewWrapper() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ScePsmMonoStringToUtf8() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss4core14IntHandleTable13NextIntHandleEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss4core8graphics14NativeGraphics18ReleaseVideoMemoryEPv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss4core8graphics14NativeGraphics19AllocateVideoMemoryEjj() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss4core8graphics14NativeGraphics19AllocateVideoMemoryEjjPv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss4core8graphics14NativeGraphics19ReleaseSystemMemoryEPv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss4core8graphics14NativeGraphics20AllocateSystemMemoryEjj() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss4core8graphics14NativeGraphics20AllocateSystemMemoryEjjPv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss4core9PsmObjectC1Ev() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss4core9PsmObjectC2Ev() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss4core9PsmObjectD0Ev() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss4core9PsmObjectD1Ev() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss4core9PsmObjectD2Ev() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss4core9threading4Cond4WaitEi() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss4core9threading4Cond6NotifyEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss4core9threading4CondC1ERNS2_5MutexEPKci() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss4core9threading4CondC2ERNS2_5MutexEPKci() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss4core9threading4CondD1Ev() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss4core9threading4CondD2Ev() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework11PsmArrayObj6LengthEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework11PsmArrayObj9ToPointerEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework11PsmCallList10UnregisterEPFvPvES4_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework11PsmCallList6UpdateEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework11PsmCallList8RegisterEPFvPvES4_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework11PsmCallListC1Ev() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework11PsmCallListC2Ev() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework12PsmEventData8SetValueERKNS_4Json5ValueE() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework12PsmFramework10InitializeERKNS2_12PsmInitParamEiPPc() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework12PsmFramework20RegisterInternalCallEPKNS2_12PsmCallEntryE() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework12PsmFramework24RegisterPInvokeCallTableEPKNS2_15PsmLibraryEntryE() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework12PsmFramework3RunEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework12PsmFramework8FinalizeEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework12PsmInitParamC1Ev() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework12PsmInitParamC2Ev() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework12PsmStringObj17NewAndStoreStringEPvPc() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework12PsmStringObj6ToUtf8Ev() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework12PsmStringObj8FreeUtf8EPc() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework14PsmDelegateObj11FreePointerEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework14PsmDelegateObj15AllocatePointerEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework18PsmFrameworkCsharp21SetEnableNativeMethodEb() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework21PsmMainThreadCallList10UnregisterEPFvPvES4_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework21PsmMainThreadCallList6UpdateEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework21PsmMainThreadCallList8RegisterEPFvPvES4_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework23TerminatePsmEventCsharpEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework24InitializePsmEventCsharpEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework27TerminatePsmFrameworkCsharpEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework28InitializePsmFrameworkCsharpEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework8PsmEvent10UnregisterEiPFvRKNS2_12PsmEventDataEPvES7_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework8PsmEvent17RegisterDelegatorEPFvRKNS2_12PsmEventDataEPvES7_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework8PsmEvent19UnregisterDelegatorEPFvRKNS2_12PsmEventDataEPvES7_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework8PsmEvent4PostERKNS2_12PsmEventDataE() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework8PsmEvent4SendERKNS2_12PsmEventDataE() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework8PsmEvent8RegisterEiPFvRKNS2_12PsmEventDataEPvES7_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePsmRegisterInternalCall() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePsmRegisterPInvokeCallTable() { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/docs/dev/debt/libSceAppInstUtil.md b/docs/dev/debt/libSceAppInstUtil.md index 28ed171a11..78231a237c 100644 --- a/docs/dev/debt/libSceAppInstUtil.md +++ b/docs/dev/debt/libSceAppInstUtil.md @@ -2,6 +2,9 @@ ### Unknown function info +- [sceAppInstUtilAppConvertAppDiscToDownload, sceAppInstUtilAppGetCustomThemeInfo, sceAppInstUtilAppGetInstallAddcontFromDiscStatus, sceAppInstUtilAppInstallCloudGame, sceAppInstUtilAppInstallMediaPlayer](../../../core/libs/prx/libSceAppInstUtil/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAppInstUtil.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [sceAppInstUtilAppInstallRequestAddcontFromDisc, sceAppInstUtilAppSetSavedataUploadStatus](../../../core/libs/prx/libSceAppInstUtil/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAppInstUtil.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. + - [sceAppInstUtilAppInstallPkg](../../../core/libs/prx/libSceAppInstUtil/Export.cpp) - the package path is followed by an opaque 64-bit value sent to the install service; its meaning and pointer-versus-integer type remain unresolved. - [sceAppInstUtilGetInstallProgressInfo, sceAppInstUtilGetInstallProgress and sceAppInstUtilGetPrimaryAppSlot](../../../core/libs/prx/libSceAppInstUtil/Export.cpp) - take a title-id string and an output pointer. The IPC replies contain 48, 4 and 4 bytes respectively; callers and reply producers are needed to establish the field types. - [sceAppInstUtilAppPrepareOverwritePkg, sceAppInstUtilAppRecoverApp and sceAppInstUtilAppUnInstallTheme](../../../core/libs/prx/libSceAppInstUtil/Export.cpp) - absent from the supplied system module and from the supplied decompiled callers; the old export inventory establishes names but not prototypes. diff --git a/docs/dev/debt/libSceAvSetting.md b/docs/dev/debt/libSceAvSetting.md index 6bbd62abd0..56072cafbe 100644 --- a/docs/dev/debt/libSceAvSetting.md +++ b/docs/dev/debt/libSceAvSetting.md @@ -2,6 +2,28 @@ ### Unknown function info +- [sceAvControlChangeOutputLayout, sceAvControlChangeOutputMode, sceAvControlChangeOutputModeForDiag, sceAvControlChangeProcessAttribute, sceAvControlChangeScanInMode](../../../core/libs/prx/libSceAvSetting/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAvSetting.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [sceAvControlGetCurrentDeviceId, sceAvControlGetCurrentOutputMode, sceAvControlGetCurrentOutputModeWithSync, sceAvControlGetDetailHdcpStatus, sceAvControlGetDeviceInfo](../../../core/libs/prx/libSceAvSetting/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAvSetting.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [sceAvControlGetHdcpStatus, sceAvControlGetMonitorInfo, sceAvControlGetNativeMonitorInfo, sceAvControlGetPuStatus, sceAvControlGetPuType](../../../core/libs/prx/libSceAvSetting/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAvSetting.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [sceAvControlInit, sceAvControlIsAudioOutModeSupportedByMonitorInfo, sceAvControlIsColorimetryHdr, sceAvControlIsDeepColorSupportedByMonitorInfo, sceAvControlIsDeviceConnected](../../../core/libs/prx/libSceAvSetting/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAvSetting.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [sceAvControlIsHdrSupportedByMonitorInfo, sceAvControlIsModeGen1Vr, sceAvControlIsModeSystemDefault, sceAvControlIsSuspendedProcessOutputModeAvailable, sceAvControlIsVideoOutModeSupportedByMonitorInfo](../../../core/libs/prx/libSceAvSetting/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAvSetting.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [sceAvControlSet2dVrMode, sceAvControlSetAudioMuteForShutdown, sceAvControlSetAvOutputMode, sceAvControlSetColorEffect, sceAvControlSetDefaultAudioOutMode](../../../core/libs/prx/libSceAvSetting/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAvSetting.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [sceAvControlSetDefaultVideoOutMode, sceAvControlSetDispclk, sceAvControlSetGamutMetadata, sceAvControlSetHdcpEncryption, sceAvControlSetHdcpStatus](../../../core/libs/prx/libSceAvSetting/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAvSetting.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [sceAvControlSetHdmiGamutMetadata, sceAvControlSetHdrMetadata, sceAvControlSetVideoOutSource, sceAvControlSimulateProcessOutputModeArbitration, sceAvSettingAddCallbacks](../../../core/libs/prx/libSceAvSetting/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAvSetting.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [sceAvSettingAddCallbacksForLnc, sceAvSettingAddCallbacksForSocialScreen, sceAvSettingCallbackFuncsInit, sceAvSettingCallbackFuncsInitForLnc, sceAvSettingCallbackFuncsInitForLnc_](../../../core/libs/prx/libSceAvSetting/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAvSetting.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [sceAvSettingCallbackFuncsInit_, sceAvSettingChangeOutputMode, sceAvSettingChangeOutputMode2, sceAvSettingChangeOutputMode3, sceAvSettingChangeOutputModeForDiag](../../../core/libs/prx/libSceAvSetting/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAvSetting.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [sceAvSettingChangeProcessAttribute, sceAvSettingCheckCallback, sceAvSettingControlHdcpEncryption, sceAvSettingDebugAddCallbacks, sceAvSettingDebugClearDiagCommand](../../../core/libs/prx/libSceAvSetting/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAvSetting.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [sceAvSettingDebugSetDiagState, sceAvSettingDebugSetHdmiMonitorInfo, sceAvSettingDebugSetProcessAttribute, sceAvSettingDriverChangeConnectionStatus, sceAvSettingDriverUpdateStatus](../../../core/libs/prx/libSceAvSetting/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAvSetting.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [sceAvSettingEnterAudioMuteForShutdown, sceAvSettingGetCurrentDeviceInfo_, sceAvSettingGetCurrentHdmiDeviceId, sceAvSettingGetCurrentOutputMode2_, sceAvSettingGetCurrentOutputMode_](../../../core/libs/prx/libSceAvSetting/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAvSetting.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [sceAvSettingGetDetailedHdcpStatus, sceAvSettingGetDeviceInfo, sceAvSettingGetHdcpStatus, sceAvSettingGetHdmiConnectDisconnectNum, sceAvSettingGetHdmiKsvList](../../../core/libs/prx/libSceAvSetting/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAvSetting.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [sceAvSettingGetHdmiMonitorInfo, sceAvSettingGetHdmiRawEdid, sceAvSettingGetMorpheusPuStatus, sceAvSettingGetNativeHdmiMonitorInfo, sceAvSettingGetRawHdmiMonitorInfo](../../../core/libs/prx/libSceAvSetting/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAvSetting.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [sceAvSettingInit, sceAvSettingIsSupportedAudioOutModeByHdmiMonitorInfo, sceAvSettingIsSupportedHdcpVersionByHdmiMonitorInfo, sceAvSettingIsSupportedVideoOutModeByHdmiMonitorInfo, sceAvSettingIsSuspendedProcessOutputModeAvailable](../../../core/libs/prx/libSceAvSetting/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAvSetting.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [sceAvSettingIsSuspendedProcessOutputModeAvailable2, sceAvSettingIsVrSupportedByHdmiMonitorInfo2, sceAvSettingNotifyAudioOutMode, sceAvSettingNotifyResponseForSocialScreen, sceAvSettingNotifyUmdEvent](../../../core/libs/prx/libSceAvSetting/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAvSetting.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [sceAvSettingNotifyVideoOutMode, sceAvSettingRemoveCallbacks, sceAvSettingRemoveProcessOutputMode, sceAvSettingSet2dVrMode, sceAvSettingSetAudioOutModeAny_](../../../core/libs/prx/libSceAvSetting/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAvSetting.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [sceAvSettingSetAudioOutModeInvalid_, sceAvSettingSetAvOutputMode, sceAvSettingSetDispclk, sceAvSettingSetDisplayBlackLevel, sceAvSettingSetHdcpMode](../../../core/libs/prx/libSceAvSetting/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAvSetting.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [sceAvSettingSetHdcpStatus, sceAvSettingSetHdmiGamutMetadata_, sceAvSettingSetVideoOutModeAny_, sceAvSettingSetVideoOutModeInvalid_, sceAvSettingSetVideoOutSource](../../../core/libs/prx/libSceAvSetting/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAvSetting.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [sceAvSettingSetVideoOutputColorEffect, sceAvSettingSimulateProcessOutputModeArbitration_, sceAvSettingTerm, sceAvSettingWaitCallback](../../../core/libs/prx/libSceAvSetting/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAvSetting.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. + - [sceAvControlGetPuTypeByMonitorInfo](../../../core/libs/prx/libSceAvSetting/Export.cpp) (libSceAvSetting) - unknown signature - [sceAvControlIsVideoOutModeSupportedByMonitorInfoWithOptions](../../../core/libs/prx/libSceAvSetting/Export.cpp) (libSceAvSetting) - unknown signature - [sceAvControlSetDitherModeForDiag](../../../core/libs/prx/libSceAvSetting/Export.cpp) (libSceAvSetting) - unknown signature diff --git a/docs/dev/debt/libSceCdlgUtilServer.md b/docs/dev/debt/libSceCdlgUtilServer.md new file mode 100644 index 0000000000..c6a2a8990a --- /dev/null +++ b/docs/dev/debt/libSceCdlgUtilServer.md @@ -0,0 +1,7 @@ +# libSceCdlgUtilServer technical debt + +### Unknown function info + +- [_ZN3sce16CommonDialogUtil6Server13setFinishDataEPvm, _ZN3sce16CommonDialogUtil6Server6setRunEPvm, _ZN3sce16CommonDialogUtil6Server9setFinishEv, _ZN3sce16CommonDialogUtil6Server9setResultEi, _ZN3sce16CommonDialogUtil6ServerC2EPKcmmis](../../../core/libs/prx/libSceCdlgUtilServer/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCdlgUtilServer.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [_ZN3sce16CommonDialogUtil6ServerD0Ev, _ZN3sce16CommonDialogUtil6ServerD1Ev, _ZN3sce16CommonDialogUtil6ServerD2Ev, _ZNK3sce16CommonDialogUtil6Server10getVersionERj, _ZNK3sce16CommonDialogUtil6Server10isCloseReqEv](../../../core/libs/prx/libSceCdlgUtilServer/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCdlgUtilServer.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [_ZNK3sce16CommonDialogUtil6Server14getServerStateEv](../../../core/libs/prx/libSceCdlgUtilServer/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCdlgUtilServer.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. diff --git a/docs/dev/debt/libSceGvMp4Parser.md b/docs/dev/debt/libSceGvMp4Parser.md new file mode 100644 index 0000000000..bcd4ee8972 --- /dev/null +++ b/docs/dev/debt/libSceGvMp4Parser.md @@ -0,0 +1,5 @@ +# libSceGvMp4Parser technical debt + +### Unknown function info + +- [_ZN23sceMetadataReaderWriter11gvMp4Parser19gvMp4ParserFinalizeEv, _ZN23sceMetadataReaderWriter11gvMp4Parser21gvMp4ParserInitializeEv, _ZN23sceMetadataReaderWriter11gvMp4Parser23gvMp4ParserReadMetadataERKSsRKNS_8MetadataERS3_, _ZN23sceMetadataReaderWriter11gvMp4Parser24gvMp4ParserWriteMetadataERKSsRKNS_8MetadataE, _ZN23sceMetadataReaderWriter11gvMp4Parser27gvMp4ParserExtractThumbnailERKSsRNS_9ThumbnailE](../../../core/libs/prx/libSceGvMp4Parser/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceGvMp4Parser.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. diff --git a/docs/dev/debt/libSceHidControl.md b/docs/dev/debt/libSceHidControl.md index fc779520a0..d7737423de 100644 --- a/docs/dev/debt/libSceHidControl.md +++ b/docs/dev/debt/libSceHidControl.md @@ -2,6 +2,12 @@ ### Unknown function info +- [sceHidControlConnectHandle, sceHidControlConnectPort, sceHidControlDisconnectDevice, sceHidControlDisconnectHandle, sceHidControlDisconnectPort](../../../core/libs/prx/libSceHidControl/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceHidControl.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [sceHidControlGetAuthInformation, sceHidControlGetBatteryState, sceHidControlGetDeviceId, sceHidControlGetDeviceInfo, sceHidControlGetDeviceName](../../../core/libs/prx/libSceHidControl/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceHidControl.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [sceHidControlGetJediExtensionUnitInfo, sceHidControlGetJediState, sceHidControlGetSparkState, sceHidControlGetVersionInfo, sceHidControlInit](../../../core/libs/prx/libSceHidControl/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceHidControl.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [sceHidControlSetApplicationFocus, sceHidControlSetHandleState, sceHidControlSetJediAudioOutPath, sceHidControlSetJediVolumeGain, sceHidControlSetProcessFocus](../../../core/libs/prx/libSceHidControl/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceHidControl.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [sceHidControlSetRevokeList, sceHidControlSetRevokeList2, sceHidControlSetUserColor](../../../core/libs/prx/libSceHidControl/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceHidControl.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. + - [sceHidControlEnableMicBeamForming](../../../core/libs/prx/libSceHidControl/Export.cpp) (libSceHidControl) - unknown signature - [sceHidControlSetAllVibrationMute](../../../core/libs/prx/libSceHidControl/Export.cpp) (libSceHidControl) - unknown signature - [sceHidControlSetVibrationTriggerEffectWeakForce](../../../core/libs/prx/libSceHidControl/Export.cpp) (libSceHidControl) - unknown signature diff --git a/docs/dev/debt/libSceJpegParser.md b/docs/dev/debt/libSceJpegParser.md new file mode 100644 index 0000000000..8fcd0951c2 --- /dev/null +++ b/docs/dev/debt/libSceJpegParser.md @@ -0,0 +1,5 @@ +# libSceJpegParser technical debt + +### Unknown function info + +- [_ZN23sceMetadataReaderWriter10jpegParser18jpegParserFinalizeEv, _ZN23sceMetadataReaderWriter10jpegParser20jpegParserInitializeEv, _ZN23sceMetadataReaderWriter10jpegParser22jpegParserReadMetadataERKSsRKNS_8MetadataERS3_, _ZN23sceMetadataReaderWriter10jpegParser23jpegParserWriteMetadataERKSsRKNS_8MetadataE, _ZN23sceMetadataReaderWriter10jpegParser26jpegParserExtractThumbnailERKSsRNS_9ThumbnailE](../../../core/libs/prx/libSceJpegParser/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceJpegParser.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. diff --git a/docs/dev/debt/libSceMetadataReaderWriter.md b/docs/dev/debt/libSceMetadataReaderWriter.md new file mode 100644 index 0000000000..8ed76ec204 --- /dev/null +++ b/docs/dev/debt/libSceMetadataReaderWriter.md @@ -0,0 +1,12 @@ +# libSceMetadataReaderWriter technical debt + +### Unknown function info + +- [_ZN23sceMetadataReaderWriter10initializeEv, _ZN23sceMetadataReaderWriter11getMimeTypeERKSsRSt6vectorIjSaIjEE, _ZN23sceMetadataReaderWriter12getMimeTypesERKSsiPi, _ZN23sceMetadataReaderWriter12readMetadataERKSsRKNS_8MetadataEjRS2_, _ZN23sceMetadataReaderWriter13writeMetadataERKSsRKNS_8MetadataEj](../../../core/libs/prx/libSceMetadataReaderWriter/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceMetadataReaderWriter.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [_ZN23sceMetadataReaderWriter13writeMetadataERKSsRKNS_8MetadataEjPS2_, _ZN23sceMetadataReaderWriter15ParserInterfaceC1ERKS0_, _ZN23sceMetadataReaderWriter15ParserInterfaceC2ERKS0_, _ZN23sceMetadataReaderWriter16extractThumbnailERKSsjRNS_9ThumbnailE, _ZN23sceMetadataReaderWriter16getNumOfMimeTypeERKSsPi](../../../core/libs/prx/libSceMetadataReaderWriter/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceMetadataReaderWriter.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [_ZN23sceMetadataReaderWriter17getParserInfoListERSt6vectorINS_10ParserInfoESaIS1_EE, _ZN23sceMetadataReaderWriter18registerParserInfoERKSt6vectorINS_10ParserInfoESaIS1_EE, _ZN23sceMetadataReaderWriter20unregisterParserInfoEv, _ZN23sceMetadataReaderWriter23registerParserInterfaceEjRKNS_15ParserInterfaceE, _ZN23sceMetadataReaderWriter5ValueaSERS0_](../../../core/libs/prx/libSceMetadataReaderWriter/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceMetadataReaderWriter.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [_ZN23sceMetadataReaderWriter8Metadata11DeserializeERN3sce4Json5ValueERS0_, _ZN23sceMetadataReaderWriter8Metadata11deleteFieldERKSs, _ZN23sceMetadataReaderWriter8Metadata15iterateAllFieldEPFiRKSsRKNS_5ValueEbE, _ZN23sceMetadataReaderWriter8Metadata8getFieldEiRSsRNS_5ValueERb, _ZN23sceMetadataReaderWriter8Metadata8setFieldERKSs](../../../core/libs/prx/libSceMetadataReaderWriter/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceMetadataReaderWriter.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [_ZN23sceMetadataReaderWriter8Metadata8setFieldERKSsRKNS_5ValueE, _ZN23sceMetadataReaderWriter8Metadata9SerializeERS0_RN3sce4Json5ValueE, _ZN23sceMetadataReaderWriter8MetadataC1EPFPvmEPFvS1_E, _ZN23sceMetadataReaderWriter8MetadataC1ERKS0_, _ZN23sceMetadataReaderWriter8MetadataC1ERKS0_PFPvmEPFvS3_E](../../../core/libs/prx/libSceMetadataReaderWriter/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceMetadataReaderWriter.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [_ZN23sceMetadataReaderWriter8MetadataC1Ev, _ZN23sceMetadataReaderWriter8MetadataC2EPFPvmEPFvS1_E, _ZN23sceMetadataReaderWriter8MetadataC2ERKS0_, _ZN23sceMetadataReaderWriter8MetadataC2ERKS0_PFPvmEPFvS3_E, _ZN23sceMetadataReaderWriter8MetadataC2Ev](../../../core/libs/prx/libSceMetadataReaderWriter/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceMetadataReaderWriter.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [_ZN23sceMetadataReaderWriter8MetadataD0Ev, _ZN23sceMetadataReaderWriter8MetadataD1Ev, _ZN23sceMetadataReaderWriter8MetadataD2Ev, _ZN23sceMetadataReaderWriter8MetadataaSERKS0_, _ZN23sceMetadataReaderWriter8finalizeEv](../../../core/libs/prx/libSceMetadataReaderWriter/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceMetadataReaderWriter.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [_ZNK23sceMetadataReaderWriter8Metadata13getFieldCountEv, _ZNK23sceMetadataReaderWriter8Metadata8getFieldEiRSsRNS_5ValueE, _ZNK23sceMetadataReaderWriter8Metadata8getValueERKSsRNS_5ValueE, _ZNK23sceMetadataReaderWriter8Metadata9dumpValueEv](../../../core/libs/prx/libSceMetadataReaderWriter/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceMetadataReaderWriter.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. diff --git a/docs/dev/debt/libScePsm.md b/docs/dev/debt/libScePsm.md new file mode 100644 index 0000000000..f0c83d9eae --- /dev/null +++ b/docs/dev/debt/libScePsm.md @@ -0,0 +1,17 @@ +# libScePsm technical debt + +### Unknown function info + +- [ScePsmMonoArrayAddrWithSize, ScePsmMonoArrayLength, ScePsmMonoDomainGet, ScePsmMonoFree, ScePsmMonoGcWbarrierGenericStore](../../../core/libs/prx/libScePsm/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libScePsm.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [ScePsmMonoStringChars, ScePsmMonoStringFromUtf16, ScePsmMonoStringLength, ScePsmMonoStringNew, ScePsmMonoStringNewWrapper](../../../core/libs/prx/libScePsm/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libScePsm.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [ScePsmMonoStringToUtf8, _ZN3sce3pss4core14IntHandleTable13NextIntHandleEv, _ZN3sce3pss4core8graphics14NativeGraphics18ReleaseVideoMemoryEPv, _ZN3sce3pss4core8graphics14NativeGraphics19AllocateVideoMemoryEjj, _ZN3sce3pss4core8graphics14NativeGraphics19AllocateVideoMemoryEjjPv](../../../core/libs/prx/libScePsm/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libScePsm.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [_ZN3sce3pss4core8graphics14NativeGraphics19ReleaseSystemMemoryEPv, _ZN3sce3pss4core8graphics14NativeGraphics20AllocateSystemMemoryEjj, _ZN3sce3pss4core8graphics14NativeGraphics20AllocateSystemMemoryEjjPv, _ZN3sce3pss4core9PsmObjectC1Ev, _ZN3sce3pss4core9PsmObjectC2Ev](../../../core/libs/prx/libScePsm/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libScePsm.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [_ZN3sce3pss4core9PsmObjectD0Ev, _ZN3sce3pss4core9PsmObjectD1Ev, _ZN3sce3pss4core9PsmObjectD2Ev, _ZN3sce3pss4core9threading4Cond4WaitEi, _ZN3sce3pss4core9threading4Cond6NotifyEv](../../../core/libs/prx/libScePsm/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libScePsm.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [_ZN3sce3pss4core9threading4CondC1ERNS2_5MutexEPKci, _ZN3sce3pss4core9threading4CondC2ERNS2_5MutexEPKci, _ZN3sce3pss4core9threading4CondD1Ev, _ZN3sce3pss4core9threading4CondD2Ev, _ZN3sce3pss5orbis9framework11PsmArrayObj6LengthEv](../../../core/libs/prx/libScePsm/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libScePsm.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [_ZN3sce3pss5orbis9framework11PsmArrayObj9ToPointerEv, _ZN3sce3pss5orbis9framework11PsmCallList10UnregisterEPFvPvES4_, _ZN3sce3pss5orbis9framework11PsmCallList6UpdateEv, _ZN3sce3pss5orbis9framework11PsmCallList8RegisterEPFvPvES4_, _ZN3sce3pss5orbis9framework11PsmCallListC1Ev](../../../core/libs/prx/libScePsm/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libScePsm.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [_ZN3sce3pss5orbis9framework11PsmCallListC2Ev, _ZN3sce3pss5orbis9framework12PsmEventData8SetValueERKNS_4Json5ValueE, _ZN3sce3pss5orbis9framework12PsmFramework10InitializeERKNS2_12PsmInitParamEiPPc, _ZN3sce3pss5orbis9framework12PsmFramework20RegisterInternalCallEPKNS2_12PsmCallEntryE, _ZN3sce3pss5orbis9framework12PsmFramework24RegisterPInvokeCallTableEPKNS2_15PsmLibraryEntryE](../../../core/libs/prx/libScePsm/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libScePsm.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [_ZN3sce3pss5orbis9framework12PsmFramework3RunEv, _ZN3sce3pss5orbis9framework12PsmFramework8FinalizeEv, _ZN3sce3pss5orbis9framework12PsmInitParamC1Ev, _ZN3sce3pss5orbis9framework12PsmInitParamC2Ev, _ZN3sce3pss5orbis9framework12PsmStringObj17NewAndStoreStringEPvPc](../../../core/libs/prx/libScePsm/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libScePsm.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [_ZN3sce3pss5orbis9framework12PsmStringObj6ToUtf8Ev, _ZN3sce3pss5orbis9framework12PsmStringObj8FreeUtf8EPc, _ZN3sce3pss5orbis9framework14PsmDelegateObj11FreePointerEv, _ZN3sce3pss5orbis9framework14PsmDelegateObj15AllocatePointerEv, _ZN3sce3pss5orbis9framework18PsmFrameworkCsharp21SetEnableNativeMethodEb](../../../core/libs/prx/libScePsm/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libScePsm.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [_ZN3sce3pss5orbis9framework21PsmMainThreadCallList10UnregisterEPFvPvES4_, _ZN3sce3pss5orbis9framework21PsmMainThreadCallList6UpdateEv, _ZN3sce3pss5orbis9framework21PsmMainThreadCallList8RegisterEPFvPvES4_, _ZN3sce3pss5orbis9framework23TerminatePsmEventCsharpEv, _ZN3sce3pss5orbis9framework24InitializePsmEventCsharpEv](../../../core/libs/prx/libScePsm/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libScePsm.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [_ZN3sce3pss5orbis9framework27TerminatePsmFrameworkCsharpEv, _ZN3sce3pss5orbis9framework28InitializePsmFrameworkCsharpEv, _ZN3sce3pss5orbis9framework8PsmEvent10UnregisterEiPFvRKNS2_12PsmEventDataEPvES7_, _ZN3sce3pss5orbis9framework8PsmEvent17RegisterDelegatorEPFvRKNS2_12PsmEventDataEPvES7_, _ZN3sce3pss5orbis9framework8PsmEvent19UnregisterDelegatorEPFvRKNS2_12PsmEventDataEPvES7_](../../../core/libs/prx/libScePsm/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libScePsm.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [_ZN3sce3pss5orbis9framework8PsmEvent4PostERKNS2_12PsmEventDataE, _ZN3sce3pss5orbis9framework8PsmEvent4SendERKNS2_12PsmEventDataE, _ZN3sce3pss5orbis9framework8PsmEvent8RegisterEiPFvRKNS2_12PsmEventDataEPvES7_, scePsmRegisterInternalCall, scePsmRegisterPInvokeCallTable](../../../core/libs/prx/libScePsm/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libScePsm.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. From 0c07dff24b29cf3cc5fa9a44f3d049ed3f77d6df Mon Sep 17 00:00:00 2001 From: SnekPolylepis <100386333+SnekPolylepis@users.noreply.github.com> Date: Thu, 22 Oct 2026 12:44:28 -0500 Subject: [PATCH 152/198] fix(prx): exclude installer exports shared with pending PRs --- core/libs/prx/libSceAppInstUtil/Export.cpp | 35 ---------------------- docs/dev/debt/libSceAppInstUtil.md | 3 -- 2 files changed, 38 deletions(-) diff --git a/core/libs/prx/libSceAppInstUtil/Export.cpp b/core/libs/prx/libSceAppInstUtil/Export.cpp index e5481ca706..51721067a8 100644 --- a/core/libs/prx/libSceAppInstUtil/Export.cpp +++ b/core/libs/prx/libSceAppInstUtil/Export.cpp @@ -573,39 +573,4 @@ int APS5_VABI sceAppInstUtilInstallAddons(void) { return 0; } -int APS5_VABI sceAppInstUtilAppConvertAppDiscToDownload(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAppInstUtilAppGetCustomThemeInfo(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAppInstUtilAppGetInstallAddcontFromDiscStatus(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAppInstUtilAppInstallCloudGame(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAppInstUtilAppInstallMediaPlayer(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAppInstUtilAppInstallRequestAddcontFromDisc(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAppInstUtilAppSetSavedataUploadStatus(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - } diff --git a/docs/dev/debt/libSceAppInstUtil.md b/docs/dev/debt/libSceAppInstUtil.md index 78231a237c..28ed171a11 100644 --- a/docs/dev/debt/libSceAppInstUtil.md +++ b/docs/dev/debt/libSceAppInstUtil.md @@ -2,9 +2,6 @@ ### Unknown function info -- [sceAppInstUtilAppConvertAppDiscToDownload, sceAppInstUtilAppGetCustomThemeInfo, sceAppInstUtilAppGetInstallAddcontFromDiscStatus, sceAppInstUtilAppInstallCloudGame, sceAppInstUtilAppInstallMediaPlayer](../../../core/libs/prx/libSceAppInstUtil/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAppInstUtil.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. -- [sceAppInstUtilAppInstallRequestAddcontFromDisc, sceAppInstUtilAppSetSavedataUploadStatus](../../../core/libs/prx/libSceAppInstUtil/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAppInstUtil.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. - - [sceAppInstUtilAppInstallPkg](../../../core/libs/prx/libSceAppInstUtil/Export.cpp) - the package path is followed by an opaque 64-bit value sent to the install service; its meaning and pointer-versus-integer type remain unresolved. - [sceAppInstUtilGetInstallProgressInfo, sceAppInstUtilGetInstallProgress and sceAppInstUtilGetPrimaryAppSlot](../../../core/libs/prx/libSceAppInstUtil/Export.cpp) - take a title-id string and an output pointer. The IPC replies contain 48, 4 and 4 bytes respectively; callers and reply producers are needed to establish the field types. - [sceAppInstUtilAppPrepareOverwritePkg, sceAppInstUtilAppRecoverApp and sceAppInstUtilAppUnInstallTheme](../../../core/libs/prx/libSceAppInstUtil/Export.cpp) - absent from the supplied system module and from the supplied decompiled callers; the old export inventory establishes names but not prototypes. From 7b355abdb9ad3b6106240d28a42e2aff900ea8e2 Mon Sep 17 00:00:00 2001 From: orezraey Date: Mon, 26 Oct 2026 19:10:59 -0300 Subject: [PATCH 153/198] docs(libkernel): shorten the socket option debt entries Co-Authored-By: Claude Opus 5.5 --- docs/dev/debt/libkernel.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 593c8889da..a5d2cb0928 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -247,12 +247,12 @@ - [Socket error numbers](../../../core/libs/prx/libkernel/Socket/include/SocketError.hpp) (libkernel) - a host socket error becomes the FreeBSD 11 `errno.h` number of the same name; the numbers are not checked on the console. Winsock `WSAESHUTDOWN` becomes `EPIPE`, as a send after `shutdown` does on FreeBSD, and a host error with no FreeBSD socket counterpart becomes `EIO`. On Windows a refused nonblocking `connect` is never reported (`SO_ERROR` is unsupported), so a repeated one keeps reporting `EALREADY` where FreeBSD reports `ECONNREFUSED`. A receive after `shutdown(SHUT_RD)` reports `EPIPE` on Windows where FreeBSD returns 0 - [getsockopt, setsockopt](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - a guest socket has the SOL_SOCKET level, eight `IPPROTO_TCP` options, `IPV6_V6ONLY` and ten `IPPROTO_IP` options. `SO_ACCEPTFILTER` (0x1000) fails with `EINVAL` for a set and a get on a socket that is not listening, as a payload process on a PS5 (firmware 13.60) did and as FreeBSD 11.4's `do_setopt_accept_filter` and `do_getopt_accept_filter` do; on a listening socket both throw, as accept filters are not modelled; the other options of these levels fail with `ENOPROTOOPT` and were not all measured. `SO_KEEPALIVE`, `SO_DONTROUTE`, `SO_BROADCAST`, `SO_OOBINLINE` and `SO_LINGER` act on the host socket, and a boolean one reads back as its own bit, as there. Winsock does not clear a pending error when `SO_ERROR` reads it, so on Windows a pending error is reported once and again only after a new `connect`; a second failure with the same code without a `connect` in between is hidden - [TCP_NOPUSH, TCP_NOOPT, TCP option 0x80, IP_RECVDSTADDR, IP_RECVIF, IP_RECVTTL](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept and read back as a payload process on a PS5 (firmware 13.60) did: the TCP ones as the bits 0x1000, 0x8 and 0x10000 (so 0x80 is not FreeBSD's `TCP_KEEPINIT` there), the IP ones as 0 or 1. None of them changes what the socket does: `TCP_NOPUSH` does not hold back sends, and the IP ones add no control message to `recvmsg`. `IP_RECVTTL` was only read, never set, on the console, and an accepted socket takes `TCP_NOPUSH` from its listener, as FreeBSD 11.4's `syncache_socket` does and as measured on that console -- [SO_DEBUG, SO_REUSEADDR, SO_USELOOPBACK, SO_REUSEPORT, SO_TIMESTAMP, SO_BINTIME and option 0x10000](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept as bits that read back as on a PS5 (firmware 13.60, payload process), and options 0x4000 and 0x8000 are accepted and cannot be read, as there; only `SO_REUSEADDR`, `SO_REUSEPORT`, `SO_TIMESTAMP` and `SO_BINTIME` change what the socket does. `SO_REUSEADDR` and `SO_REUSEPORT` are set on the host, `SO_REUSEPORT` as `SO_REUSEADDR` on Windows, which has no `SO_REUSEPORT` and there also lets the socket bind over one that did not set it; Linux spreads unicast datagrams across the sockets, where FreeBSD gives each to one. Option 0x10000 is only kept: if it is FreeBSD 12's `SO_REUSEPORT_LB`, two sockets that set it share a port there, where the second `bind` here fails with `EADDRINUSE`. An accepted socket takes the bits from its listener, as FreeBSD 11.4's `sonewconn` copies `so_options`; measured only for `SO_REUSEADDR` and 0x10000 +- [SO_DEBUG, SO_REUSEADDR, SO_USELOOPBACK, SO_REUSEPORT, SO_TIMESTAMP, SO_BINTIME and option 0x10000](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept as bits that read back as on a PS5 (firmware 13.60, payload process), and options 0x4000 and 0x8000 are accepted and cannot be read, as there; only `SO_REUSEADDR`, `SO_REUSEPORT`, `SO_TIMESTAMP` and `SO_BINTIME` change what the socket does. `SO_REUSEADDR` and `SO_REUSEPORT` are set on the host, `SO_REUSEPORT` as `SO_REUSEADDR` on Windows, which has no `SO_REUSEPORT` and there also lets the socket bind over one that did not set it; Linux spreads unicast datagrams across the sockets, where FreeBSD gives each to one. Option 0x10000 is only kept: if it is FreeBSD 12's `SO_REUSEPORT_LB`, two sockets that set it share a port there, where the second `bind` here fails with `EADDRINUSE`. An accepted socket takes the bits from its listener (FreeBSD 11.4's `sonewconn`); measured only for `SO_REUSEADDR` and 0x10000 - [IP_ADD_MEMBERSHIP, IP_DROP_MEMBERSHIP, IP_MULTICAST_IF](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - set on the host socket after the checks a PS5 (firmware 13.60, payload process) showed: `EOPNOTSUPP` on a stream socket, `EINVAL` for a short value or a group outside 224.0.0.0/4, `EADDRINUSE` for a group joined twice and `EADDRNOTAVAIL` for one not joined, tracked per socket. An interface the host lacks fails with the host's error (`EADDRNOTAVAIL` on the console). A group joined through `INADDR_ANY` and through that interface's address counts as two here. Not measured, after FreeBSD 11.4: a drop through `INADDR_ANY` leaves the first such group on any interface (`inp_leave_group`), and the other multicast options fail with `EOPNOTSUPP` on a stream socket (`inp_setmoptions`). An `ip_mreqn` with a non-zero index or address throws, as only a zeroed one was measured, and so does reading `IP_MULTICAST_IF` into 12 bytes, where FreeBSD returns an `ip_mreqn` - [TCP_MAXSEG](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - a connected socket reads 536 (1220 for `AF_INET6`) until a segment is set, where a payload process on PS5 firmware 13.60 read the negotiated segment (16344 over loopback, lo0's MTU less 40) on both ends; the segment is not set on the host socket, and a listening socket was not measured. Otherwise it follows that console: a fresh socket reads 536, or 1220 for `AF_INET6`, and fails every set with `EINVAL` (175, 176, 200, 400, 536, 537, 1460 tried); a connected one fails a segment above the current one or below 176, and stores the rest raised to 216 (176 and 200 both read back 216), as FreeBSD 11.4's `tcp_default_ctloutput` with `V_tcp_minmss` does - [TCP_KEEPIDLE, TCP_KEEPINTVL, TCP_KEEPCNT](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept per stream socket and read back as a payload process on a PS5 (firmware 13.60) did: 7200, 75 and 8 at first, 0 restoring a keepalive default, and a count stored as an unsigned value. They are not set on the host socket, so the host's keepalive timers are its own while `SO_KEEPALIVE` is applied there. An interval above 4294967 seconds fails with `EINVAL`, after FreeBSD 11.4's `tcp_default_ctloutput` with `hz` taken as 1000, and an accepted socket takes the keepalive values from its listener as `syncache_socket` copies them; neither was measured - [IP_TTL, IP_TOS, IP_MULTICAST_TTL, IP_MULTICAST_LOOP](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept per `AF_INET` socket with the defaults (64, 0, 1, 1), the low-byte storage of `IP_TTL` and `IP_TOS`, the one-byte forms and the `EINVAL` above 255 of the multicast options that a payload process on a PS5 (firmware 13.60) showed. `IP_TTL` and the multicast options are also set on the host socket, except a time to live of 0, which hosts refuse; `IP_TOS` is only kept, so the host's packets carry its own type of service, and the host's default time to live (128 on Windows) is not changed to 64. An accepted socket takes `IP_TTL` and `IP_TOS` from its listener, as FreeBSD's `syncache_socket` does, and an `int` `IP_MULTICAST_LOOP` keeps only its low byte, as FreeBSD 11.4's `inp_setmoptions` does; neither was measured. Winsock applies `IP_MULTICAST_LOOP` on the receiving socket, not the sender, so on Windows the receiver's setting decides loopback -- [SO_SNDBUF, SO_RCVBUF, SO_SNDLOWAT, SO_RCVLOWAT](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept per socket with the defaults, the 524288-byte limit, the `ENOBUFS` above 5592405 bytes and the low-water clipping of a payload process on a PS5 (firmware 13.60); the host socket gets the size asked for, so a request above the limit still enlarges the host's buffer. A connected TCP socket keeps the sizes asked for, where that console rounds a buffer larger than the segment up to a multiple of it, as FreeBSD's `tcp_mss` does (a listener's 100000 and 50000 read 114408 and 65376 on the accepted socket, and the client's defaults 49032 and 81720, with a 16344-byte segment). The host's real buffering, FreeBSD's automatic growth of those buffers, and a low-water mark's effect on `select`, `poll` and a receive are not reproduced. An accepted socket takes all four from its listener, as FreeBSD's [`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L603) does and as measured for the low-water marks +- [SO_SNDBUF, SO_RCVBUF, SO_SNDLOWAT, SO_RCVLOWAT](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept per socket with the defaults, the 524288-byte limit, the `ENOBUFS` above 5592405 bytes and the low-water clipping of a payload process on a PS5 (firmware 13.60); the host socket gets the size asked for, even above the limit. A connected TCP socket keeps the sizes asked for, where that console rounds a buffer larger than the segment up to a multiple of it, as FreeBSD's `tcp_mss` does (a listener's 100000 and 50000 read 114408 and 65376 on the accepted socket, and the client's defaults 49032 and 81720, with a 16344-byte segment). The host's real buffering, FreeBSD's automatic growth of those buffers, and a low-water mark's effect on `select`, `poll` and a receive are not reproduced. An accepted socket takes all four from its listener, as FreeBSD's `sonewconn` does and as measured for the low-water marks - [SO_NOSIGPIPE](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept per socket and read back as its bit. On Linux a `send`, `sendto`, `sendmsg` or `write` on that socket passes `MSG_NOSIGNAL` to the host, as [`kern_sendit`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_syscalls.c#L890-L895) and [`soo_write`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_socket.c#L147-L151) leave out the signal, and an accepted socket takes it from its listener, as [`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L603) copies the options and as measured on PS5 firmware 13.60. Whether a write on it raises `SIGPIPE` there was not measured - [SO_SNDTIMEO, SO_RCVTIMEO](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - the value checks and the 32.32 fixed-point read-back follow [`sosetopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L2657-L2689), as a payload process on a PS5 (firmware 13.60) answered them. A receive or send on a blocking socket waits that long for the host to report the socket readable or writable and fails with `EWOULDBLOCK`, but only before the first transfer: once writable the host send has no limit, where FreeBSD returns the bytes sent so far, and a receive whose data another thread takes after the wait blocks again. `sendfile` ignores the send timeout, the host wait has millisecond steps, and an accepted socket takes both from its listener - [IPV6_V6ONLY](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - the `IPPROTO_IPV6` option 27 is set on the host socket and reads back as 0 or 1, after FreeBSD's [`ip6_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet6/ip6_output.c#L1716-L1732): a value that is not exactly an `int` fails with `EINVAL`, as does setting it on a bound socket, which the host refuses too. `socket` turns it on for every `AF_INET6` socket, as a payload process on a PS5 (firmware 13.60) reported, and Java turns it off on the sockets it opens: Java 25 Advanced Network (PPSA99871) stops at `java.net.SocketException: Protocol not available` without it. The value checks and the bound-socket answer were not measured From 83cf615c4b7ac617d0caca6a650352141e948028 Mon Sep 17 00:00:00 2001 From: SnekPolylepis <100386333+SnekPolylepis@users.noreply.github.com> Date: Mon, 26 Oct 2026 17:14:56 -0500 Subject: [PATCH 154/198] feat(prx): declare the 252 new exports in their library headers Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAvSetting/include/Api.hpp | 104 ++++++++++++++++++ core/libs/prx/libSceCdlgUtilServer/Export.cpp | 1 + .../prx/libSceCdlgUtilServer/include/Api.hpp | 19 ++++ .../libSceCdlgUtilServer.hpp | 2 + core/libs/prx/libSceGvMp4Parser/Export.cpp | 1 + .../prx/libSceGvMp4Parser/include/Api.hpp | 13 +++ .../libSceGvMp4Parser/libSceGvMp4Parser.hpp | 2 + .../libs/prx/libSceHidControl/include/Api.hpp | 23 ++++ core/libs/prx/libSceJpegParser/Export.cpp | 1 + .../libs/prx/libSceJpegParser/include/Api.hpp | 13 +++ .../prx/libSceJpegParser/libSceJpegParser.hpp | 2 + .../prx/libSceMetadataReaderWriter/Export.cpp | 1 + .../include/Api.hpp | 47 ++++++++ .../libSceMetadataReaderWriter.hpp | 2 + core/libs/prx/libScePsm/Export.cpp | 1 + core/libs/prx/libScePsm/include/Api.hpp | 73 ++++++++++++ core/libs/prx/libScePsm/libScePsm.hpp | 2 + 17 files changed, 307 insertions(+) create mode 100644 core/libs/prx/libSceCdlgUtilServer/include/Api.hpp create mode 100644 core/libs/prx/libSceGvMp4Parser/include/Api.hpp create mode 100644 core/libs/prx/libSceJpegParser/include/Api.hpp create mode 100644 core/libs/prx/libSceMetadataReaderWriter/include/Api.hpp create mode 100644 core/libs/prx/libScePsm/include/Api.hpp diff --git a/core/libs/prx/libSceAvSetting/include/Api.hpp b/core/libs/prx/libSceAvSetting/include/Api.hpp index 6ce470cc12..d038e47803 100644 --- a/core/libs/prx/libSceAvSetting/include/Api.hpp +++ b/core/libs/prx/libSceAvSetting/include/Api.hpp @@ -28,5 +28,109 @@ int APS5_VABI sceAvControlCheckVideoOutMode(void); int APS5_VABI sceAvControlSetCheckoutputMode(void); int APS5_VABI sceAvControlSetSystemVideoMode(void); int APS5_VABI sceAvControlDebugAddCallbacks(void); +int APS5_VABI sceAvControlChangeOutputLayout(void); +int APS5_VABI sceAvControlChangeOutputMode(void); +int APS5_VABI sceAvControlChangeOutputModeForDiag(void); +int APS5_VABI sceAvControlChangeProcessAttribute(void); +int APS5_VABI sceAvControlChangeScanInMode(void); +int APS5_VABI sceAvControlGetCurrentDeviceId(void); +int APS5_VABI sceAvControlGetCurrentOutputMode(void); +int APS5_VABI sceAvControlGetCurrentOutputModeWithSync(void); +int APS5_VABI sceAvControlGetDetailHdcpStatus(void); +int APS5_VABI sceAvControlGetDeviceInfo(void); +int APS5_VABI sceAvControlGetHdcpStatus(void); +int APS5_VABI sceAvControlGetMonitorInfo(void); +int APS5_VABI sceAvControlGetNativeMonitorInfo(void); +int APS5_VABI sceAvControlGetPuStatus(void); +int APS5_VABI sceAvControlGetPuType(void); +int APS5_VABI sceAvControlInit(void); +int APS5_VABI sceAvControlIsAudioOutModeSupportedByMonitorInfo(void); +int APS5_VABI sceAvControlIsColorimetryHdr(void); +int APS5_VABI sceAvControlIsDeepColorSupportedByMonitorInfo(void); +int APS5_VABI sceAvControlIsDeviceConnected(void); +int APS5_VABI sceAvControlIsHdrSupportedByMonitorInfo(void); +int APS5_VABI sceAvControlIsModeGen1Vr(void); +int APS5_VABI sceAvControlIsModeSystemDefault(void); +int APS5_VABI sceAvControlIsSuspendedProcessOutputModeAvailable(void); +int APS5_VABI sceAvControlIsVideoOutModeSupportedByMonitorInfo(void); +int APS5_VABI sceAvControlSet2dVrMode(void); +int APS5_VABI sceAvControlSetAudioMuteForShutdown(void); +int APS5_VABI sceAvControlSetAvOutputMode(void); +int APS5_VABI sceAvControlSetColorEffect(void); +int APS5_VABI sceAvControlSetDefaultAudioOutMode(void); +int APS5_VABI sceAvControlSetDefaultVideoOutMode(void); +int APS5_VABI sceAvControlSetDispclk(void); +int APS5_VABI sceAvControlSetGamutMetadata(void); +int APS5_VABI sceAvControlSetHdcpEncryption(void); +int APS5_VABI sceAvControlSetHdcpStatus(void); +int APS5_VABI sceAvControlSetHdmiGamutMetadata(void); +int APS5_VABI sceAvControlSetHdrMetadata(void); +int APS5_VABI sceAvControlSetVideoOutSource(void); +int APS5_VABI sceAvControlSimulateProcessOutputModeArbitration(void); +int APS5_VABI sceAvSettingAddCallbacks(void); +int APS5_VABI sceAvSettingAddCallbacksForLnc(void); +int APS5_VABI sceAvSettingAddCallbacksForSocialScreen(void); +int APS5_VABI sceAvSettingCallbackFuncsInit(void); +int APS5_VABI sceAvSettingCallbackFuncsInitForLnc(void); +int APS5_VABI sceAvSettingCallbackFuncsInitForLnc_(void); +int APS5_VABI sceAvSettingCallbackFuncsInit_(void); +int APS5_VABI sceAvSettingChangeOutputMode(void); +int APS5_VABI sceAvSettingChangeOutputMode2(void); +int APS5_VABI sceAvSettingChangeOutputMode3(void); +int APS5_VABI sceAvSettingChangeOutputModeForDiag(void); +int APS5_VABI sceAvSettingChangeProcessAttribute(void); +int APS5_VABI sceAvSettingCheckCallback(void); +int APS5_VABI sceAvSettingControlHdcpEncryption(void); +int APS5_VABI sceAvSettingDebugAddCallbacks(void); +int APS5_VABI sceAvSettingDebugClearDiagCommand(void); +int APS5_VABI sceAvSettingDebugSetDiagState(void); +int APS5_VABI sceAvSettingDebugSetHdmiMonitorInfo(void); +int APS5_VABI sceAvSettingDebugSetProcessAttribute(void); +int APS5_VABI sceAvSettingDriverChangeConnectionStatus(void); +int APS5_VABI sceAvSettingDriverUpdateStatus(void); +int APS5_VABI sceAvSettingEnterAudioMuteForShutdown(void); +int APS5_VABI sceAvSettingGetCurrentDeviceInfo_(void); +int APS5_VABI sceAvSettingGetCurrentHdmiDeviceId(void); +int APS5_VABI sceAvSettingGetCurrentOutputMode2_(void); +int APS5_VABI sceAvSettingGetCurrentOutputMode_(void); +int APS5_VABI sceAvSettingGetDetailedHdcpStatus(void); +int APS5_VABI sceAvSettingGetDeviceInfo(void); +int APS5_VABI sceAvSettingGetHdcpStatus(void); +int APS5_VABI sceAvSettingGetHdmiConnectDisconnectNum(void); +int APS5_VABI sceAvSettingGetHdmiKsvList(void); +int APS5_VABI sceAvSettingGetHdmiMonitorInfo(void); +int APS5_VABI sceAvSettingGetHdmiRawEdid(void); +int APS5_VABI sceAvSettingGetMorpheusPuStatus(void); +int APS5_VABI sceAvSettingGetNativeHdmiMonitorInfo(void); +int APS5_VABI sceAvSettingGetRawHdmiMonitorInfo(void); +int APS5_VABI sceAvSettingInit(void); +int APS5_VABI sceAvSettingIsSupportedAudioOutModeByHdmiMonitorInfo(void); +int APS5_VABI sceAvSettingIsSupportedHdcpVersionByHdmiMonitorInfo(void); +int APS5_VABI sceAvSettingIsSupportedVideoOutModeByHdmiMonitorInfo(void); +int APS5_VABI sceAvSettingIsSuspendedProcessOutputModeAvailable(void); +int APS5_VABI sceAvSettingIsSuspendedProcessOutputModeAvailable2(void); +int APS5_VABI sceAvSettingIsVrSupportedByHdmiMonitorInfo2(void); +int APS5_VABI sceAvSettingNotifyAudioOutMode(void); +int APS5_VABI sceAvSettingNotifyResponseForSocialScreen(void); +int APS5_VABI sceAvSettingNotifyUmdEvent(void); +int APS5_VABI sceAvSettingNotifyVideoOutMode(void); +int APS5_VABI sceAvSettingRemoveCallbacks(void); +int APS5_VABI sceAvSettingRemoveProcessOutputMode(void); +int APS5_VABI sceAvSettingSet2dVrMode(void); +int APS5_VABI sceAvSettingSetAudioOutModeAny_(void); +int APS5_VABI sceAvSettingSetAudioOutModeInvalid_(void); +int APS5_VABI sceAvSettingSetAvOutputMode(void); +int APS5_VABI sceAvSettingSetDispclk(void); +int APS5_VABI sceAvSettingSetDisplayBlackLevel(void); +int APS5_VABI sceAvSettingSetHdcpMode(void); +int APS5_VABI sceAvSettingSetHdcpStatus(void); +int APS5_VABI sceAvSettingSetHdmiGamutMetadata_(void); +int APS5_VABI sceAvSettingSetVideoOutModeAny_(void); +int APS5_VABI sceAvSettingSetVideoOutModeInvalid_(void); +int APS5_VABI sceAvSettingSetVideoOutSource(void); +int APS5_VABI sceAvSettingSetVideoOutputColorEffect(void); +int APS5_VABI sceAvSettingSimulateProcessOutputModeArbitration_(void); +int APS5_VABI sceAvSettingTerm(void); +int APS5_VABI sceAvSettingWaitCallback(void); } diff --git a/core/libs/prx/libSceCdlgUtilServer/Export.cpp b/core/libs/prx/libSceCdlgUtilServer/Export.cpp index a7fac71365..25cb1446d7 100644 --- a/core/libs/prx/libSceCdlgUtilServer/Export.cpp +++ b/core/libs/prx/libSceCdlgUtilServer/Export.cpp @@ -1,3 +1,4 @@ +#include "prx/libSceCdlgUtilServer/include/Api.hpp" #include #include #include "SceTypes.hpp" diff --git a/core/libs/prx/libSceCdlgUtilServer/include/Api.hpp b/core/libs/prx/libSceCdlgUtilServer/include/Api.hpp new file mode 100644 index 0000000000..9b327a4d20 --- /dev/null +++ b/core/libs/prx/libSceCdlgUtilServer/include/Api.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include "SceTypes.hpp" + +extern "C" { + +int APS5_VABI _ZN3sce16CommonDialogUtil6Server13setFinishDataEPvm(void); +int APS5_VABI _ZN3sce16CommonDialogUtil6Server6setRunEPvm(void); +int APS5_VABI _ZN3sce16CommonDialogUtil6Server9setFinishEv(void); +int APS5_VABI _ZN3sce16CommonDialogUtil6Server9setResultEi(void); +int APS5_VABI _ZN3sce16CommonDialogUtil6ServerC2EPKcmmis(void); +int APS5_VABI _ZN3sce16CommonDialogUtil6ServerD0Ev(void); +int APS5_VABI _ZN3sce16CommonDialogUtil6ServerD1Ev(void); +int APS5_VABI _ZN3sce16CommonDialogUtil6ServerD2Ev(void); +int APS5_VABI _ZNK3sce16CommonDialogUtil6Server10getVersionERj(void); +int APS5_VABI _ZNK3sce16CommonDialogUtil6Server10isCloseReqEv(void); +int APS5_VABI _ZNK3sce16CommonDialogUtil6Server14getServerStateEv(void); + +} diff --git a/core/libs/prx/libSceCdlgUtilServer/libSceCdlgUtilServer.hpp b/core/libs/prx/libSceCdlgUtilServer/libSceCdlgUtilServer.hpp index 6f70f09bee..29b94d8e37 100644 --- a/core/libs/prx/libSceCdlgUtilServer/libSceCdlgUtilServer.hpp +++ b/core/libs/prx/libSceCdlgUtilServer/libSceCdlgUtilServer.hpp @@ -1 +1,3 @@ #pragma once + +#include "prx/libSceCdlgUtilServer/include/Api.hpp" diff --git a/core/libs/prx/libSceGvMp4Parser/Export.cpp b/core/libs/prx/libSceGvMp4Parser/Export.cpp index d1151c8ed6..4d1c7ba5b3 100644 --- a/core/libs/prx/libSceGvMp4Parser/Export.cpp +++ b/core/libs/prx/libSceGvMp4Parser/Export.cpp @@ -1,3 +1,4 @@ +#include "prx/libSceGvMp4Parser/include/Api.hpp" #include #include #include "SceTypes.hpp" diff --git a/core/libs/prx/libSceGvMp4Parser/include/Api.hpp b/core/libs/prx/libSceGvMp4Parser/include/Api.hpp new file mode 100644 index 0000000000..d27fe7f4be --- /dev/null +++ b/core/libs/prx/libSceGvMp4Parser/include/Api.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include "SceTypes.hpp" + +extern "C" { + +int APS5_VABI _ZN23sceMetadataReaderWriter11gvMp4Parser19gvMp4ParserFinalizeEv(void); +int APS5_VABI _ZN23sceMetadataReaderWriter11gvMp4Parser21gvMp4ParserInitializeEv(void); +int APS5_VABI _ZN23sceMetadataReaderWriter11gvMp4Parser23gvMp4ParserReadMetadataERKSsRKNS_8MetadataERS3_(void); +int APS5_VABI _ZN23sceMetadataReaderWriter11gvMp4Parser24gvMp4ParserWriteMetadataERKSsRKNS_8MetadataE(void); +int APS5_VABI _ZN23sceMetadataReaderWriter11gvMp4Parser27gvMp4ParserExtractThumbnailERKSsRNS_9ThumbnailE(void); + +} diff --git a/core/libs/prx/libSceGvMp4Parser/libSceGvMp4Parser.hpp b/core/libs/prx/libSceGvMp4Parser/libSceGvMp4Parser.hpp index 6f70f09bee..c639941f8d 100644 --- a/core/libs/prx/libSceGvMp4Parser/libSceGvMp4Parser.hpp +++ b/core/libs/prx/libSceGvMp4Parser/libSceGvMp4Parser.hpp @@ -1 +1,3 @@ #pragma once + +#include "prx/libSceGvMp4Parser/include/Api.hpp" diff --git a/core/libs/prx/libSceHidControl/include/Api.hpp b/core/libs/prx/libSceHidControl/include/Api.hpp index ca9e12b806..d19c925d3d 100644 --- a/core/libs/prx/libSceHidControl/include/Api.hpp +++ b/core/libs/prx/libSceHidControl/include/Api.hpp @@ -44,5 +44,28 @@ int APS5_VABI sceHidControlGetPswDeviceInfo(void); int APS5_VABI sceHidControlSetPswCommand(void); int APS5_VABI sceHidControlGetPswCommand(void); int APS5_VABI sceHidControlGetPswDeviceIdFromIdxAdd(void); +int APS5_VABI sceHidControlConnectHandle(void); +int APS5_VABI sceHidControlConnectPort(void); +int APS5_VABI sceHidControlDisconnectDevice(void); +int APS5_VABI sceHidControlDisconnectHandle(void); +int APS5_VABI sceHidControlDisconnectPort(void); +int APS5_VABI sceHidControlGetAuthInformation(void); +int APS5_VABI sceHidControlGetBatteryState(void); +int APS5_VABI sceHidControlGetDeviceId(void); +int APS5_VABI sceHidControlGetDeviceInfo(void); +int APS5_VABI sceHidControlGetDeviceName(void); +int APS5_VABI sceHidControlGetJediExtensionUnitInfo(void); +int APS5_VABI sceHidControlGetJediState(void); +int APS5_VABI sceHidControlGetSparkState(void); +int APS5_VABI sceHidControlGetVersionInfo(void); +int APS5_VABI sceHidControlInit(void); +int APS5_VABI sceHidControlSetApplicationFocus(void); +int APS5_VABI sceHidControlSetHandleState(void); +int APS5_VABI sceHidControlSetJediAudioOutPath(void); +int APS5_VABI sceHidControlSetJediVolumeGain(void); +int APS5_VABI sceHidControlSetProcessFocus(void); +int APS5_VABI sceHidControlSetRevokeList(void); +int APS5_VABI sceHidControlSetRevokeList2(void); +int APS5_VABI sceHidControlSetUserColor(void); } diff --git a/core/libs/prx/libSceJpegParser/Export.cpp b/core/libs/prx/libSceJpegParser/Export.cpp index 575a1af715..d42156fe9b 100644 --- a/core/libs/prx/libSceJpegParser/Export.cpp +++ b/core/libs/prx/libSceJpegParser/Export.cpp @@ -1,3 +1,4 @@ +#include "prx/libSceJpegParser/include/Api.hpp" #include #include #include "SceTypes.hpp" diff --git a/core/libs/prx/libSceJpegParser/include/Api.hpp b/core/libs/prx/libSceJpegParser/include/Api.hpp new file mode 100644 index 0000000000..a46c4d5364 --- /dev/null +++ b/core/libs/prx/libSceJpegParser/include/Api.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include "SceTypes.hpp" + +extern "C" { + +int APS5_VABI _ZN23sceMetadataReaderWriter10jpegParser18jpegParserFinalizeEv(void); +int APS5_VABI _ZN23sceMetadataReaderWriter10jpegParser20jpegParserInitializeEv(void); +int APS5_VABI _ZN23sceMetadataReaderWriter10jpegParser22jpegParserReadMetadataERKSsRKNS_8MetadataERS3_(void); +int APS5_VABI _ZN23sceMetadataReaderWriter10jpegParser23jpegParserWriteMetadataERKSsRKNS_8MetadataE(void); +int APS5_VABI _ZN23sceMetadataReaderWriter10jpegParser26jpegParserExtractThumbnailERKSsRNS_9ThumbnailE(void); + +} diff --git a/core/libs/prx/libSceJpegParser/libSceJpegParser.hpp b/core/libs/prx/libSceJpegParser/libSceJpegParser.hpp index 6f70f09bee..9b5e8e9385 100644 --- a/core/libs/prx/libSceJpegParser/libSceJpegParser.hpp +++ b/core/libs/prx/libSceJpegParser/libSceJpegParser.hpp @@ -1 +1,3 @@ #pragma once + +#include "prx/libSceJpegParser/include/Api.hpp" diff --git a/core/libs/prx/libSceMetadataReaderWriter/Export.cpp b/core/libs/prx/libSceMetadataReaderWriter/Export.cpp index 4a5ec0048e..6cdea6e0d3 100644 --- a/core/libs/prx/libSceMetadataReaderWriter/Export.cpp +++ b/core/libs/prx/libSceMetadataReaderWriter/Export.cpp @@ -1,3 +1,4 @@ +#include "prx/libSceMetadataReaderWriter/include/Api.hpp" #include #include #include "SceTypes.hpp" diff --git a/core/libs/prx/libSceMetadataReaderWriter/include/Api.hpp b/core/libs/prx/libSceMetadataReaderWriter/include/Api.hpp new file mode 100644 index 0000000000..bcf8ad80e7 --- /dev/null +++ b/core/libs/prx/libSceMetadataReaderWriter/include/Api.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include "SceTypes.hpp" + +extern "C" { + +int APS5_VABI _ZN23sceMetadataReaderWriter10initializeEv(void); +int APS5_VABI _ZN23sceMetadataReaderWriter11getMimeTypeERKSsRSt6vectorIjSaIjEE(void); +int APS5_VABI _ZN23sceMetadataReaderWriter12getMimeTypesERKSsiPi(void); +int APS5_VABI _ZN23sceMetadataReaderWriter12readMetadataERKSsRKNS_8MetadataEjRS2_(void); +int APS5_VABI _ZN23sceMetadataReaderWriter13writeMetadataERKSsRKNS_8MetadataEj(void); +int APS5_VABI _ZN23sceMetadataReaderWriter13writeMetadataERKSsRKNS_8MetadataEjPS2_(void); +int APS5_VABI _ZN23sceMetadataReaderWriter15ParserInterfaceC1ERKS0_(void); +int APS5_VABI _ZN23sceMetadataReaderWriter15ParserInterfaceC2ERKS0_(void); +int APS5_VABI _ZN23sceMetadataReaderWriter16extractThumbnailERKSsjRNS_9ThumbnailE(void); +int APS5_VABI _ZN23sceMetadataReaderWriter16getNumOfMimeTypeERKSsPi(void); +int APS5_VABI _ZN23sceMetadataReaderWriter17getParserInfoListERSt6vectorINS_10ParserInfoESaIS1_EE(void); +int APS5_VABI _ZN23sceMetadataReaderWriter18registerParserInfoERKSt6vectorINS_10ParserInfoESaIS1_EE(void); +int APS5_VABI _ZN23sceMetadataReaderWriter20unregisterParserInfoEv(void); +int APS5_VABI _ZN23sceMetadataReaderWriter23registerParserInterfaceEjRKNS_15ParserInterfaceE(void); +int APS5_VABI _ZN23sceMetadataReaderWriter5ValueaSERS0_(void); +int APS5_VABI _ZN23sceMetadataReaderWriter8Metadata11DeserializeERN3sce4Json5ValueERS0_(void); +int APS5_VABI _ZN23sceMetadataReaderWriter8Metadata11deleteFieldERKSs(void); +int APS5_VABI _ZN23sceMetadataReaderWriter8Metadata15iterateAllFieldEPFiRKSsRKNS_5ValueEbE(void); +int APS5_VABI _ZN23sceMetadataReaderWriter8Metadata8getFieldEiRSsRNS_5ValueERb(void); +int APS5_VABI _ZN23sceMetadataReaderWriter8Metadata8setFieldERKSs(void); +int APS5_VABI _ZN23sceMetadataReaderWriter8Metadata8setFieldERKSsRKNS_5ValueE(void); +int APS5_VABI _ZN23sceMetadataReaderWriter8Metadata9SerializeERS0_RN3sce4Json5ValueE(void); +int APS5_VABI _ZN23sceMetadataReaderWriter8MetadataC1EPFPvmEPFvS1_E(void); +int APS5_VABI _ZN23sceMetadataReaderWriter8MetadataC1ERKS0_(void); +int APS5_VABI _ZN23sceMetadataReaderWriter8MetadataC1ERKS0_PFPvmEPFvS3_E(void); +int APS5_VABI _ZN23sceMetadataReaderWriter8MetadataC1Ev(void); +int APS5_VABI _ZN23sceMetadataReaderWriter8MetadataC2EPFPvmEPFvS1_E(void); +int APS5_VABI _ZN23sceMetadataReaderWriter8MetadataC2ERKS0_(void); +int APS5_VABI _ZN23sceMetadataReaderWriter8MetadataC2ERKS0_PFPvmEPFvS3_E(void); +int APS5_VABI _ZN23sceMetadataReaderWriter8MetadataC2Ev(void); +int APS5_VABI _ZN23sceMetadataReaderWriter8MetadataD0Ev(void); +int APS5_VABI _ZN23sceMetadataReaderWriter8MetadataD1Ev(void); +int APS5_VABI _ZN23sceMetadataReaderWriter8MetadataD2Ev(void); +int APS5_VABI _ZN23sceMetadataReaderWriter8MetadataaSERKS0_(void); +int APS5_VABI _ZN23sceMetadataReaderWriter8finalizeEv(void); +int APS5_VABI _ZNK23sceMetadataReaderWriter8Metadata13getFieldCountEv(void); +int APS5_VABI _ZNK23sceMetadataReaderWriter8Metadata8getFieldEiRSsRNS_5ValueE(void); +int APS5_VABI _ZNK23sceMetadataReaderWriter8Metadata8getValueERKSsRNS_5ValueE(void); +int APS5_VABI _ZNK23sceMetadataReaderWriter8Metadata9dumpValueEv(void); + +} diff --git a/core/libs/prx/libSceMetadataReaderWriter/libSceMetadataReaderWriter.hpp b/core/libs/prx/libSceMetadataReaderWriter/libSceMetadataReaderWriter.hpp index 6f70f09bee..4e504bad70 100644 --- a/core/libs/prx/libSceMetadataReaderWriter/libSceMetadataReaderWriter.hpp +++ b/core/libs/prx/libSceMetadataReaderWriter/libSceMetadataReaderWriter.hpp @@ -1 +1,3 @@ #pragma once + +#include "prx/libSceMetadataReaderWriter/include/Api.hpp" diff --git a/core/libs/prx/libScePsm/Export.cpp b/core/libs/prx/libScePsm/Export.cpp index 1af3dc531d..d31d96f41f 100644 --- a/core/libs/prx/libScePsm/Export.cpp +++ b/core/libs/prx/libScePsm/Export.cpp @@ -1,3 +1,4 @@ +#include "prx/libScePsm/include/Api.hpp" #include #include #include "SceTypes.hpp" diff --git a/core/libs/prx/libScePsm/include/Api.hpp b/core/libs/prx/libScePsm/include/Api.hpp new file mode 100644 index 0000000000..d2c372b43a --- /dev/null +++ b/core/libs/prx/libScePsm/include/Api.hpp @@ -0,0 +1,73 @@ +#pragma once + +#include "SceTypes.hpp" + +extern "C" { + +int APS5_VABI ScePsmMonoArrayAddrWithSize(void); +int APS5_VABI ScePsmMonoArrayLength(void); +int APS5_VABI ScePsmMonoDomainGet(void); +int APS5_VABI ScePsmMonoFree(void); +int APS5_VABI ScePsmMonoGcWbarrierGenericStore(void); +int APS5_VABI ScePsmMonoStringChars(void); +int APS5_VABI ScePsmMonoStringFromUtf16(void); +int APS5_VABI ScePsmMonoStringLength(void); +int APS5_VABI ScePsmMonoStringNew(void); +int APS5_VABI ScePsmMonoStringNewWrapper(void); +int APS5_VABI ScePsmMonoStringToUtf8(void); +int APS5_VABI _ZN3sce3pss4core14IntHandleTable13NextIntHandleEv(void); +int APS5_VABI _ZN3sce3pss4core8graphics14NativeGraphics18ReleaseVideoMemoryEPv(void); +int APS5_VABI _ZN3sce3pss4core8graphics14NativeGraphics19AllocateVideoMemoryEjj(void); +int APS5_VABI _ZN3sce3pss4core8graphics14NativeGraphics19AllocateVideoMemoryEjjPv(void); +int APS5_VABI _ZN3sce3pss4core8graphics14NativeGraphics19ReleaseSystemMemoryEPv(void); +int APS5_VABI _ZN3sce3pss4core8graphics14NativeGraphics20AllocateSystemMemoryEjj(void); +int APS5_VABI _ZN3sce3pss4core8graphics14NativeGraphics20AllocateSystemMemoryEjjPv(void); +int APS5_VABI _ZN3sce3pss4core9PsmObjectC1Ev(void); +int APS5_VABI _ZN3sce3pss4core9PsmObjectC2Ev(void); +int APS5_VABI _ZN3sce3pss4core9PsmObjectD0Ev(void); +int APS5_VABI _ZN3sce3pss4core9PsmObjectD1Ev(void); +int APS5_VABI _ZN3sce3pss4core9PsmObjectD2Ev(void); +int APS5_VABI _ZN3sce3pss4core9threading4Cond4WaitEi(void); +int APS5_VABI _ZN3sce3pss4core9threading4Cond6NotifyEv(void); +int APS5_VABI _ZN3sce3pss4core9threading4CondC1ERNS2_5MutexEPKci(void); +int APS5_VABI _ZN3sce3pss4core9threading4CondC2ERNS2_5MutexEPKci(void); +int APS5_VABI _ZN3sce3pss4core9threading4CondD1Ev(void); +int APS5_VABI _ZN3sce3pss4core9threading4CondD2Ev(void); +int APS5_VABI _ZN3sce3pss5orbis9framework11PsmArrayObj6LengthEv(void); +int APS5_VABI _ZN3sce3pss5orbis9framework11PsmArrayObj9ToPointerEv(void); +int APS5_VABI _ZN3sce3pss5orbis9framework11PsmCallList10UnregisterEPFvPvES4_(void); +int APS5_VABI _ZN3sce3pss5orbis9framework11PsmCallList6UpdateEv(void); +int APS5_VABI _ZN3sce3pss5orbis9framework11PsmCallList8RegisterEPFvPvES4_(void); +int APS5_VABI _ZN3sce3pss5orbis9framework11PsmCallListC1Ev(void); +int APS5_VABI _ZN3sce3pss5orbis9framework11PsmCallListC2Ev(void); +int APS5_VABI _ZN3sce3pss5orbis9framework12PsmEventData8SetValueERKNS_4Json5ValueE(void); +int APS5_VABI _ZN3sce3pss5orbis9framework12PsmFramework10InitializeERKNS2_12PsmInitParamEiPPc(void); +int APS5_VABI _ZN3sce3pss5orbis9framework12PsmFramework20RegisterInternalCallEPKNS2_12PsmCallEntryE(void); +int APS5_VABI _ZN3sce3pss5orbis9framework12PsmFramework24RegisterPInvokeCallTableEPKNS2_15PsmLibraryEntryE(void); +int APS5_VABI _ZN3sce3pss5orbis9framework12PsmFramework3RunEv(void); +int APS5_VABI _ZN3sce3pss5orbis9framework12PsmFramework8FinalizeEv(void); +int APS5_VABI _ZN3sce3pss5orbis9framework12PsmInitParamC1Ev(void); +int APS5_VABI _ZN3sce3pss5orbis9framework12PsmInitParamC2Ev(void); +int APS5_VABI _ZN3sce3pss5orbis9framework12PsmStringObj17NewAndStoreStringEPvPc(void); +int APS5_VABI _ZN3sce3pss5orbis9framework12PsmStringObj6ToUtf8Ev(void); +int APS5_VABI _ZN3sce3pss5orbis9framework12PsmStringObj8FreeUtf8EPc(void); +int APS5_VABI _ZN3sce3pss5orbis9framework14PsmDelegateObj11FreePointerEv(void); +int APS5_VABI _ZN3sce3pss5orbis9framework14PsmDelegateObj15AllocatePointerEv(void); +int APS5_VABI _ZN3sce3pss5orbis9framework18PsmFrameworkCsharp21SetEnableNativeMethodEb(void); +int APS5_VABI _ZN3sce3pss5orbis9framework21PsmMainThreadCallList10UnregisterEPFvPvES4_(void); +int APS5_VABI _ZN3sce3pss5orbis9framework21PsmMainThreadCallList6UpdateEv(void); +int APS5_VABI _ZN3sce3pss5orbis9framework21PsmMainThreadCallList8RegisterEPFvPvES4_(void); +int APS5_VABI _ZN3sce3pss5orbis9framework23TerminatePsmEventCsharpEv(void); +int APS5_VABI _ZN3sce3pss5orbis9framework24InitializePsmEventCsharpEv(void); +int APS5_VABI _ZN3sce3pss5orbis9framework27TerminatePsmFrameworkCsharpEv(void); +int APS5_VABI _ZN3sce3pss5orbis9framework28InitializePsmFrameworkCsharpEv(void); +int APS5_VABI _ZN3sce3pss5orbis9framework8PsmEvent10UnregisterEiPFvRKNS2_12PsmEventDataEPvES7_(void); +int APS5_VABI _ZN3sce3pss5orbis9framework8PsmEvent17RegisterDelegatorEPFvRKNS2_12PsmEventDataEPvES7_(void); +int APS5_VABI _ZN3sce3pss5orbis9framework8PsmEvent19UnregisterDelegatorEPFvRKNS2_12PsmEventDataEPvES7_(void); +int APS5_VABI _ZN3sce3pss5orbis9framework8PsmEvent4PostERKNS2_12PsmEventDataE(void); +int APS5_VABI _ZN3sce3pss5orbis9framework8PsmEvent4SendERKNS2_12PsmEventDataE(void); +int APS5_VABI _ZN3sce3pss5orbis9framework8PsmEvent8RegisterEiPFvRKNS2_12PsmEventDataEPvES7_(void); +int APS5_VABI scePsmRegisterInternalCall(void); +int APS5_VABI scePsmRegisterPInvokeCallTable(void); + +} diff --git a/core/libs/prx/libScePsm/libScePsm.hpp b/core/libs/prx/libScePsm/libScePsm.hpp index 6f70f09bee..8ba5c40198 100644 --- a/core/libs/prx/libScePsm/libScePsm.hpp +++ b/core/libs/prx/libScePsm/libScePsm.hpp @@ -1 +1,3 @@ #pragma once + +#include "prx/libScePsm/include/Api.hpp" From 2aa0ffbbcf3ce50ee7edf3cb0f1265820a8ce338 Mon Sep 17 00:00:00 2001 From: AZLisme <8706522+AZLisme@users.noreply.github.com> Date: Fri, 23 Oct 2026 14:08:11 +0800 Subject: [PATCH 155/198] feat(libSceVideodec2): expose HEVC picture metadata --- core/libs/prx/libSceVideodec2/CMakeLists.txt | 12 + core/libs/prx/libSceVideodec2/Export.cpp | 73 ++- .../libs/prx/libSceVideodec2/HevcMetadata.cpp | 505 ++++++++++++++++++ .../libs/prx/libSceVideodec2/HevcMetadata.hpp | 61 +++ .../prx/libSceVideodec2/Unimplemented.cpp | 4 - core/libs/prx/libSceVideodec2/include/Api.hpp | 84 ++- core/libs/tests/GuestHevcMetadata.cpp | 399 ++++++++++++++ .../tests/GuestVideodec2Unimplemented.cpp | 1 - core/libs/tests/HevcMetadataFixture.hpp | 45 ++ docs/dev/debt/libSceVideodec2.md | 15 +- 10 files changed, 1186 insertions(+), 13 deletions(-) create mode 100644 core/libs/prx/libSceVideodec2/HevcMetadata.cpp create mode 100644 core/libs/prx/libSceVideodec2/HevcMetadata.hpp create mode 100644 core/libs/tests/GuestHevcMetadata.cpp create mode 100644 core/libs/tests/HevcMetadataFixture.hpp diff --git a/core/libs/prx/libSceVideodec2/CMakeLists.txt b/core/libs/prx/libSceVideodec2/CMakeLists.txt index 8bc94af3d1..244b8050b3 100644 --- a/core/libs/prx/libSceVideodec2/CMakeLists.txt +++ b/core/libs/prx/libSceVideodec2/CMakeLists.txt @@ -1,6 +1,7 @@ add_library(libSceVideodec2 SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Unimplemented.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/HevcMetadata.cpp ) target_include_directories(libSceVideodec2 PRIVATE ${LIBS_INCLUDE_DIR}) @@ -9,6 +10,17 @@ configure_windows_unwind(libSceVideodec2) target_link_libraries(libSceVideodec2 PRIVATE ffmpeg) if(BUILD_TESTING) + add_test_executable(guest_hevc_metadata_tests ${LIBS_INCLUDE_DIR}/tests/GuestHevcMetadata.cpp) + target_include_directories(guest_hevc_metadata_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_hevc_metadata_tests PRIVATE libSceVideodec2 libc) + configure_windows_unwind(guest_hevc_metadata_tests) + add_test(NAME guest_hevc_metadata COMMAND guest_hevc_metadata_tests) + set_tests_properties(guest_hevc_metadata PROPERTIES TIMEOUT 10) + if(WIN32) + set_tests_properties(guest_hevc_metadata PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_videodec2_tests ${LIBS_INCLUDE_DIR}/tests/GuestVideodec2.cpp) target_include_directories(guest_videodec2_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_videodec2_tests PRIVATE libSceVideodec2 libc) diff --git a/core/libs/prx/libSceVideodec2/Export.cpp b/core/libs/prx/libSceVideodec2/Export.cpp index df09695444..73b702fab4 100644 --- a/core/libs/prx/libSceVideodec2/Export.cpp +++ b/core/libs/prx/libSceVideodec2/Export.cpp @@ -15,6 +15,8 @@ #include #include #include "prx/libc/include/General.hpp" +#include "HevcMetadata.hpp" +#include "prx/libc/include/GuestArena.hpp" extern "C" { #include @@ -27,6 +29,8 @@ namespace { using namespace SceVideodec2; +struct UnsupportedHevcMetadata {}; + constexpr std::uint64_t WorkMemoryBytes = 1u << 20u; constexpr std::uint32_t PitchAlignment = 256; constexpr std::uint32_t HeightAlignment = 32; @@ -188,6 +192,7 @@ struct Picture { Crop crop{}; bool hevcIdr = false; std::uint8_t bitDepthMinus8 = 0; + Videodec2Hevc::Metadata hevc; }; struct PictureHeader { @@ -229,6 +234,7 @@ struct Decoder { bool flushing = false; std::array vp9References{}; Vp9PictureInfo vp9Colors{}; + Videodec2Hevc::State hevc; ~Decoder() { sws_freeContext(scaler); @@ -547,6 +553,7 @@ int APS5_VABI sceVideodec2Decode_nid_postfix(std::uint64_t handle, const InputDa const Picture picture{input->ptsData, input->dtsData, input->attachedData}; if (decoder.flushing) { avcodec_flush_buffers(decoder.context); + decoder.hevc.afterEnd = true; decoder.flushing = false; } if (input->auSize > static_cast(std::numeric_limits::max())) throw std::runtime_error("Videodec2: invalid access unit"); @@ -595,6 +602,12 @@ int APS5_VABI sceVideodec2Decode_nid_postfix(std::uint64_t handle, const InputDa packet->pts = decoder.nextKey; auto tagged = picture; tagged.hevcIdr = decoder.codec == CodecHevc && StartsHevcIdr(*unit); + if (decoder.codec == CodecHevc) { + tagged.hevc = Videodec2Hevc::Parse_nid_no_patch(decoder.hevc, *unit); + tagged.hevc.info.ptsData = picture.pts; + tagged.hevc.info.dtsData = picture.dts; + tagged.hevc.info.attachedData = picture.attached; + } decoder.inputs.emplace(decoder.nextKey++, tagged); Send(decoder, packet); EmitDecoded(handle, decoder, frame, output); @@ -628,6 +641,7 @@ int APS5_VABI sceVideodec2Reset_nid_postfix(std::uint64_t handle) { std::erase_if(pictureOwners, [handle](const auto& owner) { return owner.second == handle; }); } avcodec_flush_buffers(decoder.context); + decoder.hevc.afterEnd = true; decoder.ready.clear(); decoder.inputs.clear(); decoder.parameterSets.clear(); @@ -635,9 +649,11 @@ int APS5_VABI sceVideodec2Reset_nid_postfix(std::uint64_t handle) { return 0; } -int APS5_VABI sceVideodec2GetPictureInfo_nid_postfix(const OutputInfo* output, AvcPictureInfo* first, void* second) { +static int GetPictureInfo(const OutputInfo* output, AvcPictureInfo* first, void* second, bool allowHevc) { (void)second; if (!output || !first) throw std::runtime_error("Videodec2: invalid argument pointer"); + if (output->codecType == CodecHevc && (!Fits(output, offsetof(OutputInfo, frameBuffer) + sizeof(output->frameBuffer)) || !Fits(first, sizeof(Videodec2Hevc::PictureInfo)))) + throw std::runtime_error("Videodec2: HEVC picture structure is too small"); const auto owner = [output]() -> std::uint64_t { std::lock_guard guard(lock); const auto found = pictureOwners.find(output->frameBuffer); @@ -658,7 +674,17 @@ int APS5_VABI sceVideodec2GetPictureInfo_nid_postfix(const OutputInfo* output, A info->thisSize = size; return 0; } - if (decoder->codec != CodecAvc) throw std::runtime_error("Videodec2: picture info is only implemented for H.264 and VP9"); + if (decoder->codec == CodecHevc) { + if (!allowHevc) throw UnsupportedHevcMetadata{}; + auto* info = reinterpret_cast(first); + if (!Fits(info, sizeof(*info))) throw std::runtime_error("Videodec2: HEVC picture structure is too small"); + if (!found->second.hevc.unsupported.empty()) throw UnsupportedHevcMetadata{}; + const auto size = info->thisSize; + GuestArena::HostWrite access(info, sizeof(*info)); + *info = found->second.hevc.info; + info->thisSize = size; + return 0; + } const auto& picture = found->second; first->isValid = true; first->ptsData = picture.pts; @@ -673,12 +699,51 @@ int APS5_VABI sceVideodec2GetPictureInfo_nid_postfix(const OutputInfo* output, A } return 0; } - first->isValid = false; + if (output->codecType == CodecHevc) { + GuestArena::HostWrite access(&first->isValid, sizeof(first->isValid)); + first->isValid = false; + } else first->isValid = false; return 0; } +int APS5_VABI sceVideodec2GetPictureInfo_nid_postfix(const OutputInfo* output, AvcPictureInfo* first, void* second) { + try { return GetPictureInfo(output, first, second, true); } + catch (const UnsupportedHevcMetadata&) { NotImplemented_nid_no_patch(__func__); } +} + int APS5_VABI sceVideodec2GetAvcPictureInfo_nid_postfix(const OutputInfo* output, AvcPictureInfo* first, AvcPictureInfo* second) { - return sceVideodec2GetPictureInfo_nid_postfix(output, first, second); + try { return GetPictureInfo(output, first, second, false); } + catch (const UnsupportedHevcMetadata&) { NotImplemented_nid_no_patch(__func__); } +} + +int APS5_VABI sceVideodec2GetHevcPictureInfo_nid_postfix(const OutputInfo* output, Videodec2Hevc::PictureInfo* info) { + if (!output || !info) throw std::runtime_error("Videodec2: invalid HEVC picture argument"); + if (!Fits(output, offsetof(OutputInfo, frameBuffer) + sizeof(output->frameBuffer)) || !Fits(info, sizeof(*info))) + throw std::runtime_error("Videodec2: HEVC picture structure is too small"); + if (output->codecType != CodecHevc) throw std::runtime_error("Videodec2: HEVC info requested for another codec"); + const auto owner = [output]() -> std::uint64_t { + std::lock_guard guard(lock); + const auto found = pictureOwners.find(output->frameBuffer); + return found != pictureOwners.end() ? found->second : 0; + }; + for (auto handle = owner(); handle != 0; handle = owner()) { + const auto decoder = FindDecoder(handle); + if (!decoder) continue; + std::lock_guard guard(decoder->mutex); + if (owner() != handle) continue; + if (decoder->codec != CodecHevc) throw std::runtime_error("Videodec2: HEVC info requested for another codec"); + const auto found = decoder->pictures.find(output->frameBuffer); + if (found == decoder->pictures.end()) throw std::runtime_error("Videodec2: owned HEVC picture is missing"); + if (!found->second.hevc.unsupported.empty()) NotImplemented_nid_no_patch(__func__); + const auto size = info->thisSize; + GuestArena::HostWrite access(info, sizeof(*info)); + *info = found->second.hevc.info; + info->thisSize = size; + return 0; + } + GuestArena::HostWrite access(&info->isValid, sizeof(info->isValid)); + info->isValid = false; + return 0; } int APS5_VABI sceVideodec2GetVp9PictureInfo_nid_postfix(const OutputInfo* output, Vp9PictureInfo* info) { diff --git a/core/libs/prx/libSceVideodec2/HevcMetadata.cpp b/core/libs/prx/libSceVideodec2/HevcMetadata.cpp new file mode 100644 index 0000000000..b005af7cf4 --- /dev/null +++ b/core/libs/prx/libSceVideodec2/HevcMetadata.cpp @@ -0,0 +1,505 @@ +#include "HevcMetadata.hpp" +#include +#include +#include +#include +#include + +namespace { + +class Bits { +public: + explicit Bits(std::span bytes) : bytes(bytes) {} + + std::uint32_t Read(unsigned count) { + if (count > 32 || count > bytes.size() * 8 - position) throw std::runtime_error("truncated HEVC syntax"); + std::uint32_t value = 0; + for (unsigned bit = 0; bit < count; ++bit, ++position) + value = (value << 1) | ((bytes[position / 8] >> (7 - position % 8)) & 1); + return value; + } + + void Skip(std::size_t count) { + if (count > bytes.size() * 8 - position) throw std::runtime_error("truncated HEVC syntax"); + position += count; + } + + std::uint32_t Unsigned(std::uint32_t maximum = std::numeric_limits::max()) { + unsigned zeros = 0; + while (Read(1) == 0) if (++zeros > 31) throw std::runtime_error("HEVC exponential code overflow"); + const auto value = ((std::uint32_t{1} << zeros) - 1) + Read(zeros); + if (value > maximum) throw std::runtime_error("HEVC syntax value outside its domain"); + return value; + } + + void Signed() { Unsigned(); } + +private: + std::span bytes; + std::size_t position = 0; +}; + +std::vector Rbsp(std::span nal) { + std::vector bytes; + bytes.reserve(nal.size()); + unsigned zeros = 0; + for (std::size_t index = 0; index < nal.size(); ++index) { + const auto value = nal[index]; + if (zeros == 2 && value == 3) { + if (index + 1 < nal.size() && nal[index + 1] > 3) throw std::runtime_error("invalid HEVC emulation prevention"); + zeros = 0; + continue; + } + if (zeros == 2 && value < 3) throw std::runtime_error("missing HEVC emulation prevention"); + bytes.push_back(value); + zeros = value == 0 ? std::min(zeros + 1, 2u) : 0; + } + return bytes; +} + +std::uint32_t Profile(Bits& bits, unsigned sublayers, bool* mixed = nullptr, Videodec2Hevc::PictureInfo* info = nullptr) { + const auto space = bits.Read(2); + const auto tier = bits.Read(1); + const auto profileId = bits.Read(5); + bits.Skip(32); + const bool progressive = bits.Read(1) != 0; + const bool interlaced = bits.Read(1) != 0; + if (mixed) *mixed = progressive && interlaced; + bits.Skip(1); + const auto frameOnly = bits.Read(1); + bits.Skip(44); + const auto levelId = bits.Read(8); + if (info) { + info->profileSpace = static_cast(space); + info->tier = static_cast(tier); + info->profile = static_cast(profileId); + info->progressiveSource = progressive; + info->interlacedSource = interlaced; + info->frameOnlyConstraint = static_cast(frameOnly); + info->level = static_cast(levelId); + } + std::array profile{}; + std::array level{}; + for (unsigned layer = 0; layer < sublayers; ++layer) { + profile[layer] = bits.Read(1) != 0; + level[layer] = bits.Read(1) != 0; + } + if (sublayers != 0) bits.Skip(2 * (8 - sublayers)); + for (unsigned layer = 0; layer < sublayers; ++layer) { + if (profile[layer]) bits.Skip(88); + if (level[layer]) bits.Skip(8); + } + return progressive == interlaced ? 2 : progressive ? 1 : 0; +} + +void Ordering(Bits& bits, unsigned sublayers, Videodec2Hevc::PictureInfo* info = nullptr) { + const auto present = bits.Read(1); + const unsigned begin = present != 0 ? 0 : sublayers; + unsigned buffering = 0; + for (unsigned layer = begin; layer <= sublayers; ++layer) { + buffering = std::max(buffering, bits.Unsigned(15)); + bits.Unsigned(); + bits.Unsigned(); + } + if (info) { + info->subLayerOrderingPresent = static_cast(present); + info->maxDecPicBufferingMinus1 = static_cast(buffering); + } +} + +std::pair ParseVps(Bits& bits) { + const auto id = bits.Read(4); + bits.Skip(2); + const auto layers = bits.Read(6); + const auto sublayers = bits.Read(3); + if (layers != 0 || sublayers > 6) throw std::runtime_error("multilayer HEVC VPS metadata is unsupported"); + bits.Skip(17); + Profile(bits, sublayers); + Ordering(bits, sublayers); + const auto maxLayer = bits.Read(6); + const auto sets = bits.Unsigned(1023); + if (maxLayer != 0 || sets != 0) throw std::runtime_error("HEVC layer-set metadata is unsupported"); + Videodec2Hevc::Vps vps; + vps.timing = bits.Read(1) != 0; + if (vps.timing) { + vps.numUnitsInTick = bits.Read(32); + vps.timeScale = bits.Read(32); + } + return {id, vps}; +} + +void ScalingLists(Bits& bits) { + for (unsigned size = 0; size < 4; ++size) { + for (unsigned matrix = 0; matrix < 6; matrix += size == 3 ? 3 : 1) { + if (bits.Read(1) == 0) bits.Unsigned(); + else { + if (size > 1) bits.Signed(); + const unsigned coefficients = size == 0 ? 16 : 64; + for (unsigned coefficient = 0; coefficient < coefficients; ++coefficient) bits.Signed(); + } + } + } +} + +void ShortTermReferences(Bits& bits) { + const auto count = bits.Unsigned(64); + std::array deltas{}; + for (unsigned index = 0; index < count; ++index) { + if (index != 0 && bits.Read(1) != 0) { + bits.Skip(1); + bits.Unsigned(); + for (unsigned reference = 0; reference <= deltas[index - 1]; ++reference) { + const bool used = bits.Read(1) != 0; + if (used || bits.Read(1) != 0) ++deltas[index]; + } + } else { + const auto negative = bits.Unsigned(16); + const auto positive = bits.Unsigned(16); + if (negative + positive > 16) throw std::runtime_error("too many HEVC short-term references"); + deltas[index] = negative + positive; + for (unsigned reference = 0; reference < deltas[index]; ++reference) { + bits.Unsigned(); + bits.Skip(1); + } + } + if (deltas[index] > 16) throw std::runtime_error("too many predicted HEVC references"); + } +} + +void Hrd(Bits& bits, unsigned sublayers) { + const bool nal = bits.Read(1) != 0; + const bool vcl = bits.Read(1) != 0; + bool subpicture = false; + if (nal || vcl) { + subpicture = bits.Read(1) != 0; + if (subpicture) bits.Skip(19); + bits.Skip(8); + if (subpicture) bits.Skip(4); + bits.Skip(15); + } + for (unsigned layer = 0; layer <= sublayers; ++layer) { + const bool general = bits.Read(1) != 0; + const bool fixed = general || bits.Read(1) != 0; + bool lowDelay = false; + if (fixed) bits.Unsigned(); + else lowDelay = bits.Read(1) != 0; + const auto count = lowDelay ? 0 : bits.Unsigned(31); + for (unsigned kind = 0; kind < 2; ++kind) { + if (!(kind == 0 ? nal : vcl)) continue; + for (unsigned entry = 0; entry <= count; ++entry) { + bits.Unsigned(); + bits.Unsigned(); + if (subpicture) { + bits.Unsigned(); + bits.Unsigned(); + } + bits.Skip(1); + } + } + } +} + +void Vui(Bits& bits, unsigned sublayers, Videodec2Hevc::PictureInfo& info) { + info.aspectRatioInfoPresent = bits.Read(1); + if (info.aspectRatioInfoPresent) { + info.aspectRatioIdc = static_cast(bits.Read(8)); + if (info.aspectRatioIdc == 255) { + info.sarWidth = static_cast(bits.Read(16)); + info.sarHeight = static_cast(bits.Read(16)); + } else { + constexpr std::uint16_t ratios[17][2] = {{0, 0}, {1, 1}, {12, 11}, {10, 11}, {16, 11}, {40, 33}, {24, 11}, {20, 11}, {32, 11}, {80, 33}, {18, 11}, {15, 11}, {64, 33}, {160, 99}, {4, 3}, {3, 2}, {2, 1}}; + if (info.aspectRatioIdc > 16) throw std::runtime_error("reserved HEVC aspect ratio"); + info.sarWidth = ratios[info.aspectRatioIdc][0]; + info.sarHeight = ratios[info.aspectRatioIdc][1]; + } + } + if (bits.Read(1) != 0) bits.Skip(1); + info.videoSignalTypePresent = static_cast(bits.Read(1)); + if (info.videoSignalTypePresent) { + info.videoFormat = static_cast(bits.Read(3)); + info.fullRange = static_cast(bits.Read(1)); + info.colourDescriptionPresent = static_cast(bits.Read(1)); + if (info.colourDescriptionPresent) { + info.colourPrimaries = static_cast(bits.Read(8)); + info.transferCharacteristics = static_cast(bits.Read(8)); + info.matrixCoefficients = static_cast(bits.Read(8)); + } + } + info.chromaLocInfoPresent = static_cast(bits.Read(1)); + if (info.chromaLocInfoPresent) { + info.chromaLocTop = static_cast(bits.Unsigned(5)); + info.chromaLocBottom = static_cast(bits.Unsigned(5)); + } + bits.Skip(1); + info.fieldSequence = static_cast(bits.Read(1)); + info.frameFieldInfoPresent = static_cast(bits.Read(1)); + info.defaultDisplayWindow = bits.Read(1); + if (info.defaultDisplayWindow) { + info.displayLeft = bits.Unsigned(); + info.displayRight = bits.Unsigned(); + info.displayTop = bits.Unsigned(); + info.displayBottom = bits.Unsigned(); + } + info.timingInfoPresent = static_cast(bits.Read(1)); + if (info.timingInfoPresent) { + info.numUnitsInTick = bits.Read(32); + info.timeScale = bits.Read(32); + if (bits.Read(1) != 0) bits.Unsigned(); + if (bits.Read(1) != 0) Hrd(bits, sublayers); + } + if (bits.Read(1) != 0) { + bits.Skip(3); + for (unsigned value = 0; value < 5; ++value) bits.Unsigned(); + } +} + +std::pair ParseSps(Bits& bits) { + Videodec2Hevc::Sps sps; + auto& info = sps.info; + info.thisSize = sizeof(info); + info.isValid = true; + info.videoFormat = 5; + info.colourPrimaries = 2; + info.transferCharacteristics = 2; + info.matrixCoefficients = 2; + sps.vps = bits.Read(4); + const auto sublayers = bits.Read(3); + if (sublayers > 6) throw std::runtime_error("invalid HEVC temporal layers"); + bits.Skip(1); + bool mixed = false; + info.sourceScanType = Profile(bits, sublayers, &mixed, &info); + info.frameFieldInfoPresent = mixed ? 1 : 0; + const auto id = bits.Unsigned(15); + const auto chroma = bits.Unsigned(3); + if (chroma == 3) bits.Skip(1); + info.width = bits.Unsigned(); + info.height = bits.Unsigned(); + info.conformanceWindow = bits.Read(1); + if (info.conformanceWindow) { + info.confLeft = bits.Unsigned(); + info.confRight = bits.Unsigned(); + info.confTop = bits.Unsigned(); + info.confBottom = bits.Unsigned(); + } + info.bitDepthLumaMinus8 = static_cast(bits.Unsigned(8)); + info.bitDepthChromaMinus8 = static_cast(bits.Unsigned(8)); + const auto orderBits = bits.Unsigned(12) + 4; + Ordering(bits, sublayers, &info); + for (unsigned value = 0; value < 6; ++value) bits.Unsigned(); + if (bits.Read(1) != 0 && bits.Read(1) != 0) ScalingLists(bits); + bits.Skip(2); + if (bits.Read(1) != 0) { + bits.Skip(8); + bits.Unsigned(); + bits.Unsigned(); + bits.Skip(1); + } + ShortTermReferences(bits); + if (bits.Read(1) != 0) { + const auto count = bits.Unsigned(32); + for (unsigned reference = 0; reference < count; ++reference) bits.Skip(orderBits + 1); + } + bits.Skip(2); + if (bits.Read(1) != 0) Vui(bits, sublayers, info); + if (bits.Read(1) != 0 && bits.Read(8) != 0) sps.unsupported = "HEVC SPS extension metadata is unsupported"; + if (sublayers != 0) sps.unsupported = "HEVC sub-layer metadata projection is unsupported"; + if (info.conformanceWindow || info.defaultDisplayWindow) sps.unsupported = "HEVC crop/window metadata projection is unsupported"; + return {id, std::move(sps)}; +} + +std::vector PictureTiming(std::span bytes, Videodec2Hevc::PictureInfo& flags, bool& transferAmbiguous) { + std::vector timing; + std::size_t position = 0; + while (position < bytes.size() && !(bytes[position] == 0x80 && position + 1 == bytes.size())) { + const auto number = [&]() { + std::size_t value = 0; + for (;;) { + if (position == bytes.size()) throw std::runtime_error("truncated HEVC SEI header"); + const auto next = bytes[position++]; + if (value > std::numeric_limits::max() - next) throw std::runtime_error("HEVC SEI length overflow"); + value += next; + if (next != 255) return value; + } + }; + const auto type = number(); + const auto size = number(); + if (size > bytes.size() - position) throw std::runtime_error("truncated HEVC SEI payload"); + if (type == 1) { + if (flags.pictureTimingPresent) throw std::runtime_error("multiple HEVC picture-timing messages"); + flags.pictureTimingPresent = 1; + timing.assign(bytes.begin() + position, bytes.begin() + position + size); + } else if (type == 0) flags.bufferingPeriodPresent = 1; + else if (type == 45) flags.framePackingPresent = 1; + else if (type == 147) { + if (size != 1 || (flags.alternativeTransferPresent && flags.preferredTransfer != bytes[position])) { + transferAmbiguous = true; + throw std::runtime_error("invalid or conflicting HEVC alternative-transfer metadata"); + } + flags.alternativeTransferPresent = 1; + flags.preferredTransfer = bytes[position]; + } else if (type != 4 && type != 5 && type != 6 && type != 129 && type != 132 && type != 136 && type != 137 && type != 144) { + throw std::runtime_error("HEVC SEI metadata payload type " + std::to_string(type) + " is unsupported"); + } + position += size; + } + return timing; +} + +} // namespace + +namespace Videodec2Hevc { + +Metadata Parse_nid_no_patch(State& state, std::span annexB) { + Metadata metadata; + metadata.unsupported = state.unsupported; + PictureInfo flags{}; + std::vector starts; + for (std::size_t position = 0; position + 2 < annexB.size(); ++position) { + if (annexB[position] == 0 && annexB[position + 1] == 0 && annexB[position + 2] == 1) { + starts.push_back(position + 3); + position += 2; + } + } + std::optional pictureSps; + std::optional pictureType; + std::optional selectedSps; + std::optional selectedVps; + std::vector timing; + bool transferAmbiguous = false; + for (std::size_t index = 0; index < starts.size(); ++index) { + const auto start = starts[index]; + auto end = index + 1 == starts.size() ? annexB.size() : starts[index + 1] - 3; + while (end > start && annexB[end - 1] == 0) --end; + unsigned type = 64; + try { + if (end - start < 2) throw std::runtime_error("truncated HEVC NAL header"); + type = (annexB[start] >> 1) & 63; + if ((annexB[start] & 0x81) != 0 || (annexB[start + 1] & 0xf8) != 0) + throw std::runtime_error("multilayer HEVC picture metadata is unsupported"); + if ((annexB[start + 1] & 7) == 0) throw std::runtime_error("invalid HEVC temporal identifier"); + const auto bytes = Rbsp(annexB.subspan(start + 2, end - start - 2)); + Bits bits(bytes); + if (type == 32) { + flags.vpsPresent = 1; + auto [id, vps] = ParseVps(bits); + vps.bytes = bytes; + if (state.vps[id]) { + vps.unsupported = state.vps[id]->unsupported; + if (state.vps[id]->bytes != bytes) vps.unsupported = "HEVC VPS replacement metadata is unsupported until decoder recreation"; + } + state.vps[id] = std::move(vps); + } else if (type == 33) { + flags.spsPresent = 1; + auto [id, sps] = ParseSps(bits); + sps.bytes = bytes; + if (state.sps[id]) { + if (!state.sps[id]->unsupported.empty()) sps.unsupported = state.sps[id]->unsupported; + if (state.sps[id]->bytes != bytes) sps.unsupported = "HEVC SPS replacement metadata is unsupported until decoder recreation"; + } + state.sps[id] = std::move(sps); + } else if (type == 34) { + flags.ppsPresent = 1; + const auto id = bits.Unsigned(63); + Pps pps{bits.Unsigned(15), bytes, {}}; + if (state.pps[id]) { + pps.unsupported = state.pps[id]->unsupported; + if (state.pps[id]->bytes != bytes) pps.unsupported = "HEVC PPS replacement metadata is unsupported until decoder recreation"; + } + state.pps[id] = std::move(pps); + } else if (type == 35) flags.delimiterPresent = 1; + else if (type == 36) flags.endSequencePresent = 1; + else if (type == 37) flags.endStreamPresent = 1; + else if (type == 39 || type == 40) { + auto message = PictureTiming(bytes, flags, transferAmbiguous); + if (!message.empty()) { + if (type != 39 || !timing.empty()) throw std::runtime_error("unsupported HEVC picture-timing association"); + timing = std::move(message); + } + } else if (type < 32) { + const bool first = bits.Read(1) != 0; + if (type >= 16 && type <= 23) bits.Skip(1); + const auto pps = bits.Unsigned(63); + if (!state.pps[pps]) throw std::runtime_error("HEVC picture parameter set is unavailable"); + if (!state.pps[pps]->unsupported.empty() && metadata.unsupported.empty()) metadata.unsupported = state.pps[pps]->unsupported; + const auto sps = state.pps[pps]->sps; + if (pictureSps && (first || *pictureSps != sps)) throw std::runtime_error("multiple HEVC pictures in one access unit"); + if (pictureType && *pictureType != type) throw std::runtime_error("mixed HEVC slice NAL types in one access unit"); + if (!pictureSps) { + if (!state.sps[sps]) throw std::runtime_error("HEVC sequence parameter set is unavailable"); + selectedSps = state.sps[sps]; + selectedVps = state.vps[selectedSps->vps]; + pictureType = type; + flags.idr = type == 19 || type == 20; + flags.irap = type >= 16 && type <= 23; + } + pictureSps = sps; + } else if (type == 38) flags.fillerPresent = 1; + else throw std::runtime_error("reserved HEVC NAL metadata is unsupported"); + } catch (const std::runtime_error& error) { + if (metadata.unsupported.empty()) metadata.unsupported = error.what(); + if (type >= 32 && type <= 34 && state.unsupported.empty()) state.unsupported = error.what(); + } + } + if (!selectedSps) { + if (flags.endSequencePresent || flags.endStreamPresent) state.afterEnd = true; + if ((flags.alternativeTransferPresent || transferAmbiguous) && state.unsupported.empty()) + state.unsupported = "HEVC alternative transfer in a pictureless access unit is unsupported"; + if (metadata.unsupported.empty()) metadata.unsupported = "HEVC sequence parameter set is unavailable"; + return metadata; + } + const auto& sps = *selectedSps; + metadata.info = sps.info; + const bool newSequence = !state.hadPicture || (*pictureType >= 16 && *pictureType <= 20) || (*pictureType == 21 && state.afterEnd); + if (newSequence) { + state.preferredTransfer.reset(); + state.transferUnsupported.clear(); + } + if ((state.afterEnd || !state.hadPicture) && !flags.irap) + state.transferUnsupported = "HEVC non-IRAP continuation after reset/flush is unsupported"; + if (transferAmbiguous) state.transferUnsupported = "ambiguous HEVC alternative-transfer metadata"; + if (flags.alternativeTransferPresent) { + if (state.preferredTransfer && *state.preferredTransfer != flags.preferredTransfer) + state.transferUnsupported = "conflicting HEVC alternative transfers within one CLVS"; + state.preferredTransfer = flags.preferredTransfer; + } + if (state.preferredTransfer) metadata.info.preferredTransfer = *state.preferredTransfer; + if (!state.transferUnsupported.empty() && metadata.unsupported.empty()) metadata.unsupported = state.transferUnsupported; + state.hadPicture = true; + state.afterEnd = flags.endSequencePresent || flags.endStreamPresent; + if (!sps.unsupported.empty() && metadata.unsupported.empty()) metadata.unsupported = sps.unsupported; + if (!selectedVps) { + if (metadata.unsupported.empty()) metadata.unsupported = "HEVC video parameter set is unavailable"; + } else if (!metadata.info.timingInfoPresent && selectedVps->timing) { + if (metadata.unsupported.empty()) metadata.unsupported = "HEVC VPS-only timing projection is unsupported"; + } + if (selectedVps && !selectedVps->unsupported.empty() && metadata.unsupported.empty()) metadata.unsupported = selectedVps->unsupported; + if (metadata.info.frameFieldInfoPresent) { + try { + if (timing.empty()) throw std::runtime_error("HEVC picture timing is unavailable"); + Bits bits(timing); + metadata.info.picStruct = bits.Read(4); + metadata.info.sourceScanType = bits.Read(2); + metadata.info.duplicate = bits.Read(1); + if (metadata.info.picStruct > 12 || metadata.info.sourceScanType > 2) + throw std::runtime_error("reserved HEVC picture-timing metadata"); + } catch (const std::runtime_error& error) { + if (metadata.unsupported.empty()) metadata.unsupported = error.what(); + } + } + metadata.info.vpsPresent = flags.vpsPresent; + metadata.info.spsPresent = flags.spsPresent; + metadata.info.ppsPresent = flags.ppsPresent; + metadata.info.delimiterPresent = flags.delimiterPresent; + metadata.info.endSequencePresent = flags.endSequencePresent; + metadata.info.endStreamPresent = flags.endStreamPresent; + metadata.info.fillerPresent = flags.fillerPresent; + metadata.info.pictureTimingPresent = flags.pictureTimingPresent; + metadata.info.bufferingPeriodPresent = flags.bufferingPeriodPresent; + metadata.info.framePackingPresent = flags.framePackingPresent; + metadata.info.alternativeTransferPresent = flags.alternativeTransferPresent; + metadata.info.idr = flags.idr; + metadata.info.irap = flags.irap; + return metadata; +} + +} // namespace Videodec2Hevc diff --git a/core/libs/prx/libSceVideodec2/HevcMetadata.hpp b/core/libs/prx/libSceVideodec2/HevcMetadata.hpp new file mode 100644 index 0000000000..f9a980fef8 --- /dev/null +++ b/core/libs/prx/libSceVideodec2/HevcMetadata.hpp @@ -0,0 +1,61 @@ +#ifndef CORE_LIBS_PRX_LIBSCEVIDEODEC2_HEVCMETADATA_HPP +#define CORE_LIBS_PRX_LIBSCEVIDEODEC2_HEVCMETADATA_HPP + +#include "prx/libSceVideodec2/include/Api.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace Videodec2Hevc { + +using PictureInfo = SceVideodec2::HevcPictureInfo; + +static_assert(sizeof(PictureInfo) == 184 && offsetof(PictureInfo, width) == 40 && offsetof(PictureInfo, picStruct) == 80 && offsetof(PictureInfo, vpsPresent) == 136); +static_assert(offsetof(PictureInfo, fillerPresent) == 142 && offsetof(PictureInfo, idr) == 147 && offsetof(PictureInfo, profile) == 151 && offsetof(PictureInfo, cropping) == 165 && offsetof(PictureInfo, cropBottom) == 180); + +struct Metadata { + PictureInfo info{}; + std::string unsupported; +}; + +struct Vps { + bool timing = false; + std::uint32_t numUnitsInTick = 0; + std::uint32_t timeScale = 0; + std::vector bytes; + std::string unsupported; +}; + +struct Sps { + PictureInfo info{}; + unsigned vps = 0; + std::string unsupported; + std::vector bytes; +}; + +struct Pps { + unsigned sps = 0; + std::vector bytes; + std::string unsupported; +}; + +struct State { + std::array, 16> vps; + std::array, 16> sps; + std::array, 64> pps; + std::string unsupported; + std::optional preferredTransfer; + std::string transferUnsupported; + bool hadPicture = false; + bool afterEnd = false; +}; + +Metadata Parse_nid_no_patch(State& state, std::span annexB); + +} // namespace Videodec2Hevc + +#endif diff --git a/core/libs/prx/libSceVideodec2/Unimplemented.cpp b/core/libs/prx/libSceVideodec2/Unimplemented.cpp index 62a9691332..dfe81fd3de 100644 --- a/core/libs/prx/libSceVideodec2/Unimplemented.cpp +++ b/core/libs/prx/libSceVideodec2/Unimplemented.cpp @@ -52,10 +52,6 @@ int APS5_VABI sceVideodec2QueryHevcDecoderMemoryInfo() { NotImplemented_nid_no_patch(__func__); } -int APS5_VABI sceVideodec2GetHevcPictureInfo() { - NotImplemented_nid_no_patch(__func__); -} - int APS5_VABI sceVideodec2InitLibrary() { NotImplemented_nid_no_patch(__func__); } diff --git a/core/libs/prx/libSceVideodec2/include/Api.hpp b/core/libs/prx/libSceVideodec2/include/Api.hpp index b338ef37c4..8b00e719f1 100644 --- a/core/libs/prx/libSceVideodec2/include/Api.hpp +++ b/core/libs/prx/libSceVideodec2/include/Api.hpp @@ -155,6 +155,88 @@ struct Vp9PictureInfo { } +namespace SceVideodec2 { + +struct HevcPictureInfo { + std::uint64_t thisSize; + bool isValid; + std::uint64_t ptsData; + std::uint64_t dtsData; + std::uint64_t attachedData; + std::uint32_t width; + std::uint32_t height; + std::uint8_t bitDepthLumaMinus8; + std::uint8_t bitDepthChromaMinus8; + std::uint8_t timingInfoPresent; + std::uint32_t numUnitsInTick; + std::uint32_t timeScale; + std::uint32_t aspectRatioInfoPresent; + std::uint8_t aspectRatioIdc; + std::uint16_t sarWidth; + std::uint16_t sarHeight; + std::uint8_t videoSignalTypePresent; + std::uint8_t videoFormat; + std::uint8_t fullRange; + std::uint8_t colourDescriptionPresent; + std::uint8_t colourPrimaries; + std::uint8_t transferCharacteristics; + std::uint8_t matrixCoefficients; + std::uint8_t frameFieldInfoPresent; + std::uint32_t picStruct; + std::uint32_t sourceScanType; + std::uint32_t duplicate; + std::uint32_t conformanceWindow; + std::uint32_t confLeft; + std::uint32_t confRight; + std::uint32_t confTop; + std::uint32_t confBottom; + std::uint32_t defaultDisplayWindow; + std::uint32_t displayLeft; + std::uint32_t displayRight; + std::uint32_t displayTop; + std::uint32_t displayBottom; + std::uint8_t chromaLocInfoPresent; + std::uint8_t chromaLocTop; + std::uint8_t chromaLocBottom; + std::uint8_t fieldSequence; + std::uint8_t vpsPresent; + std::uint8_t spsPresent; + std::uint8_t ppsPresent; + std::uint8_t delimiterPresent; + std::uint8_t endSequencePresent; + std::uint8_t endStreamPresent; + std::uint8_t fillerPresent; + std::uint8_t pictureTimingPresent; + std::uint8_t bufferingPeriodPresent; + std::uint8_t framePackingPresent; + std::uint8_t alternativeTransferPresent; + std::uint8_t idr; + std::uint8_t irap; + std::uint8_t profileSpace; + std::uint8_t tier; + std::uint8_t profile; + std::uint8_t progressiveSource; + std::uint8_t interlacedSource; + std::uint8_t frameOnlyConstraint; + std::uint8_t level; + std::uint8_t subLayerProfilePresent; + std::uint8_t subLayerLevelPresent; + std::uint8_t subLayerProfileSpace; + std::uint8_t subLayerTier; + std::uint8_t subLayerProfile; + std::uint8_t subLayerLevel; + std::uint8_t subLayerOrderingPresent; + std::uint8_t maxDecPicBufferingMinus1; + std::uint8_t preferredTransfer; + std::uint8_t cropping; + std::uint32_t cropLeft; + std::uint32_t cropRight; + std::uint32_t cropTop; + std::uint32_t cropBottom; +}; + +} + extern "C" { int APS5_VABI sceVideodec2QueryComputeMemoryInfo_nid_postfix(SceVideodec2::ComputeMemoryInfo* info); @@ -181,7 +263,7 @@ int APS5_VABI sceVideodec2PresetDecoderConfigInfoVp9(); int APS5_VABI sceVideodec2CreateDecoderBid(); int APS5_VABI sceVideodec2CreateHevcDecoder(); int APS5_VABI sceVideodec2QueryHevcDecoderMemoryInfo(); -int APS5_VABI sceVideodec2GetHevcPictureInfo(); +int APS5_VABI sceVideodec2GetHevcPictureInfo_nid_postfix(const SceVideodec2::OutputInfo* output, SceVideodec2::HevcPictureInfo* info); int APS5_VABI sceVideodec2InitLibrary(); } diff --git a/core/libs/tests/GuestHevcMetadata.cpp b/core/libs/tests/GuestHevcMetadata.cpp new file mode 100644 index 0000000000..58f8debbc1 --- /dev/null +++ b/core/libs/tests/GuestHevcMetadata.cpp @@ -0,0 +1,399 @@ +#include "prx/libSceVideodec2/HevcMetadata.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "HevcFixture.hpp" +#include "HevcMetadataFixture.hpp" +#include +#include +#include +#include +#include +#include +#include + +using namespace SceVideodec2; + + +namespace { + +constexpr std::uint32_t codecHevc = 974921; + +void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "HEVC metadata: %s\n", message); + std::abort(); +} + +template +void Rejects(TCall call, const char* message) { + try { call(); } + catch (const std::runtime_error&) { return; } + Require(false, message); +} + +struct Decoder { + std::uint64_t handle = 0; + DecoderMemoryInfo memory{sizeof(memory)}; + std::vector buffer; + FrameBuffer frame{sizeof(frame)}; + OutputInfo output{sizeof(output)}; + + explicit Decoder(unsigned profile) { + const DecoderConfigInfo config{sizeof(config), 1, codecHevc, profile, 0, 64, 48, 4, 1}; + Require(sceVideodec2QueryDecoderMemoryInfo_nid_postfix(&config, &memory) == 0, "query memory"); + Require(sceVideodec2CreateDecoder_nid_postfix(&config, &memory, &handle) == 0, "create decoder"); + buffer.resize(memory.maxFrameBufferSize); + frame.frameBuffer = buffer.data(); + frame.frameBufferSize = buffer.size(); + } + + ~Decoder() { if (handle) sceVideodec2DeleteDecoder_nid_postfix(handle); } + + void Decode(const std::vector& bytes, std::uint64_t tag) { + const InputData input{sizeof(input), bytes.data(), bytes.size(), 1000 + tag, 2000 + tag, 3000 + tag}; + Require(sceVideodec2Decode_nid_postfix(handle, &input, &frame, &output) == 0, "decode unit"); + } + + Videodec2Hevc::PictureInfo Query() { + struct Guarded { + Videodec2Hevc::PictureInfo info{sizeof(info)}; + std::uint64_t guard = 0x123456789abcdef0; + } result; + Require(sceVideodec2GetHevcPictureInfo_nid_postfix(&output, &result.info) == 0, "query HEVC info"); + Require(result.info.thisSize == sizeof(result.info) && result.info.isValid, "valid HEVC picture"); + Require(result.guard == 0x123456789abcdef0, "HEVC info overwrote adjacent memory"); + Videodec2Hevc::PictureInfo generic{sizeof(generic)}; + Require(sceVideodec2GetPictureInfo_nid_postfix(&output, reinterpret_cast(&generic), nullptr) == 0, "generic HEVC query"); + Require(generic.ptsData == result.info.ptsData && generic.dtsData == result.info.dtsData && generic.attachedData == result.info.attachedData && generic.width == result.info.width && generic.timeScale == result.info.timeScale && generic.spsPresent == result.info.spsPresent, "generic and dedicated info disagree"); + return result.info; + } +}; + +void RejectUnchanged(Decoder& decoder); + +void CheckCommon(const Videodec2Hevc::PictureInfo& info, unsigned bitDepth) { + Require(info.width == 64 && info.height == 40, "coded luma dimensions"); + Require(info.bitDepthLumaMinus8 == bitDepth && info.bitDepthChromaMinus8 == bitDepth, "source bit depths"); + Require(info.timingInfoPresent == 1 && info.numUnitsInTick == 1 && info.timeScale == 30, "VUI timing syntax"); + Require(info.aspectRatioInfoPresent == 1 && info.aspectRatioIdc == 1 && info.sarWidth == 1 && info.sarHeight == 1, "VUI sample aspect ratio"); + Require(info.videoSignalTypePresent == 1 && info.videoFormat == 5 && info.fullRange == 0 && info.colourDescriptionPresent == 0 && info.colourPrimaries == 2 && info.transferCharacteristics == 2 && info.matrixCoefficients == 2, "signalled video format and inferred colours"); + Require(info.frameFieldInfoPresent == 0 && info.picStruct == 0 && info.sourceScanType == 1 && info.duplicate == 0, "PTL progressive scan inference"); + Require(info.defaultDisplayWindow == 0 && info.displayLeft == 0 && info.displayRight == 0 && info.displayTop == 0 && info.displayBottom == 0, "default display window inference"); + Require(info.chromaLocInfoPresent == 0 && info.chromaLocTop == 0 && info.chromaLocBottom == 0 && info.fieldSequence == 0, "inferred chroma location and field sequence"); + Require(info.profileSpace == 0 && info.tier == 0 && info.profile == (bitDepth == 0 ? 1 : 2) && info.level == 30 && info.progressiveSource == 1 && info.interlacedSource == 0 && info.frameOnlyConstraint == 1, "general profile-tier-level syntax"); + Require(info.subLayerProfilePresent == 0 && info.subLayerLevelPresent == 0 && info.subLayerProfileSpace == 0 && info.subLayerTier == 0 && info.subLayerProfile == 0 && info.subLayerLevel == 0 && info.subLayerOrderingPresent == 1 && info.maxDecPicBufferingMinus1 == 4, "single-layer PTL and DPB syntax"); + Require(info.cropping == 0 && info.cropLeft == 0 && info.cropRight == 0 && info.cropTop == 0 && info.cropBottom == 0, "absent tail cropping fields"); +} + +void Clips(bool alternativeTransfer = false) { + Decoder decoder(1); + std::size_t pictures = 0; + const auto take = [&] { + if (!decoder.output.isValid) return; + const auto info = decoder.Query(); + CheckCommon(info, 0); + constexpr std::array order{0, 3, 2, 1, 5, 4}; + Require(pictures < order.size(), "too many pictures"); + const auto tag = order[pictures]; + Require(info.ptsData == 1000 + tag && info.dtsData == 2000 + tag && info.attachedData == 3000 + tag, "per-picture access-unit association through B-frame reordering"); + Require(info.vpsPresent == (pictures == 0) && info.spsPresent == (pictures == 0) && info.ppsPresent == (pictures == 0) && info.delimiterPresent == 1, "presence is from the emitted picture's unit"); + Require(info.conformanceWindow == 0 && info.confLeft == 0 && info.confRight == 0 && info.confTop == 0 && info.confBottom == 0, "no conformance window"); + Require(info.idr == (pictures == 0) && info.irap == (pictures == 0) && info.fillerPresent == 0, "per-picture IDR/IRAP and filler flags"); + Require(info.alternativeTransferPresent == (alternativeTransfer && pictures == 0) && info.preferredTransfer == (alternativeTransfer ? 16 : 0), "alternative transfer persists through B-frame reordering"); + ++pictures; + }; + auto units = HevcFixture::hevcUnits(HevcFixture::HevcStream.data(), HevcFixture::HevcStream.size()); + if (alternativeTransfer) { + const std::array sei{0, 0, 1, 0x4e, 1, 147, 1, 16, 0x80}; + units[0].insert(units[0].begin() + 7, sei.begin(), sei.end()); + } + for (std::size_t unit = 0; unit < units.size(); ++unit) { + decoder.Decode(units[unit], unit); + take(); + } + for (std::size_t drain = 0; drain <= units.size(); ++drain) { + Require(sceVideodec2Flush_nid_postfix(decoder.handle, &decoder.frame, &decoder.output) == 0, "drain clip"); + if (!decoder.output.isValid) break; + take(); + } + Require(pictures == 6, "decoded all six picture metadata records"); + Videodec2Hevc::PictureInfo invalid{sizeof(invalid)}; + invalid.isValid = true; + Require(sceVideodec2Reset_nid_postfix(decoder.handle) == 0, "reset"); + Require(sceVideodec2GetHevcPictureInfo_nid_postfix(&decoder.output, &invalid) == 0 && !invalid.isValid, "reset releases picture metadata"); + invalid.thisSize = 8; + invalid.isValid = true; + Rejects([&] { sceVideodec2GetPictureInfo_nid_postfix(&decoder.output, reinterpret_cast(&invalid), nullptr); }, "unowned generic HEVC query accepted an undersized destination"); + Require(invalid.isValid, "unowned undersized generic query changed destination"); + decoder.Decode(units.front(), 99); + Require(sceVideodec2Flush_nid_postfix(decoder.handle, &decoder.frame, &decoder.output) == 0 && decoder.output.isValid, "decode after reset"); + Require(decoder.Query().attachedData == 3099, "new metadata after reset"); +} + +void Single(std::span stream, unsigned bitDepth, unsigned bottomCrop, bool delimiter) { + Decoder decoder(bitDepth == 0 ? 1 : 2); + decoder.Decode({stream.begin(), stream.end()}, 7); + if (!decoder.output.isValid) Require(sceVideodec2Flush_nid_postfix(decoder.handle, &decoder.frame, &decoder.output) == 0, "single-picture drain"); + if (bottomCrop != 0) { + Require(decoder.output.isValid, "cropped picture remains decodable"); + RejectUnchanged(decoder); + return; + } + const auto info = decoder.Query(); + CheckCommon(info, bitDepth); + Require(info.attachedData == 3007 && info.vpsPresent == 1 && info.spsPresent == 1 && info.ppsPresent == 1 && info.delimiterPresent == delimiter, "single-picture tags and presence"); + Require(info.idr == 1 && info.irap == 1 && info.preferredTransfer == 0 && info.pictureTimingPresent == 0, "single-picture tail metadata"); + Require(info.conformanceWindow == (bottomCrop != 0) && info.confBottom == bottomCrop && info.confLeft == 0 && info.confRight == 0 && info.confTop == 0, "raw chroma-unit conformance offsets"); + auto badOutput = decoder.output; + auto badInfo = info; + Rejects([&] { sceVideodec2GetHevcPictureInfo_nid_postfix(nullptr, &badInfo); }, "null output accepted"); + Rejects([&] { sceVideodec2GetHevcPictureInfo_nid_postfix(&badOutput, nullptr); }, "null destination accepted"); + badInfo.thisSize = sizeof(badInfo) - 1; + Rejects([&] { sceVideodec2GetHevcPictureInfo_nid_postfix(&badOutput, &badInfo); }, "undersized destination accepted"); + badInfo = info; + badOutput.thisSize = 8; + Rejects([&] { sceVideodec2GetHevcPictureInfo_nid_postfix(&badOutput, &badInfo); }, "undersized output accepted"); + badOutput = decoder.output; + badOutput.codecType = 1; + Rejects([&] { sceVideodec2GetHevcPictureInfo_nid_postfix(&badOutput, &badInfo); }, "another codec accepted"); + std::array before; + std::memcpy(before.data(), &badInfo, before.size()); + Rejects([&] { sceVideodec2GetAvcPictureInfo_nid_postfix(&decoder.output, reinterpret_cast(&badInfo), nullptr); }, "AVC-specific getter accepted HEVC with a large buffer"); + Require(std::memcmp(before.data(), &badInfo, before.size()) == 0, "AVC-specific failure modified its output"); + Rejects([&] { sceVideodec2GetAvcPictureInfo_nid_postfix(&badOutput, reinterpret_cast(&badInfo), nullptr); }, "AVC-specific getter trusted forged codec tag"); + Require(std::memcmp(before.data(), &badInfo, before.size()) == 0, "forged codec tag changed AVC failure output"); +} + +void UnsupportedPicture() { + Decoder decoder(1); + auto units = HevcFixture::hevcUnits(HevcFixture::HevcStream.data(), HevcFixture::HevcStream.size()); + const std::array unknownSei{0, 0, 1, 0x4e, 1, 200, 1, 0x42, 0x80}; + units[0].insert(units[0].begin() + 7, unknownSei.begin(), unknownSei.end()); + decoder.Decode(units[0], 0); + Require(sceVideodec2Flush_nid_postfix(decoder.handle, &decoder.frame, &decoder.output) == 0 && decoder.output.isValid, "unsupported metadata retains decoded output"); + Videodec2Hevc::PictureInfo untouched; + std::memset(&untouched, 0x5a, sizeof(untouched)); + untouched.thisSize = sizeof(untouched); + std::array before; + std::memcpy(before.data(), &untouched, before.size()); + Rejects([&] { sceVideodec2GetHevcPictureInfo_nid_postfix(&decoder.output, &untouched); }, "unknown SEI metadata did not throw"); + Require(std::memcmp(before.data(), &untouched, before.size()) == 0, "unsupported query changed destination"); + Rejects([&] { sceVideodec2GetPictureInfo_nid_postfix(&decoder.output, reinterpret_cast(&untouched), nullptr); }, "generic unknown metadata did not throw"); + Require(std::memcmp(before.data(), &untouched, before.size()) == 0, "generic unsupported query changed destination"); + Require(sceVideodec2Reset_nid_postfix(decoder.handle) == 0, "clear unsupported metadata on reset"); + const auto clean = HevcFixture::hevcUnits(HevcFixture::HevcStream.data(), HevcFixture::HevcStream.size()); + decoder.Decode(clean[0], 80); + Require(sceVideodec2Flush_nid_postfix(decoder.handle, &decoder.frame, &decoder.output) == 0 && decoder.output.isValid, "clean replacement picture"); + Require(decoder.Query().ptsData == 1080, "replacement metadata does not retain unsupported reason"); + const auto previous = decoder.output; + Require(sceVideodec2DeleteDecoder_nid_postfix(decoder.handle) == 0, "delete owner"); + decoder.handle = 0; + untouched = {sizeof(untouched)}; + untouched.isValid = true; + Require(sceVideodec2GetHevcPictureInfo_nid_postfix(&previous, &untouched) == 0 && !untouched.isValid, "deleted owner releases metadata"); +} + +std::vector Replacement(unsigned wanted) { + const auto& original = HevcFixture::HevcMain10Stream; + std::vector starts; + for (std::size_t index = 0; index + 2 < original.size(); ++index) + if (original[index] == 0 && original[index + 1] == 0 && original[index + 2] == 1) starts.push_back(index + 3); + for (std::size_t index = 0; index < starts.size(); ++index) { + const auto header = starts[index]; + if (((original[header] >> 1) & 63) != wanted) continue; + const auto end = index + 1 == starts.size() ? original.size() : starts[index + 1] - 3; + std::vector bits; + unsigned zeros = 0; + for (std::size_t byte = header + 2; byte < end; ++byte) { + const auto value = original[byte]; + if (zeros == 2 && value == 3) { zeros = 0; continue; } + for (int bit = 7; bit >= 0; --bit) bits.push_back(((value >> bit) & 1) != 0); + zeros = value == 0 ? zeros + 1 : 0; + } + while (!bits.empty() && !bits.back()) bits.pop_back(); + Require(bits.size() >= 3, "parameter-set fixture trailing bits"); + bits.pop_back(); + Require(!bits.back(), "fixture extension flag is zero"); + if (wanted == 32) { + bits.pop_back(); + Require(!bits.back(), "fixture VPS timing flag is zero"); + bits.pop_back(); + bits.push_back(true); + for (const unsigned value : std::array{1u, 30u}) + for (int bit = 31; bit >= 0; --bit) bits.push_back(((value >> bit) & 1) != 0); + bits.insert(bits.end(), {false, true, false}); + } else { + bits.back() = true; + bits.insert(bits.end(), 8, false); + } + bits.push_back(true); + while (bits.size() % 8 != 0) bits.push_back(false); + std::vector encoded; + zeros = 0; + for (std::size_t byte = 0; byte < bits.size(); byte += 8) { + std::uint8_t value = 0; + for (unsigned bit = 0; bit < 8; ++bit) value = static_cast((value << 1) | bits[byte + bit]); + if (zeros == 2 && value <= 3) { encoded.push_back(3); zeros = 0; } + encoded.push_back(value); + zeros = value == 0 ? zeros + 1 : 0; + } + std::vector result(original.begin(), original.begin() + header + 2); + result.insert(result.end(), encoded.begin(), encoded.end()); + result.insert(result.end(), original.begin() + end, original.end()); + return result; + } + Require(false, "fixture parameter set not found"); + return {}; +} + +void RejectUnchanged(Decoder& decoder) { + Videodec2Hevc::PictureInfo info; + std::memset(&info, 0x6b, sizeof(info)); + info.thisSize = sizeof(info); + std::array before; + std::memcpy(before.data(), &info, sizeof(info)); + Rejects([&] { sceVideodec2GetHevcPictureInfo_nid_postfix(&decoder.output, &info); }, "ambiguous replacement metadata succeeded"); + Require(std::memcmp(before.data(), &info, sizeof(info)) == 0, "ambiguous replacement query mutated output"); +} + +void ParameterReplacements() { + const std::vector original(HevcFixture::HevcMain10Stream.begin(), HevcFixture::HevcMain10Stream.end()); + for (const unsigned type : std::array{0u, 32u, 33u, 34u}) { + Decoder decoder(2); + decoder.Decode(original, 10); + Require(sceVideodec2Flush_nid_postfix(decoder.handle, &decoder.frame, &decoder.output) == 0 && decoder.output.isValid, "initial replacement-test picture"); + Require(decoder.Query().attachedData == 3010, "initial parameter metadata"); + const auto previous = decoder.output; + std::vector nextBuffer(decoder.buffer.size()); + decoder.frame.frameBuffer = nextBuffer.data(); + decoder.Decode(type == 0 ? original : Replacement(type), 20); + Require(sceVideodec2Flush_nid_postfix(decoder.handle, &decoder.frame, &decoder.output) == 0 && decoder.output.isValid, "decode repeated or replaced parameter sets"); + if (type == 0) Require(decoder.Query().attachedData == 3020, "identical repeated headers remain supported"); + else RejectUnchanged(decoder); + Videodec2Hevc::PictureInfo old{sizeof(old)}; + Require(sceVideodec2GetHevcPictureInfo_nid_postfix(&previous, &old) == 0 && old.isValid && old.attachedData == 3010, "previous picture metadata remains immutable after parameter updates"); + if (type != 0) { + Require(sceVideodec2Reset_nid_postfix(decoder.handle) == 0, "reset after replacement"); + decoder.Decode(original, 30); + Require(sceVideodec2Flush_nid_postfix(decoder.handle, &decoder.frame, &decoder.output) == 0 && decoder.output.isValid, "decode after replacement reset"); + RejectUnchanged(decoder); + } + } + Decoder recreated(2); + recreated.Decode(original, 40); + Require(sceVideodec2Flush_nid_postfix(recreated.handle, &recreated.frame, &recreated.output) == 0 && recreated.output.isValid, "recreated backend picture"); + Require(recreated.Query().attachedData == 3040, "new backend context recovers supported metadata"); +} + +void ExtendedMetadata() { + for (const bool interlaced : {false, true}) { + const auto bytes = interlaced ? std::span(HevcMetadataFixture::Interlaced) : std::span(HevcMetadataFixture::Rich); + Decoder decoder(1); + decoder.Decode({bytes.begin(), bytes.end()}, 50); + if (!decoder.output.isValid) Require(sceVideodec2Flush_nid_postfix(decoder.handle, &decoder.frame, &decoder.output) == 0, "extended metadata flush"); + const auto info = decoder.Query(); + Require(info.width == 64 && info.height == 48 && info.numUnitsInTick == 1 && info.timeScale == 24, "extended dimensions and timing"); + if (interlaced) { + Require(info.fieldSequence == 1 && info.frameFieldInfoPresent == 1 && info.picStruct == 1 && info.sourceScanType == 0 && info.duplicate == 0, "picture-timing SEI and field metadata"); + Require(info.pictureTimingPresent == 1 && info.progressiveSource == 0 && info.interlacedSource == 1 && info.frameOnlyConstraint == 0, "interlaced PTL and SEI presence tail"); + } else { + Require(info.aspectRatioIdc == 2 && info.sarWidth == 12 && info.sarHeight == 11, "table-derived non-square sample aspect"); + Require(info.videoSignalTypePresent == 1 && info.videoFormat == 1 && info.fullRange == 1 && info.colourDescriptionPresent == 1 && info.colourPrimaries == 9 && info.transferCharacteristics == 16 && info.matrixCoefficients == 9, "explicit HDR color syntax"); + Require(info.chromaLocInfoPresent == 1 && info.chromaLocTop == 3 && info.chromaLocBottom == 3, "explicit chroma sample locations"); + Require(info.defaultDisplayWindow == 0 && info.cropping == 0, "unwindowed rich picture"); + } + Require(info.profile == 4 && info.level == 30 && info.subLayerOrderingPresent == 1 && info.maxDecPicBufferingMinus1 == 2 && info.idr == 1 && info.irap == 1, "extended PTL and DPB tail"); + } + Decoder windowed(1); + windowed.Decode({HevcMetadataFixture::Windowed.begin(), HevcMetadataFixture::Windowed.end()}, 51); + if (!windowed.output.isValid) Require(sceVideodec2Flush_nid_postfix(windowed.handle, &windowed.frame, &windowed.output) == 0, "windowed picture flush"); + Require(windowed.output.isValid, "windowed picture remains decodable"); + RejectUnchanged(windowed); +} + +void CabacPadding() { + Decoder decoder(2); + std::vector padded(HevcFixture::HevcMain10Stream.begin(), HevcFixture::HevcMain10Stream.end()); + padded.insert(padded.end(), {0, 0, 3}); + decoder.Decode(padded, 60); + Require(sceVideodec2Flush_nid_postfix(decoder.handle, &decoder.frame, &decoder.output) == 0 && decoder.output.isValid, "terminal CABAC prevention byte decode"); + Require(decoder.Query().attachedData == 3060, "terminal CABAC prevention byte metadata"); + Videodec2Hevc::State state; + padded.back() = 2; + const auto invalid = Videodec2Hevc::Parse_nid_no_patch(state, padded); + Require(!invalid.unsupported.empty(), "missing emulation prevention was accepted"); +} + +void MalformedParameterHistory() { + const std::vector original(HevcFixture::HevcMain10Stream.begin(), HevcFixture::HevcMain10Stream.end()); + Decoder decoder(2); + decoder.Decode(original, 70); + Require(sceVideodec2Flush_nid_postfix(decoder.handle, &decoder.frame, &decoder.output) == 0 && decoder.output.isValid, "initial malformed-history picture"); + Require(decoder.Query().attachedData == 3070, "initial malformed-history metadata"); + std::vector malformed{0, 0, 1, 0x44, 1, 0x80}; + malformed.insert(malformed.end(), original.begin(), original.end()); + bool failed = false; + try { decoder.Decode(malformed, 71); } + catch (const std::runtime_error&) { failed = true; } + if (!failed) { + Require(sceVideodec2Flush_nid_postfix(decoder.handle, &decoder.frame, &decoder.output) == 0 && decoder.output.isValid, "malformed parameter output"); + RejectUnchanged(decoder); + } + Require(sceVideodec2Reset_nid_postfix(decoder.handle) == 0, "reset malformed parameter state"); + decoder.Decode(original, 72); + Require(sceVideodec2Flush_nid_postfix(decoder.handle, &decoder.frame, &decoder.output) == 0 && decoder.output.isValid, "clean decode after malformed parameter reset"); + RejectUnchanged(decoder); +} + +void TransferAssociation() { + const auto original = HevcFixture::hevcUnits(HevcFixture::HevcStream.data(), HevcFixture::HevcStream.size()); + const std::array transfer{0, 0, 1, 0x4e, 1, 147, 1, 16, 0x80}; + { + Decoder decoder(1); + auto units = original; + units[0].insert(units[0].begin() + 7, transfer.begin(), transfer.end()); + auto conflict = transfer; + conflict[7] = 18; + units[0].insert(units[0].begin() + 7, conflict.begin(), conflict.end()); + unsigned pictures = 0; + for (std::size_t index = 0; index < units.size(); ++index) { + decoder.Decode(units[index], index); + if (decoder.output.isValid) { RejectUnchanged(decoder); ++pictures; } + } + for (;;) { + Require(sceVideodec2Flush_nid_postfix(decoder.handle, &decoder.frame, &decoder.output) == 0, "conflicting transfer drain"); + if (!decoder.output.isValid) break; + RejectUnchanged(decoder); + ++pictures; + } + Require(pictures == 6, "conflicting transfers retain all decoded pictures"); + Require(sceVideodec2Reset_nid_postfix(decoder.handle) == 0, "reset conflicting transfer sequence"); + decoder.Decode(original[0], 90); + Require(sceVideodec2Flush_nid_postfix(decoder.handle, &decoder.frame, &decoder.output) == 0 && decoder.output.isValid, "new IRAP transfer sequence"); + Require(decoder.Query().preferredTransfer == 0, "new CLVS clears prior transfer ambiguity"); + } + { + Decoder decoder(1); + try { decoder.Decode({transfer.begin(), transfer.end()}, 91); } + catch (const std::runtime_error&) {} + Require(sceVideodec2Reset_nid_postfix(decoder.handle) == 0, "reset pictureless transfer packet"); + decoder.Decode(original[0], 92); + Require(sceVideodec2Flush_nid_postfix(decoder.handle, &decoder.frame, &decoder.output) == 0 && decoder.output.isValid, "decode after pictureless transfer"); + RejectUnchanged(decoder); + } +} + +} + +int main() { + Clips(); + Clips(true); + Single(HevcFixture::HevcMain10Stream, 2, 0, true); + Single(HevcFixture::HevcCroppedStream, 0, 2, false); + UnsupportedPicture(); + ParameterReplacements(); + ExtendedMetadata(); + CabacPadding(); + MalformedParameterHistory(); + TransferAssociation(); +} + diff --git a/core/libs/tests/GuestVideodec2Unimplemented.cpp b/core/libs/tests/GuestVideodec2Unimplemented.cpp index 25eb1d8a77..090fdf8327 100644 --- a/core/libs/tests/GuestVideodec2Unimplemented.cpp +++ b/core/libs/tests/GuestVideodec2Unimplemented.cpp @@ -31,6 +31,5 @@ int main() { passed = ReportsNotImplemented("sceVideodec2CreateDecoderBid", [] { return sceVideodec2CreateDecoderBid(); }) && passed; passed = ReportsNotImplemented("sceVideodec2CreateHevcDecoder", [] { return sceVideodec2CreateHevcDecoder(); }) && passed; passed = ReportsNotImplemented("sceVideodec2QueryHevcDecoderMemoryInfo", [] { return sceVideodec2QueryHevcDecoderMemoryInfo(); }) && passed; - passed = ReportsNotImplemented("sceVideodec2GetHevcPictureInfo", [] { return sceVideodec2GetHevcPictureInfo(); }) && passed; return passed ? 0 : 1; } diff --git a/core/libs/tests/HevcMetadataFixture.hpp b/core/libs/tests/HevcMetadataFixture.hpp new file mode 100644 index 0000000000..8db940da26 --- /dev/null +++ b/core/libs/tests/HevcMetadataFixture.hpp @@ -0,0 +1,45 @@ +#ifndef CORE_LIBS_TESTS_HEVCMETADATAFIXTURE_HPP +#define CORE_LIBS_TESTS_HEVCMETADATAFIXTURE_HPP + +#include +#include + +namespace HevcMetadataFixture { + +inline constexpr std::array Rich{ + 0x00, 0x00, 0x00, 0x01, 0x46, 0x01, 0x10, 0x00, 0x00, 0x00, 0x01, 0x40, 0x01, 0x0c, 0x01, 0xff, + 0xff, 0x04, 0x08, 0x00, 0x00, 0x03, 0x00, 0x9f, 0xa8, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1e, 0xba, + 0x02, 0x40, 0x00, 0x00, 0x00, 0x01, 0x42, 0x01, 0x01, 0x04, 0x08, 0x00, 0x00, 0x03, 0x00, 0x9f, + 0xa8, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1e, 0xa0, 0x20, 0x83, 0x16, 0x5b, 0xab, 0x93, 0x2b, 0xc0, + 0x93, 0x84, 0x88, 0x04, 0xc8, 0x40, 0x80, 0x00, 0x00, 0x03, 0x00, 0x80, 0x00, 0x00, 0x0c, 0x04, + 0x00, 0x00, 0x00, 0x01, 0x44, 0x01, 0xc1, 0x73, 0xc0, 0x89, 0x00, 0x00, 0x01, 0x28, 0x01, 0xaf, + 0x78, 0xf7, 0x04, 0x03, 0xff, 0xdb, 0xa3, 0xff, 0xed, 0x27, 0xd2, 0xf6, 0xc3, 0x94, 0x40, 0x83, + 0xc0, 0x00, 0x78, +}; + +inline constexpr std::array Windowed{ + 0x00, 0x00, 0x00, 0x01, 0x46, 0x01, 0x10, 0x00, 0x00, 0x00, 0x01, 0x40, 0x01, 0x0c, 0x01, 0xff, + 0xff, 0x04, 0x08, 0x00, 0x00, 0x03, 0x00, 0x9f, 0xa8, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1e, 0xba, + 0x02, 0x40, 0x00, 0x00, 0x00, 0x01, 0x42, 0x01, 0x01, 0x04, 0x08, 0x00, 0x00, 0x03, 0x00, 0x9f, + 0xa8, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1e, 0xa0, 0x20, 0x83, 0x16, 0x5b, 0xab, 0x93, 0x2b, 0xc0, + 0x93, 0x84, 0x88, 0x04, 0xc8, 0x41, 0x6c, 0xae, 0x00, 0x00, 0x03, 0x00, 0x02, 0x00, 0x00, 0x03, + 0x00, 0x30, 0x10, 0x00, 0x00, 0x00, 0x01, 0x44, 0x01, 0xc1, 0x73, 0xc0, 0x89, 0x00, 0x00, 0x01, + 0x28, 0x01, 0xaf, 0x78, 0xf7, 0x04, 0x03, 0xff, 0xdb, 0xa3, 0xff, 0xed, 0x27, 0xd2, 0xf6, 0xc3, + 0x94, 0x40, 0x83, 0xc0, 0x00, 0x78, +}; + +inline constexpr std::array Interlaced{ + 0x00, 0x00, 0x00, 0x01, 0x46, 0x01, 0x10, 0x00, 0x00, 0x00, 0x01, 0x40, 0x01, 0x0c, 0x01, 0xff, + 0xff, 0x04, 0x08, 0x00, 0x00, 0x03, 0x00, 0x4f, 0xa8, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1e, 0xba, + 0x02, 0x40, 0x00, 0x00, 0x00, 0x01, 0x42, 0x01, 0x01, 0x04, 0x08, 0x00, 0x00, 0x03, 0x00, 0x4f, + 0xa8, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1e, 0xa0, 0x20, 0x83, 0x16, 0x5b, 0xab, 0x93, 0x2b, 0xc0, + 0x5a, 0x1a, 0x00, 0x00, 0x03, 0x00, 0x02, 0x00, 0x00, 0x03, 0x00, 0x30, 0x10, 0x00, 0x00, 0x00, + 0x01, 0x44, 0x01, 0xc1, 0x73, 0xc0, 0x89, 0x00, 0x00, 0x01, 0x4e, 0x01, 0x81, 0x01, 0x0f, 0x80, + 0x00, 0x00, 0x01, 0x4e, 0x01, 0x01, 0x01, 0x11, 0x80, 0x00, 0x00, 0x01, 0x28, 0x01, 0xaf, 0x78, + 0xf7, 0x04, 0x03, 0xff, 0xec, 0x02, 0x34, 0x1e, 0x93, 0x2c, 0x7e, 0xf5, 0xd4, 0x40, 0x83, 0xc0, + 0x00, 0x78, +}; + +} // namespace HevcMetadataFixture + +#endif diff --git a/docs/dev/debt/libSceVideodec2.md b/docs/dev/debt/libSceVideodec2.md index 6cea4cb6a6..8dcf2c8772 100644 --- a/docs/dev/debt/libSceVideodec2.md +++ b/docs/dev/debt/libSceVideodec2.md @@ -5,16 +5,25 @@ - [sceVideodec2InitLibrary](../../../core/libs/prx/libSceVideodec2/Unimplemented.cpp) (libSceVideodec2) throws: the FFmpeg decoders this module creates need no library-level initialisation, and neither SharpProspero nor EVO-PLAYER-PS5 declares this call, so its arguments and result are unknown. A title imports it from this module - [sceVideodec2GetAvcPictureInfo](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) - runs the `sceVideodec2GetPictureInfo` body (KytyPS5 binds both NIDs to one function; SharpProspero gives both the same `outputInfo, first, second` signature); the second (bottom field) picture info is never written, since only progressive H.264 output is modelled. prosper calls the AVC layout unestablished (no title observed calling it) -- [sceVideodec2TermLibrary and twelve other exports](../../../core/libs/prx/libSceVideodec2/Unimplemented.cpp) (libSceVideodec2) - report that they are not implemented; what they do on the console is not known. The others are `QueryLibraryInfo`, `QueryDecoderInfo`, `MapMemory`, `MapDirectMemory`, `PresetDecoderConfigInfo` (plain, `Avc`, `Hevc`, `Vp9`), `CreateDecoderBid`, `CreateHevcDecoder`, `QueryHevcDecoderMemoryInfo` and `GetHevcPictureInfo`. They are exported so that a title which imports them with `sceVideodec2InitLibrary` and calls none of them directly loads. Only `sceVideodec2MapDirectMemory` has a known parameter list, `(decoder, memory)`, from [EVO-PLAYER-PS5's `sce_videodec2.h`](https://github.com/sainsaji/EVO-PLAYER-PS5/blob/9715f6ee9bf9b53461d6fdf9a2e11b4c48357783/projects/evoplayer/media/include/sce/sce_videodec2.h#L173); the others take none here +- [sceVideodec2TermLibrary and eleven other exports](../../../core/libs/prx/libSceVideodec2/Unimplemented.cpp) (libSceVideodec2) - report that they are not implemented; what they do on the console is not known. The others are `QueryLibraryInfo`, `QueryDecoderInfo`, `MapMemory`, `MapDirectMemory`, `PresetDecoderConfigInfo` (plain, `Avc`, `Hevc`, `Vp9`), `CreateDecoderBid`, `CreateHevcDecoder` and `QueryHevcDecoderMemoryInfo`. They are exported so that a title which imports them with `sceVideodec2InitLibrary` and calls none of them directly loads. Only `sceVideodec2MapDirectMemory` has a known parameter list, `(decoder, memory)`, from [EVO-PLAYER-PS5's `sce_videodec2.h`](https://github.com/sainsaji/EVO-PLAYER-PS5/blob/9715f6ee9bf9b53461d6fdf9a2e11b4c48357783/projects/evoplayer/media/include/sce/sce_videodec2.h#L173); the others take none here ### Functional +- [sceVideodec2Decode and sceVideodec2Flush](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2): the decoder core's discarded flag is not modelled: a returned picture always has `OutputInfo.isDiscardedFrame` (byte 0xb) at 0, and an unshown VP9 frame returns no picture. Per a #10913 review reading firmware 13.42's module (not reproduced here), the byte is written only with a returned picture and is left as the caller set it otherwise, as here. Name and offset: [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Video/Videodec2.cs#L150-L192) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/83f8bfd3c9c35fcb135ef6c33dfd88bbfe066ee2/src/libs/libVideoDec2.cpp#L91-L105); shadPS4 leaves it as padding. PPSA05110 sets only `thisSize` before its VP9 Decode (0x13728dd) and Flush (0x1371f7e) and takes a picture only when the byte is 0 + +- [sceVideodec2CreateDecoder](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) - One title (PPSA02225) disassembled for this entry calls it once (arguments set up at 0x8b5095-0x8b50a6) with three pointers: a structure built on its stack, a structure inside its decoder object and the address of a field of the same object. A nonzero result makes it leave the decoder setup; on one path (a flag byte set and two 32-bit fields equal to values it computed before) it passes that field to the next Videodec2 call. The codec type it requests and the meaning of the structure fields are not shown. + - [AVC parameter-set units](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) - an H.264 access unit with no slice NAL (types 1 to 5), such as the sequence and picture parameter sets Twispero (PPSA99474) submits on their own before a stream's first slices, is kept and put in front of the next unit that has a slice, and `sceVideodec2Decode` returns 0 with no picture for it, since libavcodec refuses a packet without a slice. What the console returns for such a unit is not known. HEVC units of parameter sets alone are still passed to libavcodec as they are -- [sceVideodec2CreateDecoder](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) decodes HEVC (codec type 974921) and VP9 (2382845) through FFmpeg, besides H.264. The tags come from SharpProspero's `Videodec2CodecType` and EVO-PLAYER-PS5's `sce_videodec2.h`, which records them from hardware runs. HEVC input is converted from length-prefixed NAL units to Annex B as for H.264, and VP9 frames are passed through. Output is NV12 for 8-bit pictures. A 10-bit picture is written as 16-bit little-endian words with the sample in the low 10 bits, Y rows then interleaved UV rows at `framePitchInBytes` times the picture height, with `framePitch` in samples and `framePitchInBytes` in bytes (twice the width, aligned to 256), and `QueryDecoderMemoryInfo` sizes a frame of an HEVC or VP9 profile 2 configuration with two bytes per sample. Evidence is from [EVO-PLAYER-PS5](https://github.com/sainsaji/EVO-PLAYER-PS5/tree/9715f6ee9bf9b53461d6fdf9a2e11b4c48357783), which decodes and presents 1080p HEVC Main10 on hardware through this path (`docs/evo-pro/on-demand-decoder-handoff.md`): it takes a picture as 10-bit when `pitch_bytes >= pitch * 2`, puts UV at `pitch_bytes` times the height and samples with a low-bit factor (`projects/evoplayer/media/src/evo_vdec_native.c`, `ro_harvest`), and its hardware logs give 6128 KB and 24496 KB frame buffers for 1920x1088 and 3840x2176 Main10 decoders after rounding to 16 KiB. The 1080p size is what this formula gives; at 3840x2176 it gives 16 KiB less, so the console's size there has another term that is not known. Unverified: `frameFormat` (still 0; no consumer found reads it), VP9 profile 2 output (no hardware evidence), and the layout of 12-bit pictures and of decoded formats other than yuv420p10le, which throw. `sceVideodec2GetPictureInfo` and `sceVideodec2GetAvcPictureInfo` throw for HEVC pictures (`SceVideodec2HevcPictureInfo` is not filled), and `sceVideodec2GetHevcPictureInfo` reports that it is not implemented. One title (PPSA02225) disassembled for this entry calls it once (arguments set up at 0x8b5095-0x8b50a6) with three pointers: a structure built on its stack, a structure inside its decoder object and the address of a field of the same object. A nonzero result makes it leave the decoder setup; on one path (a flag byte set and two 32-bit fields equal to values it computed before) it passes that field to the next Videodec2 call. The codec type it requests and the meaning of the structure fields are not shown. +- [HEVC and VP9 output gaps](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2): console `frameFormat` remains unknown and is 0; VP9 profile 2 output has no hardware evidence. Twelve-bit pictures and formats other than yuv420p10le throw. HEVC Main10 uses low-bit 16-bit little-endian samples, with interleaved UV after `framePitchInBytes * height`, following [EVO-PLAYER-PS5](https://github.com/sainsaji/EVO-PLAYER-PS5/tree/9715f6ee9bf9b53461d6fdf9a2e11b4c48357783) `projects/evoplayer/media/src/evo_vdec_native.c`, `ro_harvest`: the hardware consumer detects ten bits when byte pitch is at least twice sample pitch and uses a low-bit factor. + +- [HEVC frame-buffer memory sizing](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2): the console has an unknown extra term at 3840x2176. The two-byte-per-sample formula gives 16 KiB less than the hardware size of 24496 KB after 16 KiB alignment, while matching 6128 KB at 1920x1088. Evidence is in [EVO-PLAYER-PS5 on-demand-decoder-handoff.md](https://github.com/sainsaji/EVO-PLAYER-PS5/blob/9715f6ee9bf9b53461d6fdf9a2e11b4c48357783/docs/evo-pro/on-demand-decoder-handoff.md). - [sceVideodec2GetVp9PictureInfo](../../../core/libs/prx/libSceVideodec2/Export.cpp) uses the 88-byte layout and two-pointer signature from [SharpProspero Videodec2.cs](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Video/Videodec2.cs). Frame metadata follows the [VP9 specification](https://storage.googleapis.com/downloads.webmproject.org/docs/vp9/vp9-bitstream-specification-v0.7-20170222-draft.pdf), sections 5.26 and 6.2: profile, key/intra-only flags, bit depth, color space/range, coded/render dimensions and superframe count are retained per emitted buffer. Level is 0 because the elementary stream does not signal a level; the console value is unverified. Null pointers, undersized structures and querying another codec throw; console error codes and support for other structure sizes are unknown. Pictures deeper than 10 bits are rejected. Superframes are submitted as individual coded frames to keep each output associated with its input metadata. `sceVideodec2GetPictureInfo` writes this layout for a VP9 picture, as shadPS4's `videodec2.cpp` writes the decoder's own layout; Mortal Shell 2 (PPSA34008) calls it with `thisSize` 88. - [sceVideodec2Decode after sceVideodec2Flush](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) without `sceVideodec2Reset` restarts FFmpeg's decoder (`avcodec_flush_buffers`) and decodes the unit, where it threw before. TEKKEN DARK RESURRECTION (PPSA31371), which runs in Sony's PSP emulator, decodes after a flush without a reset (#7118), so the console accepts the call. Pictures the flush decoded but has not returned yet are returned before the new ones. A flush with no picture pending (each decoded unit has returned its picture) leaves the decoder and its reference pictures as they are, so a stream that continues without a new IDR picture keeps decoding: Daxter (PPSA09695), also in the PSP emulator, flushes after every access unit of its H.264 movies, and restarting the decoder there lost every P picture until the next keyframe. What the console returns for pending pictures, and for a stream that continues across a flush that had pictures pending, is not measured. - [sceVideodec2Decode](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) and `sceVideodec2Flush` output the coded picture, with the chroma plane after its luma rows, as the console does for 1080p H.264 and HEVC streams coded at 1920x1088. An HEVC or VP9 picture whose size is not a multiple of 16, such as HEVC coded at 1920x1080, is output at that size; whether the console pads it is unverified. `sceVideodec2GetPictureInfo` reports no frame cropping - [sceVideodec2Decode of an empty access unit](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) returns `0x811D010E` for a null `auData` and `0x811D010D` for an `auSize` of 0, with no picture, where it threw before. The values are `ACCESS_UNIT_POINTER` and `ACCESS_UNIT_SIZE` in the error table of [shadPS4's videodec_error.h](https://github.com/shadps4-emu/shadPS4/blob/f3f0c14c1350a768558dfd1fd058d259ccecaca2/src/core/libraries/videodec/videodec_error.h), whose libSceVdecsw codes this repository already uses; they are not measured on a PS5, and neither is whether the console leaves the decoder's state alone on that call, as this does. Media Player Classic PS5 (PPSA99015) submits empty units: its own converter reads a 4-byte length prefix of `00 00 01 xx` as a start code and drops the rest as a non-base HEVC layer. It goes on after the error. -- [sceVideodec2Decode and sceVideodec2Flush](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2): the decoder core's discarded flag is not modelled: a returned picture always has `OutputInfo.isDiscardedFrame` (byte 0xb) at 0, and an unshown VP9 frame returns no picture. Per a #10913 review reading firmware 13.42's module (not reproduced here), the byte is written only with a returned picture and is left as the caller set it otherwise, as here. Name and offset: [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Video/Videodec2.cs#L150-L192) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/83f8bfd3c9c35fcb135ef6c33dfd88bbfe066ee2/src/libs/libVideoDec2.cpp#L91-L105); shadPS4 leaves it as padding. PPSA05110 sets only `thisSize` before its VP9 Decode (0x13728dd) and Flush (0x1371f7e) and takes a picture only when the byte is 0 + +- [HEVC picture metadata](../../../core/libs/prx/libSceVideodec2/HevcMetadata.hpp): exact PS5 field values and error codes remain unverified. It uses the complete 184-byte layout from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Video/Videodec2.cs) and syntax from ITU-T H.265 (09/2023). The typed HEVC and generic getters associate each emitted picture with its submitted access-unit metadata, including delayed and flushed pictures. The typed AVC getter still rejects HEVC. Invalid arguments, undersized structures and cross-codec typed queries throw; an unowned picture only clears isValid after size validation. + +- [Unsupported HEVC metadata projections](../../../core/libs/prx/libSceVideodec2/HevcMetadata.cpp): crop/default-display windows, temporal sublayers, multilayer streams, SPS extensions, VPS-only timing, unavailable parameter sets, unsupported SEI/NAL syntax and ambiguous picture-timing association throw at query time before output writes. Differing parameter sets under an existing ID and malformed parameter histories keep queries unsupported through Reset until decoder recreation, since FFmpeg retains parameter state. Alternative-transfer persistence follows CLVS boundaries; conflicts, pictureless transfer messages and ambiguous continuation after reset/flush reject metadata. Unknown crop units, singular sublayer selection and console handling of these cases are not guessed. Decode behavior is retained when metadata cannot be projected. From 31e2d6644f70cf3cbada5e5011caa85f69388f41 Mon Sep 17 00:00:00 2001 From: AZLisme <8706522+AZLisme@users.noreply.github.com> Date: Sun, 25 Oct 2026 21:15:25 +0800 Subject: [PATCH 156/198] fix(videodec2): reject inaccessible HEVC metadata outputs --- core/libs/prx/libSceVideodec2/Export.cpp | 4 + core/libs/tests/GuestHevcMetadata.cpp | 123 ++++++++++++++++++++++- 2 files changed, 126 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceVideodec2/Export.cpp b/core/libs/prx/libSceVideodec2/Export.cpp index 73b702fab4..72244a424b 100644 --- a/core/libs/prx/libSceVideodec2/Export.cpp +++ b/core/libs/prx/libSceVideodec2/Export.cpp @@ -681,6 +681,7 @@ static int GetPictureInfo(const OutputInfo* output, AvcPictureInfo* first, void* if (!found->second.hevc.unsupported.empty()) throw UnsupportedHevcMetadata{}; const auto size = info->thisSize; GuestArena::HostWrite access(info, sizeof(*info)); + if (!access.Open()) throw UnsupportedHevcMetadata{}; *info = found->second.hevc.info; info->thisSize = size; return 0; @@ -701,6 +702,7 @@ static int GetPictureInfo(const OutputInfo* output, AvcPictureInfo* first, void* } if (output->codecType == CodecHevc) { GuestArena::HostWrite access(&first->isValid, sizeof(first->isValid)); + if (!access.Open()) throw UnsupportedHevcMetadata{}; first->isValid = false; } else first->isValid = false; return 0; @@ -737,11 +739,13 @@ int APS5_VABI sceVideodec2GetHevcPictureInfo_nid_postfix(const OutputInfo* outpu if (!found->second.hevc.unsupported.empty()) NotImplemented_nid_no_patch(__func__); const auto size = info->thisSize; GuestArena::HostWrite access(info, sizeof(*info)); + if (!access.Open()) NotImplemented_nid_no_patch(__func__); *info = found->second.hevc.info; info->thisSize = size; return 0; } GuestArena::HostWrite access(&info->isValid, sizeof(info->isValid)); + if (!access.Open()) NotImplemented_nid_no_patch(__func__); info->isValid = false; return 0; } diff --git a/core/libs/tests/GuestHevcMetadata.cpp b/core/libs/tests/GuestHevcMetadata.cpp index 58f8debbc1..816c6e38fb 100644 --- a/core/libs/tests/GuestHevcMetadata.cpp +++ b/core/libs/tests/GuestHevcMetadata.cpp @@ -1,5 +1,7 @@ #include "prx/libSceVideodec2/HevcMetadata.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" #include "HevcFixture.hpp" #include "HevcMetadataFixture.hpp" #include @@ -9,6 +11,11 @@ #include #include #include +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#define NOMINMAX +#include +#endif using namespace SceVideodec2; @@ -68,6 +75,120 @@ struct Decoder { } }; +struct GuestMetadataMapping { + static constexpr std::size_t block = 0x10000; + unsigned char* memory = nullptr; + bool watched = false; +#ifdef _WIN32 + HANDLE section = nullptr; +#endif + + GuestMetadataMapping() { + Require(GuestArena::GuestArenaAvailable_nid_postfix(), "guest arena unavailable"); + memory = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(2 * block, block)); + Require(memory && GuestArena::GuestArenaContains_nid_postfix(memory, 2 * block), "allocate guest metadata mapping"); +#ifdef _WIN32 + section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE | SEC_RESERVE, 0, 2 * block, nullptr); + Require(section != nullptr, "create guest metadata section"); + GuestArena::GuestArenaMap_nid_postfix(memory, 2 * block, section, 0, PAGE_READWRITE); +#else + GuestArena::GuestArenaCommit_nid_postfix(memory, 2 * block, 0x04, block); +#endif + watched = GuestWriteWatch::GuestWriteWatchAvailable_nid_postfix(); + if (watched) GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(memory, 2 * block); + } + + ~GuestMetadataMapping() { + if (watched) GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(memory, 2 * block); + Protect(memory, 2 * block, 0x04); + GuestArena::GuestArenaReset_nid_postfix(memory, 2 * block); + GuestArena::GuestArenaRelease_nid_postfix(memory, 2 * block); +#ifdef _WIN32 + GuestArena::GuestArenaForgetSection_nid_postfix(section); + Require(CloseHandle(section), "close guest metadata section"); +#endif + } + + Videodec2Hevc::PictureInfo* Info() const { + return reinterpret_cast(memory + block - 96); + } + + void Protect(void* pointer, std::size_t bytes, std::uint32_t protection) { +#ifdef _WIN32 + for (std::size_t offset = 0; offset < bytes; offset += block) { + DWORD previous = 0; + Require(VirtualProtect(static_cast(pointer) + offset, block, protection, &previous), "protect guest metadata section"); + } +#endif + GuestArena::GuestArenaSetProtection_nid_postfix(reinterpret_cast(pointer), bytes, protection); + } + + void Initialize() { + Protect(memory, 2 * block, 0x04); + GuestArena::HostWrite access(memory, 2 * block); + Require(access.Open(), "initialize guest metadata access"); + std::memset(memory, 0xa7, 2 * block); + Info()->thisSize = sizeof(*Info()); + Info()->isValid = true; + } + + std::size_t Collect() const { + std::size_t writes = 0; + if (watched) Require(GuestWriteWatch::GuestWriteWatchCollect_nid_postfix(reinterpret_cast(memory), 2 * block, + [](void* context, std::uintptr_t begin, std::uintptr_t end) { *static_cast(context) += end - begin; }, &writes), "collect guest metadata writes"); + return writes; + } +}; + +void GuestMetadataWrites() { + Decoder decoder(1); + decoder.Decode(HevcFixture::hevcUnits(HevcFixture::HevcStream.data(), HevcFixture::HevcStream.size()).front(), 91); + if (!decoder.output.isValid) Require(sceVideodec2Flush_nid_postfix(decoder.handle, &decoder.frame, &decoder.output) == 0, "flush mapped metadata fixture"); + Require(decoder.output.isValid, "mapped metadata fixture produced no picture"); + const auto expected = decoder.Query(); + GuestMetadataMapping mapping; + for (const bool generic : {false, true}) { + for (const bool owned : {false, true}) { + auto output = decoder.output; + if (!owned) output.frameBuffer = nullptr; + const auto query = [&] { + return generic ? sceVideodec2GetPictureInfo_nid_postfix(&output, reinterpret_cast(mapping.Info()), nullptr) : + sceVideodec2GetHevcPictureInfo_nid_postfix(&output, mapping.Info()); + }; + mapping.Initialize(); + auto* first = reinterpret_cast(mapping.Info()) - 16; + std::array initial{}; + std::memcpy(initial.data(), first, initial.size()); + mapping.Collect(); + Require(query() == 0, "guest-mapped metadata query"); + if (owned) { + Require(mapping.Info()->isValid && mapping.Info()->thisSize == sizeof(expected) && + mapping.Info()->ptsData == expected.ptsData && mapping.Info()->attachedData == expected.attachedData && + mapping.Info()->width == expected.width && mapping.Info()->profile == expected.profile, "guest-mapped metadata value"); + Require(std::memcmp(first, initial.data(), 16) == 0 && + std::memcmp(first + 16 + sizeof(expected), initial.data() + 16 + sizeof(expected), 16) == 0, "guest metadata canaries"); + } else { + initial[16 + offsetof(Videodec2Hevc::PictureInfo, isValid)] = 0; + Require(std::memcmp(first, initial.data(), initial.size()) == 0, "invalid-picture mapping changes only isValid"); + } + if (mapping.watched) Require(mapping.Collect() != 0, "guest metadata write not reported"); +#ifdef _WIN32 + mapping.Initialize(); + std::memcpy(initial.data(), first, initial.size()); + const auto refused = mapping.memory + (owned ? GuestMetadataMapping::block : 0); + mapping.Protect(refused, GuestMetadataMapping::block, 0x02); + { + GuestArena::HostWrite access(owned ? static_cast(mapping.Info()) : static_cast(&mapping.Info()->isValid), + owned ? sizeof(*mapping.Info()) : sizeof(mapping.Info()->isValid)); + Require(!access.Open(), "read-only metadata mapping unexpectedly writable"); + } + Rejects(query, "read-only metadata query did not reject access"); + Require(std::memcmp(first, initial.data(), initial.size()) == 0, "rejected metadata write changed output or canaries"); +#endif + } + } +} + void RejectUnchanged(Decoder& decoder); void CheckCommon(const Videodec2Hevc::PictureInfo& info, unsigned bitDepth) { @@ -385,6 +506,7 @@ void TransferAssociation() { } int main() { + GuestMetadataWrites(); Clips(); Clips(true); Single(HevcFixture::HevcMain10Stream, 2, 0, true); @@ -396,4 +518,3 @@ int main() { MalformedParameterHistory(); TransferAssociation(); } - From 98ce5ccf72da2c24e1add7ba410c52f94bd3e093 Mon Sep 17 00:00:00 2001 From: AZLisme <8706522+AZLisme@users.noreply.github.com> Date: Mon, 26 Oct 2026 07:06:42 +0800 Subject: [PATCH 157/198] fix(videodec2): match native picture argument validation --- core/libs/prx/libSceVideodec2/Export.cpp | 167 +++++------ core/libs/tests/GuestHevcMetadata.cpp | 341 ++++++++++++++++++++--- core/libs/tests/GuestVideodec2.cpp | 161 ++++++----- core/libs/tests/HevcFixture.hpp | 10 + docs/dev/debt/libSceVideodec2.md | 14 +- 5 files changed, 487 insertions(+), 206 deletions(-) diff --git a/core/libs/prx/libSceVideodec2/Export.cpp b/core/libs/prx/libSceVideodec2/Export.cpp index 72244a424b..d5cb05f59c 100644 --- a/core/libs/prx/libSceVideodec2/Export.cpp +++ b/core/libs/prx/libSceVideodec2/Export.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -39,6 +40,12 @@ constexpr std::uint32_t CodecHevc = 974921; constexpr std::uint32_t CodecVp9 = 2382845; constexpr int Videodec2ErrorAccessUnitSize = static_cast(0x811D010Du); constexpr int Videodec2ErrorAccessUnitPointer = static_cast(0x811D010Eu); +constexpr int Videodec2ErrorStructureSize = static_cast(0x811D0101u); +constexpr int Videodec2ErrorArgumentPointer = static_cast(0x811D0102u); +constexpr int Videodec2ErrorFrameBufferSize = static_cast(0x811D0106u); +constexpr int Videodec2ErrorFrameBufferPointer = static_cast(0x811D0107u); +constexpr int Videodec2ErrorFrameBufferAlignment = static_cast(0x811D0108u); +constexpr int Videodec2ErrorPictureCount = static_cast(0x811D010Fu); static_assert(sizeof(Vp9PictureInfo) == 88); static_assert(offsetof(Vp9PictureInfo, colorSpace) == 60); @@ -649,11 +656,50 @@ int APS5_VABI sceVideodec2Reset_nid_postfix(std::uint64_t handle) { return 0; } -static int GetPictureInfo(const OutputInfo* output, AvcPictureInfo* first, void* second, bool allowHevc) { - (void)second; - if (!output || !first) throw std::runtime_error("Videodec2: invalid argument pointer"); - if (output->codecType == CodecHevc && (!Fits(output, offsetof(OutputInfo, frameBuffer) + sizeof(output->frameBuffer)) || !Fits(first, sizeof(Videodec2Hevc::PictureInfo)))) - throw std::runtime_error("Videodec2: HEVC picture structure is too small"); +static std::uint64_t PictureInfoSize(const void* info) { + std::uint64_t size = 0; + std::memcpy(&size, info, sizeof(size)); + return size; +} + +static bool PictureInfoSizeSupported(const void* info) { + switch (PictureInfoSize(info)) { + case 0x58: + case 0x68: + case 0x78: + case 0x88: + case 0xa8: + case 0xb8: return true; + default: return false; + } +} + +static int PictureArguments(const OutputInfo* output, void* first, void* second) { + if (!first) return Videodec2ErrorArgumentPointer; + auto* firstValidity = static_cast(first) + 8; + GuestArena::HostWrite firstAccess(firstValidity, sizeof(std::uint32_t)); + if (!firstAccess.Open()) throw UnsupportedHevcMetadata{}; + std::optional secondAccess; + if (second) { + secondAccess.emplace(static_cast(second) + 8, sizeof(std::uint32_t)); + if (!secondAccess->Open()) throw UnsupportedHevcMetadata{}; + } + std::memset(firstValidity, 0, sizeof(std::uint32_t)); + if (second) std::memset(static_cast(second) + 8, 0, sizeof(std::uint32_t)); + if (!output) return Videodec2ErrorArgumentPointer; + if (output->thisSize != 0x30 && output->thisSize != 0x38) return Videodec2ErrorStructureSize; + if ((reinterpret_cast(output->frameBuffer) & 0xffu) != 0) return Videodec2ErrorFrameBufferAlignment; + if (!output->frameBuffer) return Videodec2ErrorFrameBufferPointer; + if (output->pictureCount != 1 && output->pictureCount != 2) return Videodec2ErrorPictureCount; + if (output->frameBufferSize <= std::uint64_t{output->pictureCount} * 0x400u) return Videodec2ErrorFrameBufferSize; + if (!PictureInfoSizeSupported(first) || (output->pictureCount == 2 && second && !PictureInfoSizeSupported(second))) return Videodec2ErrorStructureSize; + return 0; +} + +static int GetPictureInfo(const OutputInfo* output, void* first, void* second) { + const int result = PictureArguments(output, first, second); + if (result != 0) return result; + if (output->pictureCount != 1) throw UnsupportedHevcMetadata{}; const auto owner = [output]() -> std::uint64_t { std::lock_guard guard(lock); const auto found = pictureOwners.find(output->frameBuffer); @@ -665,115 +711,46 @@ static int GetPictureInfo(const OutputInfo* output, AvcPictureInfo* first, void* std::lock_guard guard(decoder->mutex); if (owner() != handle) continue; const auto found = decoder->pictures.find(output->frameBuffer); - if (found == decoder->pictures.end()) throw std::runtime_error("Videodec2: owned picture is missing"); - if (decoder->codec == CodecVp9) { - auto* info = reinterpret_cast(first); - if (!Fits(info, sizeof(Vp9PictureInfo))) throw std::runtime_error("Videodec2: VP9 picture structure is too small"); - const auto size = info->thisSize; - *info = found->second.vp9; - info->thisSize = size; - return 0; - } - if (decoder->codec == CodecHevc) { - if (!allowHevc) throw UnsupportedHevcMetadata{}; - auto* info = reinterpret_cast(first); - if (!Fits(info, sizeof(*info))) throw std::runtime_error("Videodec2: HEVC picture structure is too small"); - if (!found->second.hevc.unsupported.empty()) throw UnsupportedHevcMetadata{}; - const auto size = info->thisSize; - GuestArena::HostWrite access(info, sizeof(*info)); + if (found == decoder->pictures.end()) throw UnsupportedHevcMetadata{}; + const auto size = PictureInfoSize(first); + if (decoder->codec == CodecVp9 && size == sizeof(Vp9PictureInfo)) { + GuestArena::HostWrite access(first, sizeof(Vp9PictureInfo)); if (!access.Open()) throw UnsupportedHevcMetadata{}; - *info = found->second.hevc.info; - info->thisSize = size; + std::memcpy(first, &found->second.vp9, sizeof(Vp9PictureInfo)); + std::memcpy(first, &size, sizeof(size)); return 0; } - const auto& picture = found->second; - first->isValid = true; - first->ptsData = picture.pts; - first->dtsData = picture.dts; - first->attachedData = picture.attached; - if (Fits(first, offsetof(AvcPictureInfo, picHeightInLumaSamples) + 4)) { - first->idrPictureFlag = picture.idr ? 1 : 0; - first->profileIdc = static_cast(decoder->profile); - first->levelIdc = static_cast(decoder->level); - first->picWidthInLumaSamples = output->frameWidth; - first->picHeightInLumaSamples = output->frameHeight; + if (decoder->codec == CodecHevc && size == sizeof(Videodec2Hevc::PictureInfo)) { + if (!found->second.hevc.unsupported.empty()) throw UnsupportedHevcMetadata{}; + GuestArena::HostWrite access(first, sizeof(Videodec2Hevc::PictureInfo)); + if (!access.Open()) throw UnsupportedHevcMetadata{}; + std::memcpy(first, &found->second.hevc.info, sizeof(Videodec2Hevc::PictureInfo)); + std::memcpy(first, &size, sizeof(size)); + return 0; } - return 0; + throw UnsupportedHevcMetadata{}; } - if (output->codecType == CodecHevc) { - GuestArena::HostWrite access(&first->isValid, sizeof(first->isValid)); - if (!access.Open()) throw UnsupportedHevcMetadata{}; - first->isValid = false; - } else first->isValid = false; - return 0; + throw UnsupportedHevcMetadata{}; } int APS5_VABI sceVideodec2GetPictureInfo_nid_postfix(const OutputInfo* output, AvcPictureInfo* first, void* second) { - try { return GetPictureInfo(output, first, second, true); } + try { return GetPictureInfo(output, first, second); } catch (const UnsupportedHevcMetadata&) { NotImplemented_nid_no_patch(__func__); } } int APS5_VABI sceVideodec2GetAvcPictureInfo_nid_postfix(const OutputInfo* output, AvcPictureInfo* first, AvcPictureInfo* second) { - try { return GetPictureInfo(output, first, second, false); } + try { return GetPictureInfo(output, first, second); } catch (const UnsupportedHevcMetadata&) { NotImplemented_nid_no_patch(__func__); } } int APS5_VABI sceVideodec2GetHevcPictureInfo_nid_postfix(const OutputInfo* output, Videodec2Hevc::PictureInfo* info) { - if (!output || !info) throw std::runtime_error("Videodec2: invalid HEVC picture argument"); - if (!Fits(output, offsetof(OutputInfo, frameBuffer) + sizeof(output->frameBuffer)) || !Fits(info, sizeof(*info))) - throw std::runtime_error("Videodec2: HEVC picture structure is too small"); - if (output->codecType != CodecHevc) throw std::runtime_error("Videodec2: HEVC info requested for another codec"); - const auto owner = [output]() -> std::uint64_t { - std::lock_guard guard(lock); - const auto found = pictureOwners.find(output->frameBuffer); - return found != pictureOwners.end() ? found->second : 0; - }; - for (auto handle = owner(); handle != 0; handle = owner()) { - const auto decoder = FindDecoder(handle); - if (!decoder) continue; - std::lock_guard guard(decoder->mutex); - if (owner() != handle) continue; - if (decoder->codec != CodecHevc) throw std::runtime_error("Videodec2: HEVC info requested for another codec"); - const auto found = decoder->pictures.find(output->frameBuffer); - if (found == decoder->pictures.end()) throw std::runtime_error("Videodec2: owned HEVC picture is missing"); - if (!found->second.hevc.unsupported.empty()) NotImplemented_nid_no_patch(__func__); - const auto size = info->thisSize; - GuestArena::HostWrite access(info, sizeof(*info)); - if (!access.Open()) NotImplemented_nid_no_patch(__func__); - *info = found->second.hevc.info; - info->thisSize = size; - return 0; - } - GuestArena::HostWrite access(&info->isValid, sizeof(info->isValid)); - if (!access.Open()) NotImplemented_nid_no_patch(__func__); - info->isValid = false; - return 0; + try { return GetPictureInfo(output, info, nullptr); } + catch (const UnsupportedHevcMetadata&) { NotImplemented_nid_no_patch(__func__); } } int APS5_VABI sceVideodec2GetVp9PictureInfo_nid_postfix(const OutputInfo* output, Vp9PictureInfo* info) { - if (!output || !info) throw std::runtime_error("Videodec2: invalid VP9 picture argument"); - if (!Fits(output, offsetof(OutputInfo, frameBuffer) + sizeof(output->frameBuffer)) || !Fits(info, sizeof(Vp9PictureInfo))) - throw std::runtime_error("Videodec2: VP9 picture structure is too small"); - const auto owner = [output]() -> std::uint64_t { - std::lock_guard guard(lock); - const auto found = pictureOwners.find(output->frameBuffer); - return found != pictureOwners.end() ? found->second : 0; - }; - for (auto handle = owner(); handle != 0; handle = owner()) { - const auto decoder = FindDecoder(handle); - if (!decoder) continue; - std::lock_guard guard(decoder->mutex); - if (owner() != handle) continue; - const auto found = decoder->pictures.find(output->frameBuffer); - if (found == decoder->pictures.end()) throw std::runtime_error("Videodec2: owned VP9 picture is missing"); - if (decoder->codec != CodecVp9) throw std::runtime_error("Videodec2: VP9 info requested for another codec"); - const auto size = info->thisSize; - *info = found->second.vp9; - info->thisSize = size; - return 0; - } - info->isValid = false; - return 0; + try { return GetPictureInfo(output, info, nullptr); } + catch (const UnsupportedHevcMetadata&) { NotImplemented_nid_no_patch(__func__); } } void Videodec2PictureHeader_nid_no_patch(std::uint64_t handle, const void* frameBuffer, PictureHeader* header) { diff --git a/core/libs/tests/GuestHevcMetadata.cpp b/core/libs/tests/GuestHevcMetadata.cpp index 816c6e38fb..b3019f9d69 100644 --- a/core/libs/tests/GuestHevcMetadata.cpp +++ b/core/libs/tests/GuestHevcMetadata.cpp @@ -37,10 +37,208 @@ void Rejects(TCall call, const char* message) { Require(false, message); } +template +bool TryProjection(TCall call) { + try { call(); return true; } + catch (const std::runtime_error&) { return false; } +} + +int QueryPicture(unsigned getter, const OutputInfo* output, void* first, void* second = nullptr) { + switch (getter) { + case 0: return sceVideodec2GetPictureInfo_nid_postfix(output, static_cast(first), second); + case 1: return sceVideodec2GetAvcPictureInfo_nid_postfix(output, static_cast(first), static_cast(second)); + case 2: return sceVideodec2GetHevcPictureInfo_nid_postfix(output, static_cast(first)); + default: return sceVideodec2GetVp9PictureInfo_nid_postfix(output, static_cast(first)); + } +} + +void ArgumentErrors() { + constexpr int pointerError = static_cast(0x811D0102u); + constexpr int sizeError = static_cast(0x811D0101u); + constexpr int alignmentError = static_cast(0x811D0108u); + constexpr int bufferError = static_cast(0x811D0107u); + constexpr int countError = static_cast(0x811D010Fu); + using Bytes = std::array; + const auto initialize = [](Bytes& bytes) { + bytes.fill(0xa5); + const std::uint64_t size = sizeof(Videodec2Hevc::PictureInfo); + std::memcpy(bytes.data() + 16, &size, sizeof(size)); + }; + for (unsigned getter = 0; getter < 4; ++getter) { + const bool generic = getter < 2; + const auto query = [getter](const OutputInfo* output, void* first, void* second) { + return QueryPicture(getter, output, first, second); + }; + for (const std::uint32_t codec : {codecHevc, 1u, 2382845u, 0u}) { + OutputInfo valid{sizeof(OutputInfo)}; + valid.codecType = codec; + valid.frameBuffer = reinterpret_cast(0x100000); + valid.frameBufferSize = 0x10000; + valid.pictureCount = 1; + Bytes first{}, second{}; + initialize(first); + initialize(second); + const auto secondBefore = second; + Require(query(nullptr, nullptr, reinterpret_cast(1)) == pointerError, "null first must return before touching second"); + Require(query(&valid, nullptr, second.data() + 16) == pointerError && second == secondBefore, "null first mutated second"); + const auto check = [&](const OutputInfo* output, int expected) { + initialize(first); + initialize(second); + auto expectedFirst = first; + auto expectedSecond = second; + std::memset(expectedFirst.data() + 24, 0, 4); + if (generic) std::memset(expectedSecond.data() + 24, 0, 4); + Require(query(output, first.data() + 16, second.data() + 16) == expected, "native picture argument result/order"); + Require(first == expectedFirst && second == expectedSecond, "native argument rejection must only clear validity words"); + }; + check(nullptr, pointerError); + for (const std::uint64_t size : {0u, 0x28u, 0x40u}) { + auto invalid = valid; + invalid.thisSize = size; + invalid.frameBuffer = reinterpret_cast(0x100001); + invalid.pictureCount = 0; + check(&invalid, sizeError); + invalid.frameBuffer = nullptr; + check(&invalid, sizeError); + } + for (const std::uintptr_t offset : {1u, 0x80u}) { + auto invalid = valid; + invalid.frameBuffer = reinterpret_cast(0x100000 + offset); + invalid.pictureCount = 0; + invalid.frameBufferSize = 0x400; + check(&invalid, alignmentError); + } + auto invalid = valid; + invalid.frameBuffer = nullptr; + invalid.pictureCount = 0; + check(&invalid, bufferError); + for (const std::uint8_t count : {0, 3, 255}) { + invalid = valid; + invalid.pictureCount = count; + invalid.frameBufferSize = 0x400; + check(&invalid, countError); + invalid.thisSize = 0x30; + invalid.frameBuffer = reinterpret_cast(0x100100); + check(&invalid, countError); + } + initialize(first); + auto both = first; + std::memset(both.data() + 24, 0, 4); + Require(query(nullptr, first.data() + 16, first.data() + 16) == pointerError && first == both, "identical metadata destinations"); + alignas(OutputInfo) std::array aliased{}; + std::memcpy(aliased.data() + 16, &valid, sizeof(valid)); + auto* output = reinterpret_cast(aliased.data() + 16); + Require(query(output, aliased.data() + 8, nullptr) == sizeError && output->thisSize == 0, "argument fields must be read after aliased validity clear"); + if (generic) { + initialize(first); + auto outputCopy = valid; + auto* secondAlias = reinterpret_cast(&outputCopy.frameBuffer) - 8; + Require(query(&outputCopy, first.data() + 16, secondAlias) == bufferError && outputCopy.frameBuffer == nullptr, "second early clear must precede frame-buffer validation"); + } + } + } +} + +void ArgumentSizes() { + constexpr int sizeError = static_cast(0x811D0101u); + constexpr int bufferError = static_cast(0x811D0106u); + using Bytes = std::array; + alignas(256) std::array backing{}; + for (unsigned getter = 0; getter < 4; ++getter) { + for (const std::uint32_t codec : {codecHevc, 1u, 2382845u, 0u}) { + for (const std::uint64_t outputSize : {0x30u, 0x38u}) { + OutputInfo output{outputSize}; + output.codecType = codec; + output.frameBuffer = backing.data(); + Bytes first{}, second{}; + const auto initialize = [&](std::uint64_t firstSize, std::uint64_t secondSize) { + first.fill(0xa5); + second.fill(0x5a); + std::memcpy(first.data() + 16, &firstSize, sizeof(firstSize)); + std::memcpy(second.data() + 16, &secondSize, sizeof(secondSize)); + }; + const auto check = [&](int expected, bool withSecond = true) { + auto expectedFirst = first; + auto expectedSecond = second; + std::memset(expectedFirst.data() + 24, 0, 4); + if (getter < 2 && withSecond) std::memset(expectedSecond.data() + 24, 0, 4); + Require(QueryPicture(getter, &output, first.data() + 16, withSecond ? second.data() + 16 : nullptr) == expected, "native buffer/version error order"); + Require(first == expectedFirst && second == expectedSecond, "buffer/version error changed more than validity words"); + }; + for (const std::uint8_t count : {1, 2}) { + output.pictureCount = count; + const auto boundary = std::uint64_t{count} * 0x400; + for (const auto bytes : {std::uint64_t{0}, boundary - 1, boundary}) { + for (const bool withSecond : {false, true}) { + output.frameBufferSize = bytes; + initialize(0, 0x60); + check(bufferError, withSecond); + } + } + for (const auto bytes : {boundary + 1, (std::uint64_t{1} << 63) + 0x400, UINT64_MAX}) { + output.frameBufferSize = bytes; + initialize(0, 0xb8); + check(sizeError); + for (const std::uint64_t rejected : {0u, 0x38u, 0x50u, 0x60u, 0x98u, 0xb0u, 0xc8u}) { + initialize(rejected, 0xb8); + check(sizeError); + } + for (const std::uint64_t accepted : {0x58u, 0x68u, 0x78u, 0x88u, 0xa8u, 0xb8u}) { + initialize(accepted, 0x60); + if (count == 2 && getter < 2) { + check(sizeError); + } else if (bytes == boundary + 1) { + auto expectedFirst = first; + auto expectedSecond = second; + std::memset(expectedFirst.data() + 24, 0, 4); + if (getter < 2) std::memset(expectedSecond.data() + 24, 0, 4); + const bool completed = TryProjection([&] { QueryPicture(getter, &output, first.data() + 16, second.data() + 16); }); + Require(std::memcmp(first.data(), expectedFirst.data(), 16) == 0 && std::memcmp(first.data() + 16 + accepted, expectedFirst.data() + 16 + accepted, first.size() - 16 - accepted) == 0, "accepted-version output crossed its declared boundary"); + if (!completed) Require(first == expectedFirst && second == expectedSecond, "unsupported projection changed more than validity words"); + } + } + } + } + if (getter < 2) { + output.pictureCount = 1; + output.frameBufferSize = 0x401; + initialize(0xb8, 0xb8); + auto expected = first; + std::memset(expected.data() + 16, 0, 4); + std::memset(expected.data() + 24, 0, 4); + Require(QueryPicture(getter, &output, first.data() + 16, first.data() + 8) == sizeError && first == expected, "second alias must be cleared before destination version read"); + initialize(0xb8, 0xb8); + output.frameBufferSize = 0x401; + auto* sizeAlias = reinterpret_cast(&output.frameBufferSize) - 8; + Require(QueryPicture(getter, &output, first.data() + 16, sizeAlias) == bufferError && output.frameBufferSize == 0, "buffer-size gate must read after secondary validity clear"); + output.pictureCount = 2; + output.frameBufferSize = 0x801; + for (const std::uint64_t secondSize : {0x58u, 0x68u, 0x78u, 0x88u, 0xa8u, 0xb8u}) { + initialize(0xb8, secondSize); + auto beforeFirst = first; + auto beforeSecond = second; + const bool completed = TryProjection([&] { QueryPicture(getter, &output, first.data() + 16, second.data() + 16); }); + Require(std::memcmp(first.data(), beforeFirst.data(), 16) == 0 && std::memcmp(first.data() + 16 + 0xb8, beforeFirst.data() + 16 + 0xb8, 16) == 0, "two-picture primary output canary"); + Require(std::memcmp(second.data(), beforeSecond.data(), 16) == 0 && std::memcmp(second.data() + 16 + secondSize, beforeSecond.data() + 16 + secondSize, second.size() - 16 - secondSize) == 0, "two-picture secondary output canary"); + if (!completed) { + std::memset(beforeFirst.data() + 24, 0, 4); + std::memset(beforeSecond.data() + 24, 0, 4); + Require(first == beforeFirst && second == beforeSecond, "unsupported two-picture projection changed more than validity words"); + } + } + initialize(0xb8, 0); + TryProjection([&] { QueryPicture(getter, &output, first.data() + 16); }); + } + } + } + } +} + struct Decoder { std::uint64_t handle = 0; DecoderMemoryInfo memory{sizeof(memory)}; - std::vector buffer; + std::vector storage; + std::span buffer; FrameBuffer frame{sizeof(frame)}; OutputInfo output{sizeof(output)}; @@ -48,7 +246,7 @@ struct Decoder { const DecoderConfigInfo config{sizeof(config), 1, codecHevc, profile, 0, 64, 48, 4, 1}; Require(sceVideodec2QueryDecoderMemoryInfo_nid_postfix(&config, &memory) == 0, "query memory"); Require(sceVideodec2CreateDecoder_nid_postfix(&config, &memory, &handle) == 0, "create decoder"); - buffer.resize(memory.maxFrameBufferSize); + buffer = HevcFixture::AlignedFrameStorage(storage, memory.maxFrameBufferSize, memory.frameBufferAlignment); frame.frameBuffer = buffer.data(); frame.frameBufferSize = buffer.size(); } @@ -140,6 +338,56 @@ struct GuestMetadataMapping { } }; +void GuestArgumentWrites() { + GuestMetadataMapping mapping; + auto* second = reinterpret_cast(mapping.memory + GuestMetadataMapping::block + 32); + constexpr int pointerError = static_cast(0x811D0102u); + for (unsigned getter = 0; getter < 4; ++getter) { + const bool generic = getter < 2; + const auto query = [&] { + return QueryPicture(getter, nullptr, mapping.Info(), second); + }; + mapping.Initialize(); + auto expected = std::vector(mapping.memory, mapping.memory + 2 * GuestMetadataMapping::block); + std::memset(expected.data() + GuestMetadataMapping::block - 96 + 8, 0, 4); + if (generic) std::memset(expected.data() + GuestMetadataMapping::block + 32 + 8, 0, 4); + mapping.Collect(); + Require(query() == pointerError, "guest-mapped argument error result"); + Require(std::memcmp(mapping.memory, expected.data(), expected.size()) == 0, "guest-mapped argument prefix changed more than validity words"); + if (mapping.watched) Require(mapping.Collect() != 0, "guest argument prefix write not reported"); +#ifdef _WIN32 + for (const std::size_t block : {0u, 1u}) { + if (!generic && block == 1) continue; + mapping.Initialize(); + expected.assign(mapping.memory, mapping.memory + 2 * GuestMetadataMapping::block); + mapping.Protect(mapping.memory + block * GuestMetadataMapping::block, GuestMetadataMapping::block, 0x02); + Rejects(query, "read-only early argument destination did not refuse"); + Require(std::memcmp(mapping.memory, expected.data(), expected.size()) == 0, "rejected early scope partially cleared either destination"); + } +#endif + } +} + +void SecondaryVersionNoRead() { + GuestMetadataMapping mapping; + auto* second = mapping.memory + GuestMetadataMapping::block - 8; + OutputInfo output{sizeof(output)}; + alignas(256) std::array backing{}; + output.frameBuffer = backing.data(); + output.frameBufferSize = 0x401; + output.pictureCount = 1; + for (unsigned getter = 0; getter < 2; ++getter) { + mapping.Initialize(); + Videodec2Hevc::PictureInfo info{sizeof(info)}; + auto expected = std::vector(mapping.memory, mapping.memory + 2 * GuestMetadataMapping::block); + std::memset(expected.data() + GuestMetadataMapping::block, 0, 4); + mapping.Protect(mapping.memory, GuestMetadataMapping::block, 0x01); + TryProjection([&] { QueryPicture(getter, &output, &info, second); }); + mapping.Protect(mapping.memory, GuestMetadataMapping::block, 0x04); + Require(std::memcmp(mapping.memory, expected.data(), expected.size()) == 0, "count-one secondary version was accessed beyond validity word"); + } +} + void GuestMetadataWrites() { Decoder decoder(1); decoder.Decode(HevcFixture::hevcUnits(HevcFixture::HevcStream.data(), HevcFixture::HevcStream.size()).front(), 91); @@ -147,20 +395,19 @@ void GuestMetadataWrites() { Require(decoder.output.isValid, "mapped metadata fixture produced no picture"); const auto expected = decoder.Query(); GuestMetadataMapping mapping; - for (const bool generic : {false, true}) { + for (unsigned getter = 0; getter < 4; ++getter) { for (const bool owned : {false, true}) { auto output = decoder.output; if (!owned) output.frameBuffer = nullptr; const auto query = [&] { - return generic ? sceVideodec2GetPictureInfo_nid_postfix(&output, reinterpret_cast(mapping.Info()), nullptr) : - sceVideodec2GetHevcPictureInfo_nid_postfix(&output, mapping.Info()); + return QueryPicture(getter, &output, mapping.Info()); }; mapping.Initialize(); auto* first = reinterpret_cast(mapping.Info()) - 16; std::array initial{}; std::memcpy(initial.data(), first, initial.size()); mapping.Collect(); - Require(query() == 0, "guest-mapped metadata query"); + Require(query() == (owned ? 0 : static_cast(0x811D0107u)), "guest-mapped metadata query"); if (owned) { Require(mapping.Info()->isValid && mapping.Info()->thisSize == sizeof(expected) && mapping.Info()->ptsData == expected.ptsData && mapping.Info()->attachedData == expected.attachedData && @@ -168,8 +415,8 @@ void GuestMetadataWrites() { Require(std::memcmp(first, initial.data(), 16) == 0 && std::memcmp(first + 16 + sizeof(expected), initial.data() + 16 + sizeof(expected), 16) == 0, "guest metadata canaries"); } else { - initial[16 + offsetof(Videodec2Hevc::PictureInfo, isValid)] = 0; - Require(std::memcmp(first, initial.data(), initial.size()) == 0, "invalid-picture mapping changes only isValid"); + std::memset(initial.data() + 16 + offsetof(Videodec2Hevc::PictureInfo, isValid), 0, 4); + Require(std::memcmp(first, initial.data(), initial.size()) == 0, "invalid-picture mapping changes only validity word"); } if (mapping.watched) Require(mapping.Collect() != 0, "guest metadata write not reported"); #ifdef _WIN32 @@ -179,11 +426,12 @@ void GuestMetadataWrites() { mapping.Protect(refused, GuestMetadataMapping::block, 0x02); { GuestArena::HostWrite access(owned ? static_cast(mapping.Info()) : static_cast(&mapping.Info()->isValid), - owned ? sizeof(*mapping.Info()) : sizeof(mapping.Info()->isValid)); + owned ? sizeof(*mapping.Info()) : sizeof(std::uint32_t)); Require(!access.Open(), "read-only metadata mapping unexpectedly writable"); } Rejects(query, "read-only metadata query did not reject access"); - Require(std::memcmp(first, initial.data(), initial.size()) == 0, "rejected metadata write changed output or canaries"); + if (owned) std::memset(initial.data() + 16 + offsetof(Videodec2Hevc::PictureInfo, isValid), 0, 4); + Require(std::memcmp(first, initial.data(), initial.size()) == 0, "rejected payload write changed more than its early validity word"); #endif } } @@ -240,11 +488,10 @@ void Clips(bool alternativeTransfer = false) { Videodec2Hevc::PictureInfo invalid{sizeof(invalid)}; invalid.isValid = true; Require(sceVideodec2Reset_nid_postfix(decoder.handle) == 0, "reset"); - Require(sceVideodec2GetHevcPictureInfo_nid_postfix(&decoder.output, &invalid) == 0 && !invalid.isValid, "reset releases picture metadata"); + Require(sceVideodec2GetHevcPictureInfo_nid_postfix(&decoder.output, &invalid) == static_cast(0x811D010Fu) && !invalid.isValid, "reset output has no picture count"); invalid.thisSize = 8; invalid.isValid = true; - Rejects([&] { sceVideodec2GetPictureInfo_nid_postfix(&decoder.output, reinterpret_cast(&invalid), nullptr); }, "unowned generic HEVC query accepted an undersized destination"); - Require(invalid.isValid, "unowned undersized generic query changed destination"); + Require(sceVideodec2GetPictureInfo_nid_postfix(&decoder.output, reinterpret_cast(&invalid), nullptr) == static_cast(0x811D010Fu) && !invalid.isValid, "picture-count error must precede destination size"); decoder.Decode(units.front(), 99); Require(sceVideodec2Flush_nid_postfix(decoder.handle, &decoder.frame, &decoder.output) == 0 && decoder.output.isValid, "decode after reset"); Require(decoder.Query().attachedData == 3099, "new metadata after reset"); @@ -264,24 +511,35 @@ void Single(std::span stream, unsigned bitDepth, unsigned bo Require(info.attachedData == 3007 && info.vpsPresent == 1 && info.spsPresent == 1 && info.ppsPresent == 1 && info.delimiterPresent == delimiter, "single-picture tags and presence"); Require(info.idr == 1 && info.irap == 1 && info.preferredTransfer == 0 && info.pictureTimingPresent == 0, "single-picture tail metadata"); Require(info.conformanceWindow == (bottomCrop != 0) && info.confBottom == bottomCrop && info.confLeft == 0 && info.confRight == 0 && info.confTop == 0, "raw chroma-unit conformance offsets"); - auto badOutput = decoder.output; - auto badInfo = info; - Rejects([&] { sceVideodec2GetHevcPictureInfo_nid_postfix(nullptr, &badInfo); }, "null output accepted"); - Rejects([&] { sceVideodec2GetHevcPictureInfo_nid_postfix(&badOutput, nullptr); }, "null destination accepted"); - badInfo.thisSize = sizeof(badInfo) - 1; - Rejects([&] { sceVideodec2GetHevcPictureInfo_nid_postfix(&badOutput, &badInfo); }, "undersized destination accepted"); - badInfo = info; - badOutput.thisSize = 8; - Rejects([&] { sceVideodec2GetHevcPictureInfo_nid_postfix(&badOutput, &badInfo); }, "undersized output accepted"); - badOutput = decoder.output; - badOutput.codecType = 1; - Rejects([&] { sceVideodec2GetHevcPictureInfo_nid_postfix(&badOutput, &badInfo); }, "another codec accepted"); - std::array before; - std::memcpy(before.data(), &badInfo, before.size()); - Rejects([&] { sceVideodec2GetAvcPictureInfo_nid_postfix(&decoder.output, reinterpret_cast(&badInfo), nullptr); }, "AVC-specific getter accepted HEVC with a large buffer"); - Require(std::memcmp(before.data(), &badInfo, before.size()) == 0, "AVC-specific failure modified its output"); - Rejects([&] { sceVideodec2GetAvcPictureInfo_nid_postfix(&badOutput, reinterpret_cast(&badInfo), nullptr); }, "AVC-specific getter trusted forged codec tag"); - Require(std::memcmp(before.data(), &badInfo, before.size()) == 0, "forged codec tag changed AVC failure output"); + for (unsigned getter = 0; getter < 4; ++getter) { + alignas(8) std::array unaligned; + unaligned.fill(0xa5); + const std::uint64_t nativeSize = sizeof(info); + std::memcpy(unaligned.data() + 17, &nativeSize, sizeof(nativeSize)); + const auto guarded = unaligned; + Require(QueryPicture(getter, &decoder.output, unaligned.data() + 17) == 0, "unaligned metadata destination"); + Videodec2Hevc::PictureInfo aligned{}; + std::memcpy(&aligned, unaligned.data() + 17, sizeof(aligned)); + Require(aligned.isValid && aligned.ptsData == info.ptsData && aligned.width == info.width, "unaligned metadata values"); + Require(std::memcmp(unaligned.data(), guarded.data(), 17) == 0 && std::memcmp(unaligned.data() + 17 + sizeof(info), guarded.data() + 17 + sizeof(info), 15) == 0, "unaligned metadata canaries"); + for (const std::uint32_t codec : {codecHevc, 1u, 2382845u, 0u}) { + for (const std::uint64_t size : {0x30u, 0x38u}) { + auto output = decoder.output; + output.codecType = codec; + output.thisSize = size; + Videodec2Hevc::PictureInfo projected{sizeof(projected)}; + std::array second; + second.fill(0xa5); + const std::uint64_t secondSize = 0x60; + std::memcpy(second.data(), &secondSize, sizeof(secondSize)); + auto expected = second; + if (getter < 2) std::memset(expected.data() + 8, 0, 4); + Require(QueryPicture(getter, &output, &projected, second.data()) == 0, "all named wrappers must project owned HEVC metadata after native validation"); + Require(projected.thisSize == sizeof(projected) && projected.isValid && projected.ptsData == info.ptsData && projected.width == info.width, "mutable codec tag changed tracked metadata projection"); + Require(second == expected, "count-one secondary output must only clear its validity word"); + } + } + } } void UnsupportedPicture() { @@ -296,10 +554,11 @@ void UnsupportedPicture() { untouched.thisSize = sizeof(untouched); std::array before; std::memcpy(before.data(), &untouched, before.size()); + std::memset(before.data() + 8, 0, 4); Rejects([&] { sceVideodec2GetHevcPictureInfo_nid_postfix(&decoder.output, &untouched); }, "unknown SEI metadata did not throw"); - Require(std::memcmp(before.data(), &untouched, before.size()) == 0, "unsupported query changed destination"); + Require(std::memcmp(before.data(), &untouched, before.size()) == 0, "unsupported query changed more than validity word"); Rejects([&] { sceVideodec2GetPictureInfo_nid_postfix(&decoder.output, reinterpret_cast(&untouched), nullptr); }, "generic unknown metadata did not throw"); - Require(std::memcmp(before.data(), &untouched, before.size()) == 0, "generic unsupported query changed destination"); + Require(std::memcmp(before.data(), &untouched, before.size()) == 0, "generic unsupported query changed more than validity word"); Require(sceVideodec2Reset_nid_postfix(decoder.handle) == 0, "clear unsupported metadata on reset"); const auto clean = HevcFixture::hevcUnits(HevcFixture::HevcStream.data(), HevcFixture::HevcStream.size()); decoder.Decode(clean[0], 80); @@ -310,7 +569,8 @@ void UnsupportedPicture() { decoder.handle = 0; untouched = {sizeof(untouched)}; untouched.isValid = true; - Require(sceVideodec2GetHevcPictureInfo_nid_postfix(&previous, &untouched) == 0 && !untouched.isValid, "deleted owner releases metadata"); + const bool completed = TryProjection([&] { sceVideodec2GetHevcPictureInfo_nid_postfix(&previous, &untouched); }); + if (!completed) Require(!untouched.isValid, "unsupported untracked metadata retained validity"); } std::vector Replacement(unsigned wanted) { @@ -372,8 +632,9 @@ void RejectUnchanged(Decoder& decoder) { info.thisSize = sizeof(info); std::array before; std::memcpy(before.data(), &info, sizeof(info)); + std::memset(before.data() + 8, 0, 4); Rejects([&] { sceVideodec2GetHevcPictureInfo_nid_postfix(&decoder.output, &info); }, "ambiguous replacement metadata succeeded"); - Require(std::memcmp(before.data(), &info, sizeof(info)) == 0, "ambiguous replacement query mutated output"); + Require(std::memcmp(before.data(), &info, sizeof(info)) == 0, "ambiguous replacement query changed more than validity word"); } void ParameterReplacements() { @@ -384,7 +645,8 @@ void ParameterReplacements() { Require(sceVideodec2Flush_nid_postfix(decoder.handle, &decoder.frame, &decoder.output) == 0 && decoder.output.isValid, "initial replacement-test picture"); Require(decoder.Query().attachedData == 3010, "initial parameter metadata"); const auto previous = decoder.output; - std::vector nextBuffer(decoder.buffer.size()); + std::vector nextStorage; + const auto nextBuffer = HevcFixture::AlignedFrameStorage(nextStorage, decoder.buffer.size(), decoder.memory.frameBufferAlignment); decoder.frame.frameBuffer = nextBuffer.data(); decoder.Decode(type == 0 ? original : Replacement(type), 20); Require(sceVideodec2Flush_nid_postfix(decoder.handle, &decoder.frame, &decoder.output) == 0 && decoder.output.isValid, "decode repeated or replaced parameter sets"); @@ -505,7 +767,12 @@ void TransferAssociation() { } -int main() { +int main(int argc, char** argv) { + ArgumentErrors(); + ArgumentSizes(); + GuestArgumentWrites(); + SecondaryVersionNoRead(); + if (argc == 2 && std::strcmp(argv[1], "--arguments") == 0) return 0; GuestMetadataWrites(); Clips(); Clips(true); diff --git a/core/libs/tests/GuestVideodec2.cpp b/core/libs/tests/GuestVideodec2.cpp index 8537ed2d6c..62d854fbd0 100644 --- a/core/libs/tests/GuestVideodec2.cpp +++ b/core/libs/tests/GuestVideodec2.cpp @@ -101,7 +101,8 @@ void testFailures() { config.maxFrameWidth = Width; check(sceVideodec2QueryDecoderMemoryInfo_nid_postfix(&config, &memory) == 0, "memory query failed"); check(sceVideodec2CreateDecoder_nid_postfix(&config, &memory, &handle) == 0, "decoder creation failed"); - std::vector buffer(memory.maxFrameBufferSize); + std::vector storage; + const auto buffer = AlignedFrameStorage(storage, memory.maxFrameBufferSize, memory.frameBufferAlignment); FrameBuffer frame{sizeof(FrameBuffer), buffer.data(), buffer.size(), false}; OutputInfo output{sizeof(OutputInfo)}; const std::array malformed{0, 0, 0, 8, 0x65}; @@ -111,10 +112,10 @@ void testFailures() { checkThrows([&] { sceVideodec2Decode_nid_postfix(handle, &input, &frame, &output); }); check(sceVideodec2Reset_nid_postfix(handle) == 0, "reset failed"); AvcPictureInfo avc{sizeof(AvcPictureInfo)}; - checkThrows([&] { sceVideodec2GetAvcPictureInfo_nid_postfix(nullptr, &avc, nullptr); }); - checkThrows([&] { sceVideodec2GetAvcPictureInfo_nid_postfix(&output, nullptr, nullptr); }); + check(sceVideodec2GetAvcPictureInfo_nid_postfix(nullptr, &avc, nullptr) == static_cast(0x811D0102u), "null AVC output error"); + check(sceVideodec2GetAvcPictureInfo_nid_postfix(&output, nullptr, nullptr) == static_cast(0x811D0102u), "null AVC destination error"); output.frameBuffer = buffer.data(); - check(sceVideodec2GetAvcPictureInfo_nid_postfix(&output, &avc, nullptr) == 0 && !avc.isValid, "unknown frame buffer reported a picture"); + check(sceVideodec2GetAvcPictureInfo_nid_postfix(&output, &avc, nullptr) == static_cast(0x811D010Fu) && !avc.isValid, "missing picture count must precede destination size"); frame.frameBufferSize = 1; checkThrows([&] { for (const auto& unit : accessUnits(false)) { @@ -138,7 +139,8 @@ void testClip(std::uint32_t codec, std::uint32_t profile, const std::vector buffer(memory.maxFrameBufferSize); + std::vector storage; + const auto buffer = AlignedFrameStorage(storage, memory.maxFrameBufferSize, memory.frameBufferAlignment); std::size_t pictures = 0; const auto take = [&](const OutputInfo& output, const FrameBuffer& frame) { check(output.isValid == frame.isAccepted, "frame buffer acceptance disagrees with the output"); @@ -146,7 +148,7 @@ void testClip(std::uint32_t codec, std::uint32_t profile, const std::vector(0x811D0101u) && !info.isValid, "partial AVC destination size was accepted"); Vp9PictureInfo vp9{sizeof(Vp9PictureInfo)}; if (codec == CodecVp9) { check(sceVideodec2GetVp9PictureInfo_nid_postfix(&output, &vp9) == 0 && vp9.isValid, "VP9 picture metadata missing"); @@ -190,9 +192,11 @@ void testVp9PictureMetadata() { std::uint64_t handle = 0; check(sceVideodec2CreateDecoder_nid_postfix(&config, &memory, &handle) == 0, "VP9 create failed"); const auto units = ivfFrames(); - std::vector firstBuffer(memory.maxFrameBufferSize), secondBuffer(memory.maxFrameBufferSize); + std::vector firstStorage, secondStorage; + const auto firstBuffer = AlignedFrameStorage(firstStorage, memory.maxFrameBufferSize, memory.frameBufferAlignment); + const auto secondBuffer = AlignedFrameStorage(secondStorage, memory.maxFrameBufferSize, memory.frameBufferAlignment); OutputInfo firstOutput{sizeof(OutputInfo)}, secondOutput{sizeof(OutputInfo)}; - const auto decode = [&](const std::vector& bytes, std::vector& buffer, OutputInfo& output, std::uint64_t tag) { + const auto decode = [&](const std::vector& bytes, std::span buffer, OutputInfo& output, std::uint64_t tag) { const InputData input{sizeof(InputData), bytes.data(), bytes.size(), tag, tag + 1, tag + 2}; FrameBuffer frame{sizeof(FrameBuffer), buffer.data(), buffer.size(), false}; check(sceVideodec2Decode_nid_postfix(handle, &input, &frame, &output) == 0, "VP9 metadata decode failed"); @@ -203,12 +207,18 @@ void testVp9PictureMetadata() { check(guarded.guard == 0x123456789abcdef0ull && guarded.info.thisSize == sizeof(Vp9PictureInfo), "VP9 info overwrote caller storage"); return guarded.info; }; + const auto observeUntracked = [&](const OutputInfo& output) { + struct Guarded { Vp9PictureInfo info{sizeof(Vp9PictureInfo)}; std::uint64_t guard = 0x123456789abcdef0ull; } guarded; + try { sceVideodec2GetVp9PictureInfo_nid_postfix(&output, &guarded.info); } + catch (const std::runtime_error&) { check(!guarded.info.isValid, "unsupported untracked output retained validity"); } + check(guarded.guard == 0x123456789abcdef0ull, "untracked metadata crossed its output boundary"); + }; Vp9PictureInfo info{sizeof(Vp9PictureInfo)}; - checkThrows([&] { sceVideodec2GetVp9PictureInfo_nid_postfix(nullptr, &info); }); - checkThrows([&] { sceVideodec2GetVp9PictureInfo_nid_postfix(&firstOutput, nullptr); }); + check(sceVideodec2GetVp9PictureInfo_nid_postfix(nullptr, &info) == static_cast(0x811D0102u), "null VP9 output error"); + check(sceVideodec2GetVp9PictureInfo_nid_postfix(&firstOutput, nullptr) == static_cast(0x811D0102u), "null VP9 destination error"); info.thisSize = sizeof(Vp9PictureInfo) - 1; - checkThrows([&] { sceVideodec2GetVp9PictureInfo_nid_postfix(&firstOutput, &info); }); - check(!query(firstOutput).isValid, "VP9 unknown buffer reported a picture"); + check(sceVideodec2GetVp9PictureInfo_nid_postfix(&firstOutput, &info) == static_cast(0x811D0107u), "buffer error must precede VP9 destination size"); + std::vector superframe; for (std::size_t index = 0; index < 2; ++index) superframe.insert(superframe.end(), units[index].begin(), units[index].end()); superframe.push_back(0xc9); @@ -228,7 +238,8 @@ void testVp9PictureMetadata() { check(secondInfo.numFrames == 2 && secondInfo.keyFrameFlag == 0 && secondInfo.ptsData == 9000, "VP9 second superframe metadata differs"); firstOutput.frameWidth = 1; check(query(firstOutput).frameWidth == Width && query(firstOutput).keyFrameFlag == 1, "VP9 retained picture metadata changed"); - check(sceVideodec2Reset_nid_postfix(handle) == 0 && !query(firstOutput).isValid, "VP9 reset retained old picture ownership"); + check(sceVideodec2Reset_nid_postfix(handle) == 0, "VP9 reset failed"); + observeUntracked(firstOutput); auto hidden = units[0]; hidden[0] &= static_cast(~2u); decode(hidden, firstBuffer, firstOutput, 9100); @@ -257,7 +268,8 @@ void testVp9PictureMetadata() { info = query(firstOutput); check(info.isValid && info.bitDepth == 8 && info.colorRange == 1 && info.frameWidth == Width && info.frameHeight == Height && info.renderWidth == Width * 2 && info.renderHeight == Height * 2, "VP9 render size or color range metadata differs"); check(hashNv12(firstBuffer.data(), firstOutput.framePitch, firstOutput.frameHeight) == Vp9Hashes[0], "VP9 render metadata changed coded pixels"); - check(sceVideodec2DeleteDecoder_nid_postfix(handle) == 0 && !query(firstOutput).isValid, "VP9 delete retained picture ownership"); + check(sceVideodec2DeleteDecoder_nid_postfix(handle) == 0, "VP9 delete failed"); + observeUntracked(firstOutput); } void testTenBit() { @@ -305,7 +317,8 @@ void testDecode(bool lengthPrefixed, bool separateParameters = false) { check(sceVideodec2QueryDecoderMemoryInfo_nid_postfix(&config, &memory) == 0, "memory query failed"); std::uint64_t handle = 0; check(sceVideodec2CreateDecoder_nid_postfix(&config, &memory, &handle) == 0, "decoder creation failed"); - std::vector buffer(memory.maxFrameBufferSize); + std::vector storage; + const auto buffer = AlignedFrameStorage(storage, memory.maxFrameBufferSize, memory.frameBufferAlignment); std::size_t pictures = 0; const auto take = [&](const OutputInfo& output, const FrameBuffer& frame) { check(output.isValid == frame.isAccepted, "frame buffer acceptance disagrees with the output"); @@ -313,13 +326,9 @@ void testDecode(bool lengthPrefixed, bool separateParameters = false) { check(pictures < PictureHashes.size(), "more pictures than access units"); check(!output.isErrorFrame && output.frameWidth == Width && output.frameHeight == CodedHeight && output.framePitch == 256 && output.frameBuffer == buffer.data(), "unexpected picture geometry"); AvcPictureInfo info{sizeof(AvcPictureInfo)}; - check(sceVideodec2GetPictureInfo_nid_postfix(&output, &info, nullptr) == 0 && info.isValid, "picture info missing"); - const auto unit = DisplayOrderUnits[pictures]; - check(info.ptsData == 1000 + unit && info.dtsData == unit && info.attachedData == 0xa0 + unit, "picture timestamps out of display order"); - check(info.idrPictureFlag == (pictures == 0 ? 1 : 0) && info.profileIdc == 100, "picture flags mismatch"); + check(sceVideodec2GetPictureInfo_nid_postfix(&output, &info, nullptr) == static_cast(0x811D0101u) && !info.isValid, "partial AVC destination size was accepted"); AvcPictureInfo avc{sizeof(AvcPictureInfo)}; - check(sceVideodec2GetAvcPictureInfo_nid_postfix(&output, &avc, nullptr) == 0 && avc.isValid, "avc picture info missing"); - check(avc.ptsData == 1000 + unit && avc.dtsData == unit && avc.attachedData == 0xa0 + unit && avc.idrPictureFlag == (pictures == 0 ? 1 : 0) && avc.profileIdc == 100 && avc.picWidthInLumaSamples == Width && avc.picHeightInLumaSamples == CodedHeight, "avc picture info mismatch"); + check(sceVideodec2GetAvcPictureInfo_nid_postfix(&output, &avc, nullptr) == static_cast(0x811D0101u) && !avc.isValid, "partial typed AVC destination size was accepted"); check(hashNv12(buffer.data(), output.framePitch, output.frameHeight) == PictureHashes[pictures], "picture " + std::to_string(pictures) + " differs from the reference"); ++pictures; return true; @@ -472,25 +481,31 @@ std::uint64_t createDecoder(std::uint32_t codec, std::uint32_t profile) { return handle; } -AvcPictureInfo query(std::vector& buffer) { +Vp9PictureInfo query(std::span buffer) { OutputInfo output{sizeof(OutputInfo)}; output.frameBuffer = buffer.data(); - AvcPictureInfo info{sizeof(AvcPictureInfo)}; - check(sceVideodec2GetAvcPictureInfo_nid_postfix(&output, &info, nullptr) == 0, "picture info query failed"); + output.frameBufferSize = buffer.size(); + output.pictureCount = 1; + Vp9PictureInfo info{sizeof(Vp9PictureInfo)}; + try { + check(sceVideodec2GetVp9PictureInfo_nid_postfix(&output, &info) == 0, "picture info query failed"); + } catch (const std::runtime_error&) { + check(!info.isValid, "unimplemented metadata retained validity"); + } return info; } -bool owned(std::vector& buffer, std::uint64_t pts) { +bool owned(std::span buffer, std::uint64_t pts) { const auto info = query(buffer); return info.isValid && info.ptsData == pts; } -std::uint64_t publishAvc(std::uint64_t handle, std::uint64_t base, std::vector& buffer) { - const auto units = accessUnits(false); +std::uint64_t publishVp9(std::uint64_t handle, std::uint64_t base, std::span buffer) { + const auto units = ivfFrames(); std::size_t pictures = 0; const auto take = [&](const OutputInfo& output) { if (!output.isValid) return false; - check(owned(buffer, base + DisplayOrderUnits[pictures]), "picture info does not follow the latest publication"); + check(owned(buffer, base + pictures), "picture info does not follow the latest publication"); ++pictures; return true; }; @@ -507,11 +522,11 @@ std::uint64_t publishAvc(std::uint64_t handle, std::uint64_t base, std::vector>& units, std::vector& buffer) { +void publishClip(std::uint64_t handle, const std::vector>& units, std::span buffer) { for (std::size_t unit = 0; unit < units.size(); ++unit) { const InputData input{sizeof(InputData), units[unit].data(), units[unit].size(), unit, unit, 0}; FrameBuffer frame{sizeof(FrameBuffer), buffer.data(), buffer.size(), false}; @@ -526,10 +541,11 @@ void publishClip(std::uint64_t handle, const std::vector(Width), static_cast(Height), 4, 1}; +std::uint64_t frameBufferBytes(std::uint32_t* alignment = nullptr, std::uint32_t codec = 1, std::uint32_t profile = 100) { + const DecoderConfigInfo config{sizeof(DecoderConfigInfo), 1, codec, profile, 0, static_cast(Width), static_cast(Height), 4, 1}; DecoderMemoryInfo memory{sizeof(DecoderMemoryInfo)}; check(sceVideodec2QueryDecoderMemoryInfo_nid_postfix(&config, &memory) == 0, "memory query failed"); + if (alignment) *alignment = memory.frameBufferAlignment; return memory.maxFrameBufferSize; } @@ -680,7 +696,7 @@ void testDecodeAfterFlush() { std::size_t pictures = 0; const auto take = [&](const OutputInfo& output) { if (!output.isValid) return false; - check(pictures < PictureHashes.size() && owned(buffer, base + DisplayOrderUnits[pictures]), "a picture decoded after a flush is out of display order"); + check(pictures < PictureHashes.size(), "too many pictures decoded after a flush"); check(hashNv12(buffer.data(), output.framePitch, output.frameHeight) == PictureHashes[pictures], "picture " + std::to_string(pictures) + " decoded after a flush differs from the reference"); ++pictures; return true; @@ -704,31 +720,35 @@ void testDecodeAfterFlush() { } void testPictureOwnership() { - std::vector shared(frameBufferBytes()), other(frameBufferBytes()); + std::uint32_t alignment = 0; + const auto bytes = frameBufferBytes(&alignment, CodecVp9, 0); + std::vector sharedStorage, otherStorage; + const auto shared = AlignedFrameStorage(sharedStorage, bytes, alignment); + const auto other = AlignedFrameStorage(otherStorage, bytes, alignment); { - const auto older = createDecoder(1, 100); - const auto newer = createDecoder(1, 100); - publishAvc(older, 1000, shared); - const auto latest = publishAvc(newer, 5000, shared); + const auto older = createDecoder(CodecVp9, 0); + const auto newer = createDecoder(CodecVp9, 0); + publishVp9(older, 1000, shared); + const auto latest = publishVp9(newer, 5000, shared); check(owned(shared, latest), "an older decoder answered for a buffer a newer decoder wrote last"); check(sceVideodec2Reset_nid_postfix(newer) == 0, "reset failed"); check(!query(shared).isValid, "a stale picture survived the reset of the decoder that last wrote the buffer"); check(sceVideodec2DeleteDecoder_nid_postfix(older) == 0 && sceVideodec2DeleteDecoder_nid_postfix(newer) == 0, "delete failed"); } { - const auto older = createDecoder(1, 100); - const auto newer = createDecoder(1, 100); - publishAvc(older, 1000, shared); - publishAvc(newer, 5000, shared); + const auto older = createDecoder(CodecVp9, 0); + const auto newer = createDecoder(CodecVp9, 0); + publishVp9(older, 1000, shared); + publishVp9(newer, 5000, shared); check(sceVideodec2DeleteDecoder_nid_postfix(newer) == 0, "delete failed"); check(!query(shared).isValid, "a stale picture survived the deletion of the decoder that last wrote the buffer"); check(sceVideodec2DeleteDecoder_nid_postfix(older) == 0, "delete failed"); } { - const auto older = createDecoder(1, 100); - const auto newer = createDecoder(1, 100); - publishAvc(older, 1000, shared); - const auto latest = publishAvc(newer, 5000, shared); + const auto older = createDecoder(CodecVp9, 0); + const auto newer = createDecoder(CodecVp9, 0); + publishVp9(older, 1000, shared); + const auto latest = publishVp9(newer, 5000, shared); check(sceVideodec2Reset_nid_postfix(older) == 0, "reset failed"); check(owned(shared, latest), "resetting a decoder that no longer owns the buffer dropped its picture"); check(sceVideodec2DeleteDecoder_nid_postfix(older) == 0, "delete failed"); @@ -737,43 +757,46 @@ void testPictureOwnership() { check(!query(shared).isValid, "a deleted decoder still owns its picture"); } { - const auto older = createDecoder(1, 100); - const auto newer = createDecoder(1, 100); - publishAvc(newer, 5000, shared); - const auto latest = publishAvc(older, 1000, shared); - const auto third = createDecoder(1, 100); - const auto separate = publishAvc(third, 9000, other); + const auto older = createDecoder(CodecVp9, 0); + const auto newer = createDecoder(CodecVp9, 0); + publishVp9(newer, 5000, shared); + const auto latest = publishVp9(older, 1000, shared); + const auto third = createDecoder(CodecVp9, 0); + const auto separate = publishVp9(third, 9000, other); check(owned(shared, latest) && owned(other, separate), "distinct buffers are not independent"); check(sceVideodec2Reset_nid_postfix(older) == 0, "reset failed"); check(!query(shared).isValid && owned(other, separate), "resetting one buffer's owner changed another buffer"); check(sceVideodec2DeleteDecoder_nid_postfix(older) == 0 && sceVideodec2DeleteDecoder_nid_postfix(newer) == 0 && sceVideodec2DeleteDecoder_nid_postfix(third) == 0, "delete failed"); } - for (const auto codec : {CodecHevc, CodecVp9}) { - const auto units = codec == CodecHevc ? hevcUnits(HevcStream.data(), HevcStream.size()) : ivfFrames(); - const auto avc = createDecoder(1, 100); - const auto foreign = createDecoder(codec, codec == CodecHevc ? 1 : 0); - publishAvc(avc, 1000, shared); + for (const auto codec : {CodecHevc, 1u}) { + const auto units = codec == CodecHevc ? hevcUnits(HevcStream.data(), HevcStream.size()) : accessUnits(false); + const auto vp9 = createDecoder(CodecVp9, 0); + const auto foreign = createDecoder(codec, codec == CodecHevc ? 1 : 100); + publishVp9(vp9, 1000, shared); publishClip(foreign, units, shared); - checkThrows([&] { query(shared); }); - check(sceVideodec2DeleteDecoder_nid_postfix(avc) == 0, "delete failed"); - const auto newer = createDecoder(1, 100); - const auto latest = publishAvc(newer, 5000, shared); - check(owned(shared, latest), "a decoder of another codec answered for an AVC picture"); + + check(sceVideodec2DeleteDecoder_nid_postfix(vp9) == 0, "delete failed"); + const auto newer = createDecoder(CodecVp9, 0); + const auto latest = publishVp9(newer, 5000, shared); + check(owned(shared, latest), "a decoder of another codec answered for a VP9 picture"); check(sceVideodec2DeleteDecoder_nid_postfix(foreign) == 0 && sceVideodec2DeleteDecoder_nid_postfix(newer) == 0, "delete failed"); } } void testConcurrentOwnership() { - std::vector shared(frameBufferBytes()); - const auto keeper = createDecoder(1, 100); - const auto kept = publishAvc(keeper, 1000, shared); + std::uint32_t alignment = 0; + const auto bytes = frameBufferBytes(&alignment, CodecVp9, 0); + std::vector sharedStorage; + const auto shared = AlignedFrameStorage(sharedStorage, bytes, alignment); + const auto keeper = createDecoder(CodecVp9, 0); + const auto kept = publishVp9(keeper, 1000, shared); std::atomic done = false; std::string failure; std::thread reader([&] { try { while (!done.load()) { const auto info = query(shared); - check(!info.isValid || info.ptsData == kept || (info.ptsData >= 5000 && info.ptsData < 5000 + PictureHashes.size()), "a query returned metadata no decoder published"); + check(!info.isValid || info.ptsData == kept || (info.ptsData >= 5000 && info.ptsData < 5000 + Vp9Hashes.size()), "a query returned metadata no decoder published"); } } catch (const std::exception& error) { failure = error.what(); @@ -781,8 +804,8 @@ void testConcurrentOwnership() { }); try { for (int round = 0; round < 8; ++round) { - const auto writer = createDecoder(1, 100); - publishAvc(writer, 5000, shared); + const auto writer = createDecoder(CodecVp9, 0); + publishVp9(writer, 5000, shared); if (round % 2 == 0) check(sceVideodec2Reset_nid_postfix(writer) == 0, "reset failed"); check(sceVideodec2DeleteDecoder_nid_postfix(writer) == 0, "delete failed"); check(!query(shared).isValid, "a removed writer left its picture behind"); diff --git a/core/libs/tests/HevcFixture.hpp b/core/libs/tests/HevcFixture.hpp index 7bacee3a02..39e89e0ef1 100644 --- a/core/libs/tests/HevcFixture.hpp +++ b/core/libs/tests/HevcFixture.hpp @@ -3,10 +3,20 @@ #include #include #include +#include +#include #include namespace HevcFixture { +inline std::span AlignedFrameStorage(std::vector& storage, std::size_t bytes, std::size_t alignment) { + storage.resize(bytes + alignment); + void* pointer = storage.data(); + auto space = storage.size(); + if (!std::align(alignment, bytes, pointer, space)) throw std::bad_alloc(); + return {static_cast(pointer), bytes}; +} + inline constexpr std::array HevcStream{ 0x00, 0x00, 0x00, 0x01, 0x46, 0x01, 0x10, 0x00, 0x00, 0x00, 0x01, 0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x01, 0x60, 0x00, 0x00, 0x03, 0x00, 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x1e, diff --git a/docs/dev/debt/libSceVideodec2.md b/docs/dev/debt/libSceVideodec2.md index 8dcf2c8772..66e1a1ff5e 100644 --- a/docs/dev/debt/libSceVideodec2.md +++ b/docs/dev/debt/libSceVideodec2.md @@ -3,8 +3,6 @@ ### Unknown function info - [sceVideodec2InitLibrary](../../../core/libs/prx/libSceVideodec2/Unimplemented.cpp) (libSceVideodec2) throws: the FFmpeg decoders this module creates need no library-level initialisation, and neither SharpProspero nor EVO-PLAYER-PS5 declares this call, so its arguments and result are unknown. A title imports it from this module -- [sceVideodec2GetAvcPictureInfo](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) - runs the `sceVideodec2GetPictureInfo` body (KytyPS5 binds both NIDs to one function; SharpProspero gives both the same `outputInfo, first, second` signature); the second (bottom field) picture info is never written, since only progressive H.264 output is modelled. prosper calls the AVC layout unestablished (no title observed calling it) - - [sceVideodec2TermLibrary and eleven other exports](../../../core/libs/prx/libSceVideodec2/Unimplemented.cpp) (libSceVideodec2) - report that they are not implemented; what they do on the console is not known. The others are `QueryLibraryInfo`, `QueryDecoderInfo`, `MapMemory`, `MapDirectMemory`, `PresetDecoderConfigInfo` (plain, `Avc`, `Hevc`, `Vp9`), `CreateDecoderBid`, `CreateHevcDecoder` and `QueryHevcDecoderMemoryInfo`. They are exported so that a title which imports them with `sceVideodec2InitLibrary` and calls none of them directly loads. Only `sceVideodec2MapDirectMemory` has a known parameter list, `(decoder, memory)`, from [EVO-PLAYER-PS5's `sce_videodec2.h`](https://github.com/sainsaji/EVO-PLAYER-PS5/blob/9715f6ee9bf9b53461d6fdf9a2e11b4c48357783/projects/evoplayer/media/include/sce/sce_videodec2.h#L173); the others take none here ### Functional @@ -19,11 +17,17 @@ - [HEVC frame-buffer memory sizing](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2): the console has an unknown extra term at 3840x2176. The two-byte-per-sample formula gives 16 KiB less than the hardware size of 24496 KB after 16 KiB alignment, while matching 6128 KB at 1920x1088. Evidence is in [EVO-PLAYER-PS5 on-demand-decoder-handoff.md](https://github.com/sainsaji/EVO-PLAYER-PS5/blob/9715f6ee9bf9b53461d6fdf9a2e11b4c48357783/docs/evo-pro/on-demand-decoder-handoff.md). -- [sceVideodec2GetVp9PictureInfo](../../../core/libs/prx/libSceVideodec2/Export.cpp) uses the 88-byte layout and two-pointer signature from [SharpProspero Videodec2.cs](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Video/Videodec2.cs). Frame metadata follows the [VP9 specification](https://storage.googleapis.com/downloads.webmproject.org/docs/vp9/vp9-bitstream-specification-v0.7-20170222-draft.pdf), sections 5.26 and 6.2: profile, key/intra-only flags, bit depth, color space/range, coded/render dimensions and superframe count are retained per emitted buffer. Level is 0 because the elementary stream does not signal a level; the console value is unverified. Null pointers, undersized structures and querying another codec throw; console error codes and support for other structure sizes are unknown. Pictures deeper than 10 bits are rejected. Superframes are submitted as individual coded frames to keep each output associated with its input metadata. `sceVideodec2GetPictureInfo` writes this layout for a VP9 picture, as shadPS4's `videodec2.cpp` writes the decoder's own layout; Mortal Shell 2 (PPSA34008) calls it with `thisSize` 88. +- [sceVideodec2GetVp9PictureInfo](../../../core/libs/prx/libSceVideodec2/Export.cpp): console field values remain unverified. The 88-byte projection uses [SharpProspero Videodec2.cs](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Video/Videodec2.cs) and the [VP9 specification](https://storage.googleapis.com/downloads.webmproject.org/docs/vp9/vp9-bitstream-specification-v0.7-20170222-draft.pdf), sections 5.26 and 6.2. Level is 0 because the elementary stream does not signal it. Pictures deeper than 10 bits throw. Superframes are submitted as individual coded frames to retain input metadata association. Only tracked single-picture VP9 metadata is projected; untracked frame-buffer metadata and other payload versions remain unsupported. + - [sceVideodec2Decode after sceVideodec2Flush](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) without `sceVideodec2Reset` restarts FFmpeg's decoder (`avcodec_flush_buffers`) and decodes the unit, where it threw before. TEKKEN DARK RESURRECTION (PPSA31371), which runs in Sony's PSP emulator, decodes after a flush without a reset (#7118), so the console accepts the call. Pictures the flush decoded but has not returned yet are returned before the new ones. A flush with no picture pending (each decoded unit has returned its picture) leaves the decoder and its reference pictures as they are, so a stream that continues without a new IDR picture keeps decoding: Daxter (PPSA09695), also in the PSP emulator, flushes after every access unit of its H.264 movies, and restarting the decoder there lost every P picture until the next keyframe. What the console returns for pending pictures, and for a stream that continues across a flush that had pictures pending, is not measured. - [sceVideodec2Decode](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) and `sceVideodec2Flush` output the coded picture, with the chroma plane after its luma rows, as the console does for 1080p H.264 and HEVC streams coded at 1920x1088. An HEVC or VP9 picture whose size is not a multiple of 16, such as HEVC coded at 1920x1080, is output at that size; whether the console pads it is unverified. `sceVideodec2GetPictureInfo` reports no frame cropping - [sceVideodec2Decode of an empty access unit](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) returns `0x811D010E` for a null `auData` and `0x811D010D` for an `auSize` of 0, with no picture, where it threw before. The values are `ACCESS_UNIT_POINTER` and `ACCESS_UNIT_SIZE` in the error table of [shadPS4's videodec_error.h](https://github.com/shadps4-emu/shadPS4/blob/f3f0c14c1350a768558dfd1fd058d259ccecaca2/src/core/libraries/videodec/videodec_error.h), whose libSceVdecsw codes this repository already uses; they are not measured on a PS5, and neither is whether the console leaves the decoder's state alone on that call, as this does. Media Player Classic PS5 (PPSA99015) submits empty units: its own converter reads a 4-byte length prefix of `00 00 01 xx` as a start code and drops the rest as a non-base HEVC layer. It goes on after the error. -- [HEVC picture metadata](../../../core/libs/prx/libSceVideodec2/HevcMetadata.hpp): exact PS5 field values and error codes remain unverified. It uses the complete 184-byte layout from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Video/Videodec2.cs) and syntax from ITU-T H.265 (09/2023). The typed HEVC and generic getters associate each emitted picture with its submitted access-unit metadata, including delayed and flushed pictures. The typed AVC getter still rejects HEVC. Invalid arguments, undersized structures and cross-codec typed queries throw; an unowned picture only clears isValid after size validation. +- [Picture getter argument compatibility](../../../core/libs/prx/libSceVideodec2/Export.cpp): other firmware/process modes, native pointer faults and physical-console behavior remain unverified. The common prologue follows BunnyFl's [native-code emulator report](https://github.com/boykopovar/AnyPS5/pull/10959#issuecomment-6404011201) and [clarification](https://github.com/boykopovar/AnyPS5/pull/10959#issuecomment-6404317631) for 13.42 module SHA256 `5825514d06cee0b7fdc87de4f428065ff7c2b761b40443f95ee580e1be18776d`, with `sceKernelIsPs4Process` returning 0. This was not independently reproduced here and establishes no valid metadata fields. Named getters share ordered errors, early four-byte validity/padding clears and the unsigned `frameBufferSize > pictureCount * 0x400` check. Host-write refusal throws without inventing a console fault result. -- [Unsupported HEVC metadata projections](../../../core/libs/prx/libSceVideodec2/HevcMetadata.cpp): crop/default-display windows, temporal sublayers, multilayer streams, SPS extensions, VPS-only timing, unavailable parameter sets, unsupported SEI/NAL syntax and ambiguous picture-timing association throw at query time before output writes. Differing parameter sets under an existing ID and malformed parameter histories keep queries unsupported through Reset until decoder recreation, since FFmpeg retains parameter state. Alternative-transfer persistence follows CLVS boundaries; conflicts, pictureless transfer messages and ambiguous continuation after reset/flush reject metadata. Unknown crop units, singular sublayer selection and console handling of these cases are not guessed. Decode behavior is retained when metadata cannot be projected. +- [HEVC picture metadata](../../../core/libs/prx/libSceVideodec2/HevcMetadata.hpp): exact console field values remain unverified. The complete 184-byte layout follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Video/Videodec2.cs), and syntax follows ITU-T H.265 (09/2023). Named getter wrappers share argument validation and select the supported projection from tracked decoded-frame metadata, independently of the caller's codec tag. The 184-byte HEVC and 88-byte VP9 projections retain delayed/flushed picture association. Other accepted payload versions, two-picture projections and untracked frame-buffer metadata throw after the early validity clears; their native payload behavior is not inferred from successful argument checks. + +- [AVC picture-info payload](../../../core/libs/prx/libSceVideodec2/include/Api.hpp): the existing 56-byte host prefix is not a supported native picture-info version and returns the reported structure-size error. Accepted native AVC payloads have an unimplemented tail and are not filled by these getters. H.264 decoding remains available; no padded native AVC layout or missing field values are fabricated. + + +- [Unsupported HEVC metadata projections](../../../core/libs/prx/libSceVideodec2/HevcMetadata.cpp): crop/default-display windows, temporal sublayers, multilayer streams, SPS extensions, VPS-only timing, unavailable parameter sets, unsupported SEI/NAL syntax and ambiguous picture-timing association throw at query time after the common validity-word clears and before other metadata writes. Differing parameter sets under an existing ID and malformed parameter histories keep queries unsupported through Reset until decoder recreation, since FFmpeg retains parameter state. Alternative-transfer persistence follows CLVS boundaries; conflicts, pictureless transfer messages and ambiguous continuation after reset/flush reject metadata. Unknown crop units, singular sublayer selection and console handling of these cases are not guessed. Decode behavior is retained when metadata cannot be projected. From 5e4c8fdc6e9491dd4cbca99f29efaba1c740c59d Mon Sep 17 00:00:00 2001 From: AZLisme <8706522+AZLisme@users.noreply.github.com> Date: Mon, 26 Oct 2026 08:51:14 +0800 Subject: [PATCH 158/198] test(videodec2): respect Windows mapped view boundaries --- core/libs/tests/GuestHevcMetadata.cpp | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/core/libs/tests/GuestHevcMetadata.cpp b/core/libs/tests/GuestHevcMetadata.cpp index b3019f9d69..062ebca88f 100644 --- a/core/libs/tests/GuestHevcMetadata.cpp +++ b/core/libs/tests/GuestHevcMetadata.cpp @@ -4,6 +4,7 @@ #include "prx/libc/include/GuestWriteWatch.hpp" #include "HevcFixture.hpp" #include "HevcMetadataFixture.hpp" +#include #include #include #include @@ -12,8 +13,12 @@ #include #include #ifdef _WIN32 +#ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN +#endif +#ifndef NOMINMAX #define NOMINMAX +#endif #include #endif @@ -313,9 +318,17 @@ struct GuestMetadataMapping { void Protect(void* pointer, std::size_t bytes, std::uint32_t protection) { #ifdef _WIN32 - for (std::size_t offset = 0; offset < bytes; offset += block) { + auto cursor = reinterpret_cast(pointer); + const auto end = cursor + bytes; + while (cursor < end) { + MEMORY_BASIC_INFORMATION region{}; + Require(VirtualQuery(reinterpret_cast(cursor), ®ion, sizeof(region)) == sizeof(region), "query guest metadata section"); + const auto regionEnd = reinterpret_cast(region.BaseAddress) + region.RegionSize; + Require(region.State == MEM_COMMIT && regionEnd > cursor, "guest metadata section is not committed"); + const auto span = std::min(end - cursor, regionEnd - cursor); DWORD previous = 0; - Require(VirtualProtect(static_cast(pointer) + offset, block, protection, &previous), "protect guest metadata section"); + Require(VirtualProtect(reinterpret_cast(cursor), span, protection, &previous), "protect guest metadata section"); + cursor += span; } #endif GuestArena::GuestArenaSetProtection_nid_postfix(reinterpret_cast(pointer), bytes, protection); From 7843323dc2e6e27630927ab358b5518a0928a542 Mon Sep 17 00:00:00 2001 From: AZLisme <8706522+AZLisme@users.noreply.github.com> Date: Mon, 26 Oct 2026 12:26:14 +0800 Subject: [PATCH 159/198] fix(videodec): read owned metadata for internal callers --- core/libs/prx/libSceVdecsw/Export.cpp | 37 +++-------------- core/libs/prx/libSceVideodec/Export.cpp | 41 ++++++------------- core/libs/prx/libSceVideodec2/Export.cpp | 34 +++++++-------- .../include/PictureMetadata.hpp | 37 +++++++++++++++++ core/libs/tests/GuestVideodec.cpp | 3 ++ 5 files changed, 72 insertions(+), 80 deletions(-) create mode 100644 core/libs/prx/libSceVideodec2/include/PictureMetadata.hpp diff --git a/core/libs/prx/libSceVdecsw/Export.cpp b/core/libs/prx/libSceVdecsw/Export.cpp index d15ec5c2fa..b352040051 100644 --- a/core/libs/prx/libSceVdecsw/Export.cpp +++ b/core/libs/prx/libSceVdecsw/Export.cpp @@ -1,4 +1,5 @@ #include "prx/libSceVideodec2/include/Api.hpp" +#include "prx/libSceVideodec2/include/PictureMetadata.hpp" #include "prx/libSceVdecsw/include/Api.hpp" #include #include @@ -33,34 +34,6 @@ static_assert(sizeof(OutputInfo) == 0x38 && offsetof(OutputInfo, isLastFrame) == static_assert(sizeof(AvcPictureInfo) == 0x78 && offsetof(AvcPictureInfo, picWidthInMbsMinus1) == 0x2c && offsetof(AvcPictureInfo, frameCropBottomOffset) == 0x44); static_assert(sizeof(HevcPictureInfo) == 0xb8 && offsetof(HevcPictureInfo, picWidthInLumaSamples) == 0x28 && offsetof(HevcPictureInfo, sarWidth) == 0x42 && offsetof(HevcPictureInfo, picStruct) == 0x50 && offsetof(HevcPictureInfo, conformanceWindowFlag) == 0x5c && offsetof(HevcPictureInfo, idrPictureFlag) == 0x93 && offsetof(HevcPictureInfo, generalProfileIdc) == 0x97 && offsetof(HevcPictureInfo, generalLevelIdc) == 0x9b && offsetof(HevcPictureInfo, frameCroppingFlag) == 0xa5 && offsetof(HevcPictureInfo, frameCropBottomOffset) == 0xb4); -struct Videodec2Crop { - std::uint32_t left; - std::uint32_t right; - std::uint32_t top; - std::uint32_t bottom; -}; - -struct Videodec2PictureHeader { - std::uint64_t pts; - std::uint64_t dts; - std::uint64_t attached; - bool keyFrame; - bool hevcIdr; - std::uint32_t profile; - std::uint32_t level; - Videodec2Crop crop; - std::uint8_t bitDepthMinus8; -}; - -} - -extern "C" { - -void Videodec2PictureHeader_nid_no_patch(std::uint64_t handle, const void* frameBuffer, Videodec2PictureHeader* header); -} - -namespace { - struct PendingInput { SceVideodec2::InputData input; std::vector data; @@ -103,7 +76,7 @@ std::shared_ptr findDecoder(std::uint64_t handle) { } PictureInfo pictureInfo(std::uint64_t handle, const SceVideodec2::OutputInfo& output) { - Videodec2PictureHeader header{}; + SceVideodec2::Internal::PictureHeader header{}; Videodec2PictureHeader_nid_no_patch(handle, output.frameBuffer, &header); const auto& crop = header.crop; const bool cropped = crop.left != 0 || crop.right != 0 || crop.top != 0 || crop.bottom != 0; @@ -157,9 +130,9 @@ PictureInfo pictureInfo(std::uint64_t handle, const SceVideodec2::OutputInfo& ou return info; } -SceVideodec2::Vp9PictureInfo vp9PictureInfo(const SceVideodec2::OutputInfo& output) { +SceVideodec2::Vp9PictureInfo vp9PictureInfo(std::uint64_t handle, const SceVideodec2::OutputInfo& output) { SceVideodec2::Vp9PictureInfo info{sizeof(SceVideodec2::Vp9PictureInfo)}; - sceVideodec2GetVp9PictureInfo_nid_postfix(&output, &info); + Videodec2Vp9PictureInfo_nid_no_patch(handle, output.frameBuffer, &info); if (!info.isValid) throw std::runtime_error("Vdecsw: decoded picture has no VP9 picture info"); return info; @@ -331,7 +304,7 @@ int APS5_VABI sceVdecswTrySyncDecodeOutput(std::uint64_t handle, OutputInfo* out output->frameBufferSize = picture.output.frameBufferSize; output->frameFormat = picture.output.frameFormat; output->framePitchInBytes = picture.output.framePitchInBytes; - if (picture.output.codecType == CodecVp9) decoder->deliveredVp9[target.frameBuffer] = vp9PictureInfo(picture.output); + if (picture.output.codecType == CodecVp9) decoder->deliveredVp9[target.frameBuffer] = vp9PictureInfo(handle, picture.output); else decoder->delivered[target.frameBuffer] = picture.info; return 0; } diff --git a/core/libs/prx/libSceVideodec/Export.cpp b/core/libs/prx/libSceVideodec/Export.cpp index efb762dbfe..f5110f0274 100644 --- a/core/libs/prx/libSceVideodec/Export.cpp +++ b/core/libs/prx/libSceVideodec/Export.cpp @@ -1,4 +1,5 @@ #include "prx/libSceVideodec2/include/Api.hpp" +#include "prx/libSceVideodec2/include/PictureMetadata.hpp" #include "prx/libSceVideodec/include/Api.hpp" #include #include @@ -28,12 +29,7 @@ constexpr int VideodecErrorDecodeAu = static_cast(0x80C10011); constexpr std::uint32_t CodecAvc = 1; constexpr std::uint32_t PitchAlignment = 64; -struct Decoder2Crop { - std::uint32_t left; - std::uint32_t right; - std::uint32_t top; - std::uint32_t bottom; -}; +using Decoder2Crop = SceVideodec2::Internal::Crop; static_assert(sizeof(ConfigInfo) == 40 && sizeof(ResourceInfo) == 56 && sizeof(Ctrl) == 24); static_assert(offsetof(ConfigInfo, maxFrameWidth) == 20 && offsetof(ConfigInfo, videodecFlags) == 32); @@ -62,15 +58,6 @@ std::uint32_t AlignUp(std::uint32_t value, std::uint32_t alignment) { return (value + alignment - 1) / alignment * alignment; } -} - -extern "C" { - -void Videodec2PictureCrop_nid_no_patch(std::uint64_t, const void*, Decoder2Crop*); -} - -namespace { - struct Geometry { std::uint32_t width = 0; std::uint32_t height = 0; @@ -199,14 +186,12 @@ int APS5_VABI sceVideodecDecode(Ctrl* ctrl, const SceVideodec2::InputData* input } catch (const std::exception&) { return VideodecErrorDecodeAu; } - Decoder2Crop crop{}; - if (output.isValid) Videodec2PictureCrop_nid_no_patch(decoder->handle, output.frameBuffer, &crop); - const auto geometry = Measure(output, crop); - SceVideodec2::AvcPictureInfo avc{sizeof(SceVideodec2::AvcPictureInfo)}; - if (output.isValid) sceVideodec2GetAvcPictureInfo_nid_postfix(&output, &avc, nullptr); + SceVideodec2::Internal::PictureHeader header{}; + if (output.isValid) Videodec2PictureHeader_nid_no_patch(decoder->handle, output.frameBuffer, &header); + const auto geometry = Measure(output, header.crop); FillPictureInfo(geometry, output, picture); - picture->ptsData = avc.ptsData; - picture->attachedData = avc.attachedData; + picture->ptsData = header.pts; + picture->attachedData = header.attached; if (!output.isValid) return 0; return CopyPicture(output, geometry, frameBuffer); } @@ -253,14 +238,12 @@ int APS5_VABI sceVideodecFlush(Ctrl* ctrl, FrameBuffer* frameBuffer, PictureInfo } catch (const std::exception&) { return VideodecErrorDecodeAu; } - Decoder2Crop crop{}; - if (output.isValid) Videodec2PictureCrop_nid_no_patch(decoder->handle, output.frameBuffer, &crop); - const auto geometry = Measure(output, crop); - SceVideodec2::AvcPictureInfo avc{sizeof(SceVideodec2::AvcPictureInfo)}; - if (output.isValid) sceVideodec2GetAvcPictureInfo_nid_postfix(&output, &avc, nullptr); + SceVideodec2::Internal::PictureHeader header{}; + if (output.isValid) Videodec2PictureHeader_nid_no_patch(decoder->handle, output.frameBuffer, &header); + const auto geometry = Measure(output, header.crop); FillPictureInfo(geometry, output, picture); - picture->ptsData = avc.ptsData; - picture->attachedData = avc.attachedData; + picture->ptsData = header.pts; + picture->attachedData = header.attached; if (!output.isValid) return 0; return CopyPicture(output, geometry, frameBuffer); } diff --git a/core/libs/prx/libSceVideodec2/Export.cpp b/core/libs/prx/libSceVideodec2/Export.cpp index d5cb05f59c..ac97797603 100644 --- a/core/libs/prx/libSceVideodec2/Export.cpp +++ b/core/libs/prx/libSceVideodec2/Export.cpp @@ -1,4 +1,5 @@ #include "prx/libSceVideodec2/include/Api.hpp" +#include "prx/libSceVideodec2/include/PictureMetadata.hpp" #include #include #include @@ -29,6 +30,8 @@ extern "C" { namespace { using namespace SceVideodec2; +using SceVideodec2::Internal::Crop; +using SceVideodec2::Internal::PictureHeader; struct UnsupportedHevcMetadata {}; @@ -183,13 +186,6 @@ std::vector ParseVp9Packet(std::span bytes, std::ar return result; } -struct Crop { - std::uint32_t left = 0; - std::uint32_t right = 0; - std::uint32_t top = 0; - std::uint32_t bottom = 0; -}; - struct Picture { std::uint64_t pts = 0; std::uint64_t dts = 0; @@ -202,18 +198,6 @@ struct Picture { Videodec2Hevc::Metadata hevc; }; -struct PictureHeader { - std::uint64_t pts; - std::uint64_t dts; - std::uint64_t attached; - bool keyFrame; - bool hevcIdr; - std::uint32_t profile; - std::uint32_t level; - Crop crop; - std::uint8_t bitDepthMinus8; -}; - struct FrameDeleter { void operator()(AVFrame* frame) const { av_frame_free(&frame); } }; @@ -772,4 +756,16 @@ void Videodec2PictureCrop_nid_no_patch(std::uint64_t handle, const void* frameBu *crop = header.crop; } +void Videodec2Vp9PictureInfo_nid_no_patch(std::uint64_t handle, const void* frameBuffer, Vp9PictureInfo* info) { + if (!info) throw std::runtime_error("Videodec2: invalid argument pointer"); + const auto found = FindDecoder(handle); + if (!found) throw std::runtime_error("Videodec2: invalid decoder handle"); + auto& decoder = *found; + std::lock_guard guard(decoder.mutex); + if (decoder.codec != CodecVp9) throw std::runtime_error("Videodec2: VP9 picture info requested for another codec"); + const auto picture = decoder.pictures.find(frameBuffer); + if (picture == decoder.pictures.end()) throw std::runtime_error("Videodec2: unknown frame buffer"); + *info = picture->second.vp9; +} + } diff --git a/core/libs/prx/libSceVideodec2/include/PictureMetadata.hpp b/core/libs/prx/libSceVideodec2/include/PictureMetadata.hpp new file mode 100644 index 0000000000..108f7ac3cc --- /dev/null +++ b/core/libs/prx/libSceVideodec2/include/PictureMetadata.hpp @@ -0,0 +1,37 @@ +#ifndef CORE_LIBS_PRX_LIBSCEVIDEODEC2_INCLUDE_PICTUREMETADATA_HPP +#define CORE_LIBS_PRX_LIBSCEVIDEODEC2_INCLUDE_PICTUREMETADATA_HPP + +#include "Api.hpp" + +namespace SceVideodec2::Internal { + +struct Crop { + std::uint32_t left = 0; + std::uint32_t right = 0; + std::uint32_t top = 0; + std::uint32_t bottom = 0; +}; + +struct PictureHeader { + std::uint64_t pts; + std::uint64_t dts; + std::uint64_t attached; + bool keyFrame; + bool hevcIdr; + std::uint32_t profile; + std::uint32_t level; + Crop crop; + std::uint8_t bitDepthMinus8; +}; + +} + +extern "C" { + +void Videodec2PictureHeader_nid_no_patch(std::uint64_t handle, const void* frameBuffer, SceVideodec2::Internal::PictureHeader* header); +void Videodec2PictureCrop_nid_no_patch(std::uint64_t handle, const void* frameBuffer, SceVideodec2::Internal::Crop* crop); +void Videodec2Vp9PictureInfo_nid_no_patch(std::uint64_t handle, const void* frameBuffer, SceVideodec2::Vp9PictureInfo* info); + +} + +#endif diff --git a/core/libs/tests/GuestVideodec.cpp b/core/libs/tests/GuestVideodec.cpp index 236336d978..0f8e1c1d18 100644 --- a/core/libs/tests/GuestVideodec.cpp +++ b/core/libs/tests/GuestVideodec.cpp @@ -61,6 +61,7 @@ void testDecode(const std::vector>& units, std::uint32 check(units.size() == PictureHashes.size(), "unexpected access unit count"); std::vector hashes; std::vector timestamps; + std::vector attachments; const auto collect = [&](PictureInfo& picture) { if (!picture.isValid) return; check(picture.framePitch == Pitch && picture.frameHeight == PictureHeight, "unexpected picture geometry"); @@ -69,6 +70,7 @@ void testDecode(const std::vector>& units, std::uint32 check(picture.codec.avc.frameCropTopOffset == cropTop && picture.codec.avc.frameCropBottomOffset == cropBottom, "unexpected crop"); hashes.push_back(hashNv12(session.buffer.data(), Pitch, PictureHeight)); timestamps.push_back(picture.ptsData); + attachments.push_back(picture.attachedData); }; for (std::size_t unit = 0; unit < units.size(); ++unit) { const SceVideodec2::InputData input{sizeof(SceVideodec2::InputData), units[unit].data(), units[unit].size(), 1000 + unit, unit, 0xa0 + unit}; @@ -90,6 +92,7 @@ void testDecode(const std::vector>& units, std::uint32 check(hashes[index] == PictureHashes[index], "picture " + std::to_string(index) + " differs from the reference"); const auto unit = DisplayOrderUnits[index]; check(timestamps[index] == 1000 + unit, "pictures out of display order"); + check(attachments[index] == 0xa0 + unit, "picture attachment out of display order"); } } From bf47365ffa469e032eae79967920d95723dff457 Mon Sep 17 00:00:00 2001 From: AZLisme <8706522+AZLisme@users.noreply.github.com> Date: Mon, 26 Oct 2026 14:11:59 +0800 Subject: [PATCH 160/198] test(videodec2): stop requiring placeholder metadata errors --- core/libs/tests/GuestHevcMetadata.cpp | 36 ++++++++++++++------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/core/libs/tests/GuestHevcMetadata.cpp b/core/libs/tests/GuestHevcMetadata.cpp index 062ebca88f..55775f20db 100644 --- a/core/libs/tests/GuestHevcMetadata.cpp +++ b/core/libs/tests/GuestHevcMetadata.cpp @@ -450,7 +450,7 @@ void GuestMetadataWrites() { } } -void RejectUnchanged(Decoder& decoder); +void CheckProjectionWrites(Decoder& decoder); void CheckCommon(const Videodec2Hevc::PictureInfo& info, unsigned bitDepth) { Require(info.width == 64 && info.height == 40, "coded luma dimensions"); @@ -516,7 +516,7 @@ void Single(std::span stream, unsigned bitDepth, unsigned bo if (!decoder.output.isValid) Require(sceVideodec2Flush_nid_postfix(decoder.handle, &decoder.frame, &decoder.output) == 0, "single-picture drain"); if (bottomCrop != 0) { Require(decoder.output.isValid, "cropped picture remains decodable"); - RejectUnchanged(decoder); + CheckProjectionWrites(decoder); return; } const auto info = decoder.Query(); @@ -568,10 +568,12 @@ void UnsupportedPicture() { std::array before; std::memcpy(before.data(), &untouched, before.size()); std::memset(before.data() + 8, 0, 4); - Rejects([&] { sceVideodec2GetHevcPictureInfo_nid_postfix(&decoder.output, &untouched); }, "unknown SEI metadata did not throw"); - Require(std::memcmp(before.data(), &untouched, before.size()) == 0, "unsupported query changed more than validity word"); - Rejects([&] { sceVideodec2GetPictureInfo_nid_postfix(&decoder.output, reinterpret_cast(&untouched), nullptr); }, "generic unknown metadata did not throw"); - Require(std::memcmp(before.data(), &untouched, before.size()) == 0, "generic unsupported query changed more than validity word"); + const bool typedCompleted = TryProjection([&] { sceVideodec2GetHevcPictureInfo_nid_postfix(&decoder.output, &untouched); }); + if (!typedCompleted) Require(std::memcmp(before.data(), &untouched, before.size()) == 0, "unsupported query changed more than validity word"); + std::memset(&untouched, 0x5a, sizeof(untouched)); + untouched.thisSize = sizeof(untouched); + const bool genericCompleted = TryProjection([&] { sceVideodec2GetPictureInfo_nid_postfix(&decoder.output, reinterpret_cast(&untouched), nullptr); }); + if (!genericCompleted) Require(std::memcmp(before.data(), &untouched, before.size()) == 0, "generic unsupported query changed more than validity word"); Require(sceVideodec2Reset_nid_postfix(decoder.handle) == 0, "clear unsupported metadata on reset"); const auto clean = HevcFixture::hevcUnits(HevcFixture::HevcStream.data(), HevcFixture::HevcStream.size()); decoder.Decode(clean[0], 80); @@ -639,15 +641,15 @@ std::vector Replacement(unsigned wanted) { return {}; } -void RejectUnchanged(Decoder& decoder) { +void CheckProjectionWrites(Decoder& decoder) { Videodec2Hevc::PictureInfo info; std::memset(&info, 0x6b, sizeof(info)); info.thisSize = sizeof(info); std::array before; std::memcpy(before.data(), &info, sizeof(info)); std::memset(before.data() + 8, 0, 4); - Rejects([&] { sceVideodec2GetHevcPictureInfo_nid_postfix(&decoder.output, &info); }, "ambiguous replacement metadata succeeded"); - Require(std::memcmp(before.data(), &info, sizeof(info)) == 0, "ambiguous replacement query changed more than validity word"); + const bool completed = TryProjection([&] { sceVideodec2GetHevcPictureInfo_nid_postfix(&decoder.output, &info); }); + if (!completed) Require(std::memcmp(before.data(), &info, sizeof(info)) == 0, "ambiguous replacement query changed more than validity word"); } void ParameterReplacements() { @@ -664,14 +666,14 @@ void ParameterReplacements() { decoder.Decode(type == 0 ? original : Replacement(type), 20); Require(sceVideodec2Flush_nid_postfix(decoder.handle, &decoder.frame, &decoder.output) == 0 && decoder.output.isValid, "decode repeated or replaced parameter sets"); if (type == 0) Require(decoder.Query().attachedData == 3020, "identical repeated headers remain supported"); - else RejectUnchanged(decoder); + else CheckProjectionWrites(decoder); Videodec2Hevc::PictureInfo old{sizeof(old)}; Require(sceVideodec2GetHevcPictureInfo_nid_postfix(&previous, &old) == 0 && old.isValid && old.attachedData == 3010, "previous picture metadata remains immutable after parameter updates"); if (type != 0) { Require(sceVideodec2Reset_nid_postfix(decoder.handle) == 0, "reset after replacement"); decoder.Decode(original, 30); Require(sceVideodec2Flush_nid_postfix(decoder.handle, &decoder.frame, &decoder.output) == 0 && decoder.output.isValid, "decode after replacement reset"); - RejectUnchanged(decoder); + CheckProjectionWrites(decoder); } } Decoder recreated(2); @@ -703,7 +705,7 @@ void ExtendedMetadata() { windowed.Decode({HevcMetadataFixture::Windowed.begin(), HevcMetadataFixture::Windowed.end()}, 51); if (!windowed.output.isValid) Require(sceVideodec2Flush_nid_postfix(windowed.handle, &windowed.frame, &windowed.output) == 0, "windowed picture flush"); Require(windowed.output.isValid, "windowed picture remains decodable"); - RejectUnchanged(windowed); + CheckProjectionWrites(windowed); } void CabacPadding() { @@ -732,12 +734,12 @@ void MalformedParameterHistory() { catch (const std::runtime_error&) { failed = true; } if (!failed) { Require(sceVideodec2Flush_nid_postfix(decoder.handle, &decoder.frame, &decoder.output) == 0 && decoder.output.isValid, "malformed parameter output"); - RejectUnchanged(decoder); + CheckProjectionWrites(decoder); } Require(sceVideodec2Reset_nid_postfix(decoder.handle) == 0, "reset malformed parameter state"); decoder.Decode(original, 72); Require(sceVideodec2Flush_nid_postfix(decoder.handle, &decoder.frame, &decoder.output) == 0 && decoder.output.isValid, "clean decode after malformed parameter reset"); - RejectUnchanged(decoder); + CheckProjectionWrites(decoder); } void TransferAssociation() { @@ -753,12 +755,12 @@ void TransferAssociation() { unsigned pictures = 0; for (std::size_t index = 0; index < units.size(); ++index) { decoder.Decode(units[index], index); - if (decoder.output.isValid) { RejectUnchanged(decoder); ++pictures; } + if (decoder.output.isValid) { CheckProjectionWrites(decoder); ++pictures; } } for (;;) { Require(sceVideodec2Flush_nid_postfix(decoder.handle, &decoder.frame, &decoder.output) == 0, "conflicting transfer drain"); if (!decoder.output.isValid) break; - RejectUnchanged(decoder); + CheckProjectionWrites(decoder); ++pictures; } Require(pictures == 6, "conflicting transfers retain all decoded pictures"); @@ -774,7 +776,7 @@ void TransferAssociation() { Require(sceVideodec2Reset_nid_postfix(decoder.handle) == 0, "reset pictureless transfer packet"); decoder.Decode(original[0], 92); Require(sceVideodec2Flush_nid_postfix(decoder.handle, &decoder.frame, &decoder.output) == 0 && decoder.output.isValid, "decode after pictureless transfer"); - RejectUnchanged(decoder); + CheckProjectionWrites(decoder); } } From 6721b44a95ecbca7d58389242546a217dde317f7 Mon Sep 17 00:00:00 2001 From: AZLisme <8706522+AZLisme@users.noreply.github.com> Date: Mon, 26 Oct 2026 14:42:57 +0800 Subject: [PATCH 161/198] fix(videodec2): normalize internal VP9 metadata snapshots --- core/libs/prx/libSceVdecsw/Export.cpp | 2 -- core/libs/prx/libSceVideodec2/Export.cpp | 11 +++-------- .../prx/libSceVideodec2/include/PictureMetadata.hpp | 1 - core/libs/tests/GuestVideodec2.cpp | 11 ++++++++++- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libSceVdecsw/Export.cpp b/core/libs/prx/libSceVdecsw/Export.cpp index b352040051..8eab884339 100644 --- a/core/libs/prx/libSceVdecsw/Export.cpp +++ b/core/libs/prx/libSceVdecsw/Export.cpp @@ -133,8 +133,6 @@ PictureInfo pictureInfo(std::uint64_t handle, const SceVideodec2::OutputInfo& ou SceVideodec2::Vp9PictureInfo vp9PictureInfo(std::uint64_t handle, const SceVideodec2::OutputInfo& output) { SceVideodec2::Vp9PictureInfo info{sizeof(SceVideodec2::Vp9PictureInfo)}; Videodec2Vp9PictureInfo_nid_no_patch(handle, output.frameBuffer, &info); - - if (!info.isValid) throw std::runtime_error("Vdecsw: decoded picture has no VP9 picture info"); return info; } diff --git a/core/libs/prx/libSceVideodec2/Export.cpp b/core/libs/prx/libSceVideodec2/Export.cpp index ac97797603..3ffb7a3ca4 100644 --- a/core/libs/prx/libSceVideodec2/Export.cpp +++ b/core/libs/prx/libSceVideodec2/Export.cpp @@ -749,13 +749,6 @@ void Videodec2PictureHeader_nid_no_patch(std::uint64_t handle, const void* frame *header = {stored.pts, stored.dts, stored.attached, stored.idr, stored.hevcIdr, decoder.profile, decoder.level, stored.crop, stored.bitDepthMinus8}; } -void Videodec2PictureCrop_nid_no_patch(std::uint64_t handle, const void* frameBuffer, Crop* crop) { - if (!crop) throw std::runtime_error("Videodec2: invalid argument pointer"); - PictureHeader header{}; - Videodec2PictureHeader_nid_no_patch(handle, frameBuffer, &header); - *crop = header.crop; -} - void Videodec2Vp9PictureInfo_nid_no_patch(std::uint64_t handle, const void* frameBuffer, Vp9PictureInfo* info) { if (!info) throw std::runtime_error("Videodec2: invalid argument pointer"); const auto found = FindDecoder(handle); @@ -765,7 +758,9 @@ void Videodec2Vp9PictureInfo_nid_no_patch(std::uint64_t handle, const void* fram if (decoder.codec != CodecVp9) throw std::runtime_error("Videodec2: VP9 picture info requested for another codec"); const auto picture = decoder.pictures.find(frameBuffer); if (picture == decoder.pictures.end()) throw std::runtime_error("Videodec2: unknown frame buffer"); - *info = picture->second.vp9; + auto snapshot = picture->second.vp9; + snapshot.thisSize = sizeof(*info); + *info = snapshot; } } diff --git a/core/libs/prx/libSceVideodec2/include/PictureMetadata.hpp b/core/libs/prx/libSceVideodec2/include/PictureMetadata.hpp index 108f7ac3cc..c8d505c5cb 100644 --- a/core/libs/prx/libSceVideodec2/include/PictureMetadata.hpp +++ b/core/libs/prx/libSceVideodec2/include/PictureMetadata.hpp @@ -29,7 +29,6 @@ struct PictureHeader { extern "C" { void Videodec2PictureHeader_nid_no_patch(std::uint64_t handle, const void* frameBuffer, SceVideodec2::Internal::PictureHeader* header); -void Videodec2PictureCrop_nid_no_patch(std::uint64_t handle, const void* frameBuffer, SceVideodec2::Internal::Crop* crop); void Videodec2Vp9PictureInfo_nid_no_patch(std::uint64_t handle, const void* frameBuffer, SceVideodec2::Vp9PictureInfo* info); } diff --git a/core/libs/tests/GuestVideodec2.cpp b/core/libs/tests/GuestVideodec2.cpp index 62d854fbd0..f22f3285da 100644 --- a/core/libs/tests/GuestVideodec2.cpp +++ b/core/libs/tests/GuestVideodec2.cpp @@ -1,4 +1,5 @@ #include "prx/libSceVideodec2/include/Api.hpp" +#include "prx/libSceVideodec2/include/PictureMetadata.hpp" #include #include #include @@ -205,7 +206,15 @@ void testVp9PictureMetadata() { struct Guarded { Vp9PictureInfo info{sizeof(Vp9PictureInfo)}; std::uint64_t guard = 0x123456789abcdef0ull; } guarded; check(sceVideodec2GetVp9PictureInfo_nid_postfix(&output, &guarded.info) == 0, "VP9 metadata query failed"); check(guarded.guard == 0x123456789abcdef0ull && guarded.info.thisSize == sizeof(Vp9PictureInfo), "VP9 info overwrote caller storage"); - return guarded.info; + struct Snapshot { Vp9PictureInfo info{}; std::uint64_t guard = 0xfedcba9876543210ull; } snapshot; + Videodec2Vp9PictureInfo_nid_no_patch(handle, output.frameBuffer, &snapshot.info); + check(snapshot.info.thisSize == sizeof(Vp9PictureInfo) && snapshot.guard == 0xfedcba9876543210ull, "internal VP9 snapshot size or boundary differs"); + const auto& same = snapshot.info; + const auto& expected = guarded.info; + check(same.isValid && same.ptsData == expected.ptsData && same.dtsData == expected.dtsData && same.attachedData == expected.attachedData && same.profile == expected.profile && same.level == expected.level && + same.keyFrameFlag == expected.keyFrameFlag && same.intraOnly == expected.intraOnly && same.bitDepth == expected.bitDepth && same.colorRange == expected.colorRange && same.colorSpace == expected.colorSpace && same.numFrames == expected.numFrames && + same.frameWidth == expected.frameWidth && same.frameHeight == expected.frameHeight && same.renderWidth == expected.renderWidth && same.renderHeight == expected.renderHeight, "internal VP9 snapshot differs from decoded picture metadata"); + return snapshot.info; }; const auto observeUntracked = [&](const OutputInfo& output) { struct Guarded { Vp9PictureInfo info{sizeof(Vp9PictureInfo)}; std::uint64_t guard = 0x123456789abcdef0ull; } guarded; From fc4e4616c994bd2582e2ec5340ee2679ec928bf4 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sat, 24 Oct 2026 01:51:53 +0000 Subject: [PATCH 162/198] fix(libc): preserve tgammaf underflow sign --- core/libs/prx/libc/src/Math.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index 305b915f41..faccbcc855 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -91,6 +91,14 @@ float SinhfExp(float x) { return y * std::bit_cast(static_cast(0x7f + k) << 23); } +float GuestTgammaf(float x) { + const float result = static_cast(std::tgamma(static_cast(x))); + if (result != 0.0f || !std::isfinite(x) || x >= 0.0f || std::trunc(x) == x) return result; + const double interval = std::floor(-static_cast(x)); + const bool negative = std::fmod(interval, 2.0) == 0.0; + return std::copysign(0.0f, negative ? -1.0f : 1.0f); +} + float AtanhfLog1p(float x) { constexpr float Ln2High = 6.9313812256e-01f, Ln2Low = 9.0580006145e-06f; constexpr float Lp1 = 6.6666668653e-01f, Lp2 = 4.0000000596e-01f, Lp3 = 2.8571429849e-01f, Lp4 = 2.2222198546e-01f; @@ -470,7 +478,7 @@ long double APS5_VABI log1pl_nid_postfix(long double x) { return std::log1p(x); long double APS5_VABI log2l_nid_postfix(long double x) { return std::log2(x); } long double APS5_VABI lgammal_nid_postfix(long double x) { return std::lgamma(x); } long double APS5_VABI tgammal_nid_postfix(long double x) { return std::tgamma(x); } -float APS5_VABI tgammaf_nid_postfix(float x) { return static_cast(std::tgamma(static_cast(x))); } +float APS5_VABI tgammaf_nid_postfix(float x) { return GuestTgammaf(x); } long double APS5_VABI hypotl_nid_postfix(long double x, long double y) { if (std::isinf(x) || std::isinf(y)) return std::numeric_limits::infinity(); return std::hypot(x, y); From bcd99084b4a0ccab22c9b4ea44dc2bd8277bc713 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sat, 24 Oct 2026 01:52:05 +0000 Subject: [PATCH 163/198] test(libc): cover signed tgammaf underflow --- core/libs/tests/GuestMath.cpp | 4 ++++ docs/dev/debt/libc.md | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index ea2219eb63..1d21126b38 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -1594,6 +1594,10 @@ static void CheckLongDoubleFunctions() { Require(tgammaf_nid_postfix(5.0f) == 24.0f && std::abs(tgammaf_nid_postfix(0.5f) - 1.77245385f) < 1e-6f); Require(tgammaf_nid_postfix(1.0f) == 1.0f && tgammaf_nid_postfix(2.0f) == 1.0f && tgammaf_nid_postfix(3.0f) == 2.0f); Require(std::isinf(tgammaf_nid_postfix(0.0f)) && std::isnan(tgammaf_nid_postfix(-1.0f))); + const float negativeGammaUnderflow = tgammaf_nid_postfix(-4260.884765625f); + const float positiveGammaUnderflow = tgammaf_nid_postfix(-4261.2f); + Require(negativeGammaUnderflow == 0.0f && std::signbit(negativeGammaUnderflow)); + Require(positiveGammaUnderflow == 0.0f && !std::signbit(positiveGammaUnderflow)); Require(hypotl_nid_postfix(3.0L, -4.0L) == 5.0L && hypotl_nid_postfix(nan, -infinity) == infinity); Require(hypot3f_nid_postfix(3.0f, -4.0f, 12.0f) == 13.0f && hypot3f_nid_postfix(0.0f, 3.0f, -4.0f) == 5.0f); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index d404b3f2bb..089437e6ab 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -126,7 +126,7 @@ - [tanh](../../../core/libs/prx/libc/src/Math.cpp) (libc) does not compare floating-point exceptions, and its double inputs are checked by sampling, not exhaustively. It is FreeBSD msun's [`s_tanh.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/s_tanh.c) over this file's msun `Expm1`, with `tiny` read at run time; it sets no `errno`, as the titles' `libc.prx` does not. It gives the same bits as the `tanh` bundled with PPSA07923 and PPSA05916 on 16 million random and 68,211 structured arguments on a Windows host; the FMA-contracted build bundled with PPSA13934 differs on 1,054 of them. - [sinhf](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the `sinhf` of the `libc.prx` bundled with PPSA28319 (disassembly): FreeBSD msun's [`e_sinhf.c`](https://github.com/freebsd/freebsd-src/blob/5aa554c7e5b6fd9042986df07f12ac5687e41392/lib/msun/src/e_sinhf.c) from before `__ldexp_expf`, which squares `expf(|x| / 2)` between `logf(FLT_MAX)` and the overflow threshold, calling msun's [`expm1f`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/s_expm1f.c) and [`expf`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/e_expf.c), whose paths for the arguments it passes are inlined here. Past the threshold Sony's error helper sets `errno` to `ERANGE` before returning `x * 1e37`; msun itself sets no `errno`. That code, run on a Windows host, gives the same bits as this implementation for every finite argument up to the threshold, of both signs; it was not run on Linux. That the system `libSceLibcInternal`, which Ruffle (PPSA68091) and RetroArch (PPSA99169) import while bundling a zero-return `libc.prx`, has the same code is assumed - [acoshf](../../../core/libs/prx/libc/src/Math.cpp) (libc) exact system `libSceLibcInternal` behavior and FMA-contracted copies remain unverified. Positive finite inputs use [msun](https://github.com/freebsd/freebsd-src/blob/5aa554c7e5b6fd9042986df07f12ac5687e41392/lib/msun/src/e_acoshf.c) range handling and this file's `logf`; the middle range divides -1 before addition under directed rounding. Bits, errno and SSE/x87 status and controls match the supplied PPSA03766/PPSA07923 copies on 236,592 positive finite calls (4,929 inputs, four rounding modes, DAZ/FTZ combinations and sticky flags); these samples are not exhaustive. Invalid-domain errno and SSE/x87 invalid flags are checked against two supplied bundled copies; those samples are not exhaustive either. -- [acoshl, asinhl, atanhl, atan2l, cbrtl, erfl, erfcl, expm1l, hypotl, log1pl, log2l, lgammal, tgammal, j0f, j1f, jnf, y0f, y1f and ynf](../../../core/libs/prx/libc/src/Math.cpp) (libc) return the host C library's results (the float Bessel functions through its double ones), not those of the console's own code, so the last bits can differ (`y0f`, `y1f`, `ynf` follow the console at `+-0`). As in the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x56410), `lgammal` does not store the sign in `signgam`; `hypotl` returns infinity for an infinite argument even when the other is a NaN, as C11 F.10.4.3 requires. [hypot3, hypot3f and hypot3l](../../../core/libs/prx/libc/src/Math.cpp) follow that module (0x51D30, 0x520D0, 0x52420): the first NaN of x, y and z is returned unchanged, any infinity gives positive infinity, a zero argument gives the two-argument hypot of the other two, and otherwise the largest magnitude is combined with the hypot of the other two +- [acoshl, asinhl, atanhl, atan2l, cbrtl, erfl, erfcl, expm1l, hypotl, log1pl, log2l, lgammal, tgammal, j0f, j1f, jnf, y0f, y1f and ynf](../../../core/libs/prx/libc/src/Math.cpp) (libc) return the host C library's results (the float Bessel functions through its double ones), not those of the console's own code, so the last bits can differ (`y0f`, `y1f`, `ynf` follow the console at `+-0`). `tgammaf` delegates to the host's double `tgamma` as FreeBSD 11 does ([`s_tgammaf.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/msun/src/s_tgammaf.c)), then restores the sign of a zero result for negative non-integer inputs from the parity of `floor(-x)`; nine title-bundled `libc.prx` builds return that sign for underflowing inputs, where Windows UCRT can return the opposite sign ([#9707](https://github.com/boykopovar/AnyPS5/issues/9707)). As in the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x56410), `lgammal` does not store the sign in `signgam`; `hypotl` returns infinity for an infinite argument even when the other is a NaN, as C11 F.10.4.3 requires. [hypot3, hypot3f and hypot3l](../../../core/libs/prx/libc/src/Math.cpp) follow that module (0x51D30, 0x520D0, 0x52420): the first NaN of x, y and z is returned unchanged, any infinity gives positive infinity, a zero argument gives the two-argument hypot of the other two, and otherwise the largest magnitude is combined with the hypot of the other two - [asinl](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the `asinl` of the `libc.prx` bundled with PPSA28319 (disassembly), FreeBSD msun's [`e_asinl.c`](https://github.com/freebsd/freebsd-src/blob/9f0432041b8fb69832f10c32da50a67f34feeb61/lib/msun/src/e_asinl.c) with the ld80 [`invtrig.c`](https://github.com/freebsd/freebsd-src/blob/9f0432041b8fb69832f10c32da50a67f34feeb61/lib/msun/ld80/invtrig.c) coefficients, which the binary holds as the same 80-bit constants. For |x| > 1, the infinities and NaN, Sony's error helper raises `FE_INVALID` and sets `errno` to `EDOM` before returning `(x - x) / (x - x)`, so a NaN argument also sets `EDOM`; msun itself sets no `errno`. 20 million random arguments in (-1, 1) give the same bits as that code run on a Windows host. The result needs the x87 precision control at 64 bits, as FreeBSD amd64 starts threads (control word `0x37f`), but guest threads do not set it: on Windows a thread started by `_beginthreadex` runs at 53 bits (`0x27f`), where `asinl(0.75)` returns mantissa `0xd91a98ae3406e000` instead of `0xd91a98ae3406e041`. The system `libSceLibcInternal` that RetroArch (PPSA99169) imports is assumed to have the same code - [acosf](../../../core/libs/prx/libc/src/Math.cpp) (libc) - for |x| > 1 and NaN the console stores `EDOM` and raises `FE_INVALID` before returning `(x - x) / (x - x)`; here those return the same NaNs and leave `errno` as it was (#3688). For |x| <= 1 it follows the `acosf` of the `libc.prx` bundled with PPSA28319 (disassembly): FreeBSD msun's [`e_acosf.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/e_acosf.c), with the same constants, `pio2_lo` read at run time and the `0xfffff000` mask on `sqrtf(z)`, which the binary holds. Titles ship different builds of that `libc.prx`: PPSA13934's is FMA-contracted and differs from this one by 1 ulp on 129,170 inputs (#9668), so no single port matches every title's last bit. - [atanhf](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the `atanhf` of the `libc.prx` bundled with PPSA28319 (disassembly): FreeBSD msun's [`e_atanhf.c`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/e_atanhf.c) calling msun's [`log1pf`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/s_log1pf.c), whose paths for the positive arguments it passes are inlined here; the exported `log1pf` is still the host's. For |x| > 1, the infinities and NaN, Sony's error helper raises `FE_INVALID` and sets `errno` to `EDOM` before returning `(x - x) / (x - x)`, and for |x| = 1 it raises `FE_DIVBYZERO` and sets `errno` to `ERANGE` before returning `x / 0`; msun itself sets no `errno`. That code, run on a Windows host, gives the same bits, `errno` and floating-point exceptions as this implementation for all 2^32 arguments; it was not run on Linux. That the system `libSceLibcInternal`, which Dusklight (PPSA99305) imports while bundling a zero-return `libc.prx`, has the same code is assumed From fcdecab20abaf8e1bef2e041e93ab0568b96fe3d Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sun, 25 Oct 2026 02:26:23 +0000 Subject: [PATCH 164/198] fix(libc): fail wide printf on a width or precision beyond int swprintf and the other wide printf calls accumulated the digits of a field width or precision in an int without a bound, so 99999999999 overflowed: UndefinedBehaviorSanitizer reports signed integer overflow at FormattingWide.cpp:130. A '*' width of INT_MIN also overflowed when negated. A width or precision that does not fit an int now fails the call like any other invalid format; glibc's swprintf also returns -1 for it. --- core/libs/prx/libc/src/FormattingWide.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libc/src/FormattingWide.cpp b/core/libs/prx/libc/src/FormattingWide.cpp index b4d7d13bf0..1b6f82dce6 100644 --- a/core/libs/prx/libc/src/FormattingWide.cpp +++ b/core/libs/prx/libc/src/FormattingWide.cpp @@ -84,6 +84,12 @@ template void AppendNumber(std::u16string& out, const std::string& out.append(text.begin(), text.begin() + size); } +int AppendDecimalDigit(int value, char16_t digit) { + const int next = digit - u'0'; + if (value > (INT_MAX - next) / 10) throw std::invalid_argument("Format width or precision out of range"); + return value * 10 + next; +} + std::u16string WideNanText(const std::string& spec, bool negative, bool upper) { char16_t sign = negative ? u'-' : u'\0'; for (std::size_t index = 1; !negative && index < spec.size() && std::strchr("-+ #0", spec[index]); ++index) { @@ -138,10 +144,11 @@ std::u16string FormatWide(const char16_t* format, VaList* source, bool secure = if (*format == u'*') { ++format; width = star(); + if (width == INT_MIN) throw std::invalid_argument("Format width out of range"); if (width < 0) { left = true; spec += '-'; width = -width; } spec += std::to_string(width); } else { - while (*format >= u'0' && *format <= u'9') { width = width * 10 + (*format - u'0'); spec += static_cast(*format++); } + while (*format >= u'0' && *format <= u'9') { width = AppendDecimalDigit(width, *format); spec += static_cast(*format++); } } if (*format == u'.') { ++format; @@ -152,7 +159,7 @@ std::u16string FormatWide(const char16_t* format, VaList* source, bool secure = if (precision >= 0) spec += "." + std::to_string(precision); } else { spec += '.'; - while (*format >= u'0' && *format <= u'9') { precision = precision * 10 + (*format - u'0'); spec += static_cast(*format++); } + while (*format >= u'0' && *format <= u'9') { precision = AppendDecimalDigit(precision, *format); spec += static_cast(*format++); } } } std::string length; From 1cc392feaa4682e3c6716b7431b2db976b30a1ce Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sun, 25 Oct 2026 02:26:23 +0000 Subject: [PATCH 165/198] test(libc): cover wide printf widths and precisions beyond int Digit strings above INT_MAX in a width or a precision, with d, narrow s and wide s conversions, and a '*' width of INT_MIN, must fail swprintf with an empty buffer; a negative '*' width still left-justifies. Run under UndefinedBehaviorSanitizer, the case reports signed integer overflow without the fix. --- core/libs/tests/GuestWideFormatting.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/libs/tests/GuestWideFormatting.cpp b/core/libs/tests/GuestWideFormatting.cpp index 5540d7ee60..94f1d6c431 100644 --- a/core/libs/tests/GuestWideFormatting.cpp +++ b/core/libs/tests/GuestWideFormatting.cpp @@ -2,6 +2,7 @@ #include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libc/include/GuestTime.hpp" +#include #include #include #include @@ -219,6 +220,13 @@ static void CheckSwprintf() { Require(swprintf_nid_postfix(wide, 16, u"%5ls|%-3c|%lc", u"wide", 'z', 0x20ac) == 11, "swprintf padded length"); Require(wide == std::u16string(u" wide|z |€"), "swprintf padded output"); Require(swprintf_nid_postfix(wide, 16, u"%q") < 0 && wide[0] == 0, "swprintf invalid conversion"); + for (const char16_t* format : {u"%99999999999d", u"%.99999999999d", u"%4294967296s", u"%.4294967296s", u"%2147483648ls", u"%.2147483648ls"}) { + wide[0] = u'x'; + Require(swprintf_nid_postfix(wide, 16, format, 7, u"a") < 0 && wide[0] == 0, "swprintf width or precision beyond int"); + } + wide[0] = u'x'; + Require(swprintf_nid_postfix(wide, 16, u"%*d", INT_MIN, 7) < 0 && wide[0] == 0, "swprintf INT_MIN width"); + Require(swprintf_nid_postfix(wide, 16, u"%*d", -5, 7) == 5 && wide == std::u16string(u"7 "), "swprintf negative width"); } static bool WcsftimeThrows(char16_t* buffer, const char16_t* format, const GuestTm* time) { From da736c9b7ce05015e9d379dcf74beaf0ed241095 Mon Sep 17 00:00:00 2001 From: SnekPolylepis <100386333+SnekPolylepis@users.noreply.github.com> Date: Mon, 26 Oct 2026 17:43:39 -0500 Subject: [PATCH 166/198] docs(debt): note the 26 exports that firmware 13.42 does not export Co-Authored-By: Claude Opus 5.5 --- docs/dev/debt/libSceGvMp4Parser.md | 1 + docs/dev/debt/libSceHidControl.md | 1 + docs/dev/debt/libSceJpegParser.md | 1 + docs/dev/debt/libSceMetadataReaderWriter.md | 4 ++++ 4 files changed, 7 insertions(+) diff --git a/docs/dev/debt/libSceGvMp4Parser.md b/docs/dev/debt/libSceGvMp4Parser.md index bcd4ee8972..80098dde53 100644 --- a/docs/dev/debt/libSceGvMp4Parser.md +++ b/docs/dev/debt/libSceGvMp4Parser.md @@ -3,3 +3,4 @@ ### Unknown function info - [_ZN23sceMetadataReaderWriter11gvMp4Parser19gvMp4ParserFinalizeEv, _ZN23sceMetadataReaderWriter11gvMp4Parser21gvMp4ParserInitializeEv, _ZN23sceMetadataReaderWriter11gvMp4Parser23gvMp4ParserReadMetadataERKSsRKNS_8MetadataERS3_, _ZN23sceMetadataReaderWriter11gvMp4Parser24gvMp4ParserWriteMetadataERKSsRKNS_8MetadataE, _ZN23sceMetadataReaderWriter11gvMp4Parser27gvMp4ParserExtractThumbnailERKSsRNS_9ThumbnailE](../../../core/libs/prx/libSceGvMp4Parser/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceGvMp4Parser.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [_ZN23sceMetadataReaderWriter11gvMp4Parser21gvMp4ParserInitializeEv, _ZN23sceMetadataReaderWriter11gvMp4Parser23gvMp4ParserReadMetadataERKSsRKNS_8MetadataERS3_, _ZN23sceMetadataReaderWriter11gvMp4Parser24gvMp4ParserWriteMetadataERKSsRKNS_8MetadataE, _ZN23sceMetadataReaderWriter11gvMp4Parser27gvMp4ParserExtractThumbnailERKSsRNS_9ThumbnailE](../../../core/libs/prx/libSceGvMp4Parser/Export.cpp) - not exported by any firmware 13.42 module, so on 13.42 an import of these NIDs resolves to nothing in the firmware; the 3.20 declarations remain for titles built against older firmware. Not measured here. diff --git a/docs/dev/debt/libSceHidControl.md b/docs/dev/debt/libSceHidControl.md index d7737423de..372680766f 100644 --- a/docs/dev/debt/libSceHidControl.md +++ b/docs/dev/debt/libSceHidControl.md @@ -7,6 +7,7 @@ - [sceHidControlGetJediExtensionUnitInfo, sceHidControlGetJediState, sceHidControlGetSparkState, sceHidControlGetVersionInfo, sceHidControlInit](../../../core/libs/prx/libSceHidControl/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceHidControl.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. - [sceHidControlSetApplicationFocus, sceHidControlSetHandleState, sceHidControlSetJediAudioOutPath, sceHidControlSetJediVolumeGain, sceHidControlSetProcessFocus](../../../core/libs/prx/libSceHidControl/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceHidControl.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. - [sceHidControlSetRevokeList, sceHidControlSetRevokeList2, sceHidControlSetUserColor](../../../core/libs/prx/libSceHidControl/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceHidControl.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [sceHidControlSetRevokeList](../../../core/libs/prx/libSceHidControl/Export.cpp) - not exported by any firmware 13.42 module, so on 13.42 an import of these NIDs resolves to nothing in the firmware; the 3.20 declarations remain for titles built against older firmware. Not measured here. - [sceHidControlEnableMicBeamForming](../../../core/libs/prx/libSceHidControl/Export.cpp) (libSceHidControl) - unknown signature - [sceHidControlSetAllVibrationMute](../../../core/libs/prx/libSceHidControl/Export.cpp) (libSceHidControl) - unknown signature diff --git a/docs/dev/debt/libSceJpegParser.md b/docs/dev/debt/libSceJpegParser.md index 8fcd0951c2..842286c314 100644 --- a/docs/dev/debt/libSceJpegParser.md +++ b/docs/dev/debt/libSceJpegParser.md @@ -3,3 +3,4 @@ ### Unknown function info - [_ZN23sceMetadataReaderWriter10jpegParser18jpegParserFinalizeEv, _ZN23sceMetadataReaderWriter10jpegParser20jpegParserInitializeEv, _ZN23sceMetadataReaderWriter10jpegParser22jpegParserReadMetadataERKSsRKNS_8MetadataERS3_, _ZN23sceMetadataReaderWriter10jpegParser23jpegParserWriteMetadataERKSsRKNS_8MetadataE, _ZN23sceMetadataReaderWriter10jpegParser26jpegParserExtractThumbnailERKSsRNS_9ThumbnailE](../../../core/libs/prx/libSceJpegParser/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceJpegParser.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [_ZN23sceMetadataReaderWriter10jpegParser20jpegParserInitializeEv, _ZN23sceMetadataReaderWriter10jpegParser22jpegParserReadMetadataERKSsRKNS_8MetadataERS3_, _ZN23sceMetadataReaderWriter10jpegParser23jpegParserWriteMetadataERKSsRKNS_8MetadataE, _ZN23sceMetadataReaderWriter10jpegParser26jpegParserExtractThumbnailERKSsRNS_9ThumbnailE](../../../core/libs/prx/libSceJpegParser/Export.cpp) - not exported by any firmware 13.42 module, so on 13.42 an import of these NIDs resolves to nothing in the firmware; the 3.20 declarations remain for titles built against older firmware. Not measured here. diff --git a/docs/dev/debt/libSceMetadataReaderWriter.md b/docs/dev/debt/libSceMetadataReaderWriter.md index 8ed76ec204..d0c5c103e9 100644 --- a/docs/dev/debt/libSceMetadataReaderWriter.md +++ b/docs/dev/debt/libSceMetadataReaderWriter.md @@ -10,3 +10,7 @@ - [_ZN23sceMetadataReaderWriter8MetadataC1Ev, _ZN23sceMetadataReaderWriter8MetadataC2EPFPvmEPFvS1_E, _ZN23sceMetadataReaderWriter8MetadataC2ERKS0_, _ZN23sceMetadataReaderWriter8MetadataC2ERKS0_PFPvmEPFvS3_E, _ZN23sceMetadataReaderWriter8MetadataC2Ev](../../../core/libs/prx/libSceMetadataReaderWriter/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceMetadataReaderWriter.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. - [_ZN23sceMetadataReaderWriter8MetadataD0Ev, _ZN23sceMetadataReaderWriter8MetadataD1Ev, _ZN23sceMetadataReaderWriter8MetadataD2Ev, _ZN23sceMetadataReaderWriter8MetadataaSERKS0_, _ZN23sceMetadataReaderWriter8finalizeEv](../../../core/libs/prx/libSceMetadataReaderWriter/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceMetadataReaderWriter.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. - [_ZNK23sceMetadataReaderWriter8Metadata13getFieldCountEv, _ZNK23sceMetadataReaderWriter8Metadata8getFieldEiRSsRNS_5ValueE, _ZNK23sceMetadataReaderWriter8Metadata8getValueERKSsRNS_5ValueE, _ZNK23sceMetadataReaderWriter8Metadata9dumpValueEv](../../../core/libs/prx/libSceMetadataReaderWriter/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceMetadataReaderWriter.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [_ZN23sceMetadataReaderWriter10initializeEv, _ZN23sceMetadataReaderWriter11getMimeTypeERKSsRSt6vectorIjSaIjEE, _ZN23sceMetadataReaderWriter12getMimeTypesERKSsiPi, _ZN23sceMetadataReaderWriter12readMetadataERKSsRKNS_8MetadataEjRS2_, _ZN23sceMetadataReaderWriter13writeMetadataERKSsRKNS_8MetadataEj](../../../core/libs/prx/libSceMetadataReaderWriter/Export.cpp) - not exported by any firmware 13.42 module, so on 13.42 an import of these NIDs resolves to nothing in the firmware; the 3.20 declarations remain for titles built against older firmware. Not measured here. +- [_ZN23sceMetadataReaderWriter13writeMetadataERKSsRKNS_8MetadataEjPS2_, _ZN23sceMetadataReaderWriter16extractThumbnailERKSsjRNS_9ThumbnailE, _ZN23sceMetadataReaderWriter16getNumOfMimeTypeERKSsPi, _ZN23sceMetadataReaderWriter17getParserInfoListERSt6vectorINS_10ParserInfoESaIS1_EE, _ZN23sceMetadataReaderWriter18registerParserInfoERKSt6vectorINS_10ParserInfoESaIS1_EE](../../../core/libs/prx/libSceMetadataReaderWriter/Export.cpp) - not exported by any firmware 13.42 module, so on 13.42 an import of these NIDs resolves to nothing in the firmware; the 3.20 declarations remain for titles built against older firmware. Not measured here. +- [_ZN23sceMetadataReaderWriter8Metadata11deleteFieldERKSs, _ZN23sceMetadataReaderWriter8Metadata15iterateAllFieldEPFiRKSsRKNS_5ValueEbE, _ZN23sceMetadataReaderWriter8Metadata8getFieldEiRSsRNS_5ValueERb, _ZN23sceMetadataReaderWriter8Metadata8setFieldERKSs, _ZN23sceMetadataReaderWriter8Metadata8setFieldERKSsRKNS_5ValueE](../../../core/libs/prx/libSceMetadataReaderWriter/Export.cpp) - not exported by any firmware 13.42 module, so on 13.42 an import of these NIDs resolves to nothing in the firmware; the 3.20 declarations remain for titles built against older firmware. Not measured here. +- [_ZNK23sceMetadataReaderWriter8Metadata8getFieldEiRSsRNS_5ValueE, _ZNK23sceMetadataReaderWriter8Metadata8getValueERKSsRNS_5ValueE](../../../core/libs/prx/libSceMetadataReaderWriter/Export.cpp) - not exported by any firmware 13.42 module, so on 13.42 an import of these NIDs resolves to nothing in the firmware; the 3.20 declarations remain for titles built against older firmware. Not measured here. From 6988438d7732ed31c71c8fac92176d8431d783d7 Mon Sep 17 00:00:00 2001 From: simnJS Date: Tue, 27 Oct 2026 00:01:56 +0100 Subject: [PATCH 167/198] feat(libSceUsbStorage): pass host volumes through only with APS5_USB_STORAGE_PASSTHROUGH Unset, ScanHostVolumes returns no volume, so nothing is scanned or exposed and the guest sees no USB storage, as on main. The test checks that injected volumes are neither listed nor raised by a rescan with the switch off, and turns it on for the device cases. --- .../prx/libSceUsbStorage/src/HostVolumes.cpp | 11 ++++++ core/libs/tests/GuestUsbStorage.cpp | 36 +++++++++++++++++++ docs/dev/ENVIRONMENT.md | 6 ++++ docs/dev/debt/libSceUsbStorage.md | 2 +- 4 files changed, 54 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceUsbStorage/src/HostVolumes.cpp b/core/libs/prx/libSceUsbStorage/src/HostVolumes.cpp index c34a9a3ff1..d34861756d 100644 --- a/core/libs/prx/libSceUsbStorage/src/HostVolumes.cpp +++ b/core/libs/prx/libSceUsbStorage/src/HostVolumes.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -145,7 +146,17 @@ std::vector ScanSysfsVolumes(const std::filesystem::path& mountInfo, return volumes; } +namespace { + +bool PassthroughEnabled() { + const char* configured = std::getenv("APS5_USB_STORAGE_PASSTHROUGH"); + return configured != nullptr && configured[0] != '\0'; +} + +} + std::vector ScanHostVolumes() { + if (!PassthroughEnabled()) return {}; std::optional> roots; { std::lock_guard lock(rootsMutex); diff --git a/core/libs/tests/GuestUsbStorage.cpp b/core/libs/tests/GuestUsbStorage.cpp index e47a92b4dc..4013c5af5f 100644 --- a/core/libs/tests/GuestUsbStorage.cpp +++ b/core/libs/tests/GuestUsbStorage.cpp @@ -190,6 +190,14 @@ void Inject(const FakeHost& host, bool first, bool second, bool firstMoved = fal SetUsbStorageHostVolumesForTest_nid_no_patch(volumes.empty() ? &none : volumes.data(), volumes.size()); } +void SetPassthrough(bool enabled) { +#ifdef _WIN32 + Require(_putenv_s("APS5_USB_STORAGE_PASSTHROUGH", enabled ? "1" : "") == 0, "set the passthrough switch"); +#else + Require((enabled ? ::setenv("APS5_USB_STORAGE_PASSTHROUGH", "1", 1) : ::unsetenv("APS5_USB_STORAGE_PASSTHROUGH")) == 0, "set the passthrough switch"); +#endif +} + FakeHost CreateHost() { FakeHost host; host.root = std::filesystem::temp_directory_path() / ("anyps5_usb_storage_" + std::to_string(std::random_device{}())); @@ -483,6 +491,32 @@ void TestDeviceGetdents() { Require(ThrowsWith([&] { sceUsbStorageGetdentsClose(fd); }, "sceUsbStorageGetdentsClose not implemented"), "close a closed scan did not throw its marker"); } +void TestPassthroughOff(const FakeHost& host) { + SetPassthrough(false); + Inject(host, true, false); + Require(sceUsbStorageInit(nullptr) == 0, "initialize with the passthrough off"); + std::uint32_t devices[UsbStorage::MaxDevices]{}; + std::int32_t count = -1; + Require(sceUsbStorageGetDeviceList(devices, &count) == 0 && count == 0, "initialize listed a host volume with the passthrough off"); + Require(sceUsbStorageRegisterCallback(UsbStorage::MountEvent, &Record, &g_mountTag) == 0, "register the mount recorder with the passthrough off"); + const auto seen = RecordedCount(); + Inject(host, true, true); + std::this_thread::sleep_for(std::chrono::milliseconds(1500)); + Require(RecordedCount() == seen, "a rescan raised a mount event with the passthrough off"); + count = -1; + Require(sceUsbStorageGetDeviceList(devices, &count) == 0 && count == 0, "a rescan listed a host volume with the passthrough off"); + UsbStorageDeviceInfo info{}; + Require(sceUsbStorageGetDeviceInfo(0, &info) == UsbStorage::DeviceNotFound, "device 0 has info with the passthrough off"); + std::uint8_t exists = 1; + Require(sceUsbStorageIsExist(0, nullptr, &exists) == true && exists == 0, "device 0 exists with the passthrough off"); + Require(sceUsbStorageRequestMap(0, nullptr, 0, 0, nullptr, nullptr) == UsbStorage::DeviceNotFound, "device 0 maps with the passthrough off"); + std::error_code error; + Require(!std::filesystem::equivalent(ResolvePath_nid_no_patch("/mnt/usb0/song.txt"), host.firstMount / "song.txt", error), + "/mnt/usb0 reaches a host volume with the passthrough off"); + Require(sceUsbStorageUnregisterCallback(UsbStorage::MountEvent, &Record) == 0, "unregister the mount recorder with the passthrough off"); + Require(sceUsbStorageTerm() == 0, "terminate with the passthrough off"); +} + void TestDevices(const FakeHost& host) { Inject(host, true, false); Require(sceUsbStorageInit(nullptr) == 0, "initialize with a device"); @@ -638,6 +672,8 @@ int main() { TestMapRequests(); TestGetdents(); TestThrowingExports(); + TestPassthroughOff(host); + SetPassthrough(true); TestDevices(host); TestHotplug(host); TestRootChange(host); diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 1d8ecab03a..a340c9d191 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -534,6 +534,12 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti |------|------|--------|---------|-------------| | `ANYPS5_LANGUAGE` | integer | `core/libs/prx/libSceSystemService/SystemService.hpp` | 1 | console language number returned by sceSystemServiceParamGetInt, defaulting to English US | +### libSceUsbStorage + +| Name | Kind | Source | Default | Description | +|------|------|--------|---------|-------------| +| `APS5_USB_STORAGE_PASSTHROUGH` | presence | `core/libs/prx/libSceUsbStorage/src/HostVolumes.cpp` | | passes the FAT and exFAT volumes the host mounted from USB mass-storage devices through to the guest, writable; unset, no host volume is scanned and the guest sees no USB storage device | + ### libSceVideoOut | Name | Kind | Source | Default | Description | diff --git a/docs/dev/debt/libSceUsbStorage.md b/docs/dev/debt/libSceUsbStorage.md index 8d4954d45e..08030f3d5d 100644 --- a/docs/dev/debt/libSceUsbStorage.md +++ b/docs/dev/debt/libSceUsbStorage.md @@ -11,7 +11,7 @@ ### Functional -- [the host passthrough](../../../core/libs/prx/libSceUsbStorage/src/HostVolumes.cpp) (libSceUsbStorage) - the devices are the FAT and exFAT volumes the host mounted from USB mass-storage devices: on Linux the whole-file-system `vfat`, `msdos` and `exfat` entries of `/proc/self/mountinfo` whose block device has a USB interface of class 08 among its sysfs ancestors, on [Windows](../../../core/libs/prx/libSceUsbStorage/src/HostVolumesWindows.cpp) the drive letters whose FAT, FAT32 or exFAT volume reports `BusTypeUsb`, and none on macOS. NTFS and a FUSE exFAT (`fuseblk`) are not listed. `sceUsbStorageInit` scans the host and a service thread rescans it once a second; a volume keeps its id, the lowest free of eight slots, while it stays mounted, and `/mnt/usb` resolves to its current root until a scan drops it or `sceUsbStorageTerm`, with or without a map request. How the console numbers its devices is unknown. Closes when: measured on a PS5 +- [the host passthrough](../../../core/libs/prx/libSceUsbStorage/src/HostVolumes.cpp) (libSceUsbStorage) - only with `APS5_USB_STORAGE_PASSTHROUGH` set; without it no host volume is scanned. The devices are the FAT and exFAT volumes the host mounted from USB mass-storage devices: on Linux the whole-file-system `vfat`, `msdos` and `exfat` entries of `/proc/self/mountinfo` whose block device sits under a USB interface of class 08 in sysfs, on [Windows](../../../core/libs/prx/libSceUsbStorage/src/HostVolumesWindows.cpp) the drive letters whose FAT, FAT32 or exFAT volume reports `BusTypeUsb`, and none on macOS. NTFS and a FUSE exFAT (`fuseblk`) are not listed. `sceUsbStorageInit` scans the host and a service thread rescans it once a second; a volume keeps its id, the lowest free of eight slots, while it stays mounted, and `/mnt/usb` resolves to its current root until a scan drops it or `sceUsbStorageTerm`. How the console numbers its devices is unknown. Closes when: measured on a PS5 - [sceUsbStorageUnregisterCallback](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - it waits for a callback that is running, except when called from inside a callback, so a callback must not wait on a thread that is unregistering. Whether the console's unregistration waits is unknown. Closes when: measured on a PS5 - [the device-info record](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - the 0x338-byte record is unverified on hardware; its layout follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Platform/UsbStorage.cs#L34-L45), which recovered it from the module's own export: the u64 volume size at offset 0, the vendor, product and serial strings at 0x16, 0x120 and 0x228, and the flags the service masks with 0xd0 at 0x330. The vendor id, product id and release at 0x10, 0x12 and 0x14, the string lengths at 0x118, 0x220 and 0x328 and flag bits 4 (read-only), 6 (FAT) and 7 (exFAT) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/usbd/usbstorage.cpp#L28-L55), whose record otherwise starts with a device id where a title reads the volume size. Closes when: measured on a PS5 - [the device-info values](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - which strings and which size the console reports is unknown. The volume size is the capacity of the mounted file system, not of the partition; the vendor and product are the SCSI inquiry strings (sysfs `vendor` and `model`, or the storage device descriptor on Windows) without trailing spaces, and the serial number is the USB one, empty when the device has none. A string longer than 255 bytes throws. Closes when: measured on a PS5 From 5377922c507a457d3af6926919d8c458b474195d Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sat, 24 Oct 2026 18:29:16 +0000 Subject: [PATCH 168/198] feat(libSceNpWebApi): declare 2 stubs with shadPS4's signatures They still throw through NotImplemented_nid_no_patch; only the parameter lists change. The only source is shadPS4's PS4 prototypes; nothing is measured on a PS5. --- core/libs/prx/libSceNpWebApi/Export.cpp | 4 ++-- core/libs/prx/libSceNpWebApi/include/Api.hpp | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceNpWebApi/Export.cpp b/core/libs/prx/libSceNpWebApi/Export.cpp index 269bbec537..49a0b7357b 100644 --- a/core/libs/prx/libSceNpWebApi/Export.cpp +++ b/core/libs/prx/libSceNpWebApi/Export.cpp @@ -223,7 +223,7 @@ int APS5_VABI sceNpWebApiGetHttpResponseHeaderValueLength() { return 0; } -int APS5_VABI sceNpWebApiGetHttpStatusCode() { +int APS5_VABI sceNpWebApiGetHttpStatusCode(std::int64_t requestId, int* statusCode) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -239,7 +239,7 @@ int APS5_VABI sceNpWebApiInitialize(int libHttpCtxId, std::size_t poolSize) { return SCE_NP_WEBAPI_ERROR_LIB_CONTEXT_MAX; } -int APS5_VABI sceNpWebApiReadData() { +int APS5_VABI sceNpWebApiReadData(std::int64_t requestId, void* data, std::size_t size) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceNpWebApi/include/Api.hpp b/core/libs/prx/libSceNpWebApi/include/Api.hpp index ec9ce3c836..47b0d19b1b 100644 --- a/core/libs/prx/libSceNpWebApi/include/Api.hpp +++ b/core/libs/prx/libSceNpWebApi/include/Api.hpp @@ -25,9 +25,9 @@ int APS5_VABI sceNpWebApiDeleteRequest(std::int64_t requestId); int APS5_VABI sceNpWebApiDeleteServicePushEventFilter(int libCtxId, int filterId); int APS5_VABI sceNpWebApiGetHttpResponseHeaderValue(); int APS5_VABI sceNpWebApiGetHttpResponseHeaderValueLength(); -int APS5_VABI sceNpWebApiGetHttpStatusCode(); +int APS5_VABI sceNpWebApiGetHttpStatusCode(std::int64_t requestId, int* statusCode); int APS5_VABI sceNpWebApiInitialize(int libHttpCtxId, std::size_t poolSize); -int APS5_VABI sceNpWebApiReadData(); +int APS5_VABI sceNpWebApiReadData(std::int64_t requestId, void* data, std::size_t size); int APS5_VABI sceNpWebApiRegisterPushEventCallback(int titleUserCtxId, int filterId, void* callback, void* userArg); int APS5_VABI sceNpWebApiRegisterServicePushEventCallback(int titleUserCtxId, int filterId, void* callback, void* userArg); int APS5_VABI sceNpWebApiSendRequest(std::int64_t requestId, const void* data, std::size_t dataSize); From bbb2e618d032d3fe3ce1bc18e5e33e794baa491e Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Mon, 26 Oct 2026 23:17:33 +0000 Subject: [PATCH 169/198] docs(libSceNpWebApi): record the single-source shadPS4 signatures of the declared stubs --- docs/dev/debt/libSceNpWebApi.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libSceNpWebApi.md b/docs/dev/debt/libSceNpWebApi.md index af76876d59..ce821ad674 100644 --- a/docs/dev/debt/libSceNpWebApi.md +++ b/docs/dev/debt/libSceNpWebApi.md @@ -2,6 +2,7 @@ ### Unknown function info +- [sceNpWebApiGetHttpStatusCode, sceNpWebApiReadData](../../../core/libs/prx/libSceNpWebApi/Export.cpp) - throw through `NotImplemented_nid_no_patch`; behaviour and the PS5 prototypes are unmeasured. The only source of the parameter lists is shadPS4's PS4-derived prototypes (GPLv2, used as a reference only), with no second reference; structure pointers are `void*` because no layout is known. - [sceNpWebApiInitialize, sceNpWebApiTerminate, sceNpWebApiCreateHandle, sceNpWebApiDeleteHandle, sceNpWebApiCreatePushEventFilter, sceNpWebApiDeletePushEventFilter](../../../core/libs/prx/libSceNpWebApi/Export.cpp) (libSceNpWebApi, PS4) - signatures, id ranges (library contexts 1 to 0x7fff, handles and filters below 0xf0000000) and errors (`LIB_CONTEXT_NOT_FOUND`, `HANDLE_NOT_FOUND`, `PUSH_EVENT_FILTER_NOT_FOUND`, `INVALID_ARGUMENT` for a missing filter parameter) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/np/np_web_api/np_web_api_internal.cpp); they are not checked on a console. The libHttp context and pool size are kept but not used, since no request is ever sent. Limbo (CUSA01664) creates a context, a handle and filters when its game starts. - [sceNpWebApiCreateServicePushEventFilter](../../../core/libs/prx/libSceNpWebApi/Export.cpp) (libSceNpWebApi, PS4) - a filter for a PSN service needs the signed-in user, and no user is signed in to PSN, so a valid call returns `SIGNED_IN_USER_NOT_FOUND` after the context and handle checks, as shadPS4 does with its network disabled. `sceNpWebApiDeleteServicePushEventFilter` therefore never finds a filter. - [sceNpWebApiCreateContext, sceNpWebApiDeleteContext, sceNpWebApiCreateRequest, sceNpWebApiDeleteRequest, sceNpWebApiAbortRequest, sceNpWebApiSendRequest](../../../core/libs/prx/libSceNpWebApi/Export.cpp) (libSceNpWebApi, PS4) - the online id is not looked up: as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/np/np_web_api/np_web_api_internal.cpp), a user context belongs to the initial user, so a library context holds at most one. With no user signed in to PSN, `sceNpWebApiSendRequest` returns `NOT_SIGNED_IN` unless the request is aborted. Id layouts and the delete, abort and not-found results match an offline PS4 Pro probe on firmware 10.71 (#9797); other checks follow shadPS4. That homebrew probe, with a non-retail SDK version, accepted method 99, so `INVALID_ARGUMENT` for a method after DELETE from SDK 2.50 is unverified. `sceNpWebApiTerminate` also deletes user contexts and requests; shadPS4 stops after the first user context. From 82fa4474c16ca807bf08ab6bc56e97ae6d9b022d Mon Sep 17 00:00:00 2001 From: Leonardo Date: Tue, 27 Oct 2026 00:18:19 +0100 Subject: [PATCH 170/198] feat(libSceNetCtl): bound sceNetCtlGetNatInfo probe time and cache only on success --- core/libs/prx/libSceNetCtl/Export.cpp | 80 +++++++++++++++++++-------- core/libs/tests/GuestNetCtl.cpp | 33 ++++++----- docs/dev/debt/libSceNetCtl.md | 2 +- 3 files changed, 74 insertions(+), 41 deletions(-) diff --git a/core/libs/prx/libSceNetCtl/Export.cpp b/core/libs/prx/libSceNetCtl/Export.cpp index 2fcb58f748..aa261e6bad 100644 --- a/core/libs/prx/libSceNetCtl/Export.cpp +++ b/core/libs/prx/libSceNetCtl/Export.cpp @@ -24,6 +24,7 @@ #endif #include #include +#include #include #include #include @@ -37,6 +38,7 @@ #include #include #include +#include #include // Network-control state reflects host interface addressing, not PSN sign-in status. @@ -70,6 +72,8 @@ static constexpr std::uint16_t STUN_ATTR_OTHER_ADDRESS = 0x802b; static constexpr std::size_t STUN_HEADER_SIZE = 20; static constexpr std::size_t STUN_TRANSACTION_ID_SIZE = 12; static constexpr int STUN_DEFAULT_TIMEOUT_MS = 300; +static constexpr int STUN_TOTAL_BUDGET_MS = 1000; +static constexpr int STUN_CACHE_TTL_MS = 60000; static constexpr int NET_CTL_INFO_DEVICE = 1; static constexpr int NET_CTL_INFO_ETHER_ADDR = 2; static constexpr int NET_CTL_INFO_MTU = 3; @@ -515,6 +519,7 @@ struct StunConfig { std::uint64_t configGeneration = 1; std::uint64_t cachedConfigGeneration = 0; std::uint32_t cachedLocalAddr = 0; + std::chrono::steady_clock::time_point cachedUntil{}; bool hasCachedResult = false; NetCtlNatInfo cachedResult{}; }; @@ -548,7 +553,7 @@ static void AppendUniqueStunServer(std::vector& servers, const sock servers.push_back(candidate); } -static std::vector ResolveStunServers(int& timeoutMs, std::uint64_t& generationOut) { +static std::vector ResolveStunServers(int& timeoutMs, std::uint64_t& generationOut, std::chrono::steady_clock::time_point totalDeadline) { auto& config = GetStunConfig(); { std::lock_guard guard(config.lock); @@ -569,23 +574,45 @@ static std::vector ResolveStunServers(int& timeoutMs, std::uint64_t {"stun.l.google.com", "19302"}, {"stun1.l.google.com", "19302"}, }; - std::vector servers; - if (!EnsureSocketRuntime()) return servers; - for (const auto& entry : kDefaultHosts) { - addrinfo hints{}; - hints.ai_family = AF_INET; - hints.ai_socktype = SOCK_DGRAM; - hints.ai_protocol = IPPROTO_UDP; - addrinfo* list = nullptr; - if (getaddrinfo(entry.host, entry.port, &hints, &list) != 0 || !list) continue; - for (auto* item = list; item; item = item->ai_next) { - if (item->ai_addr && item->ai_addrlen >= sizeof(sockaddr_in) && item->ai_addr->sa_family == AF_INET) { - AppendUniqueStunServer(servers, *reinterpret_cast(item->ai_addr)); + struct DnsQueryState { + std::mutex lock; + std::condition_variable cv; + std::vector servers; + bool done = false; + }; + const auto now = std::chrono::steady_clock::now(); + if (!EnsureSocketRuntime() || now >= totalDeadline) return {}; + const auto dnsCap = now + std::chrono::milliseconds(STUN_DEFAULT_TIMEOUT_MS * 2); + const auto dnsDeadline = dnsCap < totalDeadline ? dnsCap : totalDeadline; + auto state = std::make_shared(); + std::thread([state, dnsDeadline] { + for (const auto& entry : kDefaultHosts) { + if (std::chrono::steady_clock::now() >= dnsDeadline) break; + addrinfo hints{}; + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_DGRAM; + hints.ai_protocol = IPPROTO_UDP; + addrinfo* list = nullptr; + if (getaddrinfo(entry.host, entry.port, &hints, &list) != 0 || !list) continue; + { + std::lock_guard guard(state->lock); + for (auto* item = list; item; item = item->ai_next) { + if (item->ai_addr && item->ai_addrlen >= sizeof(sockaddr_in) && item->ai_addr->sa_family == AF_INET) { + AppendUniqueStunServer(state->servers, *reinterpret_cast(item->ai_addr)); + } + } } + freeaddrinfo(list); } - freeaddrinfo(list); - } - return servers; + { + std::lock_guard guard(state->lock); + state->done = true; + } + state->cv.notify_one(); + }).detach(); + std::unique_lock lock(state->lock); + state->cv.wait_until(lock, dnsDeadline, [&] { return state->done; }); + return state->servers; } static StunEndpoint DecodeStunIpv4Endpoint(const std::uint8_t* value, std::uint16_t length, bool xorMask) { @@ -636,7 +663,9 @@ static bool ParseStunBindingResponse(const std::uint8_t* packet, std::size_t siz return response.mapped.valid; } -static bool ProbeStunServer(StunSocket descriptor, const sockaddr_in& server, int timeoutMs, StunProbeResponse& response) { +static bool ProbeStunServer(StunSocket descriptor, const sockaddr_in& server, int timeoutMs, std::chrono::steady_clock::time_point totalDeadline, StunProbeResponse& response) { + const auto start = std::chrono::steady_clock::now(); + if (start >= totalDeadline) return false; std::uint8_t request[STUN_HEADER_SIZE] = {}; request[0] = static_cast(STUN_BINDING_REQUEST >> 8); request[1] = static_cast(STUN_BINDING_REQUEST & 0xFF); @@ -646,7 +675,8 @@ static bool ProbeStunServer(StunSocket descriptor, const sockaddr_in& server, in const auto sent = sendto(descriptor, reinterpret_cast(request), sizeof(request), 0, reinterpret_cast(&server), sizeof(server)); if (sent != static_cast(sizeof(request))) return false; - const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeoutMs > 0 ? timeoutMs : 1); + const auto probeDeadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeoutMs > 0 ? timeoutMs : 1); + const auto deadline = probeDeadline < totalDeadline ? probeDeadline : totalDeadline; for (;;) { const auto now = std::chrono::steady_clock::now(); if (now >= deadline) return false; @@ -693,9 +723,10 @@ static NetCtlNatInfo DiscoverNatInfo(in_addr localAddress) { result.nat_type = NET_CTL_NAT_TYPE_NONE; result.mapped_addr = 0; + const auto totalDeadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(STUN_TOTAL_BUDGET_MS); int timeoutMs = STUN_DEFAULT_TIMEOUT_MS; std::uint64_t generation = 0; - const auto servers = ResolveStunServers(timeoutMs, generation); + const auto servers = ResolveStunServers(timeoutMs, generation, totalDeadline); if (servers.empty() || !EnsureSocketRuntime()) return result; const StunSocket descriptor = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); @@ -732,7 +763,7 @@ static NetCtlNatInfo DiscoverNatInfo(in_addr localAddress) { sockaddr_in primaryServer{}; std::size_t primaryIndex = servers.size(); for (std::size_t i = 0; i < servers.size(); ++i) { - if (ProbeStunServer(descriptor, servers[i], timeoutMs, firstResponse)) { + if (ProbeStunServer(descriptor, servers[i], timeoutMs, totalDeadline, firstResponse)) { primaryServer = servers[i]; primaryIndex = i; break; @@ -775,7 +806,7 @@ static NetCtlNatInfo DiscoverNatInfo(in_addr localAddress) { for (const auto& secondary : secondaryServers) { StunProbeResponse secondResponse{}; - if (ProbeStunServer(descriptor, secondary, timeoutMs, secondResponse)) { + if (ProbeStunServer(descriptor, secondary, timeoutMs, totalDeadline, secondResponse)) { CloseStunSocket(descriptor); result.stun_status = NET_CTL_STUN_STATUS_OK; result.nat_type = (secondResponse.mapped.address == firstResponse.mapped.address && @@ -912,20 +943,23 @@ int APS5_VABI sceNetCtlGetNatInfo(NetCtlNatInfo* info) { if (!FindHostIpv4Interface(host)) return SCE_NET_CTL_ERROR_NOT_CONNECTED; auto& config = GetStunConfig(); std::uint64_t generation = 0; + const auto now = std::chrono::steady_clock::now(); { std::lock_guard guard(config.lock); generation = config.configGeneration; - if (config.hasCachedResult && config.cachedConfigGeneration == generation && config.cachedLocalAddr == host.address.s_addr) { + if (config.hasCachedResult && config.cachedConfigGeneration == generation && + config.cachedLocalAddr == host.address.s_addr && now < config.cachedUntil) { *info = config.cachedResult; return 0; } } const NetCtlNatInfo discovered = DiscoverNatInfo(host.address); - { + if (discovered.stun_status == NET_CTL_STUN_STATUS_OK) { std::lock_guard guard(config.lock); if (config.configGeneration == generation) { config.cachedConfigGeneration = generation; config.cachedLocalAddr = host.address.s_addr; + config.cachedUntil = std::chrono::steady_clock::now() + std::chrono::milliseconds(STUN_CACHE_TTL_MS); config.cachedResult = discovered; config.hasCachedResult = true; } diff --git a/core/libs/tests/GuestNetCtl.cpp b/core/libs/tests/GuestNetCtl.cpp index 4a495b3564..9605642d30 100644 --- a/core/libs/tests/GuestNetCtl.cpp +++ b/core/libs/tests/GuestNetCtl.cpp @@ -323,7 +323,7 @@ void CheckNatDiscovery(in_addr address) { natInfo.stun_status = 99; natInfo.nat_type = 99; natInfo.mapped_addr = 0xFFFFFFFFu; - NetCtlSetStunServers_nid_no_patch(&server1Addr, 1, 20); + NetCtlSetStunServers_nid_no_patch(&server1Addr, 1, 40); Require(sceNetCtlGetNatInfo(&natInfo) == 0); Require(natInfo.size == sizeof(natInfo)); Require(natInfo.stun_status == 1); @@ -334,6 +334,21 @@ void CheckNatDiscovery(in_addr address) { RecordUniqueTransactionId(seenIds, ReceiveStunBindingRequest(server1, from)); } + StunTxId openTx{}; + std::thread openWorker([&] { + sockaddr_in client{}; + openTx = ReceiveStunBindingRequest(server1, client); + SendStunBindingResponse(server1, client, openTx, address.s_addr, 0, true); + }); + natInfo = {sizeof(natInfo), 99, 99, 0}; + Require(sceNetCtlGetNatInfo(&natInfo) == 0); + openWorker.join(); + RecordUniqueTransactionId(seenIds, openTx); + Require(natInfo.size == sizeof(natInfo)); + Require(natInfo.stun_status == 2); + Require(natInfo.nat_type == 1); + Require(natInfo.mapped_addr == address.s_addr); + const std::uint32_t externalIp = (ntohl(address.s_addr) == 0xCB00712Au) ? htonl(0xCB00712Bu) : htonl(0xCB00712Au); const std::uint32_t spoofedIp = htonl(0xC0000201u); @@ -355,22 +370,6 @@ void CheckNatDiscovery(in_addr address) { Require(natInfo.nat_type == 0); Require(natInfo.mapped_addr == 0); - NetCtlSetStunServers_nid_no_patch(&server1Addr, 1, 1000); - StunTxId openTx{}; - std::thread openWorker([&] { - sockaddr_in client{}; - openTx = ReceiveStunBindingRequest(server1, client); - SendStunBindingResponse(server1, client, openTx, address.s_addr, 0, true); - }); - natInfo = {sizeof(natInfo), 99, 99, 0}; - Require(sceNetCtlGetNatInfo(&natInfo) == 0); - openWorker.join(); - RecordUniqueTransactionId(seenIds, openTx); - Require(natInfo.size == sizeof(natInfo)); - Require(natInfo.stun_status == 2); - Require(natInfo.nat_type == 1); - Require(natInfo.mapped_addr == address.s_addr); - NetCtlSetStunServers_nid_no_patch(&server1Addr, 1, 1000); StunTxId incompleteTx{}; std::thread incompleteWorker([&] { diff --git a/docs/dev/debt/libSceNetCtl.md b/docs/dev/debt/libSceNetCtl.md index 8c4dbecb3e..c2b8b9a153 100644 --- a/docs/dev/debt/libSceNetCtl.md +++ b/docs/dev/debt/libSceNetCtl.md @@ -18,4 +18,4 @@ - [sceNetCtlGetInfo](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) finds the host's default route in `/proc/net/route` on Linux and, on macOS, in the routing table read with `sysctl(NET_RT_FLAGS, RTF_GATEWAY)`, where an interface-scoped default route ranks after the unscoped one - [sceNetCtlGetIfStat](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) throws on macOS: an unprivileged process reads the interface byte counters (`getifaddrs` and `sysctl(NET_RT_IFLIST2)` alike) reduced to their low 32 bits, so a total would wrap every 4 GiB - [Host interface cache](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - on Windows, `sceNetCtlGetState`, `sceNetCtlGetStateV6`, `sceNetCtlCheckCallback`, `sceNetCtlGetIfStat` and `sceNetCtlGetInfo` reuse the last host interface query until `NotifyUnicastIpAddressChange`, `NotifyIpInterfaceChange` or `NotifyRouteChange2` reports a change, instead of calling `GetAdaptersAddresses` on every call. A DNS server change with no address, interface or route change is seen only at the next such change. If the notifications cannot be registered, every call queries the host as before. -- [sceNetCtlGetNatInfo](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - performs RFC 5389 STUN binding probes from the active host IPv4 interface against `stun.playstation.net` and fallback public STUN servers (`stun.cloudflare.com`, `stun.l.google.com`, `stun1.l.google.com`) instead of reading cached state from the console's background `netctl` service, which probes `*.np.stun.playstation.net` with authenticated Classic STUN (RFC 3489). Console captures confirm NAT type 2 when mapped address and port match across STUN endpoints, NAT type 3 when the mapped port changes across endpoints, and STUN failure when no server responds; NAT type 1 when the mapped endpoint matches the local socket is not measured on a console with a direct public IP. +- [sceNetCtlGetNatInfo](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - performs RFC 5389 STUN binding probes from the active host IPv4 interface against `stun.playstation.net` and fallback third-party STUN servers (`stun.cloudflare.com`, `stun.l.google.com`, `stun1.l.google.com`) instead of reading cached state from the console's background `netctl` service, which probes `*.np.stun.playstation.net` with authenticated Classic STUN (RFC 3489); those third-party hosts receive the host's IPv4 address when an uncached call resolves or probes them. Each discovery call has a 1000 ms total time budget (DNS resolution included, 300 ms per UDP probe), and successful results are cached for 60 s per local IPv4 address while failures re-probe on the next call. Console captures confirm NAT type 2, NAT type 3, and STUN failure; NAT type 1 when the mapped endpoint matches the local socket is not measured on a console with a direct public IP. From 08fa6617660f328efbbf736d1b23ee15f9a798e7 Mon Sep 17 00:00:00 2001 From: Alexandre Figueiredo Date: Sun, 25 Oct 2026 22:22:56 +0000 Subject: [PATCH 171/198] fix(libSceNpCppWebApi): answer finish, abort and getId for never-started transactions as the module does --- core/libs/prx/libSceNpCppWebApi/Export.cpp | 80 ++++++++----------- .../prx/libSceNpCppWebApi/include/Api.hpp | 10 +-- core/libs/tests/GuestNpCppWebApiSignedOut.cpp | 60 +++++++++++++- docs/dev/debt/libSceNpCppWebApi.md | 4 +- 4 files changed, 98 insertions(+), 56 deletions(-) diff --git a/core/libs/prx/libSceNpCppWebApi/Export.cpp b/core/libs/prx/libSceNpCppWebApi/Export.cpp index 8ac2f96323..c8e9a9ca52 100644 --- a/core/libs/prx/libSceNpCppWebApi/Export.cpp +++ b/core/libs/prx/libSceNpCppWebApi/Export.cpp @@ -8,6 +8,7 @@ #include "prx/libc/include/General.hpp" static constexpr int SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN = static_cast(0x80552907); +static constexpr int TransactionNotStarted = static_cast(0x80553508); // Offline PSN: request entry points (API calls, transaction start/readData, factories) fail with // SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN, the constant shadPS4/fpPS4 document for this code; parameter @@ -419,7 +420,7 @@ int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS } int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_12Leaderboards2V122GetRankingResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv() { - return 0; + return TransactionNotStarted; } int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_12Leaderboards2V123RecordScoreResponseBodyEEENS4_INS6_9RecordApi26RecordScoreResponseHeadersEEEE5startEPNS2_10LibContextE() { @@ -427,11 +428,11 @@ int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS } int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_12Leaderboards2V123RecordScoreResponseBodyEEENS4_INS6_9RecordApi26RecordScoreResponseHeadersEEEE6finishEv() { - return 0; + return TransactionNotStarted; } int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_12Leaderboards2V127RecordLargeDataResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv() { - return 0; + return TransactionNotStarted; } int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_12Leaderboards2V130GetBoardDefinitionResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE() { @@ -439,7 +440,7 @@ int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS } int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_12Leaderboards2V130GetBoardDefinitionResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv() { - return 0; + return TransactionNotStarted; } int APS5_VABI _ZN3sce2Np9CppWebApi6Common19UpStreamTransactionINS2_12IntrusivePtrINS1_12Leaderboards2V127RecordLargeDataResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextEm() { @@ -1272,7 +1273,7 @@ void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V1 } int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V130PostPlayerSessionsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv() { - return 0; + return TransactionNotStarted; } bool APS5_VABI _ZNK3sce2Np9CppWebApi14SessionManager2V136UsersPlayerSessionsInvitationForRead14sessionIdIsSetEv(const void* self) { @@ -1438,7 +1439,7 @@ void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V1 } int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V152PostPlayerSessionsSessionIdMemberPlayersResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv() { - return 0; + return TransactionNotStarted; } int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_15DefaultResponseENS2_12IntrusivePtrINS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE() { @@ -1446,7 +1447,7 @@ int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_15DefaultResponse } int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_15DefaultResponseENS2_12IntrusivePtrINS2_18ResponseHeaderBaseEEEE6finishEv() { - return 0; + return TransactionNotStarted; } bool APS5_VABI _ZNK3sce2Np9CppWebApi14SessionManager2V136UsersPlayerSessionsInvitationForRead22receivedTimestampIsSetEv(const void* self) { @@ -1465,7 +1466,7 @@ void APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14Sessi } int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V154GetUsersAccountIdPlayerSessionsInvitationsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv() { - return 0; + return TransactionNotStarted; } int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_15DefaultResponseENS2_12IntrusivePtrINS2_18ResponseHeaderBaseEEEE11getResponseERS4_() { @@ -1668,8 +1669,7 @@ void* APS5_VABI _ZN3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_17Ti } void APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS2_6VectorINS4_INS1_13InGameCatalog2V59ContainerEEEEEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void* self) { - (void)self; - NotImplemented_nid_no_patch(__func__); + std::memset(self, 0, 0x10); } int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS2_6VectorINS4_INS1_13InGameCatalog2V59ContainerEEEEEEENS4_INS2_18ResponseHeaderBaseEEEE11getResponseERSB_(const void* self, void* response) { @@ -1681,12 +1681,10 @@ int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS2_6 void APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS2_6VectorINS4_INS1_13InGameCatalog2V59ContainerEEEEEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void* self) { (void)self; - NotImplemented_nid_no_patch(__func__); } void APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_17TitleCloudStorage2V132GetMultiDataStatusesResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void* self) { - (void)self; - NotImplemented_nid_no_patch(__func__); + std::memset(self, 0, 0x10); } int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_17TitleCloudStorage2V132GetMultiDataStatusesResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE11getResponseERS8_(const void* self, void* response) { @@ -1698,13 +1696,11 @@ int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_1 void APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_17TitleCloudStorage2V132GetMultiDataStatusesResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void* self) { (void)self; - NotImplemented_nid_no_patch(__func__); } int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS2_6VectorINS4_INS1_13InGameCatalog2V59ContainerEEEEEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv(void* self) { (void)self; - NotImplemented_nid_no_patch(__func__); - return 0; + return TransactionNotStarted; } int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS2_6VectorINS4_INS1_13InGameCatalog2V59ContainerEEEEEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE(void* self, void* context) { @@ -1716,8 +1712,7 @@ int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_17TitleCloudStorage2V132GetMultiDataStatusesResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv(void* self) { (void)self; - NotImplemented_nid_no_patch(__func__); - return 0; + return TransactionNotStarted; } int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_17TitleCloudStorage2V132GetMultiDataStatusesResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE(void* self, void* context) { @@ -2418,7 +2413,7 @@ int APS5_VABI _ZN3sce2Np9CppWebApi6Common13ConstIteratorImED2Ev(void* self) { } int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V161PostPlayerSessionsSessionIdJoinableSpecifiedUsersResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv() { - return 0; + return TransactionNotStarted; } void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_14SessionManager2V127ResponsePlayerSessionPlayerEEEEEED1Ev(IntrusivePtr* self) { @@ -2430,8 +2425,7 @@ int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS } int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V129GetPlayerSessionsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5abortEv() { - NotImplemented_nid_no_patch(__func__); - return 0; + return TransactionNotStarted; } void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V149PostPlayerSessionsSessionIdInvitationsRequestBodyEEC1Ev(IntrusivePtr* self) { @@ -2439,7 +2433,7 @@ void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V1 } int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V129GetPlayerSessionsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv() { - return 0; + return TransactionNotStarted; } int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V112JoinableUserEEEE8pushBackERKS8_(IntrusivePtr* self, const void* element) { @@ -2451,7 +2445,7 @@ void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V1 } int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V150PostPlayerSessionsSessionIdInvitationsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv() { - return 0; + return TransactionNotStarted; } void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V113PlayerSessionEEaSERS7_(IntrusivePtr* self, const IntrusivePtr* source) { @@ -2488,7 +2482,7 @@ int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS } int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv() { - return 0; + return TransactionNotStarted; } int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEE28setResponseInformationOptionEP37SceNpWebApi2ResponseInformationOption() { @@ -2500,7 +2494,7 @@ int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS } int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv() { - return 0; + return TransactionNotStarted; } int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE28setResponseInformationOptionEP37SceNpWebApi2ResponseInformationOption() { @@ -2512,7 +2506,7 @@ int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS } int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv() { - return 0; + return TransactionNotStarted; } int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEE28setResponseInformationOptionEP37SceNpWebApi2ResponseInformationOption() { @@ -2524,7 +2518,7 @@ int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS } int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv() { - return 0; + return TransactionNotStarted; } int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEE28setResponseInformationOptionEP37SceNpWebApi2ResponseInformationOption() { @@ -2536,7 +2530,7 @@ int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS } int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv() { - return 0; + return TransactionNotStarted; } int APS5_VABI _ZN3sce2Np9CppWebApi6Common18ResponseHeaderBase14getHeaderValueEPKcRNS2_12IntrusivePtrINS2_6StringEEE() { @@ -2545,8 +2539,7 @@ int APS5_VABI _ZN3sce2Np9CppWebApi6Common18ResponseHeaderBase14getHeaderValueEPK } int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V150PostPlayerSessionsSessionIdInvitationsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5abortEv() { - NotImplemented_nid_no_patch(__func__); - return 0; + return TransactionNotStarted; } int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_11UserProfile2V113BasicPresenceEEEE3endEv() { @@ -2781,8 +2774,7 @@ void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V1 } int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V161PostPlayerSessionsSessionIdJoinableSpecifiedUsersResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5abortEv() { - NotImplemented_nid_no_patch(__func__); - return 0; + return TransactionNotStarted; } int APS5_VABI _ZNK3sce2Np9CppWebApi6Common6VectorImE4sizeEv() { @@ -2790,8 +2782,7 @@ int APS5_VABI _ZNK3sce2Np9CppWebApi6Common6VectorImE4sizeEv() { return 0; } -int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V129GetPlayerSessionsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5getIdEv() { - NotImplemented_nid_no_patch(__func__); +std::int64_t APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V129GetPlayerSessionsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5getIdEv() { return 0; } @@ -2837,8 +2828,7 @@ int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_14 } int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V130PostPlayerSessionsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5abortEv() { - NotImplemented_nid_no_patch(__func__); - return 0; + return TransactionNotStarted; } int APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_11UserProfile2V113BasicPresenceEEEEneERKS9_() { @@ -2896,8 +2886,7 @@ void APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14Sessi self->libContext = libContext; } -int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V161PostPlayerSessionsSessionIdJoinableSpecifiedUsersResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5getIdEv() { - NotImplemented_nid_no_patch(__func__); +std::int64_t APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V161PostPlayerSessionsSessionIdJoinableSpecifiedUsersResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5getIdEv() { return 0; } @@ -2910,8 +2899,7 @@ void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V1 ReleaseIntrusivePtr(self, 0x00, __func__); } -int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V130PostPlayerSessionsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5getIdEv() { - NotImplemented_nid_no_patch(__func__); +std::int64_t APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V130PostPlayerSessionsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5getIdEv() { return 0; } @@ -2948,8 +2936,7 @@ int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS } int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V152PostPlayerSessionsSessionIdMemberPlayersResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5abortEv() { - NotImplemented_nid_no_patch(__func__); - return 0; + return TransactionNotStarted; } void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V149PostPlayerSessionsSessionIdInvitationsRequestBodyEED1Ev(IntrusivePtr* self) { @@ -2964,8 +2951,7 @@ void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2 return self->object; } -int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V152PostPlayerSessionsSessionIdMemberPlayersResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5getIdEv() { - NotImplemented_nid_no_patch(__func__); +std::int64_t APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V152PostPlayerSessionsSessionIdMemberPlayersResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5getIdEv() { return 0; } @@ -3236,8 +3222,7 @@ int APS5_VABI _ZN3sce2Np9CppWebApi6Common6StringeqEPKc(const CommonString* self, return other != nullptr && std::strcmp(self->buffer != nullptr ? self->buffer : "", other) == 0; } -int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_15DefaultResponseENS2_12IntrusivePtrINS2_18ResponseHeaderBaseEEEE5getIdEv() { - NotImplemented_nid_no_patch(__func__); +std::int64_t APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_15DefaultResponseENS2_12IntrusivePtrINS2_18ResponseHeaderBaseEEEE5getIdEv() { return 0; } @@ -3247,8 +3232,7 @@ void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorImEEEaSERS6_ } int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_15DefaultResponseENS2_12IntrusivePtrINS2_18ResponseHeaderBaseEEEE5abortEv() { - NotImplemented_nid_no_patch(__func__); - return 0; + return TransactionNotStarted; } int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_6StringEE5beginEv() { diff --git a/core/libs/prx/libSceNpCppWebApi/include/Api.hpp b/core/libs/prx/libSceNpCppWebApi/include/Api.hpp index 77fa374881..b827506807 100644 --- a/core/libs/prx/libSceNpCppWebApi/include/Api.hpp +++ b/core/libs/prx/libSceNpCppWebApi/include/Api.hpp @@ -580,7 +580,7 @@ int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_1 void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V12ToEED1Ev(IntrusivePtr* self); int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V161PostPlayerSessionsSessionIdJoinableSpecifiedUsersResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5abortEv(); int APS5_VABI _ZNK3sce2Np9CppWebApi6Common6VectorImE4sizeEv(); -int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V129GetPlayerSessionsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5getIdEv(); +std::int64_t APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V129GetPlayerSessionsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5getIdEv(); int APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_11UserProfile2V113BasicPresenceEEEEdeEv(); int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_14SessionManager2V152PostPlayerSessionsSessionIdMemberPlayersResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE11hasResponseEv(); void* APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V127ResponsePlayerSessionPlayerEEEEixEm(IntrusivePtr* self, unsigned long index); @@ -603,10 +603,10 @@ void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2 int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_14SessionManager2V129GetPlayerSessionsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE11getResponseERS8_(); void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V113PlayerSessionEEC1Ev(IntrusivePtr* self); void APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V130RequestPlayerSessionInvitationEEEEC1EPNS2_10LibContextE(IntrusivePtr* self, void* libContext); -int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V161PostPlayerSessionsSessionIdJoinableSpecifiedUsersResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5getIdEv(); +std::int64_t APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V161PostPlayerSessionsSessionIdJoinableSpecifiedUsersResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5getIdEv(); int APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorImEdeEv(); void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V118LeaderWithOnlineIdEED1Ev(IntrusivePtr* self); -int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V130PostPlayerSessionsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5getIdEv(); +std::int64_t APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V130PostPlayerSessionsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5getIdEv(); int APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorImEneERKS4_(); std::uint64_t APS5_VABI _ZNK3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V127ResponsePlayerSessionPlayerEEEE4sizeEv(const IntrusivePtr* self); void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_14SessionManager2V120PlayerSessionForReadEEEEEED1Ev(IntrusivePtr* self); @@ -617,7 +617,7 @@ int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V149PostPlayerSessionsSessionIdInvitationsRequestBodyEED1Ev(IntrusivePtr* self); void APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V130RequestPlayerSessionInvitationEEEED1Ev(IntrusivePtr* self); void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V120PlayerSessionForReadEEptEv(const IntrusivePtr* self); -int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V152PostPlayerSessionsSessionIdMemberPlayersResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5getIdEv(); +std::int64_t APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V152PostPlayerSessionsSessionIdMemberPlayersResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5getIdEv(); int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V127ResponsePlayerSessionPlayerEEEE8copyFromERKS9_(); void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V150PostPlayerSessionsSessionIdInvitationsResponseBodyEED1Ev(IntrusivePtr* self); void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_14SessionManager2V113PlayerSessionEEEEEEC1Ev(IntrusivePtr* self); @@ -673,7 +673,7 @@ int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_15DefaultResponseENS void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS2_6StringEEEEptEv(const IntrusivePtr* self); int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_6StringEE3endEv(); int APS5_VABI _ZN3sce2Np9CppWebApi6Common6StringeqEPKc(const CommonString* self, const char* other); -int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_15DefaultResponseENS2_12IntrusivePtrINS2_18ResponseHeaderBaseEEEE5getIdEv(); +std::int64_t APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_15DefaultResponseENS2_12IntrusivePtrINS2_18ResponseHeaderBaseEEEE5getIdEv(); void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorImEEEaSERS6_(IntrusivePtr* self, const IntrusivePtr* source); int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_15DefaultResponseENS2_12IntrusivePtrINS2_18ResponseHeaderBaseEEEE5abortEv(); int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_6StringEE5beginEv(); diff --git a/core/libs/tests/GuestNpCppWebApiSignedOut.cpp b/core/libs/tests/GuestNpCppWebApiSignedOut.cpp index 826ceb8fa0..0cfea69f00 100644 --- a/core/libs/tests/GuestNpCppWebApiSignedOut.cpp +++ b/core/libs/tests/GuestNpCppWebApiSignedOut.cpp @@ -7,6 +7,7 @@ static void Require(bool value) { if (!value) std::abort(); } static constexpr int NotSignedIn = static_cast(0x80552907); +static constexpr int TransactionNotStarted = static_cast(0x80553508); using Object = int (APS5_VABI*)(void*); using Call = int (APS5_VABI*)(); @@ -99,6 +100,62 @@ static const RequestFlow flows[] = { }, }; +using Lifecycle = void (APS5_VABI*)(void*); +using Finish = int (APS5_VABI*)(void*); +using Identifier = std::int64_t (APS5_VABI*)(); + +struct TransactionLifecycle { + Lifecycle construct; + Lifecycle destroy; + Finish finish; +}; + +static const TransactionLifecycle lifecycles[] = { + { + _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS2_6VectorINS4_INS1_13InGameCatalog2V59ContainerEEEEEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev, + _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS2_6VectorINS4_INS1_13InGameCatalog2V59ContainerEEEEEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev, + _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS2_6VectorINS4_INS1_13InGameCatalog2V59ContainerEEEEEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv, + }, + { + _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_17TitleCloudStorage2V132GetMultiDataStatusesResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev, + _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_17TitleCloudStorage2V132GetMultiDataStatusesResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev, + _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_17TitleCloudStorage2V132GetMultiDataStatusesResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv, + }, +}; + +static const Call aborts[] = { + _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_15DefaultResponseENS2_12IntrusivePtrINS2_18ResponseHeaderBaseEEEE5abortEv, + _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V129GetPlayerSessionsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5abortEv, + _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V130PostPlayerSessionsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5abortEv, + _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V150PostPlayerSessionsSessionIdInvitationsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5abortEv, + _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V152PostPlayerSessionsSessionIdMemberPlayersResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5abortEv, + _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V161PostPlayerSessionsSessionIdJoinableSpecifiedUsersResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5abortEv, +}; + +static const Identifier identifiers[] = { + _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_15DefaultResponseENS2_12IntrusivePtrINS2_18ResponseHeaderBaseEEEE5getIdEv, + _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V129GetPlayerSessionsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5getIdEv, + _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V130PostPlayerSessionsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5getIdEv, + _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V152PostPlayerSessionsSessionIdMemberPlayersResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5getIdEv, + _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V161PostPlayerSessionsSessionIdJoinableSpecifiedUsersResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5getIdEv, +}; + +static void TestNeverStartedTransactions() { + for (const auto& lifecycle : lifecycles) { + alignas(16) unsigned char transaction[0x20]; + std::memset(transaction, 0x5a, sizeof(transaction)); + lifecycle.construct(transaction); + for (std::size_t index = 0; index < sizeof(transaction); ++index) + Require(transaction[index] == (index < 0x10 ? 0 : 0x5a)); + Require(lifecycle.finish(transaction) == TransactionNotStarted); + lifecycle.destroy(transaction); + for (std::size_t index = 0; index < sizeof(transaction); ++index) + Require(transaction[index] == (index < 0x10 ? 0 : 0x5a)); + } + for (const Call abort : aborts) Require(abort() == TransactionNotStarted); + for (const Identifier identifier : identifiers) Require(identifier() == 0); +} + static void TestRequestFlow(const RequestFlow& flow) { alignas(16) unsigned char transaction[0x100]; alignas(16) unsigned char parameter[0x100]; @@ -113,7 +170,7 @@ static void TestRequestFlow(const RequestFlow& flow) { Require(flow.call() == NotSignedIn); Require(flow.start() == NotSignedIn); Require(flow.getResponseHeaders() == NotSignedIn); - Require(flow.finish() == 0); + Require(flow.finish() == TransactionNotStarted); Require(flow.terminate() == 0); Require(flow.parameterDestroy(parameter) == 0); Require(flow.transactionDestroy(transaction) == 0); @@ -122,5 +179,6 @@ static void TestRequestFlow(const RequestFlow& flow) { int main() { for (const auto& flow : flows) TestRequestFlow(flow); Require(_ZN3sce2Np9CppWebApi15ProfanityFilter2V226WebApiFilterRequestFactory6createEPNS1_6Common10LibContextEPKcPNS5_12IntrusivePtrINS3_19WebApiFilterRequestEEE() == NotSignedIn); + TestNeverStartedTransactions(); return 0; } diff --git a/docs/dev/debt/libSceNpCppWebApi.md b/docs/dev/debt/libSceNpCppWebApi.md index e796fc8e2f..f471b02d0b 100644 --- a/docs/dev/debt/libSceNpCppWebApi.md +++ b/docs/dev/debt/libSceNpCppWebApi.md @@ -2,7 +2,7 @@ ### Silent stubs -- [sce::Np::CppWebApi::Common](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - the `finish` overrides of `TransactionBase<...>` (13 instantiations), `DownStreamTransaction<...>` (2 instantiations) and `Vector>::pushBack` return success without doing work; a failed request never produces a transaction to finish. `Transaction<...>::setOnFinishedCallback` and `TransactionBase<...>::setResponseInformationOption` (5 instantiations each, the `UserProfile::V1`, `ProfanityFilter::V2` and `CommunicationRestrictionStatus::V3` responses) return success without storing the callback or the option: `start` fails, so neither is ever used. The constructors and destructors of the same five `Transaction<...>` instantiations succeed without keeping state +- [sce::Np::CppWebApi::Common](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - the `finish` overrides of `DownStreamTransaction<...>` (2 instantiations) and `Vector>::pushBack` return success without doing work; a failed request never produces a transaction to finish. `Transaction<...>::setOnFinishedCallback` and `TransactionBase<...>::setResponseInformationOption` (5 instantiations each, the `UserProfile::V1`, `ProfanityFilter::V2` and `CommunicationRestrictionStatus::V3` responses) return success without storing the callback or the option: `start` fails, so neither is ever used. The constructors and destructors of the same five `Transaction<...>` instantiations succeed without keeping state - [sce::Np::CppWebApi::Leaderboards::V1](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - request-body and parameter bookkeeping succeeds without storing state: `GetRankingRequestBody::set{StartSerialRank,Group,Limit,Users,Offset}`, `RecordScoreRequestBody::set{Comment,SmallData,NeedsTmpRank,ComparedDateTime,PcId}`, `User::{setAccountId,setPcId}`, `BoardsApi::ParameterToGetBoardDefinition::{initialize,terminate}`, `RecordApi::ParameterToRecordLargeData::{initialize,terminate}`, `RecordApi::ParameterToRecordScore::{initialize,setxPsnAtomicOperation,terminate}`, `ViewApi::ParameterToGetLargeDataByObjectId::{initialize,terminate}`, `ViewApi::ParameterToGetRanking::{initialize,setgetRankingRequestBody,terminate}` - [sce::Np::CppWebApi::SessionManager::V1](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - parameter bookkeeping succeeds without storing state: `PlayerSessionsApi::ParameterToCreatePlayerSessions::{ctor,dtor,initialize}`, `PlayerSessionsApi::ParameterToGetPlayerSessionInvitations::{ctor,dtor,initialize,setfields,setinvitationInvalidFilter}`, `PlayerSessionsApi::ParameterToJoinPlayerSessionAsPlayer::{ctor,dtor,initialize,terminate}`, `PlayerSessionsApi::ParameterToLeavePlayerSession::{ctor,dtor,initialize}`, `RequestPlayerSession::set{MaxSpectators,InvitableUserType,JoinableUserType,ExclusiveLeaderPrivileges}`, `RequestPlayerSessionPlayer::setPushContexts` - [sce::Np::CppWebApi::TitleCloudStorage::V1 and InGameCatalog::V5](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - `DataApi::ParameterToDownloadData`, `DataApi::ParameterToGetMultiDataStatusesByUser` and `ContainerApi::ParameterToGetContainer` keep only whether they are initialized. `initialize`, `terminate` and the string setters return the codes of the module bundled with PPSA21564: 0x80553503 for a null `LibContext` or string, 0x80553502 for a `LibContext` that `Common::initialize` did not set or a parameter not yet initialized, 0x8055350f for a second `initialize` and 0x80553510 for `terminate` without one. They store none of the values, and `ParameterToGetContainer::set{sort,offset,limit,serviceLabel}` store nothing either: only a request would read them @@ -17,5 +17,5 @@ - [sce::Np::CppWebApi::SessionManager2V1](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - imported by PPSA03671. Offline, the factories, `PlayerSessionsApi` calls and transaction `start`/`getResponse`/`readData` fail with `SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN`; parameter setters, `initialize`, `terminate`, constructors and destructors succeed without keeping state, as no request is ever sent. Response-body accessors (`getSessionId`, `getPlayerSessions`, `getInvitations`, the `IsSet` queries) throw: a failed request never produces a response object - [sce::Np::CppWebApi::UserProfile::V1, ProfanityFilter::V2, CommunicationRestrictionStatus::V3](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - imported by an Epic Online Services module whose dump ships no `libSceNpCppWebApi.prx`. The reference-count offset 0x00 of `ResponseHeaderBase` and `WebApiFilterRequest` is assumed from the other objects, not measured. Offline, the five API calls, `WebApiFilterRequestFactory::create` and the transactions' `start`/`getResponseHeaders` fail with `SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN`. Response accessors, `ResponseHeaderBase::getHeaderValue` and the `Vector`/`Iterator` members over `IntrusivePtr` and `unsigned long` throw: a failed request never produces a response - [libSceNpCppWebApi](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - unimplemented stubs have their parameter lists, const qualifiers and return types aligned with the demangled symbols confirmed by NID matching in [#8143](https://github.com/boykopovar/AnyPS5/issues/8143). They cover response-body accessors, iterators, and uninstantiated transactions in Leaderboards::V1, SessionManager::V1, InGameCatalog::V5, TitleCloudStorage::V1, and Common templates: each mangled name gives the method and parameter types, but the runtime layout of response objects and errors are unverified on console, so each call continues to throw NotImplemented_nid_no_patch. No call site is recorded for them -- [sce::Np::CppWebApi::SessionManager2V1 and sce::Np::CppWebApi::InGameCatalog2V5](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - 198 further exports imported by PPSA20397, named from aerolib. API calls, factories and transaction `start`/`getResponse*` fail `SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN`; parameter bookkeeping succeeds without state; `IntrusivePtr`/`Vector` helpers follow the layouts above, with `Vector::empty`/`size`/`operator[]` reading host-created bodies and throwing on title-provided ones. Response accessors, iterators, `Vector::begin`/`end`/`copyFrom` and transaction `abort`/`getId`/`operator=` throw: a failed request produces no response object, and aborting or renumbering a transaction that never ran has no defined outcome +- [sce::Np::CppWebApi::SessionManager2V1 and sce::Np::CppWebApi::InGameCatalog2V5](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - 198 further exports imported by PPSA20397, named from aerolib. API calls, factories and transaction `start`/`getResponse*` fail `SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN`; parameter bookkeeping succeeds without state; `IntrusivePtr`/`Vector` helpers follow the layouts above, with `Vector::empty`/`size`/`operator[]` reading host-created bodies and throwing on title-provided ones. Response accessors, iterators, `Vector::begin`/`end`/`copyFrom` and transaction `operator=` throw: a failed request produces no response object. As `start` never succeeds here, `TransactionBase::finish` and `abort` return 0x80553508 and `getId` returns 0, which is what the module bundled with PPSA21564 returns while the two words its `Transaction` constructor zeroes are still unset - [sce::Np::CppWebApi::UserProfile::V1, sce::Np::CppWebApi::CommunicationRestrictionStatus::V3 and the remaining sce::Np::CppWebApi::Common instantiations](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - 87 further exports imported by PPSA20397, plus the previously-throwing `ContainerApi::ParameterToGetContainer` lifecycle and `getContainer`. API verbs and transaction `start`/`getResponse` fail `SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN`; parameter and model bookkeeping (`initialize`, `terminate`, `set*`, constructors, destructors) succeeds without state. `BasicProfile`/`BasicPresence`/`PersonalDetail` response accessors, `*IsSet` queries, `Get*Response` getters, vector `begin`/`end`, iterator dereference/compare and transaction `abort`/`getId`/`operator=` throw: no response object exists offline and the model layouts are unverified From 9d6b9173887e0e4837aaf4c00ba19b3578fcdbfb Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Wed, 21 Oct 2026 09:07:05 +0000 Subject: [PATCH 172/198] fix(agc): round 16-bit float image stores toward zero --- .../execution/ImageStoreFloatRounding.cpp | 210 ++++++++++++++++++ .../Optimization/src/ResourceMaterializer.cpp | 11 +- .../SpirvBackend/src/SpirvImageEmitter.cpp | 11 +- 3 files changed, 229 insertions(+), 3 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreFloatRounding.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreFloatRounding.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreFloatRounding.cpp new file mode 100644 index 0000000000..ea4275cb14 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreFloatRounding.cpp @@ -0,0 +1,210 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Width = 32; +constexpr std::uint32_t Height = 8; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t IdentitySwizzle = 0xfacu; +constexpr std::uint32_t TwoChannelSwizzle = 0x22cu; +constexpr std::size_t BlockBytes = 65536; +constexpr std::size_t TexelBytes = 8192; + +alignas(256) std::array Input{}; +alignas(256) constexpr std::array StoreCode{ + 0x34020084, 0xe0381000, 0x80001401, 0xbf8c3f70, 0x7e3c0300, 0x7e3e0280, 0xf0201f08, 0x0001141e, + 0x7e3e0281, 0xf0201508, 0x0001141e, 0xbf810000, +}; + +struct Format { + const char* name; + std::uint32_t value; + std::uint32_t components; +}; + +constexpr std::array Formats{{ + {"16_FLOAT", 13u, 1u}, + {"16_16_FLOAT", 29u, 2u}, + {"16_16_16_16_FLOAT", 71u, 4u}, +}}; + +struct Write { + std::uint32_t row; + std::uint32_t dmask; +}; + +constexpr std::array Writes{{{0u, 0xfu}, {1u, 0x5u}}}; +constexpr std::array EdgeBits{ + 0x00000000u, 0x80000000u, 0x3f800000u, 0xbf800000u, 0x3f801000u, 0x3f803000u, + 0x3f800fffu, 0x3f801001u, 0x477fe000u, 0x477ff000u, 0x47800000u, 0xc7800000u, + 0x7f800000u, 0xff800000u, 0x7fc12345u, 0x7f812345u, 0x38800000u, 0x33800000u, + 0x33000000u, 0x000116c2u, 0x3eaaaaabu, 0x40490fdbu, 0x00000001u, 0x80000001u, +}; + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + block = static_cast(std::aligned_alloc(BlockBytes, BlockBytes)); +#endif + Require(block != nullptr, "image store float rounding: cannot allocate the guest block"); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + } + + std::uint8_t* Data() { return block; } + +private: + std::uint8_t* block = nullptr; +}; + +std::uint16_t HalfTowardZero(std::uint32_t bits) { + const auto sign = static_cast((bits >> 16u) & 0x8000u); + const auto exponent = (bits >> 23u) & 0xffu; + const auto mantissa = bits & 0x7fffffu; + if (exponent == 0xffu) { + auto payload = static_cast(mantissa >> 13u); + if (mantissa != 0u && payload == 0u) payload = 1u; + return static_cast(sign | 0x7c00u | payload); + } + if (exponent == 0u) return sign; + const int unbiased = static_cast(exponent) - 127; + if (unbiased > 15) return static_cast(sign | 0x7bffu); + if (unbiased < -14) { + const int shift = -14 - unbiased + 13; + if (shift >= 32) return sign; + const auto significand = 0x800000u | mantissa; + return static_cast(sign | (significand >> static_cast(shift))); + } + return static_cast(sign | (static_cast(unbiased + 15) << 10u) | (mantissa >> 13u)); +} + +std::uint32_t Value(std::uint32_t tid, std::uint32_t component) { + const auto slot = tid * 4u + component; + if (slot < EdgeBits.size()) return EdgeBits[slot]; + const auto seed = (slot + 1u) * 0x9e3779b1u; + const auto mixed = seed ^ (seed >> 16u); + return (mixed & 0x807fffffu) | (((mixed >> 23u) % 31u + 110u) << 23u); +} + +std::uint64_t TexelOffset(const Format& format, std::uint32_t x, std::uint32_t y) { + const auto mip = AgcDriver::Graphics::ComputeMipLayout(AgcDriver::Graphics::TextureTileMode::kLinear, format.value, Width, Height, 1u).at(0); + return mip.tiledOffset + static_cast(y) * mip.pitchBytes + static_cast(x) * format.components * sizeof(std::uint16_t); +} + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::array TextureDescriptor(const void* data, const Format& format) { + const auto address = static_cast(reinterpret_cast(data)); + return {static_cast(address >> 8u), static_cast((address >> 40u) & 0xffu) | (format.value << 20u) | (((Width - 1u) & 3u) << 30u), + ((Width - 1u) >> 2u) | ((Height - 1u) << 14u), (format.components == 2u ? TwoChannelSwizzle : IdentitySwizzle) | (Type2D << 28u), 0u, 0u, 0u, 0u}; +} + +void Store(AgcDriver::VulkanDevice& device, std::uint8_t* texels, const Format& format) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + for (std::uint32_t component = 0; component < 4u; ++component) Input[tid * 4u + component] = Value(tid, component); + } + std::fill(texels, texels + TexelBytes, 0x5au); + std::vector userData(16, 0u); + const auto buffer = BufferDescriptor(Input.data(), static_cast(Input.size() * sizeof(Input[0]))); + const auto texture = TextureDescriptor(texels, format); + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + const std::span code(StoreCode); + const std::array memory{{{reinterpret_cast(StoreCode.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(StoreCode.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(StoreCode.data())); + device.WaitIdle(); + AgcDriver::Graphics::StorageTexture::FlushPending(reinterpret_cast(texels), TexelBytes, nullptr, "test"); + device.WaitIdle(); +} + +std::uint16_t ExpectedComponent(std::uint32_t tid, std::uint32_t dmask, std::uint32_t component) { + if (((dmask >> component) & 1u) == 0u) return 0x5a5au; + std::uint32_t source = 0u; + for (std::uint32_t lower = 0u; lower < component; ++lower) source += (dmask >> lower) & 1u; + return HalfTowardZero(Value(tid, source)); +} + +void Check(const std::uint8_t* texels, const Format& format) { + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto offset = TexelOffset(format, x, y); + for (std::uint32_t component = 0; component < format.components; ++component) { + std::uint16_t actual = 0u; + std::memcpy(&actual, texels + offset + component * sizeof(actual), sizeof(actual)); + const auto dmask = y == Writes[0].row ? Writes[0].dmask : y == Writes[1].row ? Writes[1].dmask : 0u; + const auto expected = dmask != 0u ? ExpectedComponent(x, dmask, component) : 0x5a5au; + Require(actual == expected, std::string("image_store of ") + format.name + ": texel (" + std::to_string(x) + ", " + std::to_string(y) + ") component " + std::to_string(component) + " is " + std::to_string(actual) + ", expected " + std::to_string(expected)); + } + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + GuestBlock block; + for (const auto& format : Formats) { + Store(*device, block.Data(), format); + Check(block.Data(), format); + } + std::puts("image store float rounding tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index 10202666a4..336e012d94 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -290,6 +290,8 @@ ImageDecode inspectImageDescriptor(const DescriptorValue& descriptor, const Imag const bool wideSint = format == IrBufferFormat::Format32SInt || format == IrBufferFormat::Format32_32SInt || format == IrBufferFormat::Format32_32_32_32SInt; const bool narrowSint = format == IrBufferFormat::Format16SInt || format == IrBufferFormat::Format8_8SInt || format == IrBufferFormat::Format16_16SInt || format == IrBufferFormat::Format8_8_8_8SInt || format == IrBufferFormat::Format16_16_16_16SInt; const bool rawSintStorage = storage && (wideSint || (narrowSint && !base.packed)) && base.written && !base.read && !base.atomic; + const bool rawFloat16Storage = storage && base.written && !base.read && !base.atomic && + (remappedFormat == IrBufferFormat::Format16Float || remappedFormat == IrBufferFormat::Format16_16Float || remappedFormat == IrBufferFormat::Format16_16_16_16Float); decoded.numericClass = base.atomic || decoded.fmask ? IrTextureNumericClass::Uint : nativeRepacked ? IrTextureNumericClass::Float : SampledTextureNumericClass(remappedFormat); if (!storage && !base.depthCompare && IsDepthBitsTexture(descriptor.dwords[1], descriptor.dwords[3])) { decoded.depthBits = true; @@ -305,7 +307,10 @@ ImageDecode inspectImageDescriptor(const DescriptorValue& descriptor, const Imag if (rawSintStorage) { decoded.numericClass = IrTextureNumericClass::Uint; } - if ((rawSintStorage || (base.atomic && format == IrBufferFormat::Format32SInt)) && !base.packed) { + if (rawFloat16Storage) { + decoded.numericClass = IrTextureNumericClass::Uint; + } + if ((rawSintStorage || rawFloat16Storage || (base.atomic && format == IrBufferFormat::Format32SInt)) && !base.packed) { decoded.conversionFormat = format; } } else if (decoded.numericClass == IrTextureNumericClass::Unsupported || (base.depthCompare && decoded.numericClass != IrTextureNumericClass::Float)) { @@ -2024,6 +2029,10 @@ std::vector ResourceMaterializer::RuntimeImageModes(const ImageRe append(IrTextureNumericClass::Uint, format, IrBufferFormat::Invalid, false, false); } } + if (storage && !image.packed && image.written && !image.read && !image.atomic) { + constexpr std::array formats{IrBufferFormat::Format16Float, IrBufferFormat::Format16_16Float, IrBufferFormat::Format16_16_16_16Float}; + for (const auto format : formats) append(IrTextureNumericClass::Uint, format, IrBufferFormat::Invalid, false, false); + } if (image.depthCompare && !table && (image.emulatedCompare & EmulatedCompare::Unsupported) == 0u && (image.dimension == RdnaImageDimension::Dim2D || image.dimension == RdnaImageDimension::Dim2DArray)) { auto mode = image; mode.numericClass = IrTextureNumericClass::Float; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index cd5552084d..1e8a30d17e 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -813,6 +813,9 @@ std::uint32_t StoreTexel(SpirvValueEmitContext& ctx, const ImageEmitAccess& acce auto& state = ctx.state; const auto& mem = access.mem; const auto sint = SintStorageFormat(access.image); + const auto storageFormat = GetFormatInfo(access.image.conversionFormat); + const auto float16 = access.image.resourceClass == ImageResourceClass::Storage && access.image.numericClass == IrTextureNumericClass::Uint && + (access.image.conversionFormat == IrBufferFormat::Format16Float || access.image.conversionFormat == IrBufferFormat::Format16_16Float || access.image.conversionFormat == IrBufferFormat::Format16_16_16_16Float); std::uint32_t values[4] = {}; const auto dmask = EffectiveDmask(mem); for (std::uint32_t component = 0; component < 4u; component++) { @@ -834,13 +837,17 @@ std::uint32_t StoreTexel(SpirvValueEmitContext& ctx, const ImageEmitAccess& acce raw = Select(state, TypeU32(state), Binary(state, spv::OpIEqual, TypeBool(state), RuntimeImageSwizzle(state, mem.resource, source), ConstantU32(state, component + 4u)), candidate, raw); } values[component] = integer ? raw : mem.dataBits == 16u ? EmitF16BitsToF32(state, raw) : Unary(state, spv::OpBitcast, TypeF32(state), raw); + if (float16 && component < storageFormat.componentCount) { + values[component] = EmitFormatStoreComponent(state, storageFormat, component, values[component]); + } if (component < sint.componentCount) { values[component] = mem.dataBits == 16u ? EmitD16StoreComponent(state, sint, component, values[component]) : EmitFormatStoreComponent(state, sint, component, values[component]); } } const auto texel = state.module.AllocateId(); - state.module.AddFunction(spv::OpCompositeConstruct, integer ? TypeU32Vector(state, 4) : TypeF32Vector(state, 4), texel, values[0], values[1], values[2], values[3]); - return sint.format != IrBufferFormat::Invalid ? texel : PackImageTexel(ctx, access, texel); + const bool rawUintStorage = sint.format != IrBufferFormat::Invalid || float16; + state.module.AddFunction(spv::OpCompositeConstruct, integer || rawUintStorage ? TypeU32Vector(state, 4) : TypeF32Vector(state, 4), texel, values[0], values[1], values[2], values[3]); + return rawUintStorage ? texel : PackImageTexel(ctx, access, texel); } std::uint32_t PackedStoreTexel(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, std::uint32_t data) { From 027c6f06a87400ab795d20fcc7cfd6373d1504e2 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Wed, 21 Oct 2026 09:29:21 +0000 Subject: [PATCH 173/198] fix(agc): bind UINT views for float16 image stores --- .../prx/libSceAgcDriver/Graphics/include/Texture.hpp | 2 +- .../prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp | 4 ++-- core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp | 9 ++++++--- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index 7b7b420c3c..27eaa32fed 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -185,7 +185,7 @@ public: // so successive mip writes of a chain share one image and one write-back. VkImageView View(std::uint32_t mip); VkImageView FirstLayerView(std::uint32_t mip); - VkImageView StorageView(std::uint32_t mip, bool firstLayer); + VkImageView StorageView(std::uint32_t mip, bool firstLayer, bool uintStorage); VkImageView ElementView(); VkImageView AtomicView(std::uint32_t mip, bool firstLayer); VkImageView Atomic64View(std::uint32_t mip, bool firstLayer); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 4420f9ecb5..88a42a9032 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -2455,7 +2455,7 @@ void ShaderResources::buildComplete() { for (auto element = first; element < last; ++element) { const auto index = binding.imageAllocations[element]; if (binding.layout.descriptorType == VK_DESCRIPTOR_TYPE_SAMPLER) images.push_back({samplers[index]->Handle(), VK_NULL_HANDLE, VK_IMAGE_LAYOUT_UNDEFINED}); - else if (binding.layout.descriptorType == VK_DESCRIPTOR_TYPE_STORAGE_IMAGE) images.push_back({VK_NULL_HANDLE, storageAtomic64[index] ? storageTextures[index]->Atomic64View(storageMips[index], storageFirstLayer[index]) : storageAtomic[index] ? storageTextures[index]->AtomicView(storageMips[index], storageFirstLayer[index]) : storageTextures[index]->StorageView(storageMips[index], storageFirstLayer[index]), VK_IMAGE_LAYOUT_GENERAL}); + else if (binding.layout.descriptorType == VK_DESCRIPTOR_TYPE_STORAGE_IMAGE) images.push_back({VK_NULL_HANDLE, storageAtomic64[index] ? storageTextures[index]->Atomic64View(storageMips[index], storageFirstLayer[index]) : storageAtomic[index] ? storageTextures[index]->AtomicView(storageMips[index], storageFirstLayer[index]) : storageTextures[index]->StorageView(storageMips[index], storageFirstLayer[index], binding.layout.binding >= ShaderRecompiler::RuntimeAbi::FirstStorageImageBinding + 7u && binding.layout.binding < ShaderRecompiler::RuntimeAbi::FirstStorageImageBinding + 14u), VK_IMAGE_LAYOUT_GENERAL}); else images.push_back({VK_NULL_HANDLE, textureFirstLayer[index] ? textures[index]->FirstLayerView() : textures[index]->View(), textures[index]->Layout()}); } if (recorded) runs.emplace_back(write.dstArrayElement, write.descriptorCount); @@ -2500,7 +2500,7 @@ void ShaderResources::buildComplete() { for (const auto index : binding.imageAllocations) { if (index == std::numeric_limits::max()) images.push_back(absentImage(binding.layout.binding)); else if (storageTextures[index] == nullptr) images.push_back({VK_NULL_HANDLE, multisampleStorageViews[index], VK_IMAGE_LAYOUT_GENERAL}); - else images.push_back({VK_NULL_HANDLE, storageAtomic64[index] ? storageTextures[index]->Atomic64View(storageMips[index], storageFirstLayer[index]) : storageAtomic[index] ? storageTextures[index]->AtomicView(storageMips[index], storageFirstLayer[index]) : storageTextures[index]->StorageView(storageMips[index], storageFirstLayer[index]), VK_IMAGE_LAYOUT_GENERAL}); + else images.push_back({VK_NULL_HANDLE, storageAtomic64[index] ? storageTextures[index]->Atomic64View(storageMips[index], storageFirstLayer[index]) : storageAtomic[index] ? storageTextures[index]->AtomicView(storageMips[index], storageFirstLayer[index]) : storageTextures[index]->StorageView(storageMips[index], storageFirstLayer[index], binding.layout.binding >= ShaderRecompiler::RuntimeAbi::FirstStorageImageBinding + 7u && binding.layout.binding < ShaderRecompiler::RuntimeAbi::FirstStorageImageBinding + 14u), VK_IMAGE_LAYOUT_GENERAL}); } Require(binding.imageAllocations.size() == binding.layout.descriptorCount, "descriptor allocations disagree with compact binding"); break; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 48aa6481ec..35b5e8e3a0 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -1276,7 +1276,7 @@ VkFormat StorageFormatOrUndefined(const Context& context, VkFormat format) { return storage; } -VkFormat UintFormatOfSint(VkFormat format) { +VkFormat UintStorageViewFormat(VkFormat format) { switch (format) { case VK_FORMAT_R8G8_SINT: return VK_FORMAT_R8G8_UINT; case VK_FORMAT_R8G8B8A8_SINT: return VK_FORMAT_R8G8B8A8_UINT; @@ -1286,6 +1286,9 @@ VkFormat UintFormatOfSint(VkFormat format) { case VK_FORMAT_R32_SINT: return VK_FORMAT_R32_UINT; case VK_FORMAT_R32G32_SINT: return VK_FORMAT_R32G32_UINT; case VK_FORMAT_R32G32B32A32_SINT: return VK_FORMAT_R32G32B32A32_UINT; + case VK_FORMAT_R16_SFLOAT: return VK_FORMAT_R16_UINT; + case VK_FORMAT_R16G16_SFLOAT: return VK_FORMAT_R16G16_UINT; + case VK_FORMAT_R16G16B16A16_SFLOAT: return VK_FORMAT_R16G16B16A16_UINT; default: return VK_FORMAT_UNDEFINED; } } @@ -2037,9 +2040,9 @@ VkImageView StorageTexture::FirstLayerView(std::uint32_t mip) { return created; } -VkImageView StorageTexture::StorageView(std::uint32_t mip, bool firstLayer) { +VkImageView StorageTexture::StorageView(std::uint32_t mip, bool firstLayer, bool uintStorage) { const auto packed = PackedStorageViewFormat(storageFormat); - const auto format = packed != VK_FORMAT_UNDEFINED ? packed : UintFormatOfSint(storageFormat); + const auto format = packed != VK_FORMAT_UNDEFINED ? packed : uintStorage ? UintStorageViewFormat(storageFormat) : VK_FORMAT_UNDEFINED; if (format == VK_FORMAT_UNDEFINED) return firstLayer ? FirstLayerView(mip) : View(mip); if (format == VK_FORMAT_R32_UINT) return AtomicView(mip, firstLayer); const auto found = uintViews.find({mip, firstLayer}); From 1ff3696ef055eaff689a598dda995cec5c804652 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Wed, 21 Oct 2026 11:54:05 +0000 Subject: [PATCH 174/198] test(agc): expect zero for masked float image components --- .../libSceAgcDriver/tests/execution/ImageStoreFloatRounding.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreFloatRounding.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreFloatRounding.cpp index ea4275cb14..bef64fd6b2 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreFloatRounding.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreFloatRounding.cpp @@ -169,7 +169,7 @@ void Store(AgcDriver::VulkanDevice& device, std::uint8_t* texels, const Format& } std::uint16_t ExpectedComponent(std::uint32_t tid, std::uint32_t dmask, std::uint32_t component) { - if (((dmask >> component) & 1u) == 0u) return 0x5a5au; + if (((dmask >> component) & 1u) == 0u) return 0u; std::uint32_t source = 0u; for (std::uint32_t lower = 0u; lower < component; ++lower) source += (dmask >> lower) & 1u; return HalfTowardZero(Value(tid, source)); From 4a25a6560c7a12ea53350e90fb58385f914928bc Mon Sep 17 00:00:00 2001 From: MarcZX10 Date: Wed, 21 Oct 2026 17:05:23 +0000 Subject: [PATCH 175/198] fix(agc): bind UINT views for multisample float stores --- .../Graphics/include/MultisampleTarget.hpp | 2 +- .../Graphics/include/Texture.hpp | 2 +- .../Graphics/src/MultisampleTarget.cpp | 54 +++++++++++++++---- .../Graphics/src/ShaderResources.cpp | 3 +- 4 files changed, 48 insertions(+), 13 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp index 04f7e5ccbd..386aa4c94c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp @@ -72,7 +72,7 @@ VkImageView MultisampleTargetView(const Context& context, const ColorTarget& tar VkImageView MultisampleTargetView(const Context& context, const ColorTarget& target, std::shared_ptr& owner, VkImage* image = nullptr); void KeepMultisampleTarget_nid_no_patch(const std::shared_ptr& owner); void MaterializeMultisampleTargetClear(const Context& context, const ColorTarget& target); -VkImageView MultisampleTargetStorageView(const Context& context, const GuestTextureResource& resource); +VkImageView MultisampleTargetStorageView(const Context& context, const GuestTextureResource& resource, bool uintStorage = false); bool MultisampleTargetStorable(const Context& context, const ColorTarget& target); void ResolveMultisampleTarget(const Context& context, const ColorTarget& source, StorageTexture& destination, VkImageView destinationView); bool MultisampledColorFillAbsorbs(std::uint64_t address, std::size_t bytes, std::uint32_t pattern); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index 27eaa32fed..146048f634 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -185,7 +185,7 @@ public: // so successive mip writes of a chain share one image and one write-back. VkImageView View(std::uint32_t mip); VkImageView FirstLayerView(std::uint32_t mip); - VkImageView StorageView(std::uint32_t mip, bool firstLayer, bool uintStorage); + VkImageView StorageView(std::uint32_t mip, bool firstLayer, bool uintStorage = false); VkImageView ElementView(); VkImageView AtomicView(std::uint32_t mip, bool firstLayer); VkImageView Atomic64View(std::uint32_t mip, bool firstLayer); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp index 7cb1eafdad..99cc95be63 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -33,6 +33,23 @@ namespace AgcDriver::Graphics { namespace { +VkFormat uintStorageViewFormat(VkFormat format) { + switch (format) { + case VK_FORMAT_R8G8_SINT: return VK_FORMAT_R8G8_UINT; + case VK_FORMAT_R8G8B8A8_SINT: return VK_FORMAT_R8G8B8A8_UINT; + case VK_FORMAT_R16_SINT: + case VK_FORMAT_R16_SFLOAT: return VK_FORMAT_R16_UINT; + case VK_FORMAT_R16G16_SINT: + case VK_FORMAT_R16G16_SFLOAT: return VK_FORMAT_R16G16_UINT; + case VK_FORMAT_R16G16B16A16_SINT: + case VK_FORMAT_R16G16B16A16_SFLOAT: return VK_FORMAT_R16G16B16A16_UINT; + case VK_FORMAT_R32_SINT: return VK_FORMAT_R32_UINT; + case VK_FORMAT_R32G32_SINT: return VK_FORMAT_R32G32_UINT; + case VK_FORMAT_R32G32B32A32_SINT: return VK_FORMAT_R32G32B32A32_UINT; + default: return VK_FORMAT_UNDEFINED; + } +} + std::uint32_t MultisampledBlockLog2(ColorTileMode mode) { switch (mode) { case ColorTileMode::Standard4KB: return 12u; @@ -146,12 +163,19 @@ public: Check(context.imageFormatProperties(context.physical, target.format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, usage, orderFlags, &supported), "vkGetPhysicalDeviceImageFormatProperties multisampled target"); Require((supported.sampleCounts & samples) != 0 && target.extent.width <= supported.maxExtent.width && target.extent.height <= supported.maxExtent.height, "multisampled render target exceeds device image limits"); storageFormat = HostStorageFormat(context, target.format); - storageFlags = storageFormat != target.format ? VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT | VK_IMAGE_CREATE_EXTENDED_USAGE_BIT : 0u; + const auto uintFormat = uintStorageViewFormat(storageFormat); + storageFlags = storageFormat != target.format || (uintFormat != VK_FORMAT_UNDEFINED && uintFormat != target.format) ? VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT | VK_IMAGE_CREATE_EXTENDED_USAGE_BIT : 0u; const auto storageSupports = [&](VkFormat format, VkImageUsageFlags formatUsage, VkImageCreateFlags flags) { VkImageFormatProperties properties{}; return context.imageFormatProperties(context.physical, format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, formatUsage, flags, &properties) == VK_SUCCESS && (properties.sampleCounts & samples) != 0; }; - if (storageFormat != VK_FORMAT_UNDEFINED && (!context.storageImageMultisample || !storageSupports(storageFormat, VK_IMAGE_USAGE_STORAGE_BIT, 0u) || !storageSupports(target.format, usage | VK_IMAGE_USAGE_STORAGE_BIT, storageFlags))) storageFormat = VK_FORMAT_UNDEFINED; + const auto uintStorageSupported = [&] { + if (uintFormat == VK_FORMAT_UNDEFINED) return true; + VkFormatProperties uintProperties{}; + context.formatProperties(context.physical, uintFormat, &uintProperties); + return (uintProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT) != 0 && storageSupports(uintFormat, VK_IMAGE_USAGE_STORAGE_BIT, 0u); + }; + if (storageFormat != VK_FORMAT_UNDEFINED && (!context.storageImageMultisample || !storageSupports(storageFormat, VK_IMAGE_USAGE_STORAGE_BIT, 0u) || !storageSupports(target.format, usage | VK_IMAGE_USAGE_STORAGE_BIT, storageFlags) || !uintStorageSupported())) storageFormat = VK_FORMAT_UNDEFINED; twinFormat = colorTwinFormat(context, target.format); if (twinFormat != VK_FORMAT_UNDEFINED) { VkFormatProperties twinProperties{}; @@ -355,7 +379,7 @@ public: }); } - VkImageView Storage(VkFormat format) { + VkImageView Storage(VkFormat format, bool uintStorage) { Require(storageFormat != VK_FORMAT_UNDEFINED, "the device cannot write the multisampled color target's format as a storage image at its sample count"); Require(format == storageFormat, "a storage image access to a multisampled color target in another format than the target's storage form is not implemented"); Require(cmask.Refusal().empty(), cmask.Refusal()); @@ -366,20 +390,28 @@ public: Keep(); makeStorable(*recording); ApplyFastClear(target); - if (storageView == VK_NULL_HANDLE) { + const auto uintFormat = uintStorage ? uintStorageViewFormat(storageFormat) : VK_FORMAT_UNDEFINED; + const bool useUintStorage = uintStorage && uintFormat != VK_FORMAT_UNDEFINED; + auto& selectedView = useUintStorage ? uintStorageView : storageView; + if (selectedView == VK_NULL_HANDLE) { VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; viewInfo.image = image; viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; - viewInfo.format = storageFormat; + viewInfo.format = useUintStorage ? uintFormat : storageFormat; viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; - Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &storageView), "vkCreateImageView multisampled target storage"); + VkImageViewUsageCreateInfo viewUsage{VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO}; + if (useUintStorage) { + viewUsage.usage = VK_IMAGE_USAGE_STORAGE_BIT; + viewInfo.pNext = &viewUsage; + } + Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &selectedView), "vkCreateImageView multisampled target storage"); } recordCommands(context, [&](VkCommandBuffer commands, Recorder* recorder) { RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT); if (recorder != nullptr) Recorder::CountBarriers(Recorder::CommandClass::Draw, 1); }); storageWritten = true; - return storageView; + return selectedView; } bool Storable() const { @@ -528,6 +560,7 @@ private: VkImageView twinView = VK_NULL_HANDLE; std::vector retired; VkImageView storageView = VK_NULL_HANDLE; + VkImageView uintStorageView = VK_NULL_HANDLE; bool storageWritten = false; VkDeviceSize allocationBytes = 0; std::optional fmaskRange; @@ -717,6 +750,7 @@ private: if (orderView) context.Function("vkDestroyImageView")(context.device, orderView, nullptr); if (twinView) context.Function("vkDestroyImageView")(context.device, twinView, nullptr); if (storageView) context.Function("vkDestroyImageView")(context.device, storageView, nullptr); + if (uintStorageView) context.Function("vkDestroyImageView")(context.device, uintStorageView, nullptr); destroy({image, memory, view}); for (const auto& allocation : retired) destroy(allocation); } @@ -1297,7 +1331,7 @@ void MaterializeMultisampleTargetClear(const Context& context, const ColorTarget acquire(context, target); } -VkImageView MultisampleTargetStorageView(const Context& context, const GuestTextureResource& resource) { +VkImageView MultisampleTargetStorageView(const Context& context, const GuestTextureResource& resource, bool uintStorage) { std::lock_guard lock(targetsMutex()); discardRetiredTargets(); const auto format = ResolveTextureFormat(resource.format); @@ -1308,13 +1342,13 @@ VkImageView MultisampleTargetStorageView(const Context& context, const GuestText const auto& rendered = target->target; if (sameStorageForm(context, rendered.format, format) && rendered.extent.width == resource.width && rendered.extent.height == resource.height && rendered.samples == resource.samples && rendered.mip == 0) { if (!holdHostOnly(*target)) continue; - return target->Storage(StorageFormatForGuest(context, resource.format)); + return target->Storage(StorageFormatForGuest(context, resource.format), uintStorage); } other = target.get(); } if (other == nullptr) { Require(Recorder::Active() != nullptr, "a multisampled color target is only written by recorded dispatches"); - return createFilled(context, resource, format, "writing", true).Storage(StorageFormatForGuest(context, resource.format)); + return createFilled(context, resource, format, "writing", true).Storage(StorageFormatForGuest(context, resource.format), uintStorage); } char text[320]; std::snprintf(text, sizeof(text), "AGC graphics: storage image access to multisampled color target 0x%llx (%ux%u, %u samples, vk format %d) as a %ux%u texture of %u samples and guest format %u (vk %d) is not implemented", static_cast(resource.baseAddress), other->target.extent.width, other->target.extent.height, other->target.samples, static_cast(other->target.format), resource.width, resource.height, resource.samples, resource.format, static_cast(format)); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 88a42a9032..e392a045e6 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -4927,7 +4927,8 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind Require(!(element < binding.imageAtomic.size() && binding.imageAtomic[element]) && !(element < binding.imageAtomic64.size() && binding.imageAtomic64[element]), "storage image atomics on a multisampled texture are not implemented"); storageTextures.push_back(nullptr); multisampleStorageViews.resize(storageTextures.size(), VK_NULL_HANDLE); - multisampleStorageViews.back() = MultisampleTargetStorageView(context, resource); + const bool uintStorage = binding.binding >= ShaderRecompiler::RuntimeAbi::FirstStorageImageBinding + 7u && binding.binding < ShaderRecompiler::RuntimeAbi::FirstStorageImageBinding + 14u; + multisampleStorageViews.back() = MultisampleTargetStorageView(context, resource, uintStorage); multisampleSources.push_back(resource.baseAddress); storageMips.push_back(0); storageKeys.push_back(0); From 039408c144f5f1e76058ab0eda8a1a0c2d15a33e Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 27 Oct 2026 05:26:56 +0000 Subject: [PATCH 176/198] test(libkernel): use the libkernel public header in the macOS unwind module info test The test declared sceKernelGetModuleInfoForUnwind itself, so the header already declares it and the test declares it twice and public_headers fails on main. --- core/libs/tests/GuestUnwindModuleInfoMacos.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core/libs/tests/GuestUnwindModuleInfoMacos.cpp b/core/libs/tests/GuestUnwindModuleInfoMacos.cpp index ea43177833..e600903695 100644 --- a/core/libs/tests/GuestUnwindModuleInfoMacos.cpp +++ b/core/libs/tests/GuestUnwindModuleInfoMacos.cpp @@ -1,3 +1,4 @@ +#include "prx/libkernel/include/Api.hpp" #include "SceTypes.hpp" #include "prx/libc/include/specifics/linux/ElfTypes.hpp" #include "tests/GuestUnwindModuleInfoFixture.hpp" @@ -6,8 +7,6 @@ #include #include -extern "C" int APS5_VABI sceKernelGetModuleInfoForUnwind(uint64_t addr, int flags, ModuleInfoForUnwind* info); - struct GuestImageMeta { std::uint64_t version = 2; std::uint64_t loadBias = 0; From 26f2bcb33c0048eb9fb9d15c4f545750af91e69f Mon Sep 17 00:00:00 2001 From: lakhantheboss <61951686+lakhantheboss@users.noreply.github.com> Date: Mon, 26 Oct 2026 03:06:22 +0530 Subject: [PATCH 177/198] fix(libSceNpUniversalDataSystem): return errors for null arguments --- core/libs/prx/libSceNpUniversalDataSystem/src/Context.cpp | 2 +- core/libs/prx/libSceNpUniversalDataSystem/src/Handle.cpp | 2 +- core/libs/tests/GuestNpTrophyStats.cpp | 3 +++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceNpUniversalDataSystem/src/Context.cpp b/core/libs/prx/libSceNpUniversalDataSystem/src/Context.cpp index e6f9c0a4c2..b260532eab 100644 --- a/core/libs/prx/libSceNpUniversalDataSystem/src/Context.cpp +++ b/core/libs/prx/libSceNpUniversalDataSystem/src/Context.cpp @@ -41,7 +41,7 @@ extern "C" { int APS5_VABI sceNpUniversalDataSystemCreateContext(int* context, int user_id, uint32_t service_label, uint64_t options) { if (context == nullptr) { - throw std::invalid_argument(std::string(__func__) + ": invalid argument context"); + return SCE_NP_UNIVERSAL_DATA_SYSTEM_ERROR_INVALID_ARGUMENT; } auto& table = Uds::contexts(); std::lock_guard lock(table.mutex); diff --git a/core/libs/prx/libSceNpUniversalDataSystem/src/Handle.cpp b/core/libs/prx/libSceNpUniversalDataSystem/src/Handle.cpp index 331873010e..3fa2f23b22 100644 --- a/core/libs/prx/libSceNpUniversalDataSystem/src/Handle.cpp +++ b/core/libs/prx/libSceNpUniversalDataSystem/src/Handle.cpp @@ -7,7 +7,7 @@ extern "C" { int APS5_VABI sceNpUniversalDataSystemCreateHandle(int* handle) { if (handle == nullptr) { - APS5_INVALID_ARG_EX; + return SCE_NP_UNIVERSAL_DATA_SYSTEM_ERROR_INVALID_ARGUMENT; } *handle = NP_UNIVERSAL_DATA_SYSTEM_HANDLE_DEFAULT; return SCE_NP_UNIVERSAL_DATA_SYSTEM_OK; diff --git a/core/libs/tests/GuestNpTrophyStats.cpp b/core/libs/tests/GuestNpTrophyStats.cpp index 35511ae5b6..e30205562b 100644 --- a/core/libs/tests/GuestNpTrophyStats.cpp +++ b/core/libs/tests/GuestNpTrophyStats.cpp @@ -24,6 +24,7 @@ struct NpUniversalDataSystemEvent; struct NpUniversalDataSystemEventPropertyObject; static constexpr int InitialUser = 0x10000000; +static constexpr int InvalidArgument = -2141900542; static const char TropConf[] = R"({"defaultLanguage":"en-US","trophies":[ {"id":"0","grade":"P"}, @@ -222,6 +223,8 @@ static void KeepStatsAcrossRuns(int context) { } static void UnlockThroughStats(int context, Unlocks& unlocks) { + Require(sceNpUniversalDataSystemCreateContext(nullptr, InitialUser, 0, 0) == InvalidArgument, "a null context was accepted"); + Require(sceNpUniversalDataSystemCreateHandle(nullptr) == InvalidArgument, "a null handle was accepted"); const auto uds = OpenUds(0); Post(uds, "COLLECT", [](NpUniversalDataSystemEventPropertyObject* properties) { Require(sceNpUniversalDataSystemEventPropertyObjectSetUInt64(properties, "counter", 9) == 0, "counter was not set"); From 160a32416ddd090aef3eab01a0d158b876dae1e3 Mon Sep 17 00:00:00 2001 From: lakhantheboss <61951686+lakhantheboss@users.noreply.github.com> Date: Mon, 26 Oct 2026 12:36:59 +0530 Subject: [PATCH 178/198] docs(libSceNpUniversalDataSystem): record unverified null argument behavior --- docs/dev/debt/libSceNpUniversalDataSystem.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libSceNpUniversalDataSystem.md b/docs/dev/debt/libSceNpUniversalDataSystem.md index cbc943be5e..7e8304e81a 100644 --- a/docs/dev/debt/libSceNpUniversalDataSystem.md +++ b/docs/dev/debt/libSceNpUniversalDataSystem.md @@ -3,6 +3,7 @@ ### Unknown function info - [Events](../../../core/libs/prx/libSceNpUniversalDataSystem/src/Event.cpp): `sceNpUniversalDataSystemCreateEvent` copies `prop` and returns the event's own property object through `prop_ptr`, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/libTrophy.cpp) does; `sceNpUniversalDataSystemEventPropertyObjectSetObject`, `SetArray` and the array setters for objects and arrays store a copy of `value` and return the stored copy through `value_ptr`. Destroying a property object or array that its Create function did not return (an event's property object or a stored copy) does nothing, as its owner frees it. These ownership rules are not verified on a console, nor are the output of `sceNpUniversalDataSystemEventToString` (still `{}`) and `sceNpUniversalDataSystemEventEstimateSize` (still 3). +- [sceNpUniversalDataSystemCreateContext and sceNpUniversalDataSystemCreateHandle](../../../core/libs/prx/libSceNpUniversalDataSystem/src/Context.cpp) (libSceNpUniversalDataSystem) - native return codes for null output pointers (`context == nullptr` and `handle == nullptr`) are unresolved and unverified on a console. The reimplementation in [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/libTrophy.cpp#L620-L640) returns `0x80553102` (`SCE_NP_UNIVERSAL_DATA_SYSTEM_ERROR_INVALID_ARGUMENT`), but this error return is unverified against native hardware; host paths continue to throw `std::invalid_argument` until the native contract is established. - [sceNpUniversalDataSystemIntCreateRecordArray, sceNpUniversalDataSystemIntCreateRecordData and sceNpUniversalDataSystemIntCreateRecordObject](../../../core/libs/prx/libSceNpUniversalDataSystem/PendingExports.cpp) (libSceNpUniversalDataSystem) - The list is the intersection of the export table of the console's libSceNpUniversalDataSystem (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUniversalDataSystem.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. - [sceNpUniversalDataSystemIntDeleteAllRecord](../../../core/libs/prx/libSceNpUniversalDataSystem/PendingExports.cpp) (libSceNpUniversalDataSystem) - It is in the intersection of the export table of the console's libSceNpUniversalDataSystem (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUniversalDataSystem.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. It is declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. From 592736d284f72337f7862935490af23f7684849a Mon Sep 17 00:00:00 2001 From: lakhantheboss <61951686+lakhantheboss@users.noreply.github.com> Date: Mon, 26 Oct 2026 12:58:52 +0530 Subject: [PATCH 179/198] fix(libSceNpUniversalDataSystem): restore unknown null argument behavior --- core/libs/prx/libSceNpUniversalDataSystem/src/Context.cpp | 2 +- core/libs/prx/libSceNpUniversalDataSystem/src/Handle.cpp | 2 +- core/libs/tests/GuestNpTrophyStats.cpp | 3 --- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceNpUniversalDataSystem/src/Context.cpp b/core/libs/prx/libSceNpUniversalDataSystem/src/Context.cpp index b260532eab..e6f9c0a4c2 100644 --- a/core/libs/prx/libSceNpUniversalDataSystem/src/Context.cpp +++ b/core/libs/prx/libSceNpUniversalDataSystem/src/Context.cpp @@ -41,7 +41,7 @@ extern "C" { int APS5_VABI sceNpUniversalDataSystemCreateContext(int* context, int user_id, uint32_t service_label, uint64_t options) { if (context == nullptr) { - return SCE_NP_UNIVERSAL_DATA_SYSTEM_ERROR_INVALID_ARGUMENT; + throw std::invalid_argument(std::string(__func__) + ": invalid argument context"); } auto& table = Uds::contexts(); std::lock_guard lock(table.mutex); diff --git a/core/libs/prx/libSceNpUniversalDataSystem/src/Handle.cpp b/core/libs/prx/libSceNpUniversalDataSystem/src/Handle.cpp index 3fa2f23b22..331873010e 100644 --- a/core/libs/prx/libSceNpUniversalDataSystem/src/Handle.cpp +++ b/core/libs/prx/libSceNpUniversalDataSystem/src/Handle.cpp @@ -7,7 +7,7 @@ extern "C" { int APS5_VABI sceNpUniversalDataSystemCreateHandle(int* handle) { if (handle == nullptr) { - return SCE_NP_UNIVERSAL_DATA_SYSTEM_ERROR_INVALID_ARGUMENT; + APS5_INVALID_ARG_EX; } *handle = NP_UNIVERSAL_DATA_SYSTEM_HANDLE_DEFAULT; return SCE_NP_UNIVERSAL_DATA_SYSTEM_OK; diff --git a/core/libs/tests/GuestNpTrophyStats.cpp b/core/libs/tests/GuestNpTrophyStats.cpp index e30205562b..35511ae5b6 100644 --- a/core/libs/tests/GuestNpTrophyStats.cpp +++ b/core/libs/tests/GuestNpTrophyStats.cpp @@ -24,7 +24,6 @@ struct NpUniversalDataSystemEvent; struct NpUniversalDataSystemEventPropertyObject; static constexpr int InitialUser = 0x10000000; -static constexpr int InvalidArgument = -2141900542; static const char TropConf[] = R"({"defaultLanguage":"en-US","trophies":[ {"id":"0","grade":"P"}, @@ -223,8 +222,6 @@ static void KeepStatsAcrossRuns(int context) { } static void UnlockThroughStats(int context, Unlocks& unlocks) { - Require(sceNpUniversalDataSystemCreateContext(nullptr, InitialUser, 0, 0) == InvalidArgument, "a null context was accepted"); - Require(sceNpUniversalDataSystemCreateHandle(nullptr) == InvalidArgument, "a null handle was accepted"); const auto uds = OpenUds(0); Post(uds, "COLLECT", [](NpUniversalDataSystemEventPropertyObject* properties) { Require(sceNpUniversalDataSystemEventPropertyObjectSetUInt64(properties, "counter", 9) == 0, "counter was not set"); From 7bc24e2311a61fcfb5cde65590b983fef48102ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Tue, 27 Oct 2026 07:47:28 +0100 Subject: [PATCH 180/198] docs(libSceNpUniversalDataSystem): link CreateHandle to Handle.cpp Co-Authored-By: Claude Sonnet 5.5 --- docs/dev/debt/libSceNpUniversalDataSystem.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceNpUniversalDataSystem.md b/docs/dev/debt/libSceNpUniversalDataSystem.md index 7e8304e81a..d9602f375d 100644 --- a/docs/dev/debt/libSceNpUniversalDataSystem.md +++ b/docs/dev/debt/libSceNpUniversalDataSystem.md @@ -3,7 +3,7 @@ ### Unknown function info - [Events](../../../core/libs/prx/libSceNpUniversalDataSystem/src/Event.cpp): `sceNpUniversalDataSystemCreateEvent` copies `prop` and returns the event's own property object through `prop_ptr`, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/libTrophy.cpp) does; `sceNpUniversalDataSystemEventPropertyObjectSetObject`, `SetArray` and the array setters for objects and arrays store a copy of `value` and return the stored copy through `value_ptr`. Destroying a property object or array that its Create function did not return (an event's property object or a stored copy) does nothing, as its owner frees it. These ownership rules are not verified on a console, nor are the output of `sceNpUniversalDataSystemEventToString` (still `{}`) and `sceNpUniversalDataSystemEventEstimateSize` (still 3). -- [sceNpUniversalDataSystemCreateContext and sceNpUniversalDataSystemCreateHandle](../../../core/libs/prx/libSceNpUniversalDataSystem/src/Context.cpp) (libSceNpUniversalDataSystem) - native return codes for null output pointers (`context == nullptr` and `handle == nullptr`) are unresolved and unverified on a console. The reimplementation in [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/libTrophy.cpp#L620-L640) returns `0x80553102` (`SCE_NP_UNIVERSAL_DATA_SYSTEM_ERROR_INVALID_ARGUMENT`), but this error return is unverified against native hardware; host paths continue to throw `std::invalid_argument` until the native contract is established. +- [sceNpUniversalDataSystemCreateContext](../../../core/libs/prx/libSceNpUniversalDataSystem/src/Context.cpp) and [sceNpUniversalDataSystemCreateHandle](../../../core/libs/prx/libSceNpUniversalDataSystem/src/Handle.cpp) (libSceNpUniversalDataSystem) - native return codes for null output pointers (`context == nullptr` and `handle == nullptr`) are unresolved and unverified on a console. The reimplementation in [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/libTrophy.cpp#L620-L640) returns `0x80553102` (`SCE_NP_UNIVERSAL_DATA_SYSTEM_ERROR_INVALID_ARGUMENT`), but this error return is unverified against native hardware; host paths continue to throw `std::invalid_argument` until the native contract is established. - [sceNpUniversalDataSystemIntCreateRecordArray, sceNpUniversalDataSystemIntCreateRecordData and sceNpUniversalDataSystemIntCreateRecordObject](../../../core/libs/prx/libSceNpUniversalDataSystem/PendingExports.cpp) (libSceNpUniversalDataSystem) - The list is the intersection of the export table of the console's libSceNpUniversalDataSystem (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUniversalDataSystem.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. - [sceNpUniversalDataSystemIntDeleteAllRecord](../../../core/libs/prx/libSceNpUniversalDataSystem/PendingExports.cpp) (libSceNpUniversalDataSystem) - It is in the intersection of the export table of the console's libSceNpUniversalDataSystem (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUniversalDataSystem.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. It is declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. From 791a2db6bdce2679cc74f4a57f0d605400663e0e Mon Sep 17 00:00:00 2001 From: Hedi Trigui Date: Mon, 26 Oct 2026 18:00:10 +0100 Subject: [PATCH 181/198] test(libSceMsgDialog.native): cover uninitialized and finished null argument validation --- core/libs/tests/GuestMsgDialogNative.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/core/libs/tests/GuestMsgDialogNative.cpp b/core/libs/tests/GuestMsgDialogNative.cpp index 21383b3c87..7e25847352 100644 --- a/core/libs/tests/GuestMsgDialogNative.cpp +++ b/core/libs/tests/GuestMsgDialogNative.cpp @@ -31,7 +31,9 @@ int main() { Require(sceMsgDialogTerminate() == kErrNotInitialized); Require(sceMsgDialogClose() == kErrNotRunning); + Require(sceMsgDialogOpen(nullptr) == kErrNotInitialized); Require(sceMsgDialogOpen(param) == kErrNotInitialized); + Require(sceMsgDialogGetResult(nullptr) == kErrNotInitialized); Require(sceMsgDialogGetResult(&result) == kErrNotInitialized); Require(sceMsgDialogUpdateStatus() == 0); Require(sceMsgDialogGetStatus() == 0); @@ -49,6 +51,8 @@ int main() { Require(sceMsgDialogOpen(param) == 0); Require(sceMsgDialogUpdateStatus() == kStatusFinished); Require(sceMsgDialogGetStatus() == kStatusFinished); + Require(sceMsgDialogOpen(nullptr) == kErrArgNull); + Require(sceMsgDialogOpen(param) == 0); Require(sceMsgDialogProgressBarInc(0, 10) == kErrNotRunning); Require(sceMsgDialogProgressBarSetMsg(0, "progress") == kErrNotRunning); Require(sceMsgDialogProgressBarSetValue(0, 100) == kErrNotRunning); @@ -64,7 +68,9 @@ int main() { Require(sceMsgDialogTerminate() == 0); Require(sceMsgDialogTerminate() == kErrNotInitialized); + Require(sceMsgDialogOpen(nullptr) == kErrNotInitialized); Require(sceMsgDialogOpen(param) == kErrNotInitialized); + Require(sceMsgDialogGetResult(nullptr) == kErrNotInitialized); Require(sceMsgDialogGetResult(&result) == kErrNotInitialized); Require(sceMsgDialogClose() == kErrNotRunning); Require(sceMsgDialogGetStatus() == 0); From 15436ac4908d99b0cb80d06b977ddcafc732c02d Mon Sep 17 00:00:00 2001 From: Hedi Trigui Date: Mon, 26 Oct 2026 18:05:52 +0100 Subject: [PATCH 182/198] test(libScePlayGoDialog): cover uninitialized and finished dialog calls --- core/libs/tests/GuestPlayGoDialog.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/core/libs/tests/GuestPlayGoDialog.cpp b/core/libs/tests/GuestPlayGoDialog.cpp index e8a89d8e49..82e8c63de2 100644 --- a/core/libs/tests/GuestPlayGoDialog.cpp +++ b/core/libs/tests/GuestPlayGoDialog.cpp @@ -38,6 +38,8 @@ int main() { Require(scePlayGoDialogOpen(&openParam) == 0); Require(scePlayGoDialogGetStatus() == 3); Require(scePlayGoDialogUpdateStatus() == 3); + Require(scePlayGoDialogOpen(nullptr) == paramInvalid); + Require(scePlayGoDialogOpen(&openParam) == 0); PlayGoDialogResult result{}; Require(scePlayGoDialogGetResult(&result) == 0); @@ -51,5 +53,13 @@ int main() { Require(scePlayGoDialogTerminate() == 0); Require(scePlayGoDialogGetStatus() == 0); Require(scePlayGoDialogTerminate() == notInitialized); + Require(scePlayGoDialogClose() == notInitialized); + Require(scePlayGoDialogOpen(nullptr) == paramInvalid); + Require(scePlayGoDialogOpen(&openParam) == notInitialized); + Require(scePlayGoDialogGetResult(nullptr) == paramInvalid); + Require(scePlayGoDialogGetResult(&result) == static_cast(0x80B8000Au)); + result.reserved[0] = 0; + Require(scePlayGoDialogGetResult(&result) == notInitialized); + Require(scePlayGoDialogUpdateStatus() == 0); return 0; } From 86d1ba99a1a0d89d62d5af6403c38d989f0660bb Mon Sep 17 00:00:00 2001 From: Hedi Trigui Date: Mon, 26 Oct 2026 18:08:55 +0100 Subject: [PATCH 183/198] test(libScePlayerSelectionDialog): cover uninitialized and terminated argument validation --- core/libs/tests/GuestPlayerSelectionDialog.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/core/libs/tests/GuestPlayerSelectionDialog.cpp b/core/libs/tests/GuestPlayerSelectionDialog.cpp index 1930f44533..bbdf154c9d 100644 --- a/core/libs/tests/GuestPlayerSelectionDialog.cpp +++ b/core/libs/tests/GuestPlayerSelectionDialog.cpp @@ -97,7 +97,12 @@ int main() { Require(scePlayerSelectionDialogClose() == static_cast(0x80B8000Bu)); Require(scePlayerSelectionDialogTerminate() == 0); Require(scePlayerSelectionDialogTerminate() == kErrNotInitialized); - Require(scePlayerSelectionDialogGetStatus() == 0); + Require(scePlayerSelectionDialogOpen(nullptr) == kErrArgNull); + Require(scePlayerSelectionDialogOpen(¶m) == kErrNotInitialized); Require(scePlayerSelectionDialogClose() == kErrNotInitialized); + Require(scePlayerSelectionDialogGetResult(nullptr) == kErrArgNull); + Require(scePlayerSelectionDialogGetResult(&result) == kErrNotInitialized); + Require(scePlayerSelectionDialogGetStatus() == 0); + Require(scePlayerSelectionDialogUpdateStatus() == 0); Require(sceUserServiceTerminate() == 0); } From f2f35b502818bd9446cb54e54e59b73503b90ee5 Mon Sep 17 00:00:00 2001 From: Hedi Trigui Date: Mon, 26 Oct 2026 19:35:13 +0100 Subject: [PATCH 184/198] test(libSceUsbStorageDialog): cover uninitialized and terminated null argument validation --- core/libs/tests/GuestUsbStorageDialog.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/libs/tests/GuestUsbStorageDialog.cpp b/core/libs/tests/GuestUsbStorageDialog.cpp index 56615792bb..62ecc41378 100644 --- a/core/libs/tests/GuestUsbStorageDialog.cpp +++ b/core/libs/tests/GuestUsbStorageDialog.cpp @@ -45,8 +45,10 @@ int main() { result.result = kErrBusy; Require(sceUsbStorageDialogTerminate() == kErrNotInitialized); + Require(sceUsbStorageDialogOpen(nullptr) == kErrArgNull); Require(sceUsbStorageDialogOpen(¶m) == kErrNotInitialized); Require(sceUsbStorageDialogClose() == kErrNotInitialized); + Require(sceUsbStorageDialogGetResult(nullptr) == kErrArgNull); Require(sceUsbStorageDialogGetResult(&result) == kErrNotInitialized); Require(sceUsbStorageDialogGetStatus() == kStatusNone); Require(sceUsbStorageDialogUpdateStatus() == kStatusNone); @@ -92,6 +94,8 @@ int main() { Require(sceUsbStorageDialogClose() == kErrNotRunning); Require(sceUsbStorageDialogTerminate() == 0); Require(sceUsbStorageDialogTerminate() == kErrNotInitialized); + Require(sceUsbStorageDialogOpen(nullptr) == kErrArgNull); + Require(sceUsbStorageDialogGetResult(nullptr) == kErrArgNull); Require(sceUsbStorageDialogGetStatus() == kStatusNone); Require(sceUsbStorageDialogInitialize() == 0); From ffdf85b2eb6c1596368aa82d404e26a620c572ca Mon Sep 17 00:00:00 2001 From: Hedi Trigui Date: Mon, 26 Oct 2026 19:36:48 +0100 Subject: [PATCH 185/198] test(libSceHmdSetupDialog): cover uninitialized and terminated null argument handling --- core/libs/tests/GuestHmdSetupDialog.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/libs/tests/GuestHmdSetupDialog.cpp b/core/libs/tests/GuestHmdSetupDialog.cpp index 7e647a5875..347f8c4ad1 100644 --- a/core/libs/tests/GuestHmdSetupDialog.cpp +++ b/core/libs/tests/GuestHmdSetupDialog.cpp @@ -51,6 +51,8 @@ int main() { Require(sceHmdSetupDialogClose() == 0); Require(sceHmdSetupDialogTerminate() == 0); Require(sceHmdSetupDialogTerminate() == kErrNotInitialized); + Require(sceHmdSetupDialogOpen(nullptr) == kErrNotInitialized); + Require(sceHmdSetupDialogGetResult(nullptr) == kErrNotInitialized); Require(sceHmdSetupDialogGetStatus() == kStatusNone); Require(sceHmdSetupDialogUpdateStatus() == kStatusNone); From 0cef3f2546dd42699ed54674a376991a307bb85e Mon Sep 17 00:00:00 2001 From: Hedi Trigui Date: Mon, 26 Oct 2026 19:37:33 +0100 Subject: [PATCH 186/198] test(libSceVrSetupDialog): cover uninitialized and terminated argument validation --- core/libs/tests/GuestVrSetupDialog.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/libs/tests/GuestVrSetupDialog.cpp b/core/libs/tests/GuestVrSetupDialog.cpp index 3297365257..d08cedc1dd 100644 --- a/core/libs/tests/GuestVrSetupDialog.cpp +++ b/core/libs/tests/GuestVrSetupDialog.cpp @@ -25,6 +25,7 @@ int main() { std::int32_t result[2] = {-1, -1}; Require(sceVrSetupDialogTerminate() == kErrNotInitialized); + Require(sceVrSetupDialogOpen(nullptr) == kErrArgNull); Require(sceVrSetupDialogOpen(¶m) == kErrNotInitialized); Require(sceVrSetupDialogClose() == kErrNotInitialized); Require(sceVrSetupDialogGetResult(nullptr) == kErrArgNull); @@ -64,6 +65,8 @@ int main() { Require(sceVrSetupDialogClose() == 0); Require(sceVrSetupDialogTerminate() == 0); Require(sceVrSetupDialogTerminate() == kErrNotInitialized); + Require(sceVrSetupDialogOpen(nullptr) == kErrArgNull); + Require(sceVrSetupDialogGetResult(nullptr) == kErrArgNull); Require(sceVrSetupDialogGetStatus() == kStatusNone); Require(sceVrSetupDialogUpdateStatus() == kStatusNone); From e2ed742187bfdfe9632f53ff0cf0c1860d580428 Mon Sep 17 00:00:00 2001 From: Hedi Trigui Date: Mon, 26 Oct 2026 19:38:20 +0100 Subject: [PATCH 187/198] test(libSceWebBrowserDialog): cover uninitialized and terminated argument validation --- core/libs/tests/GuestWebBrowserDialog.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/core/libs/tests/GuestWebBrowserDialog.cpp b/core/libs/tests/GuestWebBrowserDialog.cpp index 01fdca210e..53ce0d3955 100644 --- a/core/libs/tests/GuestWebBrowserDialog.cpp +++ b/core/libs/tests/GuestWebBrowserDialog.cpp @@ -31,7 +31,9 @@ int main() { Require(sceWebBrowserDialogResetCookie(nullptr) == COMMON_DIALOG_ERROR_NOT_INITIALIZED); Require(sceWebBrowserDialogGetStatus() == COMMON_DIALOG_STATUS_NONE); Require(sceWebBrowserDialogClose() == COMMON_DIALOG_ERROR_NOT_INITIALIZED); + Require(sceWebBrowserDialogOpen(nullptr) == COMMON_DIALOG_ERROR_NOT_INITIALIZED); Require(sceWebBrowserDialogOpen(param) == COMMON_DIALOG_ERROR_NOT_INITIALIZED); + Require(sceWebBrowserDialogGetResult(nullptr) == COMMON_DIALOG_ERROR_ARG_NULL); Require(sceWebBrowserDialogGetResult(&result) == COMMON_DIALOG_ERROR_NOT_INITIALIZED); Require(result.result == 0x5a5a5a5a); @@ -44,6 +46,8 @@ int main() { Require(sceWebBrowserDialogTerminate() == 0); Require(sceWebBrowserDialogTerminate() == COMMON_DIALOG_ERROR_NOT_INITIALIZED); + Require(sceWebBrowserDialogOpen(nullptr) == COMMON_DIALOG_ERROR_NOT_INITIALIZED); + Require(sceWebBrowserDialogGetResult(nullptr) == COMMON_DIALOG_ERROR_ARG_NULL); Require(sceWebBrowserDialogGetStatus() == COMMON_DIALOG_STATUS_NONE); Require(sceWebBrowserDialogInitialize() == 0); @@ -59,6 +63,8 @@ int main() { Require(sceWebBrowserDialogClose() == static_cast(0x80B8000Bu)); Require(sceWebBrowserDialogTerminate() == 0); Require(sceWebBrowserDialogTerminate() == COMMON_DIALOG_ERROR_NOT_INITIALIZED); + Require(sceWebBrowserDialogOpen(nullptr) == COMMON_DIALOG_ERROR_NOT_INITIALIZED); + Require(sceWebBrowserDialogGetResult(nullptr) == COMMON_DIALOG_ERROR_ARG_NULL); Require(sceWebBrowserDialogGetStatus() == COMMON_DIALOG_STATUS_NONE); Require(sceWebBrowserDialogOpenForPredeterminedContent(param, &allowList) == COMMON_DIALOG_ERROR_NOT_INITIALIZED); From 658f048215f9ee58e2a4e9bcaf4e1a36fd56b95c Mon Sep 17 00:00:00 2001 From: Hedi Trigui Date: Mon, 26 Oct 2026 20:02:08 +0100 Subject: [PATCH 188/198] test(libSceSocialScreenDialog): cover post-terminate null argument validation --- core/libs/tests/GuestSocialScreenDialog.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/core/libs/tests/GuestSocialScreenDialog.cpp b/core/libs/tests/GuestSocialScreenDialog.cpp index fc75ebafcf..eb261f831e 100644 --- a/core/libs/tests/GuestSocialScreenDialog.cpp +++ b/core/libs/tests/GuestSocialScreenDialog.cpp @@ -51,6 +51,8 @@ int main() { Require(sceSocialScreenDialogClose() == 0); Require(sceSocialScreenDialogTerminate() == 0); Require(sceSocialScreenDialogTerminate() == kErrNotInitialized); + Require(sceSocialScreenDialogOpen(nullptr) == kErrNotInitialized); + Require(sceSocialScreenDialogGetResult(nullptr) == kErrNotInitialized); Require(sceSocialScreenDialogGetStatus() == kStatusNone); Require(sceSocialScreenDialogUpdateStatus() == kStatusNone); @@ -62,4 +64,9 @@ int main() { Require(sceSocialScreenDialogGetResult(result) == 0); Require(result[0] == kResultUserCanceled); Require(sceSocialScreenDialogTerminate() == 0); + Require(sceSocialScreenDialogTerminate() == kErrNotInitialized); + Require(sceSocialScreenDialogOpen(nullptr) == kErrNotInitialized); + Require(sceSocialScreenDialogGetResult(nullptr) == kErrNotInitialized); + Require(sceSocialScreenDialogGetStatus() == kStatusNone); + Require(sceSocialScreenDialogUpdateStatus() == kStatusNone); } From 95be7bc01663cca5d58f0e019ac0a5066098990d Mon Sep 17 00:00:00 2001 From: Hedi Trigui Date: Mon, 26 Oct 2026 20:02:55 +0100 Subject: [PATCH 189/198] test(libSceLoginDialog): cover post-terminate null and uninitialized argument validation --- core/libs/tests/GuestLoginDialog.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/core/libs/tests/GuestLoginDialog.cpp b/core/libs/tests/GuestLoginDialog.cpp index 9b567297b0..6491d4213b 100644 --- a/core/libs/tests/GuestLoginDialog.cpp +++ b/core/libs/tests/GuestLoginDialog.cpp @@ -75,7 +75,12 @@ int main() { Require(sceLoginDialogClose() == 0); Require(sceLoginDialogTerminate() == 0); Require(sceLoginDialogTerminate() == kErrNotInitialized); + Require(sceLoginDialogOpen(nullptr) == kErrArgNull); + Require(sceLoginDialogOpen(¶m) == kErrNotInitialized); + Require(sceLoginDialogGetResult(nullptr) == kErrArgNull); + Require(sceLoginDialogGetResult(&result) == kErrNotInitialized); Require(sceLoginDialogGetStatus() == kStatusNone); + Require(sceLoginDialogUpdateStatus() == kStatusNone); Require(sceLoginDialogInitialize() == 0); Require(sceLoginDialogGetStatus() == kStatusInitialized); @@ -85,4 +90,11 @@ int main() { Require(sceLoginDialogGetResult(&result) == 0); Require(result.result == kResultUserCanceled); Require(sceLoginDialogTerminate() == 0); + Require(sceLoginDialogTerminate() == kErrNotInitialized); + Require(sceLoginDialogOpen(nullptr) == kErrArgNull); + Require(sceLoginDialogOpen(¶m) == kErrNotInitialized); + Require(sceLoginDialogGetResult(nullptr) == kErrArgNull); + Require(sceLoginDialogGetResult(&result) == kErrNotInitialized); + Require(sceLoginDialogGetStatus() == kStatusNone); + Require(sceLoginDialogUpdateStatus() == kStatusNone); } From 9267414d779a8ee3c64a14ea0b8de951c03c4aaa Mon Sep 17 00:00:00 2001 From: Hedi Trigui Date: Mon, 26 Oct 2026 20:03:32 +0100 Subject: [PATCH 190/198] test(libSceVrServiceDialog): cover post-terminate null argument validation --- core/libs/tests/GuestVrServiceDialog.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/core/libs/tests/GuestVrServiceDialog.cpp b/core/libs/tests/GuestVrServiceDialog.cpp index 5b7433ce53..43664c8263 100644 --- a/core/libs/tests/GuestVrServiceDialog.cpp +++ b/core/libs/tests/GuestVrServiceDialog.cpp @@ -51,6 +51,8 @@ int main() { Require(sceVrServiceDialogClose() == 0); Require(sceVrServiceDialogTerminate() == 0); Require(sceVrServiceDialogTerminate() == kErrNotInitialized); + Require(sceVrServiceDialogOpen(nullptr) == kErrNotInitialized); + Require(sceVrServiceDialogGetResult(nullptr) == kErrNotInitialized); Require(sceVrServiceDialogGetStatus() == kStatusNone); Require(sceVrServiceDialogUpdateStatus() == kStatusNone); @@ -62,4 +64,9 @@ int main() { Require(sceVrServiceDialogGetResult(result) == 0); Require(result[0] == kResultUserCanceled); Require(sceVrServiceDialogTerminate() == 0); + Require(sceVrServiceDialogTerminate() == kErrNotInitialized); + Require(sceVrServiceDialogOpen(nullptr) == kErrNotInitialized); + Require(sceVrServiceDialogGetResult(nullptr) == kErrNotInitialized); + Require(sceVrServiceDialogGetStatus() == kStatusNone); + Require(sceVrServiceDialogUpdateStatus() == kStatusNone); } From 50814153e9a581a4ded1d710adb67555befc4e5f Mon Sep 17 00:00:00 2001 From: Hedi Trigui Date: Mon, 26 Oct 2026 20:05:28 +0100 Subject: [PATCH 191/198] test(libSceSigninDialog): cover post-terminate argument validation --- core/libs/tests/GuestSigninDialog.cpp | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/core/libs/tests/GuestSigninDialog.cpp b/core/libs/tests/GuestSigninDialog.cpp index 7baf28dae6..735cb77a9a 100644 --- a/core/libs/tests/GuestSigninDialog.cpp +++ b/core/libs/tests/GuestSigninDialog.cpp @@ -55,7 +55,13 @@ int main() { Require(sceSigninDialogClose() == 0); Require(sceSigninDialogTerminate() == 0); Require(sceSigninDialogTerminate() == kErrNotInitialized); + Require(sceSigninDialogOpen(nullptr) == kErrNotInitialized); + Require(sceSigninDialogOpen(¶m) == kErrNotInitialized); + Require(sceSigninDialogClose() == kErrNotInitialized); + Require(sceSigninDialogGetResult(nullptr) == kErrNotInitialized); + Require(sceSigninDialogGetResult(result) == kErrNotInitialized); Require(sceSigninDialogGetStatus() == kStatusNone); + Require(sceSigninDialogUpdateStatus() == kStatusNone); Require(sceSigninDialogInitialize() == 0); Require(sceSigninDialogGetStatus() == kStatusInitialized); @@ -68,4 +74,12 @@ int main() { Require(result[2] == 0); Require(result[3] == 0); Require(sceSigninDialogTerminate() == 0); + Require(sceSigninDialogTerminate() == kErrNotInitialized); + Require(sceSigninDialogOpen(nullptr) == kErrNotInitialized); + Require(sceSigninDialogOpen(¶m) == kErrNotInitialized); + Require(sceSigninDialogClose() == kErrNotInitialized); + Require(sceSigninDialogGetResult(nullptr) == kErrNotInitialized); + Require(sceSigninDialogGetResult(result) == kErrNotInitialized); + Require(sceSigninDialogGetStatus() == kStatusNone); + Require(sceSigninDialogUpdateStatus() == kStatusNone); } From 7c105a907e7b255eca269d12bbee8af8c9444d45 Mon Sep 17 00:00:00 2001 From: Hedi Trigui Date: Mon, 26 Oct 2026 20:06:17 +0100 Subject: [PATCH 192/198] test(libSceCdlgG2P): cover post-terminate argument validation --- core/libs/tests/GuestG2PDialog.cpp | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/core/libs/tests/GuestG2PDialog.cpp b/core/libs/tests/GuestG2PDialog.cpp index afb2800b64..b0c5cc8adc 100644 --- a/core/libs/tests/GuestG2PDialog.cpp +++ b/core/libs/tests/GuestG2PDialog.cpp @@ -79,7 +79,13 @@ int main() { Require(sceG2PDialogClose() == static_cast(0x80B8000Bu)); Require(sceG2PDialogTerminate() == 0); Require(sceG2PDialogTerminate() == kErrNotInitialized); + Require(sceG2PDialogOpen(nullptr) == static_cast(0x80B8000Du)); + Require(sceG2PDialogOpen(¶m) == kErrNotInitialized); + Require(sceG2PDialogClose() == kErrNotInitialized); + Require(sceG2PDialogGetResult(nullptr) == static_cast(0x80B8000Du)); + Require(sceG2PDialogGetResult(reinterpret_cast(result)) == kErrNotInitialized); Require(sceG2PDialogGetStatus() == kStatusNone); + Require(sceG2PDialogUpdateStatus() == kStatusNone); Require(sceG2PDialogInitialize() == 0); Require(sceG2PDialogGetStatus() == kStatusInitialized); @@ -90,5 +96,13 @@ int main() { Require(result[0] == -1 && result[1] == -1 && result[3] == -1); Require(result[2] == -1); Require(sceG2PDialogTerminate() == 0); + Require(sceG2PDialogTerminate() == kErrNotInitialized); + Require(sceG2PDialogOpen(nullptr) == static_cast(0x80B8000Du)); + Require(sceG2PDialogOpen(¶m) == kErrNotInitialized); + Require(sceG2PDialogClose() == kErrNotInitialized); + Require(sceG2PDialogGetResult(nullptr) == static_cast(0x80B8000Du)); + Require(sceG2PDialogGetResult(reinterpret_cast(result)) == kErrNotInitialized); + Require(sceG2PDialogGetStatus() == kStatusNone); + Require(sceG2PDialogUpdateStatus() == kStatusNone); Require(sceUserServiceTerminate() == 0); } From b3084879eaa0b45bace1191dcfcfddc3891feb28 Mon Sep 17 00:00:00 2001 From: Hedi Trigui Date: Mon, 26 Oct 2026 20:08:30 +0100 Subject: [PATCH 193/198] test(libSceNpFriendListDialog): cover uninitialized and terminated argument validation --- core/libs/tests/GuestNpFriendListDialog.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/core/libs/tests/GuestNpFriendListDialog.cpp b/core/libs/tests/GuestNpFriendListDialog.cpp index 7e85ecf4cd..897d26ebe2 100644 --- a/core/libs/tests/GuestNpFriendListDialog.cpp +++ b/core/libs/tests/GuestNpFriendListDialog.cpp @@ -51,6 +51,11 @@ int main() { Require(sceNpFriendListDialogClose() == 0); Require(sceNpFriendListDialogTerminate() == 0); Require(sceNpFriendListDialogTerminate() == kErrNotInitialized); + Require(sceNpFriendListDialogOpenA(nullptr) == kErrNotInitialized); + Require(sceNpFriendListDialogOpenA(¶m) == kErrNotInitialized); + Require(sceNpFriendListDialogClose() == kErrNotInitialized); + Require(sceNpFriendListDialogGetResultA(nullptr) == kErrNotInitialized); + Require(sceNpFriendListDialogGetResultA(result) == kErrNotInitialized); Require(sceNpFriendListDialogGetStatus() == kStatusNone); Require(sceNpFriendListDialogUpdateStatus() == kStatusNone); @@ -62,4 +67,12 @@ int main() { Require(sceNpFriendListDialogGetResultA(result) == 0); Require(result[0] == kResultUserCanceled); Require(sceNpFriendListDialogTerminate() == 0); + Require(sceNpFriendListDialogTerminate() == kErrNotInitialized); + Require(sceNpFriendListDialogOpenA(nullptr) == kErrNotInitialized); + Require(sceNpFriendListDialogOpenA(¶m) == kErrNotInitialized); + Require(sceNpFriendListDialogClose() == kErrNotInitialized); + Require(sceNpFriendListDialogGetResultA(nullptr) == kErrNotInitialized); + Require(sceNpFriendListDialogGetResultA(result) == kErrNotInitialized); + Require(sceNpFriendListDialogGetStatus() == kStatusNone); + Require(sceNpFriendListDialogUpdateStatus() == kStatusNone); } From 340ab9f9703b41635dfa6c747758cc40cadffe5f Mon Sep 17 00:00:00 2001 From: Hedi Trigui Date: Mon, 26 Oct 2026 20:09:23 +0100 Subject: [PATCH 194/198] test(libSceNpSnsFacebookDialog): cover uninitialized and terminated argument validation --- core/libs/tests/GuestNpSnsFacebookDialog.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/core/libs/tests/GuestNpSnsFacebookDialog.cpp b/core/libs/tests/GuestNpSnsFacebookDialog.cpp index fd551e915f..507569039f 100644 --- a/core/libs/tests/GuestNpSnsFacebookDialog.cpp +++ b/core/libs/tests/GuestNpSnsFacebookDialog.cpp @@ -51,6 +51,11 @@ int main() { Require(sceNpSnsFacebookDialogClose() == 0); Require(sceNpSnsFacebookDialogTerminate() == 0); Require(sceNpSnsFacebookDialogTerminate() == kErrNotInitialized); + Require(sceNpSnsFacebookDialogOpen(nullptr) == kErrNotInitialized); + Require(sceNpSnsFacebookDialogOpen(¶m) == kErrNotInitialized); + Require(sceNpSnsFacebookDialogClose() == kErrNotInitialized); + Require(sceNpSnsFacebookDialogGetResult(nullptr) == kErrNotInitialized); + Require(sceNpSnsFacebookDialogGetResult(result) == kErrNotInitialized); Require(sceNpSnsFacebookDialogGetStatus() == kStatusNone); Require(sceNpSnsFacebookDialogUpdateStatus() == kStatusNone); @@ -62,4 +67,12 @@ int main() { Require(sceNpSnsFacebookDialogGetResult(result) == 0); Require(result[0] == kResultUserCanceled); Require(sceNpSnsFacebookDialogTerminate() == 0); + Require(sceNpSnsFacebookDialogTerminate() == kErrNotInitialized); + Require(sceNpSnsFacebookDialogOpen(nullptr) == kErrNotInitialized); + Require(sceNpSnsFacebookDialogOpen(¶m) == kErrNotInitialized); + Require(sceNpSnsFacebookDialogClose() == kErrNotInitialized); + Require(sceNpSnsFacebookDialogGetResult(nullptr) == kErrNotInitialized); + Require(sceNpSnsFacebookDialogGetResult(result) == kErrNotInitialized); + Require(sceNpSnsFacebookDialogGetStatus() == kStatusNone); + Require(sceNpSnsFacebookDialogUpdateStatus() == kStatusNone); } From 5cceb6c60c6edef829e31f78c814b88741a7240d Mon Sep 17 00:00:00 2001 From: Hedi Trigui Date: Mon, 26 Oct 2026 20:10:17 +0100 Subject: [PATCH 195/198] test(libSceCdlgPlayerReview): cover post-terminate argument validation --- core/libs/tests/GuestPlayerReviewDialog.cpp | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/core/libs/tests/GuestPlayerReviewDialog.cpp b/core/libs/tests/GuestPlayerReviewDialog.cpp index c14e478429..ecdea31224 100644 --- a/core/libs/tests/GuestPlayerReviewDialog.cpp +++ b/core/libs/tests/GuestPlayerReviewDialog.cpp @@ -82,7 +82,14 @@ int main() { Require(scePlayerReviewDialogGetStatus() == COMMON_DIALOG_STATUS_FINISHED); Require(scePlayerReviewDialogUpdateStatus() == COMMON_DIALOG_STATUS_FINISHED); Require(scePlayerReviewDialogTerminate() == 0); + Require(scePlayerReviewDialogTerminate() == COMMON_DIALOG_ERROR_NOT_INITIALIZED); + Require(scePlayerReviewDialogOpen(nullptr) == static_cast(0x80B8000Du)); + Require(scePlayerReviewDialogOpen(¶m) == COMMON_DIALOG_ERROR_NOT_INITIALIZED); + Require(scePlayerReviewDialogClose() == COMMON_DIALOG_ERROR_NOT_INITIALIZED); + Require(scePlayerReviewDialogGetResult(nullptr) == static_cast(0x80B8000Du)); + Require(scePlayerReviewDialogGetResult(reinterpret_cast(result)) == COMMON_DIALOG_ERROR_NOT_INITIALIZED); Require(scePlayerReviewDialogGetStatus() == COMMON_DIALOG_STATUS_NONE); + Require(scePlayerReviewDialogUpdateStatus() == COMMON_DIALOG_STATUS_NONE); Require(scePlayerReviewDialogInitialize() == 0); Require(scePlayerReviewDialogGetStatus() == COMMON_DIALOG_STATUS_INITIALIZED); Require(scePlayerReviewDialogClose() == static_cast(0x80B8000Bu)); @@ -93,6 +100,13 @@ int main() { Require(result[0] == -1 && result[1] == -1 && result[3] == -1); Require(result[2] == -1); Require(scePlayerReviewDialogTerminate() == 0); + Require(scePlayerReviewDialogTerminate() == COMMON_DIALOG_ERROR_NOT_INITIALIZED); + Require(scePlayerReviewDialogOpen(nullptr) == static_cast(0x80B8000Du)); + Require(scePlayerReviewDialogOpen(¶m) == COMMON_DIALOG_ERROR_NOT_INITIALIZED); + Require(scePlayerReviewDialogClose() == COMMON_DIALOG_ERROR_NOT_INITIALIZED); + Require(scePlayerReviewDialogGetResult(nullptr) == static_cast(0x80B8000Du)); + Require(scePlayerReviewDialogGetResult(reinterpret_cast(result)) == COMMON_DIALOG_ERROR_NOT_INITIALIZED); + Require(scePlayerReviewDialogGetStatus() == COMMON_DIALOG_STATUS_NONE); Require(scePlayerReviewDialogUpdateStatus() == COMMON_DIALOG_STATUS_NONE); Require(sceUserServiceTerminate() == 0); } From 7480f9ed16c817ea1a705c809652007e03cd3d29 Mon Sep 17 00:00:00 2001 From: Hedi Trigui Date: Mon, 26 Oct 2026 20:11:11 +0100 Subject: [PATCH 196/198] test(libSceNetCtlApDialog): cover uninitialized and terminated argument validation --- core/libs/tests/GuestNetCtlApDialog.cpp | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/core/libs/tests/GuestNetCtlApDialog.cpp b/core/libs/tests/GuestNetCtlApDialog.cpp index 82b0318b87..f9dd7ae14d 100644 --- a/core/libs/tests/GuestNetCtlApDialog.cpp +++ b/core/libs/tests/GuestNetCtlApDialog.cpp @@ -37,8 +37,10 @@ int main() { std::memset(result, 0x5a, sizeof(result)); Require(sceNetCtlApDialogTerminate() == kErrNotInitialized); + Require(sceNetCtlApDialogOpen(nullptr) == kErrArgNull); Require(sceNetCtlApDialogOpen(¶m) == kErrNotInitialized); Require(sceNetCtlApDialogClose() == kErrNotInitialized); + Require(sceNetCtlApDialogGetResult(nullptr) == kErrArgNull); Require(sceNetCtlApDialogGetResult(reinterpret_cast(result)) == kErrNotInitialized); Require(sceNetCtlApDialogGetStatus() == kStatusNone); Require(sceNetCtlApDialogUpdateStatus() == kStatusNone); @@ -78,11 +80,25 @@ int main() { Require(sceNetCtlApDialogClose() == static_cast(0x80B8000Bu)); Require(sceNetCtlApDialogTerminate() == 0); Require(sceNetCtlApDialogTerminate() == kErrNotInitialized); + Require(sceNetCtlApDialogOpen(nullptr) == kErrArgNull); + Require(sceNetCtlApDialogOpen(¶m) == kErrNotInitialized); + Require(sceNetCtlApDialogClose() == kErrNotInitialized); + Require(sceNetCtlApDialogGetResult(nullptr) == kErrArgNull); + Require(sceNetCtlApDialogGetResult(reinterpret_cast(result)) == kErrNotInitialized); Require(sceNetCtlApDialogGetStatus() == kStatusNone); + Require(sceNetCtlApDialogUpdateStatus() == kStatusNone); Require(sceNetCtlApDialogInitialize() == 0); Require(sceNetCtlApDialogClose() == static_cast(0x80B8000Bu)); Require(sceNetCtlApDialogGetStatus() == kStatusInitialized); Require(sceNetCtlApDialogGetResult(reinterpret_cast(result)) == kErrNotFinished); Require(sceNetCtlApDialogTerminate() == 0); + Require(sceNetCtlApDialogTerminate() == kErrNotInitialized); + Require(sceNetCtlApDialogOpen(nullptr) == kErrArgNull); + Require(sceNetCtlApDialogOpen(¶m) == kErrNotInitialized); + Require(sceNetCtlApDialogClose() == kErrNotInitialized); + Require(sceNetCtlApDialogGetResult(nullptr) == kErrArgNull); + Require(sceNetCtlApDialogGetResult(reinterpret_cast(result)) == kErrNotInitialized); + Require(sceNetCtlApDialogGetStatus() == kStatusNone); + Require(sceNetCtlApDialogUpdateStatus() == kStatusNone); } From 960a96ebc48ee7ea5690f506c531b8d9769d34c8 Mon Sep 17 00:00:00 2001 From: Hedi Trigui Date: Mon, 26 Oct 2026 20:12:24 +0100 Subject: [PATCH 197/198] test(libSceNpAuthAuthorizedAppDialog): cover uninitialized and terminated argument validation --- .../libs/tests/GuestNpAuthAuthorizedAppDialog.cpp | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/core/libs/tests/GuestNpAuthAuthorizedAppDialog.cpp b/core/libs/tests/GuestNpAuthAuthorizedAppDialog.cpp index a8fecd529b..d75bc43596 100644 --- a/core/libs/tests/GuestNpAuthAuthorizedAppDialog.cpp +++ b/core/libs/tests/GuestNpAuthAuthorizedAppDialog.cpp @@ -35,9 +35,12 @@ int main() { std::int32_t result[2] = {-1, -1}; Require(sceNpAuthAuthorizedAppDialogGetStatus() == kStatusNone); + Require(sceNpAuthAuthorizedAppDialogUpdateStatus() == kStatusNone); Require(sceNpAuthAuthorizedAppDialogTerminate() == kErrNotInitialized); Require(sceNpAuthAuthorizedAppDialogClose() == kErrNotInitialized); + Require(sceNpAuthAuthorizedAppDialogOpen(nullptr) == kErrArgNull); Require(sceNpAuthAuthorizedAppDialogOpen(¶m) == kErrNotInitialized); + Require(sceNpAuthAuthorizedAppDialogGetResult(nullptr) == kErrArgNull); Require(sceNpAuthAuthorizedAppDialogGetResult(result) == kErrNotInitialized); Require(sceNpAuthAuthorizedAppDialogInitialize() == 0); @@ -83,7 +86,13 @@ int main() { Require(sceNpAuthAuthorizedAppDialogClose() == static_cast(0x80B8000Bu)); Require(sceNpAuthAuthorizedAppDialogTerminate() == 0); Require(sceNpAuthAuthorizedAppDialogTerminate() == kErrNotInitialized); + Require(sceNpAuthAuthorizedAppDialogClose() == kErrNotInitialized); + Require(sceNpAuthAuthorizedAppDialogOpen(nullptr) == kErrArgNull); + Require(sceNpAuthAuthorizedAppDialogOpen(¶m) == kErrNotInitialized); + Require(sceNpAuthAuthorizedAppDialogGetResult(nullptr) == kErrArgNull); + Require(sceNpAuthAuthorizedAppDialogGetResult(result) == kErrNotInitialized); Require(sceNpAuthAuthorizedAppDialogGetStatus() == kStatusNone); + Require(sceNpAuthAuthorizedAppDialogUpdateStatus() == kStatusNone); Require(sceNpAuthAuthorizedAppDialogInitialize() == 0); Require(sceNpAuthAuthorizedAppDialogGetStatus() == kStatusInitialized); @@ -91,8 +100,14 @@ int main() { Require(sceNpAuthAuthorizedAppDialogGetStatus() == kStatusInitialized); Require(sceNpAuthAuthorizedAppDialogGetResult(result) == kErrNotFinished); Require(sceNpAuthAuthorizedAppDialogTerminate() == 0); + Require(sceNpAuthAuthorizedAppDialogTerminate() == kErrNotInitialized); + Require(sceNpAuthAuthorizedAppDialogClose() == kErrNotInitialized); Require(sceNpAuthAuthorizedAppDialogOpen(nullptr) == kErrArgNull); + Require(sceNpAuthAuthorizedAppDialogOpen(¶m) == kErrNotInitialized); Require(sceNpAuthAuthorizedAppDialogGetResult(nullptr) == kErrArgNull); + Require(sceNpAuthAuthorizedAppDialogGetResult(result) == kErrNotInitialized); + Require(sceNpAuthAuthorizedAppDialogGetStatus() == kStatusNone); + Require(sceNpAuthAuthorizedAppDialogUpdateStatus() == kStatusNone); Require(sceUserServiceTerminate() == 0); return 0; } From 3841816986a8d7694cfd8ddf36e35f717855ef5e Mon Sep 17 00:00:00 2001 From: Hedi Trigui Date: Tue, 27 Oct 2026 08:35:17 +0100 Subject: [PATCH 198/198] test(libSceGameCustomDataDialog): cover post-terminate argument validation --- core/libs/tests/GuestGameCustomDataDialog.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/libs/tests/GuestGameCustomDataDialog.cpp b/core/libs/tests/GuestGameCustomDataDialog.cpp index cdf6474337..6c9228eeab 100644 --- a/core/libs/tests/GuestGameCustomDataDialog.cpp +++ b/core/libs/tests/GuestGameCustomDataDialog.cpp @@ -65,4 +65,12 @@ int main() { Require(sceGameCustomDataDialogGetResultA(result) == 0); Require(result[0] == kResultUserCanceled); Require(sceGameCustomDataDialogTerminate() == 0); + Require(sceGameCustomDataDialogTerminate() == kErrNotInitialized); + Require(sceGameCustomDataDialogOpenA(nullptr) == kErrNotInitialized); + Require(sceGameCustomDataDialogOpenA(¶m) == kErrNotInitialized); + Require(sceGameCustomDataDialogClose() == kErrNotInitialized); + Require(sceGameCustomDataDialogGetResultA(nullptr) == kErrNotInitialized); + Require(sceGameCustomDataDialogGetResultA(result) == kErrNotInitialized); + Require(sceGameCustomDataDialogGetStatus() == kStatusNone); + Require(sceGameCustomDataDialogUpdateStatus() == kStatusNone); }