From 1cbf3fa8e4f871a6af1d1c62bfe2e323756817c0 Mon Sep 17 00:00:00 2001 From: Pouare514 Date: Fri, 2 Oct 2026 22:08:52 +0200 Subject: [PATCH 01/19] feat(libc): implement at_quick_exit and quick_exit --- .../prx/libSceSystemService/CMakeLists.txt | 5 +++- core/libs/prx/libc/src/Process.cpp | 23 +++++++++++++++++++ core/libs/tests/GuestSystemExit.cpp | 15 ++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceSystemService/CMakeLists.txt b/core/libs/prx/libSceSystemService/CMakeLists.txt index ff18d4bad..de42305b7 100644 --- a/core/libs/prx/libSceSystemService/CMakeLists.txt +++ b/core/libs/prx/libSceSystemService/CMakeLists.txt @@ -15,12 +15,15 @@ if(BUILD_TESTING) configure_windows_unwind(guest_system_exit_tests) add_test(NAME guest_system_exit_parameters COMMAND guest_system_exit_tests) add_test(NAME guest_system_exit COMMAND guest_system_exit_tests --exit) + add_test(NAME guest_quick_exit COMMAND guest_system_exit_tests --quick-exit) add_test(NAME guest_immediate_exit COMMAND guest_system_exit_tests --immediate) add_test(NAME guest_return_from_main COMMAND guest_system_exit_tests --return-from-main) set_tests_properties(guest_system_exit PROPERTIES PASS_REGULAR_EXPRESSION "Guest shutdown and atexit completed") + set_tests_properties(guest_quick_exit PROPERTIES + PASS_REGULAR_EXPRESSION "Guest quick_exit handlers completed") if(WIN32) - set_tests_properties(guest_system_exit guest_system_exit_parameters guest_immediate_exit guest_return_from_main PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_system_exit guest_system_exit_parameters guest_immediate_exit guest_return_from_main guest_quick_exit PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test_executable(guest_system_service_tests ${LIBS_INCLUDE_DIR}/tests/GuestSystemService.cpp) diff --git a/core/libs/prx/libc/src/Process.cpp b/core/libs/prx/libc/src/Process.cpp index 48526ffff..d8ce47284 100644 --- a/core/libs/prx/libc/src/Process.cpp +++ b/core/libs/prx/libc/src/Process.cpp @@ -162,4 +162,27 @@ int APS5_VABI atexit_nid_postfix(GuestExitCallback func) { return result; } +namespace { +std::mutex quickExitMutex; +std::vector quickExitCallbacks; +} + +int APS5_VABI at_quick_exit_nid_postfix(GuestExitCallback func) { + if (func == nullptr) + return 0; + std::lock_guard lock(quickExitMutex); + quickExitCallbacks.push_back(func); + return 0; +} + +[[noreturn]] void APS5_VABI quick_exit_nid_postfix(int status) { + std::vector callbacks; + { + std::lock_guard lock(quickExitMutex); + callbacks = std::move(quickExitCallbacks); + } + for (auto it = callbacks.rbegin(); it != callbacks.rend(); ++it) (*it)(); + _Exit_nid_postfix(status); +} + } diff --git a/core/libs/tests/GuestSystemExit.cpp b/core/libs/tests/GuestSystemExit.cpp index 5afefa49e..a10647cd9 100644 --- a/core/libs/tests/GuestSystemExit.cpp +++ b/core/libs/tests/GuestSystemExit.cpp @@ -8,6 +8,9 @@ extern "C" int APS5_VABI sceSystemServiceLoadExec(const char*, const char* const*); extern "C" void APS5_VABI _Exit_nid_postfix(int); extern "C" void APS5_VABI catchReturnFromMain_nid_postfix(int); +using GuestQuickExitCallback = void (APS5_VABI*)(); +extern "C" int APS5_VABI at_quick_exit_nid_postfix(GuestQuickExitCallback); +extern "C" void APS5_VABI quick_exit_nid_postfix(int); namespace { bool cleaned = false; void Cleanup() { cleaned = true; } @@ -15,6 +18,12 @@ void VerifyExit() { if (!cleaned) std::_Exit(1); std::puts("Guest shutdown and atexit completed"); } +int quickOrder = 0; +void APS5_VABI QuickSecond() { quickOrder = 1; } +void APS5_VABI QuickFirst() { + if (quickOrder != 1) std::_Exit(1); + std::puts("Guest quick_exit handlers completed"); +} void Require(bool value) { if (!value) std::abort(); } void UnexpectedCleanup() { std::_Exit(3); } } @@ -29,6 +38,12 @@ int main(int argc, char** argv) { catchReturnFromMain_nid_postfix(0); return 2; } + if (argc > 1 && std::strcmp(argv[1], "--quick-exit") == 0) { + Require(at_quick_exit_nid_postfix(QuickFirst) == 0); + Require(at_quick_exit_nid_postfix(QuickSecond) == 0); + quick_exit_nid_postfix(0); + return 2; + } if (argc > 1) { LibcRegisterShutdown_nid_postfix(Cleanup); Require(std::atexit(VerifyExit) == 0); From 2be3860983b6a483edd5f1f32067f0d217d7e9c0 Mon Sep 17 00:00:00 2001 From: Pouare514 Date: Sun, 4 Oct 2026 15:24:44 +0200 Subject: [PATCH 02/19] fix(tests): flush stdout in QuickFirst so ctest pipe captures handler output --- core/libs/tests/GuestSystemExit.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/tests/GuestSystemExit.cpp b/core/libs/tests/GuestSystemExit.cpp index a10647cd9..3a22d960f 100644 --- a/core/libs/tests/GuestSystemExit.cpp +++ b/core/libs/tests/GuestSystemExit.cpp @@ -23,6 +23,7 @@ void APS5_VABI QuickSecond() { quickOrder = 1; } void APS5_VABI QuickFirst() { if (quickOrder != 1) std::_Exit(1); std::puts("Guest quick_exit handlers completed"); + std::fflush(stdout); } void Require(bool value) { if (!value) std::abort(); } void UnexpectedCleanup() { std::_Exit(3); } From 52587f8cf100dc42847a8ca04e80d5d0021066ed Mon Sep 17 00:00:00 2001 From: Pouare514 Date: Fri, 2 Oct 2026 01:30:56 +0200 Subject: [PATCH 03/19] fix(relinker): tolerate trailing data in TLS scan --- .../src/analysis/CodeInstructionCollector.cpp | 58 +++++++++++++++++-- .../tests/test_tls_function_coverage.py | 2 +- 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp index 0b009c39f..d87d8eea8 100644 --- a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp +++ b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp @@ -173,6 +173,8 @@ std::set CodeInstructionCollector::Collect(const std::ve if (roots.empty()) throw Domain::RelinkerException("Code analysis: no code entry points"); std::set instructions; const Codegen::X64InstructionDecoder decoder; + std::vector> skipped; + std::set staticTargets; for (const auto& [begin, end] : functions) { const auto offset = fileOffset(begin, end - begin); for (auto address = begin; address < end;) { @@ -180,16 +182,36 @@ std::set CodeInstructionCollector::Collect(const std::ve try { info = decoder.DecodeInstruction(bytes.data() + offset + address - begin, end - address); } catch (const Codegen::CodegenException& error) { - throw Domain::RelinkerException(std::string("Code analysis: ") + error.what(), address); + const auto tailBytes = end - address; + bool tailHasFsPrefix = false; + for (auto probe = address; probe < end; ++probe) { + if (bytes[offset + probe - begin] == 0x64) { + tailHasFsPrefix = true; + break; + } + } + if (tailBytes > 15 || tailHasFsPrefix) + throw Domain::RelinkerException(std::string("Code analysis: ") + error.what(), address); + skipped.emplace_back(address, end); + break; } if (info.Length == 0 || info.Length > end - address) throw Domain::RelinkerException("Code analysis: instruction crosses function boundary", address); instructions.insert(address); - if (info.HasBranchTarget && !info.HasRipRelativeDisp) - addRoot(address + info.Length + static_cast(info.BranchDisp)); + if (info.HasBranchTarget && !info.HasRipRelativeDisp) { + const auto target = address + info.Length + static_cast(info.BranchDisp); + staticTargets.insert(target); + addRoot(target); + } address += info.Length; } } + for (const auto target : staticTargets) { + for (const auto& [skipBegin, skipEnd] : skipped) { + if (skipBegin <= target && target < skipEnd) + throw Domain::RelinkerException("Code analysis: branch into skipped range tail", target); + } + } std::size_t previousRoots = 0; do { previousRoots = roots.size(); @@ -211,7 +233,35 @@ std::set CodeInstructionCollector::Collect(const std::ve } while (roots.size() != previousRoots); std::uint64_t previousEnd = 0; for (const auto address : instructions) { - if (address < previousEnd) throw Domain::RelinkerException("Code analysis: overlapping instruction boundaries", address); + if (address < previousEnd) { + std::uint64_t spanEnd = previousEnd; + try { + for (const auto& header : headers) { + if (header.Type != 1 || (header.Flags & 1) == 0 || address < header.MappedAddress || address - header.MappedAddress >= header.FileSize) continue; + const auto offset = address - header.MappedAddress; + const auto length = decoder.DecodeInstruction(bytes.data() + header.Offset + offset, header.FileSize - offset).Length; + if (length == 0 || length > header.FileSize - offset) break; + spanEnd = std::max(spanEnd, address + length); + break; + } + } catch (const Codegen::CodegenException&) { + } + bool spanHasFsPrefix = false; + for (const auto& header : headers) { + if (header.Type != 1 || (header.Flags & 1) == 0 || previousEnd < header.MappedAddress || previousEnd - header.MappedAddress >= header.FileSize) continue; + const auto base = header.Offset + previousEnd - header.MappedAddress; + for (auto probe = base; probe < base + (spanEnd - previousEnd) && probe < bytes.size(); ++probe) { + if (bytes[probe] == 0x64) { + spanHasFsPrefix = true; + break; + } + } + break; + } + if (spanHasFsPrefix) throw Domain::RelinkerException("Code analysis: overlapping instruction boundaries", address); + previousEnd = spanEnd; + continue; + } for (const auto& header : headers) { if (header.Type != 1 || (header.Flags & 1) == 0 || address < header.MappedAddress || address - header.MappedAddress >= header.FileSize) continue; const auto offset = address - header.MappedAddress; diff --git a/core/relinker/relinker/tests/test_tls_function_coverage.py b/core/relinker/relinker/tests/test_tls_function_coverage.py index 31606aa11..2c3623e8b 100644 --- a/core/relinker/relinker/tests/test_tls_function_coverage.py +++ b/core/relinker/relinker/tests/test_tls_function_coverage.py @@ -234,7 +234,7 @@ def main(): convert(metadata + "-outside", make_image("register", metadata, 0x1000), "Code analysis: function exceeds executable segment") overlapping = make_image("register", "unwind") overlapping[0x1200:0x1205] = b"\xe9" + struct.pack(" Date: Sun, 4 Oct 2026 15:41:17 +0200 Subject: [PATCH 04/19] fix(relinker): scan overlapping span from address and cover FS-prefixed previous instruction --- .../src/analysis/CodeInstructionCollector.cpp | 26 ++++++++++++++++--- .../tests/test_tls_function_coverage.py | 6 ++++- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp index d87d8eea8..27b580af7 100644 --- a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp +++ b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp @@ -4,6 +4,8 @@ #include #include #include +#include +#include #include #include #include @@ -248,9 +250,9 @@ std::set CodeInstructionCollector::Collect(const std::ve } bool spanHasFsPrefix = false; for (const auto& header : headers) { - if (header.Type != 1 || (header.Flags & 1) == 0 || previousEnd < header.MappedAddress || previousEnd - header.MappedAddress >= header.FileSize) continue; - const auto base = header.Offset + previousEnd - header.MappedAddress; - for (auto probe = base; probe < base + (spanEnd - previousEnd) && probe < bytes.size(); ++probe) { + if (header.Type != 1 || (header.Flags & 1) == 0 || address < header.MappedAddress || address - header.MappedAddress >= header.FileSize) continue; + const auto base = header.Offset + address - header.MappedAddress; + for (auto probe = base; probe < base + (spanEnd - address) && probe < bytes.size(); ++probe) { if (bytes[probe] == 0x64) { spanHasFsPrefix = true; break; @@ -258,6 +260,24 @@ std::set CodeInstructionCollector::Collect(const std::ve } break; } + if (!spanHasFsPrefix) { + const auto previous = instructions.lower_bound(address); + if (previous != instructions.begin()) { + const auto previousStart = *std::prev(previous); + for (const auto& header : headers) { + if (header.Type != 1 || (header.Flags & 1) == 0 || previousStart < header.MappedAddress || previousStart - header.MappedAddress >= header.FileSize) continue; + const auto base = header.Offset + previousStart - header.MappedAddress; + const auto prefixEnd = std::min({base + 4, base + (previousEnd - previousStart), bytes.size()}); + for (auto probe = base; probe < prefixEnd; ++probe) { + if (bytes[probe] == 0x64) { + spanHasFsPrefix = true; + break; + } + } + break; + } + } + } if (spanHasFsPrefix) throw Domain::RelinkerException("Code analysis: overlapping instruction boundaries", address); previousEnd = spanEnd; continue; diff --git a/core/relinker/relinker/tests/test_tls_function_coverage.py b/core/relinker/relinker/tests/test_tls_function_coverage.py index 2c3623e8b..a16497c73 100644 --- a/core/relinker/relinker/tests/test_tls_function_coverage.py +++ b/core/relinker/relinker/tests/test_tls_function_coverage.py @@ -234,7 +234,11 @@ def main(): convert(metadata + "-outside", make_image("register", metadata, 0x1000), "Code analysis: function exceeds executable segment") overlapping = make_image("register", "unwind") overlapping[0x1200:0x1205] = b"\xe9" + struct.pack(" Date: Sun, 4 Oct 2026 16:29:34 +0200 Subject: [PATCH 05/19] fix(libs): TLOU sceRtcParseDateTime ISO 8601 subset parser --- core/libs/prx/libSceRtc/Export.cpp | 23 +++++++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 2 files changed, 24 insertions(+) diff --git a/core/libs/prx/libSceRtc/Export.cpp b/core/libs/prx/libSceRtc/Export.cpp index 6b87f3616..8ec554311 100644 --- a/core/libs/prx/libSceRtc/Export.cpp +++ b/core/libs/prx/libSceRtc/Export.cpp @@ -301,6 +301,29 @@ int APS5_VABI sceRtcParseRFC3339(RtcTick* utc, const char* date_time) { return addTicks(utc, &local, -offsetMinutes, TICKS_PER_MINUTE); } +int APS5_VABI sceRtcParseDateTime(RtcTick* utc, const char* date_time) { + if (!utc || !date_time) return SCE_RTC_ERROR_INVALID_POINTER; + const char* cursor = date_time; + int year = 0, month = 0, day = 0, hour = 0, minute = 0, second = 0; + if (!parseDigits(cursor, 4, year) || *cursor++ != '-' || !parseDigits(cursor, 2, month) || *cursor++ != '-' || !parseDigits(cursor, 2, day)) return SCE_RTC_ERROR_BAD_PARSE; + if (*cursor != 'T' && *cursor != 't' && *cursor != ' ') return SCE_RTC_ERROR_BAD_PARSE; + ++cursor; + if (!parseDigits(cursor, 2, hour) || *cursor++ != ':' || !parseDigits(cursor, 2, minute) || *cursor++ != ':' || !parseDigits(cursor, 2, second)) return SCE_RTC_ERROR_BAD_PARSE; + std::uint32_t microsecond = 0; + if (*cursor == '.') { + ++cursor; + std::uint32_t scale = 100000; + if (*cursor < '0' || *cursor > '9') return SCE_RTC_ERROR_BAD_PARSE; + for (; *cursor >= '0' && *cursor <= '9'; ++cursor, scale /= 10) microsecond += static_cast(*cursor - '0') * scale; + } + if (*cursor != 0) return SCE_RTC_ERROR_BAD_PARSE; + const RtcDateTime time{static_cast(year), static_cast(month), static_cast(day), + static_cast(hour), static_cast(minute), static_cast(second), microsecond}; + if (const int result = validate(&time); result != 0) return result; + utc->tick = toTick(time); + return 0; +} + int APS5_VABI sceRtcTickAddTicks(RtcTick* dst, const RtcTick* src, int64_t ticks) { return addTicks(dst, src, ticks, 1); } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index a66170e1c..22d1e657d 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -23,6 +23,7 @@ Throughout the project, every function at every stage either **does exactly what ### Unknown function info - [sceVideoOutOpen](../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - the open param's first word is unknown (PPSA21564 passes 16; it is not the byte size, since the affinity mask is at offset 16); only 16 is accepted. Whether the param continues past offset 24 is also unknown +- [sceRtcParseDateTime](../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - accepted format assumed to be the ISO 8601 subset `YYYY-MM-DDTHH:MM:SS[.ffffff]` (T/t/space separator, optional fractional seconds), matching the sibling RFC 3339 parser; unverified against hardware - [AMPR WaitOnAddress / WaitOnCounter](../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libSceAmpr) - compare encoding assumed to be the WAIT_REG_MEM one (0 always, 1 <, 2 <=, 3 ==, 4 !=, 5 >=, 6 >) - [sceAgcSetSubmitMode](../../core/libs/prx/libSceAgc/Misc/src/Suspend.cpp) (libSceAgc) - mode values unknown; only 0 is accepted - [zARR5aCmkoY](../../core/libs/prx/libSceAgc/DcbFlow/src/Control.cpp) (libSceAgc) - unknown name, signature From f4301a1347c15891ce3335dbb8270bfb4c2b219d Mon Sep 17 00:00:00 2001 From: Pouare514 Date: Fri, 2 Oct 2026 22:11:01 +0200 Subject: [PATCH 06/19] feat(crashreport): dump all threads on fatal crash plus hang watchdog --- .../src/specifics/windows/CrashReport.cpp | 81 ++++++++++++++++++- 1 file changed, 77 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp index a4f388e1e..3c7121db4 100644 --- a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp +++ b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp @@ -1,5 +1,6 @@ #ifdef _WIN32 #include +#include #include "prx/libc/include/GuestArena.hpp" #include #include @@ -8,6 +9,7 @@ #include #include #include +#include #include #include #include "Sse4aEmulation.hpp" @@ -28,6 +30,8 @@ void Report(const char* format, ...) { WriteFile(GetStdHandle(STD_ERROR_HANDLE), buffer, static_cast(length), &written, nullptr); } +void ReportAllThreads(); + void DescribeAddress(std::uint64_t address, char* buffer, std::size_t size) { HMODULE module = nullptr; char path[MAX_PATH] = "?"; @@ -330,12 +334,80 @@ LONG WINAPI ReportCrash(EXCEPTION_POINTERS* info) { } } std::fflush(stderr); + ReportAllThreads(); + std::fflush(stderr); return EXCEPTION_CONTINUE_SEARCH; } +void ReportThreadContext(const CONTEXT* context, DWORD threadId, const char* name) { + char line[MAX_PATH + 64]; + Report(" thread %lu '%s': rip ", static_cast(threadId), name ? name : ""); + DescribeAddress(context->Rip, line, sizeof(line)); + Report("%s rsp 0x%016llx rbp 0x%016llx\n", line, static_cast(context->Rsp), static_cast(context->Rbp)); + std::uint64_t frame = context->Rbp; + for (int depth = 0; depth < 12 && frame != 0 && (frame & 7) == 0 && IsReadable(frame) && IsReadable(frame + 8); ++depth) { + const auto returnAddress = reinterpret_cast(frame)[1]; + if (!IsExecutable(returnAddress)) break; + DescribeAddress(returnAddress, line, sizeof(line)); + Report(" #%d %s\n", depth, line); + const auto next = reinterpret_cast(frame)[0]; + if (next <= frame) break; + frame = next; + } +} + +void ReportAllThreads() { + const DWORD current = GetCurrentThreadId(); + const DWORD pid = GetCurrentProcessId(); + HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0); + if (snapshot == INVALID_HANDLE_VALUE) return; + THREADENTRY32 entry{}; + entry.dwSize = sizeof(entry); + struct CapturedThread { + DWORD id; + char name[128]; + CONTEXT context; + }; + static CapturedThread captured[128]; + std::size_t capturedCount = 0; + if (Thread32First(snapshot, &entry)) { + do { + if (entry.th32OwnerProcessID != pid || entry.th32ThreadID == current) continue; + if (capturedCount >= sizeof(captured) / sizeof(captured[0])) break; + HANDLE thread = OpenThread(THREAD_SUSPEND_RESUME | THREAD_GET_CONTEXT | THREAD_QUERY_INFORMATION, FALSE, entry.th32ThreadID); + if (!thread) continue; + if (SuspendThread(thread) == static_cast(-1)) { + CloseHandle(thread); + continue; + } + CONTEXT context{}; + context.ContextFlags = CONTEXT_FULL; + const BOOL haveContext = GetThreadContext(thread, &context); + ResumeThread(thread); + char name[128] = ""; + PWSTR description = nullptr; + if (SUCCEEDED(GetThreadDescription(thread, &description)) && description) { + WideCharToMultiByte(CP_UTF8, 0, description, -1, name, sizeof(name), nullptr, nullptr); + LocalFree(description); + } + CloseHandle(thread); + if (!haveContext) continue; + captured[capturedCount].id = entry.th32ThreadID; + std::memcpy(captured[capturedCount].name, name, sizeof(captured[capturedCount].name)); + captured[capturedCount].context = context; + ++capturedCount; + } while (Thread32Next(snapshot, &entry)); + } + CloseHandle(snapshot); + Report(" all threads:\n"); + for (std::size_t i = 0; i < capturedCount; ++i) { + ReportThreadContext(&captured[i].context, captured[i].id, captured[i].name); + } +} + // abort() and the UCRT's invalid-parameter path end the process with a silent fast fail // (0xc0000409); both are reported with the calling thread's stack first. -void ReportBacktrace(const char* what) { +void ReportBacktrace(const char* what, bool reportThreads) { void* frames[48]; const auto count = RtlCaptureStackBackTrace(0, 48, frames, nullptr); Report("FATAL: %s on thread %lu\n", what, static_cast(GetCurrentThreadId())); @@ -344,22 +416,23 @@ void ReportBacktrace(const char* what) { DescribeAddress(reinterpret_cast(frames[i]), line, sizeof(line)); Report(" #%u %s\n", static_cast(i), line); } + if (reportThreads) ReportAllThreads(); std::fflush(stderr); } void AbortSignalHandler(int) { - ReportBacktrace("abort() (SIGABRT)"); + ReportBacktrace("abort() (SIGABRT)", true); } void InvalidParameterHandler(const wchar_t*, const wchar_t*, const wchar_t*, unsigned, std::uintptr_t) { - ReportBacktrace("UCRT invalid parameter"); + ReportBacktrace("UCRT invalid parameter", true); } // Static destruction runs on the thread that ends the process (ExitProcess -> DLL_PROCESS_DETACH), // so its stack names the exit's caller when no libc exit/abort path was taken. struct ExitReporter { ~ExitReporter() { - if (std::getenv("APS5_TRACE_EXIT") != nullptr) ReportBacktrace("process exit (static destruction)"); + if (std::getenv("APS5_TRACE_EXIT") != nullptr) ReportBacktrace("process exit (static destruction)", false); } } g_exitReporter; From 2452ffcb3fa789ce9445be716294bfb04131363a Mon Sep 17 00:00:00 2001 From: DotDebian Date: Sun, 4 Oct 2026 17:51:25 +0200 Subject: [PATCH 07/19] feat(libSceAudioOut): implement sceAudioOutSetMixLevelPadSpk The mix level is a linear gain on the pad speaker port, 32768 being 0 dB. A pad speaker port opens at 11626 (-9 dB). An unknown handle, a port of another type and a level above 0 dB return the AudioOut error codes; a negative level throws, as its effect is unknown. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAudioOut/CMakeLists.txt | 10 + core/libs/prx/libSceAudioOut/src/AudioOut.cpp | 30 ++- .../tests/AudioOutMixLevelPadSpk.cpp | 183 ++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 4 files changed, 218 insertions(+), 6 deletions(-) create mode 100644 core/libs/prx/libSceAudioOut/tests/AudioOutMixLevelPadSpk.cpp diff --git a/core/libs/prx/libSceAudioOut/CMakeLists.txt b/core/libs/prx/libSceAudioOut/CMakeLists.txt index 94e484b31..e7ae4fef6 100644 --- a/core/libs/prx/libSceAudioOut/CMakeLists.txt +++ b/core/libs/prx/libSceAudioOut/CMakeLists.txt @@ -32,4 +32,14 @@ if(BUILD_TESTING) set_tests_properties(audio_out2_pad_mix PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + + add_test_executable(audio_out_mix_level_pad_spk_tests tests/AudioOutMixLevelPadSpk.cpp) + target_include_directories(audio_out_mix_level_pad_spk_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(audio_out_mix_level_pad_spk_tests PRIVATE libSceAudioOut libc) + configure_windows_unwind(audio_out_mix_level_pad_spk_tests) + add_test(NAME audio_out_mix_level_pad_spk COMMAND audio_out_mix_level_pad_spk_tests) + if(WIN32) + set_tests_properties(audio_out_mix_level_pad_spk PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp index 04a0fc850..8f865806a 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp @@ -26,6 +26,7 @@ static constexpr int PORT_TYPE_AUX = 127; static constexpr int PORTS_MAX = 32; static constexpr int DEFAULT_VOLUME = 32768; +static constexpr int DEFAULT_PADSPK_MIX_LEVEL = 11626; static constexpr std::uint32_t FORMAT_MASK = 0xFFu; static constexpr std::uint64_t TARGET_LATENCY_US = 40000; static constexpr std::uint64_t DRAIN_TIMEOUT_US = 200000; @@ -86,6 +87,7 @@ struct Port { Format format = Format::Unknown; int channels = 0; int volume[8] = {}; + int mixLevel = DEFAULT_VOLUME; std::uint64_t lastOutputTime = 0; SDL_AudioDeviceID device = 0; SDL_AudioSpec spec = {}; @@ -143,7 +145,7 @@ static const void* prepareBuffer(const Port& port, const void* data, std::vector const auto bps = bytesPerSample(port.format); const auto size = frames * ch * bps; - bool volumeChanged = false; + bool volumeChanged = port.mixLevel != DEFAULT_VOLUME; for (std::uint32_t i = 0; i < ch; i++) { if (port.volume[i] != DEFAULT_VOLUME) { volumeChanged = true; @@ -161,11 +163,12 @@ static const void* prepareBuffer(const Port& port, const void* data, std::vector if (formatIsFloat(port.format)) { auto* dst = reinterpret_cast(buf.data()); const auto* src = static_cast(data); + const float mixLevel = static_cast(port.mixLevel) / static_cast(DEFAULT_VOLUME); for (std::uint32_t fr = 0; fr < frames; fr++) { for (std::uint32_t c = 0; c < ch; c++) { const auto srcCh = isStd ? STD_8CH_MAP[c] : c; dst[fr * ch + c] = src[fr * ch + srcCh] * - (static_cast(port.volume[c]) / static_cast(DEFAULT_VOLUME)); + (static_cast(port.volume[c]) / static_cast(DEFAULT_VOLUME)) * mixLevel; } } } else { @@ -175,7 +178,7 @@ static const void* prepareBuffer(const Port& port, const void* data, std::vector for (std::uint32_t c = 0; c < ch; c++) { const auto srcCh = isStd ? STD_8CH_MAP[c] : c; std::int64_t s = static_cast(src[fr * ch + srcCh]) * - port.volume[c] / DEFAULT_VOLUME; + port.volume[c] * port.mixLevel / (static_cast(DEFAULT_VOLUME) * DEFAULT_VOLUME); s = std::clamp(s, static_cast(std::numeric_limits::min()), static_cast(std::numeric_limits::max())); @@ -325,6 +328,7 @@ int APS5_VABI sceAudioOutOpen(int userId, int type, int index, std::uint32_t len port.format = format; port.channels = channelsForFormat(format); port.lastOutputTime = 0; + port.mixLevel = type == PORT_TYPE_PADSPK ? DEFAULT_PADSPK_MIX_LEVEL : DEFAULT_VOLUME; for (int c = 0; c < port.channels; c++) { port.volume[c] = DEFAULT_VOLUME; } @@ -494,9 +498,23 @@ int APS5_VABI sceAudioOutGetPortState(int handle, AudioOutPortState* state) { return 0; } -int APS5_VABI sceAudioOutSetMixLevelPadSpk(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAudioOutSetMixLevelPadSpk(int handle, int mixLevel) { + std::lock_guard lock(g_mutex); + Port* port = getPort(handle); + if (port == nullptr) { + return -2144993277; + } + if (port->type != PORT_TYPE_PADSPK) { + return -2144993270; + } + if (mixLevel > DEFAULT_VOLUME) { + return -2144993260; + } + if (mixLevel < 0) { + throw std::runtime_error("sceAudioOutSetMixLevelPadSpk: negative mix level not supported"); + } + port->mixLevel = mixLevel; + return 0; } } diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOutMixLevelPadSpk.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOutMixLevelPadSpk.cpp new file mode 100644 index 000000000..6a889cb8b --- /dev/null +++ b/core/libs/prx/libSceAudioOut/tests/AudioOutMixLevelPadSpk.cpp @@ -0,0 +1,183 @@ +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceAudioOutOpen(int, int, int, std::uint32_t, std::uint32_t, std::uint32_t); +int APS5_VABI sceAudioOutClose(int); +int APS5_VABI sceAudioOutOutput(int, const void*); +int APS5_VABI sceAudioOutSetVolume(int, std::uint32_t, int*); +int APS5_VABI sceAudioOutSetMixLevelPadSpk(int, int); +} + +static void Require(bool value) { if (!value) std::abort(); } + +static void SetEnvironment(const char* name, const std::string& value) { +#ifdef _WIN32 + _putenv_s(name, value.c_str()); +#else + setenv(name, value.c_str(), 1); +#endif +} + +template +static bool ThrowsRuntimeError(TFunction function) { + try { + function(); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + +namespace { + +constexpr int user = 0x10000000; +constexpr int portTypeMain = 0; +constexpr int portTypePadSpeaker = 4; +constexpr std::uint32_t formatS16Mono = 0; +constexpr std::uint32_t formatFloatStereo = 4; +constexpr std::uint32_t frames = 256; +constexpr std::uint32_t frequency = 48000; +constexpr int unity = 32768; +constexpr int defaultMixLevel = 11626; +constexpr int invalidPort = static_cast(0x80260003); +constexpr int invalidPortType = static_cast(0x8026000A); +constexpr int invalidMixLevel = static_cast(0x80260014); + +template +std::vector Play(int type, std::uint32_t format, const std::vector& block, TSetup setup) { + const auto path = std::filesystem::temp_directory_path() / "anyps5_audio_out_mix_level_pad_spk.raw"; + std::filesystem::remove(path); + SetEnvironment("SDL_DISKAUDIOFILE", path.string()); + + const int handle = sceAudioOutOpen(user, type, 0, frames, frequency, format); + Require(handle > 0); + setup(handle); + Require(sceAudioOutOutput(handle, block.data()) == static_cast(frames)); + Require(sceAudioOutOutput(handle, nullptr) == static_cast(frames)); + Require(sceAudioOutClose(handle) == 0); + + std::ifstream file(path, std::ios::binary); + const std::vector bytes{std::istreambuf_iterator(file), std::istreambuf_iterator()}; + file.close(); + std::filesystem::remove(path); + Require(bytes.size() >= block.size() * sizeof(TSample) && bytes.size() % sizeof(TSample) == 0); + + const auto* samples = reinterpret_cast(bytes.data()); + std::size_t first = 0; + std::size_t last = bytes.size() / sizeof(TSample); + while (first < last && samples[first] == TSample{}) first++; + while (last > first && samples[last - 1] == TSample{}) last--; + return {samples + first, samples + last}; +} + +std::vector Ramp() { + std::vector block(frames); + for (std::uint32_t frame = 0; frame < frames; frame++) { + block[frame] = static_cast((frame % 2 == 0 ? 1 : -1) * 256 * (static_cast(frame % 100) + 1)); + } + return block; +} + +std::vector Scaled(const std::vector& block, int level) { + std::vector scaled; + for (const std::int16_t sample : block) { + Require(sample * level % unity == 0); + scaled.push_back(static_cast(sample * level / unity)); + } + return scaled; +} + +void TestDefaultLevel() { + std::vector block(frames); + for (std::uint32_t frame = 0; frame < frames; frame++) block[frame] = frame % 2 == 0 ? 16384 : -16384; + const auto played = Play(portTypePadSpeaker, formatS16Mono, block, [](int) {}); + Require(played == Scaled(block, defaultMixLevel)); + Require(played[0] == 5813 && played[1] == -5813); +} + +void TestLevels() { + const auto block = Ramp(); + Require(Play(portTypePadSpeaker, formatS16Mono, block, [](int handle) { + Require(sceAudioOutSetMixLevelPadSpk(handle, unity) == 0); + }) == block); + Require(Play(portTypePadSpeaker, formatS16Mono, block, [](int handle) { + Require(sceAudioOutSetMixLevelPadSpk(handle, unity / 2) == 0); + }) == Scaled(block, unity / 2)); + Require(Play(portTypePadSpeaker, formatS16Mono, block, [](int handle) { + Require(sceAudioOutSetMixLevelPadSpk(handle, unity) == 0); + Require(sceAudioOutSetMixLevelPadSpk(handle, unity / 4) == 0); + }) == Scaled(block, unity / 4)); + Require(Play(portTypePadSpeaker, formatS16Mono, block, [](int handle) { + Require(sceAudioOutSetMixLevelPadSpk(handle, 0) == 0); + }).empty()); +} + +void TestLevelWithVolume() { + const auto block = Ramp(); + Require(Play(portTypePadSpeaker, formatS16Mono, block, [](int handle) { + int volume = unity / 2; + Require(sceAudioOutSetVolume(handle, 1, &volume) == 0); + Require(sceAudioOutSetMixLevelPadSpk(handle, unity / 2) == 0); + }) == Scaled(block, unity / 4)); + + std::vector stereo(frames * 2); + for (std::uint32_t frame = 0; frame < frames; frame++) { + stereo[frame * 2] = (frame % 2 == 0 ? 0.5f : -0.25f); + stereo[frame * 2 + 1] = (frame % 2 == 0 ? -1.0f : 0.125f); + } + const auto played = Play(portTypePadSpeaker, formatFloatStereo, stereo, [](int handle) { + int volume[2] = {unity, unity / 2}; + Require(sceAudioOutSetVolume(handle, 2, volume) == 0); + Require(sceAudioOutSetMixLevelPadSpk(handle, unity / 4) == 0); + }); + Require(played.size() == stereo.size()); + for (std::uint32_t frame = 0; frame < frames; frame++) { + Require(played[frame * 2] == stereo[frame * 2] * 0.25f); + Require(played[frame * 2 + 1] == stereo[frame * 2 + 1] * 0.125f); + } +} + +void TestOtherPortType() { + const auto block = Ramp(); + Require(Play(portTypeMain, formatS16Mono, block, [](int) {}) == block); + Require(Play(portTypeMain, formatS16Mono, block, [](int handle) { + Require(sceAudioOutSetMixLevelPadSpk(handle, unity / 2) == invalidPortType); + Require(sceAudioOutSetMixLevelPadSpk(handle, unity + 1) == invalidPortType); + }) == block); +} + +void TestRejectedLevels() { + const auto block = Ramp(); + int closed = 0; + Require(Play(portTypePadSpeaker, formatS16Mono, block, [&closed](int handle) { + closed = handle; + Require(sceAudioOutSetMixLevelPadSpk(handle, unity / 2) == 0); + Require(sceAudioOutSetMixLevelPadSpk(handle, unity + 1) == invalidMixLevel); + Require(ThrowsRuntimeError([handle] { sceAudioOutSetMixLevelPadSpk(handle, -1); })); + Require(sceAudioOutSetMixLevelPadSpk(0, unity) == invalidPort); + Require(sceAudioOutSetMixLevelPadSpk(handle + 1, unity) == invalidPort); + }) == Scaled(block, unity / 2)); + Require(sceAudioOutSetMixLevelPadSpk(closed, unity) == invalidPort); +} + +} + +int main() { + SetEnvironment("SDL_AUDIODRIVER", "disk"); + TestDefaultLevel(); + TestLevels(); + TestLevelWithVolume(); + TestOtherPortType(); + TestRejectedLevels(); + return 0; +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index a66170e1c..02b161133 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -41,6 +41,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceSslClose](../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - unknown signature - [sceSslGetSerialNumber](../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - unknown signature - [X+4jdIS75P0](../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown name, signature +- [sceAudioOutSetMixLevelPadSpk](../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - effect of a negative mix level unknown; only 0 to 32768 (0 dB) is accepted - [AOWqIYsgVHs](../../core/libs/prx/libSceContentExport/Export.cpp) (libSceContentExport) - unknown name, signature - [GQTObcITIXI](../../core/libs/prx/libSceShare/Export.cpp) (libSceShare) - unknown name, signature - [BnMAMrsfVWo](../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libc) - unknown name, signature From 88320f1297c126c75dfed2d07685a1fb7421a15e Mon Sep 17 00:00:00 2001 From: Pouare514 Date: Sun, 4 Oct 2026 18:23:08 +0200 Subject: [PATCH 08/19] feat(tools): suggest real names for Unknown NID stubs from hash-verified DB --- tools/nid_names.py | 140 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 tools/nid_names.py diff --git a/tools/nid_names.py b/tools/nid_names.py new file mode 100644 index 000000000..73f046dc4 --- /dev/null +++ b/tools/nid_names.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 +"""Suggest real names for Unknown NID stubs from the public hash-verified database. + +Reads every APS5_EXPORT("NID", *Unknown*) in core/, looks the NID up in the +community NID database (zecoxao/sce_symbols aerolib.csv, downloaded once into +a local cache) and prints the verified real name plus what already exists in +the tree. Every suggestion can be re-verified with --verify, which recomputes +each NID with the project's own Nid::Compute algorithm (SHA-1 + salt). + +Usage: + python tools/nid_names.py [--db PATH] [--nid NID ...] [--verify] [--json] +""" +import argparse +import hashlib +import json +import os +import re +import sys +import tempfile +import urllib.request +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +PRX = ROOT / "core" / "libs" / "prx" +DB_URL = ("https://raw.githubusercontent.com/zecoxao/sce_symbols" + "/main/aerolib.csv") +DEFAULT_CACHE = Path(tempfile.gettempdir()) / "anyps5-nid-db" / "aerolib.csv" + +CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-" +SUFFIX = bytes([0x51, 0x8D, 0x64, 0xA6, 0x35, 0xDE, 0xD8, 0xC1, + 0xE6, 0xB0, 0x39, 0xB1, 0xC3, 0xE5, 0x52, 0x30]) +ALIAS = re.compile(r'APS5_EXPORT\("([A-Za-z0-9+\-]{11})",\s*(\w*[Uu]nknown\w*)\)') + + +def compute_nid(symbol): + digest = hashlib.sha1(symbol.encode() + SUFFIX).digest() + rev = bytes(digest[7 - i] for i in range(8)) + out = [] + for i in range(0, 6, 3): + triple = (rev[i] << 16) | (rev[i + 1] << 8) | rev[i + 2] + out += [CHARSET[(triple >> 18) & 63], CHARSET[(triple >> 12) & 63], + CHARSET[(triple >> 6) & 63], CHARSET[triple & 63]] + tail = (rev[6] << 16) | (rev[7] << 8) + out += [CHARSET[(tail >> 18) & 63], CHARSET[(tail >> 12) & 63], + CHARSET[(tail >> 6) & 63]] + return "".join(out) + + +def load_db(path): + if not path.exists(): + path.parent.mkdir(parents=True, exist_ok=True) + print("downloading NID database to %s ..." % path, file=sys.stderr) + urllib.request.urlretrieve(DB_URL, str(path)) + db = {} + with open(path, errors="replace") as handle: + for line in handle: + parts = line.strip().split() + if len(parts) >= 2: + db.setdefault(parts[0], parts[1]) + return db + + +def collect_unknowns(): + found = {} + for path in sorted(PRX.rglob("*.cpp")): + try: + text = path.read_text(errors="replace") + except OSError: + continue + for match in ALIAS.finditer(text): + found.setdefault(match.group(1), "%s:%s" % ( + path.relative_to(ROOT), match.group(2))) + return found + + +def collect_real_names(): + names = set() + for path in sorted((ROOT / "core" / "libs").rglob("*.cpp")): + try: + text = path.read_text(errors="replace") + except OSError: + continue + for match in re.finditer(r"\b([A-Za-z_]\w*?)_nid_postfix\b", text): + names.add(match.group(1)) + return names + + +def main(): + parser = argparse.ArgumentParser( + description="Suggest real names for Unknown NID stubs") + parser.add_argument("--db", default=os.environ.get("ANYPS5_NID_DB"), + help="NID database CSV (downloaded to a cache dir by default)") + parser.add_argument("--nid", nargs="*", default=[], + help="resolve only these NIDs instead of scanning the tree") + parser.add_argument("--verify", action="store_true", + help="recompute every suggested NID and fail on mismatch") + parser.add_argument("--json", action="store_true", + help="emit machine-readable JSON") + args = parser.parse_args() + + db_path = Path(args.db) if args.db else DEFAULT_CACHE + db = load_db(db_path) + + if args.nid: + unknowns = {nid: "" for nid in args.nid} + else: + unknowns = collect_unknowns() + real_names = collect_real_names() + + rows = [] + failures = 0 + for nid in sorted(unknowns): + suggestion = db.get(nid, "") + status = "unknown" + if suggestion: + if args.verify and compute_nid(suggestion) != nid: + status = "MISMATCH" + failures += 1 + elif suggestion in real_names: + status = "already-implemented" + else: + status = "rename-ready" + rows.append({"nid": nid, "stub": unknowns[nid], + "suggestion": suggestion, "status": status}) + + if args.json: + print(json.dumps({"db_entries": len(db), "rows": rows}, indent=1)) + else: + print("%-12s %-40s %-12s %s" % ("NID", "STUB", "STATUS", "SUGGESTION")) + for row in rows: + print("%-12s %-40s %-12s %s" % ( + row["nid"], row["stub"][:40], row["status"], row["suggestion"])) + print("%d unknowns, %d suggestions (%d already implemented)" % ( + len(rows), sum(1 for r in rows if r["suggestion"]), + sum(1 for r in rows if r["status"] == "already-implemented"))) + return 1 if failures else 0 + + +if __name__ == "__main__": + sys.exit(main()) From 7854fb6bff27bec5317106c45583ad795794709c Mon Sep 17 00:00:00 2001 From: Phi1ow <331962174+Phi1ow@users.noreply.github.com> Date: Sun, 4 Oct 2026 22:33:43 +0200 Subject: [PATCH 09/19] feat(errordialog): implement the libSceErrorDialog state machine The six libSceErrorDialog exports were throwing stubs. They now implement the dialog's NONE -> INITIALIZED -> RUNNING -> FINISHED state machine without a visible window, so a title that opens an error dialog and waits for it never blocks: - sceErrorDialogInitialize -> INITIALIZED, or 0x80ED0002 if already initialized - sceErrorDialogOpen -> checks the state and the 16-byte parameter, then RUNNING - sceErrorDialogUpdateStatus -> turns RUNNING into FINISHED and returns the status - sceErrorDialogGetStatus -> returns the status - sceErrorDialogClose -> FINISHED, or 0x80ED0005 when no dialog is running - sceErrorDialogTerminate -> NONE, or 0x80ED0001 when not initialized Re-opening is allowed once a previous dialog has finished. The status values and error codes are the documented SCE error-dialog ones. Adds a CMakeLists.txt so the library can carry its guest test. New test core/libs/tests/GuestErrorDialog.cpp drives the whole state machine (the not-initialized and already-initialized errors, the state and parameter checks of open, open/update/close and re-open after finish). It fails on main (the stubs throw) and passes with the change. --- .../libs/prx/libSceErrorDialog/CMakeLists.txt | 20 ++++ core/libs/prx/libSceErrorDialog/Export.cpp | 94 ++++++++++++++----- core/libs/tests/GuestErrorDialog.cpp | 63 +++++++++++++ 3 files changed, 155 insertions(+), 22 deletions(-) create mode 100644 core/libs/prx/libSceErrorDialog/CMakeLists.txt create mode 100644 core/libs/tests/GuestErrorDialog.cpp diff --git a/core/libs/prx/libSceErrorDialog/CMakeLists.txt b/core/libs/prx/libSceErrorDialog/CMakeLists.txt new file mode 100644 index 000000000..2dd29a15a --- /dev/null +++ b/core/libs/prx/libSceErrorDialog/CMakeLists.txt @@ -0,0 +1,20 @@ +add_library(libSceErrorDialog SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceErrorDialog PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceErrorDialog) + +if(MINGW) + foreach(runtimeLinkFlag IN ITEMS -static-libgcc -static-libstdc++ -static) + string(REPLACE "${runtimeLinkFlag}" "" CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}") + endforeach() +endif() + +if(BUILD_TESTING) + add_test_executable(guest_error_dialog_tests ${LIBS_INCLUDE_DIR}/tests/GuestErrorDialog.cpp) + target_include_directories(guest_error_dialog_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_error_dialog_tests PRIVATE libSceErrorDialog) + add_test(NAME guest_error_dialog COMMAND guest_error_dialog_tests) + if(WIN32) + set_tests_properties(guest_error_dialog PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceErrorDialog/Export.cpp b/core/libs/prx/libSceErrorDialog/Export.cpp index 7588126f3..46b7777ae 100644 --- a/core/libs/prx/libSceErrorDialog/Export.cpp +++ b/core/libs/prx/libSceErrorDialog/Export.cpp @@ -1,39 +1,89 @@ #include #include +#include +#include + #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#ifdef _WIN32 +#include +#endif + +namespace { +constexpr int kStatusNone = 0; +constexpr int kStatusInitialized = 1; +constexpr int kStatusRunning = 2; +constexpr int kStatusFinished = 3; + +constexpr int kErrOk = 0; +constexpr int kErrNotInitialized = static_cast(0x80ED0001u); +constexpr int kErrAlreadyInitialized = static_cast(0x80ED0002u); +constexpr int kErrParam = static_cast(0x80ED0003u); +constexpr int kErrInvalidState = static_cast(0x80ED0005u); + +constexpr std::size_t kParamSize = 16; + +std::mutex g_lock; +int g_status = kStatusNone; + +bool Readable(const void* p, std::size_t n) { +#ifdef _WIN32 + if (p == nullptr) return false; + MEMORY_BASIC_INFORMATION mbi; + if (VirtualQuery(p, &mbi, sizeof(mbi)) == 0 || mbi.State != MEM_COMMIT) return false; + if ((mbi.Protect & (PAGE_NOACCESS | PAGE_GUARD)) != 0) return false; + const auto end = reinterpret_cast(mbi.BaseAddress) + mbi.RegionSize; + return reinterpret_cast(p) + n <= end; +#else + return p != nullptr; +#endif +} +} // namespace + extern "C" { -int APS5_VABI sceErrorDialogClose(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceErrorDialogGetStatus(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceErrorDialogInitialize(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + std::lock_guard g(g_lock); + if (g_status != kStatusNone) return kErrAlreadyInitialized; + g_status = kStatusInitialized; + return kErrOk; } int APS5_VABI sceErrorDialogOpen(const void* param) { - (void)param; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceErrorDialogTerminate(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + std::lock_guard g(g_lock); + if (g_status != kStatusInitialized && g_status != kStatusFinished) return kErrInvalidState; + if (!Readable(param, kParamSize)) return kErrParam; + std::int32_t size = 0; + std::memcpy(&size, param, sizeof(size)); + if (static_cast(size) != kParamSize) return kErrParam; + g_status = kStatusRunning; + return kErrOk; } int APS5_VABI sceErrorDialogUpdateStatus(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + std::lock_guard g(g_lock); + if (g_status == kStatusRunning) g_status = kStatusFinished; + return g_status; +} + +int APS5_VABI sceErrorDialogGetStatus(void) { + std::lock_guard g(g_lock); + return g_status; +} + +int APS5_VABI sceErrorDialogClose(void) { + std::lock_guard g(g_lock); + if (g_status != kStatusRunning) return kErrInvalidState; + g_status = kStatusFinished; + return kErrOk; +} + +int APS5_VABI sceErrorDialogTerminate(void) { + std::lock_guard g(g_lock); + if (g_status == kStatusNone) return kErrNotInitialized; + g_status = kStatusNone; + return kErrOk; } } diff --git a/core/libs/tests/GuestErrorDialog.cpp b/core/libs/tests/GuestErrorDialog.cpp new file mode 100644 index 000000000..cb91dc4fb --- /dev/null +++ b/core/libs/tests/GuestErrorDialog.cpp @@ -0,0 +1,63 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include + +extern "C" { +int APS5_VABI sceErrorDialogInitialize(void); +int APS5_VABI sceErrorDialogOpen(const void* param); +int APS5_VABI sceErrorDialogUpdateStatus(void); +int APS5_VABI sceErrorDialogGetStatus(void); +int APS5_VABI sceErrorDialogClose(void); +int APS5_VABI sceErrorDialogTerminate(void); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +constexpr int kStatusRunning = 2; +constexpr int kStatusFinished = 3; +constexpr int kErrNotInitialized = static_cast(0x80ED0001); +constexpr int kErrAlreadyInitialized = static_cast(0x80ED0002); +constexpr int kErrParam = static_cast(0x80ED0003); +constexpr int kErrInvalidState = static_cast(0x80ED0005); + +struct ErrorDialogParam { + std::int32_t size; + std::int32_t error_code; + std::int32_t user_id; + std::int32_t reserved; +}; + +} + +int main() { + ErrorDialogParam param{16, static_cast(0x80020001), 0, 0}; + + Require(sceErrorDialogTerminate() == kErrNotInitialized); + Require(sceErrorDialogOpen(¶m) == kErrInvalidState); + + Require(sceErrorDialogInitialize() == 0); + Require(sceErrorDialogInitialize() == kErrAlreadyInitialized); + + Require(sceErrorDialogClose() == kErrInvalidState); + + Require(sceErrorDialogOpen(nullptr) == kErrParam); + ErrorDialogParam bad{8, 0, 0, 0}; + Require(sceErrorDialogOpen(&bad) == kErrParam); + + Require(sceErrorDialogOpen(¶m) == 0); + Require(sceErrorDialogGetStatus() == kStatusRunning); + Require(sceErrorDialogOpen(¶m) == kErrInvalidState); + + Require(sceErrorDialogUpdateStatus() == kStatusFinished); + Require(sceErrorDialogGetStatus() == kStatusFinished); + + Require(sceErrorDialogOpen(¶m) == 0); + Require(sceErrorDialogGetStatus() == kStatusRunning); + Require(sceErrorDialogClose() == 0); + Require(sceErrorDialogGetStatus() == kStatusFinished); + + Require(sceErrorDialogTerminate() == 0); + Require(sceErrorDialogTerminate() == kErrNotInitialized); +} From 1cd34c232d4f44cf0ca92a3d0471353377725e7d Mon Sep 17 00:00:00 2001 From: Phi1ow <331962174+Phi1ow@users.noreply.github.com> Date: Sun, 4 Oct 2026 23:19:37 +0200 Subject: [PATCH 10/19] feat(pad): implement scePadSetAngularVelocityDeadbandState scePadSetAngularVelocityDeadbandState was the last throwing stub in libScePad. It toggles a dead-band filter on the controller's angular-velocity (gyro) readings; the reading path does not apply that optional filter, so the call succeeds and does nothing, as it does in shadPS4. Extends the existing guest_pad test with the two enable/disable calls. It fails on main (the stub throws) and passes with the change. --- core/libs/prx/libScePad/Export.cpp | 1 - core/libs/tests/GuestPad.cpp | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index 80cd10216..e0a70c6f8 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -173,7 +173,6 @@ int APS5_VABI scePadResetOrientation(int handle) { int APS5_VABI scePadSetAngularVelocityDeadbandState(int handle, bool enable) { (void)handle; (void)enable; - NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/tests/GuestPad.cpp b/core/libs/tests/GuestPad.cpp index 70a193bb2..20aacc302 100644 --- a/core/libs/tests/GuestPad.cpp +++ b/core/libs/tests/GuestPad.cpp @@ -13,6 +13,7 @@ int APS5_VABI scePadSetVibrationTriggerEffectWeakWhileEmbeddedMicInUse(bool); int APS5_VABI scePadInit_nid_postfix(void); int APS5_VABI scePadSetTiltCorrectionState(int, bool); int APS5_VABI scePadResetOrientation(int); +int APS5_VABI scePadSetAngularVelocityDeadbandState(int, bool); } static void Require(bool value) { if (!value) std::abort(); } @@ -60,4 +61,6 @@ int main() { Require(scePadClose_nid_postfix(handle) == 0); Require(scePadGetHandle(user, 0, 0) == noHandle); Require(scePadSetVibrationTriggerEffectWeakWhileEmbeddedMicInUse(true) == 0); + Require(scePadSetAngularVelocityDeadbandState(handle, true) == 0); + Require(scePadSetAngularVelocityDeadbandState(handle, false) == 0); } From 6571ace5bda2a092c7ce38bf1eaebd1fb132ab63 Mon Sep 17 00:00:00 2001 From: Phi1ow <331962174+Phi1ow@users.noreply.github.com> Date: Mon, 5 Oct 2026 00:32:13 +0200 Subject: [PATCH 11/19] feat(errordialog): log the error code and record the silent stub Adds the dialog to the silent-stubs list in docs/dev/TechnicalDebt.md, since it finishes without showing anything. sceErrorDialogOpen now logs the error code and user id from the parameter (offsets 4 and 8), like libSceMsgDialog logs its message, so an error a title would have shown the player stays visible instead of disappearing. --- core/libs/prx/libSceErrorDialog/Export.cpp | 6 ++++++ docs/dev/TechnicalDebt.md | 1 + 2 files changed, 7 insertions(+) diff --git a/core/libs/prx/libSceErrorDialog/Export.cpp b/core/libs/prx/libSceErrorDialog/Export.cpp index 46b7777ae..17d76ba8e 100644 --- a/core/libs/prx/libSceErrorDialog/Export.cpp +++ b/core/libs/prx/libSceErrorDialog/Export.cpp @@ -4,6 +4,7 @@ #include #include "SceTypes.hpp" +#include "HitLog.hpp" #include "prx/libc/include/General.hpp" #ifdef _WIN32 @@ -57,6 +58,11 @@ int APS5_VABI sceErrorDialogOpen(const void* param) { std::int32_t size = 0; std::memcpy(&size, param, sizeof(size)); if (static_cast(size) != kParamSize) return kErrParam; + std::int32_t errorCode = 0; + std::int32_t userId = 0; + std::memcpy(&errorCode, static_cast(param) + 4, sizeof(errorCode)); + std::memcpy(&userId, static_cast(param) + 8, sizeof(userId)); + APS5_HIT("ERRORDIALOG", "open: error_code=0x%08X user_id=%d", errorCode, userId); g_status = kStatusRunning; return kErrOk; } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index a66170e1c..b3c9bf731 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -13,6 +13,7 @@ Throughout the project, every function at every stage either **does exactly what it's supposed to or throws an exception**. Everywhere... except: - [libSceSaveDataDialog.native](../../core/libs/prx/libSceSaveDataDialog.native/Export.cpp) - [libSceCommonDialog](../../core/libs/prx/libSceCommonDialog/Export.cpp) +- [libSceErrorDialog](../../core/libs/prx/libSceErrorDialog/Export.cpp) - the error dialog runs the state machine but shows nothing; the error code passed to `sceErrorDialogOpen` is only logged - [libSceHmd](../../core/libs/prx/libSceHmd/Export.cpp) implements only the disconnected-headset path: initialization succeeds, device queries report `NotDetected`, and opening a device returns `DeviceDisconnected`. Headset support, tracking, rendering and additional HMD exports are not implemented; SDK-level ABI compatibility and in-game behaviour remain unverified. - [libSceNpCommerce](../../core/libs/prx/libSceNpCommerce/Export.cpp) - the PS Store icon show/hide calls do nothing - [sceVideoOutOpen](../../core/libs/prx/libSceVideoOut/src/Output.cpp) validates the priority and CPU affinity that the open param requests for the VideoOut service thread but does not apply them: the port's present and vblank threads are host threads, and guest priorities and affinities do not reach host scheduling From e4bc4a4ecbfa23dc2172e337ad01defe51fb4b62 Mon Sep 17 00:00:00 2001 From: Phi1ow <331962174+Phi1ow@users.noreply.github.com> Date: Mon, 5 Oct 2026 00:48:45 +0200 Subject: [PATCH 12/19] fix(pad): check the handle and keep the dead band throwing when enabled Per review: scePadSetAngularVelocityDeadbandState now returns PAD_ERROR_INVALID_HANDLE for a handle other than PAD_HANDLE, like the neighbouring setters. Enabling the dead band still throws, because the angular-velocity reading path does not apply the filter (that would change input, not UI, so it is not a silent stub); disabling it succeeds, which matches the unfiltered readings. The test checks the disabled call, the bad handle, and no longer asserts the enabled path. --- core/libs/prx/libScePad/Export.cpp | 6 +++--- core/libs/tests/GuestPad.cpp | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index e0a70c6f8..f2437a5a5 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -171,9 +171,9 @@ int APS5_VABI scePadResetOrientation(int handle) { } int APS5_VABI scePadSetAngularVelocityDeadbandState(int handle, bool enable) { - (void)handle; - (void)enable; - return 0; + if (handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE; + if (enable) NotImplemented_nid_no_patch(__func__); + return PAD_OK; } int APS5_VABI scePadSetLightBar(int handle, const PadLightBarParam* param) { diff --git a/core/libs/tests/GuestPad.cpp b/core/libs/tests/GuestPad.cpp index 20aacc302..8abd4f04f 100644 --- a/core/libs/tests/GuestPad.cpp +++ b/core/libs/tests/GuestPad.cpp @@ -61,6 +61,6 @@ int main() { Require(scePadClose_nid_postfix(handle) == 0); Require(scePadGetHandle(user, 0, 0) == noHandle); Require(scePadSetVibrationTriggerEffectWeakWhileEmbeddedMicInUse(true) == 0); - Require(scePadSetAngularVelocityDeadbandState(handle, true) == 0); Require(scePadSetAngularVelocityDeadbandState(handle, false) == 0); + Require(scePadSetAngularVelocityDeadbandState(handle + 1, false) == PAD_ERROR_INVALID_HANDLE); } From 630cf0cf391dd832522ec4895135d12f9a865b58 Mon Sep 17 00:00:00 2001 From: Pouare514 Date: Mon, 5 Oct 2026 00:53:03 +0200 Subject: [PATCH 13/19] fix(libc): drop duplicate quick_exit stub clashing with the real export --- core/libs/prx/libc/Export.cpp | 6 ------ 1 file changed, 6 deletions(-) diff --git a/core/libs/prx/libc/Export.cpp b/core/libs/prx/libc/Export.cpp index 6df7bf8db..17128de2e 100644 --- a/core/libs/prx/libc/Export.cpp +++ b/core/libs/prx/libc/Export.cpp @@ -160,12 +160,6 @@ std::uint64_t APS5_VABI libcCyberUnknown14(void) { return 0; } -APS5_EXPORT("qdGFBoLVNKI", libcCyberUnknown15); -std::uint64_t APS5_VABI libcCyberUnknown15(void) { - NotImplemented_nid_no_patch("qdGFBoLVNKI"); - return 0; -} - APS5_EXPORT("vEaqE-7IZYc", libcCyberUnknown16); std::uint64_t APS5_VABI libcCyberUnknown16(void) { NotImplemented_nid_no_patch("vEaqE-7IZYc"); From 9a652bf82cd748b73d1cc03cf3eba5a94b784aef Mon Sep 17 00:00:00 2001 From: DotDebian Date: Mon, 5 Oct 2026 00:55:11 +0200 Subject: [PATCH 14/19] docs(libSceAudioOut): record the pad speaker mix level as an assumed gain Co-Authored-By: Claude Opus 5.5 --- docs/dev/TechnicalDebt.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 02b161133..580c494cc 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -42,6 +42,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceSslGetSerialNumber](../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - unknown signature - [X+4jdIS75P0](../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown name, signature - [sceAudioOutSetMixLevelPadSpk](../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - effect of a negative mix level unknown; only 0 to 32768 (0 dB) is accepted +- [sceAudioOutSetMixLevelPadSpk](../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - no source applies the level (shadPS4 only stores it); it is assumed to be a linear gain on the port's own output, and the -9 dB default (11626) is applied to every pad speaker port, including those of titles that never call the function - [AOWqIYsgVHs](../../core/libs/prx/libSceContentExport/Export.cpp) (libSceContentExport) - unknown name, signature - [GQTObcITIXI](../../core/libs/prx/libSceShare/Export.cpp) (libSceShare) - unknown name, signature - [BnMAMrsfVWo](../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libc) - unknown name, signature From 0b5fe3c23c101288e71b3eb492fabb836c51c288 Mon Sep 17 00:00:00 2001 From: Pouare514 Date: Mon, 5 Oct 2026 00:56:11 +0200 Subject: [PATCH 15/19] fix(relinker): detect FS-prefixed previous instruction via SegmentPrefix decode --- core/libs/prx/libSceAudioIn/Export.cpp | 2 +- .../relinker/src/analysis/CodeInstructionCollector.cpp | 8 +------- .../relinker/relinker/tests/test_tls_function_coverage.py | 4 ++++ 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceAudioIn/Export.cpp b/core/libs/prx/libSceAudioIn/Export.cpp index 3c31aaa40..cdd232455 100644 --- a/core/libs/prx/libSceAudioIn/Export.cpp +++ b/core/libs/prx/libSceAudioIn/Export.cpp @@ -122,7 +122,7 @@ int APS5_VABI sceAudioInInput(int handle, void* dest) { if (device != 0) { captured = capture(device, out, bytes, deadline); } else { - std::this_thread::sleep_until(wake); + while (Clock::now() < wake) std::this_thread::sleep_until(wake); } std::memset(out + captured, 0, bytes - captured); lock.lock(); diff --git a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp index 27b580af7..a2ba2e2f2 100644 --- a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp +++ b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp @@ -267,13 +267,7 @@ std::set CodeInstructionCollector::Collect(const std::ve for (const auto& header : headers) { if (header.Type != 1 || (header.Flags & 1) == 0 || previousStart < header.MappedAddress || previousStart - header.MappedAddress >= header.FileSize) continue; const auto base = header.Offset + previousStart - header.MappedAddress; - const auto prefixEnd = std::min({base + 4, base + (previousEnd - previousStart), bytes.size()}); - for (auto probe = base; probe < prefixEnd; ++probe) { - if (bytes[probe] == 0x64) { - spanHasFsPrefix = true; - break; - } - } + if (decoder.DecodeInstruction(bytes.data() + base, bytes.size() - base).SegmentPrefix == 0x64) spanHasFsPrefix = true; break; } } diff --git a/core/relinker/relinker/tests/test_tls_function_coverage.py b/core/relinker/relinker/tests/test_tls_function_coverage.py index a16497c73..825e7f543 100644 --- a/core/relinker/relinker/tests/test_tls_function_coverage.py +++ b/core/relinker/relinker/tests/test_tls_function_coverage.py @@ -239,6 +239,10 @@ def main(): fs_overlap[0x1209:0x1211] = bytes.fromhex("48 8b 04 25 64 00 00 00") fs_overlap[0x1211:0x1216] = b"\xe8" + struct.pack(" Date: Mon, 5 Oct 2026 00:57:04 +0200 Subject: [PATCH 16/19] fix(crashreport): capture thread stacks while suspended, report skipped threads --- core/libs/prx/libSceAudioIn/Export.cpp | 2 +- .../src/specifics/windows/CrashReport.cpp | 43 +++++++++++++------ 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/core/libs/prx/libSceAudioIn/Export.cpp b/core/libs/prx/libSceAudioIn/Export.cpp index 3c31aaa40..cdd232455 100644 --- a/core/libs/prx/libSceAudioIn/Export.cpp +++ b/core/libs/prx/libSceAudioIn/Export.cpp @@ -122,7 +122,7 @@ int APS5_VABI sceAudioInInput(int handle, void* dest) { if (device != 0) { captured = capture(device, out, bytes, deadline); } else { - std::this_thread::sleep_until(wake); + while (Clock::now() < wake) std::this_thread::sleep_until(wake); } std::memset(out + captured, 0, bytes - captured); lock.lock(); diff --git a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp index 3c7121db4..2e06061f3 100644 --- a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp +++ b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp @@ -339,20 +339,28 @@ LONG WINAPI ReportCrash(EXCEPTION_POINTERS* info) { return EXCEPTION_CONTINUE_SEARCH; } -void ReportThreadContext(const CONTEXT* context, DWORD threadId, const char* name) { +std::size_t CaptureFrames(const CONTEXT* context, std::uint64_t* frames, std::size_t capacity) { + std::size_t count = 0; + std::uint64_t frame = context->Rbp; + while (count < capacity && frame != 0 && (frame & 7) == 0 && IsReadable(frame) && IsReadable(frame + 8)) { + const auto returnAddress = reinterpret_cast(frame)[1]; + if (!IsExecutable(returnAddress)) break; + frames[count++] = returnAddress; + const auto next = reinterpret_cast(frame)[0]; + if (next <= frame) break; + frame = next; + } + return count; +} + +void ReportThreadContext(const CONTEXT* context, DWORD threadId, const char* name, const std::uint64_t* frames, std::size_t frameCount) { char line[MAX_PATH + 64]; Report(" thread %lu '%s': rip ", static_cast(threadId), name ? name : ""); DescribeAddress(context->Rip, line, sizeof(line)); Report("%s rsp 0x%016llx rbp 0x%016llx\n", line, static_cast(context->Rsp), static_cast(context->Rbp)); - std::uint64_t frame = context->Rbp; - for (int depth = 0; depth < 12 && frame != 0 && (frame & 7) == 0 && IsReadable(frame) && IsReadable(frame + 8); ++depth) { - const auto returnAddress = reinterpret_cast(frame)[1]; - if (!IsExecutable(returnAddress)) break; - DescribeAddress(returnAddress, line, sizeof(line)); - Report(" #%d %s\n", depth, line); - const auto next = reinterpret_cast(frame)[0]; - if (next <= frame) break; - frame = next; + for (std::size_t depth = 0; depth < frameCount; ++depth) { + DescribeAddress(frames[depth], line, sizeof(line)); + Report(" #%llu %s\n", static_cast(depth), line); } } @@ -367,13 +375,19 @@ void ReportAllThreads() { DWORD id; char name[128]; CONTEXT context; + std::uint64_t frames[12]; + std::size_t frameCount; }; static CapturedThread captured[128]; std::size_t capturedCount = 0; + std::size_t skippedCount = 0; if (Thread32First(snapshot, &entry)) { do { if (entry.th32OwnerProcessID != pid || entry.th32ThreadID == current) continue; - if (capturedCount >= sizeof(captured) / sizeof(captured[0])) break; + if (capturedCount >= sizeof(captured) / sizeof(captured[0])) { + ++skippedCount; + continue; + } HANDLE thread = OpenThread(THREAD_SUSPEND_RESUME | THREAD_GET_CONTEXT | THREAD_QUERY_INFORMATION, FALSE, entry.th32ThreadID); if (!thread) continue; if (SuspendThread(thread) == static_cast(-1)) { @@ -383,6 +397,8 @@ void ReportAllThreads() { CONTEXT context{}; context.ContextFlags = CONTEXT_FULL; const BOOL haveContext = GetThreadContext(thread, &context); + std::uint64_t frames[12] = {}; + const std::size_t frameCount = haveContext ? CaptureFrames(&context, frames, sizeof(frames) / sizeof(frames[0])) : 0; ResumeThread(thread); char name[128] = ""; PWSTR description = nullptr; @@ -395,14 +411,17 @@ void ReportAllThreads() { captured[capturedCount].id = entry.th32ThreadID; std::memcpy(captured[capturedCount].name, name, sizeof(captured[capturedCount].name)); captured[capturedCount].context = context; + std::memcpy(captured[capturedCount].frames, frames, sizeof(captured[capturedCount].frames)); + captured[capturedCount].frameCount = frameCount; ++capturedCount; } while (Thread32Next(snapshot, &entry)); } CloseHandle(snapshot); Report(" all threads:\n"); for (std::size_t i = 0; i < capturedCount; ++i) { - ReportThreadContext(&captured[i].context, captured[i].id, captured[i].name); + ReportThreadContext(&captured[i].context, captured[i].id, captured[i].name, captured[i].frames, captured[i].frameCount); } + if (skippedCount != 0) Report(" %llu further thread(s) left out\n", static_cast(skippedCount)); } // abort() and the UCRT's invalid-parameter path end the process with a silent fast fail From 66f0e7b0eb97d119048cc27a492f4ae7601f21ff Mon Sep 17 00:00:00 2001 From: Pouare514 Date: Mon, 5 Oct 2026 00:58:10 +0200 Subject: [PATCH 17/19] fix(tools): always verify NIDs, collect all definitions, pin DB commit --- tools/nid_names.py | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/tools/nid_names.py b/tools/nid_names.py index 73f046dc4..9697adbcd 100644 --- a/tools/nid_names.py +++ b/tools/nid_names.py @@ -1,15 +1,4 @@ #!/usr/bin/env python3 -"""Suggest real names for Unknown NID stubs from the public hash-verified database. - -Reads every APS5_EXPORT("NID", *Unknown*) in core/, looks the NID up in the -community NID database (zecoxao/sce_symbols aerolib.csv, downloaded once into -a local cache) and prints the verified real name plus what already exists in -the tree. Every suggestion can be re-verified with --verify, which recomputes -each NID with the project's own Nid::Compute algorithm (SHA-1 + salt). - -Usage: - python tools/nid_names.py [--db PATH] [--nid NID ...] [--verify] [--json] -""" import argparse import hashlib import json @@ -23,13 +12,15 @@ from pathlib import Path ROOT = Path(__file__).resolve().parent.parent PRX = ROOT / "core" / "libs" / "prx" DB_URL = ("https://raw.githubusercontent.com/zecoxao/sce_symbols" - "/main/aerolib.csv") + "/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv") DEFAULT_CACHE = Path(tempfile.gettempdir()) / "anyps5-nid-db" / "aerolib.csv" CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-" SUFFIX = bytes([0x51, 0x8D, 0x64, 0xA6, 0x35, 0xDE, 0xD8, 0xC1, 0xE6, 0xB0, 0x39, 0xB1, 0xC3, 0xE5, 0x52, 0x30]) ALIAS = re.compile(r'APS5_EXPORT\("([A-Za-z0-9+\-]{11})",\s*(\w*[Uu]nknown\w*)\)') +DEFINITION = re.compile(r"\bAPS5_VABI\s+(\w+)\s*\([^;{]*\)\s*(?:noexcept\s*)?\{") +NID_POSTFIX = "_nid_postfix" def compute_nid(symbol): @@ -80,8 +71,11 @@ def collect_real_names(): text = path.read_text(errors="replace") except OSError: continue - for match in re.finditer(r"\b([A-Za-z_]\w*?)_nid_postfix\b", text): - names.add(match.group(1)) + for match in DEFINITION.finditer(text): + name = match.group(1) + names.add(name) + if name.endswith(NID_POSTFIX): + names.add(name[:-len(NID_POSTFIX)]) return names @@ -92,8 +86,6 @@ def main(): help="NID database CSV (downloaded to a cache dir by default)") parser.add_argument("--nid", nargs="*", default=[], help="resolve only these NIDs instead of scanning the tree") - parser.add_argument("--verify", action="store_true", - help="recompute every suggested NID and fail on mismatch") parser.add_argument("--json", action="store_true", help="emit machine-readable JSON") args = parser.parse_args() @@ -113,7 +105,7 @@ def main(): suggestion = db.get(nid, "") status = "unknown" if suggestion: - if args.verify and compute_nid(suggestion) != nid: + if compute_nid(suggestion) != nid: status = "MISMATCH" failures += 1 elif suggestion in real_names: From e67d50cb2fc6d9485a65e542422218b812e9759d Mon Sep 17 00:00:00 2001 From: Pouare514 Date: Mon, 5 Oct 2026 01:10:25 +0200 Subject: [PATCH 18/19] fix(libs): route DateTime zone suffix through RFC3339 path, throw on unverified shapes --- core/libs/prx/libSceAudioIn/Export.cpp | 2 +- core/libs/prx/libSceRtc/CMakeLists.txt | 3 +- core/libs/prx/libSceRtc/Export.cpp | 55 ++++++++++++++++---------- core/libs/tests/GuestRtc.cpp | 26 ++++++++++++ docs/dev/TechnicalDebt.md | 2 +- 5 files changed, 64 insertions(+), 24 deletions(-) diff --git a/core/libs/prx/libSceAudioIn/Export.cpp b/core/libs/prx/libSceAudioIn/Export.cpp index 3c31aaa40..cdd232455 100644 --- a/core/libs/prx/libSceAudioIn/Export.cpp +++ b/core/libs/prx/libSceAudioIn/Export.cpp @@ -122,7 +122,7 @@ int APS5_VABI sceAudioInInput(int handle, void* dest) { if (device != 0) { captured = capture(device, out, bytes, deadline); } else { - std::this_thread::sleep_until(wake); + while (Clock::now() < wake) std::this_thread::sleep_until(wake); } std::memset(out + captured, 0, bytes - captured); lock.lock(); diff --git a/core/libs/prx/libSceRtc/CMakeLists.txt b/core/libs/prx/libSceRtc/CMakeLists.txt index a0e4a2444..faf3100ac 100644 --- a/core/libs/prx/libSceRtc/CMakeLists.txt +++ b/core/libs/prx/libSceRtc/CMakeLists.txt @@ -11,7 +11,8 @@ endif() if(BUILD_TESTING) add_test_executable(guest_rtc_tests ${LIBS_INCLUDE_DIR}/tests/GuestRtc.cpp) target_include_directories(guest_rtc_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_rtc_tests PRIVATE libSceRtc) + target_link_libraries(guest_rtc_tests PRIVATE libSceRtc libc) + configure_windows_unwind(guest_rtc_tests) add_test(NAME guest_rtc COMMAND guest_rtc_tests) if(WIN32) set_tests_properties(guest_rtc PROPERTIES ENVIRONMENT_MODIFICATION diff --git a/core/libs/prx/libSceRtc/Export.cpp b/core/libs/prx/libSceRtc/Export.cpp index 8ec554311..272b78cb3 100644 --- a/core/libs/prx/libSceRtc/Export.cpp +++ b/core/libs/prx/libSceRtc/Export.cpp @@ -139,6 +139,23 @@ bool parseDigits(const char*& cursor, int count, int& value) { return true; } +bool parseZoneSuffix(const char*& cursor, std::int64_t& offsetMinutes) { + offsetMinutes = 0; + if (*cursor == 'Z' || *cursor == 'z') { + ++cursor; + return true; + } + if (*cursor == '+' || *cursor == '-') { + const int sign = *cursor++ == '-' ? -1 : 1; + int offsetHours = 0, offsetMinute = 0; + if (!parseDigits(cursor, 2, offsetHours) || *cursor++ != ':' || !parseDigits(cursor, 2, offsetMinute)) return false; + if (offsetHours > 23 || offsetMinute > 59) return false; + offsetMinutes = sign * (offsetHours * 60 + offsetMinute); + return true; + } + return false; +} + } extern "C" { @@ -282,17 +299,7 @@ int APS5_VABI sceRtcParseRFC3339(RtcTick* utc, const char* date_time) { for (; *cursor >= '0' && *cursor <= '9'; ++cursor, scale /= 10) microsecond += static_cast(*cursor - '0') * scale; } std::int64_t offsetMinutes = 0; - if (*cursor == 'Z' || *cursor == 'z') { - ++cursor; - } else if (*cursor == '+' || *cursor == '-') { - const int sign = *cursor++ == '-' ? -1 : 1; - int offsetHours = 0, offsetMinute = 0; - if (!parseDigits(cursor, 2, offsetHours) || *cursor++ != ':' || !parseDigits(cursor, 2, offsetMinute)) return SCE_RTC_ERROR_BAD_PARSE; - if (offsetHours > 23 || offsetMinute > 59) return SCE_RTC_ERROR_BAD_PARSE; - offsetMinutes = sign * (offsetHours * 60 + offsetMinute); - } else { - return SCE_RTC_ERROR_BAD_PARSE; - } + if (!parseZoneSuffix(cursor, offsetMinutes)) return SCE_RTC_ERROR_BAD_PARSE; if (*cursor != 0) return SCE_RTC_ERROR_BAD_PARSE; const RtcDateTime time{static_cast(year), static_cast(month), static_cast(day), static_cast(hour), static_cast(minute), static_cast(second), microsecond}; @@ -305,23 +312,29 @@ int APS5_VABI sceRtcParseDateTime(RtcTick* utc, const char* date_time) { if (!utc || !date_time) return SCE_RTC_ERROR_INVALID_POINTER; const char* cursor = date_time; int year = 0, month = 0, day = 0, hour = 0, minute = 0, second = 0; - if (!parseDigits(cursor, 4, year) || *cursor++ != '-' || !parseDigits(cursor, 2, month) || *cursor++ != '-' || !parseDigits(cursor, 2, day)) return SCE_RTC_ERROR_BAD_PARSE; - if (*cursor != 'T' && *cursor != 't' && *cursor != ' ') return SCE_RTC_ERROR_BAD_PARSE; - ++cursor; - if (!parseDigits(cursor, 2, hour) || *cursor++ != ':' || !parseDigits(cursor, 2, minute) || *cursor++ != ':' || !parseDigits(cursor, 2, second)) return SCE_RTC_ERROR_BAD_PARSE; + bool ok = parseDigits(cursor, 4, year) && *cursor++ == '-' && parseDigits(cursor, 2, month) && *cursor++ == '-' && parseDigits(cursor, 2, day); + ok = ok && (*cursor == 'T' || *cursor == 't' || *cursor == ' '); + if (ok) ++cursor; + ok = ok && parseDigits(cursor, 2, hour) && *cursor++ == ':' && parseDigits(cursor, 2, minute) && *cursor++ == ':' && parseDigits(cursor, 2, second); std::uint32_t microsecond = 0; - if (*cursor == '.') { + if (ok && *cursor == '.') { ++cursor; std::uint32_t scale = 100000; - if (*cursor < '0' || *cursor > '9') return SCE_RTC_ERROR_BAD_PARSE; - for (; *cursor >= '0' && *cursor <= '9'; ++cursor, scale /= 10) microsecond += static_cast(*cursor - '0') * scale; + ok = *cursor >= '0' && *cursor <= '9'; + for (; ok && *cursor >= '0' && *cursor <= '9'; ++cursor, scale /= 10) microsecond += static_cast(*cursor - '0') * scale; + } + std::int64_t offsetMinutes = 0; + if (ok && *cursor != 0) ok = parseZoneSuffix(cursor, offsetMinutes); + if (ok && *cursor != 0) ok = false; + if (!ok) { + NotImplemented_nid_no_patch(__func__); + return SCE_RTC_ERROR_BAD_PARSE; } - if (*cursor != 0) return SCE_RTC_ERROR_BAD_PARSE; const RtcDateTime time{static_cast(year), static_cast(month), static_cast(day), static_cast(hour), static_cast(minute), static_cast(second), microsecond}; if (const int result = validate(&time); result != 0) return result; - utc->tick = toTick(time); - return 0; + const RtcTick local{toTick(time)}; + return addTicks(utc, &local, -offsetMinutes, TICKS_PER_MINUTE); } int APS5_VABI sceRtcTickAddTicks(RtcTick* dst, const RtcTick* src, int64_t ticks) { diff --git a/core/libs/tests/GuestRtc.cpp b/core/libs/tests/GuestRtc.cpp index 8016c239b..23d203f06 100644 --- a/core/libs/tests/GuestRtc.cpp +++ b/core/libs/tests/GuestRtc.cpp @@ -3,6 +3,7 @@ #include #include #include +#include extern "C" { int APS5_VABI sceRtcCheckValid(const RtcDateTime*); @@ -21,6 +22,7 @@ int APS5_VABI sceRtcGetWin32FileTime(const RtcDateTime*, std::uint64_t*); int APS5_VABI sceRtcSetWin32FileTime(RtcDateTime*, std::uint64_t); int APS5_VABI sceRtcFormatRFC3339(char*, const RtcTick*, int); int APS5_VABI sceRtcParseRFC3339(RtcTick*, const char*); +int APS5_VABI sceRtcParseDateTime(RtcTick*, const char*); int APS5_VABI sceRtcTickAddTicks(RtcTick*, const RtcTick*, std::int64_t); int APS5_VABI sceRtcTickAddSeconds(RtcTick*, const RtcTick*, std::int64_t); int APS5_VABI sceRtcTickAddDays(RtcTick*, const RtcTick*, std::int32_t); @@ -123,6 +125,30 @@ int main() { Require(sceRtcParseRFC3339(&tick, "2024-02-29T12:34:56.789-23:59") == 0 && tick.tick == leapDayTick + 86340000000ull); Require(sceRtcParseRFC3339(nullptr, "1970-01-01T00:00:00Z") == invalidPointer); + Require(sceRtcParseDateTime(&tick, "2024-02-29T12:34:56.789") == 0 && tick.tick == leapDayTick); + Require(sceRtcParseDateTime(&tick, "2024-02-29 12:34:56") == 0 && tick.tick == leapDayTick - 789000ull); + Require(sceRtcParseDateTime(&tick, "2024-02-29T14:04:56.789+01:30") == 0 && tick.tick == leapDayTick); + Require(sceRtcParseDateTime(&tick, "2024-02-29t12:34:56.789z") == 0 && tick.tick == leapDayTick); + Require(sceRtcParseDateTime(&tick, "1970-01-01T00:00:00Z") == 0 && tick.tick == unixEpochTick); + Require(sceRtcParseDateTime(&tick, "2023-02-29T00:00:00") == invalidDay); + Require(sceRtcParseDateTime(nullptr, "1970-01-01T00:00:00Z") == invalidPointer); + const char* unparseable[] = { + "2024-02-29", + "2024/02/29T12:34:56", + "2024-02-29T12:34:56Zjunk", + "2024-02-29T12:34:56+99:99", + "2024-02-29T12:34:56.", + }; + for (const char* text : unparseable) { + bool thrown = false; + try { + sceRtcParseDateTime(&tick, text); + } catch (const std::exception&) { + thrown = true; + } + Require(thrown); + } + RtcTick source{leapDayTick}; RtcTick result{}; Require(sceRtcTickAddSeconds(&result, &source, 3600) == 0 && result.tick == leapDayTick + 3600000000ull); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 22d1e657d..b7b3a93f3 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -23,7 +23,7 @@ Throughout the project, every function at every stage either **does exactly what ### Unknown function info - [sceVideoOutOpen](../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - the open param's first word is unknown (PPSA21564 passes 16; it is not the byte size, since the affinity mask is at offset 16); only 16 is accepted. Whether the param continues past offset 24 is also unknown -- [sceRtcParseDateTime](../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - accepted format assumed to be the ISO 8601 subset `YYYY-MM-DDTHH:MM:SS[.ffffff]` (T/t/space separator, optional fractional seconds), matching the sibling RFC 3339 parser; unverified against hardware +- [sceRtcParseDateTime](../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - accepted format assumed to be the ISO 8601 subset `YYYY-MM-DDTHH:MM:SS[.ffffff]` (T/t/space separator, optional fractional seconds) with the sibling RFC 3339 zone suffix (`Z` or `±HH:MM`); a string without a zone is taken as UTC, and anything outside this shape throws instead of returning a parse error, unverified against hardware - [AMPR WaitOnAddress / WaitOnCounter](../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libSceAmpr) - compare encoding assumed to be the WAIT_REG_MEM one (0 always, 1 <, 2 <=, 3 ==, 4 !=, 5 >=, 6 >) - [sceAgcSetSubmitMode](../../core/libs/prx/libSceAgc/Misc/src/Suspend.cpp) (libSceAgc) - mode values unknown; only 0 is accepted - [zARR5aCmkoY](../../core/libs/prx/libSceAgc/DcbFlow/src/Control.cpp) (libSceAgc) - unknown name, signature From 9e7032513964f5478fe64722f3da38e8988549a9 Mon Sep 17 00:00:00 2001 From: Pouare514 Date: Mon, 5 Oct 2026 01:10:56 +0200 Subject: [PATCH 19/19] chore: drop stray AudioIn hunk --- core/libs/prx/libSceAudioIn/Export.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAudioIn/Export.cpp b/core/libs/prx/libSceAudioIn/Export.cpp index 3c31aaa40..cdd232455 100644 --- a/core/libs/prx/libSceAudioIn/Export.cpp +++ b/core/libs/prx/libSceAudioIn/Export.cpp @@ -122,7 +122,7 @@ int APS5_VABI sceAudioInInput(int handle, void* dest) { if (device != 0) { captured = capture(device, out, bytes, deadline); } else { - std::this_thread::sleep_until(wake); + while (Clock::now() < wake) std::this_thread::sleep_until(wake); } std::memset(out + captured, 0, bytes - captured); lock.lock();