From 221086beb607cd8b506a98454f1eebb3cd9b7f2b Mon Sep 17 00:00:00 2001 From: SamG-Coder <13449963+SamG-Coder@users.noreply.github.com> Date: Fri, 25 Sep 2026 05:32:49 +1000 Subject: [PATCH 01/94] fix(relinker): support ELF images without PLT relocations --- core/relinker/CMakeLists.txt | 3 + .../src/pipeline/RelinkerPipeline.cpp | 31 ++++++--- .../relinker/tests/test_optional_plt.py | 69 +++++++++++++++++++ 3 files changed, 92 insertions(+), 11 deletions(-) create mode 100644 core/relinker/relinker/tests/test_optional_plt.py diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index c556e72ad..2daa44c99 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -73,6 +73,9 @@ endif() if(BUILD_TESTING) + find_package(Python3 REQUIRED COMPONENTS Interpreter) + add_test(NAME optional_plt COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_optional_plt.py $) add_executable(strict_nid_filter_tests ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/StrictNidFilterTests.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp diff --git a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp index 7bacccdd1..f53becebc 100644 --- a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp +++ b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp @@ -99,10 +99,14 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf return hasTag(osTag) ? getTagValue(osTag) : getTagValue(sysvTag); }; - gotVAddr = requireExactlyOneOf(DT_OS_PLTGOT, DT_PLTGOT, "DT_PLTGOT") - ? getTagValue(DT_OS_PLTGOT) - : getTagValue(DT_PLTGOT); - gotSize = readAsSize(DT_OS_PLTRELSZ, DT_PLTRELSZ, "DT_PLTRELSZ"); + const bool hasPltRelocations = hasTag(DT_OS_PLTRELSZ) || hasTag(DT_PLTRELSZ) + || hasTag(DT_OS_PLTREL) || hasTag(DT_PLTREL) + || hasTag(DT_OS_JMPREL) || hasTag(DT_JMPREL); + if (hasPltRelocations || hasTag(DT_OS_PLTGOT) || hasTag(DT_PLTGOT)) { + gotVAddr = requireExactlyOneOf(DT_OS_PLTGOT, DT_PLTGOT, "DT_PLTGOT") + ? getTagValue(DT_OS_PLTGOT) + : getTagValue(DT_PLTGOT); + } const FileByteOffset dynStrTabOffset = readAsOffset(DT_OS_STRTAB, DT_STRTAB, "DT_STRTAB"); requireExactlyOneOf(DT_OS_STRSZ, DT_STRSZ, "DT_STRSZ"); @@ -112,13 +116,18 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf if (readAsSize(DT_OS_SYMENT, DT_SYMENT, "DT_SYMENT") != symEntSize) throw RelinkerException("Unsupported DT_SYMENT value"); - const std::int64_t jmprelType = requireExactlyOneOf(DT_OS_PLTREL, DT_PLTREL, "DT_PLTREL") - ? getTagValue(DT_OS_PLTREL) - : getTagValue(DT_PLTREL); - if (jmprelType != DT_RELA) - throw RelinkerException("Unsupported DT_PLTREL type"); - - const FileByteOffset dynJmpRelOffset = readAsOffset(DT_OS_JMPREL, DT_JMPREL, "DT_JMPREL"); + FileByteOffset dynJmpRelOffset = 0; + if (hasPltRelocations) { + gotSize = readAsSize(DT_OS_PLTRELSZ, DT_PLTRELSZ, "DT_PLTRELSZ"); + const std::int64_t jmprelType = requireExactlyOneOf(DT_OS_PLTREL, DT_PLTREL, "DT_PLTREL") + ? getTagValue(DT_OS_PLTREL) + : getTagValue(DT_PLTREL); + if (jmprelType != DT_RELA) + throw RelinkerException("Unsupported DT_PLTREL type"); + dynJmpRelOffset = readAsOffset(DT_OS_JMPREL, DT_JMPREL, "DT_JMPREL"); + if (gotSize % 24 != 0) + throw RelinkerException("Invalid DT_PLTRELSZ value"); + } const ByteCount dynJmpRelSize = gotSize; const FileByteOffset dynRelaOffset = readAsOffset(DT_OS_RELA, DT_RELA, "DT_RELA"); diff --git a/core/relinker/relinker/tests/test_optional_plt.py b/core/relinker/relinker/tests/test_optional_plt.py new file mode 100644 index 000000000..894b47a7b --- /dev/null +++ b/core/relinker/relinker/tests/test_optional_plt.py @@ -0,0 +1,69 @@ +"""Exercise optional PLT metadata through the real ELF-to-PE pipeline.""" + +import os +from pathlib import Path +import struct +import subprocess +import sys +import tempfile + + +def fixture(extra_tags=()): + image = bytearray(0x1000) + image[:16] = b"\x7fELF\x02\x01\x01" + bytes(9) + struct.pack_into(" Date: Fri, 25 Sep 2026 05:53:32 +1000 Subject: [PATCH 02/94] fix(relinker): accept unused empty ELF TLS segments --- core/relinker/CMakeLists.txt | 2 + .../src/windows/WindowsTlsBuilder.cpp | 9 +++- .../relinker/relinker/tests/test_empty_tls.py | 50 +++++++++++++++++++ 3 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 core/relinker/relinker/tests/test_empty_tls.py diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 2daa44c99..f82728eb4 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -76,6 +76,8 @@ if(BUILD_TESTING) find_package(Python3 REQUIRED COMPONENTS Interpreter) add_test(NAME optional_plt COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_optional_plt.py $) + add_test(NAME empty_tls COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_empty_tls.py $) add_executable(strict_nid_filter_tests ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/StrictNidFilterTests.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp diff --git a/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp index f2390a534..ea58d8138 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp @@ -77,8 +77,15 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co return {}; } - if (tls->MemorySize == 0 || tls->FileSize > tls->MemorySize || tls->Offset > source.size() || tls->FileSize > source.size() - tls->Offset || !std::has_single_bit(tls->Alignment) || tls->Alignment > 8192 || tls->MemorySize > 0x7fff0000u) + if (tls->FileSize > tls->MemorySize || tls->Offset > source.size() || tls->FileSize > source.size() - tls->Offset || !std::has_single_bit(tls->Alignment) || tls->Alignment > 8192 || tls->MemorySize > 0x7fff0000u) throw Domain::RelinkerException("Invalid or unsupported ELF TLS layout", tls->Offset); + // Native homebrew linkers can emit an empty PT_TLS placeholder. + // It needs no Windows TLS directory unless guest code accesses TLS. + if (tls->MemorySize == 0) { + if (!accesses.empty()) + throw Domain::RelinkerException("Guest TLS access with empty PT_TLS", tls->Offset); + return {}; + } const auto alignment = std::max(tls->Alignment, 16); const auto blockSize = CheckedRva((tls->MemorySize + alignment - 1) & ~(alignment - 1)); const auto templateOffset = CheckedRva((64 + alignment - 1) & ~(alignment - 1)); diff --git a/core/relinker/relinker/tests/test_empty_tls.py b/core/relinker/relinker/tests/test_empty_tls.py new file mode 100644 index 000000000..aaf0c633e --- /dev/null +++ b/core/relinker/relinker/tests/test_empty_tls.py @@ -0,0 +1,50 @@ +"""Validate empty PT_TLS handling through ELF conversion and PE execution.""" + +import os +from pathlib import Path +import struct +import subprocess +import sys +import tempfile + +from test_optional_plt import fixture + + +def main(): + relinker = Path(sys.argv[1]).resolve() + cases = [ + ("empty", 0, 0, False, None), + ("nonempty", 8, 16, False, None), + ("invalid-sizes", 1, 0, False, "Invalid or unsupported ELF TLS layout"), + ("empty-access", 0, 0, True, "Guest TLS access with empty PT_TLS"), + ] + with tempfile.TemporaryDirectory(prefix="anyps5-tls-") as directory: + for name, file_size, memory_size, access, error in cases: + source = Path(directory) / (name + ".elf") + output = source.with_suffix(".exe") + data = fixture() + struct.pack_into(" Date: Fri, 25 Sep 2026 05:53:32 +1000 Subject: [PATCH 03/94] docs: record native Yamagi Quake II conversion and startup --- docs/YAMAGI_QUAKE2.md | 71 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 docs/YAMAGI_QUAKE2.md diff --git a/docs/YAMAGI_QUAKE2.md b/docs/YAMAGI_QUAKE2.md new file mode 100644 index 000000000..d24ea2d25 --- /dev/null +++ b/docs/YAMAGI_QUAKE2.md @@ -0,0 +1,71 @@ +# Yamagi Quake II compatibility investigation + +Target: [BlackBearReloaded's native PS5 release v0.2.0-alpha.1](https://github.com/blackbearreloaded/ps5-yamagi-quake2/releases/tag/v0.2.0-alpha.1), title PPSA99007. Investigated on Windows on 2026-09-25. + +The release includes the Quake II demo and its original notices. All downloaded +game files, extracted executables, and logs remain under ignored `downloads/`. + +## Input preparation + +`PPSA99007.zip` SHA-256: +`4FA5DA07498FE64FFC67229EED2347E0DDFFD5073F1FEA248BA5E69307229A70`. + +The released `eboot.bin` is a plaintext FSELF container. AnyPS5's raw ELF reader +rejects it with `Invalid ELF magic number`. Extract the ELF using the game's +[native application tooling](https://github.com/blackbearreloaded/ps5-native-app-boilerplate), +revision `722f2227a8bb6fa2229120546995b6562552c752`: + +```powershell +downloads/ps5-yamagi-quake2/ps5-native-tool.exe self --extract ` + --file downloads/ps5-yamagi-quake2/extracted/PPSA99007/eboot.bin ` + --out downloads/ps5-yamagi-quake2/eboot.elf +``` + +The extraction tool was built with MSYS2 UCRT64 g++, C++20, and zlib from +`native_app_builder.cpp`, `self_container.cpp`, `elf_object.cpp`, and +`sce_module_writer.cpp` in that repository's `tooling/native/` directory. + +Extracted ELF SHA-256: +`FC0879B50EF23B604FA81C8C28843C75AD54A515A6944D8809327C3A51B40A27`. + +## Conversion and startup + +The ELF contains an empty PT_TLS segment (zero file and memory sizes, alignment +one). The Windows TLS builder previously rejected this. It now accepts an empty +segment when no guest TLS accesses exist, without adding a PE TLS directory. +Malformed size combinations and accesses to an empty segment remain errors. + +```powershell +build-winlibs/core/relinker/relinker.exe --windows --windows-diagnostics ` + --rpath D:/AnyPS5/build-winlibs/core/libs/libs ` + downloads/ps5-yamagi-quake2/eboot.elf ` + downloads/ps5-yamagi-quake2/quake2.exe +``` + +Conversion succeeds with all 382 import references preserved. This command +embeds a workstation-specific library path. Run it from the download directory +if the generated `windows-diagnostics-imports.txt` should also remain there +(adjust the relative paths accordingly). + +The compatibility libraries were built using the project's documented WinLibs +GCC 15.2.0 toolchain. Place its `libgcc_s_seh-1.dll`, `libstdc++-6.dll`, and +`libwinpthread-1.dll` beside the generated executable for this startup check. + +Observed startup result: + +- Loads libSceSystemService, libSceUserService, libScePad, libSceAudioOut, + libSceVideoOut, libSceKeyboard, libSceImeDialog, libSceAgc, libSceAgcDriver, + and libSceLibcInternal compatibility modules successfully. +- Stops loading `libScePosixForWebKit.prx`, which AnyPS5 does not currently build. +- Exits with `0xc0000135`; no transfer to the guest entry point and no game frame. + +The next work is to audit the game's required POSIX imports against existing +AnyPS5 implementations before providing the missing module. Successful module +loading alone does not establish API behavior, rendering, or playability. + +## Regression coverage + +`ctest --test-dir build-winlibs --output-on-failure`: four suites pass. The new +`empty_tls` integration suite checks absent PE TLS for an empty guest segment, +retained TLS for a nonempty segment, rejection of malformed sizes and empty TLS +accesses, and execution of both valid generated Windows fixtures (exit 42). From 6ba24a895c97951509baae532a3fe434c8471808 Mon Sep 17 00:00:00 2001 From: SamG-Coder <13449963+SamG-Coder@users.noreply.github.com> Date: Fri, 25 Sep 2026 05:59:07 +1000 Subject: [PATCH 04/94] feat(libs): implement homebrew string compatibility functions --- core/libs/CMakeLists.txt | 9 +++ core/libs/prx/libScePosixForWebKit/Export.cpp | 22 +++++++ core/libs/prx/libc/CMakeLists.txt | 1 + core/libs/prx/libc/src/StringExtensions.cpp | 50 ++++++++++++++++ core/libs/tests/GuestStrings.cpp | 59 +++++++++++++++++++ 5 files changed, 141 insertions(+) create mode 100644 core/libs/prx/libScePosixForWebKit/Export.cpp create mode 100644 core/libs/prx/libc/src/StringExtensions.cpp create mode 100644 core/libs/tests/GuestStrings.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 24d3d6794..b45e3cbcf 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -103,6 +103,15 @@ endforeach() add_custom_target(libs DEPENDS ${PATCHED_OUTPUTS}) +if(BUILD_TESTING) + add_executable(guest_string_tests + tests/GuestStrings.cpp + prx/libc/src/StringExtensions.cpp + prx/libScePosixForWebKit/Export.cpp) + target_include_directories(guest_string_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + add_test(NAME guest_strings COMMAND guest_string_tests) +endif() + add_executable(application_heap_tests EXCLUDE_FROM_ALL tests/ApplicationHeap.cpp prx/libc/src/ApplicationHeap.cpp) target_include_directories(application_heap_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) set_target_properties(application_heap_tests PROPERTIES CXX_EXTENSIONS OFF) diff --git a/core/libs/prx/libScePosixForWebKit/Export.cpp b/core/libs/prx/libScePosixForWebKit/Export.cpp new file mode 100644 index 000000000..71f7b26f6 --- /dev/null +++ b/core/libs/prx/libScePosixForWebKit/Export.cpp @@ -0,0 +1,22 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include + +extern "C" { + +char* APS5_VABI strcasestr_nid_postfix(const char* text, const char* needle) { + if (*needle == '\0') return const_cast(text); + for (; *text != '\0'; ++text) { + const char* candidate = text; + const char* match = needle; + while (*candidate != '\0' && *match != '\0' && + std::tolower(static_cast(*candidate)) == + std::tolower(static_cast(*match))) { + ++candidate; + ++match; + } + if (*match == '\0') return const_cast(text); + } + return nullptr; +} + +} diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index 6ae1d0f20..4b490e626 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -4,6 +4,7 @@ target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/specifics/gcc/StackGuard.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/specifics/linux/DlIteratePhdr.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Strings.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/StringExtensions.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Math.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Time.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/FileAndHeap.cpp diff --git a/core/libs/prx/libc/src/StringExtensions.cpp b/core/libs/prx/libc/src/StringExtensions.cpp new file mode 100644 index 000000000..5f31137aa --- /dev/null +++ b/core/libs/prx/libc/src/StringExtensions.cpp @@ -0,0 +1,50 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include + +extern "C" { + +std::size_t APS5_VABI strnlen_nid_postfix(const char* text, std::size_t limit) { + std::size_t length = 0; + while (length < limit && text[length] != '\0') ++length; + return length; +} + +char* APS5_VABI strncat_nid_postfix(char* destination, const char* source, std::size_t limit) { + return std::strncat(destination, source, limit); +} + +char* APS5_VABI strpbrk_nid_postfix(const char* text, const char* accept) { + return const_cast(std::strpbrk(text, accept)); +} + +std::size_t APS5_VABI strcspn_nid_postfix(const char* text, const char* reject) { + return std::strcspn(text, reject); +} + +std::size_t APS5_VABI strlcat_nid_postfix(char* destination, const char* source, std::size_t capacity) { + const auto destinationLength = strnlen_nid_postfix(destination, capacity); + const auto sourceLength = std::strlen(source); + if (destinationLength < capacity) { + const auto remaining = capacity - destinationLength - 1; + const auto count = sourceLength < remaining ? sourceLength : remaining; + std::memcpy(destination + destinationLength, source, count); + destination[destinationLength + count] = '\0'; + } + return destinationLength + sourceLength; +} + +char* APS5_VABI strtok_r_nid_postfix(char* text, const char* delimiters, char** state) { + if (text == nullptr) text = *state; + text += std::strspn(text, delimiters); + if (*text == '\0') { + *state = text; + return nullptr; + } + char* end = text + std::strcspn(text, delimiters); + if (*end != '\0') *end++ = '\0'; + *state = end; + return text; +} + +} diff --git a/core/libs/tests/GuestStrings.cpp b/core/libs/tests/GuestStrings.cpp new file mode 100644 index 000000000..7357ec8d2 --- /dev/null +++ b/core/libs/tests/GuestStrings.cpp @@ -0,0 +1,59 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include + +extern "C" { +std::size_t APS5_VABI strnlen_nid_postfix(const char*, std::size_t); +char* APS5_VABI strncat_nid_postfix(char*, const char*, std::size_t); +char* APS5_VABI strpbrk_nid_postfix(const char*, const char*); +std::size_t APS5_VABI strcspn_nid_postfix(const char*, const char*); +std::size_t APS5_VABI strlcat_nid_postfix(char*, const char*, std::size_t); +char* APS5_VABI strtok_r_nid_postfix(char*, const char*, char**); +char* APS5_VABI strcasestr_nid_postfix(const char*, const char*); +} + +static void Require(bool condition) { + if (!condition) { + std::fputs("Guest string check failed\n", stderr); + std::abort(); + } +} + +int main() { + const char bounded[] = {'a', 'b', 'c'}; + Require(strnlen_nid_postfix(bounded, 0) == 0); + Require(strnlen_nid_postfix(bounded, sizeof(bounded)) == 3); + Require(strnlen_nid_postfix("a", 8) == 1); + char truncated[] = "abXX"; + Require(strlcat_nid_postfix(truncated, "cd", 2) == 4); + Require(std::strcmp(truncated, "abXX") == 0); + char buffer[8] = "ab"; + Require(strlcat_nid_postfix(buffer, "cdefgh", sizeof(buffer)) == 8); + Require(std::strcmp(buffer, "abcdefg") == 0); + Require(strlcat_nid_postfix(buffer, "xyz", 0) == 3); + buffer[0] = '\0'; + Require(strlcat_nid_postfix(buffer, "x", 1) == 1 && buffer[0] == '\0'); + Require(strncat_nid_postfix(buffer, "xyz", 2) == buffer); + Require(std::strcmp(buffer, "xy") == 0); + Require(strpbrk_nid_postfix(buffer, "ay") == buffer + 1); + Require(strpbrk_nid_postfix(buffer, "") == nullptr); + Require(strcspn_nid_postfix(buffer, "y") == 1); + char first[] = ",a,,b,"; + char second[] = "x:y"; + char* firstState = nullptr; + char* secondState = nullptr; + Require(std::strcmp(strtok_r_nid_postfix(first, ",", &firstState), "a") == 0); + Require(std::strcmp(strtok_r_nid_postfix(second, ":", &secondState), "x") == 0); + Require(std::strcmp(strtok_r_nid_postfix(nullptr, ",", &firstState), "b") == 0); + Require(strtok_r_nid_postfix(nullptr, ",", &firstState) == nullptr); + Require(strtok_r_nid_postfix(nullptr, ",", &firstState) == nullptr); + Require(std::strcmp(strtok_r_nid_postfix(nullptr, "", &secondState), "y") == 0); + const char text[] = "aABAbC"; + Require(strcasestr_nid_postfix(text, "ababc") == text + 1); + Require(strcasestr_nid_postfix(text, "") == text); + Require(strcasestr_nid_postfix(text, "abcdef") == nullptr); + Require(strcasestr_nid_postfix("", "a") == nullptr); + const char highBytes[] = {static_cast(0xff), 'A', 0}; + Require(strcasestr_nid_postfix(highBytes, "a") == highBytes + 1); +} From f146a3d069b8b2adf73fee6e6a3c0907f94fa374 Mon Sep 17 00:00:00 2001 From: SamG-Coder <13449963+SamG-Coder@users.noreply.github.com> Date: Fri, 25 Sep 2026 06:00:07 +1000 Subject: [PATCH 05/94] fix(windows): expose shared libc for internal runtime imports --- .../src/windows/WindowsImportBuilder.cpp | 5 ++++ .../WindowsDependencyDiagnosticsTests.cpp | 25 +++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/core/relinker/elfpatcher/src/windows/WindowsImportBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsImportBuilder.cpp index d1b54e371..7a449830c 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsImportBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsImportBuilder.cpp @@ -48,6 +48,11 @@ std::vector WindowsImportBuilder::ReadLibraries(const Domain::SysVD throw Domain::RelinkerException("Invalid or duplicate DT_NEEDED library: " + name); result.push_back(std::move(name)); } + // AnyPS5 implements the C runtime in libc.prx. Windows GetProcAddress + // does not search a module's dependencies as ELF symbol lookup does. + // Keep the requested module first, then make its shared runtime visible. + if (unique.contains("libSceLibcInternal.prx") && !unique.contains("libc.prx")) + result.push_back("libc.prx"); return result; } diff --git a/core/relinker/elfpatcher/tests/WindowsDependencyDiagnosticsTests.cpp b/core/relinker/elfpatcher/tests/WindowsDependencyDiagnosticsTests.cpp index 77f0a6f89..9866f1f07 100644 --- a/core/relinker/elfpatcher/tests/WindowsDependencyDiagnosticsTests.cpp +++ b/core/relinker/elfpatcher/tests/WindowsDependencyDiagnosticsTests.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -14,6 +15,29 @@ namespace { namespace Fs = std::filesystem; using namespace Elfpatcher::Windows; +void checkRuntimeDependencies() { + const auto check = [](const std::vector& input, const std::vector& expected) { + Domain::SysVDynamicSection dynamic; + for (const auto& name : input) { + const auto offset = dynamic.DynStrData.size(); + dynamic.DynStrData.insert(dynamic.DynStrData.end(), name.begin(), name.end()); + dynamic.DynStrData.push_back(0); + const auto position = dynamic.DynamicSegmentData.size(); + dynamic.DynamicSegmentData.resize(position + 16); + Io::WriteU64(dynamic.DynamicSegmentData, position, 1); + Io::WriteU64(dynamic.DynamicSegmentData, position + 8, offset); + } + if (WindowsImportBuilder{}.ReadLibraries(dynamic) != expected) + throw std::runtime_error("Incorrect Windows runtime dependency visibility"); + }; + check({}, {}); + check({"libkernel.prx"}, {"libkernel.prx"}); + check({"libSceLibcInternal.prx", "libkernel.prx"}, + {"libSceLibcInternal.prx", "libkernel.prx", "libc.prx"}); + check({"libc.prx", "libSceLibcInternal.prx"}, + {"libc.prx", "libSceLibcInternal.prx"}); +} + void writeFile(const Fs::path& path, const std::vector& bytes) { std::ofstream stream(path, std::ios::binary); if (!stream.write(reinterpret_cast(bytes.data()), static_cast(bytes.size()))) @@ -154,6 +178,7 @@ void expectDiagnostic(const Fs::path& runner, const std::vector& ex int main() { try { + checkRuntimeDependencies(); std::vector filename(32768); const auto size = GetModuleFileNameA(nullptr, filename.data(), static_cast(filename.size())); if (size == 0 || size >= filename.size()) From 9cc8f872cc8f7a5ea8f413bb53e129c53675b01d Mon Sep 17 00:00:00 2001 From: SamG-Coder <13449963+SamG-Coder@users.noreply.github.com> Date: Fri, 25 Sep 2026 06:00:07 +1000 Subject: [PATCH 06/94] docs: update Quake II import audit and startup progress --- docs/YAMAGI_QUAKE2.md | 39 +++++++++++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/docs/YAMAGI_QUAKE2.md b/docs/YAMAGI_QUAKE2.md index d24ea2d25..0c65715fd 100644 --- a/docs/YAMAGI_QUAKE2.md +++ b/docs/YAMAGI_QUAKE2.md @@ -51,7 +51,7 @@ The compatibility libraries were built using the project's documented WinLibs GCC 15.2.0 toolchain. Place its `libgcc_s_seh-1.dll`, `libstdc++-6.dll`, and `libwinpthread-1.dll` beside the generated executable for this startup check. -Observed startup result: +Initial startup result (before the follow-up changes below): - Loads libSceSystemService, libSceUserService, libScePad, libSceAudioOut, libSceVideoOut, libSceKeyboard, libSceImeDialog, libSceAgc, libSceAgcDriver, @@ -59,9 +59,34 @@ Observed startup result: - Stops loading `libScePosixForWebKit.prx`, which AnyPS5 does not currently build. - Exits with `0xc0000135`; no transfer to the guest entry point and no game frame. -The next work is to audit the game's required POSIX imports against existing -AnyPS5 implementations before providing the missing module. Successful module -loading alone does not establish API behavior, rendering, or playability. +Successful module loading alone does not establish API behavior, rendering, +or playability. + +## Follow-up: string exports and Windows runtime visibility + +The static import audit found 373 unique imported NIDs (382 relocation +references). Before this follow-up, 168 NIDs were absent from the combined +export tables of the built compatibility modules. This is an export-availability +check, not proof that the other implementations satisfy the game's requirements. + +The seven imports attributed to libScePosixForWebKit are `getaddrinfo`, +`freeaddrinfo`, `gai_strerror`, `getnameinfo`, `isatty`, `mkstemp`, and `strcasestr`. +The new module implements `strcasestr`; the other six remain unsupported. +The shared libc also gains `strnlen`, `strncat`, `strpbrk`, `strcspn`, `strlcat`, +and `strtok_r`. None of these additions is a success-returning placeholder. + +After adding the module, startup exposed a separate Windows lookup problem: +`_init_env` exists in AnyPS5's libc.prx, but GetProcAddress does not search the +dependencies of libSceLibcInternal.prx. For Windows conversion, a request for +libSceLibcInternal.prx now also adds libc.prx to the explicit symbol search list, +after the game's requested modules. An existing explicit libc.prx entry is +retained without duplication; unrelated inputs gain no dependency. + +Latest verified startup loads all twelve game-requested modules plus libc.prx. +It then stops at missing import `BPE9s9vQQXo` (`mmap`) with exit `0xc0000139`. +There is still no transfer to the guest entry point and no game frame. The next +runtime work is guest memory-mapping support, alongside the remaining import +gaps in file I/O, threading, libc, POSIX services, and graphics. ## Regression coverage @@ -69,3 +94,9 @@ loading alone does not establish API behavior, rendering, or playability. `empty_tls` integration suite checks absent PE TLS for an empty guest segment, retained TLS for a nonempty segment, rejection of malformed sizes and empty TLS accesses, and execution of both valid generated Windows fixtures (exit 42). + +After the follow-up: five suites pass. `guest_strings` exercises the guest SysV +calling convention, bounded unterminated input, concatenation truncation, +interleaved tokenizer state, and case-insensitive searches. Windows dependency +tests also cover implicit libc visibility, ordering, duplicate avoidance, and +unrelated inputs. The complete `libs` target rebuild succeeds. From 1bbdf7daa69d7f588ff9d01ded25317f66c3b40c Mon Sep 17 00:00:00 2001 From: SamG-Coder <13449963+SamG-Coder@users.noreply.github.com> Date: Fri, 25 Sep 2026 06:06:31 +1000 Subject: [PATCH 07/94] feat(kernel): support private anonymous guest memory mappings --- core/libs/CMakeLists.txt | 9 +++ core/libs/prx/libkernel/CMakeLists.txt | 1 + .../libkernel/DirectMemory/PosixMemory.cpp | 81 +++++++++++++++++++ core/libs/tests/GuestMemory.cpp | 70 ++++++++++++++++ docs/YAMAGI_QUAKE2.md | 17 ++++ 5 files changed, 178 insertions(+) create mode 100644 core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp create mode 100644 core/libs/tests/GuestMemory.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index b45e3cbcf..f9970a384 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -104,6 +104,15 @@ endforeach() add_custom_target(libs DEPENDS ${PATCHED_OUTPUTS}) if(BUILD_TESTING) + add_executable(guest_memory_tests tests/GuestMemory.cpp) + target_include_directories(guest_memory_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_memory_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_memory_tests) + add_test(NAME guest_memory COMMAND guest_memory_tests) + if(WIN32) + set_tests_properties(guest_memory PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_executable(guest_string_tests tests/GuestStrings.cpp prx/libc/src/StringExtensions.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 359346859..598cca734 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -7,6 +7,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Semaphore/src/Semaphore.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DirectMemory/Export.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DirectMemory/DirectMemory.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/DirectMemory/PosixMemory.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DirectMemory/MemoryPool.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DirectMemory/MemoryPoolExport.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/src/Mutex.cpp diff --git a/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp b/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp new file mode 100644 index 000000000..e3d2f1796 --- /dev/null +++ b/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp @@ -0,0 +1,81 @@ +#include "DirectMemory.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include + +extern "C" int* APS5_VABI __error_nid_postfix(); + +namespace { +// FreeBSD/PS5 ABI values, independent of the host's errno and mmap constants. +constexpr int GuestInvalid = 22; +constexpr int GuestNoMemory = 12; +constexpr int GuestNotSupported = 45; +constexpr int GuestPrivate = 0x2; +constexpr int GuestAnonymous = 0x1000; + +bool RoundLength(std::size_t length, std::size_t& rounded) { + constexpr auto mask = PS5_PAGE_SIZE - 1; + if (length == 0 || length > std::numeric_limits::max() - mask) + return false; + rounded = (length + mask) & ~mask; + return true; +} + +void SetError(int error) { + *__error_nid_postfix() = error; +} +} + +extern "C" { + +void* APS5_VABI mmap_nid_postfix(void* address, std::size_t length, int protection, + int flags, int descriptor, std::int64_t offset) noexcept { + const auto failed = [](int error) -> void* { + SetError(error); + return reinterpret_cast(static_cast(-1)); + }; + std::size_t rounded; + if (!RoundLength(length, rounded) || (protection & ~7) != 0) + return failed(GuestInvalid); + if (flags != (GuestPrivate | GuestAnonymous)) + return failed(GuestNotSupported); + if (descriptor != -1 || offset != 0) + return failed(GuestInvalid); + // A non-fixed address is a hint; the kernel may choose another address. + (void)address; + void* mapped = nullptr; + try { + const auto result = DoMapAnon(&mapped, rounded, protection, 0); + if (result != 0) return failed(GuestNoMemory); + return mapped; + } catch (const std::bad_alloc&) { + return failed(GuestNoMemory); + } catch (const std::exception&) { + return failed(GuestNoMemory); + } +} + +int APS5_VABI munmap_nid_postfix(void* address, std::size_t length) noexcept { + const auto failed = [](int error) { + SetError(error); + return -1; + }; + std::size_t rounded; + const auto start = reinterpret_cast(address); + if (!RoundLength(length, rounded) || start == 0 || + (start & (PS5_PAGE_SIZE - 1)) != 0 || + rounded > std::numeric_limits::max() - start) + return failed(GuestInvalid); + try { + if (DoMunmap(address, rounded) != 0) return failed(GuestInvalid); + return 0; + } catch (const std::bad_alloc&) { + return failed(GuestNoMemory); + } catch (const std::exception&) { + // The allocation tracker rejects foreign, pinned, and noncontiguous ranges. + return failed(GuestInvalid); + } +} + +} diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp new file mode 100644 index 000000000..e0e906e3d --- /dev/null +++ b/core/libs/tests/GuestMemory.cpp @@ -0,0 +1,70 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include +#include +#include +#include + +extern "C" { +void* APS5_VABI mmap_nid_postfix(void*, std::size_t, int, int, int, std::int64_t) noexcept; +int APS5_VABI munmap_nid_postfix(void*, std::size_t) noexcept; +int* APS5_VABI __error_nid_postfix(); +} + +static void Require(bool condition) { + if (!condition) { + std::fputs("Guest memory check failed\n", stderr); + std::abort(); + } +} + +int main() { + constexpr std::size_t page = 0x4000; + const auto failed = reinterpret_cast(static_cast(-1)); + const auto reject = [&](std::size_t length, int protection, int flags, int fd, + std::int64_t offset, int error) { + *__error_nid_postfix() = 0; + Require(mmap_nid_postfix(nullptr, length, protection, flags, fd, offset) == failed); + Require(*__error_nid_postfix() == error); + }; + reject(0, 3, 0x1002, -1, 0, 22); + reject(std::numeric_limits::max(), 3, 0x1002, -1, 0, 22); + reject(page, 8, 0x1002, -1, 0, 22); + reject(page, 3, 0x1002, 0, 0, 22); + reject(page, 3, 0x1002, -1, 1, 22); + reject(page, 3, 0x1001, -1, 0, 45); // shared + reject(page, 3, 0x1012, -1, 0, 45); // fixed + reject(page, 3, 0x2, 0, 0, 45); // file-backed + reject(page, 3, 0x22, -1, 0, 45); // Linux MAP_ANON is not guest MAP_ANON + + auto* memory = static_cast(mmap_nid_postfix(nullptr, page * 3 - 1, 3, 0x1002, -1, 0)); + Require(memory != failed && (reinterpret_cast(memory) & (page - 1)) == 0); + for (std::size_t i = 0; i < page * 3; ++i) Require(memory[i] == 0); + memory[0] = 42; + memory[page * 2] = 73; + { + GuestAllocations::Mutation mutation; + const auto range = mutation.Find(memory); + Require(range.bytes == page * 3 && range.readable && range.writable); + } + Require(munmap_nid_postfix(memory + 1, page) == -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(memory[0] == 42 && memory[page * 2] == 73); + Require(munmap_nid_postfix(memory, page) == 0); + Require(memory[page * 2] == 73); + Require(munmap_nid_postfix(memory + page * 2, page) == 0); + Require(munmap_nid_postfix(memory, page) == -1); + for (int protection : {0, 1, 3, 5}) { + void* mapped = mmap_nid_postfix(memory, 1, protection, 0x1002, -1, 0); + Require(mapped != failed); + { + GuestAllocations::Mutation mutation; + const auto range = mutation.Find(mapped); + Require(range.readable == ((protection & 3) != 0)); + Require(range.writable == ((protection & 2) != 0)); + } + Require(munmap_nid_postfix(mapped, 1) == 0); + } +} diff --git a/docs/YAMAGI_QUAKE2.md b/docs/YAMAGI_QUAKE2.md index 0c65715fd..2b7f4bcde 100644 --- a/docs/YAMAGI_QUAKE2.md +++ b/docs/YAMAGI_QUAKE2.md @@ -100,3 +100,20 @@ calling convention, bounded unterminated input, concatenation truncation, interleaved tokenizer state, and case-insensitive searches. Windows dependency tests also cover implicit libc visibility, ordering, duplicate avoidance, and unrelated inputs. The complete `libs` target rebuild succeeds. + +## Follow-up: anonymous memory mapping + +The game's `tooling/native/app_heap.c` requests private anonymous read/write +memory for its OpenGL heap. `mmap` and `munmap` now route this use through +AnyPS5's existing tracked guest allocations, with 16 KiB page rounding and +partial unmapping. Guest MAP_ANON is 0x1000, not the Linux host value 0x20. +Failures return MAP_FAILED or -1 and set guest errno through `__error`. +Shared, fixed-address, and file-backed mappings remain explicitly unsupported +(guest EOPNOTSUPP 45). Unmapping currently requires a contiguous tracked range +within one allocation; holes, foreign memory, and pinned ranges are rejected. + +Six CTest suites pass, including real allocations, zero-fill, alignment, +read/write access, tracking, partial unmapping, protection metadata, and invalid +argument/unsupported-mode errors. The unchanged converted game now resolves +the memory-mapping imports and stops at `-hn1tcVHq5Q` (`sceLibcMspaceCreate`). +It still has not reached its entry point or drawn a frame. From dec5841b2d8c6dad8b39b31bdef31d07e9f3a12e Mon Sep 17 00:00:00 2001 From: SamG-Coder <13449963+SamG-Coder@users.noreply.github.com> Date: Fri, 25 Sep 2026 06:10:50 +1000 Subject: [PATCH 08/94] feat(libc): implement caller-backed mspace heaps --- core/libs/CMakeLists.txt | 9 ++ core/libs/prx/libc/CMakeLists.txt | 1 + core/libs/prx/libc/src/Mspace.cpp | 177 ++++++++++++++++++++++++++++++ core/libs/tests/Mspace.cpp | 65 +++++++++++ 4 files changed, 252 insertions(+) create mode 100644 core/libs/prx/libc/src/Mspace.cpp create mode 100644 core/libs/tests/Mspace.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index f9970a384..e51d2c57b 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -104,6 +104,15 @@ endforeach() add_custom_target(libs DEPENDS ${PATCHED_OUTPUTS}) if(BUILD_TESTING) + add_executable(mspace_tests tests/Mspace.cpp) + target_include_directories(mspace_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(mspace_tests PRIVATE libc) + configure_windows_unwind(mspace_tests) + add_test(NAME mspace COMMAND mspace_tests) + if(WIN32) + set_tests_properties(mspace PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_executable(guest_memory_tests tests/GuestMemory.cpp) target_include_directories(guest_memory_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_memory_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index 4b490e626..b3428c80d 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -10,6 +10,7 @@ target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/FileAndHeap.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/GuestAllocations.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/GuestHeap.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/Mspace.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/ApplicationHeap.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/ApplicationProcessParameters.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/JumpBuffer.cpp diff --git a/core/libs/prx/libc/src/Mspace.cpp b/core/libs/prx/libc/src/Mspace.cpp new file mode 100644 index 000000000..5e1119fa5 --- /dev/null +++ b/core/libs/prx/libc/src/Mspace.cpp @@ -0,0 +1,177 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include + +extern "C" int* APS5_VABI __error_nid_postfix(); + +namespace { +struct alignas(16) Block { + std::size_t capacity; + Block* next; + void* pointer; + std::size_t used; +}; +struct alignas(16) Arena { + Arena* next; + Block* first; + std::uintptr_t end; +}; +std::mutex arenaMutex; +Arena* arenas = nullptr; + +void Error(int value) { *__error_nid_postfix() = value; } +Arena* Find(void* handle) { + for (auto* arena = arenas; arena; arena = arena->next) + if (arena == handle) return arena; + Error(22); + return nullptr; +} +Block* FindBlock(Arena* arena, const void* pointer) { + if (arena && pointer) + for (auto* block = arena->first; block; block = block->next) + if (block->pointer == pointer) return block; + Error(22); + return nullptr; +} +void* Allocate(Arena* arena, std::size_t size, std::size_t alignment) { + if (!arena) return nullptr; + size = std::max(size, 1); + for (auto* block = arena->first; block; block = block->next) { + if (block->pointer) continue; + const auto start = reinterpret_cast(block + 1); + if (start > std::numeric_limits::max() - (alignment - 1)) continue; + const auto aligned = (start + alignment - 1) & ~(alignment - 1); + const auto padding = aligned - start; + if (padding > block->capacity || size > block->capacity - padding) continue; + auto consumed = padding + size; + // Splitting preserves header alignment and avoids small unusable fragments. + if (consumed <= std::numeric_limits::max() - 15) { + const auto rounded = (consumed + 15) & ~std::size_t{15}; + if (rounded <= block->capacity && block->capacity - rounded >= sizeof(Block) + 16) { + auto* tail = reinterpret_cast(start + rounded); + *tail = {block->capacity - rounded - sizeof(Block), block->next, nullptr, 0}; + block->capacity = rounded; + block->next = tail; + } + } + block->pointer = reinterpret_cast(aligned); + block->used = size; + return block->pointer; + } + Error(12); + return nullptr; +} +void Release(Arena* arena, Block* released) { + released->pointer = nullptr; + released->used = 0; + for (auto* block = arena->first; block && block->next;) { + if (!block->pointer && !block->next->pointer) { + block->capacity += sizeof(Block) + block->next->capacity; + block->next = block->next->next; + } else block = block->next; + } +} +} + +extern "C" { +void* APS5_VABI sceLibcMspaceCreate_nid_postfix(const char* name, void* base, + std::size_t size, unsigned flags) { + (void)name; + const auto start = reinterpret_cast(base); + if (!base || (start & 15) || size < sizeof(Arena) + sizeof(Block) + 16 || + size > std::numeric_limits::max() - start || flags != 0) { + Error(22); + return nullptr; + } + std::lock_guard lock(arenaMutex); + for (auto* arena = arenas; arena; arena = arena->next) { + if (start < arena->end && reinterpret_cast(arena) < start + size) { + Error(22); + return nullptr; + } + } + auto* arena = static_cast(base); + auto* block = reinterpret_cast(arena + 1); + *block = {size - sizeof(Arena) - sizeof(Block), nullptr, nullptr, 0}; + *arena = {arenas, block, start + size}; + arenas = arena; + return arena; +} + +int APS5_VABI sceLibcMspaceDestroy_nid_postfix(void* handle) { + std::lock_guard lock(arenaMutex); + for (auto** entry = &arenas; *entry; entry = &(*entry)->next) { + if (*entry == handle) { + *entry = (*entry)->next; + return 0; + } + } + Error(22); + return -1; +} + +void* APS5_VABI sceLibcMspaceMalloc_nid_postfix(void* handle, std::size_t size) { + std::lock_guard lock(arenaMutex); + return Allocate(Find(handle), size, 16); +} + +void APS5_VABI sceLibcMspaceFree_nid_postfix(void* handle, void* pointer) { + if (!pointer) return; + std::lock_guard lock(arenaMutex); + auto* arena = Find(handle); + if (auto* block = FindBlock(arena, pointer)) Release(arena, block); +} + +void* APS5_VABI sceLibcMspaceCalloc_nid_postfix(void* handle, std::size_t count, std::size_t size) { + if (size && count > std::numeric_limits::max() / size) { + Error(12); + return nullptr; + } + std::lock_guard lock(arenaMutex); + void* result = Allocate(Find(handle), count * size, 16); + if (result) std::memset(result, 0, count * size); + return result; +} + +void* APS5_VABI sceLibcMspaceRealloc_nid_postfix(void* handle, void* pointer, std::size_t size) { + std::lock_guard lock(arenaMutex); + auto* arena = Find(handle); + if (!pointer) return Allocate(arena, size, 16); + auto* block = FindBlock(arena, pointer); + if (!block) return nullptr; + if (!size) { Release(arena, block); return nullptr; } + const auto padding = reinterpret_cast(pointer) - reinterpret_cast(block + 1); + if (size <= block->capacity - padding) { block->used = size; return pointer; } + void* result = Allocate(arena, size, 16); + if (result) { + std::memcpy(result, pointer, block->used); + Release(arena, block); + } + return result; +} + +int APS5_VABI sceLibcMspacePosixMemalign_nid_postfix(void* handle, void** result, + std::size_t alignment, std::size_t size) { + if (!result || alignment < sizeof(void*) || (alignment & (alignment - 1))) return 22; + std::lock_guard lock(arenaMutex); + auto* arena = Find(handle); + if (!arena) return 22; + void* pointer = Allocate(arena, size, std::max(alignment, 16)); + if (!pointer) return 12; + *result = pointer; + return 0; +} + +std::size_t APS5_VABI sceLibcMspaceMallocUsableSize_nid_postfix(const void* pointer) { + if (!pointer) return 0; + std::lock_guard lock(arenaMutex); + for (auto* arena = arenas; arena; arena = arena->next) + for (auto* block = arena->first; block; block = block->next) + if (block->pointer == pointer) return block->used; + Error(22); + return 0; +} +} diff --git a/core/libs/tests/Mspace.cpp b/core/libs/tests/Mspace.cpp new file mode 100644 index 000000000..f286a7dc5 --- /dev/null +++ b/core/libs/tests/Mspace.cpp @@ -0,0 +1,65 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +void* APS5_VABI sceLibcMspaceCreate_nid_postfix(const char*, void*, std::size_t, unsigned); +int APS5_VABI sceLibcMspaceDestroy_nid_postfix(void*); +void* APS5_VABI sceLibcMspaceMalloc_nid_postfix(void*, std::size_t); +void* APS5_VABI sceLibcMspaceCalloc_nid_postfix(void*, std::size_t, std::size_t); +void* APS5_VABI sceLibcMspaceRealloc_nid_postfix(void*, void*, std::size_t); +void APS5_VABI sceLibcMspaceFree_nid_postfix(void*, void*); +int APS5_VABI sceLibcMspacePosixMemalign_nid_postfix(void*, void**, std::size_t, std::size_t); +std::size_t APS5_VABI sceLibcMspaceMallocUsableSize_nid_postfix(const void*); +} +static void Require(bool value) { + if (!value) { std::fputs("Mspace check failed\n", stderr); std::abort(); } +} +int main() { + alignas(16) std::array storage{}; + void* arena = sceLibcMspaceCreate_nid_postfix("test", storage.data(), storage.size(), 0); + Require(arena != nullptr); + Require(sceLibcMspaceCreate_nid_postfix("overlap", storage.data(), storage.size(), 0) == nullptr); + auto* first = static_cast(sceLibcMspaceCalloc_nid_postfix(arena, 32, 4)); + Require(first > storage.data() && first + 128 <= storage.data() + storage.size()); + for (int i = 0; i < 128; ++i) { Require(first[i] == 0); first[i] = static_cast(i); } + void* blocker = sceLibcMspaceMalloc_nid_postfix(arena, 128); + auto* grown = static_cast(sceLibcMspaceRealloc_nid_postfix(arena, first, 4096)); + Require(grown && grown != first); + for (int i = 0; i < 128; ++i) Require(grown[i] == i); + Require(sceLibcMspaceMallocUsableSize_nid_postfix(grown) >= 4096); + Require(sceLibcMspaceRealloc_nid_postfix(arena, grown, storage.size()) == nullptr); + Require(grown[127] == 127); + void* aligned = nullptr; + Require(sceLibcMspacePosixMemalign_nid_postfix(arena, &aligned, 4096, 1024) == 0); + Require((reinterpret_cast(aligned) & 4095) == 0); + void* unchanged = aligned; + Require(sceLibcMspacePosixMemalign_nid_postfix(arena, &unchanged, 3, 8) == 22 && unchanged == aligned); + Require(sceLibcMspaceCalloc_nid_postfix(arena, std::numeric_limits::max(), 2) == nullptr); + sceLibcMspaceFree_nid_postfix(arena, blocker); + sceLibcMspaceFree_nid_postfix(arena, aligned); + Require(sceLibcMspaceRealloc_nid_postfix(arena, grown, 0) == nullptr); + void* large = sceLibcMspaceMalloc_nid_postfix(arena, storage.size() - 256); + Require(large != nullptr); // freeing coalesced all the fragmented blocks + sceLibcMspaceFree_nid_postfix(arena, large); + std::array workers; + for (auto& worker : workers) worker = std::thread([&] { + for (int i = 0; i < 1000; ++i) { + void* pointer = sceLibcMspaceMalloc_nid_postfix(arena, 97); + Require(pointer != nullptr); + std::memset(pointer, 42, 97); + Require(sceLibcMspaceMallocUsableSize_nid_postfix(pointer) >= 97); + sceLibcMspaceFree_nid_postfix(arena, pointer); + } + }); + for (auto& worker : workers) worker.join(); + Require(sceLibcMspaceDestroy_nid_postfix(arena) == 0); + Require(sceLibcMspaceMalloc_nid_postfix(arena, 8) == nullptr); + Require(sceLibcMspaceCreate_nid_postfix("reuse", storage.data(), storage.size(), 0) == arena); + Require(sceLibcMspaceDestroy_nid_postfix(arena) == 0); +} From cbf836225133048ad8fee300f49237b97ba97b52 Mon Sep 17 00:00:00 2001 From: SamG-Coder <13449963+SamG-Coder@users.noreply.github.com> Date: Fri, 25 Sep 2026 06:15:32 +1000 Subject: [PATCH 09/94] feat(posix): translate guest address resolution APIs --- core/libs/CMakeLists.txt | 8 + .../prx/libScePosixForWebKit/CMakeLists.txt | 6 + .../libScePosixForWebKit/GuestResolver.hpp | 19 ++ .../prx/libScePosixForWebKit/Resolver.cpp | 184 ++++++++++++++++++ core/libs/tests/GuestResolver.cpp | 61 ++++++ docs/YAMAGI_QUAKE2.md | 25 +++ 6 files changed, 303 insertions(+) create mode 100644 core/libs/prx/libScePosixForWebKit/CMakeLists.txt create mode 100644 core/libs/prx/libScePosixForWebKit/GuestResolver.hpp create mode 100644 core/libs/prx/libScePosixForWebKit/Resolver.cpp create mode 100644 core/libs/tests/GuestResolver.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index e51d2c57b..209da779b 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -104,6 +104,14 @@ endforeach() add_custom_target(libs DEPENDS ${PATCHED_OUTPUTS}) if(BUILD_TESTING) + add_executable(guest_resolver_tests tests/GuestResolver.cpp) + target_include_directories(guest_resolver_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_resolver_tests PRIVATE libScePosixForWebKit) + add_test(NAME guest_resolver COMMAND guest_resolver_tests) + if(WIN32) + set_tests_properties(guest_resolver PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_executable(mspace_tests tests/Mspace.cpp) target_include_directories(mspace_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(mspace_tests PRIVATE libc) diff --git a/core/libs/prx/libScePosixForWebKit/CMakeLists.txt b/core/libs/prx/libScePosixForWebKit/CMakeLists.txt new file mode 100644 index 000000000..f5e668f80 --- /dev/null +++ b/core/libs/prx/libScePosixForWebKit/CMakeLists.txt @@ -0,0 +1,6 @@ +add_library(libScePosixForWebKit SHARED EXCLUDE_FROM_ALL Export.cpp Resolver.cpp) +target_include_directories(libScePosixForWebKit PRIVATE ${LIBS_INCLUDE_DIR}) +if(WIN32) + target_link_libraries(libScePosixForWebKit PRIVATE ws2_32) +endif() +configure_windows_unwind(libScePosixForWebKit) diff --git a/core/libs/prx/libScePosixForWebKit/GuestResolver.hpp b/core/libs/prx/libScePosixForWebKit/GuestResolver.hpp new file mode 100644 index 000000000..d14d4524e --- /dev/null +++ b/core/libs/prx/libScePosixForWebKit/GuestResolver.hpp @@ -0,0 +1,19 @@ +#pragma once +#include +#include + +namespace GuestResolver { +struct AddressInfo { + std::int32_t flags; + std::int32_t family; + std::int32_t socketType; + std::int32_t protocol; + std::uint32_t addressLength; + char* canonicalName; + void* address; + AddressInfo* next; +}; +static_assert(sizeof(AddressInfo) == 48); +static_assert(offsetof(AddressInfo, canonicalName) == 24); +static_assert(offsetof(AddressInfo, address) == 32); +} diff --git a/core/libs/prx/libScePosixForWebKit/Resolver.cpp b/core/libs/prx/libScePosixForWebKit/Resolver.cpp new file mode 100644 index 000000000..5952e6276 --- /dev/null +++ b/core/libs/prx/libScePosixForWebKit/Resolver.cpp @@ -0,0 +1,184 @@ +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#include +#else +#include +#include +#include +#endif +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include "GuestResolver.hpp" + +namespace { +bool Ready() { +#ifdef _WIN32 + static const int status = [] { WSADATA data{}; return WSAStartup(MAKEWORD(2, 2), &data); }(); + return status == 0; +#else + return true; +#endif +} +int GuestError(int error) { + if (error == 0) return 0; + if (error == EAI_AGAIN) return 2; + if (error == EAI_BADFLAGS) return 3; + if (error == EAI_FAMILY) return 5; + if (error == EAI_MEMORY) return 6; + if (error == EAI_NONAME) return 8; + if (error == EAI_SERVICE) return 9; + if (error == EAI_SOCKTYPE) return 10; +#ifdef _WIN32 + if (error == WSAEFAULT) return 14; +#else + if (error == EAI_OVERFLOW) return 14; +#endif + return 4; +} +} + +extern "C" { +void APS5_VABI freeaddrinfo_nid_postfix(GuestResolver::AddressInfo* first) { + while (first) { + auto* next = first->next; + std::free(first->canonicalName); + std::free(first->address); + std::free(first); + first = next; + } +} + +int APS5_VABI getaddrinfo_nid_postfix(const char* node, const char* service, + const GuestResolver::AddressInfo* hints, GuestResolver::AddressInfo** output) { + if (!output) return 4; + *output = nullptr; + addrinfo nativeHints{}; + if (hints) { + if (hints->addressLength || hints->address || hints->canonicalName || hints->next) return 12; + if ((hints->flags & ~0xd0f) != 0) return 3; + if (hints->family != 0 && hints->family != 2 && hints->family != 28) return 5; + if (hints->socketType != 0 && hints->socketType != 1 && hints->socketType != 2) return 10; + nativeHints.ai_family = hints->family == 28 ? AF_INET6 : hints->family == 2 ? AF_INET : AF_UNSPEC; + nativeHints.ai_socktype = hints->socketType == 1 ? SOCK_STREAM : hints->socketType == 2 ? SOCK_DGRAM : 0; + nativeHints.ai_protocol = hints->protocol; + if (hints->flags & 1) nativeHints.ai_flags |= AI_PASSIVE; + if (hints->flags & 2) nativeHints.ai_flags |= AI_CANONNAME; + if (hints->flags & 4) nativeHints.ai_flags |= AI_NUMERICHOST; + if (hints->flags & 8) nativeHints.ai_flags |= AI_NUMERICSERV; + if (hints->flags & 0x100) nativeHints.ai_flags |= AI_ALL; + if (hints->flags & 0x400) nativeHints.ai_flags |= AI_ADDRCONFIG; + if (hints->flags & 0x800) nativeHints.ai_flags |= AI_V4MAPPED; + } + if (!Ready()) return 4; + addrinfo* native = nullptr; + const auto error = ::getaddrinfo(node, service, &nativeHints, &native); + if (error) return GuestError(error); + GuestResolver::AddressInfo* first = nullptr; + auto** tail = &first; + int failure = 0; + for (auto* item = native; item; item = item->ai_next) { + if (item->ai_family != AF_INET && item->ai_family != AF_INET6) { failure = 5; break; } + auto* converted = static_cast(std::calloc(1, sizeof(GuestResolver::AddressInfo))); + if (!converted) { failure = 6; break; } + *tail = converted; + tail = &converted->next; + converted->flags = hints ? hints->flags : 0; + converted->family = item->ai_family == AF_INET ? 2 : 28; + converted->socketType = item->ai_socktype == SOCK_STREAM ? 1 : item->ai_socktype == SOCK_DGRAM ? 2 : 0; + converted->protocol = item->ai_protocol; + converted->addressLength = item->ai_family == AF_INET ? 16 : 28; + auto* bytes = static_cast(std::calloc(1, converted->addressLength)); + converted->address = bytes; + if (!bytes) { failure = 6; break; } + bytes[0] = static_cast(converted->addressLength); + bytes[1] = static_cast(converted->family); + if (item->ai_family == AF_INET) { + const auto* v4 = reinterpret_cast(item->ai_addr); + std::memcpy(bytes + 2, &v4->sin_port, 2); + std::memcpy(bytes + 4, &v4->sin_addr, 4); + } else { + const auto* v6 = reinterpret_cast(item->ai_addr); + std::memcpy(bytes + 2, &v6->sin6_port, 2); + std::memcpy(bytes + 4, &v6->sin6_flowinfo, 4); + std::memcpy(bytes + 8, &v6->sin6_addr, 16); + std::memcpy(bytes + 24, &v6->sin6_scope_id, 4); + } + if (item->ai_canonname) { + const auto size = std::strlen(item->ai_canonname) + 1; + converted->canonicalName = static_cast(std::malloc(size)); + if (!converted->canonicalName) { failure = 6; break; } + std::memcpy(converted->canonicalName, item->ai_canonname, size); + } + } + ::freeaddrinfo(native); + if (failure) { freeaddrinfo_nid_postfix(first); return failure; } + *output = first; + return 0; +} + +int APS5_VABI getnameinfo_nid_postfix(const void* address, std::uint32_t length, + char* host, std::uint32_t hostLength, char* service, std::uint32_t serviceLength, int flags) { + // PS5 sockaddr begins with byte-sized length and family. The host's does not. + if (!address || length < 2) return 5; + if ((!host && hostLength) || (!service && serviceLength)) return 4; + if ((flags & ~0x1f) != 0) return 3; + const auto* bytes = static_cast(address); + sockaddr_storage native{}; + socklen_t nativeLength; + if (bytes[1] == 2) { + if (length < 16 || bytes[0] != 16) return 5; + auto& v4 = reinterpret_cast(native); + v4.sin_family = AF_INET; + std::memcpy(&v4.sin_port, bytes + 2, 2); + std::memcpy(&v4.sin_addr, bytes + 4, 4); + nativeLength = sizeof(v4); + } else if (bytes[1] == 28) { + if (length < 28 || bytes[0] != 28) return 5; + auto& v6 = reinterpret_cast(native); + v6.sin6_family = AF_INET6; + std::memcpy(&v6.sin6_port, bytes + 2, 2); + std::memcpy(&v6.sin6_flowinfo, bytes + 4, 4); + std::memcpy(&v6.sin6_addr, bytes + 8, 16); + std::memcpy(&v6.sin6_scope_id, bytes + 24, 4); + nativeLength = sizeof(v6); + } else return 5; + int nativeFlags = 0; + if (flags & 1) nativeFlags |= NI_NOFQDN; + if (flags & 2) nativeFlags |= NI_NUMERICHOST; + if (flags & 4) nativeFlags |= NI_NAMEREQD; + if (flags & 8) nativeFlags |= NI_NUMERICSERV; + if (flags & 16) nativeFlags |= NI_DGRAM; + if (!Ready()) return 4; + // Stage results so host-specific short-buffer errors do not leak into the + // guest ABI, and neither guest output is partially modified on overflow. + char resolvedHost[NI_MAXHOST]{}; + char resolvedService[NI_MAXSERV]{}; + const bool wantHost = host && hostLength; + const bool wantService = service && serviceLength; + const int error = ::getnameinfo(reinterpret_cast(&native), nativeLength, + wantHost ? resolvedHost : nullptr, wantHost ? sizeof(resolvedHost) : 0, + wantService ? resolvedService : nullptr, wantService ? sizeof(resolvedService) : 0, nativeFlags); + if (error) return GuestError(error); + if ((wantHost && std::strlen(resolvedHost) >= hostLength) || + (wantService && std::strlen(resolvedService) >= serviceLength)) return 14; + if (wantHost) std::memcpy(host, resolvedHost, std::strlen(resolvedHost) + 1); + if (wantService) std::memcpy(service, resolvedService, std::strlen(resolvedService) + 1); + return 0; +} + +const char* APS5_VABI gai_strerror_nid_postfix(int error) { + static const char* messages[] = { + "Success", "Address family for hostname not supported", "Temporary failure in name resolution", + "Invalid flags", "Name resolution failed", "Address family not supported", + "Memory allocation failed", "No address for hostname", "Name or service not known", + "Service not supported for socket type", "Socket type not supported", "System error", + "Invalid hints", "Unknown protocol", "Buffer too small" + }; + if (error < 0 || error >= static_cast(sizeof(messages) / sizeof(messages[0]))) + return "Unknown address resolution error"; + return messages[error]; +} +} diff --git a/core/libs/tests/GuestResolver.cpp b/core/libs/tests/GuestResolver.cpp new file mode 100644 index 000000000..d65089c55 --- /dev/null +++ b/core/libs/tests/GuestResolver.cpp @@ -0,0 +1,61 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libScePosixForWebKit/GuestResolver.hpp" +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI getaddrinfo_nid_postfix(const char*, const char*, const GuestResolver::AddressInfo*, GuestResolver::AddressInfo**); +void APS5_VABI freeaddrinfo_nid_postfix(GuestResolver::AddressInfo*); +int APS5_VABI getnameinfo_nid_postfix(const void*, std::uint32_t, char*, std::uint32_t, char*, std::uint32_t, int); +const char* APS5_VABI gai_strerror_nid_postfix(int); +} +void Require(bool condition, std::source_location location = std::source_location::current()) { + if (!condition) { + std::fprintf(stderr, "Resolver check failed at line %u\n", location.line()); + std::abort(); + } +} +int main() { + std::array v4{16, 2, 0x6d, 0x06, 127, 0, 0, 1}; + char host[128]{}, service[32]{}; + Require(getnameinfo_nid_postfix(v4.data(), v4.size(), host, sizeof(host), service, sizeof(service), 10) == 0); + Require(std::strcmp(host, "127.0.0.1") == 0 && std::strcmp(service, "27910") == 0); + std::array v6{28, 28}; + v6[23] = 1; + Require(getnameinfo_nid_postfix(v6.data(), v6.size(), host, sizeof(host), nullptr, 0, 10) == 0); + Require(std::strcmp(host, "::1") == 0); + Require(getnameinfo_nid_postfix(v4.data(), 2, host, sizeof(host), nullptr, 0, 10) == 5); + Require(getnameinfo_nid_postfix(v4.data(), v4.size(), host, sizeof(host), nullptr, 0, 32) == 3); + Require(getnameinfo_nid_postfix(v4.data(), v4.size(), host, 1, nullptr, 0, 10) == 14); + Require(std::strlen(gai_strerror_nid_postfix(14)) > 0); + Require(std::strstr(gai_strerror_nid_postfix(-1), "Unknown") != nullptr); + GuestResolver::AddressInfo hints{}; + hints.flags = 12; // numeric host and service: no external DNS dependency + hints.socketType = 1; + for (const auto* numeric : {"127.0.0.1", "::1"}) { + GuestResolver::AddressInfo* result = nullptr; + Require(getaddrinfo_nid_postfix(numeric, "27910", &hints, &result) == 0); + Require(result != nullptr); + for (auto* entry = result; entry; entry = entry->next) { + Require(entry->family == 2 || entry->family == 28); + Require(entry->socketType == 1); + Require(getnameinfo_nid_postfix(entry->address, entry->addressLength, + host, sizeof(host), service, sizeof(service), 10) == 0); + Require(std::strcmp(host, numeric) == 0 && std::strcmp(service, "27910") == 0); + } + freeaddrinfo_nid_postfix(result); + } + GuestResolver::AddressInfo* result = reinterpret_cast(1); + hints.family = 1234; + Require(getaddrinfo_nid_postfix("127.0.0.1", "1", &hints, &result) == 5 && !result); + hints.family = 0; + hints.flags = 0x200; + Require(getaddrinfo_nid_postfix("127.0.0.1", "1", &hints, &result) == 3 && !result); + hints.flags = 12; + Require(getaddrinfo_nid_postfix("invalid-numeric-address", "1", &hints, &result) == 8 && !result); + freeaddrinfo_nid_postfix(nullptr); +} diff --git a/docs/YAMAGI_QUAKE2.md b/docs/YAMAGI_QUAKE2.md index 2b7f4bcde..a2283d17b 100644 --- a/docs/YAMAGI_QUAKE2.md +++ b/docs/YAMAGI_QUAKE2.md @@ -117,3 +117,28 @@ read/write access, tracking, partial unmapping, protection metadata, and invalid argument/unsupported-mode errors. The unchanged converted game now resolves the memory-mapping imports and stops at `-hn1tcVHq5Q` (`sceLibcMspaceCreate`). It still has not reached its entry point or drawn a frame. + +## Follow-up: caller-backed heaps and address resolution + +The generic `sceLibcMspace*` implementation allocates within the memory region +provided by the caller. It supports malloc, calloc, realloc, free, aligned +allocation, usable-size queries, and destruction. Free blocks coalesce; failed +reallocation preserves the original block. Tests cover region ownership, +alignment, overflow, exhaustion, data preservation, block reuse, and concurrent +calls. Only creation flags zero are currently supported. Arena lifetime remains +the caller's responsibility: destroy it before releasing its backing memory. +No game code or game-specific heap size is embedded in this implementation. + +The POSIX module now supplies `getnameinfo`, `getaddrinfo`, `freeaddrinfo`, and +`gai_strerror`, converting guest sockaddr/addrinfo layouts, IPv6 family IDs, +flags, and resolver error values. Native results are copied into owned guest +layout structures and released through the matching free function. Short +getnameinfo buffers return guest EAI_OVERFLOW without copying partial results. +Supported families are IPv4/IPv6; socket hints support unspecified, stream, +and datagram. Unsupported flags and invalid hints fail explicitly. + +All eight CTest suites pass on Windows. Resolver tests use numeric loopback +addresses and ports, so they do not depend on external DNS. Live DNS and online +multiplayer are not validated by these tests. The full compatibility-library +build succeeds, and the unchanged game now stops at `RIa6GnWp+iU` (`strerror`). +No guest entry-point execution or rendered frame has been observed yet. From 97e6ce992b8f09e4e173c20487fda883d4c66907 Mon Sep 17 00:00:00 2001 From: SamG-Coder <13449963+SamG-Coder@users.noreply.github.com> Date: Fri, 25 Sep 2026 06:17:18 +1000 Subject: [PATCH 10/94] feat(libc): implement guest errno message APIs --- core/libs/CMakeLists.txt | 9 ++++ core/libs/prx/libc/CMakeLists.txt | 1 + core/libs/prx/libc/src/Errors.cpp | 75 +++++++++++++++++++++++++++++++ core/libs/tests/GuestErrors.cpp | 37 +++++++++++++++ 4 files changed, 122 insertions(+) create mode 100644 core/libs/prx/libc/src/Errors.cpp create mode 100644 core/libs/tests/GuestErrors.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 209da779b..686be48ce 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -104,6 +104,15 @@ endforeach() add_custom_target(libs DEPENDS ${PATCHED_OUTPUTS}) if(BUILD_TESTING) + add_executable(guest_error_tests tests/GuestErrors.cpp) + target_include_directories(guest_error_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_error_tests PRIVATE libc) + configure_windows_unwind(guest_error_tests) + add_test(NAME guest_errors COMMAND guest_error_tests) + if(WIN32) + set_tests_properties(guest_errors PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_executable(guest_resolver_tests tests/GuestResolver.cpp) target_include_directories(guest_resolver_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_resolver_tests PRIVATE libScePosixForWebKit) diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index b3428c80d..574558de6 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -18,6 +18,7 @@ target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/RuntimeSupport.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/LocaleSupport.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Process.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/Errors.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/CxxAbiSupport.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/exception/Exports.cpp ) diff --git a/core/libs/prx/libc/src/Errors.cpp b/core/libs/prx/libc/src/Errors.cpp new file mode 100644 index 000000000..963191cf6 --- /dev/null +++ b/core/libs/prx/libc/src/Errors.cpp @@ -0,0 +1,75 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include + +extern "C" int* APS5_VABI __error_nid_postfix(); + +namespace { +// Guest FreeBSD errno numbering, not the host CRT's numbering. +constexpr const char* messages[] = { + "No error", "Operation not permitted", "No such file or directory", "No such process", + "Interrupted system call", "Input/output error", "Device not configured", "Argument list too long", + "Exec format error", "Bad file descriptor", "No child processes", "Resource deadlock avoided", + "Cannot allocate memory", "Permission denied", "Bad address", "Block device required", + "Device busy", "File exists", "Cross-device link", "Operation not supported by device", + "Not a directory", "Is a directory", "Invalid argument", "Too many open files in system", + "Too many open files", "Inappropriate ioctl for device", "Text file busy", "File too large", + "No space left on device", "Illegal seek", "Read-only filesystem", "Too many links", "Broken pipe", + "Numerical argument out of domain", "Result too large", "Resource temporarily unavailable", + "Operation now in progress", "Operation already in progress", "Socket operation on non-socket", + "Destination address required", "Message too long", "Protocol wrong type for socket", + "Protocol not available", "Protocol not supported", "Socket type not supported", + "Operation not supported", "Protocol family not supported", "Address family not supported", + "Address already in use", "Cannot assign requested address", "Network is down", "Network is unreachable", + "Network connection reset", "Connection aborted", "Connection reset by peer", "No buffer space available", + "Socket is already connected", "Socket is not connected", "Socket is shut down", "Too many references", + "Operation timed out", "Connection refused", "Too many symbolic links", "File name too long", + "Host is down", "No route to host", "Directory not empty", "Too many processes", "Too many users", + "Disk quota exceeded", "Stale file handle", "Remote path has too many levels", "Invalid RPC structure", + "RPC version mismatch", "RPC program unavailable", "Program version mismatch", "Invalid RPC procedure", + "No locks available", "Function not implemented", "Inappropriate file type", "Authentication error", + "Authentication required", "Identifier removed", "No message of requested type", "Value too large", + "Operation canceled", "Invalid byte sequence", "Attribute not found", "Programming error", + "Bad message", "Multihop attempted", "Link severed", "Protocol error", "Insufficient capabilities", + "Operation not permitted in capability mode", "State not recoverable", "Previous owner died" +}; +static_assert(sizeof(messages) / sizeof(messages[0]) == 97); +bool Known(int error) { return error >= 0 && error < 97; } +} + +extern "C" { +char* APS5_VABI strerror_nid_postfix(int error) { + thread_local char buffer[128]; + const int saved = *__error_nid_postfix(); + if (Known(error)) std::snprintf(buffer, sizeof(buffer), "%s", messages[error]); + else std::snprintf(buffer, sizeof(buffer), "Unknown error: %d", error); + *__error_nid_postfix() = Known(error) ? saved : 22; + return buffer; +} + +int APS5_VABI strerror_r_nid_postfix(int error, char* buffer, std::size_t length) { + const int saved = *__error_nid_postfix(); + char temporary[128]; + if (Known(error)) std::snprintf(temporary, sizeof(temporary), "%s", messages[error]); + else std::snprintf(temporary, sizeof(temporary), "Unknown error: %d", error); + const auto required = std::strlen(temporary) + 1; + int result = Known(error) ? 0 : 22; + if (!buffer || length < required) result = 34; + if (buffer && length) { + const auto count = required <= length ? required - 1 : length - 1; + std::memcpy(buffer, temporary, count); + buffer[count] = '\0'; + } + *__error_nid_postfix() = saved; + return result; +} + +void APS5_VABI perror_nid_postfix(const char* prefix) { + const int saved = *__error_nid_postfix(); + char message[128]; + strerror_r_nid_postfix(saved, message, sizeof(message)); + if (prefix && *prefix) std::fprintf(stderr, "%s: %s\n", prefix, message); + else std::fprintf(stderr, "%s\n", message); + *__error_nid_postfix() = saved; +} +} diff --git a/core/libs/tests/GuestErrors.cpp b/core/libs/tests/GuestErrors.cpp new file mode 100644 index 000000000..453dae221 --- /dev/null +++ b/core/libs/tests/GuestErrors.cpp @@ -0,0 +1,37 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include + +extern "C" { +char* APS5_VABI strerror_nid_postfix(int); +int APS5_VABI strerror_r_nid_postfix(int, char*, std::size_t); +int* APS5_VABI __error_nid_postfix(); +} +static void Require(bool value) { if (!value) std::abort(); } +int main() { + *__error_nid_postfix() = 13; + Require(std::strcmp(strerror_nid_postfix(35), "Resource temporarily unavailable") == 0); + Require(*__error_nid_postfix() == 13); + Require(std::strcmp(strerror_nid_postfix(78), "Function not implemented") == 0); + char* parent = strerror_nid_postfix(22); + std::thread worker([] { + Require(std::strcmp(strerror_nid_postfix(45), "Operation not supported") == 0); + }); + worker.join(); + Require(std::strcmp(parent, "Invalid argument") == 0); + char buffer[128]; + for (int error = 0; error <= 96; ++error) { + Require(strerror_r_nid_postfix(error, buffer, sizeof(buffer)) == 0); + Require(buffer[0] && std::strstr(buffer, "Unknown") == nullptr); + } + Require(strerror_r_nid_postfix(-1, buffer, sizeof(buffer)) == 22); + Require(std::strstr(buffer, "-1") != nullptr); + Require(*__error_nid_postfix() == 13); + char sentinel[] = "xyz"; + Require(strerror_r_nid_postfix(22, sentinel, 1) == 34); + Require(sentinel[0] == 0 && sentinel[1] == 'y'); + sentinel[0] = 'x'; + Require(strerror_r_nid_postfix(22, sentinel, 0) == 34 && sentinel[0] == 'x'); + Require(strerror_r_nid_postfix(22, nullptr, 0) == 34); +} From ff725218cba938c39ef6287e48e9d2c6bfb0b746 Mon Sep 17 00:00:00 2001 From: SamG-Coder <13449963+SamG-Coder@users.noreply.github.com> Date: Fri, 25 Sep 2026 06:18:32 +1000 Subject: [PATCH 11/94] feat(kernel): translate guest IP address conversion APIs --- core/libs/CMakeLists.txt | 9 ++++ core/libs/prx/libkernel/CMakeLists.txt | 2 + .../Socket/src/AddressConversion.cpp | 47 +++++++++++++++++++ core/libs/tests/GuestAddressConversion.cpp | 34 ++++++++++++++ 4 files changed, 92 insertions(+) create mode 100644 core/libs/prx/libkernel/Socket/src/AddressConversion.cpp create mode 100644 core/libs/tests/GuestAddressConversion.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 686be48ce..51aa2d888 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -104,6 +104,15 @@ endforeach() add_custom_target(libs DEPENDS ${PATCHED_OUTPUTS}) if(BUILD_TESTING) + add_executable(guest_address_tests tests/GuestAddressConversion.cpp) + target_include_directories(guest_address_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_address_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_address_tests) + add_test(NAME guest_addresses COMMAND guest_address_tests) + if(WIN32) + set_tests_properties(guest_addresses PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_executable(guest_error_tests tests/GuestErrors.cpp) target_include_directories(guest_error_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_error_tests PRIVATE libc) diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 598cca734..6467aebce 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -25,6 +25,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Tsd.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Sem.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Socket/src/Socket.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Socket/src/AddressConversion.cpp ${CMAKE_CURRENT_SOURCE_DIR}/EventFlag/src/EventFlag.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Module/src/Module.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Aio/src/Aio.cpp @@ -41,6 +42,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL target_include_directories(libkernel PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(libkernel PRIVATE libc) if(WIN32) + target_link_libraries(libkernel PRIVATE ws2_32) target_compile_definitions(libkernel PRIVATE _WIN32_WINNT=0x0602) endif() configure_windows_unwind(libkernel) diff --git a/core/libs/prx/libkernel/Socket/src/AddressConversion.cpp b/core/libs/prx/libkernel/Socket/src/AddressConversion.cpp new file mode 100644 index 000000000..b10fb9633 --- /dev/null +++ b/core/libs/prx/libkernel/Socket/src/AddressConversion.cpp @@ -0,0 +1,47 @@ +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#include +#else +#include +#endif +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include + +extern "C" int* APS5_VABI __error_nid_postfix(); + +extern "C" { +int APS5_VABI __inet_pton_nid_postfix(int family, const char* text, void* destination) { + if (family != 2 && family != 28) { *__error_nid_postfix() = 47; return -1; } + if (!text || !destination) { *__error_nid_postfix() = 14; return -1; } + unsigned char address[16]{}; + const int nativeFamily = family == 2 ? AF_INET : AF_INET6; +#ifdef _WIN32 + const int result = InetPtonA(nativeFamily, text, address); +#else + const int result = ::inet_pton(nativeFamily, text, address); +#endif + if (result == 1) std::memcpy(destination, address, family == 2 ? 4 : 16); + else if (result == -1) *__error_nid_postfix() = 22; + return result; +} + +const char* APS5_VABI __inet_ntop_nid_postfix(int family, const void* source, + char* destination, std::uint32_t capacity) { + if (family != 2 && family != 28) { *__error_nid_postfix() = 47; return nullptr; } + if (!source || !destination) { *__error_nid_postfix() = 14; return nullptr; } + char text[INET6_ADDRSTRLEN]{}; + const int nativeFamily = family == 2 ? AF_INET : AF_INET6; +#ifdef _WIN32 + const auto* result = InetNtopA(nativeFamily, const_cast(source), text, sizeof(text)); +#else + const auto* result = ::inet_ntop(nativeFamily, source, text, sizeof(text)); +#endif + if (!result) { *__error_nid_postfix() = 22; return nullptr; } + const auto size = std::strlen(text) + 1; + if (size > capacity) { *__error_nid_postfix() = 28; return nullptr; } + std::memcpy(destination, text, size); + return destination; +} +} diff --git a/core/libs/tests/GuestAddressConversion.cpp b/core/libs/tests/GuestAddressConversion.cpp new file mode 100644 index 000000000..458ec1b13 --- /dev/null +++ b/core/libs/tests/GuestAddressConversion.cpp @@ -0,0 +1,34 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include + +extern "C" { +int APS5_VABI __inet_pton_nid_postfix(int, const char*, void*); +const char* APS5_VABI __inet_ntop_nid_postfix(int, const void*, char*, std::uint32_t); +int* APS5_VABI __error_nid_postfix(); +} +static void Require(bool value) { if (!value) std::abort(); } +int main() { + for (int family : {2, 28}) { + const char* input = family == 2 ? "192.0.2.17" : "2001:db8::17"; + std::array address{}; + Require(__inet_pton_nid_postfix(family, input, address.data()) == 1); + char output[64]{}; + Require(__inet_ntop_nid_postfix(family, address.data(), output, sizeof(output)) == output); + Require(std::strcmp(input, output) == 0); + output[0] = 'x'; + Require(__inet_ntop_nid_postfix(family, address.data(), output, 1) == nullptr); + Require(*__error_nid_postfix() == 28 && output[0] == 'x'); + const auto original = address; + Require(__inet_pton_nid_postfix(family, "not-an-address", address.data()) == 0); + Require(address == original); + } + std::array address{}; + Require(__inet_pton_nid_postfix(10, "::1", address.data()) == -1); + Require(*__error_nid_postfix() == 47); // Linux AF_INET6 is not PS5 AF_INET6 + Require(__inet_pton_nid_postfix(2, "256.1.2.3", address.data()) == 0); + Require(__inet_pton_nid_postfix(28, "::ffff:192.0.2.17", address.data()) == 1); + Require(address[10] == 255 && address[11] == 255 && address[12] == 192 && address[15] == 17); +} From b42c33ca6c605c80c212099eb40dbe137a69d881 Mon Sep 17 00:00:00 2001 From: SamG-Coder <13449963+SamG-Coder@users.noreply.github.com> Date: Fri, 25 Sep 2026 06:18:52 +1000 Subject: [PATCH 12/94] docs: record error and address conversion compatibility --- docs/YAMAGI_QUAKE2.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/YAMAGI_QUAKE2.md b/docs/YAMAGI_QUAKE2.md index a2283d17b..10d81af65 100644 --- a/docs/YAMAGI_QUAKE2.md +++ b/docs/YAMAGI_QUAKE2.md @@ -142,3 +142,20 @@ addresses and ports, so they do not depend on external DNS. Live DNS and online multiplayer are not validated by these tests. The full compatibility-library build succeeds, and the unchanged game now stops at `RIa6GnWp+iU` (`strerror`). No guest entry-point execution or rendered frame has been observed yet. + +## Follow-up: error reporting and IP conversion + +Added generic `strerror`, POSIX `strerror_r`, and `perror` using guest errno +numbering. Tests cover every defined guest errno value, thread-local message +storage, unknown errors, short buffers, and errno preservation. Messages are +currently English; locale-dependent message translation is not implemented. + +Added `__inet_pton` and `__inet_ntop` with guest IPv4/IPv6 family translation, +network-byte-order buffers, invalid-input handling, and guest error codes. +Tests exercise IPv4, IPv6, IPv4-mapped IPv6, untouched outputs on failure, and +short buffers. All ten CTest suites pass and the full library build succeeds. + +Latest startup stops at `PfccT7qURYE` (`ioctl`). The existing libkernel socket +operations include unimplemented placeholders, so the socket lifecycle needs +review before networking behavior can be considered supported. No gameplay +or guest entry-point execution is established by resolving these imports. From 9cbefb7e0417d450e14d5f195ce6fa1544b28966 Mon Sep 17 00:00:00 2001 From: SamG-Coder <13449963+SamG-Coder@users.noreply.github.com> Date: Fri, 25 Sep 2026 06:23:44 +1000 Subject: [PATCH 13/94] feat(kernel): implement tracked UDP sockets and ioctl --- core/libs/CMakeLists.txt | 10 + core/libs/prx/libkernel/CMakeLists.txt | 1 + core/libs/prx/libkernel/File/src/Stdio.cpp | 15 +- .../Socket/include/SocketRuntime.hpp | 5 + core/libs/prx/libkernel/Socket/src/Socket.cpp | 59 ---- core/libs/prx/libkernel/Socket/src/Udp.cpp | 254 ++++++++++++++++++ core/libs/tests/GuestUdp.cpp | 61 +++++ docs/YAMAGI_QUAKE2.md | 21 ++ 8 files changed, 364 insertions(+), 62 deletions(-) create mode 100644 core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp create mode 100644 core/libs/prx/libkernel/Socket/src/Udp.cpp create mode 100644 core/libs/tests/GuestUdp.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 51aa2d888..eea69a9a3 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -104,6 +104,16 @@ endforeach() add_custom_target(libs DEPENDS ${PATCHED_OUTPUTS}) if(BUILD_TESTING) + add_executable(guest_udp_tests tests/GuestUdp.cpp) + target_include_directories(guest_udp_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_udp_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_udp_tests) + add_test(NAME guest_udp COMMAND guest_udp_tests) + set_tests_properties(guest_udp PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_udp PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_executable(guest_address_tests tests/GuestAddressConversion.cpp) target_include_directories(guest_address_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_address_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 6467aebce..a734c3feb 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -25,6 +25,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Tsd.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Sem.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Socket/src/Socket.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Socket/src/Udp.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Socket/src/AddressConversion.cpp ${CMAKE_CURRENT_SOURCE_DIR}/EventFlag/src/EventFlag.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Module/src/Module.cpp diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index f3dc9fa59..714948a58 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -2,6 +2,12 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libkernel/Socket/include/SocketRuntime.hpp" +#ifdef _WIN32 +#include +#else +#include +#endif extern "C" { @@ -13,9 +19,12 @@ int APS5_VABI chmod_nid_postfix(const char* path, int mode) { } int APS5_VABI close_nid_postfix(int d) { - (void)d; - NotImplemented_nid_no_patch(__func__); - return 0; + if (d >= GuestSockets::FirstDescriptor) return GuestSockets::Close(d); +#ifdef _WIN32 + return _close(d); +#else + return ::close(d); +#endif } int APS5_VABI flock_nid_postfix(int d, int operation) { diff --git a/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp b/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp new file mode 100644 index 000000000..ee0899030 --- /dev/null +++ b/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp @@ -0,0 +1,5 @@ +#pragma once +namespace GuestSockets { +constexpr int FirstDescriptor = 0x10000000; +int Close(int descriptor); +} diff --git a/core/libs/prx/libkernel/Socket/src/Socket.cpp b/core/libs/prx/libkernel/Socket/src/Socket.cpp index 40ad59a0a..89b612603 100644 --- a/core/libs/prx/libkernel/Socket/src/Socket.cpp +++ b/core/libs/prx/libkernel/Socket/src/Socket.cpp @@ -13,13 +13,6 @@ int APS5_VABI accept_nid_postfix(int s, void* addr, uint32_t* addrlen) { return 0; } -int APS5_VABI bind_nid_postfix(int s, const void* addr, uint32_t addrlen) { - (void)s; - (void)addr; - (void)addrlen; - NotImplemented_nid_no_patch(__func__); - return 0; -} int APS5_VABI connect_nid_postfix(int s, const void* addr, uint32_t addrlen) { (void)s; @@ -36,41 +29,9 @@ int APS5_VABI listen_nid_postfix(int s, int backlog) { return 0; } -int APS5_VABI socket_nid_postfix(int family, int type, int protocol) { - (void)family; - (void)type; - (void)protocol; - NotImplemented_nid_no_patch(__func__); - return 0; -} -int APS5_VABI getsockname_nid_postfix(int s, void* addr, uint32_t* addrlen) { - (void)s; - (void)addr; - (void)addrlen; - NotImplemented_nid_no_patch(__func__); - return 0; -} -int APS5_VABI getsockopt_nid_postfix(int s, int level, int optname, void* optval, uint32_t* optlen) { - (void)s; - (void)level; - (void)optname; - (void)optval; - (void)optlen; - NotImplemented_nid_no_patch(__func__); - return 0; -} -int APS5_VABI setsockopt_nid_postfix(int s, int level, int optname, const void* optval, uint32_t optlen) { - (void)s; - (void)level; - (void)optname; - (void)optval; - (void)optlen; - NotImplemented_nid_no_patch(__func__); - return 0; -} int64_t APS5_VABI send_nid_postfix(int s, const void* buf, uint64_t len, int flags) { (void)s; @@ -81,16 +42,6 @@ int64_t APS5_VABI send_nid_postfix(int s, const void* buf, uint64_t len, int fla return 0; } -int64_t APS5_VABI sendto_nid_postfix(int s, const void* buf, uint64_t len, int flags, const void* addr, uint32_t addrlen) { - (void)s; - (void)buf; - (void)len; - (void)flags; - (void)addr; - (void)addrlen; - NotImplemented_nid_no_patch(__func__); - return 0; -} int64_t APS5_VABI recv_nid_postfix(int s, void* buf, uint64_t len, int flags) { (void)s; @@ -101,16 +52,6 @@ int64_t APS5_VABI recv_nid_postfix(int s, void* buf, uint64_t len, int flags) { return 0; } -int64_t APS5_VABI recvfrom_nid_postfix(int s, void* buf, uint64_t len, int flags, void* addr, uint32_t* addrlen) { - (void)s; - (void)buf; - (void)len; - (void)flags; - (void)addr; - (void)addrlen; - NotImplemented_nid_no_patch(__func__); - return 0; -} const char* APS5_VABI inet_ntop_nid_postfix(int af, const void* src, char* dst, uint32_t size) { (void)af; diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp new file mode 100644 index 000000000..8227779f1 --- /dev/null +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -0,0 +1,254 @@ +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#include +#else +#include +#include +#include +#include +#include +#endif +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/Socket/include/SocketRuntime.hpp" +#include +#include +#include +#include +#include +#include +#include + +extern "C" int* APS5_VABI __error_nid_postfix(); +namespace { +#ifdef _WIN32 +using NativeSocket = SOCKET; +constexpr auto Invalid = INVALID_SOCKET; +#else +using NativeSocket = int; +constexpr auto Invalid = -1; +#endif +int Fail(int error) { *__error_nid_postfix() = error; return -1; } +int NativeError() { +#ifdef _WIN32 + switch (WSAGetLastError()) { + case WSAEWOULDBLOCK: return 35; + case WSAEADDRINUSE: return 48; + case WSAEADDRNOTAVAIL: return 49; + case WSAEACCES: return 13; + case WSAEMSGSIZE: return 40; + case WSAENETUNREACH: return 51; + case WSAECONNRESET: return 54; + case WSAENOBUFS: return 55; + case WSAETIMEDOUT: return 60; + case WSAECONNREFUSED: return 61; + case WSAEINTR: return 4; + case WSAEINVAL: return 22; + default: return 5; + } +#else + switch (errno) { + case EAGAIN: return 35; + case EADDRINUSE: return 48; + case EADDRNOTAVAIL: return 49; + case EMSGSIZE: return 40; + case ENETUNREACH: return 51; + case ECONNREFUSED: return 61; + default: return 5; + } +#endif +} +struct Socket { + NativeSocket value; + explicit Socket(NativeSocket value) : value(value) {} + ~Socket() { + if (value == Invalid) return; +#ifdef _WIN32 + closesocket(value); +#else + ::close(value); +#endif + } +}; +std::mutex socketsMutex; +std::map> sockets; +int nextDescriptor = GuestSockets::FirstDescriptor; +std::shared_ptr Lookup(int descriptor) { + std::lock_guard lock(socketsMutex); + const auto found = sockets.find(descriptor); + if (found != sockets.end()) return found->second; + Fail(9); + return {}; +} +int Option(int guest) { + switch (guest) { + case 0x4: return SO_REUSEADDR; + case 0x20: return SO_BROADCAST; + case 0x1001: return SO_SNDBUF; + case 0x1002: return SO_RCVBUF; + default: return -1; + } +} +bool Address(const void* input, std::uint32_t length, sockaddr_storage& native, socklen_t& size) { + if (!input || length < 2) { Fail(14); return false; } + const auto* bytes = static_cast(input); + if (bytes[1] == 2 && length >= 16 && bytes[0] == 16) { + auto& v4 = reinterpret_cast(native); + v4.sin_family = AF_INET; + std::memcpy(&v4.sin_port, bytes + 2, 2); + std::memcpy(&v4.sin_addr, bytes + 4, 4); + size = sizeof(v4); + return true; + } + if (bytes[1] == 28 && length >= 28 && bytes[0] == 28) { + auto& v6 = reinterpret_cast(native); + v6.sin6_family = AF_INET6; + std::memcpy(&v6.sin6_port, bytes + 2, 2); + std::memcpy(&v6.sin6_flowinfo, bytes + 4, 4); + std::memcpy(&v6.sin6_addr, bytes + 8, 16); + std::memcpy(&v6.sin6_scope_id, bytes + 24, 4); + size = sizeof(v6); + return true; + } + Fail(47); + return false; +} +void GuestAddress(const sockaddr_storage& native, void* output, std::uint32_t* length) { + unsigned char bytes[28]{}; + bytes[0] = native.ss_family == AF_INET ? 16 : 28; + bytes[1] = native.ss_family == AF_INET ? 2 : 28; + if (native.ss_family == AF_INET) { + const auto& v4 = reinterpret_cast(native); + std::memcpy(bytes + 2, &v4.sin_port, 2); + std::memcpy(bytes + 4, &v4.sin_addr, 4); + } else { + const auto& v6 = reinterpret_cast(native); + std::memcpy(bytes + 2, &v6.sin6_port, 2); + std::memcpy(bytes + 4, &v6.sin6_flowinfo, 4); + std::memcpy(bytes + 8, &v6.sin6_addr, 16); + std::memcpy(bytes + 24, &v6.sin6_scope_id, 4); + } + std::memcpy(output, bytes, std::min(*length, bytes[0])); + *length = bytes[0]; +} +} + +int GuestSockets::Close(int descriptor) { + std::lock_guard lock(socketsMutex); + return sockets.erase(descriptor) ? 0 : Fail(9); +} + +extern "C" { +int APS5_VABI setsockopt_nid_postfix(int descriptor, int level, int option, + const void* value, std::uint32_t length) { + const auto socket = Lookup(descriptor); + if (!socket) return -1; + const int nativeOption = Option(option); + if (level != 0xffff || nativeOption == -1) return Fail(42); + if (!value || length != sizeof(int)) return Fail(22); + return ::setsockopt(socket->value, SOL_SOCKET, nativeOption, + static_cast(value), sizeof(int)) ? Fail(NativeError()) : 0; +} +int APS5_VABI getsockopt_nid_postfix(int descriptor, int level, int option, + void* value, std::uint32_t* length) { + const auto socket = Lookup(descriptor); + if (!socket) return -1; + const int nativeOption = Option(option); + if (level != 0xffff || nativeOption == -1) return Fail(42); + if (!value || !length || *length < sizeof(int)) return Fail(22); + int result = 0; + socklen_t size = sizeof(result); + if (::getsockopt(socket->value, SOL_SOCKET, nativeOption, reinterpret_cast(&result), &size)) + return Fail(NativeError()); + std::memcpy(value, &result, sizeof(result)); + *length = sizeof(result); + return 0; +} +int APS5_VABI socket_nid_postfix(int family, int type, int protocol) { + if (family != 2 && family != 28) return Fail(47); + if (type != 2 || (protocol != 0 && protocol != 17)) return Fail(43); +#ifdef _WIN32 + static const int startup = [] { WSADATA data{}; return WSAStartup(MAKEWORD(2, 2), &data); }(); + if (startup) return Fail(5); +#endif + const auto native = ::socket(family == 2 ? AF_INET : AF_INET6, SOCK_DGRAM, protocol); + if (native == Invalid) return Fail(NativeError()); + Socket guard(native); + try { + auto socket = std::make_shared(native); + guard.value = Invalid; + std::lock_guard lock(socketsMutex); + if (nextDescriptor == INT_MAX) return Fail(24); + const int descriptor = nextDescriptor++; + sockets.emplace(descriptor, std::move(socket)); + return descriptor; + } catch (const std::bad_alloc&) { + return Fail(12); + } +} +int APS5_VABI bind_nid_postfix(int descriptor, const void* address, std::uint32_t length) { + const auto socket = Lookup(descriptor); + if (!socket) return -1; + sockaddr_storage native{}; + socklen_t size; + if (!Address(address, length, native, size)) return -1; + return ::bind(socket->value, reinterpret_cast(&native), size) ? Fail(NativeError()) : 0; +} +int APS5_VABI getsockname_nid_postfix(int descriptor, void* address, std::uint32_t* length) { + const auto socket = Lookup(descriptor); + if (!socket) return -1; + if (!address || !length) return Fail(14); + sockaddr_storage native{}; + socklen_t size = sizeof(native); + if (::getsockname(socket->value, reinterpret_cast(&native), &size)) return Fail(NativeError()); + GuestAddress(native, address, length); + return 0; +} +int APS5_VABI ioctl_nid_postfix(int descriptor, std::uint64_t request, void* argument) { + const auto socket = Lookup(descriptor); + if (!socket) return -1; + if (!argument) return Fail(14); + if (request != 0x8004667e && request != 0x4004667f) return Fail(25); + unsigned long value = 0; + if (request == 0x8004667e) value = *static_cast(argument) != 0; +#ifdef _WIN32 + const auto result = ioctlsocket(socket->value, request == 0x8004667e ? FIONBIO : FIONREAD, &value); +#else + int nativeValue = static_cast(value); + const auto result = ::ioctl(socket->value, request == 0x8004667e ? FIONBIO : FIONREAD, &nativeValue); + value = static_cast(nativeValue); +#endif + if (result) return Fail(NativeError()); + if (request == 0x4004667f) *static_cast(argument) = static_cast(value); + return 0; +} +std::int64_t APS5_VABI sendto_nid_postfix(int descriptor, const void* buffer, std::uint64_t length, + int flags, const void* address, std::uint32_t addressLength) { + const auto socket = Lookup(descriptor); + if (!socket) return -1; + if (flags != 0) return Fail(45); + if (length > INT_MAX) return Fail(40); + if (!buffer && length) return Fail(14); + sockaddr_storage native{}; + socklen_t size; + if (!Address(address, addressLength, native, size)) return -1; + const auto result = ::sendto(socket->value, static_cast(buffer), static_cast(length), 0, + reinterpret_cast(&native), size); + return result < 0 ? Fail(NativeError()) : result; +} +std::int64_t APS5_VABI recvfrom_nid_postfix(int descriptor, void* buffer, std::uint64_t length, + int flags, void* address, std::uint32_t* addressLength) { + const auto socket = Lookup(descriptor); + if (!socket) return -1; + if ((flags & ~2) != 0) return Fail(45); + if (length > INT_MAX) return Fail(40); + if ((!buffer && length) || (address && !addressLength)) return Fail(14); + sockaddr_storage native{}; + socklen_t size = sizeof(native); + const auto result = ::recvfrom(socket->value, static_cast(buffer), static_cast(length), + flags & 2 ? MSG_PEEK : 0, reinterpret_cast(&native), &size); + if (result < 0) return Fail(NativeError()); + if (address) GuestAddress(native, address, addressLength); + return result; +} +} diff --git a/core/libs/tests/GuestUdp.cpp b/core/libs/tests/GuestUdp.cpp new file mode 100644 index 000000000..90d42c810 --- /dev/null +++ b/core/libs/tests/GuestUdp.cpp @@ -0,0 +1,61 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +extern "C" { +int APS5_VABI socket_nid_postfix(int, int, int); +int APS5_VABI setsockopt_nid_postfix(int, int, int, const void*, std::uint32_t); +int APS5_VABI getsockopt_nid_postfix(int, int, int, void*, std::uint32_t*); +int APS5_VABI bind_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI getsockname_nid_postfix(int, void*, std::uint32_t*); +int APS5_VABI ioctl_nid_postfix(int, std::uint64_t, void*); +std::int64_t APS5_VABI sendto_nid_postfix(int, const void*, std::uint64_t, int, const void*, std::uint32_t); +std::int64_t APS5_VABI recvfrom_nid_postfix(int, void*, std::uint64_t, int, void*, std::uint32_t*); +int APS5_VABI close_nid_postfix(int); +int* APS5_VABI __error_nid_postfix(); +} +static void Require(bool value) { if (!value) std::abort(); } +int main() { + const int receiver = socket_nid_postfix(2, 2, 0); + const int sender = socket_nid_postfix(2, 2, 17); + Require(receiver >= 0 && sender >= 0 && receiver != sender); + std::array destination{16, 2, 0, 0, 127, 0, 0, 1}; + Require(bind_nid_postfix(receiver, destination.data(), destination.size()) == 0); + std::uint32_t size = destination.size(); + Require(getsockname_nid_postfix(receiver, destination.data(), &size) == 0 && size == 16); + Require(destination[2] || destination[3]); + int enabled = 1; + Require(setsockopt_nid_postfix(sender, 0xffff, 0x20, &enabled, sizeof(enabled)) == 0); + int option = 0; + std::uint32_t optionSize = sizeof(option); + Require(getsockopt_nid_postfix(sender, 0xffff, 0x20, &option, &optionSize) == 0 && option != 0); + Require(setsockopt_nid_postfix(sender, 0xffff, 12345, &enabled, sizeof(enabled)) == -1); + Require(*__error_nid_postfix() == 42); + Require(ioctl_nid_postfix(receiver, 0x8004667e, &enabled) == 0); + char buffer[64]{}; + Require(recvfrom_nid_postfix(receiver, buffer, sizeof(buffer), 0, nullptr, nullptr) == -1); + Require(*__error_nid_postfix() == 35); + const char message[] = "guest UDP loopback"; + Require(sendto_nid_postfix(sender, message, sizeof(message), 0, destination.data(), destination.size()) == sizeof(message)); + int queued = 0; + for (int i = 0; i < 100 && queued == 0; ++i) { + Require(ioctl_nid_postfix(receiver, 0x4004667f, &queued) == 0); + if (!queued) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + Require(queued == sizeof(message)); + Require(recvfrom_nid_postfix(receiver, buffer, sizeof(buffer), 2, nullptr, nullptr) == sizeof(message)); + std::array source{}; + size = source.size(); + Require(recvfrom_nid_postfix(receiver, buffer, sizeof(buffer), 0, source.data(), &size) == sizeof(message)); + Require(std::strcmp(buffer, message) == 0 && source[1] == 2 && source[4] == 127); + Require(ioctl_nid_postfix(receiver, 0x4004667f, &queued) == 0 && queued == 0); + Require(ioctl_nid_postfix(receiver, 123, &queued) == -1 && *__error_nid_postfix() == 25); + Require(close_nid_postfix(receiver) == 0); + Require(close_nid_postfix(receiver) == -1 && *__error_nid_postfix() == 9); + Require(ioctl_nid_postfix(receiver, 0x4004667f, &queued) == -1); + Require(close_nid_postfix(sender) == 0); + Require(socket_nid_postfix(2, 1, 0) == -1); // unsupported TCP must not appear to work +} diff --git a/docs/YAMAGI_QUAKE2.md b/docs/YAMAGI_QUAKE2.md index 10d81af65..b3ee9745a 100644 --- a/docs/YAMAGI_QUAKE2.md +++ b/docs/YAMAGI_QUAKE2.md @@ -159,3 +159,24 @@ Latest startup stops at `PfccT7qURYE` (`ioctl`). The existing libkernel socket operations include unimplemented placeholders, so the socket lifecycle needs review before networking behavior can be considered supported. No gameplay or guest entry-point execution is established by resolving these imports. + +## Follow-up: tracked UDP sockets + +Replaced the UDP-path placeholders with tracked guest descriptors, IPv4/IPv6 +address conversion, bind, getsockname, sendto, recvfrom, and close. Added guest +FIONBIO/FIONREAD ioctl translation and integer socket options for reuse-address, +broadcast, send-buffer size, and receive-buffer size. Descriptors occupy a +separate range from host CRT file descriptors and are not reused. In-flight +operations retain native socket ownership while guest close removes the handle. +Allocation failure closes the native socket and reports guest ENOMEM. + +The new loopback test sends and receives a real IPv4 UDP datagram through guest +exports, checks nonblocking EAGAIN, queued-byte counts, MSG_PEEK, broadcast +options, and closed-descriptor rejection. All eleven suites pass. IPv6 socket +address translation is implemented but this UDP test exercises IPv4 only. +TCP, select, connected send/recv, and unimplemented options remain unsupported; +online multiplayer is not validated. ioctl currently accepts tracked sockets +only. POSIX close also closes ordinary host CRT descriptors. + +The unchanged game resolves ioctl and now stops at `mkawd0NA9ts` (`sysconf`), +before guest entry-point execution. No rendered frame has been observed. From cbd22dd854bdc158d65b41e5d618257e6652b357 Mon Sep 17 00:00:00 2001 From: SamG-Coder <13449963+SamG-Coder@users.noreply.github.com> Date: Fri, 25 Sep 2026 06:25:16 +1000 Subject: [PATCH 14/94] feat(kernel): implement guest system configuration queries --- core/libs/CMakeLists.txt | 9 ++++ core/libs/prx/libkernel/CMakeLists.txt | 1 + .../libs/prx/libkernel/System/src/Process.cpp | 4 +- .../System/src/SystemConfiguration.cpp | 51 +++++++++++++++++++ core/libs/tests/GuestSystemConfiguration.cpp | 21 ++++++++ 5 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 core/libs/prx/libkernel/System/src/SystemConfiguration.cpp create mode 100644 core/libs/tests/GuestSystemConfiguration.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index eea69a9a3..30154d47e 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -104,6 +104,15 @@ endforeach() add_custom_target(libs DEPENDS ${PATCHED_OUTPUTS}) if(BUILD_TESTING) + add_executable(guest_system_configuration_tests tests/GuestSystemConfiguration.cpp) + target_include_directories(guest_system_configuration_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_system_configuration_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_system_configuration_tests) + add_test(NAME guest_system_configuration COMMAND guest_system_configuration_tests) + if(WIN32) + set_tests_properties(guest_system_configuration PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_executable(guest_udp_tests tests/GuestUdp.cpp) target_include_directories(guest_udp_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_udp_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index a734c3feb..67b598bbb 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}/System/src/Sanitizer.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/Rtld.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/Process.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/System/src/SystemConfiguration.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Time/Time.cpp ${CMAKE_CURRENT_SOURCE_DIR}/AppMetadata/src/AppMetadata.cpp ${CMAKE_CURRENT_SOURCE_DIR}/AppMetadata/src/ParamJsonParser.cpp diff --git a/core/libs/prx/libkernel/System/src/Process.cpp b/core/libs/prx/libkernel/System/src/Process.cpp index a9d28ee5e..821e2cff3 100644 --- a/core/libs/prx/libkernel/System/src/Process.cpp +++ b/core/libs/prx/libkernel/System/src/Process.cpp @@ -2,6 +2,7 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libkernel/DirectMemory/DirectMemory.hpp" extern "C" { @@ -16,8 +17,7 @@ const char** APS5_VABI getargv_nid_postfix(void) { } int APS5_VABI getpagesize_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return PS5_PAGE_SIZE; } int APS5_VABI getpid_nid_postfix(void) { diff --git a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp new file mode 100644 index 000000000..b5a5e777b --- /dev/null +++ b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp @@ -0,0 +1,51 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/DirectMemory/DirectMemory.hpp" +#include +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#else +#include +#endif + +extern "C" int* APS5_VABI __error_nid_postfix(); + +extern "C" { +// Guest long is 64 bits, including when the Windows host long is 32 bits. +std::int64_t APS5_VABI sysconf_nid_postfix(int name) { + const int saved = *__error_nid_postfix(); + std::int64_t result = -1; + switch (name) { + case 47: // _SC_PAGESIZE + result = PS5_PAGE_SIZE; + break; + case 57: // _SC_NPROCESSORS_CONF + case 58: // _SC_NPROCESSORS_ONLN +#ifdef _WIN32 + result = GetActiveProcessorCount(ALL_PROCESSOR_GROUPS); +#else + result = ::sysconf(name == 57 ? _SC_NPROCESSORS_CONF : _SC_NPROCESSORS_ONLN); +#endif + break; + case 121: { // _SC_PHYS_PAGES: report host physical capacity in guest pages +#ifdef _WIN32 + MEMORYSTATUSEX memory{}; + memory.dwLength = sizeof(memory); + if (GlobalMemoryStatusEx(&memory)) result = memory.ullTotalPhys / PS5_PAGE_SIZE; +#else + const auto pages = ::sysconf(_SC_PHYS_PAGES); + const auto pageSize = ::sysconf(_SC_PAGESIZE); + if (pages > 0 && pageSize > 0) + result = static_cast(pages) * pageSize / PS5_PAGE_SIZE; +#endif + break; + } + default: + *__error_nid_postfix() = 22; + return -1; + } + if (result <= 0) { *__error_nid_postfix() = 5; return -1; } + *__error_nid_postfix() = saved; + return result; +} +} diff --git a/core/libs/tests/GuestSystemConfiguration.cpp b/core/libs/tests/GuestSystemConfiguration.cpp new file mode 100644 index 000000000..be698e8b5 --- /dev/null +++ b/core/libs/tests/GuestSystemConfiguration.cpp @@ -0,0 +1,21 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +extern "C" { +std::int64_t APS5_VABI sysconf_nid_postfix(int); +int APS5_VABI getpagesize_nid_postfix(); +int* APS5_VABI __error_nid_postfix(); +} +static void Require(bool value) { if (!value) std::abort(); } +int main() { + *__error_nid_postfix() = 13; + Require(sysconf_nid_postfix(47) == 0x4000); + Require(getpagesize_nid_postfix() == sysconf_nid_postfix(47)); + Require(sysconf_nid_postfix(57) > 0); + Require(sysconf_nid_postfix(58) > 0); + Require(sysconf_nid_postfix(121) > 0); + Require(*__error_nid_postfix() == 13); + Require(sysconf_nid_postfix(-1) == -1); // verifies full-width signed return + Require(*__error_nid_postfix() == 22); + Require(sysconf_nid_postfix(0x7fffffff) == -1); +} From 56c7a589dbcb8b424c1b339e07b8c59ba98552a5 Mon Sep 17 00:00:00 2001 From: SamG-Coder <13449963+SamG-Coder@users.noreply.github.com> Date: Fri, 25 Sep 2026 06:27:11 +1000 Subject: [PATCH 15/94] feat(kernel): bridge guest signal registration and delivery --- core/libs/CMakeLists.txt | 9 +++ core/libs/prx/libkernel/CMakeLists.txt | 1 + .../libs/prx/libkernel/System/src/Signals.cpp | 60 +++++++++++++++++++ core/libs/tests/GuestSignals.cpp | 28 +++++++++ docs/YAMAGI_QUAKE2.md | 20 +++++++ 5 files changed, 118 insertions(+) create mode 100644 core/libs/prx/libkernel/System/src/Signals.cpp create mode 100644 core/libs/tests/GuestSignals.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 30154d47e..fb24a39c4 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -104,6 +104,15 @@ endforeach() add_custom_target(libs DEPENDS ${PATCHED_OUTPUTS}) if(BUILD_TESTING) + add_executable(guest_signal_tests tests/GuestSignals.cpp) + target_include_directories(guest_signal_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_signal_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_signal_tests) + add_test(NAME guest_signals COMMAND guest_signal_tests) + if(WIN32) + set_tests_properties(guest_signals PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_executable(guest_system_configuration_tests tests/GuestSystemConfiguration.cpp) target_include_directories(guest_system_configuration_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_system_configuration_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 67b598bbb..2a8d35118 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -36,6 +36,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/System/src/Rtld.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/Process.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/SystemConfiguration.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/System/src/Signals.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Time/Time.cpp ${CMAKE_CURRENT_SOURCE_DIR}/AppMetadata/src/AppMetadata.cpp ${CMAKE_CURRENT_SOURCE_DIR}/AppMetadata/src/ParamJsonParser.cpp diff --git a/core/libs/prx/libkernel/System/src/Signals.cpp b/core/libs/prx/libkernel/System/src/Signals.cpp new file mode 100644 index 000000000..c662d4858 --- /dev/null +++ b/core/libs/prx/libkernel/System/src/Signals.cpp @@ -0,0 +1,60 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include + +extern "C" int* APS5_VABI __error_nid_postfix(); +namespace { +using GuestHandler = void (APS5_VABI *)(int); +std::atomic handlers[32]{}; +static_assert(std::atomic::is_always_lock_free); +std::mutex registration; +int NativeSignal(int guest) { + switch (guest) { + case 2: return SIGINT; + case 4: return SIGILL; + case 6: return SIGABRT; + case 8: return SIGFPE; + case 11: return SIGSEGV; + case 15: return SIGTERM; + default: return 0; + } +} +void Dispatch(int native) { + int guest = 0; + for (int candidate : {2, 4, 6, 8, 11, 15}) + if (NativeSignal(candidate) == native) { guest = candidate; break; } + if (!guest) return; +#ifdef _WIN32 + // Preserve the guest's persistent registration across CRT delivery. + std::signal(native, Dispatch); +#endif + const auto callback = handlers[guest].load(); + if (reinterpret_cast(callback) > 1) callback(guest); +} +} +extern "C" { +GuestHandler APS5_VABI signal_nid_postfix(int guest, GuestHandler handler) { + const auto invalid = reinterpret_cast(static_cast(-1)); + const int native = NativeSignal(guest); + if (!native || handler == invalid) { *__error_nid_postfix() = 22; return invalid; } + std::lock_guard lock(registration); + const auto previous = handlers[guest].exchange(handler); + const auto address = reinterpret_cast(handler); + auto hostHandler = address == 0 ? SIG_DFL : address == 1 ? SIG_IGN : Dispatch; + if (std::signal(native, hostHandler) == SIG_ERR) { + handlers[guest].store(previous); + *__error_nid_postfix() = 22; + return invalid; + } + return previous; +} +int APS5_VABI raise_nid_postfix(int guest) { + const int native = NativeSignal(guest); + if (!native) { *__error_nid_postfix() = 22; return -1; } + const int result = std::raise(native); + if (result) *__error_nid_postfix() = 22; + return result ? -1 : 0; +} +} diff --git a/core/libs/tests/GuestSignals.cpp b/core/libs/tests/GuestSignals.cpp new file mode 100644 index 000000000..9d3ba7afe --- /dev/null +++ b/core/libs/tests/GuestSignals.cpp @@ -0,0 +1,28 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +using Handler = void (APS5_VABI *)(int); +extern "C" { +Handler APS5_VABI signal_nid_postfix(int, Handler); +int APS5_VABI raise_nid_postfix(int); +int* APS5_VABI __error_nid_postfix(); +} +volatile std::sig_atomic_t received = 0; +void APS5_VABI Callback(int value) { received = value; } +static void Require(bool value) { if (!value) std::abort(); } +int main() { + const auto invalid = reinterpret_cast(static_cast(-1)); + const auto ignore = reinterpret_cast(std::uintptr_t{1}); + Require(signal_nid_postfix(9, Callback) == invalid); + Require(*__error_nid_postfix() == 22); + Require(signal_nid_postfix(15, Callback) != invalid); + Require(raise_nid_postfix(15) == 0 && received == 15); + received = 0; + Require(raise_nid_postfix(15) == 0 && received == 15); + Require(signal_nid_postfix(15, ignore) != invalid); + received = 0; + Require(raise_nid_postfix(15) == 0 && received == 0); + Require(signal_nid_postfix(15, nullptr) == ignore); + Require(raise_nid_postfix(100) == -1 && *__error_nid_postfix() == 22); +} diff --git a/docs/YAMAGI_QUAKE2.md b/docs/YAMAGI_QUAKE2.md index b3ee9745a..ef5f22b83 100644 --- a/docs/YAMAGI_QUAKE2.md +++ b/docs/YAMAGI_QUAKE2.md @@ -180,3 +180,23 @@ only. POSIX close also closes ordinary host CRT descriptors. The unchanged game resolves ioctl and now stops at `mkawd0NA9ts` (`sysconf`), before guest entry-point execution. No rendered frame has been observed. + +## Follow-up: configuration queries and signal callbacks + +Added sysconf for guest page size, configured/online processor counts, and +physical page count. The return type is explicitly 64-bit for the guest ABI. +Memory capacity is reported in 16 KiB guest pages; CPU and capacity values +reflect the executing host. Unsupported queries return -1 with guest EINVAL. +The previous getpagesize placeholder now returns the same guest page size. + +Added signal/raise bridges for SIGINT, SIGILL, SIGABRT, SIGFPE, SIGSEGV, and +SIGTERM. Host callbacks dispatch through the guest SysV calling convention; +default/ignore handlers and guest-to-host signal numbers are translated. +Tests cover repeated explicit SIGTERM delivery, ignore/default restoration, +and invalid signals. This does not validate hardware-fault recovery, signal +masks, sigaction, or complete asynchronous POSIX semantics. + +All thirteen suites pass. Latest game startup resolves these imports and stops +at `zqJhBxAKfsc` (`__stdoutp`). The next work requires the guest FILE layout and +standard stream exports; mapping a host FILE pointer directly would be invalid. +The game still has not executed its entry point or rendered a frame. From e8ecb29170fd03d2f0a392e9a5395080fd038f7b Mon Sep 17 00:00:00 2001 From: SamG-Coder <13449963+SamG-Coder@users.noreply.github.com> Date: Fri, 25 Sep 2026 06:30:30 +1000 Subject: [PATCH 16/94] feat(libc): expose guest standard streams and character IO --- core/libs/CMakeLists.txt | 9 +++ core/libs/prx/libc/CMakeLists.txt | 1 + core/libs/prx/libc/include/FileStream.hpp | 44 ++++++++++++++ core/libs/prx/libc/src/FileAndHeap.cpp | 3 + core/libs/prx/libc/src/StandardStreams.cpp | 68 ++++++++++++++++++++++ core/libs/tests/GuestStandardStreams.cpp | 49 ++++++++++++++++ docs/YAMAGI_QUAKE2.md | 17 ++++++ 7 files changed, 191 insertions(+) create mode 100644 core/libs/prx/libc/src/StandardStreams.cpp create mode 100644 core/libs/tests/GuestStandardStreams.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index fb24a39c4..6d96dd95a 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -104,6 +104,15 @@ endforeach() add_custom_target(libs DEPENDS ${PATCHED_OUTPUTS}) if(BUILD_TESTING) + add_executable(guest_standard_stream_tests tests/GuestStandardStreams.cpp) + target_include_directories(guest_standard_stream_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_standard_stream_tests PRIVATE libc) + configure_windows_unwind(guest_standard_stream_tests) + add_test(NAME guest_standard_streams COMMAND guest_standard_stream_tests) + if(WIN32) + set_tests_properties(guest_standard_streams PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_executable(guest_signal_tests tests/GuestSignals.cpp) target_include_directories(guest_signal_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_signal_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index 574558de6..d3516042f 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -8,6 +8,7 @@ target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/Math.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Time.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/FileAndHeap.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/StandardStreams.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/GuestAllocations.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/GuestHeap.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Mspace.cpp diff --git a/core/libs/prx/libc/include/FileStream.hpp b/core/libs/prx/libc/include/FileStream.hpp index 40289ad23..5df643c16 100644 --- a/core/libs/prx/libc/include/FileStream.hpp +++ b/core/libs/prx/libc/include/FileStream.hpp @@ -4,6 +4,28 @@ #include #include #include +#include +#include +#include +#ifdef _WIN32 +#include +#endif + +struct GuestFilePrefix { + unsigned char* position = nullptr; + std::int32_t readRemaining = 0; + std::int32_t writeRemaining = 0; + std::int16_t flags = 0; + std::int16_t descriptor = -1; + unsigned char* buffer = nullptr; + std::int32_t bufferSize = 0; + std::int32_t bufferPadding = 0; + std::int32_t lineBufferSize = 0; +}; +static_assert(offsetof(GuestFilePrefix, flags) == 16); +static_assert(offsetof(GuestFilePrefix, descriptor) == 18); +static_assert(offsetof(GuestFilePrefix, buffer) == 24); +static_assert(offsetof(GuestFilePrefix, lineBufferSize) == 40); static constexpr const char* FOPEN_EXT_VERT = ".vert"; static constexpr const char* FOPEN_MSG_NULL_ARG = "null argument"; @@ -11,12 +33,23 @@ static constexpr const char* FOPEN_MSG_NOT_FOUND = "file not found"; static constexpr const char* FOPEN_MSG_OPEN_FAILED = "open failed"; class FileStream { + // Only the macro-accessed FreeBSD prefix is exposed. The remaining guest + // FILE area is reserved; host state lives beyond it and is never a guest FILE*. + GuestFilePrefix _guest{}; + std::byte _reserved[256 - sizeof(GuestFilePrefix)]{}; std::FILE* _handle; bool _dynamic; public: explicit FileStream(std::FILE* handle, bool dynamic = false) : _handle(handle), _dynamic(dynamic) { if (!_handle) throw std::runtime_error("FileStream: null handle"); + _guest.flags = handle == stdin ? 4 : handle == stdout || handle == stderr ? 8 : 0x10; +#ifdef _WIN32 + const int descriptor = _fileno(handle); +#else + const int descriptor = ::fileno(handle); +#endif + _guest.descriptor = descriptor >= 0 && descriptor <= 32767 ? static_cast(descriptor) : -1; } FileStream(const FileStream&) = delete; @@ -31,11 +64,22 @@ public: return _dynamic; } + GuestFilePrefix& GuestState() { return _guest; } + void SyncStatus() { + _guest.readRemaining = 0; + _guest.writeRemaining = 0; + _guest.flags = static_cast((_guest.flags & ~0x60) | + (std::feof(GetHandle()) ? 0x20 : 0) | (std::ferror(GetHandle()) ? 0x40 : 0)); + } + void Close() { GetHandle(); if (std::fclose(std::exchange(_handle, nullptr)) != 0) throw std::runtime_error("FileStream: close failed"); + _guest.flags = 0; + _guest.descriptor = -1; } }; +static_assert(std::is_standard_layout_v); inline std::FILE* GetNativeStream(FileStream* stream) { if (!stream) throw std::runtime_error("FileStream: null stream"); diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index a4785f8e7..adfaff080 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -55,6 +55,7 @@ size_t APS5_VABI fread_nid_postfix(void* buffer, size_t size, size_t count, File if (size == 0 || count == 0) return 0; if (!buffer) throw std::runtime_error("fread: null buffer"); const auto result = std::fread(buffer, size, count, handle); + stream->SyncStatus(); if (std::ferror(handle)) throw std::runtime_error("fread: read failed"); return result; } @@ -64,6 +65,7 @@ size_t APS5_VABI fwrite_nid_postfix(const void* buffer, size_t size, size_t coun if (size == 0 || count == 0) return 0; if (!buffer) throw std::runtime_error("fwrite: null buffer"); const auto result = std::fwrite(buffer, size, count, handle); + stream->SyncStatus(); if (result != count || std::ferror(handle)) throw std::runtime_error("fwrite: write failed"); return result; } @@ -71,6 +73,7 @@ size_t APS5_VABI fwrite_nid_postfix(const void* buffer, size_t size, size_t coun int APS5_VABI fseek_nid_postfix(FileStream* stream, long offset, int origin) { if (origin != SEEK_SET && origin != SEEK_CUR && origin != SEEK_END) throw std::runtime_error("fseek: invalid origin"); if (std::fseek(GetNativeStream(stream), offset, origin) != 0) throw std::runtime_error("fseek: seek failed"); + stream->SyncStatus(); return 0; } diff --git a/core/libs/prx/libc/src/StandardStreams.cpp b/core/libs/prx/libc/src/StandardStreams.cpp new file mode 100644 index 000000000..ac9f10191 --- /dev/null +++ b/core/libs/prx/libc/src/StandardStreams.cpp @@ -0,0 +1,68 @@ +#include "prx/libc/include/FileStream.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#ifdef _WIN32 +#include +#else +#include +#endif + +namespace { FileStream input{stdin}; } +extern "C" { +FileStream* __stdinp_nid_postfix = &input; +FileStream* __stdoutp_nid_postfix = &_Stdout_nid_postfix; +FileStream* __stderrp_nid_postfix = &_Stderr_nid_postfix; +int __isthreaded_nid_postfix = 1; + +int APS5_VABI fgetc_nid_postfix(FileStream* stream) { + const int result = std::fgetc(GetNativeStream(stream)); + stream->SyncStatus(); + return result; +} +int APS5_VABI getc_nid_postfix(FileStream* stream) { return fgetc_nid_postfix(stream); } +int APS5_VABI __srget_nid_postfix(FileStream* stream) { return fgetc_nid_postfix(stream); } +int APS5_VABI getchar_nid_postfix() { return fgetc_nid_postfix(__stdinp_nid_postfix); } +int APS5_VABI fputc_nid_postfix(int value, FileStream* stream) { + const int result = std::fputc(value, GetNativeStream(stream)); + stream->SyncStatus(); + return result; +} +int APS5_VABI putc_nid_postfix(int value, FileStream* stream) { return fputc_nid_postfix(value, stream); } +int APS5_VABI __swbuf_nid_postfix(int value, FileStream* stream) { return fputc_nid_postfix(value, stream); } +int APS5_VABI putchar_nid_postfix(int value) { return fputc_nid_postfix(value, __stdoutp_nid_postfix); } +int APS5_VABI ungetc_nid_postfix(int value, FileStream* stream) { + const int result = std::ungetc(value, GetNativeStream(stream)); + stream->SyncStatus(); + return result; +} +char* APS5_VABI fgets_nid_postfix(char* buffer, int size, FileStream* stream) { + auto* result = std::fgets(buffer, size, GetNativeStream(stream)); + stream->SyncStatus(); + return result; +} +int APS5_VABI feof_nid_postfix(FileStream* stream) { + stream->SyncStatus(); + return (stream->GuestState().flags & 0x20) != 0; +} +int APS5_VABI ferror_nid_postfix(FileStream* stream) { + stream->SyncStatus(); + return (stream->GuestState().flags & 0x40) != 0; +} +void APS5_VABI clearerr_nid_postfix(FileStream* stream) { + std::clearerr(GetNativeStream(stream)); + stream->SyncStatus(); +} +int APS5_VABI fileno_nid_postfix(FileStream* stream) { +#ifdef _WIN32 + const int descriptor = _fileno(GetNativeStream(stream)); +#else + const int descriptor = ::fileno(GetNativeStream(stream)); +#endif + stream->GuestState().descriptor = static_cast(descriptor); + return descriptor; +} +int APS5_VABI setvbuf_nid_postfix(FileStream* stream, char* buffer, int mode, std::size_t size) { + if (mode < 0 || mode > 2) return -1; + const int native = mode == 0 ? _IOFBF : mode == 1 ? _IOLBF : _IONBF; + return std::setvbuf(GetNativeStream(stream), buffer, native, size); +} +} diff --git a/core/libs/tests/GuestStandardStreams.cpp b/core/libs/tests/GuestStandardStreams.cpp new file mode 100644 index 000000000..3ced61b07 --- /dev/null +++ b/core/libs/tests/GuestStandardStreams.cpp @@ -0,0 +1,49 @@ +#include "prx/libc/include/FileStream.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +extern "C" { +extern FileStream* __stdinp_nid_postfix; +extern FileStream* __stdoutp_nid_postfix; +extern FileStream* __stderrp_nid_postfix; +extern int __isthreaded_nid_postfix; +int APS5_VABI fgetc_nid_postfix(FileStream*); +int APS5_VABI fputc_nid_postfix(int, FileStream*); +int APS5_VABI __srget_nid_postfix(FileStream*); +int APS5_VABI __swbuf_nid_postfix(int, FileStream*); +int APS5_VABI ungetc_nid_postfix(int, FileStream*); +char* APS5_VABI fgets_nid_postfix(char*, int, FileStream*); +int APS5_VABI feof_nid_postfix(FileStream*); +int APS5_VABI fileno_nid_postfix(FileStream*); +void APS5_VABI clearerr_nid_postfix(FileStream*); +int APS5_VABI setvbuf_nid_postfix(FileStream*, char*, int, std::size_t); +} +static void Require(bool value) { if (!value) std::abort(); } +int main() { + Require(__isthreaded_nid_postfix == 1); + Require(__stdoutp_nid_postfix == &_Stdout_nid_postfix); + Require(__stderrp_nid_postfix == &_Stderr_nid_postfix); + Require(fileno_nid_postfix(__stdinp_nid_postfix) == 0); + Require(fileno_nid_postfix(__stdoutp_nid_postfix) == 1); + Require(fileno_nid_postfix(__stderrp_nid_postfix) == 2); + FileStream stream(std::tmpfile()); + auto& guest = *reinterpret_cast(&stream); + Require(&guest == &stream.GuestState()); + Require(guest.position == nullptr && guest.readRemaining == 0 && guest.writeRemaining == 0); + Require(guest.descriptor == fileno_nid_postfix(&stream)); + Require(setvbuf_nid_postfix(&stream, nullptr, 2, 0) == 0); + Require(fputc_nid_postfix('A', &stream) == 'A'); + Require(--guest.writeRemaining < 0 && __swbuf_nid_postfix('\n', &stream) == '\n'); + std::rewind(stream.GetHandle()); + Require(--guest.readRemaining < 0 && __srget_nid_postfix(&stream) == 'A'); + Require(ungetc_nid_postfix('B', &stream) == 'B'); + char text[8]{}; + Require(fgets_nid_postfix(text, sizeof(text), &stream) == text); + Require(std::strcmp(text, "B\n") == 0); + Require(fgetc_nid_postfix(&stream) == EOF); + Require(feof_nid_postfix(&stream) && (guest.flags & 0x20)); + clearerr_nid_postfix(&stream); + Require(!feof_nid_postfix(&stream) && !(guest.flags & 0x20)); + stream.Close(); + Require(guest.flags == 0 && guest.descriptor == -1); +} diff --git a/docs/YAMAGI_QUAKE2.md b/docs/YAMAGI_QUAKE2.md index ef5f22b83..8f4d13e40 100644 --- a/docs/YAMAGI_QUAKE2.md +++ b/docs/YAMAGI_QUAKE2.md @@ -200,3 +200,20 @@ All thirteen suites pass. Latest game startup resolves these imports and stops at `zqJhBxAKfsc` (`__stdoutp`). The next work requires the guest FILE layout and standard stream exports; mapping a host FILE pointer directly would be invalid. The game still has not executed its entry point or rendered a frame. + +## Follow-up: standard stream globals and character I/O + +Added __stdinp, __stdoutp, __stderrp, and __isthreaded exports. FileStream now +starts with the verified macro-accessed guest FILE prefix (position, read/write +counts, flags, descriptor, and buffer fields); native stream state follows a +reserved guest area. This is not a complete implementation of FreeBSD's private +FILE operations, locking fields, or wide-stream state. + +Added character input/output, __srget/__swbuf fallbacks, fgets, ungetc, EOF/error +queries, clearerr, fileno, and buffering-mode translation. Existing fread/fwrite +and fseek now refresh the guest status fields. Tests exercise stream globals, +field offsets, inline-macro fallback paths, temporary-file I/O, EOF, ungetc, +and closure. All fourteen suites pass and the compatibility libraries rebuild. + +Latest startup resolves the stream globals and stops at `8nY19bKoiZk` (`fcntl`). +The game has not reached its entry point or displayed a frame. From b8c1097db473305250e649f53612e17292d93ce8 Mon Sep 17 00:00:00 2001 From: SamG-Coder <13449963+SamG-Coder@users.noreply.github.com> Date: Fri, 25 Sep 2026 06:34:30 +1000 Subject: [PATCH 17/94] feat(kernel): support socket nonblocking flags through fcntl --- core/libs/prx/libkernel/Socket/src/Udp.cpp | 33 ++++++++++++++++++++++ core/libs/tests/GuestUdp.cpp | 8 ++++++ 2 files changed, 41 insertions(+) diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 8227779f1..386c95ed5 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -18,6 +18,7 @@ #include #include #include +#include extern "C" int* APS5_VABI __error_nid_postfix(); namespace { @@ -60,6 +61,8 @@ int NativeError() { } struct Socket { NativeSocket value; + std::mutex modeMutex; + bool nonblocking = false; explicit Socket(NativeSocket value) : value(value) {} ~Socket() { if (value == Invalid) return; @@ -139,6 +142,34 @@ int GuestSockets::Close(int descriptor) { } extern "C" { +int APS5_VABI fcntl_nid_postfix(int descriptor, int command, ...) { + const auto socket = Lookup(descriptor); + if (!socket) return -1; + std::lock_guard lock(socket->modeMutex); + if (command == 3) return 2 | (socket->nonblocking ? 4 : 0); // F_GETFL, O_RDWR + if (command != 4) return Fail(22); +#ifdef _WIN32 + __builtin_sysv_va_list arguments; + __builtin_sysv_va_start(arguments, command); + const int flags = __builtin_va_arg(arguments, int); + __builtin_sysv_va_end(arguments); +#else + std::va_list arguments; + va_start(arguments, command); + const int flags = va_arg(arguments, int); + va_end(arguments); +#endif + if ((flags & ~7) != 0) return Fail(45); +#ifdef _WIN32 + unsigned long enabled = (flags & 4) != 0; + if (ioctlsocket(socket->value, FIONBIO, &enabled)) return Fail(NativeError()); +#else + int enabled = (flags & 4) != 0; + if (::ioctl(socket->value, FIONBIO, &enabled)) return Fail(NativeError()); +#endif + socket->nonblocking = enabled != 0; + return 0; +} int APS5_VABI setsockopt_nid_postfix(int descriptor, int level, int option, const void* value, std::uint32_t length) { const auto socket = Lookup(descriptor); @@ -209,6 +240,7 @@ int APS5_VABI ioctl_nid_postfix(int descriptor, std::uint64_t request, void* arg if (!socket) return -1; if (!argument) return Fail(14); if (request != 0x8004667e && request != 0x4004667f) return Fail(25); + std::lock_guard lock(socket->modeMutex); unsigned long value = 0; if (request == 0x8004667e) value = *static_cast(argument) != 0; #ifdef _WIN32 @@ -219,6 +251,7 @@ int APS5_VABI ioctl_nid_postfix(int descriptor, std::uint64_t request, void* arg value = static_cast(nativeValue); #endif if (result) return Fail(NativeError()); + if (request == 0x8004667e) socket->nonblocking = value != 0; if (request == 0x4004667f) *static_cast(argument) = static_cast(value); return 0; } diff --git a/core/libs/tests/GuestUdp.cpp b/core/libs/tests/GuestUdp.cpp index 90d42c810..572e8dc3a 100644 --- a/core/libs/tests/GuestUdp.cpp +++ b/core/libs/tests/GuestUdp.cpp @@ -7,6 +7,7 @@ #include extern "C" { int APS5_VABI socket_nid_postfix(int, int, int); +int APS5_VABI fcntl_nid_postfix(int, int, ...); int APS5_VABI setsockopt_nid_postfix(int, int, int, const void*, std::uint32_t); int APS5_VABI getsockopt_nid_postfix(int, int, int, void*, std::uint32_t*); int APS5_VABI bind_nid_postfix(int, const void*, std::uint32_t); @@ -35,6 +36,13 @@ int main() { Require(setsockopt_nid_postfix(sender, 0xffff, 12345, &enabled, sizeof(enabled)) == -1); Require(*__error_nid_postfix() == 42); Require(ioctl_nid_postfix(receiver, 0x8004667e, &enabled) == 0); + Require(fcntl_nid_postfix(receiver, 3) == 6); + Require(fcntl_nid_postfix(receiver, 4, 2) == 0); + Require(fcntl_nid_postfix(receiver, 3) == 2); + Require(fcntl_nid_postfix(receiver, 4, 6) == 0); + Require(fcntl_nid_postfix(receiver, 3) == 6); + Require(fcntl_nid_postfix(receiver, 4, 0x8000) == -1); + Require(fcntl_nid_postfix(receiver, 3) == 6); char buffer[64]{}; Require(recvfrom_nid_postfix(receiver, buffer, sizeof(buffer), 0, nullptr, nullptr) == -1); Require(*__error_nid_postfix() == 35); From f49cace1ca98e98c77bf9124b23920be6e032bda Mon Sep 17 00:00:00 2001 From: SamG-Coder <13449963+SamG-Coder@users.noreply.github.com> Date: Fri, 25 Sep 2026 06:35:32 +1000 Subject: [PATCH 18/94] feat(libc): implement guest formatted stream output --- core/libs/prx/libc/src/Formatting.cpp | 32 +++++++++++++++++++++++ core/libs/tests/GuestStandardStreams.cpp | 33 ++++++++++++++++++++++++ docs/YAMAGI_QUAKE2.md | 19 ++++++++++++++ 3 files changed, 84 insertions(+) diff --git a/core/libs/prx/libc/src/Formatting.cpp b/core/libs/prx/libc/src/Formatting.cpp index b85e0eab2..38c54e381 100644 --- a/core/libs/prx/libc/src/Formatting.cpp +++ b/core/libs/prx/libc/src/Formatting.cpp @@ -4,6 +4,7 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/VarArgsAbi.hpp" +#include "prx/libc/include/FileStream.hpp" #ifdef _WIN32 #include "prx/libc/include/WindowsFormatting.hpp" @@ -11,6 +12,37 @@ extern "C" { +int APS5_VABI vfprintf_nid_postfix(FileStream* stream, const char* format, VaList* args) { + auto* native = GetNativeStream(stream); +#ifdef _WIN32 + std::string buffer; + const int count = LibcDetail::FormatWindows(nullptr, 0, format, args, &buffer); + const int result = std::fwrite(buffer.data(), 1, static_cast(count), native) == + static_cast(count) ? count : -1; +#else + const int result = std::vfprintf(native, format, *reinterpret_cast(args)); +#endif + stream->SyncStatus(); + return result; +} + +int APS5_VABI fprintf_nid_postfix(FileStream* stream, const char* format, ...) { +#ifdef _WIN32 + __builtin_sysv_va_list args; + __builtin_sysv_va_start(args, format); +#else + std::va_list args; + va_start(args, format); +#endif + const int result = vfprintf_nid_postfix(stream, format, reinterpret_cast(args)); +#ifdef _WIN32 + __builtin_sysv_va_end(args); +#else + va_end(args); +#endif + return result; +} + #ifdef _WIN32 int APS5_VABI printf_nid_postfix(const char* format, ...) { diff --git a/core/libs/tests/GuestStandardStreams.cpp b/core/libs/tests/GuestStandardStreams.cpp index 3ced61b07..add7614aa 100644 --- a/core/libs/tests/GuestStandardStreams.cpp +++ b/core/libs/tests/GuestStandardStreams.cpp @@ -2,11 +2,15 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include #include +#include +#include extern "C" { extern FileStream* __stdinp_nid_postfix; extern FileStream* __stdoutp_nid_postfix; extern FileStream* __stderrp_nid_postfix; extern int __isthreaded_nid_postfix; +int APS5_VABI fprintf_nid_postfix(FileStream*, const char*, ...); +int APS5_VABI vfprintf_nid_postfix(FileStream*, const char*, void*); int APS5_VABI fgetc_nid_postfix(FileStream*); int APS5_VABI fputc_nid_postfix(int, FileStream*); int APS5_VABI __srget_nid_postfix(FileStream*); @@ -19,6 +23,22 @@ void APS5_VABI clearerr_nid_postfix(FileStream*); int APS5_VABI setvbuf_nid_postfix(FileStream*, char*, int, std::size_t); } static void Require(bool value) { if (!value) std::abort(); } +static int APS5_VABI WriteFormatted(FileStream* stream, const char* format, ...) { +#ifdef _WIN32 + __builtin_sysv_va_list args; + __builtin_sysv_va_start(args, format); +#else + std::va_list args; + va_start(args, format); +#endif + const int result = vfprintf_nid_postfix(stream, format, args); +#ifdef _WIN32 + __builtin_sysv_va_end(args); +#else + va_end(args); +#endif + return result; +} int main() { Require(__isthreaded_nid_postfix == 1); Require(__stdoutp_nid_postfix == &_Stdout_nid_postfix); @@ -46,4 +66,17 @@ int main() { Require(!feof_nid_postfix(&stream) && !(guest.flags & 0x20)); stream.Close(); Require(guest.flags == 0 && guest.descriptor == -1); + + FileStream formatted(std::tmpfile()); + const char expected[] = "guest 4294967297 1.25 1 2 3 4 5 6 7 8\n"; + Require(fprintf_nid_postfix(&formatted, "%s %ld %.2f %d %d %d %d %d %d %d %d\n", + "guest", std::int64_t{4294967297}, 1.25, 1, 2, 3, 4, 5, 6, 7, 8) == sizeof(expected) - 1); + Require(WriteFormatted(&formatted, "%*.*f:%s", 6, 2, 3.5, "end") == 10); + std::rewind(formatted.GetHandle()); + char output[128]{}; + Require(fgets_nid_postfix(output, sizeof(output), &formatted) == output); + Require(std::strcmp(output, expected) == 0); + Require(fgets_nid_postfix(output, sizeof(output), &formatted) == output); + Require(std::strcmp(output, " 3.50:end") == 0); + formatted.Close(); } diff --git a/docs/YAMAGI_QUAKE2.md b/docs/YAMAGI_QUAKE2.md index 8f4d13e40..d2b86cc65 100644 --- a/docs/YAMAGI_QUAKE2.md +++ b/docs/YAMAGI_QUAKE2.md @@ -217,3 +217,22 @@ and closure. All fourteen suites pass and the compatibility libraries rebuild. Latest startup resolves the stream globals and stops at `8nY19bKoiZk` (`fcntl`). The game has not reached its entry point or displayed a frame. + +## Follow-up: descriptor flags and formatted stream output + +Added fcntl F_GETFL/F_SETFL for tracked UDP sockets. Nonblocking state is shared +with FIONBIO ioctl and protected against concurrent mode updates. Unsupported +flags are rejected; ordinary file descriptors and other fcntl commands are not +implemented. The UDP test checks both APIs agree, mode transitions, and unchanged +state after rejected flags. + +Added fprintf and vfprintf using the guest FileStream wrapper and the existing +SysV variadic formatter on Windows. Tests write and read back mixed strings, +64-bit guest long integers, floating-point values, register-overflow arguments, +and dynamic width/precision through an explicit guest va_list. Write failure +returns a negative result and refreshes the guest stream status. + +All fourteen CTest suites pass and patched compatibility libraries rebuild. +The unchanged converted game now resolves fcntl and fprintf, and startup stops +at `smbQukfxYJM` (`getenv`). This remains an import-resolution result: guest +entry-point execution and a rendered frame are still unverified. From 18790691bd72dbaa58c3e1c161beda8b99835986 Mon Sep 17 00:00:00 2001 From: SamG-Coder <13449963+SamG-Coder@users.noreply.github.com> Date: Fri, 25 Sep 2026 06:39:14 +1000 Subject: [PATCH 19/94] feat(libc): implement guest environment variable storage --- core/libs/CMakeLists.txt | 9 ++ core/libs/prx/libc/CMakeLists.txt | 1 + core/libs/prx/libc/src/Environment.cpp | 102 ++++++++++++++++++ core/libs/prx/libc/src/Process.cpp | 1 - .../libc/src/specifics/linux/PosixProcess.cpp | 25 ----- core/libs/tests/GuestEnvironment.cpp | 46 ++++++++ docs/YAMAGI_QUAKE2.md | 17 +++ 7 files changed, 175 insertions(+), 26 deletions(-) create mode 100644 core/libs/prx/libc/src/Environment.cpp delete mode 100644 core/libs/prx/libc/src/specifics/linux/PosixProcess.cpp create mode 100644 core/libs/tests/GuestEnvironment.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 6d96dd95a..70a665415 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -104,6 +104,15 @@ endforeach() add_custom_target(libs DEPENDS ${PATCHED_OUTPUTS}) if(BUILD_TESTING) + add_executable(guest_environment_tests tests/GuestEnvironment.cpp) + target_include_directories(guest_environment_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_environment_tests PRIVATE libc) + configure_windows_unwind(guest_environment_tests) + add_test(NAME guest_environment COMMAND guest_environment_tests) + if(WIN32) + set_tests_properties(guest_environment PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_executable(guest_standard_stream_tests tests/GuestStandardStreams.cpp) target_include_directories(guest_standard_stream_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_standard_stream_tests PRIVATE libc) diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index d3516042f..48a352602 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -19,6 +19,7 @@ target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/RuntimeSupport.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/LocaleSupport.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Process.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/Environment.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Errors.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/CxxAbiSupport.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/exception/Exports.cpp diff --git a/core/libs/prx/libc/src/Environment.cpp b/core/libs/prx/libc/src/Environment.cpp new file mode 100644 index 000000000..080c8b110 --- /dev/null +++ b/core/libs/prx/libc/src/Environment.cpp @@ -0,0 +1,102 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include "prx/libc/include/general/VabiMacros.hpp" + +#ifndef _WIN32 +extern char** environ; +#endif + +namespace { +struct EnvironmentEntry { + std::string owned; + char* borrowed = nullptr; + char* Data() { return borrowed ? borrowed : owned.data(); } + bool Matches(std::string_view name) { + const char* data = Data(); + const char* separator = std::strchr(data, '='); + return separator && std::string_view(data, separator - data) == name; + } +}; +std::mutex environmentMutex; +std::vector& Environment() { + // Seed once from the host. Guest mutations remain local to the guest runtime. + static std::vector entries = [] { + std::vector result; +#ifdef _WIN32 + char** source = _environ; +#else + char** source = environ; +#endif + if (source) for (; *source; ++source) { + if (**source && **source != '=' && std::strchr(*source, '=')) + result.push_back({*source, nullptr}); + } + return result; + }(); + return entries; +} +bool ValidEnvironmentName(const char* name) { + return name && *name && !std::strchr(name, '='); +} +int EnvironmentError(int error) { errno = error; return -1; } +} + +extern "C" { +char* APS5_VABI getenv_nid_postfix(const char* name) { + if (!ValidEnvironmentName(name)) return nullptr; + try { + std::lock_guard lock(environmentMutex); + for (auto& entry : Environment()) + if (entry.Matches(name)) return entry.Data() + std::strlen(name) + 1; + return nullptr; + } catch (const std::bad_alloc&) { errno = 12; return nullptr; } +} + +int APS5_VABI setenv_nid_postfix(const char* name, const char* value, int overwrite) { + if (!ValidEnvironmentName(name) || !value) return EnvironmentError(22); + try { + std::lock_guard lock(environmentMutex); + auto& entries = Environment(); + auto found = std::find_if(entries.begin(), entries.end(), + [name](auto& entry) { return entry.Matches(name); }); + if (found != entries.end() && !overwrite) return 0; + EnvironmentEntry replacement{std::string(name) + '=' + value, nullptr}; + if (found != entries.end()) *found = std::move(replacement); + else entries.push_back(std::move(replacement)); + return 0; + } catch (const std::bad_alloc&) { return EnvironmentError(12); } +} + +int APS5_VABI unsetenv_nid_postfix(const char* name) { + if (!ValidEnvironmentName(name)) return EnvironmentError(22); + try { + std::lock_guard lock(environmentMutex); + auto& entries = Environment(); + const std::string key(name); + std::erase_if(entries, [&key](auto& entry) { return entry.Matches(key); }); + return 0; + } catch (const std::bad_alloc&) { return EnvironmentError(12); } +} + +int APS5_VABI putenv_nid_postfix(char* text) { + const char* separator = text ? std::strchr(text, '=') : nullptr; + if (!separator || separator == text) return EnvironmentError(22); + try { + std::lock_guard lock(environmentMutex); + auto& entries = Environment(); + const std::string_view name(text, separator - text); + auto found = std::find_if(entries.begin(), entries.end(), + [name](auto& entry) { return entry.Matches(name); }); + EnvironmentEntry replacement{{}, text}; + if (found != entries.end()) *found = std::move(replacement); + else entries.push_back(std::move(replacement)); + return 0; + } catch (const std::bad_alloc&) { return EnvironmentError(12); } +} +} diff --git a/core/libs/prx/libc/src/Process.cpp b/core/libs/prx/libc/src/Process.cpp index 5e8bf25b3..ae3a6e938 100644 --- a/core/libs/prx/libc/src/Process.cpp +++ b/core/libs/prx/libc/src/Process.cpp @@ -91,4 +91,3 @@ int APS5_VABI atexit_nid_postfix(atexit_func_t func) { } -#include "prx/libc/src/specifics/linux/PosixProcess.cpp" diff --git a/core/libs/prx/libc/src/specifics/linux/PosixProcess.cpp b/core/libs/prx/libc/src/specifics/linux/PosixProcess.cpp deleted file mode 100644 index a362f9f48..000000000 --- a/core/libs/prx/libc/src/specifics/linux/PosixProcess.cpp +++ /dev/null @@ -1,25 +0,0 @@ -#include -#include -#include - -extern "C" { - -int APS5_VABI setenv_nid_postfix(const char* name, const char* value, int overwrite) { - if (overwrite == 0 && std::getenv(name) != nullptr) { - return 0; - } - const size_t nameLength = std::strlen(name); - const size_t valueLength = std::strlen(value); - char* entry = static_cast(std::malloc(nameLength + valueLength + 2)); - if (entry == nullptr) { - errno = ENOMEM; - return -1; - } - std::memcpy(entry, name, nameLength); - entry[nameLength] = '='; - std::memcpy(entry + nameLength + 1, value, valueLength); - entry[nameLength + 1 + valueLength] = '\0'; - return ::putenv(entry); -} - -} diff --git a/core/libs/tests/GuestEnvironment.cpp b/core/libs/tests/GuestEnvironment.cpp new file mode 100644 index 000000000..df1910cf4 --- /dev/null +++ b/core/libs/tests/GuestEnvironment.cpp @@ -0,0 +1,46 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +extern "C" { +char* APS5_VABI getenv_nid_postfix(const char*); +int APS5_VABI setenv_nid_postfix(const char*, const char*, int); +int APS5_VABI unsetenv_nid_postfix(const char*); +int APS5_VABI putenv_nid_postfix(char*); +int* APS5_VABI __error_nid_postfix(); +} +static void Require(bool value) { if (!value) std::abort(); } +int main() { + const char* key = "ANYPS5_GUEST_ENV_TEST_4C27"; +#ifdef _WIN32 + Require(_putenv_s(key, "inherited") == 0); +#else + Require(::setenv(key, "inherited", 1) == 0); +#endif + Require(std::strcmp(getenv_nid_postfix(key), "inherited") == 0); + Require(setenv_nid_postfix(key, "kept out", 0) == 0); + Require(std::strcmp(getenv_nid_postfix(key), "inherited") == 0); + char value[] = "copied"; + Require(setenv_nid_postfix(key, value, 1) == 0); + value[0] = 'X'; + Require(std::strcmp(getenv_nid_postfix(key), "copied") == 0); + Require(std::strcmp(std::getenv(key), "inherited") == 0); + Require(setenv_nid_postfix(key, "", 1) == 0); + Require(getenv_nid_postfix(key) && *getenv_nid_postfix(key) == 0); + char borrowed[] = "ANYPS5_GUEST_ENV_TEST_4C27=one"; + Require(putenv_nid_postfix(borrowed) == 0); + char* position = std::strchr(borrowed, '=') + 1; + Require(getenv_nid_postfix(key) == position); + std::memcpy(position, "two", 3); + Require(std::strcmp(getenv_nid_postfix(key), "two") == 0); + Require(setenv_nid_postfix("anyps5_guest_env_test_4c27", "lower", 1) == 0); + Require(std::strcmp(getenv_nid_postfix(key), "two") == 0); + Require(setenv_nid_postfix("bad=name", "x", 1) == -1); + Require(*__error_nid_postfix() == 22); + Require(unsetenv_nid_postfix("") == -1); + char invalid[] = "no-separator"; + Require(putenv_nid_postfix(invalid) == -1); + Require(unsetenv_nid_postfix(key) == 0); + Require(getenv_nid_postfix(key) == nullptr); + Require(unsetenv_nid_postfix(key) == 0); + Require(unsetenv_nid_postfix("anyps5_guest_env_test_4c27") == 0); +} diff --git a/docs/YAMAGI_QUAKE2.md b/docs/YAMAGI_QUAKE2.md index d2b86cc65..e4b56936e 100644 --- a/docs/YAMAGI_QUAKE2.md +++ b/docs/YAMAGI_QUAKE2.md @@ -236,3 +236,20 @@ All fourteen CTest suites pass and patched compatibility libraries rebuild. The unchanged converted game now resolves fcntl and fprintf, and startup stops at `smbQukfxYJM` (`getenv`). This remains an import-resolution result: guest entry-point execution and a rendered frame are still unverified. + +## Follow-up: guest environment variables + +Implemented getenv, setenv, unsetenv, and putenv as one case-sensitive guest +environment, initialized from the host CRT environment on first access. Guest +updates do not mutate the host environment. setenv owns a copy, while putenv +retains the caller's string so later changes are visible. Empty values remain +distinct from missing variables; invalid update names return guest EINVAL. +The old allocating host-putenv setenv implementation has been replaced. + +Tests cover inherited values, overwrite control, copying versus borrowed +storage, empty values, case sensitivity, removal, invalid names, and unchanged +host values. All fifteen suites pass. Direct environ access and propagation to +host child processes or host locale functions are not implemented by this store. + +Latest unchanged-game startup resolves getenv and stops at `ay3uROQAc5A` +(`opendir`). Guest entry-point execution and rendered output remain unverified. From 5301f89092fe7d84e0c244e6ffa0f273d4340235 Mon Sep 17 00:00:00 2001 From: SamG-Coder <13449963+SamG-Coder@users.noreply.github.com> Date: Fri, 25 Sep 2026 06:41:55 +1000 Subject: [PATCH 20/94] feat(libc): translate guest directory enumeration --- core/libs/CMakeLists.txt | 9 ++ core/libs/prx/libc/CMakeLists.txt | 1 + core/libs/prx/libc/include/GuestDirectory.hpp | 14 +++ core/libs/prx/libc/src/Directory.cpp | 102 ++++++++++++++++++ core/libs/tests/GuestDirectory.cpp | 52 +++++++++ docs/YAMAGI_QUAKE2.md | 18 ++++ 6 files changed, 196 insertions(+) create mode 100644 core/libs/prx/libc/include/GuestDirectory.hpp create mode 100644 core/libs/prx/libc/src/Directory.cpp create mode 100644 core/libs/tests/GuestDirectory.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 70a665415..de77bf0d4 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -104,6 +104,15 @@ endforeach() add_custom_target(libs DEPENDS ${PATCHED_OUTPUTS}) if(BUILD_TESTING) + add_executable(guest_directory_tests tests/GuestDirectory.cpp) + target_include_directories(guest_directory_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_directory_tests PRIVATE libc) + configure_windows_unwind(guest_directory_tests) + add_test(NAME guest_directory COMMAND guest_directory_tests) + if(WIN32) + set_tests_properties(guest_directory PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_executable(guest_environment_tests tests/GuestEnvironment.cpp) target_include_directories(guest_environment_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_environment_tests PRIVATE libc) diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index 48a352602..0de9f3210 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -20,6 +20,7 @@ target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/LocaleSupport.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Process.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Environment.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/Directory.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Errors.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/CxxAbiSupport.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/exception/Exports.cpp diff --git a/core/libs/prx/libc/include/GuestDirectory.hpp b/core/libs/prx/libc/include/GuestDirectory.hpp new file mode 100644 index 000000000..532db73fb --- /dev/null +++ b/core/libs/prx/libc/include/GuestDirectory.hpp @@ -0,0 +1,14 @@ +#pragma once +#include +#include + +// FreeBSD 11 ABI used by the PS5 SDK: names start at byte 8. +struct GuestDirectoryEntry { + std::uint32_t fileNumber; + std::uint16_t recordLength; + std::uint8_t type; + std::uint8_t nameLength; + char name[256]; +}; +static_assert(offsetof(GuestDirectoryEntry, name) == 8); +static_assert(sizeof(GuestDirectoryEntry) == 264); diff --git a/core/libs/prx/libc/src/Directory.cpp b/core/libs/prx/libc/src/Directory.cpp new file mode 100644 index 000000000..0cc8ae972 --- /dev/null +++ b/core/libs/prx/libc/src/Directory.cpp @@ -0,0 +1,102 @@ +#include "prx/libc/include/GuestDirectory.hpp" +#include "prx/libc/include/General.hpp" +#include +#include +#include +#include +#include + +namespace { +int DirectoryError(int error) { + switch (error) { + case ENOENT: return 2; + case EACCES: return 13; + case ENOTDIR: return 20; + case EINVAL: return 22; + case EMFILE: return 24; + case ENFILE: return 23; + case ENOMEM: return 12; + case ENAMETOOLONG: return 63; + default: return 5; + } +} +struct Directory { + DIR* native = nullptr; + std::filesystem::path path; + GuestDirectoryEntry entry{}; + std::mutex mutex; + ~Directory() { if (native) ::closedir(native); } +}; +std::uint8_t DirectoryType(const std::filesystem::path& path) { + std::error_code error; + switch (std::filesystem::symlink_status(path, error).type()) { + case std::filesystem::file_type::regular: return 8; + case std::filesystem::file_type::directory: return 4; + case std::filesystem::file_type::symlink: return 10; + case std::filesystem::file_type::block: return 6; + case std::filesystem::file_type::character: return 2; + case std::filesystem::file_type::fifo: return 1; + case std::filesystem::file_type::socket: return 12; + default: return 0; + } +} +} + +extern "C" { +void* APS5_VABI opendir_nid_postfix(const char* path) { + if (!path) { errno = 14; return nullptr; } + if (!*path) { errno = 2; return nullptr; } + try { + auto directory = std::make_unique(); + directory->path = ResolvePath_nid_no_patch(path); + directory->native = ::opendir(directory->path.string().c_str()); + if (!directory->native) { errno = DirectoryError(errno); return nullptr; } + return directory.release(); + } catch (const std::bad_alloc&) { errno = 12; return nullptr; } + catch (const std::filesystem::filesystem_error&) { errno = 5; return nullptr; } +} + +GuestDirectoryEntry* APS5_VABI readdir_nid_postfix(void* handle) { + if (!handle) { errno = 9; return nullptr; } + auto& directory = *static_cast(handle); + std::lock_guard lock(directory.mutex); + const int savedError = errno; + errno = 0; + auto* entry = ::readdir(directory.native); + if (!entry) { + errno = errno ? DirectoryError(errno) : savedError; + return nullptr; + } + const auto length = std::strlen(entry->d_name); + if (length > 255) { errno = 63; return nullptr; } + try { + directory.entry = {}; + // MinGW reports no inode identity (zero); do not fabricate one. + const auto inode = static_cast(entry->d_ino); + directory.entry.fileNumber = inode <= UINT32_MAX ? static_cast(inode) : 0; + directory.entry.recordLength = static_cast(8 + ((length + 1 + 3) & ~3)); + directory.entry.nameLength = static_cast(length); + directory.entry.type = DirectoryType(directory.path / entry->d_name); + std::memcpy(directory.entry.name, entry->d_name, length + 1); + errno = savedError; + return &directory.entry; + } catch (const std::bad_alloc&) { errno = 12; return nullptr; } + catch (const std::filesystem::filesystem_error&) { errno = 5; return nullptr; } +} + +int APS5_VABI closedir_nid_postfix(void* handle) { + if (!handle) { errno = 9; return -1; } + std::unique_ptr directory(static_cast(handle)); + const int result = ::closedir(directory->native); + directory->native = nullptr; + if (result) errno = DirectoryError(errno); + return result; +} + +void APS5_VABI rewinddir_nid_postfix(void* handle) { + if (!handle) { errno = 9; return; } + auto& directory = *static_cast(handle); + std::lock_guard lock(directory.mutex); + ::rewinddir(directory.native); +} +} diff --git a/core/libs/tests/GuestDirectory.cpp b/core/libs/tests/GuestDirectory.cpp new file mode 100644 index 000000000..6d3288f0d --- /dev/null +++ b/core/libs/tests/GuestDirectory.cpp @@ -0,0 +1,52 @@ +#include "prx/libc/include/GuestDirectory.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +extern "C" { +void* APS5_VABI opendir_nid_postfix(const char*); +GuestDirectoryEntry* APS5_VABI readdir_nid_postfix(void*); +int APS5_VABI closedir_nid_postfix(void*); +void APS5_VABI rewinddir_nid_postfix(void*); +int* APS5_VABI __error_nid_postfix(); +} +static void Require(bool value) { if (!value) std::abort(); } +int main() { + const auto root = std::filesystem::path("anyps5-directory-test-" + + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + Require(std::filesystem::create_directory(root)); + Require(std::filesystem::create_directory(root / "subdirectory")); + { std::ofstream file(root / "sample.txt"); file << "test"; } + void* directory = opendir_nid_postfix(root.string().c_str()); + Require(directory != nullptr); + std::map entries; + *__error_nid_postfix() = 13; + while (auto* entry = readdir_nid_postfix(directory)) { + Require(entry->nameLength == std::strlen(entry->name)); + Require(entry->recordLength == 8 + ((entry->nameLength + 4) & ~3)); + Require(entry->recordLength <= sizeof(*entry)); + entries[entry->name] = entry->type; + } + Require(*__error_nid_postfix() == 13); // EOF preserves errno. + Require(entries.at("sample.txt") == 8); + Require(entries.at("subdirectory") == 4); + Require(entries.at(".") == 4 && entries.at("..") == 4); + rewinddir_nid_postfix(directory); + std::size_t count = 0; + while (readdir_nid_postfix(directory)) ++count; + Require(count == entries.size()); + Require(closedir_nid_postfix(directory) == 0); + Require(opendir_nid_postfix((root / "missing").string().c_str()) == nullptr); + Require(*__error_nid_postfix() == 2); + Require(opendir_nid_postfix((root / "sample.txt").string().c_str()) == nullptr); + Require(*__error_nid_postfix() == 20); + Require(opendir_nid_postfix("") == nullptr && *__error_nid_postfix() == 2); + Require(closedir_nid_postfix(nullptr) == -1 && *__error_nid_postfix() == 9); + std::filesystem::remove(root / "sample.txt"); + std::filesystem::remove(root / "subdirectory"); + std::filesystem::remove(root); +} diff --git a/docs/YAMAGI_QUAKE2.md b/docs/YAMAGI_QUAKE2.md index e4b56936e..5d4c4fd51 100644 --- a/docs/YAMAGI_QUAKE2.md +++ b/docs/YAMAGI_QUAKE2.md @@ -253,3 +253,21 @@ host child processes or host locale functions are not implemented by this store. Latest unchanged-game startup resolves getenv and stops at `ay3uROQAc5A` (`opendir`). Guest entry-point execution and rendered output remain unverified. + +## Follow-up: directory enumeration + +Added opendir, readdir, closedir, and rewinddir using the existing guest path +resolver. Directory entries are translated into the SDK's FreeBSD 11 layout: +32-bit file number, 16-bit record length, byte type/name length, and the name +starting at offset 8. Record lengths include four-byte padding. Each directory +owns its returned entry buffer. End-of-directory preserves the caller's errno; +open/read failures translate common host errors into guest error values. + +Tests enumerate actual files and subdirectories, verify names/types and record +lengths, rewind, and check missing paths, non-directory paths, and EOF behavior. +All sixteen suites pass. The Windows MinGW backend provides no inode identity +(file number is zero) and uses host narrow-character filenames; full Unicode +filename coverage and direct access to opaque DIR internals are not established. + +The unchanged converted game now resolves opendir and stops at `MZO7FXyAPU8` +(`remove`). No guest entry-point execution or rendered frame has been observed. From 91bf8e3a7afab90791a02439524a4a3215951a46 Mon Sep 17 00:00:00 2001 From: SamG-Coder <13449963+SamG-Coder@users.noreply.github.com> Date: Fri, 25 Sep 2026 06:45:30 +1000 Subject: [PATCH 21/94] feat(libc): implement guest file removal and renaming --- core/libs/CMakeLists.txt | 9 ++++ core/libs/prx/libc/CMakeLists.txt | 1 + core/libs/prx/libc/src/Filesystem.cpp | 77 +++++++++++++++++++++++++++ core/libs/tests/GuestFilesystem.cpp | 46 ++++++++++++++++ docs/YAMAGI_QUAKE2.md | 17 ++++++ 5 files changed, 150 insertions(+) create mode 100644 core/libs/prx/libc/src/Filesystem.cpp create mode 100644 core/libs/tests/GuestFilesystem.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index de77bf0d4..bf8951bda 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -104,6 +104,15 @@ endforeach() add_custom_target(libs DEPENDS ${PATCHED_OUTPUTS}) if(BUILD_TESTING) + add_executable(guest_filesystem_tests tests/GuestFilesystem.cpp) + target_include_directories(guest_filesystem_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_filesystem_tests PRIVATE libc) + configure_windows_unwind(guest_filesystem_tests) + add_test(NAME guest_filesystem COMMAND guest_filesystem_tests) + if(WIN32) + set_tests_properties(guest_filesystem PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_executable(guest_directory_tests tests/GuestDirectory.cpp) target_include_directories(guest_directory_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_directory_tests PRIVATE libc) diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index 0de9f3210..107816cc9 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -21,6 +21,7 @@ target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/Process.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Environment.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Directory.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/Filesystem.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Errors.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/CxxAbiSupport.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/exception/Exports.cpp diff --git a/core/libs/prx/libc/src/Filesystem.cpp b/core/libs/prx/libc/src/Filesystem.cpp new file mode 100644 index 000000000..60a4c02c6 --- /dev/null +++ b/core/libs/prx/libc/src/Filesystem.cpp @@ -0,0 +1,77 @@ +#include "prx/libc/include/General.hpp" +#include +#ifdef _WIN32 +#define NOMINMAX +#include +#endif + +namespace { +int FilesystemError(const std::error_code& error) { + if (error == std::errc::no_such_file_or_directory) return 2; + if (error == std::errc::permission_denied) return 13; + if (error == std::errc::operation_not_permitted) return 1; + if (error == std::errc::not_a_directory) return 20; + if (error == std::errc::is_a_directory) return 21; + if (error == std::errc::directory_not_empty) return 66; + if (error == std::errc::device_or_resource_busy) return 16; + if (error == std::errc::read_only_file_system) return 30; + if (error == std::errc::filename_too_long) return 63; + if (error == std::errc::too_many_symbolic_link_levels) return 62; + if (error == std::errc::not_enough_memory) return 12; + if (error == std::errc::invalid_argument) return 22; + if (error == std::errc::cross_device_link) return 18; + if (error == std::errc::file_exists) return 17; + if (error == std::errc::no_space_on_device) return 28; + return 5; +} +} + +extern "C" int APS5_VABI rename_nid_postfix(const char* from, const char* to) { + if (!from || !to) { errno = 14; return -1; } + if (!*from || !*to) { errno = 2; return -1; } + try { + const auto source = ResolvePath_nid_no_patch(from); + const auto destination = ResolvePath_nid_no_patch(to); + std::error_code error; + std::filesystem::rename(source, destination, error); + if (error) { errno = FilesystemError(error); return -1; } + return 0; + } catch (const std::bad_alloc&) { errno = 12; return -1; } + catch (const std::filesystem::filesystem_error& error) { + errno = FilesystemError(error.code()); + return -1; + } +} + +extern "C" int APS5_VABI remove_nid_postfix(const char* path) { + if (!path) { errno = 14; return -1; } + if (!*path) { errno = 2; return -1; } + try { + const auto resolved = ResolvePath_nid_no_patch(path); + std::error_code error; +#ifdef _WIN32 + const DWORD attributes = GetFileAttributesW(resolved.c_str()); + bool removed = false; + if (attributes != INVALID_FILE_ATTRIBUTES) { + removed = (attributes & FILE_ATTRIBUTE_DIRECTORY) ? + RemoveDirectoryW(resolved.c_str()) != 0 : DeleteFileW(resolved.c_str()) != 0; + } + if (!removed) { + const DWORD nativeError = GetLastError(); + if (nativeError == ERROR_DIR_NOT_EMPTY) { errno = 66; return -1; } + error = std::error_code(static_cast(nativeError), std::system_category()); + } +#else + const bool removed = std::filesystem::remove(resolved, error); +#endif + if (error || !removed) { + errno = error ? FilesystemError(error) : 2; + return -1; + } + return 0; + } catch (const std::bad_alloc&) { errno = 12; return -1; } + catch (const std::filesystem::filesystem_error& error) { + errno = FilesystemError(error.code()); + return -1; + } +} diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp new file mode 100644 index 000000000..0a5b998cc --- /dev/null +++ b/core/libs/tests/GuestFilesystem.cpp @@ -0,0 +1,46 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +extern "C" { +int APS5_VABI remove_nid_postfix(const char*); +int APS5_VABI rename_nid_postfix(const char*, const char*); +int* APS5_VABI __error_nid_postfix(); +} +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "Filesystem check failed at line %d (guest errno %d)\n", line, *__error_nid_postfix()); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) +int main() { + const auto root = std::filesystem::path("anyps5-filesystem-test-" + + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + Require(std::filesystem::create_directory(root)); + const auto file = root / "file.txt"; + { std::ofstream stream(file); stream << "retained until removal"; } + Require(remove_nid_postfix(root.string().c_str()) == -1); + Require(*__error_nid_postfix() == 66); + Require(std::filesystem::is_regular_file(file)); + Require(remove_nid_postfix((file / "invalid").string().c_str()) == -1); + Require(remove_nid_postfix("") == -1 && *__error_nid_postfix() == 2); + Require(remove_nid_postfix(nullptr) == -1 && *__error_nid_postfix() == 14); + const auto renamed = root / "renamed.txt"; + { std::ofstream stream(renamed); stream << "old contents"; } + Require(rename_nid_postfix(file.string().c_str(), renamed.string().c_str()) == 0); + Require(!std::filesystem::exists(file)); + { std::ifstream stream(renamed); std::string contents; std::getline(stream, contents); + Require(contents == "retained until removal"); } + Require(rename_nid_postfix(renamed.string().c_str(), renamed.string().c_str()) == 0); + Require(rename_nid_postfix(file.string().c_str(), renamed.string().c_str()) == -1); + Require(*__error_nid_postfix() == 2); + Require(rename_nid_postfix(renamed.string().c_str(), file.string().c_str()) == 0); + Require(remove_nid_postfix(file.string().c_str()) == 0); + Require(!std::filesystem::exists(file)); + Require(remove_nid_postfix(file.string().c_str()) == -1 && *__error_nid_postfix() == 2); + Require(remove_nid_postfix(root.string().c_str()) == 0); + Require(!std::filesystem::exists(root)); +} diff --git a/docs/YAMAGI_QUAKE2.md b/docs/YAMAGI_QUAKE2.md index 5d4c4fd51..bd3753009 100644 --- a/docs/YAMAGI_QUAKE2.md +++ b/docs/YAMAGI_QUAKE2.md @@ -271,3 +271,20 @@ filename coverage and direct access to opaque DIR internals are not established. The unchanged converted game now resolves opendir and stops at `MZO7FXyAPU8` (`remove`). No guest entry-point execution or rendered frame has been observed. + +## Follow-up: file removal and renaming + +Added remove and rename using the existing guest path resolver. Removal handles +files and empty directories without recursive deletion. Windows removal uses +the native APIs because the host CRT collapses nonempty-directory failures into +access errors; the guest now receives ENOTEMPTY for that case. Renaming supports +replacing existing files and maps common host errors into guest errno values. + +Tests verify nonempty-directory preservation, file and empty-directory removal, +missing and invalid paths, replacement contents, same-path rename, and missing +rename sources. All seventeen suites pass. These checks do not establish full +PS5 mount semantics, cross-volume behavior, or replacement of existing directories. + +The unchanged converted game resolves remove and rename. Startup now stops at +`biY+kcVB5D4` (`dlsym`), requiring dynamic-module symbol lookup. It still has not +executed the guest entry point or displayed a frame. From 453fec28c583e2a0846a062e1a9826d0ef88812c Mon Sep 17 00:00:00 2001 From: SamG-Coder <13449963+SamG-Coder@users.noreply.github.com> Date: Fri, 25 Sep 2026 06:48:50 +1000 Subject: [PATCH 22/94] feat(kernel): add tracked dynamic module loading and NID lookup --- core/libs/CMakeLists.txt | 18 +++ core/libs/prx/libkernel/CMakeLists.txt | 5 + .../libkernel/Module/src/DynamicLoader.cpp | 126 ++++++++++++++++++ core/libs/tests/GuestDynamicLoader.cpp | 40 ++++++ core/libs/tests/GuestModuleFixture.cpp | 9 ++ docs/YAMAGI_QUAKE2.md | 24 ++++ 6 files changed, 222 insertions(+) create mode 100644 core/libs/prx/libkernel/Module/src/DynamicLoader.cpp create mode 100644 core/libs/tests/GuestDynamicLoader.cpp create mode 100644 core/libs/tests/GuestModuleFixture.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index bf8951bda..64601945e 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -104,6 +104,24 @@ endforeach() add_custom_target(libs DEPENDS ${PATCHED_OUTPUTS}) if(BUILD_TESTING) + add_library(guest_module_fixture SHARED tests/GuestModuleFixture.cpp) + target_include_directories(guest_module_fixture PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + add_custom_command(TARGET guest_module_fixture POST_BUILD + COMMAND $ guest_module_fixture $ + VERBATIM) + add_dependencies(guest_module_fixture nid_patcher) + add_executable(guest_dynamic_loader_tests tests/GuestDynamicLoader.cpp) + target_include_directories(guest_dynamic_loader_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_dynamic_loader_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_dynamic_loader_tests) + add_dependencies(guest_dynamic_loader_tests guest_module_fixture) + add_test(NAME guest_dynamic_loader COMMAND guest_dynamic_loader_tests $) + set_tests_properties(guest_dynamic_loader PROPERTIES + WORKING_DIRECTORY "$") + if(WIN32) + set_tests_properties(guest_dynamic_loader PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_executable(guest_filesystem_tests tests/GuestFilesystem.cpp) target_include_directories(guest_filesystem_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_filesystem_tests PRIVATE libc) diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 2a8d35118..08098fd1e 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -29,6 +29,9 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Socket/src/AddressConversion.cpp ${CMAKE_CURRENT_SOURCE_DIR}/EventFlag/src/EventFlag.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Module/src/Module.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Module/src/DynamicLoader.cpp + ${LIBS_INCLUDE_DIR}/nid/src/NidCompute.cpp + ${LIBS_INCLUDE_DIR}/nid/src/Sha1.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Aio/src/Aio.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/Coredump.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/Exception.cpp @@ -43,6 +46,8 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ) target_include_directories(libkernel PRIVATE ${LIBS_INCLUDE_DIR}) +target_include_directories(libkernel PRIVATE ${LIBS_INCLUDE_DIR}/nid/include) +target_link_libraries(libkernel PRIVATE ${CMAKE_DL_LIBS}) target_link_libraries(libkernel PRIVATE libc) if(WIN32) target_link_libraries(libkernel PRIVATE ws2_32) diff --git a/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp b/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp new file mode 100644 index 000000000..e1091def8 --- /dev/null +++ b/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp @@ -0,0 +1,126 @@ +#include "prx/libc/include/General.hpp" +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#define NOMINMAX +#include +#else +#include +#endif + +namespace { +thread_local std::array loaderError{}; +thread_local bool pendingError = false; +void Error(const char* message) { + std::snprintf(loaderError.data(), loaderError.size(), "%s", message); + pendingError = true; +} +struct Module { + void* native = nullptr; + bool owned = true; + bool global = false; + ~Module() { + if (owned && native) { +#ifdef _WIN32 + FreeLibrary(static_cast(native)); +#else + ::dlclose(native); +#endif + } + } +}; +std::mutex modulesMutex; +std::map> modules; +std::uintptr_t nextHandle = 0x20000000; +void* Symbol(Module& module, const char* name) { +#ifdef _WIN32 + return reinterpret_cast(GetProcAddress(static_cast(module.native), name)); +#else + return ::dlsym(module.native, name); +#endif +} +void* FindSymbol(Module& module, const char* name) { + if (auto* symbol = Symbol(module, name)) return symbol; + const auto nid = Nid::ComputeNid(name, ""); + return Symbol(module, nid.c_str()); +} +} + +extern "C" { +char* APS5_VABI dlerror_nid_postfix() { + if (!pendingError) return nullptr; + pendingError = false; + return loaderError.data(); +} +void* APS5_VABI dlopen_nid_postfix(const char* path, int flags) { + if ((flags & ~0x103) || (flags & 3) == 0 || (flags & 3) == 3) { + Error("dlopen: unsupported flags"); return nullptr; + } + try { + auto module = std::make_shared(); + module->global = (flags & 0x100) != 0 || !path; +#ifdef _WIN32 + if (!path) { + module->native = GetModuleHandleW(nullptr); + module->owned = false; + } else { + if (!*path) { Error("dlopen: empty module path"); return nullptr; } + const auto resolved = ResolvePath_nid_no_patch(path); + module->native = LoadLibraryExW(resolved.c_str(), nullptr, LOAD_WITH_ALTERED_SEARCH_PATH); + } + if (!module->native) { + char message[128]; + std::snprintf(message, sizeof(message), "dlopen: Windows loader error %lu (module must be host-compatible)", GetLastError()); + Error(message); return nullptr; + } +#else + const auto resolved = path ? ResolvePath_nid_no_patch(path).string() : std::string{}; + const int nativeFlags = ((flags & 3) == 1 ? RTLD_LAZY : RTLD_NOW) | + ((flags & 0x100) ? RTLD_GLOBAL : RTLD_LOCAL); + module->native = ::dlopen(path ? resolved.c_str() : nullptr, nativeFlags); + if (!module->native) { Error(::dlerror()); return nullptr; } +#endif + std::lock_guard lock(modulesMutex); + const auto handle = nextHandle++; + modules.emplace(handle, std::move(module)); + return reinterpret_cast(handle); + } catch (const std::exception& error) { Error(error.what()); return nullptr; } +} +void* APS5_VABI dlsym_nid_postfix(void* handle, const char* name) { + if (!name || !*name) { Error("dlsym: empty symbol name"); return nullptr; } + try { + std::vector> search; + { + std::lock_guard lock(modulesMutex); + if (handle == reinterpret_cast(static_cast(-2))) { + for (const auto& [key, module] : modules) if (module->global) search.push_back(module); + } else { + auto found = modules.find(reinterpret_cast(handle)); + if (found == modules.end()) { Error("dlsym: invalid or unsupported module handle"); return nullptr; } + search.push_back(found->second); + } + } + for (const auto& module : search) if (auto* result = FindSymbol(*module, name)) return result; + Error("dlsym: symbol not found in supported module scope"); + return nullptr; + } catch (const std::exception& error) { Error(error.what()); return nullptr; } +} +int APS5_VABI dlclose_nid_postfix(void* handle) { + std::shared_ptr module; + { + std::lock_guard lock(modulesMutex); + auto found = modules.find(reinterpret_cast(handle)); + if (found == modules.end()) { Error("dlclose: invalid module handle"); return -1; } + module = std::move(found->second); + modules.erase(found); + } + // Unload outside the registry lock: module destructors may call loader APIs. + module.reset(); + return 0; +} +} diff --git a/core/libs/tests/GuestDynamicLoader.cpp b/core/libs/tests/GuestDynamicLoader.cpp new file mode 100644 index 000000000..2fa27e917 --- /dev/null +++ b/core/libs/tests/GuestDynamicLoader.cpp @@ -0,0 +1,40 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +extern "C" { +void* APS5_VABI dlopen_nid_postfix(const char*, int); +void* APS5_VABI dlsym_nid_postfix(void*, const char*); +int APS5_VABI dlclose_nid_postfix(void*); +char* APS5_VABI dlerror_nid_postfix(); +} +static void Require(bool value) { if (!value) std::abort(); } +int main(int argc, char** argv) { + Require(argc == 2); + Require(dlerror_nid_postfix() == nullptr); + Require(dlopen_nid_postfix(argv[1], 0x2000) == nullptr); + Require(dlerror_nid_postfix() != nullptr); + Require(dlerror_nid_postfix() == nullptr); + Require(dlopen_nid_postfix("anyps5-missing-module-for-test.prx", 2) == nullptr); + Require(dlerror_nid_postfix() != nullptr); + void* executable = dlopen_nid_postfix(nullptr, 2); + Require(executable != nullptr && dlclose_nid_postfix(executable) == 0); + void* module = dlopen_nid_postfix(argv[1], 2 | 0x100); + Require(module != nullptr); + using Add = int (APS5_VABI *)(int, int); + auto add = reinterpret_cast(dlsym_nid_postfix(module, "GuestModuleAdd")); + Require(add && add(17, 25) == 42); + Require(dlsym_nid_postfix(reinterpret_cast(-2), "GuestModuleAdd") == reinterpret_cast(add)); + Require(dlsym_nid_postfix(module, "missing_symbol") == nullptr); + std::thread other([] { Require(dlerror_nid_postfix() == nullptr); }); + other.join(); + Require(dlerror_nid_postfix() != nullptr); + Require(dlerror_nid_postfix() == nullptr); + void* second = dlopen_nid_postfix(argv[1], 1); + Require(second && second != module); + Require(dlclose_nid_postfix(module) == 0); + Require(dlsym_nid_postfix(module, "GuestModuleAdd") == nullptr); + add = reinterpret_cast(dlsym_nid_postfix(second, "GuestModuleAdd")); + Require(add && add(2, 3) == 5); + Require(dlclose_nid_postfix(second) == 0); + Require(dlclose_nid_postfix(second) == -1); +} diff --git a/core/libs/tests/GuestModuleFixture.cpp b/core/libs/tests/GuestModuleFixture.cpp new file mode 100644 index 000000000..e8f8521d6 --- /dev/null +++ b/core/libs/tests/GuestModuleFixture.cpp @@ -0,0 +1,9 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#ifdef _WIN32 +#define MODULE_EXPORT __declspec(dllexport) +#else +#define MODULE_EXPORT __attribute__((visibility("default"))) +#endif +extern "C" MODULE_EXPORT int APS5_VABI GuestModuleAdd_nid_postfix(int a, int b) { + return a + b; +} diff --git a/docs/YAMAGI_QUAKE2.md b/docs/YAMAGI_QUAKE2.md index bd3753009..0d26407ae 100644 --- a/docs/YAMAGI_QUAKE2.md +++ b/docs/YAMAGI_QUAKE2.md @@ -288,3 +288,27 @@ PS5 mount semantics, cross-volume behavior, or replacement of existing directori The unchanged converted game resolves remove and rename. Startup now stops at `biY+kcVB5D4` (`dlsym`), requiring dynamic-module symbol lookup. It still has not executed the guest entry point or displayed a frame. + +## Follow-up: dynamic-module lookup + +Added dlopen, dlsym, dlclose, and dlerror to libkernel. Handles are tracked rather +than exposing native loader handles. Symbol lookup tries the literal export and +then its PS5 NID, reusing the project's existing hash implementation. Repeated +opens hold separate loader references. Error messages are thread-local and are +consumed by dlerror. Native unload runs outside the module registry lock. + +A test builds and NID-patches a separate shared module, loads it, finds an export +by its original name, and calls it through the guest SysV ABI. It also checks +global visibility for explicitly opened modules, invalid/closed handles, +repeated opens, missing symbols, unsupported flags, and error isolation. +All eighteen suites pass. + +This is host-compatible module loading, not raw PS5 ELF loading on Windows. +RTLD_LAZY/NOW and GLOBAL are accepted; Windows binds eagerly. Default lookup +currently searches explicitly registered global modules only. Automatic lookup +across the startup dependency graph, RTLD_NEXT/SELF, other flags, and console +load/start entry points remain unsupported. The guest path resolver is reused; +full console library-search and mount semantics are not established. + +Latest unchanged-game startup resolves dlsym and stops at `DYivN1nO-JQ` +(`getcwd`). No guest entry-point execution or rendered frame has been observed. From 1b45b0fe686fff1aea1842dcec27ac95f4d4bb7f Mon Sep 17 00:00:00 2001 From: SamG-Coder <13449963+SamG-Coder@users.noreply.github.com> Date: Fri, 25 Sep 2026 06:51:37 +1000 Subject: [PATCH 23/94] feat(libc): track guest working directory and resolve relative paths --- core/libs/CMakeLists.txt | 9 +++ core/libs/prx/libc/src/General.cpp | 92 +++++++++++++++++++---- core/libs/tests/GuestWorkingDirectory.cpp | 40 ++++++++++ docs/YAMAGI_QUAKE2.md | 17 +++++ 4 files changed, 143 insertions(+), 15 deletions(-) create mode 100644 core/libs/tests/GuestWorkingDirectory.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 64601945e..644cff8d6 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -104,6 +104,15 @@ endforeach() add_custom_target(libs DEPENDS ${PATCHED_OUTPUTS}) if(BUILD_TESTING) + add_executable(guest_working_directory_tests tests/GuestWorkingDirectory.cpp) + target_include_directories(guest_working_directory_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_working_directory_tests PRIVATE libc) + configure_windows_unwind(guest_working_directory_tests) + add_test(NAME guest_working_directory COMMAND guest_working_directory_tests) + if(WIN32) + set_tests_properties(guest_working_directory PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_library(guest_module_fixture SHARED tests/GuestModuleFixture.cpp) target_include_directories(guest_module_fixture PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) add_custom_command(TARGET guest_module_fixture POST_BUILD diff --git a/core/libs/prx/libc/src/General.cpp b/core/libs/prx/libc/src/General.cpp index 7abd12819..f75c39264 100644 --- a/core/libs/prx/libc/src/General.cpp +++ b/core/libs/prx/libc/src/General.cpp @@ -1,24 +1,86 @@ #include #include #include - +#include +#include +#include #include "prx/libc/include/General.hpp" +#include "prx/libc/include/GuestHeap.hpp" + +namespace { +struct WorkingDirectory { + std::mutex mutex; + const std::filesystem::path root = std::filesystem::canonical(std::filesystem::current_path()); + std::filesystem::path current = root; +}; +WorkingDirectory& Directories() { static WorkingDirectory state; return state; } +std::filesystem::path Resolve(WorkingDirectory& state, const char* path) { + std::string text(path); + for (auto& character : text) if (character == '\\') character = '/'; + std::filesystem::path input(text); +#ifdef _WIN32 + // Preserve the existing ability to pass explicit native drive paths. + if (input.has_root_name()) return input; +#endif + auto guest = (std::filesystem::path("/") / state.current.lexically_relative(state.root)); + guest = (input.is_absolute() ? input : guest / input).lexically_normal(); + return (state.root / guest.relative_path()).make_preferred(); +} +int DirectoryFailure(const std::error_code& error) { + if (error == std::errc::permission_denied) return 13; + if (error == std::errc::not_a_directory) return 20; + if (error == std::errc::no_such_file_or_directory) return 2; + if (error == std::errc::filename_too_long) return 63; + if (error == std::errc::too_many_symbolic_link_levels) return 62; + return 5; +} +} extern "C" std::filesystem::path ResolvePath_nid_no_patch(const char* path) { - if (path == nullptr) { - APS5_INVALID_ARG_EX; - } - std::string s(path); - std::size_t start = 0; - while (start < s.size() && (s[start] == '/' || s[start] == '\\')) { - ++start; - } - std::size_t end = s.size(); - while (end > start && (s[end - 1] == '/' || s[end - 1] == '\\')) { - --end; - } - std::filesystem::path result = std::filesystem::current_path() / std::filesystem::path(s.substr(start, end - start)); - return result.make_preferred(); + if (!path) { APS5_INVALID_ARG_EX; } + auto& state = Directories(); + std::lock_guard lock(state.mutex); + return Resolve(state, path); +} + +extern "C" int APS5_VABI chdir_nid_postfix(const char* path) { + if (!path) { errno = 14; return -1; } + if (!*path) { errno = 2; return -1; } + try { + auto& state = Directories(); + std::lock_guard lock(state.mutex); + std::error_code error; + const auto resolved = std::filesystem::canonical(Resolve(state, path), error); + if (error) { errno = DirectoryFailure(error); return -1; } + if (!std::filesystem::is_directory(resolved, error)) { + errno = error ? DirectoryFailure(error) : 20; return -1; + } + const auto relative = resolved.lexically_relative(state.root); + if (relative.empty() || *relative.begin() == "..") { errno = 45; return -1; } + state.current = resolved; + return 0; + } catch (const std::bad_alloc&) { errno = 12; return -1; } + catch (const std::filesystem::filesystem_error& error) { errno = DirectoryFailure(error.code()); return -1; } +} + +extern "C" char* APS5_VABI getcwd_nid_postfix(char* buffer, std::size_t size) { + if (buffer && size == 0) { errno = 22; return nullptr; } + try { + auto& state = Directories(); + std::lock_guard lock(state.mutex); + std::error_code error; + if (!std::filesystem::is_directory(state.current, error)) { + errno = error ? DirectoryFailure(error) : 2; return nullptr; + } + const auto relative = state.current.lexically_relative(state.root); + const auto path = relative == "." ? std::string("/") : "/" + relative.generic_string(); + const auto required = path.size() + 1; + if ((buffer || size) && size < required) { errno = 34; return nullptr; } + if (!buffer) buffer = static_cast(GuestHeap::GuestHeapAllocate_nid_postfix(size ? size : required)); + std::memcpy(buffer, path.c_str(), required); + return buffer; + } catch (const std::bad_alloc&) { errno = 12; return nullptr; } + catch (const std::filesystem::filesystem_error& error) { errno = DirectoryFailure(error.code()); return nullptr; } } extern "C" void NotImplemented_nid_no_patch(const char* funcName) { diff --git a/core/libs/tests/GuestWorkingDirectory.cpp b/core/libs/tests/GuestWorkingDirectory.cpp new file mode 100644 index 000000000..feeb9a9f4 --- /dev/null +++ b/core/libs/tests/GuestWorkingDirectory.cpp @@ -0,0 +1,40 @@ +#include "prx/libc/include/General.hpp" +#include "prx/libc/include/GuestHeap.hpp" +#include +#include +#include +#include +extern "C" { +int APS5_VABI chdir_nid_postfix(const char*); +char* APS5_VABI getcwd_nid_postfix(char*, std::size_t); +int* APS5_VABI __error_nid_postfix(); +} +static void Require(bool value) { if (!value) std::abort(); } +int main() { + const auto host = std::filesystem::canonical(std::filesystem::current_path()); + char path[1024]; + Require(getcwd_nid_postfix(path, sizeof(path)) == path && std::strcmp(path, "/") == 0); + const auto name = "anyps5-cwd-test-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); + const auto directory = host / name; + Require(std::filesystem::create_directory(directory)); + { std::ofstream file(directory / "sample.txt"); file << "sample"; } + Require(chdir_nid_postfix(name.c_str()) == 0); + Require(std::filesystem::current_path() == host); + Require(getcwd_nid_postfix(path, sizeof(path)) == path && path == "/" + name); + Require(ResolvePath_nid_no_patch("sample.txt") == directory / "sample.txt"); + Require(ResolvePath_nid_no_patch(("/" + name + "/sample.txt").c_str()) == directory / "sample.txt"); + char tiny[] = "xyz"; + Require(getcwd_nid_postfix(tiny, 2) == nullptr && *__error_nid_postfix() == 34); + Require(std::strcmp(tiny, "xyz") == 0); + char* allocated = getcwd_nid_postfix(nullptr, 0); + Require(allocated && std::strcmp(allocated, path) == 0); + GuestHeap::GuestHeapFree_nid_postfix(allocated); + Require(chdir_nid_postfix("sample.txt") == -1 && *__error_nid_postfix() == 20); + Require(chdir_nid_postfix("missing") == -1 && *__error_nid_postfix() == 2); + Require(chdir_nid_postfix("..") == 0); + Require(getcwd_nid_postfix(path, sizeof(path)) == path && std::strcmp(path, "/") == 0); + Require(chdir_nid_postfix("../..") == 0); + Require(getcwd_nid_postfix(path, sizeof(path)) == path && std::strcmp(path, "/") == 0); + std::filesystem::remove(directory / "sample.txt"); + std::filesystem::remove(directory); +} diff --git a/docs/YAMAGI_QUAKE2.md b/docs/YAMAGI_QUAKE2.md index 0d26407ae..222dca1b2 100644 --- a/docs/YAMAGI_QUAKE2.md +++ b/docs/YAMAGI_QUAKE2.md @@ -312,3 +312,20 @@ full console library-search and mount semantics are not established. Latest unchanged-game startup resolves dlsym and stops at `DYivN1nO-JQ` (`getcwd`). No guest entry-point execution or rendered frame has been observed. + +## Follow-up: working-directory state + +Added getcwd and chdir with shared guest path state. The initial host working +directory is captured as the guest root; guest cwd begins at `/`. Relative paths +follow guest cwd while absolute guest paths remain rooted at that captured +directory. Guest chdir does not change the host process cwd. Canonical directory +targets within that root are supported; targets outside it return EOPNOTSUPP. +Explicit Windows drive paths remain accepted by the general resolver for +compatibility with existing host-path callers. This is not a filesystem sandbox +or a complete PS5 mount implementation. + +Tests verify root/parent traversal, relative and absolute resolution after chdir, +missing/non-directory failures, unchanged host cwd, short-buffer handling, and +getcwd allocation freed through the guest heap. All nineteen suites pass. +Latest unchanged-game startup resolves getcwd and stops at `oVkZ8W8-Q8A` +(`strtok`). Guest execution and rendered output remain unverified. From 730afc79f8fdf27ca86cf03c6e5d6d26d40542dc Mon Sep 17 00:00:00 2001 From: SamG-Coder <13449963+SamG-Coder@users.noreply.github.com> Date: Fri, 25 Sep 2026 06:52:05 +1000 Subject: [PATCH 24/94] feat(libc): add independent guest strtok state --- core/libs/prx/libc/src/StringExtensions.cpp | 6 ++++++ core/libs/tests/GuestStrings.cpp | 10 ++++++++++ docs/YAMAGI_QUAKE2.md | 7 +++++++ 3 files changed, 23 insertions(+) diff --git a/core/libs/prx/libc/src/StringExtensions.cpp b/core/libs/prx/libc/src/StringExtensions.cpp index 5f31137aa..7289514f2 100644 --- a/core/libs/prx/libc/src/StringExtensions.cpp +++ b/core/libs/prx/libc/src/StringExtensions.cpp @@ -36,6 +36,7 @@ std::size_t APS5_VABI strlcat_nid_postfix(char* destination, const char* source, char* APS5_VABI strtok_r_nid_postfix(char* text, const char* delimiters, char** state) { if (text == nullptr) text = *state; + if (text == nullptr) return nullptr; text += std::strspn(text, delimiters); if (*text == '\0') { *state = text; @@ -47,4 +48,9 @@ char* APS5_VABI strtok_r_nid_postfix(char* text, const char* delimiters, char** return text; } +char* APS5_VABI strtok_nid_postfix(char* text, const char* delimiters) { + thread_local char* state = nullptr; + return strtok_r_nid_postfix(text, delimiters, &state); +} + } diff --git a/core/libs/tests/GuestStrings.cpp b/core/libs/tests/GuestStrings.cpp index 7357ec8d2..cd1c9dce8 100644 --- a/core/libs/tests/GuestStrings.cpp +++ b/core/libs/tests/GuestStrings.cpp @@ -10,6 +10,7 @@ char* APS5_VABI strpbrk_nid_postfix(const char*, const char*); std::size_t APS5_VABI strcspn_nid_postfix(const char*, const char*); std::size_t APS5_VABI strlcat_nid_postfix(char*, const char*, std::size_t); char* APS5_VABI strtok_r_nid_postfix(char*, const char*, char**); +char* APS5_VABI strtok_nid_postfix(char*, const char*); char* APS5_VABI strcasestr_nid_postfix(const char*, const char*); } @@ -49,6 +50,15 @@ int main() { Require(strtok_r_nid_postfix(nullptr, ",", &firstState) == nullptr); Require(strtok_r_nid_postfix(nullptr, ",", &firstState) == nullptr); Require(std::strcmp(strtok_r_nid_postfix(nullptr, "", &secondState), "y") == 0); + char hostTokens[] = "host:next"; + Require(std::strcmp(std::strtok(hostTokens, ":"), "host") == 0); + char guestTokens[] = ",one,,two:three"; + Require(std::strcmp(strtok_nid_postfix(guestTokens, ","), "one") == 0); + Require(std::strcmp(strtok_nid_postfix(nullptr, ":,"), "two") == 0); + Require(std::strcmp(strtok_nid_postfix(nullptr, ""), "three") == 0); + Require(strtok_nid_postfix(nullptr, ",") == nullptr); + Require(strtok_nid_postfix(nullptr, ",") == nullptr); + Require(std::strcmp(std::strtok(nullptr, ":"), "next") == 0); const char text[] = "aABAbC"; Require(strcasestr_nid_postfix(text, "ababc") == text + 1); Require(strcasestr_nid_postfix(text, "") == text); diff --git a/docs/YAMAGI_QUAKE2.md b/docs/YAMAGI_QUAKE2.md index 222dca1b2..a3f9672a5 100644 --- a/docs/YAMAGI_QUAKE2.md +++ b/docs/YAMAGI_QUAKE2.md @@ -329,3 +329,10 @@ missing/non-directory failures, unchanged host cwd, short-buffer handling, and getcwd allocation freed through the guest heap. All nineteen suites pass. Latest unchanged-game startup resolves getcwd and stops at `oVkZ8W8-Q8A` (`strtok`). Guest execution and rendered output remain unverified. + +Added strtok using the existing strtok_r scanner with independent thread-local +guest state. Tests exercise repeated delimiters, changing delimiter sets, empty +delimiter sets, repeated end-of-input calls, and independence from host strtok. +All nineteen suites pass after rebuilding the libraries. Latest unchanged-game +startup resolves strtok and stops at `88Vv-AzHVj8` (`fmodf`). The game has still +not reached its entry point or produced a rendered frame. From c7d26dda5e622fccf51a3c9b8171ee582d0e5886 Mon Sep 17 00:00:00 2001 From: SamG-Coder <13449963+SamG-Coder@users.noreply.github.com> Date: Fri, 25 Sep 2026 06:54:49 +1000 Subject: [PATCH 25/94] feat(libc): add missing guest math exports and 64-bit rounding --- core/libs/CMakeLists.txt | 9 +++++ core/libs/prx/libc/src/Math.cpp | 22 ++++++++++++ core/libs/tests/GuestMath.cpp | 60 +++++++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+) create mode 100644 core/libs/tests/GuestMath.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 644cff8d6..8256607b2 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -104,6 +104,15 @@ endforeach() add_custom_target(libs DEPENDS ${PATCHED_OUTPUTS}) if(BUILD_TESTING) + add_executable(guest_math_tests tests/GuestMath.cpp) + target_include_directories(guest_math_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_math_tests PRIVATE libc) + configure_windows_unwind(guest_math_tests) + add_test(NAME guest_math COMMAND guest_math_tests) + if(WIN32) + set_tests_properties(guest_math PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_executable(guest_working_directory_tests tests/GuestWorkingDirectory.cpp) target_include_directories(guest_working_directory_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_working_directory_tests PRIVATE libc) diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index 87253bcf0..1ca55ee4d 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -1,9 +1,31 @@ #include +#include #include "prx/libc/include/General.hpp" extern "C" { +float APS5_VABI fmodf_nid_postfix(float x, float y) { return std::fmod(x, y); } +float APS5_VABI asinf_nid_postfix(float x) { return std::asin(x); } +float APS5_VABI acosf_nid_postfix(float x) { return std::acos(x); } +float APS5_VABI atan2f_nid_postfix(float y, float x) { return std::atan2(y, x); } +float APS5_VABI tanf_nid_postfix(float x) { return std::tan(x); } +float APS5_VABI log10f_nid_postfix(float x) { return std::log10(x); } +double APS5_VABI exp2_nid_postfix(double x) { return std::exp2(x); } +double APS5_VABI ldexp_nid_postfix(double x, int exponent) { return std::ldexp(x, exponent); } +double APS5_VABI scalbn_nid_postfix(double x, int exponent) { return std::scalbn(x, exponent); } +float APS5_VABI scalbnf_nid_postfix(float x, int exponent) { return std::scalbn(x, exponent); } +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); } +// 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 std::llround(x); } +std::int64_t APS5_VABI lroundf_nid_postfix(float x) { return std::llround(x); } +std::int64_t APS5_VABI llround_nid_postfix(double x) { return std::llround(x); } +int APS5_VABI __isfinitef_nid_postfix(float x) { return std::isfinite(x) ? 1 : 0; } +int APS5_VABI __isnormal_nid_postfix(double x) { return std::isnormal(x) ? 1 : 0; } +int APS5_VABI __isnormalf_nid_postfix(float x) { return std::isnormal(x) ? 1 : 0; } +int APS5_VABI __isinff_nid_postfix(float x) { return std::isinf(x) ? 1 : 0; } + double APS5_VABI cbrt_nid_postfix(double x) { return std::cbrt(x); } double APS5_VABI asin_nid_postfix(double x) { return std::asin(x); } double APS5_VABI acos_nid_postfix(double x) { return std::acos(x); } diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp new file mode 100644 index 000000000..92d33d69c --- /dev/null +++ b/core/libs/tests/GuestMath.cpp @@ -0,0 +1,60 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +extern "C" { +float APS5_VABI fmodf_nid_postfix(float, float); +float APS5_VABI asinf_nid_postfix(float); +float APS5_VABI acosf_nid_postfix(float); +float APS5_VABI atan2f_nid_postfix(float, float); +float APS5_VABI tanf_nid_postfix(float); +float APS5_VABI log10f_nid_postfix(float); +double APS5_VABI exp2_nid_postfix(double); +double APS5_VABI ldexp_nid_postfix(double, int); +double APS5_VABI scalbn_nid_postfix(double, int); +float APS5_VABI scalbnf_nid_postfix(float, int); +double APS5_VABI frexp_nid_postfix(double, int*); +float APS5_VABI frexpf_nid_postfix(float, int*); +std::int64_t APS5_VABI lround_nid_postfix(double); +std::int64_t APS5_VABI lroundf_nid_postfix(float); +std::int64_t APS5_VABI llround_nid_postfix(double); +int APS5_VABI __isfinitef_nid_postfix(float); +int APS5_VABI __isnormal_nid_postfix(double); +int APS5_VABI __isnormalf_nid_postfix(float); +int APS5_VABI __isinff_nid_postfix(float); +} +static void Require(bool value) { if (!value) std::abort(); } +int main() { + 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))); + Require(std::isnan(fmodf_nid_postfix(1.f, 0.f))); + Require(std::abs(asinf_nid_postfix(0.5f) - 0.5235988f) < 0.000001f); + Require(std::abs(acosf_nid_postfix(0.5f) - 1.0471976f) < 0.000001f); + Require(std::abs(atan2f_nid_postfix(1.f, -1.f) - 2.3561945f) < 0.000001f); + Require(tanf_nid_postfix(0.f) == 0.f); + Require(log10f_nid_postfix(100.f) == 2.f); + Require(exp2_nid_postfix(-3.) == 0.125); + Require(ldexp_nid_postfix(0.75, 4) == 12.); + Require(scalbn_nid_postfix(0.75, -2) == 0.1875); + Require(scalbnf_nid_postfix(0.75f, 4) == 12.f); + int exponent = 0; + Require(frexp_nid_postfix(12., &exponent) == 0.75 && exponent == 4); + Require(frexpf_nid_postfix(-12.f, &exponent) == -0.75f && exponent == 4); + Require(lround_nid_postfix(4294967296.5) == INT64_C(4294967297)); + Require(lround_nid_postfix(-2.5) == -3); + Require(lroundf_nid_postfix(4294967296.f) == INT64_C(4294967296)); + Require(lroundf_nid_postfix(2.5f) == 3); + Require(llround_nid_postfix(-4294967296.5) == -INT64_C(4294967297)); + const auto infinity = std::numeric_limits::infinity(); + const auto nan = std::numeric_limits::quiet_NaN(); + Require(__isinff_nid_postfix(infinity) == 1 && __isinff_nid_postfix(-infinity) == 1); + Require(__isinff_nid_postfix(nan) == 0 && __isinff_nid_postfix(1.f) == 0); + Require(__isfinitef_nid_postfix(0.f) == 1 && __isfinitef_nid_postfix(infinity) == 0); + Require(__isfinitef_nid_postfix(nan) == 0); + Require(__isnormalf_nid_postfix(1.f) == 1 && __isnormalf_nid_postfix(0.f) == 0); + Require(__isnormalf_nid_postfix(std::numeric_limits::denorm_min()) == 0); + Require(__isnormal_nid_postfix(1.) == 1 && __isnormal_nid_postfix(0.) == 0); + Require(__isnormal_nid_postfix(std::numeric_limits::denorm_min()) == 0); +} From 7d07187ac8d17db5617433ae51e29ec9432e955f Mon Sep 17 00:00:00 2001 From: SamG-Coder <13449963+SamG-Coder@users.noreply.github.com> Date: Fri, 25 Sep 2026 06:55:31 +1000 Subject: [PATCH 26/94] feat(libc): add floating-point string conversions --- core/libs/prx/libc/src/Strings.cpp | 7 +++++++ core/libs/tests/GuestMath.cpp | 14 ++++++++++++++ docs/YAMAGI_QUAKE2.md | 20 ++++++++++++++++++++ 3 files changed, 41 insertions(+) diff --git a/core/libs/prx/libc/src/Strings.cpp b/core/libs/prx/libc/src/Strings.cpp index 667ce9dd0..108af376c 100644 --- a/core/libs/prx/libc/src/Strings.cpp +++ b/core/libs/prx/libc/src/Strings.cpp @@ -97,6 +97,13 @@ double APS5_VABI strtod_nid_postfix(const char* str, char** endptr) { return std::strtod(str, endptr); } +double APS5_VABI atof_nid_postfix(const char* str) { return std::atof(str); } +float APS5_VABI strtof_nid_postfix(const char* str, char** endptr) { return std::strtof(str, endptr); } +long double APS5_VABI strtold_nid_postfix(const char* str, char** endptr) { + static_assert(sizeof(long double) == 16, "Guest long double requires x87 extended precision storage"); + return std::strtold(str, endptr); +} + int APS5_VABI atoi_nid_postfix(const char* str) { return std::atoi(str); } diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index 92d33d69c..96ccf6af8 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -4,6 +4,10 @@ #include #include extern "C" { +double APS5_VABI atof_nid_postfix(const char*); +float APS5_VABI strtof_nid_postfix(const char*, char**); +long double APS5_VABI strtold_nid_postfix(const char*, char**); +int* APS5_VABI __error_nid_postfix(); float APS5_VABI fmodf_nid_postfix(float, float); float APS5_VABI asinf_nid_postfix(float); float APS5_VABI acosf_nid_postfix(float); @@ -26,6 +30,16 @@ int APS5_VABI __isinff_nid_postfix(float); } static void Require(bool value) { if (!value) std::abort(); } int main() { + Require(atof_nid_postfix(" -12.5tail") == -12.5); + char* end = nullptr; + const char input[] = "0x1.8p+2 remainder"; + Require(strtof_nid_postfix(input, &end) == 6.f && end == input + 8); + const char invalid[] = "invalid"; + Require(strtof_nid_postfix(invalid, &end) == 0.f && end == invalid); + *__error_nid_postfix() = 0; + Require(std::isinf(strtof_nid_postfix("1e1000", nullptr))); + Require(*__error_nid_postfix() == 34); + Require(strtold_nid_postfix("1.0000000000000000001!", &end) > 1.L && *end == '!'); 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/YAMAGI_QUAKE2.md b/docs/YAMAGI_QUAKE2.md index a3f9672a5..b1881722e 100644 --- a/docs/YAMAGI_QUAKE2.md +++ b/docs/YAMAGI_QUAKE2.md @@ -336,3 +336,23 @@ delimiter sets, repeated end-of-input calls, and independence from host strtok. All nineteen suites pass after rebuilding the libraries. Latest unchanged-game startup resolves strtok and stops at `88Vv-AzHVj8` (`fmodf`). The game has still not reached its entry point or produced a rendered frame. + +## Follow-up: math and floating-point parsing + +Added nineteen math exports covering single-precision remainder/trigonometry, +binary scaling and decomposition, integer rounding, and value classification. +Guest lround/lroundf return explicit 64-bit integers through host llround, avoiding +Windows' 32-bit long truncation. __isinff returns one for either sign of infinity, +matching the [FreeBSD 11 implementation](https://raw.githubusercontent.com/freebsd/freebsd-src/releng/11.4/lib/libc/gen/isinf.c). +Added atof, strtof, and strtold using the host CRT through the guest calling +convention. Tests verify extended-precision results through the long-double ABI. + +Tests cover negative remainders and signed zero, NaN/infinity/subnormal classes, +integer results above 32 bits, exponent output pointers, numeric parsing end +pointers, invalid input, and overflow errno. The math suite passes and the full +suite contains twenty passing tests. Exhaustive numerical accuracy, alternate +rounding modes, locale-dependent parsing, and all floating-point exception +semantics have not been validated. + +The unchanged game resolves fmodf and atof. Startup now stops at `TYE4irxSmko` +(`toupper`), still before guest entry-point execution or rendered output. From 39b81364b76d116d9edbe03aecd1f88194f5ea95 Mon Sep 17 00:00:00 2001 From: SamG-Coder <13449963+SamG-Coder@users.noreply.github.com> Date: Fri, 25 Sep 2026 06:58:41 +1000 Subject: [PATCH 27/94] feat(libc): add C-locale character APIs and guest basename --- core/libs/CMakeLists.txt | 20 +++++++-- core/libs/prx/libc/src/LocaleSupport.cpp | 19 +++++++++ core/libs/prx/libc/src/StringExtensions.cpp | 16 ++++++++ core/libs/tests/GuestCharacter.cpp | 45 +++++++++++++++++++++ core/libs/tests/GuestStrings.cpp | 12 ++++++ docs/YAMAGI_QUAKE2.md | 18 +++++++++ 6 files changed, 126 insertions(+), 4 deletions(-) create mode 100644 core/libs/tests/GuestCharacter.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 8256607b2..f00f78d4e 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -104,6 +104,15 @@ endforeach() add_custom_target(libs DEPENDS ${PATCHED_OUTPUTS}) if(BUILD_TESTING) + add_executable(guest_character_tests tests/GuestCharacter.cpp) + target_include_directories(guest_character_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_character_tests PRIVATE libc) + configure_windows_unwind(guest_character_tests) + add_test(NAME guest_character COMMAND guest_character_tests) + if(WIN32) + set_tests_properties(guest_character PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_executable(guest_math_tests tests/GuestMath.cpp) target_include_directories(guest_math_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_math_tests PRIVATE libc) @@ -248,12 +257,15 @@ if(BUILD_TESTING) set_tests_properties(guest_memory PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() - add_executable(guest_string_tests - tests/GuestStrings.cpp - prx/libc/src/StringExtensions.cpp - prx/libScePosixForWebKit/Export.cpp) + add_executable(guest_string_tests tests/GuestStrings.cpp) target_include_directories(guest_string_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_string_tests PRIVATE libc libScePosixForWebKit) + configure_windows_unwind(guest_string_tests) add_test(NAME guest_strings COMMAND guest_string_tests) + if(WIN32) + set_tests_properties(guest_strings PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() add_executable(application_heap_tests EXCLUDE_FROM_ALL tests/ApplicationHeap.cpp prx/libc/src/ApplicationHeap.cpp) diff --git a/core/libs/prx/libc/src/LocaleSupport.cpp b/core/libs/prx/libc/src/LocaleSupport.cpp index b16a1bb7c..0d178f899 100644 --- a/core/libs/prx/libc/src/LocaleSupport.cpp +++ b/core/libs/prx/libc/src/LocaleSupport.cpp @@ -84,6 +84,25 @@ constexpr auto g_upperTable = MakeCaseTable(true); extern "C" { +// The runtime currently exposes the classic C locale. Keep byte classification +// independent of any locale selected by host-side libraries. +int APS5_VABI isupper_nid_postfix(int c) { return c >= 'A' && c <= 'Z'; } +int APS5_VABI islower_nid_postfix(int c) { return c >= 'a' && c <= 'z'; } +int APS5_VABI isalpha_nid_postfix(int c) { return isupper_nid_postfix(c) || islower_nid_postfix(c); } +int APS5_VABI isdigit_nid_postfix(int c) { return c >= '0' && c <= '9'; } +int APS5_VABI isalnum_nid_postfix(int c) { return isalpha_nid_postfix(c) || isdigit_nid_postfix(c); } +int APS5_VABI isspace_nid_postfix(int c) { return c == ' ' || (c >= '\t' && c <= '\r'); } +int APS5_VABI isblank_nid_postfix(int c) { return c == ' ' || c == '\t'; } +int APS5_VABI iscntrl_nid_postfix(int c) { return (c >= 0 && c < 32) || c == 127; } +int APS5_VABI isprint_nid_postfix(int c) { return c >= 32 && c <= 126; } +int APS5_VABI isgraph_nid_postfix(int c) { return c >= 33 && c <= 126; } +int APS5_VABI ispunct_nid_postfix(int c) { return isgraph_nid_postfix(c) && !isalnum_nid_postfix(c); } +int APS5_VABI isxdigit_nid_postfix(int c) { + return isdigit_nid_postfix(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); +} +int APS5_VABI toupper_nid_postfix(int c) { return islower_nid_postfix(c) ? c - ('a' - 'A') : c; } +int APS5_VABI tolower_nid_postfix(int c) { return isupper_nid_postfix(c) ? c + ('a' - 'A') : c; } + std::uint64_t _ZNSt5ctypeIcE2idE_nid_postfix = 0; std::uint64_t _ZNSt5ctypeIwE2idE_nid_postfix = 0; std::uint64_t _ZNSt7collateIwE2idE_nid_postfix = 0; diff --git a/core/libs/prx/libc/src/StringExtensions.cpp b/core/libs/prx/libc/src/StringExtensions.cpp index 7289514f2..9533fd26b 100644 --- a/core/libs/prx/libc/src/StringExtensions.cpp +++ b/core/libs/prx/libc/src/StringExtensions.cpp @@ -1,9 +1,25 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include #include +#include +#include extern "C" { +char* APS5_VABI basename_nid_postfix(const char* path) { + thread_local char buffer[1024]; + std::string_view name = path && *path ? path : "."; + while (name.size() > 1 && name.back() == '/') name.remove_suffix(1); + if (name != "/") { + const auto separator = name.find_last_of('/'); + if (separator != std::string_view::npos) name.remove_prefix(separator + 1); + } + if (name.size() >= sizeof(buffer)) { errno = 63; return nullptr; } + std::memcpy(buffer, name.data(), name.size()); + buffer[name.size()] = '\0'; + return buffer; +} + std::size_t APS5_VABI strnlen_nid_postfix(const char* text, std::size_t limit) { std::size_t length = 0; while (length < limit && text[length] != '\0') ++length; diff --git a/core/libs/tests/GuestCharacter.cpp b/core/libs/tests/GuestCharacter.cpp new file mode 100644 index 000000000..60331fc87 --- /dev/null +++ b/core/libs/tests/GuestCharacter.cpp @@ -0,0 +1,45 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +extern "C" { +int APS5_VABI isupper_nid_postfix(int); +int APS5_VABI islower_nid_postfix(int); +int APS5_VABI isalpha_nid_postfix(int); +int APS5_VABI isdigit_nid_postfix(int); +int APS5_VABI isalnum_nid_postfix(int); +int APS5_VABI isspace_nid_postfix(int); +int APS5_VABI isblank_nid_postfix(int); +int APS5_VABI iscntrl_nid_postfix(int); +int APS5_VABI isprint_nid_postfix(int); +int APS5_VABI isgraph_nid_postfix(int); +int APS5_VABI ispunct_nid_postfix(int); +int APS5_VABI isxdigit_nid_postfix(int); +int APS5_VABI toupper_nid_postfix(int); +int APS5_VABI tolower_nid_postfix(int); +const short* APS5_VABI _Getptolower_nid_postfix(); +const short* APS5_VABI _Getptoupper_nid_postfix(); +} +static void Require(bool value) { if (!value) std::abort(); } +int main() { + Require(std::setlocale(LC_CTYPE, "C") != nullptr); + for (int c = EOF; c <= 255; ++c) { + Require(bool(isupper_nid_postfix(c)) == bool(std::isupper(c))); + Require(bool(islower_nid_postfix(c)) == bool(std::islower(c))); + Require(bool(isalpha_nid_postfix(c)) == bool(std::isalpha(c))); + Require(bool(isdigit_nid_postfix(c)) == bool(std::isdigit(c))); + Require(bool(isalnum_nid_postfix(c)) == bool(std::isalnum(c))); + Require(bool(isspace_nid_postfix(c)) == bool(std::isspace(c))); + Require(bool(isblank_nid_postfix(c)) == bool(std::isblank(c))); + Require(bool(iscntrl_nid_postfix(c)) == bool(std::iscntrl(c))); + Require(bool(isprint_nid_postfix(c)) == bool(std::isprint(c))); + Require(bool(isgraph_nid_postfix(c)) == bool(std::isgraph(c))); + Require(bool(ispunct_nid_postfix(c)) == bool(std::ispunct(c))); + Require(bool(isxdigit_nid_postfix(c)) == bool(std::isxdigit(c))); + Require(toupper_nid_postfix(c) == std::toupper(c)); + Require(tolower_nid_postfix(c) == std::tolower(c)); + Require(toupper_nid_postfix(c) == _Getptoupper_nid_postfix()[c]); + Require(tolower_nid_postfix(c) == _Getptolower_nid_postfix()[c]); + } +} diff --git a/core/libs/tests/GuestStrings.cpp b/core/libs/tests/GuestStrings.cpp index cd1c9dce8..cc8eefab6 100644 --- a/core/libs/tests/GuestStrings.cpp +++ b/core/libs/tests/GuestStrings.cpp @@ -2,8 +2,11 @@ #include #include #include +#include extern "C" { +char* APS5_VABI basename_nid_postfix(const char*); +int* APS5_VABI __error_nid_postfix(); std::size_t APS5_VABI strnlen_nid_postfix(const char*, std::size_t); char* APS5_VABI strncat_nid_postfix(char*, const char*, std::size_t); char* APS5_VABI strpbrk_nid_postfix(const char*, const char*); @@ -22,6 +25,15 @@ static void Require(bool condition) { } int main() { + Require(std::strcmp(basename_nid_postfix(nullptr), ".") == 0); + Require(std::strcmp(basename_nid_postfix(""), ".") == 0); + Require(std::strcmp(basename_nid_postfix("////"), "/") == 0); + const char path[] = "/one/two///"; + Require(std::strcmp(basename_nid_postfix(path), "two") == 0); + Require(std::strcmp(path, "/one/two///") == 0); + Require(std::strcmp(basename_nid_postfix("one\\two"), "one\\two") == 0); + const std::string longName(1024, 'x'); + Require(basename_nid_postfix(longName.c_str()) == nullptr && *__error_nid_postfix() == 63); const char bounded[] = {'a', 'b', 'c'}; Require(strnlen_nid_postfix(bounded, 0) == 0); Require(strnlen_nid_postfix(bounded, sizeof(bounded)) == 3); diff --git a/docs/YAMAGI_QUAKE2.md b/docs/YAMAGI_QUAKE2.md index b1881722e..10495b95a 100644 --- a/docs/YAMAGI_QUAKE2.md +++ b/docs/YAMAGI_QUAKE2.md @@ -356,3 +356,21 @@ semantics have not been validated. The unchanged game resolves fmodf and atof. Startup now stops at `TYE4irxSmko` (`toupper`), still before guest entry-point execution or rendered output. + +## Follow-up: character classification and basename + +Added fourteen byte-character classification and case-conversion exports for +the runtime's classic C locale. Tests compare every byte value and EOF against +C-locale classification and verify case conversion agrees with existing guest +locale tables. Non-C locales are not implemented by these exports. + +Added basename with non-mutating input handling, trailing-slash removal, +root/empty/null cases, and the SDK's 1024-byte component buffer limit. The +contract follows [FreeBSD 11 basename](https://raw.githubusercontent.com/freebsd/freebsd-src/releng/11.4/lib/libc/gen/basename.c); +result storage is thread-local. Tests include unchanged input, literal backslash +handling, and ENAMETOOLONG. String tests now link the built compatibility +libraries rather than compiling implementation copies into the test executable. + +All twenty-one suites pass. The unchanged converted game resolves toupper and +basename and now stops at `5qP1iVQkdck` (`ftello`). Guest entry-point execution and +rendered output remain unverified. From 79edb20f17283422c56eebbc01969d18f5f47b56 Mon Sep 17 00:00:00 2001 From: SamG-Coder <13449963+SamG-Coder@users.noreply.github.com> Date: Fri, 25 Sep 2026 07:00:45 +1000 Subject: [PATCH 28/94] fix(libc): preserve 64-bit guest stream offsets --- core/libs/prx/libc/src/FileAndHeap.cpp | 37 ++++++++++++++++++------ core/libs/tests/GuestStandardStreams.cpp | 23 +++++++++++++++ docs/YAMAGI_QUAKE2.md | 17 +++++++++++ 3 files changed, 68 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index adfaff080..2bc2d6cb7 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include "prx/libc/include/FileStream.hpp" #include "prx/libc/include/ApplicationHeap.hpp" @@ -70,19 +71,37 @@ size_t APS5_VABI fwrite_nid_postfix(const void* buffer, size_t size, size_t coun return result; } -int APS5_VABI fseek_nid_postfix(FileStream* stream, long offset, int origin) { - if (origin != SEEK_SET && origin != SEEK_CUR && origin != SEEK_END) throw std::runtime_error("fseek: invalid origin"); - if (std::fseek(GetNativeStream(stream), offset, origin) != 0) throw std::runtime_error("fseek: seek failed"); +int APS5_VABI fseeko_nid_postfix(FileStream* stream, std::int64_t offset, int origin) { + if (origin != SEEK_SET && origin != SEEK_CUR && origin != SEEK_END) { errno = 22; return -1; } +#ifdef _WIN32 + const int result = _fseeki64(GetNativeStream(stream), offset, origin); +#else + static_assert(sizeof(off_t) == 8); + const int result = ::fseeko(GetNativeStream(stream), offset, origin); +#endif + const int nativeError = errno; stream->SyncStatus(); - return 0; -} - -long APS5_VABI ftell_nid_postfix(FileStream* stream) { - const auto result = std::ftell(GetNativeStream(stream)); - if (result == -1L) throw std::runtime_error("ftell: position query failed"); + if (result) errno = nativeError == EOVERFLOW ? 84 : nativeError; return result; } +std::int64_t APS5_VABI ftello_nid_postfix(FileStream* stream) { +#ifdef _WIN32 + const auto result = _ftelli64(GetNativeStream(stream)); +#else + static_assert(sizeof(off_t) == 8); + const auto result = ::ftello(GetNativeStream(stream)); +#endif + if (result == -1 && errno == EOVERFLOW) errno = 84; + return result; +} + +int APS5_VABI fseek_nid_postfix(FileStream* stream, std::int64_t offset, int origin) { + return fseeko_nid_postfix(stream, offset, origin); +} + +std::int64_t APS5_VABI ftell_nid_postfix(FileStream* stream) { return ftello_nid_postfix(stream); } + int APS5_VABI fputs_nid_postfix(const char* str, FileStream* stream) { if (!str) throw std::runtime_error("fputs: null string"); const int result = std::fputs(str, GetNativeStream(stream)); diff --git a/core/libs/tests/GuestStandardStreams.cpp b/core/libs/tests/GuestStandardStreams.cpp index add7614aa..f4d85607e 100644 --- a/core/libs/tests/GuestStandardStreams.cpp +++ b/core/libs/tests/GuestStandardStreams.cpp @@ -5,6 +5,11 @@ #include #include extern "C" { +int APS5_VABI fseeko_nid_postfix(FileStream*, std::int64_t, int); +std::int64_t APS5_VABI ftello_nid_postfix(FileStream*); +int APS5_VABI fseek_nid_postfix(FileStream*, std::int64_t, int); +std::int64_t APS5_VABI ftell_nid_postfix(FileStream*); +int* APS5_VABI __error_nid_postfix(); extern FileStream* __stdinp_nid_postfix; extern FileStream* __stdoutp_nid_postfix; extern FileStream* __stderrp_nid_postfix; @@ -79,4 +84,22 @@ int main() { Require(fgets_nid_postfix(output, sizeof(output), &formatted) == output); Require(std::strcmp(output, " 3.50:end") == 0); formatted.Close(); + + FileStream positioned(std::tmpfile()); + constexpr std::int64_t largeOffset = INT64_C(4294967313); + Require(fseeko_nid_postfix(&positioned, largeOffset, SEEK_SET) == 0); + Require(ftello_nid_postfix(&positioned) == largeOffset); + Require(ftell_nid_postfix(&positioned) == largeOffset); + Require(fseek_nid_postfix(&positioned, -9, SEEK_CUR) == 0); + Require(ftello_nid_postfix(&positioned) == largeOffset - 9); + Require(fseek_nid_postfix(&positioned, largeOffset, SEEK_SET) == 0); + Require(ftell_nid_postfix(&positioned) == largeOffset); + Require(fseeko_nid_postfix(&positioned, 0, 12345) == -1 && *__error_nid_postfix() == 22); + Require(ftello_nid_postfix(&positioned) == largeOffset); + Require(fseeko_nid_postfix(&positioned, 0, SEEK_END) == 0); + Require(ftello_nid_postfix(&positioned) == 0); // Seeking alone did not extend the file. + Require(fgetc_nid_postfix(&positioned) == EOF && feof_nid_postfix(&positioned)); + Require(fseeko_nid_postfix(&positioned, 0, SEEK_SET) == 0); + Require(!feof_nid_postfix(&positioned)); + positioned.Close(); } diff --git a/docs/YAMAGI_QUAKE2.md b/docs/YAMAGI_QUAKE2.md index 10495b95a..dcdc3e9a5 100644 --- a/docs/YAMAGI_QUAKE2.md +++ b/docs/YAMAGI_QUAKE2.md @@ -374,3 +374,20 @@ libraries rather than compiling implementation copies into the test executable. All twenty-one suites pass. The unchanged converted game resolves toupper and basename and now stops at `5qP1iVQkdck` (`ftello`). Guest entry-point execution and rendered output remain unverified. + +## Follow-up: 64-bit stream positioning + +Added ftello/fseeko and corrected fseek/ftell to use explicit 64-bit guest +offsets and return values. Windows uses _fseeki64/_ftelli64, avoiding native +32-bit long truncation. Invalid origins and native seek failures return errors +rather than throwing; successful seeks refresh the guest stream status. + +Tests seek past 4 GiB without writing a large file, verify both API pairs, +relative seeks, invalid-origin position preservation, SEEK_END, and clearing +EOF after seeking. All twenty-one suites pass and libraries rebuild. + +Latest unchanged-game startup resolves ftello and stops at `JoBqSQt1yyA` +(`sceSystemServiceLoadExec`). Inspection of the released port's entry source +shows it requests the console's `exit` operation from its quit path. This is +source evidence only; the guest entry point still has not executed and no frame +has been rendered. From 2920c7f04cbc1b05ffa8c3e18e4b22c384e2a95c Mon Sep 17 00:00:00 2001 From: SamG-Coder <13449963+SamG-Coder@users.noreply.github.com> Date: Fri, 25 Sep 2026 07:02:37 +1000 Subject: [PATCH 29/94] feat(system): handle console exit through runtime shutdown --- core/libs/CMakeLists.txt | 12 ++++++++ core/libs/prx/libSceSystemService/Export.cpp | 12 ++++++++ core/libs/tests/GuestSystemExit.cpp | 30 ++++++++++++++++++++ docs/YAMAGI_QUAKE2.md | 17 +++++++++++ 4 files changed, 71 insertions(+) create mode 100644 core/libs/tests/GuestSystemExit.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index f00f78d4e..c7b0e51ab 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -104,6 +104,18 @@ endforeach() add_custom_target(libs DEPENDS ${PATCHED_OUTPUTS}) if(BUILD_TESTING) + add_executable(guest_system_exit_tests tests/GuestSystemExit.cpp) + target_include_directories(guest_system_exit_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_system_exit_tests PRIVATE libSceSystemService libc) + 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) + set_tests_properties(guest_system_exit PROPERTIES + PASS_REGULAR_EXPRESSION "Guest shutdown and atexit completed") + if(WIN32) + set_tests_properties(guest_system_exit guest_system_exit_parameters PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_executable(guest_character_tests tests/GuestCharacter.cpp) target_include_directories(guest_character_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_character_tests PRIVATE libc) diff --git a/core/libs/prx/libSceSystemService/Export.cpp b/core/libs/prx/libSceSystemService/Export.cpp index 3756dc36f..a9274a3cb 100644 --- a/core/libs/prx/libSceSystemService/Export.cpp +++ b/core/libs/prx/libSceSystemService/Export.cpp @@ -1,12 +1,24 @@ #include #include #include +#include +#include "prx/libc/include/Shutdown.hpp" #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "prx/libSceSystemService/SystemService.hpp" extern "C" { +int APS5_VABI sceSystemServiceLoadExec(const char* path, const char* const* arguments) { + if (!path || !*path) return SYSTEM_SERVICE_ERROR_PARAMETER; + if (std::strcmp(path, "exit") != 0) { + NotImplemented_nid_no_patch("sceSystemServiceLoadExec: executable replacement"); + } + (void)arguments; + LibcRunShutdown_nid_postfix(); + std::exit(0); +} + int APS5_VABI sceSystemServiceDisableNoticeScreenSkipFlagAutoSet(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/tests/GuestSystemExit.cpp b/core/libs/tests/GuestSystemExit.cpp new file mode 100644 index 000000000..fc83c47eb --- /dev/null +++ b/core/libs/tests/GuestSystemExit.cpp @@ -0,0 +1,30 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/Shutdown.hpp" +#include "prx/libSceSystemService/SystemService.hpp" +#include +#include +#include +extern "C" int APS5_VABI sceSystemServiceLoadExec(const char*, const char* const*); +namespace { +bool cleaned = false; +void Cleanup() { cleaned = true; } +void VerifyExit() { + if (!cleaned) std::_Exit(1); + std::puts("Guest shutdown and atexit completed"); +} +void Require(bool value) { if (!value) std::abort(); } +} +int main(int argc, char**) { + if (argc > 1) { + LibcRegisterShutdown_nid_postfix(Cleanup); + Require(std::atexit(VerifyExit) == 0); + sceSystemServiceLoadExec("exit", nullptr); + return 2; + } + Require(sceSystemServiceLoadExec(nullptr, nullptr) == SYSTEM_SERVICE_ERROR_PARAMETER); + Require(sceSystemServiceLoadExec("", nullptr) == SYSTEM_SERVICE_ERROR_PARAMETER); + bool rejected = false; + try { sceSystemServiceLoadExec("/app0/another.bin", nullptr); } + catch (const std::runtime_error&) { rejected = true; } + Require(rejected); +} diff --git a/docs/YAMAGI_QUAKE2.md b/docs/YAMAGI_QUAKE2.md index dcdc3e9a5..6be6905af 100644 --- a/docs/YAMAGI_QUAKE2.md +++ b/docs/YAMAGI_QUAKE2.md @@ -391,3 +391,20 @@ Latest unchanged-game startup resolves ftello and stops at `JoBqSQt1yyA` shows it requests the console's `exit` operation from its quit path. This is source evidence only; the guest entry point still has not executed and no frame has been rendered. + +## Follow-up: console exit request + +Implemented sceSystemServiceLoadExec for the `exit` operation through the shared +runtime shutdown callbacks followed by normal process exit. Null/empty paths +return the existing system-service parameter error. Replacing the process with +another executable remains explicitly unsupported and raises the runtime's +existing not-implemented diagnostic; it does not report a successful launch. + +An isolated CTest process verifies that shutdown callbacks execute before +atexit handlers and that the service does not return after requesting exit. +A separate test checks invalid arguments and executable-replacement rejection. +All twenty-three tests pass and libraries rebuild. + +The unchanged game resolves sceSystemServiceLoadExec and now stops at +`gkWgn0p1AfU` (`freopen`). This does not establish that the game has executed +its quit path: guest entry-point execution and rendered output remain unverified. From dd0debe967f33c70f07d3dce0de68a914e657cd7 Mon Sep 17 00:00:00 2001 From: SamG-Coder <13449963+SamG-Coder@users.noreply.github.com> Date: Fri, 25 Sep 2026 07:05:07 +1000 Subject: [PATCH 30/94] feat(libc): reopen guest streams while preserving identity --- core/libs/prx/libc/include/FileStream.hpp | 14 +++++++++++++ core/libs/prx/libc/src/FileAndHeap.cpp | 19 ++++++++++++++++++ core/libs/tests/GuestStandardStreams.cpp | 24 +++++++++++++++++++++++ 3 files changed, 57 insertions(+) diff --git a/core/libs/prx/libc/include/FileStream.hpp b/core/libs/prx/libc/include/FileStream.hpp index 5df643c16..00c9ebcb9 100644 --- a/core/libs/prx/libc/include/FileStream.hpp +++ b/core/libs/prx/libc/include/FileStream.hpp @@ -65,6 +65,20 @@ public: } GuestFilePrefix& GuestState() { return _guest; } + bool Reopen(const char* filename, const char* mode) { + auto* previous = GetHandle(); + _guest = {}; + _handle = std::freopen(filename, mode, previous); + if (!_handle) return false; + _guest.flags = 0x10; +#ifdef _WIN32 + const int descriptor = _fileno(_handle); +#else + const int descriptor = ::fileno(_handle); +#endif + _guest.descriptor = descriptor >= 0 && descriptor <= 32767 ? static_cast(descriptor) : -1; + return true; + } void SyncStatus() { _guest.readRemaining = 0; _guest.writeRemaining = 0; diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index 2bc2d6cb7..79b7d70fb 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -16,6 +16,25 @@ extern "C" { [[noreturn]] void APS5_VABI _ZSt11_Xbad_allocv_nid_postfix(); +FileStream* APS5_VABI freopen_nid_postfix(const char* filename, const char* mode, FileStream* stream) { + if (!stream || !mode) { errno = 22; return nullptr; } + if (!filename) { errno = 45; return nullptr; } // Mode-only reopening is not supported. + const char* supported[] = {"r", "w", "a", "rb", "wb", "ab", "r+", "w+", "a+", + "rb+", "wb+", "ab+", "r+b", "w+b", "a+b"}; + bool valid = false; + for (const auto* candidate : supported) if (std::strcmp(mode, candidate) == 0) valid = true; + if (!valid) { errno = 22; return nullptr; } + try { + const auto path = *filename ? ResolvePath_nid_no_patch(filename).string() : std::string{}; + if (stream->Reopen(path.c_str(), mode)) return stream; + const int error = errno; + if (stream->IsDynamic()) delete stream; + errno = error; + return nullptr; + } catch (const std::bad_alloc&) { errno = 12; return nullptr; } + catch (const std::filesystem::filesystem_error&) { errno = 5; return nullptr; } +} + FileStream* APS5_VABI fopen_nid_postfix(const char* filename, const char* mode) { if (!filename || !mode) throw std::runtime_error(std::string(__func__) + ": " + FOPEN_MSG_NULL_ARG); const std::filesystem::path fpath = ResolvePath_nid_no_patch(filename); diff --git a/core/libs/tests/GuestStandardStreams.cpp b/core/libs/tests/GuestStandardStreams.cpp index f4d85607e..62ce707ca 100644 --- a/core/libs/tests/GuestStandardStreams.cpp +++ b/core/libs/tests/GuestStandardStreams.cpp @@ -4,7 +4,11 @@ #include #include #include +#include +#include +#include extern "C" { +FileStream* APS5_VABI freopen_nid_postfix(const char*, const char*, FileStream*); int APS5_VABI fseeko_nid_postfix(FileStream*, std::int64_t, int); std::int64_t APS5_VABI ftello_nid_postfix(FileStream*); int APS5_VABI fseek_nid_postfix(FileStream*, std::int64_t, int); @@ -102,4 +106,24 @@ int main() { Require(fseeko_nid_postfix(&positioned, 0, SEEK_SET) == 0); Require(!feof_nid_postfix(&positioned)); positioned.Close(); + + const auto filename = "anyps5-reopen-test-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); + FileStream redirected(std::tmpfile()); + Require(fgetc_nid_postfix(&redirected) == EOF && feof_nid_postfix(&redirected)); + Require(freopen_nid_postfix(filename.c_str(), "w+b", &redirected) == &redirected); + Require(!feof_nid_postfix(&redirected)); + Require(fputc_nid_postfix('R', &redirected) == 'R'); + Require(fseeko_nid_postfix(&redirected, 0, SEEK_SET) == 0); + Require(fgetc_nid_postfix(&redirected) == 'R'); + Require(freopen_nid_postfix(filename.c_str(), "ab", &redirected) == &redirected); + Require(fputc_nid_postfix('S', &redirected) == 'S'); + Require(freopen_nid_postfix(nullptr, "r", &redirected) == nullptr && *__error_nid_postfix() == 45); + redirected.Close(); + { std::ifstream input(filename, std::ios::binary); std::string contents; std::getline(input, contents); + Require(contents == "RS"); } + Require(std::filesystem::remove(filename)); + FileStream failed(std::tmpfile()); + Require(freopen_nid_postfix(filename.c_str(), "rb", &failed) == nullptr); + Require(*__error_nid_postfix() == 2); + Require(failed.GuestState().flags == 0 && failed.GuestState().descriptor == -1); } From 18ce03cba26b0ec95910552f418f130b0e0d099c Mon Sep 17 00:00:00 2001 From: SamG-Coder <13449963+SamG-Coder@users.noreply.github.com> Date: Fri, 25 Sep 2026 07:05:07 +1000 Subject: [PATCH 31/94] feat(libc): implement immediate guest process exit --- core/libs/CMakeLists.txt | 3 ++- core/libs/prx/libc/src/Process.cpp | 4 ++++ core/libs/tests/GuestSystemExit.cpp | 11 ++++++++++- docs/YAMAGI_QUAKE2.md | 18 ++++++++++++++++++ 4 files changed, 34 insertions(+), 2 deletions(-) diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index c7b0e51ab..1df1309a1 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -110,10 +110,11 @@ 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_immediate_exit COMMAND guest_system_exit_tests --immediate) set_tests_properties(guest_system_exit PROPERTIES PASS_REGULAR_EXPRESSION "Guest shutdown and atexit completed") if(WIN32) - set_tests_properties(guest_system_exit guest_system_exit_parameters PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_system_exit guest_system_exit_parameters guest_immediate_exit PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_executable(guest_character_tests tests/GuestCharacter.cpp) diff --git a/core/libs/prx/libc/src/Process.cpp b/core/libs/prx/libc/src/Process.cpp index ae3a6e938..49c5b9308 100644 --- a/core/libs/prx/libc/src/Process.cpp +++ b/core/libs/prx/libc/src/Process.cpp @@ -57,6 +57,10 @@ extern "C" void LibcRunShutdown_nid_postfix() { extern "C" { +[[noreturn]] void APS5_VABI _Exit_nid_postfix(int code) { + std::_Exit(code); +} + void APS5_VABI exit_nid_postfix(int code) { LibcRunShutdown_nid_postfix(); std::exit(code); diff --git a/core/libs/tests/GuestSystemExit.cpp b/core/libs/tests/GuestSystemExit.cpp index fc83c47eb..701aca721 100644 --- a/core/libs/tests/GuestSystemExit.cpp +++ b/core/libs/tests/GuestSystemExit.cpp @@ -4,7 +4,9 @@ #include #include #include +#include extern "C" int APS5_VABI sceSystemServiceLoadExec(const char*, const char* const*); +extern "C" void APS5_VABI _Exit_nid_postfix(int); namespace { bool cleaned = false; void Cleanup() { cleaned = true; } @@ -13,8 +15,15 @@ void VerifyExit() { std::puts("Guest shutdown and atexit completed"); } void Require(bool value) { if (!value) std::abort(); } +void UnexpectedCleanup() { std::_Exit(3); } } -int main(int argc, char**) { +int main(int argc, char** argv) { + if (argc > 1 && std::strcmp(argv[1], "--immediate") == 0) { + LibcRegisterShutdown_nid_postfix(UnexpectedCleanup); + Require(std::atexit(UnexpectedCleanup) == 0); + _Exit_nid_postfix(0); + return 2; + } if (argc > 1) { LibcRegisterShutdown_nid_postfix(Cleanup); Require(std::atexit(VerifyExit) == 0); diff --git a/docs/YAMAGI_QUAKE2.md b/docs/YAMAGI_QUAKE2.md index 6be6905af..2ee53fcc8 100644 --- a/docs/YAMAGI_QUAKE2.md +++ b/docs/YAMAGI_QUAKE2.md @@ -408,3 +408,21 @@ All twenty-three tests pass and libraries rebuild. The unchanged game resolves sceSystemServiceLoadExec and now stops at `gkWgn0p1AfU` (`freopen`). This does not establish that the game has executed its quit path: guest entry-point execution and rendered output remain unverified. + +## Follow-up: stream reopening and immediate exit + +Added freopen while preserving the guest FileStream address on success. Reopen +clears buffered guest metadata and refreshes the descriptor. A failed native +reopen closes the underlying file and clears the wrapper state; dynamically +allocated wrappers are released on failure. Tests exercise write/read reopening, +append behavior, unchanged wrapper identity, EOF clearing, and missing files. +Mode-only reopening (null filename) is explicitly unsupported; common r/w/a +modes with optional binary/update modifiers are accepted. + +Added _Exit through immediate host termination. An isolated process test proves +it bypasses both runtime cleanup and atexit handlers, complementing the normal +shutdown test. All twenty-four tests pass and the libraries rebuild. + +The unchanged game resolves freopen and _Exit. Startup now stops at +`MqAdbRMdNz4` (`sceAgcLinkShaders`), moving the next investigation into shader +linking. No guest entry-point execution or rendered frame has been observed. From d6f23df679c3d08adc60fdf864a358056fcf3089 Mon Sep 17 00:00:00 2001 From: SamG-Coder <13449963+SamG-Coder@users.noreply.github.com> Date: Fri, 25 Sep 2026 07:08:10 +1000 Subject: [PATCH 32/94] feat(agc): compose basic shader linkage register blocks --- core/libs/CMakeLists.txt | 9 +++ core/libs/prx/libSceAgc/CMakeLists.txt | 1 + .../prx/libSceAgc/Shader/src/LinkShaders.cpp | 35 +++++++++++ core/libs/tests/GuestShaderLink.cpp | 63 +++++++++++++++++++ docs/YAMAGI_QUAKE2.md | 22 +++++++ 5 files changed, 130 insertions(+) create mode 100644 core/libs/prx/libSceAgc/Shader/src/LinkShaders.cpp create mode 100644 core/libs/tests/GuestShaderLink.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 1df1309a1..61a60877d 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -104,6 +104,15 @@ endforeach() add_custom_target(libs DEPENDS ${PATCHED_OUTPUTS}) if(BUILD_TESTING) + add_executable(guest_shader_link_tests tests/GuestShaderLink.cpp) + target_include_directories(guest_shader_link_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_shader_link_tests PRIVATE libSceAgc libc) + configure_windows_unwind(guest_shader_link_tests) + add_test(NAME guest_shader_link COMMAND guest_shader_link_tests) + if(WIN32) + set_tests_properties(guest_shader_link PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_executable(guest_system_exit_tests tests/GuestSystemExit.cpp) target_include_directories(guest_system_exit_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_system_exit_tests PRIVATE libSceSystemService libc) diff --git a/core/libs/prx/libSceAgc/CMakeLists.txt b/core/libs/prx/libSceAgc/CMakeLists.txt index 6961369ba..3e4af7a62 100644 --- a/core/libs/prx/libSceAgc/CMakeLists.txt +++ b/core/libs/prx/libSceAgc/CMakeLists.txt @@ -43,6 +43,7 @@ add_library(libSceAgc SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Predication/src/Predication.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Shader/src/CreateShader.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Shader/src/InterpolantMapping.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Shader/src/LinkShaders.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Shader/src/PrimState.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Shader/src/ShaderUtils.cpp ) diff --git a/core/libs/prx/libSceAgc/Shader/src/LinkShaders.cpp b/core/libs/prx/libSceAgc/Shader/src/LinkShaders.cpp new file mode 100644 index 000000000..cac2bb956 --- /dev/null +++ b/core/libs/prx/libSceAgc/Shader/src/LinkShaders.cpp @@ -0,0 +1,35 @@ +#include "SceShaders.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libSceAgc/Shader/include/ShaderConstants.hpp" +#include +#include +#include + +extern "C" int APS5_VABI sceAgcCreatePrimState(ShaderRegister*, ShaderRegister*, const Shader*, const Shader*, std::uint32_t); +extern "C" int APS5_VABI sceAgcCreateInterpolantMapping(ShaderRegister*, const Shader*, const Shader*); + +extern "C" int APS5_VABI sceAgcLinkShaders(ShaderRegister* context, ShaderRegister* primitive, + const void* reserved, const Shader* vertex, const Shader* pixel, std::uint32_t primitiveType) { + using namespace ShaderRegs; + if (!context || !primitive || reserved || !vertex || + vertex->type != static_cast(ShaderBinaryType::Gs) || + (vertex->num_output_semantics && !vertex->output_semantics) || + (pixel && (pixel->type != static_cast(ShaderBinaryType::Ps) || + pixel->num_input_semantics > 32 || + (pixel->num_input_semantics && !pixel->input_semantics)))) + return GRAPHICS5_ERROR_INVALID_SHADER_PROGRAM; + + // The simple vertex/pixel pipeline combines two primitive context registers + // and 32 interpolant registers. Publish only after both helpers succeed. + std::array contextValues{}; + std::array primitiveValues{}; + try { + sceAgcCreatePrimState(contextValues.data(), primitiveValues.data(), nullptr, vertex, primitiveType); + sceAgcCreateInterpolantMapping(contextValues.data() + 2, vertex, pixel); + } catch (const std::runtime_error&) { + return GRAPHICS5_ERROR_INVALID_SHADER_PROGRAM; + } + std::copy(contextValues.begin(), contextValues.end(), context); + std::copy(primitiveValues.begin(), primitiveValues.end(), primitive); + return 0; +} diff --git a/core/libs/tests/GuestShaderLink.cpp b/core/libs/tests/GuestShaderLink.cpp new file mode 100644 index 000000000..17f32efc5 --- /dev/null +++ b/core/libs/tests/GuestShaderLink.cpp @@ -0,0 +1,63 @@ +#include "SceShaders.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libSceAgc/Shader/include/ShaderConstants.hpp" +#include +#include +#include +extern "C" int APS5_VABI sceAgcLinkShaders(ShaderRegister*, ShaderRegister*, const void*, const Shader*, const Shader*, std::uint32_t); +static void Require(bool value) { if (!value) std::abort(); } +int main() { + using namespace ShaderRegs; + ShaderSpecialRegs special{}; + special.vgt_shader_stages_en = {VGT_SHADER_STAGES_EN, VGT_SHADER_STAGES_NGG_BIT}; + special.vgt_gs_out_prim_type = {VGT_GS_OUT_PRIM_TYPE, 0}; + special.ge_cntl = {GE_CNTL, 0x123}; + special.ge_user_vgpr_en = {GE_USER_VGPR_EN, 7}; + Shader vertex{}; + vertex.type = static_cast(ShaderBinaryType::Gs); + vertex.specials = &special; + Shader pixel{}; + pixel.type = static_cast(ShaderBinaryType::Ps); + std::array context{}; + std::array primitive{}; + context.back() = {0xdeadbeef, 0xcafebabe}; + primitive.back() = context.back(); + Require(sceAgcLinkShaders(context.data(), primitive.data(), nullptr, &vertex, &pixel, 4) == 0); + Require(context[0].offset == VGT_SHADER_STAGES_EN && context[0].value == VGT_SHADER_STAGES_NGG_BIT); + Require(context[1].offset == VGT_GS_OUT_PRIM_TYPE && context[1].value == 2); + for (unsigned i = 0; i < 32; ++i) + Require(context[i + 2].offset == SPI_PS_INPUT_CNTL_0 + i && context[i + 2].value == i); + Require(primitive[0].offset == GE_CNTL && primitive[0].value == 0x123); + Require(primitive[1].offset == GE_USER_VGPR_EN && primitive[1].value == 7); + Require(primitive[2].offset == VGT_PRIMITIVE_TYPE && primitive[2].value == 4); + Require(context.back().value == 0xcafebabe && primitive.back().value == 0xcafebabe); + const auto savedContext = context; + const auto savedPrimitive = primitive; + pixel.num_input_semantics = 33; + Require(sceAgcLinkShaders(context.data(), primitive.data(), nullptr, &vertex, &pixel, 4) == GRAPHICS5_ERROR_INVALID_SHADER_PROGRAM); + Require(std::memcmp(context.data(), savedContext.data(), sizeof(context)) == 0); + Require(std::memcmp(primitive.data(), savedPrimitive.data(), sizeof(primitive)) == 0); + pixel.num_input_semantics = 0; + special.ge_cntl.offset = 0; + Require(sceAgcLinkShaders(context.data(), primitive.data(), nullptr, &vertex, &pixel, 4) == GRAPHICS5_ERROR_INVALID_SHADER_PROGRAM); + Require(std::memcmp(context.data(), savedContext.data(), sizeof(context)) == 0); + special.ge_cntl.offset = GE_CNTL; + Require(sceAgcLinkShaders(context.data(), primitive.data(), nullptr, &vertex, nullptr, 2) == 0); + Require(context[1].value == 1 && primitive[2].value == 2); + ShaderSemantic output{}; + output.semantic = 9; + output.hardware_mapping = 5; + vertex.output_semantics = &output; + vertex.num_output_semantics = 1; + ShaderSemantic input{}; + input.semantic = 9; + input.is_flat_shaded = 1; + pixel.input_semantics = &input; + pixel.num_input_semantics = 1; + Require(sceAgcLinkShaders(context.data(), primitive.data(), nullptr, &vertex, &pixel, 4) == 0); + Require(context[2].offset == SPI_PS_INPUT_CNTL_0 && context[2].value == (5 | 0x400)); + input.semantic = 10; + input.default_value = 2; + Require(sceAgcLinkShaders(context.data(), primitive.data(), nullptr, &vertex, &pixel, 4) == 0); + Require(context[2].value == 0x220); +} diff --git a/docs/YAMAGI_QUAKE2.md b/docs/YAMAGI_QUAKE2.md index 2ee53fcc8..d30259ec1 100644 --- a/docs/YAMAGI_QUAKE2.md +++ b/docs/YAMAGI_QUAKE2.md @@ -426,3 +426,25 @@ shutdown test. All twenty-four tests pass and the libraries rebuild. The unchanged game resolves freopen and _Exit. Startup now stops at `MqAdbRMdNz4` (`sceAgcLinkShaders`), moving the next investigation into shader linking. No guest entry-point execution or rendered frame has been observed. + +## Follow-up: basic shader linking + +Added sceAgcLinkShaders for the basic vertex/pixel pipeline by composing the +existing primitive-state and interpolant-mapping implementations. It produces +34 context-register records and three user-configuration records, matching the +buffer sizes consumed by the native ps5link-sdk gpu_cube example. The context +layout combines two primitive-state records with 32 interpolant records; this +composition is inferred from the existing helpers and native caller, not from +a captured hardware result. No homebrew rendering code was copied into the library. + +Inputs are validated and results are staged before touching caller outputs. +Tests check triangle/line primitive values, identity interpolants, semantic +matching, flat shading, missing-semantic defaults, buffer boundaries, and +unchanged outputs on invalid input. Reserved/non-basic pipeline inputs are +rejected. All twenty-five tests pass; comparison against real console register +output and end-to-end rendering remains outstanding. + +The unchanged game resolves sceAgcLinkShaders and stops at `Wi82ArQtAwg` +(`sceAgcGetRegisterDefaults`). The project already has a versioned defaults +implementation; the legacy entry point still needs its contract verified. +Guest entry-point execution and rendered output remain unverified. From 2187436d0d34c5fc8cd9019a1608bfc6ab1a640f Mon Sep 17 00:00:00 2001 From: SamG-Coder <13449963+SamG-Coder@users.noreply.github.com> Date: Fri, 25 Sep 2026 07:10:15 +1000 Subject: [PATCH 33/94] feat(agc): expose legacy register defaults entry point --- .../libSceAgc/Misc/src/RegisterDefaults.cpp | 5 +++++ core/libs/tests/GuestShaderLink.cpp | 19 +++++++++++++++++++ docs/YAMAGI_QUAKE2.md | 18 ++++++++++++++++++ 3 files changed, 42 insertions(+) diff --git a/core/libs/prx/libSceAgc/Misc/src/RegisterDefaults.cpp b/core/libs/prx/libSceAgc/Misc/src/RegisterDefaults.cpp index 4866a9186..820fb9a77 100644 --- a/core/libs/prx/libSceAgc/Misc/src/RegisterDefaults.cpp +++ b/core/libs/prx/libSceAgc/Misc/src/RegisterDefaults.cpp @@ -9,6 +9,11 @@ extern "C" { +void* APS5_VABI sceAgcGetRegisterDefaults() { + // The legacy API has no version argument; use the baseline public table. + return Agc::Command::GetRegisterDefaults(0, false, __func__); +} + void* APS5_VABI sceAgcGetRegisterDefaults2(std::uint32_t version) { return Agc::Command::GetRegisterDefaults(version, false, __func__); } diff --git a/core/libs/tests/GuestShaderLink.cpp b/core/libs/tests/GuestShaderLink.cpp index 17f32efc5..4163bb538 100644 --- a/core/libs/tests/GuestShaderLink.cpp +++ b/core/libs/tests/GuestShaderLink.cpp @@ -5,9 +5,28 @@ #include #include extern "C" int APS5_VABI sceAgcLinkShaders(ShaderRegister*, ShaderRegister*, const void*, const Shader*, const Shader*, std::uint32_t); +extern "C" void* APS5_VABI sceAgcGetRegisterDefaults(); +extern "C" void* APS5_VABI sceAgcGetRegisterDefaults2(std::uint32_t); static void Require(bool value) { if (!value) std::abort(); } int main() { using namespace ShaderRegs; + auto* defaults = static_cast(sceAgcGetRegisterDefaults()); + Require(defaults && defaults == sceAgcGetRegisterDefaults()); + Require(defaults == sceAgcGetRegisterDefaults2(0)); + // These are the byte offsets used by native callers, independent of the + // private C++ structure used to construct the defaults object. + ShaderRegister** contextBlocks = nullptr; + std::uint32_t contextCount = 0; + std::memcpy(&contextBlocks, defaults, sizeof(contextBlocks)); + std::memcpy(&contextCount, defaults + 0x20, sizeof(contextCount)); + Require(contextBlocks && contextBlocks[0] && contextCount == 523); + bool hasRenderTarget = false; + bool hasRasterizer = false; + for (std::uint32_t i = 0; i < contextCount; ++i) { + hasRenderTarget |= contextBlocks[0][i].offset == 0x318; + hasRasterizer |= contextBlocks[0][i].offset == 0x205; + } + Require(hasRenderTarget && hasRasterizer); ShaderSpecialRegs special{}; special.vgt_shader_stages_en = {VGT_SHADER_STAGES_EN, VGT_SHADER_STAGES_NGG_BIT}; special.vgt_gs_out_prim_type = {VGT_GS_OUT_PRIM_TYPE, 0}; diff --git a/docs/YAMAGI_QUAKE2.md b/docs/YAMAGI_QUAKE2.md index d30259ec1..6516b9272 100644 --- a/docs/YAMAGI_QUAKE2.md +++ b/docs/YAMAGI_QUAKE2.md @@ -448,3 +448,21 @@ The unchanged game resolves sceAgcLinkShaders and stops at `Wi82ArQtAwg` (`sceAgcGetRegisterDefaults`). The project already has a versioned defaults implementation; the legacy entry point still needs its contract verified. Guest entry-point execution and rendered output remain unverified. + +## Follow-up: legacy register-defaults entry point + +Added the no-argument sceAgcGetRegisterDefaults export using the existing +baseline public table. The legacy caller reads its context-block pointer at +offset 0 and its context-record count at offset 0x20; tests now exercise that +layout directly, scan all 523 context records, check render-target/rasterizer +register presence, and verify stable pointers across calls. + +Selecting public version zero is a compatibility assumption based on the legacy +API lacking a version parameter; it has not been compared with a captured +console response. Versioned APIs remain available. All twenty-five tests pass. + +The unchanged game now stops at `0MtUJ3BpGhE` +(`sceAgcDriverGetWaitRenderingPacketSizeInDwords`). The corresponding +sceAgcDriverWaitUntilSafeForRendering import is also absent. These need a real +command-packet and buffer-ownership implementation, not a zero-size/no-op shim. +Guest entry-point execution and rendered output are still unverified. From cca00c13ab4a1eb8e719e33ad2969e7045a2bb58 Mon Sep 17 00:00:00 2001 From: SamG-Coder <13449963+SamG-Coder@users.noreply.github.com> Date: Fri, 25 Sep 2026 07:13:15 +1000 Subject: [PATCH 34/94] feat(video): track ordered buffer reuse reservations --- core/libs/CMakeLists.txt | 9 ++++++ .../include/BufferReuseTracker.hpp | 29 +++++++++++++++++++ .../libSceVideoOut/include/VideoOutDriver.hpp | 3 ++ .../prx/libSceVideoOut/src/VideoOutDriver.cpp | 16 ++++++++-- core/libs/tests/GuestBufferReuse.cpp | 26 +++++++++++++++++ docs/YAMAGI_QUAKE2.md | 20 +++++++++++++ 6 files changed, 100 insertions(+), 3 deletions(-) create mode 100644 core/libs/prx/libSceVideoOut/include/BufferReuseTracker.hpp create mode 100644 core/libs/tests/GuestBufferReuse.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 61a60877d..afcf3cb1c 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -104,6 +104,15 @@ endforeach() add_custom_target(libs DEPENDS ${PATCHED_OUTPUTS}) if(BUILD_TESTING) + add_executable(guest_buffer_reuse_tests tests/GuestBufferReuse.cpp) + target_include_directories(guest_buffer_reuse_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_buffer_reuse_tests PRIVATE libc) + configure_windows_unwind(guest_buffer_reuse_tests) + add_test(NAME guest_buffer_reuse COMMAND guest_buffer_reuse_tests) + if(WIN32) + set_tests_properties(guest_buffer_reuse PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_executable(guest_shader_link_tests tests/GuestShaderLink.cpp) target_include_directories(guest_shader_link_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_shader_link_tests PRIVATE libSceAgc libc) diff --git a/core/libs/prx/libSceVideoOut/include/BufferReuseTracker.hpp b/core/libs/prx/libSceVideoOut/include/BufferReuseTracker.hpp new file mode 100644 index 000000000..da5c4bf26 --- /dev/null +++ b/core/libs/prx/libSceVideoOut/include/BufferReuseTracker.hpp @@ -0,0 +1,29 @@ +#pragma once +#include +#include +#include +#include +#include + +// Caller serializes access with the owning VideoOutConfig mutex. +class BufferReuseTracker { + std::uint64_t _issued = 0; + std::set _pending; +public: + std::uint64_t Reserve() { + if (_issued == std::numeric_limits::max()) + throw std::overflow_error("VideoOut: buffer reservation sequence exhausted"); + const auto ticket = _issued + 1; + _pending.insert(ticket); + _issued = ticket; + return ticket; + } + std::uint64_t Capture() const { return _issued; } + bool IsComplete(std::uint64_t through) const { + if (through > _issued) throw std::invalid_argument("VideoOut: unissued buffer fence"); + return _pending.empty() || *_pending.begin() > through; + } + void Complete(std::uint64_t ticket) noexcept { + if (_pending.erase(ticket) != 1) std::terminate(); + } +}; diff --git a/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp b/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp index e9212806f..2cd20fb3a 100644 --- a/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp +++ b/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp @@ -16,6 +16,7 @@ #include "prx/libSceAgcDriver/Execution/include/VideoOutput.hpp" #include "prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp" #include "prx/libSceVideoOut/include/DisplayWindow.hpp" +#include "prx/libSceVideoOut/include/BufferReuseTracker.hpp" #include "SDL.h" #include "SceTypes.hpp" @@ -111,6 +112,7 @@ struct VideoOutConfig { std::array buffers{}; std::array bufferPending{}; + std::array bufferReuse; std::array groups{}; void Check() const { @@ -122,6 +124,7 @@ struct VideoOutConfig { struct FlipQueue; struct FlipRequest final : AgcDriver::IFlipRequest, std::enable_shared_from_this { + std::uint64_t reuseTicket = 0; std::shared_ptr cfg; std::shared_ptr queue; uint64_t generation = 0; diff --git a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp index d4efe8873..d3440fdf8 100644 --- a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp +++ b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp @@ -62,6 +62,7 @@ public: } request->generation = cfg->generation; request->flipRate = cfg->flipRate; + if (info.index >= 0) request->reuseTicket = cfg->bufferReuse[info.index].Reserve(); ++queue->reservations; ++cfg->flipStatus.flipPendingNum; if (info.index >= 0) ++cfg->bufferPending[info.index]; @@ -96,7 +97,10 @@ FlipRequest::~FlipRequest() { if (!terminal) { --cfg->flipStatus.flipPendingNum; --queue->reservations; - if (index >= 0) --cfg->bufferPending[index]; + if (index >= 0) { + --cfg->bufferPending[index]; + cfg->bufferReuse[index].Complete(reuseTicket); + } cfg->vblankCond.notify_all(); } } @@ -132,7 +136,10 @@ void FlipRequest::Fail(std::exception_ptr error) noexcept { if (reserved && !terminal) { --cfg->flipStatus.flipPendingNum; --queue->reservations; - if (index >= 0) --cfg->bufferPending[index]; + if (index >= 0) { + --cfg->bufferPending[index]; + cfg->bufferReuse[index].Complete(reuseTicket); + } terminal = true; } cfg->vblankCond.notify_all(); @@ -390,7 +397,10 @@ void VideoOutDriver::processFlip(FlipRequest& req) { req.cfg->height = req.height; --req.cfg->flipStatus.flipPendingNum; --req.queue->reservations; - if (req.index >= 0) --req.cfg->bufferPending[req.index]; + if (req.index >= 0) { + --req.cfg->bufferPending[req.index]; + req.cfg->bufferReuse[req.index].Complete(req.reuseTicket); + } req.terminal = true; req.cfg->vblankCond.notify_all(); timing.Mark("notify_game"); diff --git a/core/libs/tests/GuestBufferReuse.cpp b/core/libs/tests/GuestBufferReuse.cpp new file mode 100644 index 000000000..06b629f32 --- /dev/null +++ b/core/libs/tests/GuestBufferReuse.cpp @@ -0,0 +1,26 @@ +#include "prx/libSceVideoOut/include/BufferReuseTracker.hpp" +#include +static void Require(bool value) { if (!value) std::abort(); } +int main() { + BufferReuseTracker buffer; + Require(buffer.IsComplete(buffer.Capture())); + const auto first = buffer.Reserve(); + const auto second = buffer.Reserve(); + const auto fence = buffer.Capture(); + const auto future = buffer.Reserve(); + Require(!buffer.IsComplete(fence)); + buffer.Complete(second); // Out-of-order retirement must not release first. + Require(!buffer.IsComplete(fence)); + buffer.Complete(first); + Require(buffer.IsComplete(fence)); // Later reservations do not block this wait. + Require(!buffer.IsComplete(buffer.Capture())); + buffer.Complete(future); // Cancellation uses the same retirement operation. + Require(buffer.IsComplete(buffer.Capture())); + const auto next = buffer.Reserve(); + Require(next > future && buffer.IsComplete(fence)); + buffer.Complete(next); + bool rejected = false; + try { buffer.IsComplete(next + 1); } + catch (const std::invalid_argument&) { rejected = true; } + Require(rejected); +} diff --git a/docs/YAMAGI_QUAKE2.md b/docs/YAMAGI_QUAKE2.md index 6516b9272..0503635a0 100644 --- a/docs/YAMAGI_QUAKE2.md +++ b/docs/YAMAGI_QUAKE2.md @@ -466,3 +466,23 @@ The unchanged game now stops at `0MtUJ3BpGhE` sceAgcDriverWaitUntilSafeForRendering import is also absent. These need a real command-packet and buffer-ownership implementation, not a zero-size/no-op shim. Guest entry-point execution and rendered output are still unverified. + +## Follow-up: buffer reuse ordering + +Added per-buffer reservation tickets to VideoOut. Flip reservations acquire a +monotonic ticket; presentation completion, failure, and reservation cancellation +retire it. A captured fence is complete once all earlier tickets retire, even +if later flips for the same buffer are already reserved. This avoids the +self-dependency that a wait on the total pending-buffer count would create when +submission reserves a future flip before executing its preceding wait packet. + +Tests cover out-of-order retirement, future reservations, empty fences, and +invalid future fences. All twenty-six CTest cases and the existing VideoOut +flip-lifetime executable pass. This is the ownership foundation; the rendering +wait exports and command-dispatch connection are not implemented yet, so the +game remains stopped at the same missing packet-size import. + +Inspection of the native ps5-opengl caller establishes the wait argument order +as command pointer, packet capacity, mode, video handle, buffer index. The +parameter names in the link-only stub do not reflect that calling convention. +No game runtime source was copied into the compatibility library. From 95dc22c173aaf1c0817709e15ae22f131b889083 Mon Sep 17 00:00:00 2001 From: SamG-Coder <13449963+SamG-Coder@users.noreply.github.com> Date: Fri, 25 Sep 2026 07:16:55 +1000 Subject: [PATCH 35/94] feat(agc): execute rendering waits against buffer reuse fences --- core/libs/CMakeLists.txt | 9 +++ .../Execution/include/VideoOutput.hpp | 13 +++ .../libSceAgcDriver/Execution/src/Driver.cpp | 13 ++- .../prx/libSceAgcDriver/Execution/src/Pm4.cpp | 4 +- .../prx/libSceAgcDriver/Submit/src/Dcb.cpp | 20 +++++ .../prx/libSceVideoOut/src/VideoOutDriver.cpp | 24 ++++++ core/libs/tests/GuestRenderingWait.cpp | 80 +++++++++++++++++++ docs/YAMAGI_QUAKE2.md | 24 ++++++ 8 files changed, 184 insertions(+), 3 deletions(-) create mode 100644 core/libs/tests/GuestRenderingWait.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index afcf3cb1c..d7d1421a1 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -104,6 +104,15 @@ endforeach() add_custom_target(libs DEPENDS ${PATCHED_OUTPUTS}) if(BUILD_TESTING) + add_executable(guest_rendering_wait_tests tests/GuestRenderingWait.cpp) + target_include_directories(guest_rendering_wait_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_rendering_wait_tests PRIVATE libSceAgcDriver libc) + configure_windows_unwind(guest_rendering_wait_tests) + add_test(NAME guest_rendering_wait COMMAND guest_rendering_wait_tests) + if(WIN32) + set_tests_properties(guest_rendering_wait PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_executable(guest_buffer_reuse_tests tests/GuestBufferReuse.cpp) target_include_directories(guest_buffer_reuse_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_buffer_reuse_tests PRIVATE libc) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VideoOutput.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VideoOutput.hpp index f5b6ab74a..79f96d8c1 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VideoOutput.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VideoOutput.hpp @@ -4,6 +4,7 @@ #include #include #include +#include namespace AgcDriver { @@ -11,6 +12,15 @@ class FrameTiming; inline constexpr std::uint32_t FlipPacketHeader = 0xc004105cu; inline constexpr std::uint32_t FlipPacketWords = 6; +// AnyPS5's internal encoding for the driver-generated rendering wait. +inline constexpr std::uint32_t RenderingWaitPacketHeader = 0xc0021018u; +inline constexpr std::uint32_t RenderingWaitPacketWords = 4; + +class IRenderingWait { +public: + virtual ~IRenderingWait() = default; + virtual void Wait() = 0; +}; struct FlipInfo { std::uint32_t handle; @@ -30,6 +40,9 @@ class IVideoOutput { public: virtual ~IVideoOutput() = default; virtual std::shared_ptr Reserve(const FlipInfo& info) = 0; + virtual std::shared_ptr CaptureRenderingWait(std::uint32_t index) { + throw std::runtime_error("VideoOut: rendering waits are unsupported by this output"); + } virtual void Fail(std::exception_ptr error) noexcept = 0; }; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver.cpp index a221b2015..58fca2e72 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver.cpp @@ -49,6 +49,7 @@ struct Submission { std::vector commands; std::map> shaders; std::map> flips; + std::map> renderingWaits; bool suspend = false; FrameTiming::Clock::time_point received; FrameTiming::Clock::time_point copied; @@ -118,6 +119,13 @@ public: require(accepted != std::numeric_limits::max(), "submission serial overflow"); for (std::size_t cursor = 0; cursor < submission.commands.size();) { const auto* words = submission.commands.data() + cursor; + if (words[0] == RenderingWaitPacketHeader) { + const auto output = outputs.find(words[1]); + require(output != outputs.end(), "rendering wait references an unregistered video output"); + auto wait = output->second->CaptureRenderingWait(words[2]); + require(wait != nullptr, "video output returned a null rendering wait"); + submission.renderingWaits.emplace(cursor, std::move(wait)); + } if (words[0] == FlipPacketHeader) { const auto output = outputs.find(words[1]); require(output != outputs.end(), "flip references an unregistered video output"); @@ -567,7 +575,10 @@ private: if (device != nullptr) device->WaitIdle(); timing.Mark("device_idle_wait"); } - if (header == FlipPacketHeader) { + if (header == RenderingWaitPacketHeader) { + submission.renderingWaits.at(cursor)->Wait(); + timing.Mark("rendering_wait"); + } else if (header == FlipPacketHeader) { CheckFailure(); timing.Mark("flip_prepare"); } else if (opcode == 0x15) { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp index c22d4a440..0c6acf712 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp @@ -91,8 +91,7 @@ std::string_view UnsupportedReason(std::uint32_t header) { const auto opcode = (header >> 8u) & 0xffu; if (opcode == 0x10) { switch ((header >> 2u) & 0x3fu) { - case 0: case 0x09: case 0x0b: case 0x0c: case 0x17: case 0x1a: return {}; - case 0x06: return "VideoOut buffer-completion wait interface is not implemented"; + case 0: case 0x06: case 0x09: case 0x0b: case 0x0c: case 0x17: case 0x1a: return {}; case 0x14: case 0x18: return "guest cache actions and GPU release events are not implemented"; default: return "custom packet has no implemented contract in the reference dispatch table"; } @@ -137,6 +136,7 @@ void Validate(std::span packet, std::uint32_t queue) { require((packet[1] & 0xffff0000u) != 0x68750000u, "typed user-data and legacy flip markers are not implemented"); break; case 0x09: size(2); break; + case 0x06: graphics(); size(4); require(packet[3] == 0, "unsupported rendering wait mode"); break; case 0x0b: { const auto data = std::as_bytes(packet.subspan(1)); require(std::find(data.begin(), data.end(), std::byte{}) != data.end(), "unterminated marker text"); diff --git a/core/libs/prx/libSceAgcDriver/Submit/src/Dcb.cpp b/core/libs/prx/libSceAgcDriver/Submit/src/Dcb.cpp index 6c217c0b4..48da441e6 100644 --- a/core/libs/prx/libSceAgcDriver/Submit/src/Dcb.cpp +++ b/core/libs/prx/libSceAgcDriver/Submit/src/Dcb.cpp @@ -1,5 +1,9 @@ #include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/VideoOutput.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include +#include #include #include @@ -8,6 +12,22 @@ extern "C" { +std::uint32_t APS5_VABI sceAgcDriverGetWaitRenderingPacketSizeInDwords() { + return AgcDriver::RenderingWaitPacketWords; +} + +std::uint32_t APS5_VABI sceAgcDriverWaitUntilSafeForRendering(std::uint32_t** command, + std::uint32_t capacity, std::uint32_t mode, std::uint32_t handle, int index) { + if (!command || capacity < AgcDriver::RenderingWaitPacketWords || mode != 0 || index < 0) + throw std::invalid_argument("AGC driver: invalid rendering wait arguments"); + AgcDriver::GuestMemory::CheckRange(command, sizeof(*command), alignof(std::uint32_t*), true); + AgcDriver::GuestMemory::CheckRange(*command, AgcDriver::RenderingWaitPacketWords * sizeof(std::uint32_t), alignof(std::uint32_t), true); + const std::uint32_t words[] = {AgcDriver::RenderingWaitPacketHeader, handle, static_cast(index), mode}; + std::copy(std::begin(words), std::end(words), *command); + *command += AgcDriver::RenderingWaitPacketWords; + return 0; +} + int APS5_VABI sceAgcDriverSubmitDcb(const Packet* packet) { AgcDriver::Submit(packet, 0); return 0; diff --git a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp index d3440fdf8..9f1b13b34 100644 --- a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp +++ b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp @@ -26,10 +26,34 @@ void checkConfig(const VideoOutConfig& cfg) { cfg.Check(); } +class RenderingWait final : public AgcDriver::IRenderingWait { + std::shared_ptr _config; + std::uint32_t _index; + std::uint64_t _ticket; +public: + RenderingWait(std::shared_ptr config, std::uint32_t index, std::uint64_t ticket) + : _config(std::move(config)), _index(index), _ticket(ticket) {} + void Wait() override { + std::unique_lock lock(_config->mutex); + _config->vblankCond.wait(lock, [&] { + return _config->failure || _config->closing || !_config->opened || + _config->bufferReuse[_index].IsComplete(_ticket); + }); + checkConfig(*_config); + } +}; + class VideoOutput final : public AgcDriver::IVideoOutput { public: VideoOutput(std::shared_ptr config, std::shared_ptr requests) : cfg(std::move(config)), queue(std::move(requests)) {} + std::shared_ptr CaptureRenderingWait(std::uint32_t index) override { + std::lock_guard lock(cfg->mutex); + checkConfig(*cfg); + require(index < VIDEO_OUT_BUFFER_NUM_MAX && cfg->buffers[index].Occupied(), "wait buffer is not registered"); + return std::make_shared(cfg, index, cfg->bufferReuse[index].Capture()); + } + std::shared_ptr Reserve(const AgcDriver::FlipInfo& info) override { require(info.mode == VIDEO_OUT_FLIP_MODE_VSYNC, "unsupported flip mode"); require(info.index >= VIDEO_OUT_BUFFER_INDEX_BLACK && info.index < VIDEO_OUT_BUFFER_NUM_MAX, "invalid flip index"); diff --git a/core/libs/tests/GuestRenderingWait.cpp b/core/libs/tests/GuestRenderingWait.cpp new file mode 100644 index 000000000..71d3cbe98 --- /dev/null +++ b/core/libs/tests/GuestRenderingWait.cpp @@ -0,0 +1,80 @@ +#include "prx/libSceAgcDriver/Execution/include/VideoOutput.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "prx/libSceVideoOut/include/BufferReuseTracker.hpp" +#include "prx/libc/include/Shutdown.hpp" +#include +#include +#include +#include +#include +extern "C" std::uint32_t APS5_VABI sceAgcDriverGetWaitRenderingPacketSizeInDwords(); +extern "C" std::uint32_t APS5_VABI sceAgcDriverWaitUntilSafeForRendering(std::uint32_t**, std::uint32_t, std::uint32_t, std::uint32_t, int); +static void Require(bool value) { if (!value) std::abort(); } +class Output final : public AgcDriver::IVideoOutput, public AgcDriver::IRenderingWait, + public AgcDriver::IFlipRequest, public std::enable_shared_from_this { +public: + std::mutex mutex; + std::condition_variable changed; + BufferReuseTracker reuse; + const std::uint64_t earlier = reuse.Reserve(); + std::uint64_t captured = 0, later = 0; + bool entered = false, flipped = false; + std::shared_ptr CaptureRenderingWait(std::uint32_t index) override { + Require(index == 0); + std::lock_guard lock(mutex); + captured = reuse.Capture(); + return shared_from_this(); + } + std::shared_ptr Reserve(const AgcDriver::FlipInfo& info) override { + Require(info.index == 0); + std::lock_guard lock(mutex); + later = reuse.Reserve(); + return shared_from_this(); + } + void Wait() override { + std::unique_lock lock(mutex); + entered = true; + changed.notify_all(); + Require(changed.wait_for(lock, std::chrono::seconds(5), [&] { return reuse.IsComplete(captured); })); + } + void GpuReady(const std::shared_ptr&) override { + std::lock_guard lock(mutex); + Require(reuse.IsComplete(captured)); + reuse.Complete(later); + flipped = true; + } + void Fail(std::exception_ptr) noexcept override { std::abort(); } +}; +int main() { + auto output = std::make_shared(); + AgcDriverRegisterVideoOutput_nid_postfix(7, output); + std::array words{}; + words.back() = 0x12345678; + auto* cursor = words.data(); + const auto size = sceAgcDriverGetWaitRenderingPacketSizeInDwords(); + Require(size == AgcDriver::RenderingWaitPacketWords); + bool rejected = false; + try { sceAgcDriverWaitUntilSafeForRendering(&cursor, size - 1, 0, 7, 0); } + catch (const std::invalid_argument&) { rejected = true; } + Require(rejected && cursor == words.data() && words[0] == 0); + Require(sceAgcDriverWaitUntilSafeForRendering(&cursor, size, 0, 7, 0) == 0); + Require(cursor == words.data() + size); + Require(words[0] == AgcDriver::RenderingWaitPacketHeader && words[1] == 7 && words[2] == 0 && words[3] == 0); + const std::uint32_t flip[] = {AgcDriver::FlipPacketHeader, 7, 0, 1, 0, 0}; + for (auto word : flip) *cursor++ = word; + Require(words.back() == 0x12345678); + Packet packet{words.data(), 10, 0, {}}; + sceAgcDriverSubmitDcb(&packet); + { + std::unique_lock lock(output->mutex); + Require(output->changed.wait_for(lock, std::chrono::seconds(5), [&] { return output->entered; })); + Require(!output->flipped && output->later > output->captured); + output->reuse.Complete(output->earlier); + output->changed.notify_all(); + } + AgcDriverWaitIdle_nid_postfix(); + Require(output->flipped); + AgcDriverUnregisterVideoOutput_nid_postfix(7, output); + LibcRunShutdown_nid_postfix(); +} diff --git a/docs/YAMAGI_QUAKE2.md b/docs/YAMAGI_QUAKE2.md index 0503635a0..0563951e6 100644 --- a/docs/YAMAGI_QUAKE2.md +++ b/docs/YAMAGI_QUAKE2.md @@ -486,3 +486,27 @@ Inspection of the native ps5-opengl caller establishes the wait argument order as command pointer, packet capacity, mode, video handle, buffer index. The parameter names in the link-only stub do not reflect that calling convention. No game runtime source was copied into the compatibility library. + +## Follow-up: rendering-wait command execution + +Implemented sceAgcDriverGetWaitRenderingPacketSizeInDwords and +sceAgcDriverWaitUntilSafeForRendering for mode zero. The writer validates +capacity and writable memory, emits a four-dword internal wait packet, and +advances the guest command pointer. This encoding is owned by AnyPS5's paired +writer/decoder; it is not claimed to reproduce console-generated packet bytes. + +Submission captures a VideoOut buffer ticket before reserving subsequent flips. +The worker executes the wait outside the GPU mutex. VideoOut waits for earlier +reservations to retire and wakes on close or failure. The captured configuration +object prevents a reopened handle from satisfying a wait for an old port. + +A test submits a generated wait followed by a flip through the real AGC driver +with a controlled output. It proves the wait blocks on earlier ownership and +continues after that ownership is released, despite the later flip reservation. +It also checks packet capacity, pointer advancement, and write boundaries. +All twenty-seven CTest cases pass, along with the existing VideoOut flip lifetime +and reopen regression executables. Actual console packet compatibility, other +wait modes, and rendering a game frame remain unverified. + +The unchanged game now resolves both rendering-wait imports and stops at +`Z4QosVuAsA0` (`pthread_once`). Guest entry-point execution has not yet occurred. From 322dc24c9eb378b52346d8437b8bc6bd699c91d7 Mon Sep 17 00:00:00 2001 From: SamG-Coder <13449963+SamG-Coder@users.noreply.github.com> Date: Fri, 25 Sep 2026 07:22:05 +1000 Subject: [PATCH 36/94] feat(kernel): implement guest once initialization --- core/libs/CMakeLists.txt | 10 +++++ core/libs/prx/libkernel/CMakeLists.txt | 1 + .../libs/prx/libkernel/Pthread/Posix/Once.cpp | 43 ++++++++++++++++++ core/libs/tests/GuestOnce.cpp | 44 +++++++++++++++++++ 4 files changed, 98 insertions(+) create mode 100644 core/libs/prx/libkernel/Pthread/Posix/Once.cpp create mode 100644 core/libs/tests/GuestOnce.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index d7d1421a1..20103697f 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -225,6 +225,16 @@ if(BUILD_TESTING) set_tests_properties(guest_standard_streams PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_executable(guest_once_tests tests/GuestOnce.cpp) + target_include_directories(guest_once_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_once_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_once_tests) + add_test(NAME guest_once COMMAND guest_once_tests) + set_tests_properties(guest_once PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_once PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_executable(guest_signal_tests tests/GuestSignals.cpp) target_include_directories(guest_signal_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_signal_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 08098fd1e..394bad513 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -22,6 +22,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Mutex.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Rwlock.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Thread.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Once.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Tsd.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Sem.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Socket/src/Socket.cpp diff --git a/core/libs/prx/libkernel/Pthread/Posix/Once.cpp b/core/libs/prx/libkernel/Pthread/Posix/Once.cpp new file mode 100644 index 000000000..bd7d8a24f --- /dev/null +++ b/core/libs/prx/libkernel/Pthread/Posix/Once.cpp @@ -0,0 +1,43 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include + +namespace { +// FreeBSD guest ABI: PTHREAD_ONCE_INIT is {0, nullptr}; completed state is 1. +struct GuestOnce { + std::int32_t state; + void* mutex; +}; +static_assert(offsetof(GuestOnce, mutex) == 8 && sizeof(GuestOnce) == 16); +std::mutex onceMutex; +std::condition_variable onceChanged; +} + +extern "C" int APS5_VABI pthread_once_nid_postfix( + GuestOnce* control, void (APS5_VABI *initialize)()) { + if (!control || !initialize) return 22; + std::unique_lock lock(onceMutex); + onceChanged.wait(lock, [&] { return control->state != 2; }); + if (control->state == 1) return 0; + if (control->state != 0) return 22; + control->state = 2; + lock.unlock(); + // Initializers may initialize other once controls. Never run guest code + // under the shared lock. The lock publishes all initializer writes. + try { + initialize(); + } catch (...) { + lock.lock(); + control->state = 0; + lock.unlock(); + onceChanged.notify_all(); + throw; + } + lock.lock(); + control->state = 1; + lock.unlock(); + onceChanged.notify_all(); + return 0; +} diff --git a/core/libs/tests/GuestOnce.cpp b/core/libs/tests/GuestOnce.cpp new file mode 100644 index 000000000..b82ec22ef --- /dev/null +++ b/core/libs/tests/GuestOnce.cpp @@ -0,0 +1,44 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include + +struct GuestOnce { std::int32_t state; void* mutex; }; +static_assert(sizeof(GuestOnce) == 16 && offsetof(GuestOnce, mutex) == 8); +using Initializer = void (APS5_VABI *)(); +extern "C" int APS5_VABI pthread_once_nid_postfix(GuestOnce*, Initializer); +static void Require(bool value) { if (!value) std::abort(); } +static GuestOnce control{}, nested{}, retry{}; +static std::atomic calls{0}, arrived{0}; +static int published = 0, nestedValue = 0, attempts = 0; +static void APS5_VABI Nested() { nestedValue = 17; } +static void APS5_VABI Initialize() { + ++calls; + while (arrived.load() != 16) std::this_thread::yield(); + Require(pthread_once_nid_postfix(&nested, Nested) == 0); + published = 42; +} +static void APS5_VABI Retry() { + if (++attempts == 1) throw 7; +} +int main() { + std::array workers; + for (auto& worker : workers) worker = std::thread([] { + ++arrived; + Require(pthread_once_nid_postfix(&control, Initialize) == 0); + Require(published == 42 && nestedValue == 17); + }); + for (auto& worker : workers) worker.join(); + Require(calls == 1 && control.state == 1 && control.mutex == nullptr); + Require(pthread_once_nid_postfix(&control, Initialize) == 0 && calls == 1); + try { pthread_once_nid_postfix(&retry, Retry); std::abort(); } + catch (int value) { Require(value == 7 && retry.state == 0); } + Require(pthread_once_nid_postfix(&retry, Retry) == 0 && attempts == 2); + Require(pthread_once_nid_postfix(nullptr, Initialize) == 22); + Require(pthread_once_nid_postfix(&control, nullptr) == 22); + GuestOnce invalid{9, nullptr}; + Require(pthread_once_nid_postfix(&invalid, Initialize) == 22); +} From 473a4f8d1c5e7302a18e24949ffc8f3afae3d385 Mon Sep 17 00:00:00 2001 From: SamG-Coder <13449963+SamG-Coder@users.noreply.github.com> Date: Fri, 25 Sep 2026 07:22:06 +1000 Subject: [PATCH 37/94] fix(agc): preserve strict shader linkage failures --- .../prx/libSceAgc/Shader/src/LinkShaders.cpp | 9 ++------ core/libs/tests/GuestShaderLink.cpp | 6 +++++- docs/YAMAGI_QUAKE2.md | 21 +++++++++++++++++++ 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceAgc/Shader/src/LinkShaders.cpp b/core/libs/prx/libSceAgc/Shader/src/LinkShaders.cpp index cac2bb956..8f0b7e266 100644 --- a/core/libs/prx/libSceAgc/Shader/src/LinkShaders.cpp +++ b/core/libs/prx/libSceAgc/Shader/src/LinkShaders.cpp @@ -3,7 +3,6 @@ #include "prx/libSceAgc/Shader/include/ShaderConstants.hpp" #include #include -#include extern "C" int APS5_VABI sceAgcCreatePrimState(ShaderRegister*, ShaderRegister*, const Shader*, const Shader*, std::uint32_t); extern "C" int APS5_VABI sceAgcCreateInterpolantMapping(ShaderRegister*, const Shader*, const Shader*); @@ -23,12 +22,8 @@ extern "C" int APS5_VABI sceAgcLinkShaders(ShaderRegister* context, ShaderRegist // and 32 interpolant registers. Publish only after both helpers succeed. std::array contextValues{}; std::array primitiveValues{}; - try { - sceAgcCreatePrimState(contextValues.data(), primitiveValues.data(), nullptr, vertex, primitiveType); - sceAgcCreateInterpolantMapping(contextValues.data() + 2, vertex, pixel); - } catch (const std::runtime_error&) { - return GRAPHICS5_ERROR_INVALID_SHADER_PROGRAM; - } + sceAgcCreatePrimState(contextValues.data(), primitiveValues.data(), nullptr, vertex, primitiveType); + sceAgcCreateInterpolantMapping(contextValues.data() + 2, vertex, pixel); std::copy(contextValues.begin(), contextValues.end(), context); std::copy(primitiveValues.begin(), primitiveValues.end(), primitive); return 0; diff --git a/core/libs/tests/GuestShaderLink.cpp b/core/libs/tests/GuestShaderLink.cpp index 4163bb538..5e8f3b31a 100644 --- a/core/libs/tests/GuestShaderLink.cpp +++ b/core/libs/tests/GuestShaderLink.cpp @@ -4,6 +4,7 @@ #include #include #include +#include extern "C" int APS5_VABI sceAgcLinkShaders(ShaderRegister*, ShaderRegister*, const void*, const Shader*, const Shader*, std::uint32_t); extern "C" void* APS5_VABI sceAgcGetRegisterDefaults(); extern "C" void* APS5_VABI sceAgcGetRegisterDefaults2(std::uint32_t); @@ -58,7 +59,10 @@ int main() { Require(std::memcmp(primitive.data(), savedPrimitive.data(), sizeof(primitive)) == 0); pixel.num_input_semantics = 0; special.ge_cntl.offset = 0; - Require(sceAgcLinkShaders(context.data(), primitive.data(), nullptr, &vertex, &pixel, 4) == GRAPHICS5_ERROR_INVALID_SHADER_PROGRAM); + bool rejected = false; + try { sceAgcLinkShaders(context.data(), primitive.data(), nullptr, &vertex, &pixel, 4); } + catch (const std::runtime_error&) { rejected = true; } + Require(rejected); Require(std::memcmp(context.data(), savedContext.data(), sizeof(context)) == 0); special.ge_cntl.offset = GE_CNTL; Require(sceAgcLinkShaders(context.data(), primitive.data(), nullptr, &vertex, nullptr, 2) == 0); diff --git a/docs/YAMAGI_QUAKE2.md b/docs/YAMAGI_QUAKE2.md index 0563951e6..9e9173fd6 100644 --- a/docs/YAMAGI_QUAKE2.md +++ b/docs/YAMAGI_QUAKE2.md @@ -510,3 +510,24 @@ wait modes, and rendering a game frame remain unverified. The unchanged game now resolves both rendering-wait imports and stops at `Z4QosVuAsA0` (`pthread_once`). Guest entry-point execution has not yet occurred. + +## Follow-up: once initialization and architecture audit + +Implemented pthread_once with the guest 16-byte control layout, a SysV callback, +serialized initialization, and publication of initialized data to waiting callers. +Independent controls can initialize recursively; an initializer that unwinds with +a C++ exception resets its control for retry. Guest thread cancellation and fork +recovery are not implemented. Tests use host threads calling the guest ABI and do +not establish that guest pthread_create works. + +All twenty-eight CTest cases pass. The unchanged converted game now stops at +`NesIgTmfF0Q` (`bsearch`) with exit `0xc0000139`, before guest entry. + +The README architecture remains the target: native executable relinking and +in-process replacement system libraries. There is no replacement game engine or +game source in these additions. Windows compatibility tests do not establish +Linux compatibility or a successful game launch. Shader register layout and the +internal rendering-wait encoding still require actual game execution validation. +The shader-link wrapper now propagates unsupported-state runtime exceptions from +its helpers, preserving the README's strict failure policy; its regression test +checks the exception and that output registers are not partially published. From 6cdf4a5be1634d19ff977e242efc992190acf7c7 Mon Sep 17 00:00:00 2001 From: SamG-Coder <13449963+SamG-Coder@users.noreply.github.com> Date: Fri, 25 Sep 2026 07:24:17 +1000 Subject: [PATCH 38/94] feat(libc): implement binary search with guest comparator ABI --- core/libs/CMakeLists.txt | 9 ++++++ core/libs/prx/libc/src/FileAndHeap.cpp | 21 ++++++++++++ core/libs/tests/GuestSearch.cpp | 45 ++++++++++++++++++++++++++ docs/YAMAGI_QUAKE2.md | 10 ++++++ 4 files changed, 85 insertions(+) create mode 100644 core/libs/tests/GuestSearch.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 20103697f..35cdadb35 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -307,6 +307,15 @@ if(BUILD_TESTING) set_tests_properties(guest_memory PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_executable(guest_search_tests tests/GuestSearch.cpp) + target_include_directories(guest_search_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_search_tests PRIVATE libc) + configure_windows_unwind(guest_search_tests) + add_test(NAME guest_search COMMAND guest_search_tests) + if(WIN32) + set_tests_properties(guest_search PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_executable(guest_string_tests tests/GuestStrings.cpp) target_include_directories(guest_string_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_string_tests PRIVATE libc libScePosixForWebKit) diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index 79b7d70fb..b3e3ed183 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -157,6 +157,27 @@ int APS5_VABI posix_memalign_nid_postfix(void** pointer, size_t alignment, size_ return ApplicationHeapPosixAlign_nid_no_patch(pointer, alignment, 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*)) { + if (count == 0) return nullptr; + if (!key || !base || !compare || size == 0) + throw std::invalid_argument("bsearch: invalid arguments"); + if (count > std::numeric_limits::max() / size) + throw std::overflow_error("bsearch: array size overflow"); + const auto* bytes = static_cast(base); + size_t first = 0; + while (count != 0) { + const size_t half = count / 2; + const size_t middle = first + half; + const auto* element = bytes + middle * size; + const int result = compare(key, element); + if (result == 0) return const_cast(element); + if (result < 0) count = half; + else { first = middle + 1; count -= half + 1; } + } + return nullptr; +} + 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/GuestSearch.cpp b/core/libs/tests/GuestSearch.cpp new file mode 100644 index 000000000..f1572c364 --- /dev/null +++ b/core/libs/tests/GuestSearch.cpp @@ -0,0 +1,45 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +using Comparator = int (APS5_VABI *)(const void*, const void*); +extern "C" void* APS5_VABI bsearch_nid_postfix(const void*, const void*, std::size_t, std::size_t, Comparator); +static void Require(bool value, std::source_location location = std::source_location::current()) { + if (!value) { std::fprintf(stderr, "Guest search failed at line %u\n", location.line()); std::abort(); } +} +struct Record { int key; char payload[13]; }; +static int calls = 0; +static int APS5_VABI Compare(const void* key, const void* record) { + ++calls; + const int left = *static_cast(key); + const int right = static_cast(record)->key; + return (left > right) - (left < right); +} +int main() { + const std::array records{{{1, "one"}, {3, "three"}, {5, "five"}, + {5, "duplicate"}, {7, "seven"}, {9, "nine"}}}; + std::array before{}; + std::memcpy(before.data(), records.data(), sizeof(records)); + for (int key = 0; key <= 10; ++key) { + calls = 0; + const auto* found = static_cast(bsearch_nid_postfix(&key, records.data(), records.size(), sizeof(Record), Compare)); + Require(calls <= 3); + if (key % 2) Require(found && found->key == key && found >= records.data() && found < records.data() + records.size()); + else Require(!found); + } + Require(std::memcmp(before.data(), records.data(), sizeof(records)) == 0); + calls = 0; + int key = 1; + Require(bsearch_nid_postfix(&key, nullptr, 0, sizeof(Record), Compare) == nullptr && calls == 0); + Require(bsearch_nid_postfix(&key, records.data(), 1, sizeof(Record), Compare) == records.data()); + key = 2; + Require(!bsearch_nid_postfix(&key, records.data(), 1, sizeof(Record), Compare)); + bool rejected = false; + try { bsearch_nid_postfix(&key, records.data(), std::numeric_limits::max(), sizeof(Record), Compare); } + catch (const std::overflow_error&) { rejected = true; } + Require(rejected); +} diff --git a/docs/YAMAGI_QUAKE2.md b/docs/YAMAGI_QUAKE2.md index 9e9173fd6..6cbfe79b1 100644 --- a/docs/YAMAGI_QUAKE2.md +++ b/docs/YAMAGI_QUAKE2.md @@ -531,3 +531,13 @@ internal rendering-wait encoding still require actual game execution validation. The shader-link wrapper now propagates unsupported-state runtime exceptions from its helpers, preserving the README's strict failure policy; its regression test checks the exception and that output registers are not partially published. + +## Follow-up: binary search + +Implemented bsearch with direct SysV comparator calls, key-first argument order, +and overflow-checked element addressing. Regression coverage uses records and a +distinct key type to check callback argument order, duplicates, absent keys, +empty and singleton arrays, and unchanged input bytes. All twenty-nine tests +have passed (the new test was rerun after correcting its padding-byte snapshot). +The unchanged game resolves bsearch and stops at `jbz9I9vkqkk` (`vsprintf`), +still before guest entry with exit `0xc0000139`. From f37fffdacd43c777977b8e2e71f344760afc8dd8 Mon Sep 17 00:00:00 2001 From: SamG-Coder <13449963+SamG-Coder@users.noreply.github.com> Date: Fri, 25 Sep 2026 07:26:06 +1000 Subject: [PATCH 39/94] feat(libc): expose guest vsprintf formatting --- core/libs/prx/libc/src/Formatting.cpp | 8 +++++++ core/libs/tests/GuestStandardStreams.cpp | 29 ++++++++++++++++++++++++ docs/YAMAGI_QUAKE2.md | 16 +++++++++++++ 3 files changed, 53 insertions(+) diff --git a/core/libs/prx/libc/src/Formatting.cpp b/core/libs/prx/libc/src/Formatting.cpp index 38c54e381..207069071 100644 --- a/core/libs/prx/libc/src/Formatting.cpp +++ b/core/libs/prx/libc/src/Formatting.cpp @@ -152,6 +152,14 @@ int APS5_VABI vprintf_nid_postfix(const char* str, VaList* c) { #endif } +int APS5_VABI vsprintf_nid_postfix(char* str, const char* format, VaList* args) { +#ifdef _WIN32 + return LibcDetail::FormatWindows(str, SIZE_MAX, format, args); +#else + return std::vsprintf(str, format, *reinterpret_cast(args)); +#endif +} + int APS5_VABI vsnprintf_nid_postfix(char* str, size_t size, const char* format, VaList* c) { #ifdef _WIN32 return LibcDetail::FormatWindows(str, size, format, c); diff --git a/core/libs/tests/GuestStandardStreams.cpp b/core/libs/tests/GuestStandardStreams.cpp index 62ce707ca..4ffb0fcaf 100644 --- a/core/libs/tests/GuestStandardStreams.cpp +++ b/core/libs/tests/GuestStandardStreams.cpp @@ -20,6 +20,7 @@ extern FileStream* __stderrp_nid_postfix; extern int __isthreaded_nid_postfix; int APS5_VABI fprintf_nid_postfix(FileStream*, const char*, ...); int APS5_VABI vfprintf_nid_postfix(FileStream*, const char*, void*); +int APS5_VABI vsprintf_nid_postfix(char*, const char*, void*); int APS5_VABI fgetc_nid_postfix(FileStream*); int APS5_VABI fputc_nid_postfix(int, FileStream*); int APS5_VABI __srget_nid_postfix(FileStream*); @@ -48,7 +49,35 @@ static int APS5_VABI WriteFormatted(FileStream* stream, const char* format, ...) #endif return result; } +static int APS5_VABI FormatString(char* buffer, const char* format, ...) { +#ifdef _WIN32 + __builtin_sysv_va_list args; + __builtin_sysv_va_start(args, format); +#else + std::va_list args; + va_start(args, format); +#endif + const int result = vsprintf_nid_postfix(buffer, format, args); +#ifdef _WIN32 + __builtin_sysv_va_end(args); +#else + va_end(args); +#endif + return result; +} int main() { + char stringOutput[256]; + std::memset(stringOutput, '!', sizeof(stringOutput)); + std::int64_t count = -1; + const char expectedString[] = "guest:4294967297: 3.50:1,2,3,4,5,6,7,8:%"; + const int written = FormatString(stringOutput, "%s:%ld:%*.*f:%d,%d,%d,%d,%d,%d,%d,%d:%%%ln", + "guest", std::int64_t{4294967297}, 6, 2, 3.5, 1, 2, 3, 4, 5, 6, 7, 8, &count); + Require(written == sizeof(expectedString) - 1 && count == written); + Require(std::strcmp(stringOutput, expectedString) == 0 && stringOutput[written + 1] == '!'); + Require(FormatString(stringOutput, "%.0f %.0f %.0f %.0f %.0f %.0f %.0f %.0f %.0f %.0f %.2Lf", + 1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 1.25L) == 25); + Require(std::strcmp(stringOutput, "1 2 3 4 5 6 7 8 9 10 1.25") == 0); + Require(FormatString(stringOutput, "") == 0 && stringOutput[0] == '\0'); Require(__isthreaded_nid_postfix == 1); Require(__stdoutp_nid_postfix == &_Stdout_nid_postfix); Require(__stderrp_nid_postfix == &_Stderr_nid_postfix); diff --git a/docs/YAMAGI_QUAKE2.md b/docs/YAMAGI_QUAKE2.md index 6cbfe79b1..8e133d90d 100644 --- a/docs/YAMAGI_QUAKE2.md +++ b/docs/YAMAGI_QUAKE2.md @@ -541,3 +541,19 @@ empty and singleton arrays, and unchanged input bytes. All twenty-nine tests have passed (the new test was rerun after correcting its padding-byte snapshot). The unchanged game resolves bsearch and stops at `jbz9I9vkqkk` (`vsprintf`), still before guest entry with exit `0xc0000139`. + +## Follow-up: unbounded varargs formatting + +Added vsprintf through the existing guest varargs formatter on Windows and the +native SysV va_list path on Linux. The caller remains responsible for buffer +capacity, as with the C interface. Windows tests cover mixed integer/string/ +floating arguments, guest 64-bit long, width and precision arguments, register +overflow onto the stack, long double, percent escapes, count writes, empty +output, termination, and the byte immediately after the output. The formatter's +existing unsupported conversions still fail explicitly; this does not establish +complete printf compatibility or Linux runtime validation. + +All twenty-nine CTest cases pass. The unchanged Quake II executable now resolves +vsprintf and stops at `Jc6E7N+dHz0` (`system`), before guest entry, with exit +`0xc0000139`. No game frame has been rendered. Upstream was fetched again and +had no new commits to merge. From 5a2790c93e35a328f6ab4d565ebe179e912cc999 Mon Sep 17 00:00:00 2001 From: SamG-Coder <13449963+SamG-Coder@users.noreply.github.com> Date: Fri, 25 Sep 2026 07:27:07 +1000 Subject: [PATCH 40/94] style(libc): remove trailing blank line in process exports --- core/libs/prx/libc/src/Process.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/core/libs/prx/libc/src/Process.cpp b/core/libs/prx/libc/src/Process.cpp index 49c5b9308..01b63f769 100644 --- a/core/libs/prx/libc/src/Process.cpp +++ b/core/libs/prx/libc/src/Process.cpp @@ -94,4 +94,3 @@ int APS5_VABI atexit_nid_postfix(atexit_func_t func) { } } - From 24cb69f2a8d30392dfbffb80c7a9f78e872c2d67 Mon Sep 17 00:00:00 2001 From: SamG-Coder <13449963+SamG-Coder@users.noreply.github.com> Date: Fri, 25 Sep 2026 12:04:26 +1000 Subject: [PATCH 41/94] docs: keep homebrew guide limited to instructions and links --- docs/HOMEBREW_TESTING.md | 87 +++++++++++----------------------------- 1 file changed, 23 insertions(+), 64 deletions(-) diff --git a/docs/HOMEBREW_TESTING.md b/docs/HOMEBREW_TESTING.md index d1136e260..922378e5c 100644 --- a/docs/HOMEBREW_TESTING.md +++ b/docs/HOMEBREW_TESTING.md @@ -1,76 +1,35 @@ -# Homebrew investigation, 2026-09-25 +# Homebrew testing -Baseline: SamG-Coder/AnyPS5, commit `2778c1e`. +## Build and test -## Verified locally +On Windows, install CMake, Ninja, and a C++20 compiler. For compatibility libraries, +use the toolchain specified in [README.md](../README.md). -- Cloned the fork and initialized all seven submodules at their pinned revisions. -- Configured on Windows with CMake/Ninja and MSYS2 UCRT64 GCC 16.2.0. -- Built `relinker`, `strict_nid_filter_tests`, and `windows_dependency_diagnostics_tests`. -- Both existing test executables passed. Initially CTest discovered zero tests. -- Enabled root CTest registration when `BUILD_TESTING=ON` and registered the Windows diagnostics test. The latter is included in normal builds only when testing is enabled. -- After the CMake changes, CTest discovered and passed both tests. -- Tried relinking two real PS5 homebrew binaries to Windows. Both failed with exit code 2; neither produced a game executable. +Run from the repository root with the compiler and Ninja on `PATH`: -This verifies the relinker build and these two test suites only. The compatibility libraries, shader pipeline, and game execution have not been built or validated in this investigation. Upstream documents a specific WinLibs GCC 15.2.0 toolchain requirement for its Windows libc implementation; the compiler used here does not establish compatibility with that requirement. - -## Reproduce the build - -Run from the repository root in PowerShell. These paths match this workstation: - -```powershell -$env:PATH = 'C:\msys64\ucrt64\bin;' + $env:PATH -cmake -S . -B build -G Ninja ` - -DCMAKE_MAKE_PROGRAM=C:/msys64/ucrt64/bin/ninja.exe ` - -DCMAKE_C_COMPILER=C:/msys64/ucrt64/bin/gcc.exe ` - -DCMAKE_CXX_COMPILER=C:/msys64/ucrt64/bin/g++.exe ` - -DCMAKE_BUILD_TYPE=Debug -DBUILD_TESTING=ON -cmake --build build --target relinker strict_nid_filter_tests windows_dependency_diagnostics_tests -j 8 -ctest --test-dir build --output-on-failure +```sh +git submodule update --init --recursive +cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug -DBUILD_TESTING=ON +cmake --build build --target relinker strict_nid_filter_tests windows_dependency_diagnostics_tests +ctest --test-dir build --output-on-failure -R "^(strict_nid_filter|windows_dependency_diagnostics)$" ``` -## Public PS5 homebrew inputs +## PS5 homebrew projects -Downloaded directly from [ps5-payload-dev/websrv v0.34](https://github.com/ps5-payload-dev/websrv/releases/tag/v0.34). Downloads remain in the ignored `downloads/` directory; no game binaries or data are added to Git. +- [Omnispeak](https://github.com/sulix/omnispeak): [PS5 build recipe](https://github.com/ps5-payload-dev/websrv/blob/master/homebrew/Omnispeak/build.sh). +- [Crispy Doom PS5 build recipe](https://github.com/ps5-payload-dev/websrv/blob/master/homebrew/CrispyDoom/build.sh). +- [PS5 homebrew packages, websrv v0.34](https://github.com/ps5-payload-dev/websrv/releases/tag/v0.34). +- [PS5 payload SDK](https://github.com/ps5-payload-dev/sdk). +- [Freedoom game data](https://freedoom.github.io/). -| Archive | SHA-256 | -| --- | --- | -| Omnispeak.zip | 7A4D82BF58EF7406A4C9050EC298195C548D387FEBC28FD814C3BF9026BB231B | -| CrispyDoom.zip | C0C37713703E109F43B1B598CF5790D969097A2BAF961693A0FD0E03956A3C59 | +## Convert an ELF executable -[Omnispeak](https://github.com/sulix/omnispeak) is an open-source Commander Keen engine. Its upstream README links Keen 4 shareware data, and the PS5 package includes CK4 data. The [PS5 build recipe](https://github.com/ps5-payload-dev/websrv/blob/master/homebrew/Omnispeak/build.sh) identifies how that package is assembled. +Keep downloaded binaries and game data outside version control. Check the engine +and game-data licenses separately. -[Crispy Doom's PS5 recipe](https://github.com/ps5-payload-dev/websrv/blob/master/homebrew/CrispyDoom/build.sh) builds the engine and packages shareware data. [Freedoom](https://freedoom.github.io/) is a separate free-content candidate for later compatibility testing; it has not been downloaded or tested here. - -These PS5 builds are ELF payloads, not ISO disc images. An engine's open-source license does not automatically apply to original commercial game data. - -```powershell -.\build\core\relinker\relinker.exe --windows ` - downloads\Omnispeak\omnispeak.elf downloads\omnispeak.exe -.\build\core\relinker\relinker.exe --windows ` - downloads\CrispyDoom\CrispyDoom\crispy-doom.elf downloads\crispy-doom.exe +```sh +build/core/relinker/relinker.exe --windows downloads/input.elf downloads/output.exe ``` -Both currently report: - -```text -System: Windows; unused-filter=0 -FAIL: Neither DT_OS_ nor DT_ variant present for DT_PLTGOT -``` - -## Why this is more than a missing table - -`RelinkerPipeline.cpp` unconditionally requires PLTGOT, PLTRELSZ, PLTREL, and JMPREL metadata. Inspection of Omnispeak with objdump shows ordinary RELA relocations but no PLTGOT/PLT relocation tags. Supporting ELF files without PLT relocations is a candidate improvement, but simply bypassing the requirement does not establish a runnable conversion. - -The [PS5 payload SDK startup](https://github.com/ps5-payload-dev/sdk/blob/master/crt/crt.c) takes a `payload_args_t` pointer and initializes console-specific kernel and dynamic-linker facilities. Its [entry assembly](https://github.com/ps5-payload-dev/sdk/blob/master/crt/_start.S) calls that startup. Homebrew payload startup therefore needs separate compatibility analysis and likely an adapter. - -Omnispeak imports `libkernel_web.sprx` and `libSceLibcInternal.sprx`, among other libraries. In this AnyPS5 revision, the former contains only `APS5_DUMMY_FUN`, while the latter exposes only a heap tracing stub. File presence is not evidence of implemented compatibility. - -## Suggested next milestones - -1. Create a tiny source-owned executable fixture with a documented entry ABI, one imported function, and a verifiable exit value. Relink it and execute the result on Windows. -2. Add regression coverage for ELF files without PLT relocations before changing their handling. -3. Report unsupported payload startup and missing imports explicitly, then implement the minimum required compatibility path. -4. Advance to a framebuffer demo, input, and audio before retrying a complete homebrew game. - -Keep conversion, loader startup, first rendered frame, and playable gameplay as distinct results. A native Windows build of the same open-source engine is useful as a reference but is not evidence that PS5 binary conversion works. +Check conversion, startup, rendering, input, and audio separately; +conversion alone does not establish game compatibility. From 51ab46d418d607f93d238bdad372bc2ea8debd5e Mon Sep 17 00:00:00 2001 From: SamG-Coder <13449963+SamG-Coder@users.noreply.github.com> Date: Fri, 25 Sep 2026 12:06:10 +1000 Subject: [PATCH 42/94] docs: remove homebrew testing guide --- docs/HOMEBREW_TESTING.md | 35 ----------------------------------- 1 file changed, 35 deletions(-) delete mode 100644 docs/HOMEBREW_TESTING.md diff --git a/docs/HOMEBREW_TESTING.md b/docs/HOMEBREW_TESTING.md deleted file mode 100644 index 922378e5c..000000000 --- a/docs/HOMEBREW_TESTING.md +++ /dev/null @@ -1,35 +0,0 @@ -# Homebrew testing - -## Build and test - -On Windows, install CMake, Ninja, and a C++20 compiler. For compatibility libraries, -use the toolchain specified in [README.md](../README.md). - -Run from the repository root with the compiler and Ninja on `PATH`: - -```sh -git submodule update --init --recursive -cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug -DBUILD_TESTING=ON -cmake --build build --target relinker strict_nid_filter_tests windows_dependency_diagnostics_tests -ctest --test-dir build --output-on-failure -R "^(strict_nid_filter|windows_dependency_diagnostics)$" -``` - -## PS5 homebrew projects - -- [Omnispeak](https://github.com/sulix/omnispeak): [PS5 build recipe](https://github.com/ps5-payload-dev/websrv/blob/master/homebrew/Omnispeak/build.sh). -- [Crispy Doom PS5 build recipe](https://github.com/ps5-payload-dev/websrv/blob/master/homebrew/CrispyDoom/build.sh). -- [PS5 homebrew packages, websrv v0.34](https://github.com/ps5-payload-dev/websrv/releases/tag/v0.34). -- [PS5 payload SDK](https://github.com/ps5-payload-dev/sdk). -- [Freedoom game data](https://freedoom.github.io/). - -## Convert an ELF executable - -Keep downloaded binaries and game data outside version control. Check the engine -and game-data licenses separately. - -```sh -build/core/relinker/relinker.exe --windows downloads/input.elf downloads/output.exe -``` - -Check conversion, startup, rendering, input, and audio separately; -conversion alone does not establish game compatibility. From cf3c8081fdb9d815d4ff35bdde0509cc313c70af Mon Sep 17 00:00:00 2001 From: SamG-Coder <13449963+SamG-Coder@users.noreply.github.com> Date: Sat, 26 Sep 2026 06:03:50 +1000 Subject: [PATCH 43/94] fix(build): make Python relinker tests optional --- core/relinker/CMakeLists.txt | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index f82728eb4..a48325819 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -73,11 +73,15 @@ endif() if(BUILD_TESTING) - find_package(Python3 REQUIRED COMPONENTS Interpreter) - add_test(NAME optional_plt COMMAND ${Python3_EXECUTABLE} - ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_optional_plt.py $) - add_test(NAME empty_tls COMMAND ${Python3_EXECUTABLE} - ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_empty_tls.py $) + find_package(Python3 QUIET COMPONENTS Interpreter) + if(Python3_Interpreter_FOUND) + add_test(NAME optional_plt COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_optional_plt.py $) + add_test(NAME empty_tls COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_empty_tls.py $) + else() + message(STATUS "Python3 interpreter not found; skipping optional_plt and empty_tls tests") + endif() add_executable(strict_nid_filter_tests ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/StrictNidFilterTests.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp From a1dedc4a63377fcae0f67f7716549f18e6692c51 Mon Sep 17 00:00:00 2001 From: SamG-Coder <13449963+SamG-Coder@users.noreply.github.com> Date: Sat, 26 Sep 2026 06:03:50 +1000 Subject: [PATCH 44/94] docs: move Yamagi investigation to pull request --- docs/YAMAGI_QUAKE2.md | 559 ------------------------------------------ 1 file changed, 559 deletions(-) delete mode 100644 docs/YAMAGI_QUAKE2.md diff --git a/docs/YAMAGI_QUAKE2.md b/docs/YAMAGI_QUAKE2.md deleted file mode 100644 index 8e133d90d..000000000 --- a/docs/YAMAGI_QUAKE2.md +++ /dev/null @@ -1,559 +0,0 @@ -# Yamagi Quake II compatibility investigation - -Target: [BlackBearReloaded's native PS5 release v0.2.0-alpha.1](https://github.com/blackbearreloaded/ps5-yamagi-quake2/releases/tag/v0.2.0-alpha.1), title PPSA99007. Investigated on Windows on 2026-09-25. - -The release includes the Quake II demo and its original notices. All downloaded -game files, extracted executables, and logs remain under ignored `downloads/`. - -## Input preparation - -`PPSA99007.zip` SHA-256: -`4FA5DA07498FE64FFC67229EED2347E0DDFFD5073F1FEA248BA5E69307229A70`. - -The released `eboot.bin` is a plaintext FSELF container. AnyPS5's raw ELF reader -rejects it with `Invalid ELF magic number`. Extract the ELF using the game's -[native application tooling](https://github.com/blackbearreloaded/ps5-native-app-boilerplate), -revision `722f2227a8bb6fa2229120546995b6562552c752`: - -```powershell -downloads/ps5-yamagi-quake2/ps5-native-tool.exe self --extract ` - --file downloads/ps5-yamagi-quake2/extracted/PPSA99007/eboot.bin ` - --out downloads/ps5-yamagi-quake2/eboot.elf -``` - -The extraction tool was built with MSYS2 UCRT64 g++, C++20, and zlib from -`native_app_builder.cpp`, `self_container.cpp`, `elf_object.cpp`, and -`sce_module_writer.cpp` in that repository's `tooling/native/` directory. - -Extracted ELF SHA-256: -`FC0879B50EF23B604FA81C8C28843C75AD54A515A6944D8809327C3A51B40A27`. - -## Conversion and startup - -The ELF contains an empty PT_TLS segment (zero file and memory sizes, alignment -one). The Windows TLS builder previously rejected this. It now accepts an empty -segment when no guest TLS accesses exist, without adding a PE TLS directory. -Malformed size combinations and accesses to an empty segment remain errors. - -```powershell -build-winlibs/core/relinker/relinker.exe --windows --windows-diagnostics ` - --rpath D:/AnyPS5/build-winlibs/core/libs/libs ` - downloads/ps5-yamagi-quake2/eboot.elf ` - downloads/ps5-yamagi-quake2/quake2.exe -``` - -Conversion succeeds with all 382 import references preserved. This command -embeds a workstation-specific library path. Run it from the download directory -if the generated `windows-diagnostics-imports.txt` should also remain there -(adjust the relative paths accordingly). - -The compatibility libraries were built using the project's documented WinLibs -GCC 15.2.0 toolchain. Place its `libgcc_s_seh-1.dll`, `libstdc++-6.dll`, and -`libwinpthread-1.dll` beside the generated executable for this startup check. - -Initial startup result (before the follow-up changes below): - -- Loads libSceSystemService, libSceUserService, libScePad, libSceAudioOut, - libSceVideoOut, libSceKeyboard, libSceImeDialog, libSceAgc, libSceAgcDriver, - and libSceLibcInternal compatibility modules successfully. -- Stops loading `libScePosixForWebKit.prx`, which AnyPS5 does not currently build. -- Exits with `0xc0000135`; no transfer to the guest entry point and no game frame. - -Successful module loading alone does not establish API behavior, rendering, -or playability. - -## Follow-up: string exports and Windows runtime visibility - -The static import audit found 373 unique imported NIDs (382 relocation -references). Before this follow-up, 168 NIDs were absent from the combined -export tables of the built compatibility modules. This is an export-availability -check, not proof that the other implementations satisfy the game's requirements. - -The seven imports attributed to libScePosixForWebKit are `getaddrinfo`, -`freeaddrinfo`, `gai_strerror`, `getnameinfo`, `isatty`, `mkstemp`, and `strcasestr`. -The new module implements `strcasestr`; the other six remain unsupported. -The shared libc also gains `strnlen`, `strncat`, `strpbrk`, `strcspn`, `strlcat`, -and `strtok_r`. None of these additions is a success-returning placeholder. - -After adding the module, startup exposed a separate Windows lookup problem: -`_init_env` exists in AnyPS5's libc.prx, but GetProcAddress does not search the -dependencies of libSceLibcInternal.prx. For Windows conversion, a request for -libSceLibcInternal.prx now also adds libc.prx to the explicit symbol search list, -after the game's requested modules. An existing explicit libc.prx entry is -retained without duplication; unrelated inputs gain no dependency. - -Latest verified startup loads all twelve game-requested modules plus libc.prx. -It then stops at missing import `BPE9s9vQQXo` (`mmap`) with exit `0xc0000139`. -There is still no transfer to the guest entry point and no game frame. The next -runtime work is guest memory-mapping support, alongside the remaining import -gaps in file I/O, threading, libc, POSIX services, and graphics. - -## Regression coverage - -`ctest --test-dir build-winlibs --output-on-failure`: four suites pass. The new -`empty_tls` integration suite checks absent PE TLS for an empty guest segment, -retained TLS for a nonempty segment, rejection of malformed sizes and empty TLS -accesses, and execution of both valid generated Windows fixtures (exit 42). - -After the follow-up: five suites pass. `guest_strings` exercises the guest SysV -calling convention, bounded unterminated input, concatenation truncation, -interleaved tokenizer state, and case-insensitive searches. Windows dependency -tests also cover implicit libc visibility, ordering, duplicate avoidance, and -unrelated inputs. The complete `libs` target rebuild succeeds. - -## Follow-up: anonymous memory mapping - -The game's `tooling/native/app_heap.c` requests private anonymous read/write -memory for its OpenGL heap. `mmap` and `munmap` now route this use through -AnyPS5's existing tracked guest allocations, with 16 KiB page rounding and -partial unmapping. Guest MAP_ANON is 0x1000, not the Linux host value 0x20. -Failures return MAP_FAILED or -1 and set guest errno through `__error`. -Shared, fixed-address, and file-backed mappings remain explicitly unsupported -(guest EOPNOTSUPP 45). Unmapping currently requires a contiguous tracked range -within one allocation; holes, foreign memory, and pinned ranges are rejected. - -Six CTest suites pass, including real allocations, zero-fill, alignment, -read/write access, tracking, partial unmapping, protection metadata, and invalid -argument/unsupported-mode errors. The unchanged converted game now resolves -the memory-mapping imports and stops at `-hn1tcVHq5Q` (`sceLibcMspaceCreate`). -It still has not reached its entry point or drawn a frame. - -## Follow-up: caller-backed heaps and address resolution - -The generic `sceLibcMspace*` implementation allocates within the memory region -provided by the caller. It supports malloc, calloc, realloc, free, aligned -allocation, usable-size queries, and destruction. Free blocks coalesce; failed -reallocation preserves the original block. Tests cover region ownership, -alignment, overflow, exhaustion, data preservation, block reuse, and concurrent -calls. Only creation flags zero are currently supported. Arena lifetime remains -the caller's responsibility: destroy it before releasing its backing memory. -No game code or game-specific heap size is embedded in this implementation. - -The POSIX module now supplies `getnameinfo`, `getaddrinfo`, `freeaddrinfo`, and -`gai_strerror`, converting guest sockaddr/addrinfo layouts, IPv6 family IDs, -flags, and resolver error values. Native results are copied into owned guest -layout structures and released through the matching free function. Short -getnameinfo buffers return guest EAI_OVERFLOW without copying partial results. -Supported families are IPv4/IPv6; socket hints support unspecified, stream, -and datagram. Unsupported flags and invalid hints fail explicitly. - -All eight CTest suites pass on Windows. Resolver tests use numeric loopback -addresses and ports, so they do not depend on external DNS. Live DNS and online -multiplayer are not validated by these tests. The full compatibility-library -build succeeds, and the unchanged game now stops at `RIa6GnWp+iU` (`strerror`). -No guest entry-point execution or rendered frame has been observed yet. - -## Follow-up: error reporting and IP conversion - -Added generic `strerror`, POSIX `strerror_r`, and `perror` using guest errno -numbering. Tests cover every defined guest errno value, thread-local message -storage, unknown errors, short buffers, and errno preservation. Messages are -currently English; locale-dependent message translation is not implemented. - -Added `__inet_pton` and `__inet_ntop` with guest IPv4/IPv6 family translation, -network-byte-order buffers, invalid-input handling, and guest error codes. -Tests exercise IPv4, IPv6, IPv4-mapped IPv6, untouched outputs on failure, and -short buffers. All ten CTest suites pass and the full library build succeeds. - -Latest startup stops at `PfccT7qURYE` (`ioctl`). The existing libkernel socket -operations include unimplemented placeholders, so the socket lifecycle needs -review before networking behavior can be considered supported. No gameplay -or guest entry-point execution is established by resolving these imports. - -## Follow-up: tracked UDP sockets - -Replaced the UDP-path placeholders with tracked guest descriptors, IPv4/IPv6 -address conversion, bind, getsockname, sendto, recvfrom, and close. Added guest -FIONBIO/FIONREAD ioctl translation and integer socket options for reuse-address, -broadcast, send-buffer size, and receive-buffer size. Descriptors occupy a -separate range from host CRT file descriptors and are not reused. In-flight -operations retain native socket ownership while guest close removes the handle. -Allocation failure closes the native socket and reports guest ENOMEM. - -The new loopback test sends and receives a real IPv4 UDP datagram through guest -exports, checks nonblocking EAGAIN, queued-byte counts, MSG_PEEK, broadcast -options, and closed-descriptor rejection. All eleven suites pass. IPv6 socket -address translation is implemented but this UDP test exercises IPv4 only. -TCP, select, connected send/recv, and unimplemented options remain unsupported; -online multiplayer is not validated. ioctl currently accepts tracked sockets -only. POSIX close also closes ordinary host CRT descriptors. - -The unchanged game resolves ioctl and now stops at `mkawd0NA9ts` (`sysconf`), -before guest entry-point execution. No rendered frame has been observed. - -## Follow-up: configuration queries and signal callbacks - -Added sysconf for guest page size, configured/online processor counts, and -physical page count. The return type is explicitly 64-bit for the guest ABI. -Memory capacity is reported in 16 KiB guest pages; CPU and capacity values -reflect the executing host. Unsupported queries return -1 with guest EINVAL. -The previous getpagesize placeholder now returns the same guest page size. - -Added signal/raise bridges for SIGINT, SIGILL, SIGABRT, SIGFPE, SIGSEGV, and -SIGTERM. Host callbacks dispatch through the guest SysV calling convention; -default/ignore handlers and guest-to-host signal numbers are translated. -Tests cover repeated explicit SIGTERM delivery, ignore/default restoration, -and invalid signals. This does not validate hardware-fault recovery, signal -masks, sigaction, or complete asynchronous POSIX semantics. - -All thirteen suites pass. Latest game startup resolves these imports and stops -at `zqJhBxAKfsc` (`__stdoutp`). The next work requires the guest FILE layout and -standard stream exports; mapping a host FILE pointer directly would be invalid. -The game still has not executed its entry point or rendered a frame. - -## Follow-up: standard stream globals and character I/O - -Added __stdinp, __stdoutp, __stderrp, and __isthreaded exports. FileStream now -starts with the verified macro-accessed guest FILE prefix (position, read/write -counts, flags, descriptor, and buffer fields); native stream state follows a -reserved guest area. This is not a complete implementation of FreeBSD's private -FILE operations, locking fields, or wide-stream state. - -Added character input/output, __srget/__swbuf fallbacks, fgets, ungetc, EOF/error -queries, clearerr, fileno, and buffering-mode translation. Existing fread/fwrite -and fseek now refresh the guest status fields. Tests exercise stream globals, -field offsets, inline-macro fallback paths, temporary-file I/O, EOF, ungetc, -and closure. All fourteen suites pass and the compatibility libraries rebuild. - -Latest startup resolves the stream globals and stops at `8nY19bKoiZk` (`fcntl`). -The game has not reached its entry point or displayed a frame. - -## Follow-up: descriptor flags and formatted stream output - -Added fcntl F_GETFL/F_SETFL for tracked UDP sockets. Nonblocking state is shared -with FIONBIO ioctl and protected against concurrent mode updates. Unsupported -flags are rejected; ordinary file descriptors and other fcntl commands are not -implemented. The UDP test checks both APIs agree, mode transitions, and unchanged -state after rejected flags. - -Added fprintf and vfprintf using the guest FileStream wrapper and the existing -SysV variadic formatter on Windows. Tests write and read back mixed strings, -64-bit guest long integers, floating-point values, register-overflow arguments, -and dynamic width/precision through an explicit guest va_list. Write failure -returns a negative result and refreshes the guest stream status. - -All fourteen CTest suites pass and patched compatibility libraries rebuild. -The unchanged converted game now resolves fcntl and fprintf, and startup stops -at `smbQukfxYJM` (`getenv`). This remains an import-resolution result: guest -entry-point execution and a rendered frame are still unverified. - -## Follow-up: guest environment variables - -Implemented getenv, setenv, unsetenv, and putenv as one case-sensitive guest -environment, initialized from the host CRT environment on first access. Guest -updates do not mutate the host environment. setenv owns a copy, while putenv -retains the caller's string so later changes are visible. Empty values remain -distinct from missing variables; invalid update names return guest EINVAL. -The old allocating host-putenv setenv implementation has been replaced. - -Tests cover inherited values, overwrite control, copying versus borrowed -storage, empty values, case sensitivity, removal, invalid names, and unchanged -host values. All fifteen suites pass. Direct environ access and propagation to -host child processes or host locale functions are not implemented by this store. - -Latest unchanged-game startup resolves getenv and stops at `ay3uROQAc5A` -(`opendir`). Guest entry-point execution and rendered output remain unverified. - -## Follow-up: directory enumeration - -Added opendir, readdir, closedir, and rewinddir using the existing guest path -resolver. Directory entries are translated into the SDK's FreeBSD 11 layout: -32-bit file number, 16-bit record length, byte type/name length, and the name -starting at offset 8. Record lengths include four-byte padding. Each directory -owns its returned entry buffer. End-of-directory preserves the caller's errno; -open/read failures translate common host errors into guest error values. - -Tests enumerate actual files and subdirectories, verify names/types and record -lengths, rewind, and check missing paths, non-directory paths, and EOF behavior. -All sixteen suites pass. The Windows MinGW backend provides no inode identity -(file number is zero) and uses host narrow-character filenames; full Unicode -filename coverage and direct access to opaque DIR internals are not established. - -The unchanged converted game now resolves opendir and stops at `MZO7FXyAPU8` -(`remove`). No guest entry-point execution or rendered frame has been observed. - -## Follow-up: file removal and renaming - -Added remove and rename using the existing guest path resolver. Removal handles -files and empty directories without recursive deletion. Windows removal uses -the native APIs because the host CRT collapses nonempty-directory failures into -access errors; the guest now receives ENOTEMPTY for that case. Renaming supports -replacing existing files and maps common host errors into guest errno values. - -Tests verify nonempty-directory preservation, file and empty-directory removal, -missing and invalid paths, replacement contents, same-path rename, and missing -rename sources. All seventeen suites pass. These checks do not establish full -PS5 mount semantics, cross-volume behavior, or replacement of existing directories. - -The unchanged converted game resolves remove and rename. Startup now stops at -`biY+kcVB5D4` (`dlsym`), requiring dynamic-module symbol lookup. It still has not -executed the guest entry point or displayed a frame. - -## Follow-up: dynamic-module lookup - -Added dlopen, dlsym, dlclose, and dlerror to libkernel. Handles are tracked rather -than exposing native loader handles. Symbol lookup tries the literal export and -then its PS5 NID, reusing the project's existing hash implementation. Repeated -opens hold separate loader references. Error messages are thread-local and are -consumed by dlerror. Native unload runs outside the module registry lock. - -A test builds and NID-patches a separate shared module, loads it, finds an export -by its original name, and calls it through the guest SysV ABI. It also checks -global visibility for explicitly opened modules, invalid/closed handles, -repeated opens, missing symbols, unsupported flags, and error isolation. -All eighteen suites pass. - -This is host-compatible module loading, not raw PS5 ELF loading on Windows. -RTLD_LAZY/NOW and GLOBAL are accepted; Windows binds eagerly. Default lookup -currently searches explicitly registered global modules only. Automatic lookup -across the startup dependency graph, RTLD_NEXT/SELF, other flags, and console -load/start entry points remain unsupported. The guest path resolver is reused; -full console library-search and mount semantics are not established. - -Latest unchanged-game startup resolves dlsym and stops at `DYivN1nO-JQ` -(`getcwd`). No guest entry-point execution or rendered frame has been observed. - -## Follow-up: working-directory state - -Added getcwd and chdir with shared guest path state. The initial host working -directory is captured as the guest root; guest cwd begins at `/`. Relative paths -follow guest cwd while absolute guest paths remain rooted at that captured -directory. Guest chdir does not change the host process cwd. Canonical directory -targets within that root are supported; targets outside it return EOPNOTSUPP. -Explicit Windows drive paths remain accepted by the general resolver for -compatibility with existing host-path callers. This is not a filesystem sandbox -or a complete PS5 mount implementation. - -Tests verify root/parent traversal, relative and absolute resolution after chdir, -missing/non-directory failures, unchanged host cwd, short-buffer handling, and -getcwd allocation freed through the guest heap. All nineteen suites pass. -Latest unchanged-game startup resolves getcwd and stops at `oVkZ8W8-Q8A` -(`strtok`). Guest execution and rendered output remain unverified. - -Added strtok using the existing strtok_r scanner with independent thread-local -guest state. Tests exercise repeated delimiters, changing delimiter sets, empty -delimiter sets, repeated end-of-input calls, and independence from host strtok. -All nineteen suites pass after rebuilding the libraries. Latest unchanged-game -startup resolves strtok and stops at `88Vv-AzHVj8` (`fmodf`). The game has still -not reached its entry point or produced a rendered frame. - -## Follow-up: math and floating-point parsing - -Added nineteen math exports covering single-precision remainder/trigonometry, -binary scaling and decomposition, integer rounding, and value classification. -Guest lround/lroundf return explicit 64-bit integers through host llround, avoiding -Windows' 32-bit long truncation. __isinff returns one for either sign of infinity, -matching the [FreeBSD 11 implementation](https://raw.githubusercontent.com/freebsd/freebsd-src/releng/11.4/lib/libc/gen/isinf.c). -Added atof, strtof, and strtold using the host CRT through the guest calling -convention. Tests verify extended-precision results through the long-double ABI. - -Tests cover negative remainders and signed zero, NaN/infinity/subnormal classes, -integer results above 32 bits, exponent output pointers, numeric parsing end -pointers, invalid input, and overflow errno. The math suite passes and the full -suite contains twenty passing tests. Exhaustive numerical accuracy, alternate -rounding modes, locale-dependent parsing, and all floating-point exception -semantics have not been validated. - -The unchanged game resolves fmodf and atof. Startup now stops at `TYE4irxSmko` -(`toupper`), still before guest entry-point execution or rendered output. - -## Follow-up: character classification and basename - -Added fourteen byte-character classification and case-conversion exports for -the runtime's classic C locale. Tests compare every byte value and EOF against -C-locale classification and verify case conversion agrees with existing guest -locale tables. Non-C locales are not implemented by these exports. - -Added basename with non-mutating input handling, trailing-slash removal, -root/empty/null cases, and the SDK's 1024-byte component buffer limit. The -contract follows [FreeBSD 11 basename](https://raw.githubusercontent.com/freebsd/freebsd-src/releng/11.4/lib/libc/gen/basename.c); -result storage is thread-local. Tests include unchanged input, literal backslash -handling, and ENAMETOOLONG. String tests now link the built compatibility -libraries rather than compiling implementation copies into the test executable. - -All twenty-one suites pass. The unchanged converted game resolves toupper and -basename and now stops at `5qP1iVQkdck` (`ftello`). Guest entry-point execution and -rendered output remain unverified. - -## Follow-up: 64-bit stream positioning - -Added ftello/fseeko and corrected fseek/ftell to use explicit 64-bit guest -offsets and return values. Windows uses _fseeki64/_ftelli64, avoiding native -32-bit long truncation. Invalid origins and native seek failures return errors -rather than throwing; successful seeks refresh the guest stream status. - -Tests seek past 4 GiB without writing a large file, verify both API pairs, -relative seeks, invalid-origin position preservation, SEEK_END, and clearing -EOF after seeking. All twenty-one suites pass and libraries rebuild. - -Latest unchanged-game startup resolves ftello and stops at `JoBqSQt1yyA` -(`sceSystemServiceLoadExec`). Inspection of the released port's entry source -shows it requests the console's `exit` operation from its quit path. This is -source evidence only; the guest entry point still has not executed and no frame -has been rendered. - -## Follow-up: console exit request - -Implemented sceSystemServiceLoadExec for the `exit` operation through the shared -runtime shutdown callbacks followed by normal process exit. Null/empty paths -return the existing system-service parameter error. Replacing the process with -another executable remains explicitly unsupported and raises the runtime's -existing not-implemented diagnostic; it does not report a successful launch. - -An isolated CTest process verifies that shutdown callbacks execute before -atexit handlers and that the service does not return after requesting exit. -A separate test checks invalid arguments and executable-replacement rejection. -All twenty-three tests pass and libraries rebuild. - -The unchanged game resolves sceSystemServiceLoadExec and now stops at -`gkWgn0p1AfU` (`freopen`). This does not establish that the game has executed -its quit path: guest entry-point execution and rendered output remain unverified. - -## Follow-up: stream reopening and immediate exit - -Added freopen while preserving the guest FileStream address on success. Reopen -clears buffered guest metadata and refreshes the descriptor. A failed native -reopen closes the underlying file and clears the wrapper state; dynamically -allocated wrappers are released on failure. Tests exercise write/read reopening, -append behavior, unchanged wrapper identity, EOF clearing, and missing files. -Mode-only reopening (null filename) is explicitly unsupported; common r/w/a -modes with optional binary/update modifiers are accepted. - -Added _Exit through immediate host termination. An isolated process test proves -it bypasses both runtime cleanup and atexit handlers, complementing the normal -shutdown test. All twenty-four tests pass and the libraries rebuild. - -The unchanged game resolves freopen and _Exit. Startup now stops at -`MqAdbRMdNz4` (`sceAgcLinkShaders`), moving the next investigation into shader -linking. No guest entry-point execution or rendered frame has been observed. - -## Follow-up: basic shader linking - -Added sceAgcLinkShaders for the basic vertex/pixel pipeline by composing the -existing primitive-state and interpolant-mapping implementations. It produces -34 context-register records and three user-configuration records, matching the -buffer sizes consumed by the native ps5link-sdk gpu_cube example. The context -layout combines two primitive-state records with 32 interpolant records; this -composition is inferred from the existing helpers and native caller, not from -a captured hardware result. No homebrew rendering code was copied into the library. - -Inputs are validated and results are staged before touching caller outputs. -Tests check triangle/line primitive values, identity interpolants, semantic -matching, flat shading, missing-semantic defaults, buffer boundaries, and -unchanged outputs on invalid input. Reserved/non-basic pipeline inputs are -rejected. All twenty-five tests pass; comparison against real console register -output and end-to-end rendering remains outstanding. - -The unchanged game resolves sceAgcLinkShaders and stops at `Wi82ArQtAwg` -(`sceAgcGetRegisterDefaults`). The project already has a versioned defaults -implementation; the legacy entry point still needs its contract verified. -Guest entry-point execution and rendered output remain unverified. - -## Follow-up: legacy register-defaults entry point - -Added the no-argument sceAgcGetRegisterDefaults export using the existing -baseline public table. The legacy caller reads its context-block pointer at -offset 0 and its context-record count at offset 0x20; tests now exercise that -layout directly, scan all 523 context records, check render-target/rasterizer -register presence, and verify stable pointers across calls. - -Selecting public version zero is a compatibility assumption based on the legacy -API lacking a version parameter; it has not been compared with a captured -console response. Versioned APIs remain available. All twenty-five tests pass. - -The unchanged game now stops at `0MtUJ3BpGhE` -(`sceAgcDriverGetWaitRenderingPacketSizeInDwords`). The corresponding -sceAgcDriverWaitUntilSafeForRendering import is also absent. These need a real -command-packet and buffer-ownership implementation, not a zero-size/no-op shim. -Guest entry-point execution and rendered output are still unverified. - -## Follow-up: buffer reuse ordering - -Added per-buffer reservation tickets to VideoOut. Flip reservations acquire a -monotonic ticket; presentation completion, failure, and reservation cancellation -retire it. A captured fence is complete once all earlier tickets retire, even -if later flips for the same buffer are already reserved. This avoids the -self-dependency that a wait on the total pending-buffer count would create when -submission reserves a future flip before executing its preceding wait packet. - -Tests cover out-of-order retirement, future reservations, empty fences, and -invalid future fences. All twenty-six CTest cases and the existing VideoOut -flip-lifetime executable pass. This is the ownership foundation; the rendering -wait exports and command-dispatch connection are not implemented yet, so the -game remains stopped at the same missing packet-size import. - -Inspection of the native ps5-opengl caller establishes the wait argument order -as command pointer, packet capacity, mode, video handle, buffer index. The -parameter names in the link-only stub do not reflect that calling convention. -No game runtime source was copied into the compatibility library. - -## Follow-up: rendering-wait command execution - -Implemented sceAgcDriverGetWaitRenderingPacketSizeInDwords and -sceAgcDriverWaitUntilSafeForRendering for mode zero. The writer validates -capacity and writable memory, emits a four-dword internal wait packet, and -advances the guest command pointer. This encoding is owned by AnyPS5's paired -writer/decoder; it is not claimed to reproduce console-generated packet bytes. - -Submission captures a VideoOut buffer ticket before reserving subsequent flips. -The worker executes the wait outside the GPU mutex. VideoOut waits for earlier -reservations to retire and wakes on close or failure. The captured configuration -object prevents a reopened handle from satisfying a wait for an old port. - -A test submits a generated wait followed by a flip through the real AGC driver -with a controlled output. It proves the wait blocks on earlier ownership and -continues after that ownership is released, despite the later flip reservation. -It also checks packet capacity, pointer advancement, and write boundaries. -All twenty-seven CTest cases pass, along with the existing VideoOut flip lifetime -and reopen regression executables. Actual console packet compatibility, other -wait modes, and rendering a game frame remain unverified. - -The unchanged game now resolves both rendering-wait imports and stops at -`Z4QosVuAsA0` (`pthread_once`). Guest entry-point execution has not yet occurred. - -## Follow-up: once initialization and architecture audit - -Implemented pthread_once with the guest 16-byte control layout, a SysV callback, -serialized initialization, and publication of initialized data to waiting callers. -Independent controls can initialize recursively; an initializer that unwinds with -a C++ exception resets its control for retry. Guest thread cancellation and fork -recovery are not implemented. Tests use host threads calling the guest ABI and do -not establish that guest pthread_create works. - -All twenty-eight CTest cases pass. The unchanged converted game now stops at -`NesIgTmfF0Q` (`bsearch`) with exit `0xc0000139`, before guest entry. - -The README architecture remains the target: native executable relinking and -in-process replacement system libraries. There is no replacement game engine or -game source in these additions. Windows compatibility tests do not establish -Linux compatibility or a successful game launch. Shader register layout and the -internal rendering-wait encoding still require actual game execution validation. -The shader-link wrapper now propagates unsupported-state runtime exceptions from -its helpers, preserving the README's strict failure policy; its regression test -checks the exception and that output registers are not partially published. - -## Follow-up: binary search - -Implemented bsearch with direct SysV comparator calls, key-first argument order, -and overflow-checked element addressing. Regression coverage uses records and a -distinct key type to check callback argument order, duplicates, absent keys, -empty and singleton arrays, and unchanged input bytes. All twenty-nine tests -have passed (the new test was rerun after correcting its padding-byte snapshot). -The unchanged game resolves bsearch and stops at `jbz9I9vkqkk` (`vsprintf`), -still before guest entry with exit `0xc0000139`. - -## Follow-up: unbounded varargs formatting - -Added vsprintf through the existing guest varargs formatter on Windows and the -native SysV va_list path on Linux. The caller remains responsible for buffer -capacity, as with the C interface. Windows tests cover mixed integer/string/ -floating arguments, guest 64-bit long, width and precision arguments, register -overflow onto the stack, long double, percent escapes, count writes, empty -output, termination, and the byte immediately after the output. The formatter's -existing unsupported conversions still fail explicitly; this does not establish -complete printf compatibility or Linux runtime validation. - -All twenty-nine CTest cases pass. The unchanged Quake II executable now resolves -vsprintf and stops at `Jc6E7N+dHz0` (`system`), before guest entry, with exit -`0xc0000139`. No game frame has been rendered. Upstream was fetched again and -had no new commits to merge. From 223777d3df2b8ae1c15fa4c556796bb048d37af1 Mon Sep 17 00:00:00 2001 From: boykopovar Date: Sat, 26 Sep 2026 11:00:59 +0300 Subject: [PATCH 45/94] docs(dev): add item about custom sce_module load to tech debt --- docs/TechnicalDebt.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/TechnicalDebt.md b/docs/TechnicalDebt.md index bf4a06b7c..8d46709fd 100644 --- a/docs/TechnicalDebt.md +++ b/docs/TechnicalDebt.md @@ -30,3 +30,4 @@ Throughout the project, every function at every stage either **does exactly what - [Shader recompilation](../core/shader/recompiler/Recompiler.cpp) currently occurs right before it was transferred to Vulkan with caching, but should be moved to the [relinker](../core/relinker/main.cpp) stage. For this purpose, [shader/recompiler](../core/shader/recompiler) was written completely independently from [libs/prx](../core/libs/prx). - The executable file that [relinker](../core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp) generates opens the console when launched, which is inconvenient for playability. - [Relinker](../core/relinker/elfpatcher/src) doesn't add an icon to the generated executable. This should be done without adding dependencies (only standard). +- The game can expect its modified prx from the `sce_module`/`sce_modules` folder - this support is not implemented. From c6f2d98fddf88ecadbff411582792958acfb75df Mon Sep 17 00:00:00 2001 From: Pacuka <69674426+Pacuka@users.noreply.github.com> Date: Sat, 26 Sep 2026 11:15:16 +0200 Subject: [PATCH 46/94] fix(build): configure bundled SDL2 without static executable linking Distributions without static X11 libraries (such as Arch-based ones) fail SDL2's X11 feature checks because the global -static linker flag also applies to its try_compile probes. Restore the flag after SDL2 is added. Co-Authored-By: Claude Opus 5.5 --- CMakeLists.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 98a1b94b4..8308a7613 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,7 +28,10 @@ set(SDL_HAPTIC OFF CACHE BOOL "" FORCE) set(SDL_HIDAPI OFF CACHE BOOL "" FORCE) set(SDL_SENSOR OFF CACHE BOOL "" FORCE) set(SDL_STATIC_PIC ON CACHE BOOL "" FORCE) +set(ANYPS5_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}") +set(CMAKE_EXE_LINKER_FLAGS "") add_subdirectory(3rdparty/SDL2) +set(CMAKE_EXE_LINKER_FLAGS "${ANYPS5_EXE_LINKER_FLAGS}") add_subdirectory(core/shader/recompiler) add_subdirectory(core/libs) From a4273c10dcf279b5506690e97ebc73caccb4be0a Mon Sep 17 00:00:00 2001 From: Pacuka <69674426+Pacuka@users.noreply.github.com> Date: Sat, 26 Sep 2026 11:15:16 +0200 Subject: [PATCH 47/94] fix(videoout): drop __stdcall from the window procedure GCC on Linux rejects the attribute, which breaks the libs build since the F11 fullscreen change. It has no effect on x86-64 Windows. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceVideoOut/include/DisplayWindow.hpp | 2 +- core/libs/prx/libSceVideoOut/src/DisplayWindow.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceVideoOut/include/DisplayWindow.hpp b/core/libs/prx/libSceVideoOut/include/DisplayWindow.hpp index 1c7130a78..18cfc77f1 100644 --- a/core/libs/prx/libSceVideoOut/include/DisplayWindow.hpp +++ b/core/libs/prx/libSceVideoOut/include/DisplayWindow.hpp @@ -29,7 +29,7 @@ private: void removeSubclass() noexcept; void applyAspectRatio(void* hwnd, std::uintptr_t edge, void* rect) const; - static std::intptr_t __stdcall windowProc(void* hwnd, unsigned int message, std::uintptr_t wParam, std::intptr_t lParam, std::uintptr_t subclassId, std::uintptr_t referenceData); + static std::intptr_t windowProc(void* hwnd, unsigned int message, std::uintptr_t wParam, std::intptr_t lParam, std::uintptr_t subclassId, std::uintptr_t referenceData); SDL_Window* window = nullptr; std::uint32_t aspectWidth = 0; diff --git a/core/libs/prx/libSceVideoOut/src/DisplayWindow.cpp b/core/libs/prx/libSceVideoOut/src/DisplayWindow.cpp index cced33f9a..6717efe30 100644 --- a/core/libs/prx/libSceVideoOut/src/DisplayWindow.cpp +++ b/core/libs/prx/libSceVideoOut/src/DisplayWindow.cpp @@ -158,7 +158,7 @@ void DisplayWindow::applyAspectRatio(void* hwnd, std::uintptr_t edge, void* rect #endif } -std::intptr_t __stdcall DisplayWindow::windowProc(void* hwnd, unsigned int message, std::uintptr_t wParam, std::intptr_t lParam, std::uintptr_t subclassId, std::uintptr_t referenceData) { +std::intptr_t DisplayWindow::windowProc(void* hwnd, unsigned int message, std::uintptr_t wParam, std::intptr_t lParam, std::uintptr_t subclassId, std::uintptr_t referenceData) { #ifdef _WIN32 static_cast(subclassId); if (message == WM_SIZING) { From 998605fe4e2f54ef5b662ea516dd7597943354ff Mon Sep 17 00:00:00 2001 From: Pacuka <69674426+Pacuka@users.noreply.github.com> Date: Sat, 26 Sep 2026 11:15:16 +0200 Subject: [PATCH 48/94] fix(libc): swap calloc and realloc slots of the malloc replacement table SceLibcMallocReplace lists calloc before realloc (malloc, free, calloc, realloc, memalign, reallocalign, posix_memalign, ...), as the table in retail titles shows. realloc therefore called the application's calloc with the old pointer as the element count, and calloc called realloc. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libc/src/ApplicationHeap.cpp | 4 ++-- core/libs/tests/ApplicationHeap.cpp | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libc/src/ApplicationHeap.cpp b/core/libs/prx/libc/src/ApplicationHeap.cpp index ffd62cc5a..81e17250d 100644 --- a/core/libs/prx/libc/src/ApplicationHeap.cpp +++ b/core/libs/prx/libc/src/ApplicationHeap.cpp @@ -142,7 +142,7 @@ void* ApplicationHeapReallocate_nid_no_patch(void* pointer, std::size_t bytes) { ApplicationHeapFree_nid_no_patch(pointer); return nullptr; } - const auto reallocate = callback(2); + const auto reallocate = callback(3); CallbackScope scope; return requireAllocation(reallocate(pointer, bytes)); } @@ -158,7 +158,7 @@ void* ApplicationHeapAlign_nid_no_patch(std::size_t alignment, std::size_t bytes void* ApplicationHeapCalloc_nid_no_patch(std::size_t count, std::size_t bytes) { if (bytes != 0 && count > std::numeric_limits::max() / bytes) throw std::length_error("application heap: calloc size overflow"); - const auto calloc = callback(3); + const auto calloc = callback(2); CallbackScope scope; return requireAllocation(calloc(count, bytes)); } diff --git a/core/libs/tests/ApplicationHeap.cpp b/core/libs/tests/ApplicationHeap.cpp index 0d4a8e900..48009f88e 100644 --- a/core/libs/tests/ApplicationHeap.cpp +++ b/core/libs/tests/ApplicationHeap.cpp @@ -87,8 +87,8 @@ int main(int argc, char**) { write(replacement, 0x18, &finalize); write(replacement, 0x20, &allocate); write(replacement, 0x28, &release); - write(replacement, 0x30, &reallocate); - write(replacement, 0x38, &allocateZeroed); + write(replacement, 0x30, &allocateZeroed); + write(replacement, 0x38, &reallocate); write(replacement, 0x40, &align); write(replacement, 0x48, &reallocate); write(replacement, 0x50, &posixAlign); From d15e93681fe22f47f7beff8fc37c2be339d1281a Mon Sep 17 00:00:00 2001 From: Pacuka <69674426+Pacuka@users.noreply.github.com> Date: Sat, 26 Sep 2026 11:15:16 +0200 Subject: [PATCH 49/94] fix(kernel): map SCE protections to readable host pages SCE_KERNEL_PROT_CPU_RW (0x02) and CPU_EXEC (0x04) include read access, but were translated to write-only and execute-only host pages. GPU read and write bits also need host access because the emulated GPU reads and writes guest memory from the CPU. Write-only mappings made the AGC driver reject guest data segments as unreadable. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp index 69be5eb95..9c74f54c9 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp @@ -66,9 +66,9 @@ int LinuxProtFromSce(int prot) { throw std::invalid_argument("Unsupported memory protection bits"); } int result = PROT_NONE; - if (prot & 1) result |= PROT_READ; - if (prot & 2) result |= PROT_WRITE; - if (prot & 4) result |= PROT_EXEC; + if (prot & 0x13) result |= PROT_READ; + if (prot & 0x22) result |= PROT_READ | PROT_WRITE; + if (prot & 4) result |= PROT_READ | PROT_EXEC; return result; } From ca23f1061296d68c954311b4cbe5c0e747443d83 Mon Sep 17 00:00:00 2001 From: Pacuka <69674426+Pacuka@users.noreply.github.com> Date: Sat, 26 Sep 2026 11:15:16 +0200 Subject: [PATCH 50/94] fix(kernel): accept AMPR and ACP access bits in memory protections Retail titles pass protections such as 0xC2 (CPU read/write plus the 0x40 and 0x80 device access bits). The device bits do not affect CPU access and were rejected as unsupported. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp index 9c74f54c9..ac952bac7 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp @@ -61,7 +61,7 @@ void ValidateRange(const void* addr, size_t len, size_t alignment) { } int LinuxProtFromSce(int prot) { - if ((prot & ~0x37) != 0) { + if ((prot & ~0xF7) != 0) { // return SCE_KERNEL_ERROR_EINVAL; throw std::invalid_argument("Unsupported memory protection bits"); } From b98257b75ddaf8a0b24a08167ace9b0cdb10a6fa Mon Sep 17 00:00:00 2001 From: Pacuka <69674426+Pacuka@users.noreply.github.com> Date: Sat, 26 Sep 2026 11:15:16 +0200 Subject: [PATCH 51/94] fix(libc): register the main guest image on Linux Guest mprotect calls on the executable's own segments failed with "guest protection or unmap range is not registered" because only the Windows build registered the main image. Register the PT_LOAD segments of the main executable page by page, merging the protections of segments that share a page. Co-Authored-By: Claude Opus 5.5 --- .../prx/libc/include/GuestAllocations.hpp | 4 -- core/libs/prx/libc/src/GuestAllocations.cpp | 48 +++++++++++++++++++ .../libkernel/DirectMemory/DirectMemory.cpp | 2 + 3 files changed, 50 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libc/include/GuestAllocations.hpp b/core/libs/prx/libc/include/GuestAllocations.hpp index db5f0394f..b1fd0f44c 100644 --- a/core/libs/prx/libc/include/GuestAllocations.hpp +++ b/core/libs/prx/libc/include/GuestAllocations.hpp @@ -24,9 +24,7 @@ using Lease = std::vector>; extern "C" { void* GuestAllocationsBegin_nid_postfix(); -#ifdef _WIN32 void GuestAllocationsRegisterMainImage_nid_postfix(void* mutation); -#endif void GuestAllocationsEnd_nid_postfix(void* mutation) noexcept; void GuestAllocationsAdd_nid_postfix(void* mutation, void* pointer, std::size_t bytes, bool readable, bool writable); void GuestAllocationsRequireUnpinned_nid_postfix(void* mutation, const void* pointer, std::size_t bytes); @@ -44,9 +42,7 @@ public: ~Mutation() { GuestAllocationsEnd_nid_postfix(handle); } Mutation(const Mutation&) = delete; Mutation& operator=(const Mutation&) = delete; -#ifdef _WIN32 void RegisterMainImage() { GuestAllocationsRegisterMainImage_nid_postfix(handle); } -#endif void Add(void* pointer, std::size_t bytes, bool readable, bool writable) { GuestAllocationsAdd_nid_postfix(handle, pointer, bytes, readable, writable); } void RequireUnpinned(const void* pointer, std::size_t bytes) const { GuestAllocationsRequireUnpinned_nid_postfix(handle, pointer, bytes); } void RequireAvailable(const void* pointer, std::size_t bytes) const { GuestAllocationsRequireAvailable_nid_postfix(handle, pointer, bytes); } diff --git a/core/libs/prx/libc/src/GuestAllocations.cpp b/core/libs/prx/libc/src/GuestAllocations.cpp index bcac0d0cf..08e9dfc0c 100644 --- a/core/libs/prx/libc/src/GuestAllocations.cpp +++ b/core/libs/prx/libc/src/GuestAllocations.cpp @@ -9,6 +9,9 @@ #define NOMINMAX #endif #include +#else +#include +#include #endif namespace GuestAllocations { @@ -75,6 +78,51 @@ void GuestAllocationsRegisterMainImage_nid_postfix(void*) { state.ranges.swap(replacement); state.mainImageRegistered = true; } +#else +void GuestAllocationsRegisterMainImage_nid_postfix(void*) { + auto& state = registry(); + if (state.mainImageRegistered) return; + struct Page { + bool readable = false; + bool writable = false; + }; + std::map pages; + const auto pageSize = static_cast(::sysconf(_SC_PAGESIZE)); + std::pair*, std::uint64_t> collection{&pages, pageSize}; + dl_iterate_phdr([](dl_phdr_info* image, std::size_t, void* data) { + auto& collected = *static_cast*, std::uint64_t>*>(data); + for (int index = 0; index < image->dlpi_phnum; ++index) { + const auto& header = image->dlpi_phdr[index]; + if (header.p_type != PT_LOAD || header.p_memsz == 0) continue; + const auto start = (image->dlpi_addr + header.p_vaddr) & ~(collected.second - 1); + const auto end = (image->dlpi_addr + header.p_vaddr + header.p_memsz + collected.second - 1) & ~(collected.second - 1); + for (auto page = start; page < end; page += collected.second) { + auto& entry = (*collected.first)[page]; + entry.readable = entry.readable || (header.p_flags & (PF_R | PF_W)) != 0; + entry.writable = entry.writable || (header.p_flags & PF_W) != 0; + } + } + return 1; + }, &collection); + require(!pages.empty(), "main guest image has no loadable segments"); + auto replacement = state.ranges; + for (auto page = pages.begin(); page != pages.end();) { + auto last = page; + while (std::next(last) != pages.end() && std::next(last)->first == last->first + pageSize && std::next(last)->second.readable == page->second.readable && std::next(last)->second.writable == page->second.writable) ++last; + const auto address = page->first; + const auto bytes = static_cast(last->first + pageSize - address); + const auto next = replacement.lower_bound(address); + require(next == replacement.end() || address + bytes <= next->first, "guest image overlaps a registered allocation"); + if (next != replacement.begin()) { + const auto& previous = *std::prev(next)->second; + require(previous.address + previous.bytes <= address, "guest image overlaps a registered allocation"); + } + replacement.emplace(address, std::make_shared(Range{address, bytes, page->second.readable, page->second.writable, address, bytes, false})); + page = std::next(last); + } + state.ranges.swap(replacement); + state.mainImageRegistered = true; +} #endif void GuestAllocationsAdd_nid_postfix(void*, void* pointer, std::size_t bytes, bool readable, bool writable) { diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp index ac952bac7..6bb6e60f8 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp @@ -149,6 +149,8 @@ int DoMprotect(const void* addr, size_t len, int prot) { if (memory.AllocationBase != GetModuleHandleW(nullptr)) throw std::invalid_argument("Memory protection of a foreign image is not supported"); mutation.RegisterMainImage(); } +#else + mutation.RegisterMainImage(); #endif mutation.Protect(pointer, bytes, (prot & 3) != 0, (prot & 2) != 0, [&] { if (mprotect(const_cast(pointer), bytes, nativeProtection) != 0) throw std::system_error(errno, std::generic_category(), "mprotect failed"); From f9695c1069d91ec6ecf34ed58cb95cf1abfd1f62 Mon Sep 17 00:00:00 2001 From: Pacuka <69674426+Pacuka@users.noreply.github.com> Date: Sat, 26 Sep 2026 11:15:16 +0200 Subject: [PATCH 52/94] fix(agc): accept the SDK default GDS range in sceAgcCbReleaseMem The SDK's inline release wrapper passes gdsOffset 0 and gdsSize 1 for every release, not only GDS ones. The packet ignores the range unless the GDS data selector is used, so accept that default. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgc/Cb/src/Sync.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgc/Cb/src/Sync.cpp b/core/libs/prx/libSceAgc/Cb/src/Sync.cpp index 74115f047..e87166ec6 100644 --- a/core/libs/prx/libSceAgc/Cb/src/Sync.cpp +++ b/core/libs/prx/libSceAgc/Cb/src/Sync.cpp @@ -38,7 +38,7 @@ std::uint32_t* APS5_VABI sceAgcCbReleaseMem(CommandBuffer* buf, std::uint8_t act Agc::Command::Require(data == 0, __func__, "GDS release cannot use immediate data"); value = gdsOffset | (static_cast(gdsSize) << 16u); } else { - Agc::Command::Require(gdsOffset == 0 && gdsSize == 0, __func__, "GDS parameters supplied for a non-GDS release"); + Agc::Command::Require(gdsOffset == 0 && gdsSize <= 1, __func__, "GDS parameters supplied for a non-GDS release"); } if (dataSelect != 0 && interrupt != 4) { Agc::Command::CheckAddress(guestAddress, dataSelect == 2 || dataSelect == 3 ? 8 : 4, __func__); From 37074ab8930d0ead7df9695d0c9707dc5cba29bc Mon Sep 17 00:00:00 2001 From: Pacuka <69674426+Pacuka@users.noreply.github.com> Date: Sat, 26 Sep 2026 11:15:16 +0200 Subject: [PATCH 53/94] fix(build): link guest library tests dynamically on Linux Tests in core/libs link against the prx shared libraries, which the global -static flag rejects on Linux ("attempted static link of dynamic object"). Drop -static in that directory on non-Windows hosts while keeping the static GCC runtime. Co-Authored-By: Claude Opus 5.5 --- core/libs/CMakeLists.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 35cdadb35..57f29f6fc 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -1,3 +1,8 @@ +if(NOT WIN32) + string(REPLACE "-static " "" CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ") + string(STRIP "${CMAKE_EXE_LINKER_FLAGS}" CMAKE_EXE_LINKER_FLAGS) +endif() + add_executable(nid_patcher ${CMAKE_CURRENT_SOURCE_DIR}/nid/src/Sha1.cpp ${CMAKE_CURRENT_SOURCE_DIR}/nid/src/NidCompute.cpp From 2f30b62a698b648a53aa2abc57740476fa522967 Mon Sep 17 00:00:00 2001 From: boykopovar Date: Sat, 26 Sep 2026 12:47:31 +0300 Subject: [PATCH 54/94] fix(relinker): use general pipeline after converting for --to-intel flag --- core/relinker/main.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index 58c2658d3..90f087e60 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -44,12 +44,12 @@ int main(const int argc, char* argv[]) { const Relinker::ElfReader elfReader(sourceBytes); const auto converter = Codegen::MakeAmd64OnlyConverter(); - auto result = converter->Convert(std::move(sourceBytes), elfReader.ReadCodeSegments()); - fileWriter.Write(absPath, std::move(result.Bytes)); + auto codeSegments = elfReader.ReadCodeSegments(); + auto result = converter->Convert(std::move(sourceBytes), codeSegments); + + sourceBytes = std::move(result.Bytes); std::cout << "OK: " << result.ReplacedCount << " instructions replaced\n"; - - if (args.autorun) return Cli::Autorun(absPath, args.toWindows); } auto elfReader = std::make_shared(sourceBytes); From d1c25097e317529dc4301d4f2df516e46fed9c69 Mon Sep 17 00:00:00 2001 From: Pacuka <69674426+Pacuka@users.noreply.github.com> Date: Sat, 26 Sep 2026 12:14:46 +0200 Subject: [PATCH 55/94] feat(random): implement sceRandomGetRandomNumber Fill the buffer from std::random_device and reject a null buffer or a size above SCE_RANDOM_MAX_SIZE (64) with SCE_RANDOM_ERROR_INVALID, as the system library does. Co-Authored-By: Claude Opus 5.5 --- core/libs/CMakeLists.txt | 8 ++++++++ core/libs/prx/libSceRandom/Export.cpp | 22 ++++++++++++++++++---- core/libs/tests/GuestRandom.cpp | 26 ++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 4 deletions(-) create mode 100644 core/libs/tests/GuestRandom.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 57f29f6fc..cdc06788b 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -286,6 +286,14 @@ if(BUILD_TESTING) set_tests_properties(guest_errors PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_executable(guest_random_tests tests/GuestRandom.cpp) + target_include_directories(guest_random_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_random_tests PRIVATE libSceRandom) + add_test(NAME guest_random COMMAND guest_random_tests) + if(WIN32) + set_tests_properties(guest_random PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_executable(guest_resolver_tests tests/GuestResolver.cpp) target_include_directories(guest_resolver_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_resolver_tests PRIVATE libScePosixForWebKit) diff --git a/core/libs/prx/libSceRandom/Export.cpp b/core/libs/prx/libSceRandom/Export.cpp index 4ab56faf2..4e9ef2674 100644 --- a/core/libs/prx/libSceRandom/Export.cpp +++ b/core/libs/prx/libSceRandom/Export.cpp @@ -1,15 +1,29 @@ #include #include +#include +#include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { + +constexpr int SCE_RANDOM_ERROR_INVALID = static_cast(0x817C0016); +constexpr std::size_t SCE_RANDOM_MAX_SIZE = 64; + +} + extern "C" { int APS5_VABI sceRandomGetRandomNumber(void* buf, size_t size) { - (void)buf; - (void)size; - NotImplemented_nid_no_patch(__func__); - return 0; + if (!buf || size > SCE_RANDOM_MAX_SIZE) return SCE_RANDOM_ERROR_INVALID; + static thread_local std::random_device device; + auto* bytes = static_cast(buf); + for (std::size_t offset = 0; offset < size; offset += sizeof(unsigned int)) { + const unsigned int value = device(); + std::memcpy(bytes + offset, &value, std::min(sizeof(value), size - offset)); + } + return 0; } } diff --git a/core/libs/tests/GuestRandom.cpp b/core/libs/tests/GuestRandom.cpp new file mode 100644 index 000000000..f611927f8 --- /dev/null +++ b/core/libs/tests/GuestRandom.cpp @@ -0,0 +1,26 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include + +extern "C" { +int APS5_VABI sceRandomGetRandomNumber(void*, std::size_t); +} +static void Require(bool value) { if (!value) std::abort(); } +int main() { + constexpr int invalid = static_cast(0x817C0016); + unsigned char buffer[80]; + Require(sceRandomGetRandomNumber(nullptr, 16) == invalid); + std::memset(buffer, 0xAA, sizeof(buffer)); + Require(sceRandomGetRandomNumber(buffer, 65) == invalid); + Require(buffer[0] == 0xAA); + Require(sceRandomGetRandomNumber(buffer, 0) == 0); + Require(buffer[0] == 0xAA); + Require(sceRandomGetRandomNumber(buffer, 7) == 0); + Require(buffer[7] == 0xAA); + unsigned char first[64] = {}; + unsigned char second[64] = {}; + Require(sceRandomGetRandomNumber(first, sizeof(first)) == 0); + Require(sceRandomGetRandomNumber(second, sizeof(second)) == 0); + Require(std::memcmp(first, second, sizeof(first)) != 0); +} From c0508a7f5f60616f80398f130a2f0ef717a73f4b Mon Sep 17 00:00:00 2001 From: Pacuka <69674426+Pacuka@users.noreply.github.com> Date: Sat, 26 Sep 2026 12:21:06 +0200 Subject: [PATCH 56/94] feat(rtc): implement libSceRtc date and tick functions Ticks count microseconds since 0001-01-01 in the proleptic Gregorian calendar, up to 9999-12-31 23:59:59.999999. Implement validation, tick and date conversion, time_t and Win32 FILETIME conversion, UTC and local time conversion through the host time zone, RFC 3339 formatting and parsing, and tick arithmetic. Add sceRtcGetDaysInMonth, sceRtcTickAddMonths and sceRtcTickAddYears, which clamp the day to the end of the target month. The network clock uses the host clock. Co-Authored-By: Claude Opus 5.5 --- core/libs/CMakeLists.txt | 8 + core/libs/prx/libSceRtc/Export.cpp | 454 +++++++++++++++++++---------- core/libs/tests/GuestRtc.cpp | 130 +++++++++ 3 files changed, 442 insertions(+), 150 deletions(-) create mode 100644 core/libs/tests/GuestRtc.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 57f29f6fc..3df423d28 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -294,6 +294,14 @@ if(BUILD_TESTING) set_tests_properties(guest_resolver PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_executable(guest_rtc_tests tests/GuestRtc.cpp) + target_include_directories(guest_rtc_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_rtc_tests PRIVATE libSceRtc) + add_test(NAME guest_rtc COMMAND guest_rtc_tests) + if(WIN32) + set_tests_properties(guest_rtc PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_executable(mspace_tests tests/Mspace.cpp) target_include_directories(mspace_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(mspace_tests PRIVATE libc) diff --git a/core/libs/prx/libSceRtc/Export.cpp b/core/libs/prx/libSceRtc/Export.cpp index 4065bf0bc..827c70395 100644 --- a/core/libs/prx/libSceRtc/Export.cpp +++ b/core/libs/prx/libSceRtc/Export.cpp @@ -1,185 +1,339 @@ +#include #include #include +#include +#include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { + +constexpr int SCE_RTC_ERROR_INVALID_POINTER = static_cast(0x80B50002); +constexpr int SCE_RTC_ERROR_INVALID_VALUE = static_cast(0x80B50003); +constexpr int SCE_RTC_ERROR_BAD_PARSE = static_cast(0x80B50007); +constexpr int SCE_RTC_ERROR_INVALID_YEAR = static_cast(0x80B50008); +constexpr int SCE_RTC_ERROR_INVALID_MONTH = static_cast(0x80B50009); +constexpr int SCE_RTC_ERROR_INVALID_DAY = static_cast(0x80B5000A); +constexpr int SCE_RTC_ERROR_INVALID_HOUR = static_cast(0x80B5000B); +constexpr int SCE_RTC_ERROR_INVALID_MINUTE = static_cast(0x80B5000C); +constexpr int SCE_RTC_ERROR_INVALID_SECOND = static_cast(0x80B5000D); +constexpr int SCE_RTC_ERROR_INVALID_MICROSECOND = static_cast(0x80B5000E); + +constexpr std::int64_t TICKS_PER_SECOND = 1000000; +constexpr std::int64_t TICKS_PER_MINUTE = 60 * TICKS_PER_SECOND; +constexpr std::int64_t TICKS_PER_HOUR = 60 * TICKS_PER_MINUTE; +constexpr std::int64_t TICKS_PER_DAY = 24 * TICKS_PER_HOUR; +constexpr std::int64_t UNIX_EPOCH_DAYS = 719162; +constexpr std::int64_t WIN32_EPOCH_DAYS = 584388; +constexpr std::uint64_t UNIX_EPOCH_TICK = UNIX_EPOCH_DAYS * TICKS_PER_DAY; +constexpr std::uint64_t WIN32_EPOCH_TICK = WIN32_EPOCH_DAYS * TICKS_PER_DAY; +constexpr std::uint64_t MAX_TICK = 3652059ull * TICKS_PER_DAY - 1; + +bool isLeapYear(std::int64_t year) { + return (year % 4 == 0 && year % 100 != 0) || year % 400 == 0; +} + +int daysInMonth(std::int64_t year, int month) { + static constexpr int days[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; + return month == 2 && isLeapYear(year) ? 29 : days[month - 1]; +} + +std::int64_t daysFromCivil(std::int64_t year, unsigned month, unsigned day) { + year -= month <= 2; + const std::int64_t era = (year >= 0 ? year : year - 399) / 400; + const auto yearOfEra = static_cast(year - era * 400); + const unsigned dayOfYear = (153 * (month > 2 ? month - 3 : month + 9) + 2) / 5 + day - 1; + const unsigned dayOfEra = yearOfEra * 365 + yearOfEra / 4 - yearOfEra / 100 + dayOfYear; + return era * 146097 + static_cast(dayOfEra) - 719468 + UNIX_EPOCH_DAYS; +} + +void civilFromDays(std::int64_t days, std::int64_t& year, unsigned& month, unsigned& day) { + days -= UNIX_EPOCH_DAYS - 719468; + const std::int64_t era = (days >= 0 ? days : days - 146096) / 146097; + const auto dayOfEra = static_cast(days - era * 146097); + const unsigned yearOfEra = (dayOfEra - dayOfEra / 1460 + dayOfEra / 36524 - dayOfEra / 146096) / 365; + const unsigned dayOfYear = dayOfEra - (365 * yearOfEra + yearOfEra / 4 - yearOfEra / 100); + const unsigned shiftedMonth = (5 * dayOfYear + 2) / 153; + day = dayOfYear - (153 * shiftedMonth + 2) / 5 + 1; + month = shiftedMonth < 10 ? shiftedMonth + 3 : shiftedMonth - 9; + year = static_cast(yearOfEra) + era * 400 + (month <= 2); +} + +int validate(const RtcDateTime* time) { + if (!time) return SCE_RTC_ERROR_INVALID_POINTER; + if (time->year < 1 || time->year > 9999) return SCE_RTC_ERROR_INVALID_YEAR; + if (time->month < 1 || time->month > 12) return SCE_RTC_ERROR_INVALID_MONTH; + if (time->day < 1 || time->day > daysInMonth(time->year, time->month)) return SCE_RTC_ERROR_INVALID_DAY; + if (time->hour > 23) return SCE_RTC_ERROR_INVALID_HOUR; + if (time->minute > 59) return SCE_RTC_ERROR_INVALID_MINUTE; + if (time->second > 59) return SCE_RTC_ERROR_INVALID_SECOND; + if (time->microsecond >= TICKS_PER_SECOND) return SCE_RTC_ERROR_INVALID_MICROSECOND; + return 0; +} + +std::uint64_t toTick(const RtcDateTime& time) { + return static_cast(daysFromCivil(time.year, time.month, time.day)) * TICKS_PER_DAY + + time.hour * TICKS_PER_HOUR + time.minute * TICKS_PER_MINUTE + time.second * TICKS_PER_SECOND + time.microsecond; +} + +RtcDateTime fromTick(std::uint64_t tick) { + std::int64_t year = 0; + unsigned month = 0; + unsigned day = 0; + civilFromDays(static_cast(tick / TICKS_PER_DAY), year, month, day); + const std::uint64_t timeOfDay = tick % TICKS_PER_DAY; + return RtcDateTime{ + static_cast(year), static_cast(month), static_cast(day), + static_cast(timeOfDay / TICKS_PER_HOUR), static_cast(timeOfDay % TICKS_PER_HOUR / TICKS_PER_MINUTE), + static_cast(timeOfDay % TICKS_PER_MINUTE / TICKS_PER_SECOND), static_cast(timeOfDay % TICKS_PER_SECOND)}; +} + +std::uint64_t currentTick() { + const auto now = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); + return UNIX_EPOCH_TICK + static_cast(now); +} + +std::int64_t localOffsetSeconds(std::uint64_t utcTick) { + const auto seconds = static_cast((static_cast(utcTick) - static_cast(UNIX_EPOCH_TICK)) / TICKS_PER_SECOND); + std::tm local{}; +#ifdef _WIN32 + if (localtime_s(&local, &seconds) != 0) return 0; +#else + if (!localtime_r(&seconds, &local)) return 0; +#endif + const std::int64_t localSeconds = (daysFromCivil(local.tm_year + 1900, static_cast(local.tm_mon + 1), static_cast(local.tm_mday)) - UNIX_EPOCH_DAYS) * 86400 + + local.tm_hour * 3600 + local.tm_min * 60 + local.tm_sec; + return localSeconds - static_cast(seconds); +} + +int addTicks(RtcTick* dst, const RtcTick* src, std::int64_t count, std::int64_t unit) { + if (!dst || !src) return SCE_RTC_ERROR_INVALID_POINTER; + if (count != 0 && (count > std::numeric_limits::max() / unit || count < std::numeric_limits::min() / unit)) return SCE_RTC_ERROR_INVALID_VALUE; + const std::int64_t delta = count * unit; + if (delta < 0 ? src->tick < static_cast(-(delta + 1)) + 1 : MAX_TICK - src->tick < static_cast(delta)) return SCE_RTC_ERROR_INVALID_VALUE; + dst->tick = src->tick + static_cast(delta); + return 0; +} + +int addMonths(RtcTick* dst, const RtcTick* src, std::int64_t months) { + if (!dst || !src) return SCE_RTC_ERROR_INVALID_POINTER; + RtcDateTime time = fromTick(src->tick); + const std::int64_t monthIndex = time.year * 12 + (time.month - 1) + months; + const std::int64_t year = monthIndex / 12; + if (monthIndex < 12 || year > 9999) return SCE_RTC_ERROR_INVALID_VALUE; + time.year = static_cast(year); + time.month = static_cast(monthIndex % 12 + 1); + const int lastDay = daysInMonth(time.year, time.month); + if (time.day > lastDay) time.day = static_cast(lastDay); + dst->tick = toTick(time); + return 0; +} + +bool parseDigits(const char*& cursor, int count, int& value) { + value = 0; + for (int index = 0; index < count; ++index, ++cursor) { + if (*cursor < '0' || *cursor > '9') return false; + value = value * 10 + (*cursor - '0'); + } + return true; +} + +} + extern "C" { int APS5_VABI sceRtcCheckValid(const RtcDateTime* time) { - (void)time; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceRtcConvertLocalTimeToUtc(const RtcTick* local_time, RtcTick* utc) { - (void)local_time; - (void)utc; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceRtcConvertUtcToLocalTime(const RtcTick* utc, RtcTick* local_time) { - (void)utc; - (void)local_time; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceRtcFormatRFC3339(char* date_time, const RtcTick* utc, int time_zone_minutes) { - (void)date_time; - (void)utc; - (void)time_zone_minutes; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceRtcGetCurrentClock(RtcDateTime* time, int time_zone_minutes) { - (void)time; - (void)time_zone_minutes; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceRtcGetCurrentClockLocalTime(RtcDateTime* time) { - (void)time; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceRtcGetCurrentNetworkTick(RtcTick* tick) { - (void)tick; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceRtcGetCurrentTick(RtcTick* tick) { - (void)tick; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceRtcGetDayOfWeek(int year, int month, int day) { - (void)year; - (void)month; - (void)day; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceRtcGetTick(const RtcDateTime* time, RtcTick* tick) { - (void)time; - (void)tick; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceRtcGetTickResolution(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceRtcGetTime_t(const RtcDateTime* time, int64_t* seconds) { - (void)time; - (void)seconds; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceRtcGetWin32FileTime(const RtcDateTime* time, uint64_t* win32_time) { - (void)time; - (void)win32_time; - NotImplemented_nid_no_patch(__func__); - return 0; + return validate(time); } int APS5_VABI sceRtcIsLeapYear(int year) { - (void)year; - NotImplemented_nid_no_patch(__func__); - return 0; + if (year < 1) return SCE_RTC_ERROR_INVALID_YEAR; + return isLeapYear(year) ? 1 : 0; } -int APS5_VABI sceRtcParseRFC3339(RtcTick* utc, const char* date_time) { - (void)utc; - (void)date_time; - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceRtcGetDaysInMonth(int year, int month) { + if (year < 1) return SCE_RTC_ERROR_INVALID_YEAR; + if (month < 1 || month > 12) return SCE_RTC_ERROR_INVALID_MONTH; + return daysInMonth(year, month); +} + +int APS5_VABI sceRtcGetDayOfWeek(int year, int month, int day) { + if (year < 1 || year > 9999) return SCE_RTC_ERROR_INVALID_YEAR; + if (month < 1 || month > 12) return SCE_RTC_ERROR_INVALID_MONTH; + if (day < 1 || day > daysInMonth(year, month)) return SCE_RTC_ERROR_INVALID_DAY; + return static_cast((daysFromCivil(year, static_cast(month), static_cast(day)) + 1) % 7); +} + +int APS5_VABI sceRtcGetTickResolution(void) { + return static_cast(TICKS_PER_SECOND); +} + +int APS5_VABI sceRtcGetTick(const RtcDateTime* time, RtcTick* tick) { + if (!tick) return SCE_RTC_ERROR_INVALID_POINTER; + if (const int result = validate(time); result != 0) return result; + tick->tick = toTick(*time); + return 0; } int APS5_VABI sceRtcSetTick(RtcDateTime* time, const RtcTick* tick) { - (void)time; - (void)tick; - NotImplemented_nid_no_patch(__func__); - return 0; + if (!time || !tick) return SCE_RTC_ERROR_INVALID_POINTER; + if (tick->tick > MAX_TICK) return SCE_RTC_ERROR_INVALID_VALUE; + *time = fromTick(tick->tick); + return 0; +} + +int APS5_VABI sceRtcGetCurrentTick(RtcTick* tick) { + if (!tick) return SCE_RTC_ERROR_INVALID_POINTER; + tick->tick = currentTick(); + return 0; +} + +int APS5_VABI sceRtcGetCurrentNetworkTick(RtcTick* tick) { + return sceRtcGetCurrentTick(tick); +} + +int APS5_VABI sceRtcGetCurrentClock(RtcDateTime* time, int time_zone_minutes) { + if (!time) return SCE_RTC_ERROR_INVALID_POINTER; + RtcTick tick{currentTick()}; + if (const int result = addTicks(&tick, &tick, time_zone_minutes, TICKS_PER_MINUTE); result != 0) return result; + *time = fromTick(tick.tick); + return 0; +} + +int APS5_VABI sceRtcGetCurrentClockLocalTime(RtcDateTime* time) { + if (!time) return SCE_RTC_ERROR_INVALID_POINTER; + const std::uint64_t utc = currentTick(); + *time = fromTick(utc + static_cast(localOffsetSeconds(utc) * TICKS_PER_SECOND)); + return 0; +} + +int APS5_VABI sceRtcConvertUtcToLocalTime(const RtcTick* utc, RtcTick* local_time) { + if (!utc || !local_time) return SCE_RTC_ERROR_INVALID_POINTER; + return addTicks(local_time, utc, localOffsetSeconds(utc->tick), TICKS_PER_SECOND); +} + +int APS5_VABI sceRtcConvertLocalTimeToUtc(const RtcTick* local_time, RtcTick* utc) { + if (!local_time || !utc) return SCE_RTC_ERROR_INVALID_POINTER; + const std::int64_t offset = localOffsetSeconds(local_time->tick - static_cast(localOffsetSeconds(local_time->tick) * TICKS_PER_SECOND)); + return addTicks(utc, local_time, -offset, TICKS_PER_SECOND); +} + +int APS5_VABI sceRtcGetTime_t(const RtcDateTime* time, int64_t* seconds) { + if (!seconds) return SCE_RTC_ERROR_INVALID_POINTER; + if (const int result = validate(time); result != 0) return result; + const std::uint64_t tick = toTick(*time); + if (tick < UNIX_EPOCH_TICK) return SCE_RTC_ERROR_INVALID_VALUE; + *seconds = static_cast((tick - UNIX_EPOCH_TICK) / TICKS_PER_SECOND); + return 0; } int APS5_VABI sceRtcSetTime_t(RtcDateTime* time, int64_t seconds) { - (void)time; - (void)seconds; - NotImplemented_nid_no_patch(__func__); - return 0; + if (!time) return SCE_RTC_ERROR_INVALID_POINTER; + if (seconds < 0 || static_cast(seconds) > (MAX_TICK - UNIX_EPOCH_TICK) / TICKS_PER_SECOND) return SCE_RTC_ERROR_INVALID_VALUE; + *time = fromTick(UNIX_EPOCH_TICK + static_cast(seconds) * TICKS_PER_SECOND); + return 0; +} + +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; + const std::uint64_t tick = toTick(*time); + if (tick < WIN32_EPOCH_TICK) return SCE_RTC_ERROR_INVALID_VALUE; + *win32_time = (tick - WIN32_EPOCH_TICK) * 10; + return 0; } int APS5_VABI sceRtcSetWin32FileTime(RtcDateTime* time, uint64_t win32_time) { - (void)time; - (void)win32_time; - NotImplemented_nid_no_patch(__func__); - return 0; + if (!time) return SCE_RTC_ERROR_INVALID_POINTER; + if (win32_time / 10 > MAX_TICK - WIN32_EPOCH_TICK) return SCE_RTC_ERROR_INVALID_VALUE; + *time = fromTick(WIN32_EPOCH_TICK + win32_time / 10); + return 0; } -int APS5_VABI sceRtcTickAddDays(RtcTick* dst, const RtcTick* src, int32_t days) { - (void)dst; - (void)src; - (void)days; - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceRtcFormatRFC3339(char* date_time, const RtcTick* utc, int time_zone_minutes) { + if (!date_time || !utc) return SCE_RTC_ERROR_INVALID_POINTER; + RtcTick local{}; + if (const int result = addTicks(&local, utc, time_zone_minutes, TICKS_PER_MINUTE); result != 0) return result; + const RtcDateTime time = fromTick(local.tick); + int written = std::snprintf(date_time, 23, "%04u-%02u-%02uT%02u:%02u:%02u.%02u", time.year, time.month, time.day, time.hour, time.minute, time.second, time.microsecond / 10000); + if (time_zone_minutes == 0) { + std::snprintf(date_time + written, 2, "Z"); + } else { + const int offset = time_zone_minutes < 0 ? -time_zone_minutes : time_zone_minutes; + std::snprintf(date_time + written, 7, "%c%02d:%02d", time_zone_minutes < 0 ? '-' : '+', offset / 60, offset % 60); + } + return 0; } -int APS5_VABI sceRtcTickAddHours(RtcTick* dst, const RtcTick* src, int32_t hours) { - (void)dst; - (void)src; - (void)hours; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceRtcTickAddMicroseconds(RtcTick* dst, const RtcTick* src, int64_t usec) { - (void)dst; - (void)src; - (void)usec; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceRtcTickAddMinutes(RtcTick* dst, const RtcTick* src, int64_t minutes) { - (void)dst; - (void)src; - (void)minutes; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceRtcTickAddSeconds(RtcTick* dst, const RtcTick* src, int64_t seconds) { - (void)dst; - (void)src; - (void)seconds; - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceRtcParseRFC3339(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; + } + 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; + offsetMinutes = sign * (offsetHours * 60 + offsetMinute); + } else { + 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; + 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) { - (void)dst; - (void)src; - (void)ticks; - NotImplemented_nid_no_patch(__func__); - return 0; + return addTicks(dst, src, ticks, 1); +} + +int APS5_VABI sceRtcTickAddMicroseconds(RtcTick* dst, const RtcTick* src, int64_t usec) { + return addTicks(dst, src, usec, 1); +} + +int APS5_VABI sceRtcTickAddSeconds(RtcTick* dst, const RtcTick* src, int64_t seconds) { + return addTicks(dst, src, seconds, TICKS_PER_SECOND); +} + +int APS5_VABI sceRtcTickAddMinutes(RtcTick* dst, const RtcTick* src, int64_t minutes) { + return addTicks(dst, src, minutes, TICKS_PER_MINUTE); +} + +int APS5_VABI sceRtcTickAddHours(RtcTick* dst, const RtcTick* src, int32_t hours) { + return addTicks(dst, src, hours, TICKS_PER_HOUR); +} + +int APS5_VABI sceRtcTickAddDays(RtcTick* dst, const RtcTick* src, int32_t days) { + return addTicks(dst, src, days, TICKS_PER_DAY); } int APS5_VABI sceRtcTickAddWeeks(RtcTick* dst, const RtcTick* src, int32_t weeks) { - (void)dst; - (void)src; - (void)weeks; - NotImplemented_nid_no_patch(__func__); - return 0; + return addTicks(dst, src, weeks, 7 * TICKS_PER_DAY); +} + +int APS5_VABI sceRtcTickAddMonths(RtcTick* dst, const RtcTick* src, int32_t months) { + return addMonths(dst, src, months); +} + +int APS5_VABI sceRtcTickAddYears(RtcTick* dst, const RtcTick* src, int16_t years) { + return addMonths(dst, src, static_cast(years) * 12); } } diff --git a/core/libs/tests/GuestRtc.cpp b/core/libs/tests/GuestRtc.cpp new file mode 100644 index 000000000..aa393d449 --- /dev/null +++ b/core/libs/tests/GuestRtc.cpp @@ -0,0 +1,130 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include + +extern "C" { +int APS5_VABI sceRtcCheckValid(const RtcDateTime*); +int APS5_VABI sceRtcIsLeapYear(int); +int APS5_VABI sceRtcGetDaysInMonth(int, int); +int APS5_VABI sceRtcGetDayOfWeek(int, int, int); +int APS5_VABI sceRtcGetTickResolution(void); +int APS5_VABI sceRtcGetTick(const RtcDateTime*, RtcTick*); +int APS5_VABI sceRtcSetTick(RtcDateTime*, const RtcTick*); +int APS5_VABI sceRtcGetCurrentTick(RtcTick*); +int APS5_VABI sceRtcConvertUtcToLocalTime(const RtcTick*, RtcTick*); +int APS5_VABI sceRtcConvertLocalTimeToUtc(const RtcTick*, RtcTick*); +int APS5_VABI sceRtcGetTime_t(const RtcDateTime*, std::int64_t*); +int APS5_VABI sceRtcSetTime_t(RtcDateTime*, std::int64_t); +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 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); +int APS5_VABI sceRtcTickAddMonths(RtcTick*, const RtcTick*, std::int32_t); +int APS5_VABI sceRtcTickAddYears(RtcTick*, const RtcTick*, std::int16_t); +} + +static void Require(bool value) { if (!value) std::abort(); } + +static bool Equal(const RtcDateTime& left, const RtcDateTime& right) { + return left.year == right.year && left.month == right.month && left.day == right.day && left.hour == right.hour + && left.minute == right.minute && left.second == right.second && left.microsecond == right.microsecond; +} + +int main() { + constexpr int invalidPointer = static_cast(0x80B50002); + constexpr int invalidValue = static_cast(0x80B50003); + constexpr int badParse = static_cast(0x80B50007); + constexpr int invalidYear = static_cast(0x80B50008); + constexpr int invalidMonth = static_cast(0x80B50009); + constexpr int invalidDay = static_cast(0x80B5000A); + constexpr int invalidHour = static_cast(0x80B5000B); + constexpr int invalidMicrosecond = static_cast(0x80B5000E); + constexpr std::uint64_t unixEpochTick = 62135596800000000ull; + constexpr std::uint64_t leapDayTick = 63844806896789000ull; + constexpr std::uint64_t maxTick = 315537897599999999ull; + + Require(sceRtcGetTickResolution() == 1000000); + Require(sceRtcIsLeapYear(2000) == 1 && sceRtcIsLeapYear(1900) == 0 && sceRtcIsLeapYear(2024) == 1); + Require(sceRtcIsLeapYear(0) == invalidYear); + Require(sceRtcGetDaysInMonth(2023, 2) == 28 && sceRtcGetDaysInMonth(2024, 2) == 29 && sceRtcGetDaysInMonth(2024, 4) == 30); + Require(sceRtcGetDaysInMonth(2024, 13) == invalidMonth); + Require(sceRtcGetDayOfWeek(1, 1, 1) == 1 && sceRtcGetDayOfWeek(2026, 9, 26) == 6); + Require(sceRtcGetDayOfWeek(2023, 2, 29) == invalidDay); + + RtcDateTime leapDay{2024, 2, 29, 12, 34, 56, 789000}; + Require(sceRtcCheckValid(&leapDay) == 0); + Require(sceRtcCheckValid(nullptr) == invalidPointer); + RtcDateTime invalid = leapDay; + invalid.year = 2023; + Require(sceRtcCheckValid(&invalid) == invalidDay); + invalid = leapDay; + invalid.hour = 24; + Require(sceRtcCheckValid(&invalid) == invalidHour); + invalid = leapDay; + invalid.microsecond = 1000000; + Require(sceRtcCheckValid(&invalid) == invalidMicrosecond); + + RtcTick tick{}; + Require(sceRtcGetTick(&leapDay, &tick) == 0 && tick.tick == leapDayTick); + RtcDateTime converted{}; + Require(sceRtcSetTick(&converted, &tick) == 0 && Equal(converted, leapDay)); + tick.tick = maxTick; + Require(sceRtcSetTick(&converted, &tick) == 0 && Equal(converted, RtcDateTime{9999, 12, 31, 23, 59, 59, 999999})); + tick.tick = maxTick + 1; + Require(sceRtcSetTick(&converted, &tick) == invalidValue); + + RtcDateTime epoch{1970, 1, 1, 0, 0, 0, 0}; + std::int64_t seconds = -1; + Require(sceRtcGetTime_t(&epoch, &seconds) == 0 && seconds == 0); + 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(sceRtcSetTime_t(&converted, -1) == invalidValue); + std::uint64_t fileTime = 0; + Require(sceRtcGetWin32FileTime(&epoch, &fileTime) == 0 && fileTime == 116444736000000000ull); + Require(sceRtcSetWin32FileTime(&converted, 116444736000000000ull) == 0 && Equal(converted, epoch)); + + char text[32]; + tick.tick = leapDayTick; + Require(sceRtcFormatRFC3339(text, &tick, 0) == 0 && std::strcmp(text, "2024-02-29T12:34:56.78Z") == 0); + Require(sceRtcFormatRFC3339(text, &tick, 90) == 0 && std::strcmp(text, "2024-02-29T14:04:56.78+01:30") == 0); + Require(sceRtcFormatRFC3339(text, &tick, -300) == 0 && std::strcmp(text, "2024-02-29T07:34:56.78-05:00") == 0); + Require(sceRtcParseRFC3339(&tick, "2024-02-29T14:04:56.789+01:30") == 0 && tick.tick == leapDayTick); + Require(sceRtcParseRFC3339(&tick, "2024-02-29t12:34:56.789z") == 0 && tick.tick == leapDayTick); + Require(sceRtcParseRFC3339(&tick, "1970-01-01T00:00:00Z") == 0 && tick.tick == unixEpochTick); + Require(sceRtcParseRFC3339(&tick, "2023-02-29T00:00:00Z") == invalidDay); + Require(sceRtcParseRFC3339(&tick, "2024-02-29T12:34:56") == badParse); + Require(sceRtcParseRFC3339(&tick, "2024-02-29T12:34:56Zjunk") == badParse); + Require(sceRtcParseRFC3339(nullptr, "1970-01-01T00:00:00Z") == invalidPointer); + + RtcTick source{leapDayTick}; + RtcTick result{}; + Require(sceRtcTickAddSeconds(&result, &source, 3600) == 0 && result.tick == leapDayTick + 3600000000ull); + Require(sceRtcTickAddDays(&result, &source, 1) == 0); + Require(sceRtcSetTick(&converted, &result) == 0 && converted.month == 3 && converted.day == 1); + Require(sceRtcTickAddYears(&result, &source, 1) == 0); + Require(sceRtcSetTick(&converted, &result) == 0 && converted.year == 2025 && converted.month == 2 && converted.day == 28); + RtcDateTime endOfJanuary{2024, 1, 31, 8, 0, 0, 0}; + Require(sceRtcGetTick(&endOfJanuary, &source) == 0); + Require(sceRtcTickAddMonths(&result, &source, 1) == 0); + Require(sceRtcSetTick(&converted, &result) == 0 && Equal(converted, RtcDateTime{2024, 2, 29, 8, 0, 0, 0})); + Require(sceRtcTickAddMonths(&result, &source, -12 * 2024) == invalidValue); + source.tick = 5; + Require(sceRtcTickAddTicks(&result, &source, -6) == invalidValue); + Require(sceRtcTickAddTicks(&result, &source, -5) == 0 && result.tick == 0); + source.tick = maxTick; + Require(sceRtcTickAddTicks(&result, &source, 1) == invalidValue); + Require(sceRtcTickAddTicks(nullptr, &source, 1) == invalidPointer); + + RtcTick now{}; + Require(sceRtcGetCurrentTick(&now) == 0 && now.tick > leapDayTick); + RtcTick local{}; + RtcTick back{}; + Require(sceRtcConvertUtcToLocalTime(&now, &local) == 0); + Require(sceRtcConvertLocalTimeToUtc(&local, &back) == 0 && back.tick == now.tick); +} From a4c519b262e8029c62d672ff478fe4f2d77d451f Mon Sep 17 00:00:00 2001 From: owner Date: Sat, 26 Sep 2026 13:01:53 -0600 Subject: [PATCH 57/94] fix(exceptions): interpose libstdc++ ABI on Linux so the project's own unwinder drives it On native Linux these PRX shared libraries and their test executables dynamically link real libstdc++/libgcc, so a plain throw went through the system's __cxa_throw/_Unwind_RaiseException. That still invoked this project's own __gxx_personality_v0_nid_postfix (the CIE's personality pointer is a raw function address, agnostic to which unwinder is walking the stack), but handed it libgcc's incompatible private _Unwind_Context layout, corrupting every field Personality.cpp reads and aborting with no exception message. Fix: OwnPersonality() now recognises the bare __gxx_personality_v0 wrapper on Linux too (previously Windows-only), and Exports.cpp exports the real ABI names (__cxa_throw, __cxa_begin_catch/end_catch, __cxa_rethrow, __cxa_allocate/free_exception, __cxa_call_terminate, __cxa_get_exception_ptr, __cxa_current_exception_type, __cxa_init_primary_exception, the std:: terminate/uncaught_exception(s) mangled names, and the core _Unwind_* entry points) via the existing GCC_GLOBAL_ALIAS assembler-alias mechanism, so ELF symbol interposition makes this project's own implementation win process-wide over libgcc_s.so.1/libstdc++.so.6. __cxa_init_primary_exception is newly implemented; GCC 13's inline std::make_exception_ptr needs it and nothing provided it before. Fixes 7 previously-aborting tests: guest_rendering_wait, guest_buffer_reuse, guest_shader_link, guest_system_exit_parameters, guest_once, guest_memory, guest_search. Co-Authored-By: Claude Sonnet 5 --- core/libs/prx/libc/src/exception/Exports.cpp | 22 +++++++++++++++++--- core/libs/prx/libc/src/exception/Runtime.cpp | 12 +++++++++++ core/libs/prx/libc/src/exception/Unwind.cpp | 4 ++-- 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libc/src/exception/Exports.cpp b/core/libs/prx/libc/src/exception/Exports.cpp index 2c7f8be9d..802fad44a 100644 --- a/core/libs/prx/libc/src/exception/Exports.cpp +++ b/core/libs/prx/libc/src/exception/Exports.cpp @@ -20,9 +20,25 @@ GCC_GLOBAL_ALIAS(__cxa_end_catch_nid_no_patch_cut, __cxa_end_catch_nid_postfix); GCC_GLOBAL_ALIAS(__cxa_rethrow_nid_no_patch_cut, __cxa_rethrow_nid_postfix); GCC_GLOBAL_ALIAS(__gxx_personality_v0_nid_no_patch_cut, __gxx_personality_v0_nid_postfix); GCC_GLOBAL_ALIAS(_Unwind_Resume_nid_no_patch_cut, _Unwind_Resume_nid_postfix); -GCC_LOCAL_ALIAS(__cxa_free_exception, __cxa_free_exception_nid_postfix); -GCC_LOCAL_ALIAS(_ZSt9terminatev, _ZSt9terminatev_nid_postfix); -GCC_LOCAL_ALIAS(__cxa_call_terminate, __cxa_call_terminate_nid_postfix); +GCC_GLOBAL_ALIAS(__cxa_free_exception, __cxa_free_exception_nid_postfix); +GCC_GLOBAL_ALIAS(_ZSt9terminatev, _ZSt9terminatev_nid_postfix); +GCC_GLOBAL_ALIAS(__cxa_call_terminate, __cxa_call_terminate_nid_postfix); +GCC_GLOBAL_ALIAS(__cxa_allocate_exception, __cxa_allocate_exception_nid_postfix); +GCC_GLOBAL_ALIAS(__cxa_throw, __cxa_throw_nid_postfix); +GCC_GLOBAL_ALIAS(__cxa_init_primary_exception, __cxa_init_primary_exception_nid_postfix); +GCC_GLOBAL_ALIAS(__cxa_begin_catch, __cxa_begin_catch_nid_postfix); +GCC_GLOBAL_ALIAS(__cxa_end_catch, __cxa_end_catch_nid_postfix); +GCC_GLOBAL_ALIAS(__cxa_rethrow, __cxa_rethrow_nid_postfix); +GCC_GLOBAL_ALIAS(__cxa_get_exception_ptr, __cxa_get_exception_ptr_nid_postfix); +GCC_GLOBAL_ALIAS(__cxa_current_exception_type, __cxa_current_exception_type_nid_postfix); +GCC_GLOBAL_ALIAS(_ZSt13set_terminatePFvvE, _ZSt13set_terminatePFvvE_nid_postfix); +GCC_GLOBAL_ALIAS(_ZSt13get_terminatev, _ZSt13get_terminatev_nid_postfix); +GCC_GLOBAL_ALIAS(_ZSt18uncaught_exceptionv, __cxa_uncaught_exception_nid_postfix); +GCC_GLOBAL_ALIAS(_ZSt19uncaught_exceptionsv, __cxa_uncaught_exceptions_nid_postfix); +GCC_GLOBAL_ALIAS(_Unwind_Resume, _Unwind_Resume_nid_postfix); +GCC_GLOBAL_ALIAS(_Unwind_RaiseException, _Unwind_RaiseException_nid_postfix); +GCC_GLOBAL_ALIAS(_Unwind_Resume_or_Rethrow, _Unwind_Resume_or_Rethrow_nid_postfix); +GCC_GLOBAL_ALIAS(_Unwind_DeleteException, _Unwind_DeleteException_nid_postfix); #ifdef _WIN32 extern "C" { diff --git a/core/libs/prx/libc/src/exception/Runtime.cpp b/core/libs/prx/libc/src/exception/Runtime.cpp index 6e1c52cff..90bc5c023 100644 --- a/core/libs/prx/libc/src/exception/Runtime.cpp +++ b/core/libs/prx/libc/src/exception/Runtime.cpp @@ -221,6 +221,18 @@ void APS5_VABI __cxa_free_exception_nid_postfix(void* object) { InvokeTerminate(header->terminate); } +LibcException::Header* __cxa_init_primary_exception_nid_postfix(void* object, std::type_info* type, void (*destructor)(void*)) { + using namespace LibcException; + auto* header = FromObject(object); + header->type = type; + header->destructor = destructor; + header->terminate = terminateHandler.load(std::memory_order_acquire); + header->adjusted = object; + header->unwind.exception_class = PrimaryClass; + header->unwind.exception_cleanup = Cleanup; + return header; +} + void* APS5_VABI __cxa_begin_catch_nid_postfix(void* exception) { using namespace LibcException; auto* unwind = static_cast<_Unwind_Exception*>(exception); diff --git a/core/libs/prx/libc/src/exception/Unwind.cpp b/core/libs/prx/libc/src/exception/Unwind.cpp index 371803ac8..6accabc61 100644 --- a/core/libs/prx/libc/src/exception/Unwind.cpp +++ b/core/libs/prx/libc/src/exception/Unwind.cpp @@ -8,7 +8,7 @@ #if defined(__linux__) || defined(_WIN32) -#ifdef _WIN32 +#if defined(_WIN32) || defined(__linux__) extern "C" _Unwind_Reason_Code __gxx_personality_v0(int, _Unwind_Action, std::uint64_t, _Unwind_Exception*, _Unwind_Context*); #endif @@ -23,7 +23,7 @@ bool OwnPersonality(Word personality) { } #endif if (personality == reinterpret_cast(__gxx_personality_v0_nid_postfix)) return true; -#ifdef _WIN32 +#if defined(_WIN32) || defined(__linux__) if (personality == reinterpret_cast(__gxx_personality_v0)) return true; #endif return false; From d01be2b0e9e48a906f3cfa6feecccb1b0cf82300 Mon Sep 17 00:00:00 2001 From: owner Date: Sat, 26 Sep 2026 13:06:08 -0600 Subject: [PATCH 58/94] fix(exceptions): alias uncaught_exception(s) to their purpose-built wrappers Exports.cpp aliased the real std::uncaught_exception(s) mangled names directly to __cxa_uncaught_exception(s)_nid_postfix instead of the existing _ZSt18uncaught_exceptionv_nid_postfix/_ZSt19uncaught_exceptionsv_nid_postfix wrappers, leaving the latter as dead code and breaking the file's own bare-name -> matching-nid_postfix alias pattern (caught by fleet QA review, no functional impact since it's a raw symbol alias and both sides return in the same register). Co-Authored-By: Claude Sonnet 5 --- core/libs/prx/libc/src/exception/Exports.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libc/src/exception/Exports.cpp b/core/libs/prx/libc/src/exception/Exports.cpp index 802fad44a..462c2c479 100644 --- a/core/libs/prx/libc/src/exception/Exports.cpp +++ b/core/libs/prx/libc/src/exception/Exports.cpp @@ -33,8 +33,8 @@ GCC_GLOBAL_ALIAS(__cxa_get_exception_ptr, __cxa_get_exception_ptr_nid_postfix); GCC_GLOBAL_ALIAS(__cxa_current_exception_type, __cxa_current_exception_type_nid_postfix); GCC_GLOBAL_ALIAS(_ZSt13set_terminatePFvvE, _ZSt13set_terminatePFvvE_nid_postfix); GCC_GLOBAL_ALIAS(_ZSt13get_terminatev, _ZSt13get_terminatev_nid_postfix); -GCC_GLOBAL_ALIAS(_ZSt18uncaught_exceptionv, __cxa_uncaught_exception_nid_postfix); -GCC_GLOBAL_ALIAS(_ZSt19uncaught_exceptionsv, __cxa_uncaught_exceptions_nid_postfix); +GCC_GLOBAL_ALIAS(_ZSt18uncaught_exceptionv, _ZSt18uncaught_exceptionv_nid_postfix); +GCC_GLOBAL_ALIAS(_ZSt19uncaught_exceptionsv, _ZSt19uncaught_exceptionsv_nid_postfix); GCC_GLOBAL_ALIAS(_Unwind_Resume, _Unwind_Resume_nid_postfix); GCC_GLOBAL_ALIAS(_Unwind_RaiseException, _Unwind_RaiseException_nid_postfix); GCC_GLOBAL_ALIAS(_Unwind_Resume_or_Rethrow, _Unwind_Resume_or_Rethrow_nid_postfix); From 63209b584fc6a15b91cb25c1b3f36ec4849ccae9 Mon Sep 17 00:00:00 2001 From: owner Date: Sat, 26 Sep 2026 13:01:53 -0600 Subject: [PATCH 59/94] fix: correct sscanf %x signedness and avoid a substr copy libSceSysmodule/Export.cpp parsed /proc/self/maps device major/minor numbers into signed int with a %x format, which is undefined behaviour per the C standard even though it happened to work on this target; switched to unsigned int to match the format specifier. NidPatcherUtils.hpp's StripNidPostfix built a truncated copy with substr(0, n) where the prefix is already the string itself; resize(n) does the same truncation without the extra allocation and copy. Co-Authored-By: Claude Sonnet 5 --- core/libs/nid/include/nid/NidPatcherUtils.hpp | 4 ++-- core/libs/prx/libSceSysmodule/Export.cpp | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core/libs/nid/include/nid/NidPatcherUtils.hpp b/core/libs/nid/include/nid/NidPatcherUtils.hpp index 886296fef..f46775612 100644 --- a/core/libs/nid/include/nid/NidPatcherUtils.hpp +++ b/core/libs/nid/include/nid/NidPatcherUtils.hpp @@ -44,7 +44,7 @@ inline std::string StripNidPostfix(const std::string& name) { std::string result = name; if (result.size() >= kNidPostfixLen && result.compare(result.size() - kNidPostfixLen, kNidPostfixLen, kNidPostfix) == 0) { - result = result.substr(0u, result.size() - kNidPostfixLen); + result.resize(result.size() - kNidPostfixLen); } const auto disambigPos = result.rfind(kNidDisambigMarker); if (disambigPos != std::string::npos) { @@ -54,7 +54,7 @@ inline std::string StripNidPostfix(const std::string& name) { [](unsigned char c) { return std::isdigit(c) != 0; } ); if (allDigits && suffixStart < result.size()) - result = result.substr(0u, disambigPos); + result.resize(disambigPos); } return result; } diff --git a/core/libs/prx/libSceSysmodule/Export.cpp b/core/libs/prx/libSceSysmodule/Export.cpp index 9bb667574..b0457018b 100644 --- a/core/libs/prx/libSceSysmodule/Export.cpp +++ b/core/libs/prx/libSceSysmodule/Export.cpp @@ -55,8 +55,8 @@ bool fillModuleInfoForUnwind(std::uint64_t addr, ModuleInfoForUnwind* info) { std::uint64_t end = 0; char perms[8] = {}; std::uint64_t offset = 0; - int devMajor = 0; - int devMinor = 0; + unsigned int devMajor = 0; + unsigned int devMinor = 0; std::uint64_t inode = 0; char path[4096] = {}; int parsed = std::sscanf( From 156d4c5f4684a4adc338c86cfbeafd500df17108 Mon Sep 17 00:00:00 2001 From: owner Date: Sat, 26 Sep 2026 13:20:15 -0600 Subject: [PATCH 60/94] docs: correct SPIR-V validation claim and a stale line citation README stated SPIR-V is "successfully produces validated via Spirv-Tools" unconditionally, but ANYPS5_ENABLE_SPIRV_TOOLS defaults OFF and the only validation call is gated behind it, so this doesn't happen in a default build. Caveated the README line and added the gap to TechnicalDebt.md's silent-stubs list. Also fixed a stale line-number citation there (a prior refactor moved the SpirvTargetOptions assignment block from row 219 to 212). Co-Authored-By: Claude Sonnet 5 --- README.md | 2 +- docs/TechnicalDebt.md | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 41af87e63..2f746fbfd 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Releases will be published after the first full successful launch of at least on Execution reaches `_start`, [stack unwinding](core/libs/prx/libc/src/exception/Unwind.cpp) and exception handling tables are built, reaches main. Unsupported or unexpected states strictly throw `std::runtime_error`. `what()` is printed to stderr and the process terminates. -The [shader recompiler](core/shader/recompiler/Recompiler.cpp) successfully produces validated via [Spirv-Tools](3rdparty/SPIRV-Tools) SPIR-V. +The [shader recompiler](core/shader/recompiler/Recompiler.cpp) successfully produces SPIR-V, validated via [Spirv-Tools](3rdparty/SPIRV-Tools) when built with `ANYPS5_ENABLE_SPIRV_TOOLS`. The real game reaches the logo, main menu, and [gameplay](https://gist.github.com/user-attachments/assets/81d28e9b-c237-4545-b2ca-720071129816) with audio. diff --git a/docs/TechnicalDebt.md b/docs/TechnicalDebt.md index 8d46709fd..28f74897d 100644 --- a/docs/TechnicalDebt.md +++ b/docs/TechnicalDebt.md @@ -13,7 +13,8 @@ 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) -- The shader recompiler [skips baryctric coordinates](../core/shader/recompiler/Recompiler.cpp) (is not even passed to SpirvTargetOptions at row 219). +- The shader recompiler [skips baryctric coordinates](../core/shader/recompiler/Recompiler.cpp) (is not even passed to SpirvTargetOptions at row 212). +- [SPIR-V validation](../core/shader/recompiler/Recompiler.cpp) via Spirv-Tools is gated behind the `ANYPS5_ENABLE_SPIRV_TOOLS` CMake option (default OFF) and does not run in a default build. ### Unknown function names From d3a1b89be36e89952e64adb93227df6b1698adb6 Mon Sep 17 00:00:00 2001 From: owner Date: Sat, 26 Sep 2026 13:20:15 -0600 Subject: [PATCH 61/94] test: wire up the orphaned exception runtime test ExceptionRuntime.cpp existed complete and working but was never registered in CMakeLists.txt, so it never built or ran. Wired it in following the existing Guest*.cpp test pattern. The other three orphaned test files (PthreadStart.cpp, FileStreams.cpp, Formatting.cpp) were left out: each has genuine bitrot against current code (a moved header, fseek returning errno instead of throwing on an invalid whence, and vsnprintf's expected va_list mutation) that needs a source-level decision, not a CMake wiring fix. Co-Authored-By: Claude Sonnet 5 --- core/libs/CMakeLists.txt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 2ad90fd7e..ac01dac0e 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -346,6 +346,15 @@ if(BUILD_TESTING) set_tests_properties(guest_strings PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_executable(exception_runtime_tests tests/ExceptionRuntime.cpp) + target_include_directories(exception_runtime_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(exception_runtime_tests PRIVATE libc) + configure_windows_unwind(exception_runtime_tests) + add_test(NAME exception_runtime COMMAND exception_runtime_tests) + if(WIN32) + set_tests_properties(exception_runtime PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() add_executable(application_heap_tests EXCLUDE_FROM_ALL tests/ApplicationHeap.cpp prx/libc/src/ApplicationHeap.cpp) From f4ff3be07ff1b700192bff87508e7bb4bf8e5426 Mon Sep 17 00:00:00 2001 From: owner Date: Sat, 26 Sep 2026 13:20:15 -0600 Subject: [PATCH 62/94] test(agc): fix and re-enable 3 of 8 bitrotted AGC driver tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These EXCLUDE_FROM_ALL targets weren't registered with add_test, so they silently bitrotted with no CI/ctest visibility: - agc_driver_graphics_tests: fixed a link failure (Draw.cpp, Pipeline.cpp and GraphicsPipelineCache.cpp were never added to the target's sources when the test started calling into them). Still fails at runtime against several stale assertions (a removed "not implemented" error message, a sampler fixture value that doesn't fit its bitfield) accumulated over many commits of not being built; left EXCLUDE_FROM_ALL pending an assertion-maintenance pass. - agc_driver_recompiler_tests: root-caused (missing COMPUTE_NUM_THREAD_X register setup, plus a pass condition checking for a stub error message that no longer exists now that the real recompiler is implemented) but left unfixed and EXCLUDE_FROM_ALL — restoring its original intent needs a shader deliberately built to fail recompilation, a design decision. agc_driver_tests, agc_shader_memory_tests, agc_driver_flip_tests and agc_driver_pm4_tests already passed; registered them with add_test too so none of the five can bitrot silently again. agc_driver_bda_device_tests: dropped from this commit's scope. Upstream PR #20 already proposes the same one-line BdaContracts.cpp fix (matching the runtime message's "signature" text), so BdaContracts.cpp is left untouched here to avoid duplicating that fix. The test target stays defined (EXCLUDE_FROM_ALL, still buildable by name) but is no longer registered with add_test until #20 or an equivalent fix lands. Co-Authored-By: Claude Sonnet 5 --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 8cb5e76be..8f7660eec 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -195,6 +195,9 @@ add_executable(agc_driver_graphics_tests EXCLUDE_FROM_ALL tests/GuestTextureResource.cpp tests/GuestSamplerResource.cpp tests/TextureDetiler.cpp + Graphics/src/Draw.cpp + Graphics/src/Pipeline.cpp + Graphics/src/GraphicsPipelineCache.cpp Graphics/src/State.cpp Graphics/src/ColorTargetLayout.cpp Graphics/src/ColorTargetTransfer.cpp @@ -228,6 +231,7 @@ target_include_directories(agc_driver_graphics_tests PRIVATE ) target_link_libraries(agc_driver_graphics_tests PRIVATE + shader_recompiler_agc_driver libc ) @@ -253,6 +257,18 @@ target_link_libraries(agc_driver_bda_device_tests PRIVATE SDL2-static libc shade set_target_properties(agc_driver_bda_device_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_driver_bda_device_tests) +if(BUILD_TESTING) + foreach(agcTest IN ITEMS agc_driver_tests agc_shader_memory_tests agc_driver_flip_tests agc_driver_pm4_tests) + string(REGEX REPLACE "_tests$" "" agcTestName "${agcTest}") + set_target_properties(${agcTest} PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME ${agcTestName} COMMAND ${agcTest}) + if(WIN32) + set_tests_properties(${agcTestName} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() +endif() + option(AGC_BUILD_VISUAL_TEST "Enable the standalone AGC SPIR-V visual test" OFF) if(AGC_BUILD_VISUAL_TEST) From 6e810feb506442fb00f3c45e846c709de349d4b8 Mon Sep 17 00:00:00 2001 From: owner Date: Sat, 26 Sep 2026 23:30:21 -0600 Subject: [PATCH 63/94] docs: drop SPIR-V gate note from TechnicalDebt.md per maintainer feedback Maintainer noted the opt-in-via-documented-flag SPIR-V validation gap isn't the silent-stub category this doc tracks; the README correction stands on its own. Co-Authored-By: Claude Sonnet 5 --- docs/TechnicalDebt.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/TechnicalDebt.md b/docs/TechnicalDebt.md index 28f74897d..922c79b78 100644 --- a/docs/TechnicalDebt.md +++ b/docs/TechnicalDebt.md @@ -14,7 +14,6 @@ Throughout the project, every function at every stage either **does exactly what - [libSceSaveDataDialog.native](../core/libs/prx/libSceSaveDataDialog.native/Export.cpp) - [libSceCommonDialog](../core/libs/prx/libSceCommonDialog/Export.cpp) - The shader recompiler [skips baryctric coordinates](../core/shader/recompiler/Recompiler.cpp) (is not even passed to SpirvTargetOptions at row 212). -- [SPIR-V validation](../core/shader/recompiler/Recompiler.cpp) via Spirv-Tools is gated behind the `ANYPS5_ENABLE_SPIRV_TOOLS` CMake option (default OFF) and does not run in a default build. ### Unknown function names From 1379ec001b63e92d34741fc19317f6699bc8b806 Mon Sep 17 00:00:00 2001 From: boykopovar Date: Sat, 26 Sep 2026 15:10:21 +0300 Subject: [PATCH 64/94] feat(relinker): add native guest module loading with export priority --- core/relinker/CMakeLists.txt | 5 + .../domain/include/domain/GuestRuntime.hpp | 27 +++ core/relinker/domain/include/domain/Types.hpp | 2 + .../elfpatcher/general/GuestModuleWriter.hpp | 16 ++ .../windows/WindowsEntryStubBuilder.hpp | 2 +- .../windows/WindowsGuestStartup.hpp | 21 ++ .../elfpatcher/windows/WindowsPeFormat.hpp | 2 + .../elfpatcher/windows/WindowsTlsBuilder.hpp | 2 +- .../src/linux/LinuxGuestModuleWriter.cpp | 131 ++++++++++++ .../src/windows/WindowsEntryStubBuilder.cpp | 34 ++- .../src/windows/WindowsGuestModuleWriter.cpp | 160 +++++++++++++++ .../src/windows/WindowsGuestStartup.cpp | 43 ++++ .../src/windows/WindowsPePatcher.cpp | 11 +- .../src/windows/WindowsTlsBuilder.cpp | 3 +- core/relinker/main.cpp | 11 +- .../include/relinker/guest/GuestImage.hpp | 49 +++++ .../relinker/src/guest/GuestImageReader.cpp | 193 ++++++++++++++++++ .../relinker/src/guest/GuestModuleBuilder.cpp | 156 ++++++++++++++ 18 files changed, 856 insertions(+), 12 deletions(-) create mode 100644 core/relinker/domain/include/domain/GuestRuntime.hpp create mode 100644 core/relinker/elfpatcher/include/elfpatcher/general/GuestModuleWriter.hpp create mode 100644 core/relinker/elfpatcher/include/elfpatcher/windows/WindowsGuestStartup.hpp create mode 100644 core/relinker/elfpatcher/src/linux/LinuxGuestModuleWriter.cpp create mode 100644 core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp create mode 100644 core/relinker/elfpatcher/src/windows/WindowsGuestStartup.cpp create mode 100644 core/relinker/relinker/include/relinker/guest/GuestImage.hpp create mode 100644 core/relinker/relinker/src/guest/GuestImageReader.cpp create mode 100644 core/relinker/relinker/src/guest/GuestModuleBuilder.cpp diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index a48325819..66f3d02bd 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -3,6 +3,11 @@ add_executable(relinker ${CMAKE_CURRENT_SOURCE_DIR}/cli/src/CliArgs.cpp ${CMAKE_CURRENT_SOURCE_DIR}/cli/src/Autorun.cpp ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/linux/LinuxElfPatcher.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/linux/LinuxGuestModuleWriter.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/windows/WindowsGuestStartup.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/guest/GuestImageReader.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/guest/GuestModuleBuilder.cpp ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/windows/WindowsPePatcher.cpp ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/windows/WindowsPeFormat.cpp ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/windows/WindowsLoadImage.cpp diff --git a/core/relinker/domain/include/domain/GuestRuntime.hpp b/core/relinker/domain/include/domain/GuestRuntime.hpp new file mode 100644 index 000000000..43aae70b0 --- /dev/null +++ b/core/relinker/domain/include/domain/GuestRuntime.hpp @@ -0,0 +1,27 @@ +#ifndef DOMAIN_GUESTRUNTIME_HPP +#define DOMAIN_GUESTRUNTIME_HPP + +#include +#include +#include + +namespace Domain { + +struct GuestImport { + std::string Name; + std::uint32_t TargetRva; + std::uint64_t Addend; + std::uint32_t RelocationType = 1; +}; + +struct GuestRuntime { + std::string Path; + std::vector Imports; + std::uint32_t InitRva = 0; + std::uint32_t FiniRva = 0; + bool UsePlatformTlsResolver = true; +}; + +} + +#endif diff --git a/core/relinker/domain/include/domain/Types.hpp b/core/relinker/domain/include/domain/Types.hpp index f75570c5c..40bd960ce 100644 --- a/core/relinker/domain/include/domain/Types.hpp +++ b/core/relinker/domain/include/domain/Types.hpp @@ -7,6 +7,7 @@ #include #include #include +#include namespace Domain { @@ -92,6 +93,7 @@ struct SysVDynamicSection { std::vector DynStrData; std::vector RelaData; std::vector RelaPltData; + std::vector GuestModules; }; struct CallRegistryEntry { diff --git a/core/relinker/elfpatcher/include/elfpatcher/general/GuestModuleWriter.hpp b/core/relinker/elfpatcher/include/elfpatcher/general/GuestModuleWriter.hpp new file mode 100644 index 000000000..c820c3879 --- /dev/null +++ b/core/relinker/elfpatcher/include/elfpatcher/general/GuestModuleWriter.hpp @@ -0,0 +1,16 @@ +#ifndef ELFPATCHER_GUESTMODULEWRITER_HPP +#define ELFPATCHER_GUESTMODULEWRITER_HPP + +#include + +namespace Elfpatcher { + +class GuestModuleWriter { +public: + std::vector WriteLinux(const Relinker::GuestImage& image, const std::vector& dependencies, const std::string& runPath) const; + std::vector WriteWindows(const Relinker::GuestImage& image, Domain::GuestRuntime& runtime) const; +}; + +} + +#endif diff --git a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsEntryStubBuilder.hpp b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsEntryStubBuilder.hpp index 1ab27beca..a7a1225c1 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsEntryStubBuilder.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsEntryStubBuilder.hpp @@ -19,7 +19,7 @@ struct WindowsEntryStub { class WindowsEntryStubBuilder { public: - WindowsEntryStub Build(std::uint32_t dataRva, std::uint32_t entryRva, const WindowsImports& nativeImports, const std::vector& libraries, const std::vector& imports, const std::string& runPath, bool lazyBinding, bool dependencyDiagnostics) const; + WindowsEntryStub Build(std::uint32_t dataRva, std::uint32_t entryRva, const WindowsImports& nativeImports, const std::vector& libraries, const std::vector& imports, const std::string& runPath, bool lazyBinding, bool dependencyDiagnostics, const std::vector& guestModules = {}) const; }; } diff --git a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsGuestStartup.hpp b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsGuestStartup.hpp new file mode 100644 index 000000000..209c8d00b --- /dev/null +++ b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsGuestStartup.hpp @@ -0,0 +1,21 @@ +#ifndef ELFPATCHER_WINDOWSGUESTSTARTUP_HPP +#define ELFPATCHER_WINDOWSGUESTSTARTUP_HPP + +#include +#include + +namespace Elfpatcher::Windows { + +class WindowsGuestStartup { +public: + void WriteImport(WindowsStubEmitter& code, const PeImport& import, std::uint32_t handles) const; + void Initialize(WindowsStubEmitter& code, const std::vector& modules, std::uint32_t handles) const; + void Finalize(WindowsStubEmitter& code, const std::vector& modules, std::uint32_t handles, std::uint32_t finished) const; + std::uint32_t EmitTlsResolver(WindowsStubEmitter& code) const; +private: + void callLifecycle(WindowsStubEmitter& code, std::uint32_t handle, std::uint32_t rva) const; +}; + +} + +#endif diff --git a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeFormat.hpp b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeFormat.hpp index 9c2d5036d..bc1c0984d 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeFormat.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeFormat.hpp @@ -32,6 +32,8 @@ struct PeImport { std::string Name; std::uint32_t TargetRva; std::uint64_t Addend; + std::int32_t TargetModule = -1; + std::uint32_t RelocationType = 1; }; struct PeRelocations { diff --git a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsTlsBuilder.hpp b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsTlsBuilder.hpp index 297ef5885..e7dc1ed7d 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsTlsBuilder.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsTlsBuilder.hpp @@ -7,7 +7,7 @@ namespace Elfpatcher::Windows { class WindowsTlsBuilder { public: - PeDirectory Build(const std::vector& source, const std::vector& headers, const WindowsLoadImage& image, std::vector& sections, std::vector& relocations, std::uint32_t& nextRva) const; + PeDirectory Build(const std::vector& source, const std::vector& headers, const WindowsLoadImage& image, std::vector& sections, std::vector& relocations, std::uint32_t& nextRva, std::uint32_t* tlsIndexRva = nullptr) const; }; } diff --git a/core/relinker/elfpatcher/src/linux/LinuxGuestModuleWriter.cpp b/core/relinker/elfpatcher/src/linux/LinuxGuestModuleWriter.cpp new file mode 100644 index 000000000..31168ebd6 --- /dev/null +++ b/core/relinker/elfpatcher/src/linux/LinuxGuestModuleWriter.cpp @@ -0,0 +1,131 @@ +#include +#include +#include +#include + +namespace Elfpatcher { + +std::vector GuestModuleWriter::WriteLinux(const Relinker::GuestImage& image, const std::vector& dependencies, const std::string& runPath) const { + auto bytes = image.Bytes; + std::vector headers; + std::uint64_t end = 0; + for (auto header : image.Headers) { + if (header.Type != 1 && header.Type != 7 && header.Type != 0x6474e550 && header.Type != 0x6474e551) continue; + if (header.Type == 1) { + header.Flags |= 4; + end = std::max(end, header.MappedAddress + header.MemorySize); + } + headers.push_back(header); + } + if (end > std::numeric_limits::max() - 0x4000) throw Domain::RelinkerException("Guest virtual address overflow"); + Io::AlignBuffer(bytes, 0x4000); + const auto extraOffset = bytes.size(); + const auto extraAddress = Io::AlignUp64(end, 0x4000); + const auto address = [&] { return extraAddress + bytes.size() - extraOffset; }; + auto strings = image.Dynamic.DynStrData; + auto symbols = image.Dynamic.DynSymData; + const auto addString = [&](const std::string& value) { + if (strings.size() > std::numeric_limits::max()) throw Domain::RelinkerException("Guest string table too large"); + const auto offset = static_cast(strings.size()); + Io::AppendString(strings, value); + return offset; + }; + bool needsTlsResolver = false; + for (std::size_t index = 1; index < image.Symbols.size(); ++index) { + const auto& symbol = image.Symbols[index]; + if (image.UsePlatformTlsResolver && symbol.Section == 0 && symbol.Name == "vNe1w4diLCs") { + Io::WriteU32(symbols, index * 24, addString("__tls_get_addr")); + needsTlsResolver = true; + } + if (symbol.Section == 0 && (symbol.Info >> 4) == 2) symbols[index * 24 + 4] = static_cast(0x10 | (symbol.Info & 15)); + } + std::vector needed; + for (const auto& dependency : dependencies) needed.push_back(addString(dependency)); + if (needsTlsResolver) needed.push_back(addString("ld-linux-x86-64.so.2")); + const auto soname = addString(image.OutputName); + const auto search = addString(runPath); + const auto strAddress = address(); + bytes.insert(bytes.end(), strings.begin(), strings.end()); + Io::AlignBuffer(bytes, 8); + const auto symAddress = address(); + bytes.insert(bytes.end(), symbols.begin(), symbols.end()); + const auto hashAddress = address(); + const auto count = static_cast(image.Symbols.size()); + Io::AppendU32(bytes, 1); + Io::AppendU32(bytes, count); + Io::AppendU32(bytes, count > 1 ? 1 : 0); + for (std::uint32_t index = 0; index < count; ++index) Io::AppendU32(bytes, index != 0 && index + 1 < count ? index + 1 : 0); + Io::AlignBuffer(bytes, 8); + const auto relaAddress = address(); + bytes.insert(bytes.end(), image.Dynamic.RelaData.begin(), image.Dynamic.RelaData.end()); + const auto pltAddress = address(); + bytes.insert(bytes.end(), image.Dynamic.RelaPltData.begin(), image.Dynamic.RelaPltData.end()); + const auto lifecycle = [&](std::uint64_t target) { + if (target == 0) return std::uint64_t{}; + const auto start = address(); + bytes.insert(bytes.end(), {0x31, 0xff, 0x31, 0xf6, 0x31, 0xd2, 0xe9}); + const auto displacement = static_cast(target) - static_cast(address() + 4); + if (displacement < std::numeric_limits::min() || displacement > std::numeric_limits::max()) throw Domain::RelinkerException("Guest initializer exceeds relative branch range"); + Io::AppendU32(bytes, static_cast(displacement)); + return start; + }; + const auto init = lifecycle(image.Init); + const auto fini = lifecycle(image.Fini); + Io::AlignBuffer(bytes, 8); + const auto dynamicOffset = bytes.size(); + const auto dynamicAddress = address(); + const auto tag = [&](std::uint64_t key, std::uint64_t value) { Io::AppendU64(bytes, key); Io::AppendU64(bytes, value); }; + for (const auto offset : needed) tag(1, offset); + tag(14, soname); + tag(29, search); + tag(4, hashAddress); + tag(5, strAddress); + tag(10, strings.size()); + tag(6, symAddress); + tag(11, 24); + if (!image.Dynamic.RelaData.empty()) { + tag(7, relaAddress); + tag(8, image.Dynamic.RelaData.size()); + tag(9, 24); + } + if (!image.Dynamic.RelaPltData.empty()) { + tag(23, pltAddress); + tag(2, image.Dynamic.RelaPltData.size()); + tag(20, 7); + tag(3, image.Got); + } + if (init != 0) tag(12, init); + if (fini != 0) tag(13, fini); + tag(30, 8); + tag(0, 0); + const auto dynamicSize = bytes.size() - dynamicOffset; + const auto phOffset = bytes.size(); + const auto phCount = headers.size() + 2; + if (phCount > std::numeric_limits::max()) throw Domain::RelinkerException("Too many guest program headers"); + const auto extraSize = bytes.size() + phCount * 56 - extraOffset; + headers.push_back({1, 7, extraOffset, extraAddress, extraAddress, extraSize, extraSize, 0x4000}); + headers.push_back({2, 6, dynamicOffset, dynamicAddress, dynamicAddress, dynamicSize, dynamicSize, 8}); + for (const auto& header : headers) { + Io::AppendU32(bytes, header.Type); + Io::AppendU32(bytes, header.Flags); + Io::AppendU64(bytes, header.Offset); + Io::AppendU64(bytes, header.MappedAddress); + Io::AppendU64(bytes, header.PhysicalAddress); + Io::AppendU64(bytes, header.FileSize); + Io::AppendU64(bytes, header.MemorySize); + Io::AppendU64(bytes, header.Alignment); + } + bytes[7] = 0; + bytes[8] = 0; + Io::WriteU16(bytes, 16, 3); + Io::WriteU64(bytes, 24, 0); + Io::WriteU64(bytes, 32, phOffset); + Io::WriteU64(bytes, 40, 0); + Io::WriteU16(bytes, 56, static_cast(phCount)); + Io::WriteU16(bytes, 58, 0); + Io::WriteU16(bytes, 60, 0); + Io::WriteU16(bytes, 62, 0); + return bytes; +} + +} diff --git a/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp index 51f41f827..c0bde9668 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -29,7 +30,7 @@ std::string normalizeRunPath(std::string path) { } -WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, const std::uint32_t entryRva, const WindowsImports& nativeImports, const std::vector& libraries, const std::vector& imports, const std::string& runPath, const bool lazyBinding, const bool dependencyDiagnostics) const { +WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, const std::uint32_t entryRva, const WindowsImports& nativeImports, const std::vector& libraries, const std::vector& imports, const std::string& runPath, const bool lazyBinding, const bool dependencyDiagnostics, const std::vector& guestModules) const { if (!imports.empty() && libraries.empty()) throw Domain::RelinkerException("ELF imports have no DT_NEEDED libraries"); const auto path = normalizeRunPath(runPath); @@ -54,14 +55,17 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con const auto programPath = reserve(PathCapacity); const auto modulePath = reserve(PathCapacity); const auto handles = reserve(libraries.size() * 8); + const auto guestFinished = reserve(4); + const WindowsGuestStartup guestStartup; const auto functionTable = reserve(12 * 32); const auto unwindRva = CheckedRva(dataRva + data.size()); data.insert(data.end(), {1, 10, 6, 0, 10, 0xb2, 6, 0xc0, 4, 0x70, 3, 0x60, 2, 0x50, 1, 0x30}); std::vector libraryPaths; std::vector libraryNames; - for (const auto& library : libraries) { - auto name = path + library; + for (std::size_t index = 0; index < libraries.size(); ++index) { + auto name = index < guestModules.size() ? libraries[index] : path + libraries[index]; + std::replace(name.begin(), name.end(), '/', '\\'); if (name.size() + 1 >= PathCapacity) throw Domain::RelinkerException("Windows library path exceeds the startup buffer: " + name); libraryPaths.push_back(addString(name)); @@ -218,7 +222,7 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con code.Emit({0x49, 0xff, 0xc4}); for (std::size_t index = 0; index < libraries.size(); ++index) { - if (absolutePath) { + if (absolutePath && index >= guestModules.size()) { code.Rip({0x48, 0x8d, 0x0d}, libraryPaths[index]); } else { code.Emit({0x4c, 0x89, 0xe0, 0x48, 0x29, 0xd8, 0x48, 0x05}); @@ -270,7 +274,14 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con std::vector unresolvedBranches; std::vector lazyUnresolvedImports; + std::vector tlsResolverAddresses; for (std::size_t index = 0; index < imports.size(); ++index) { + if (!guestModules.empty() && guestModules.front().UsePlatformTlsResolver && imports[index].Name == "vNe1w4diLCs") { + if (imports[index].Addend != 0) throw Domain::RelinkerException("TLS resolver import has an addend"); + tlsResolverAddresses.push_back(code.Branch({0x48, 0x8d, 0x05})); + guestStartup.WriteImport(code, imports[index], handles); + continue; + } code.Rip({0x48, 0x8d, 0x1d}, handles); code.Rip({0x48, 0x8d, 0x35}, symbolNames[index]); code.Emit({0xbd}); @@ -291,7 +302,7 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con code.U64(imports[index].Addend); code.Emit({0x48, 0x01, 0xd0}); } - code.Rip({0x48, 0x89, 0x05}, imports[index].TargetRva); + guestStartup.WriteImport(code, imports[index], handles); code.PatchBranch(skipGotWrite, code.GetRva()); continue; } @@ -299,20 +310,26 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con writeString(errorRvas.at(3 + index), true); unresolvedBranches.push_back(code.Branch({0xe9})); code.PatchBranch(resolved, code.GetRva()); + if (imports[index].RelocationType == 16) code.Emit({0x48, 0x8b, 0x00}); + if (imports[index].RelocationType == 17) code.Emit({0x48, 0x8b, 0x40, 8}); if (imports[index].Addend != 0) { code.Emit({0x48, 0xba}); code.U64(imports[index].Addend); code.Emit({0x48, 0x01, 0xd0}); } - code.Rip({0x48, 0x89, 0x05}, imports[index].TargetRva); + guestStartup.WriteImport(code, imports[index], handles); } + guestStartup.Initialize(code, guestModules, handles); writeString(enteringElf); code.Emit({0x48, 0xc7, 0x44, 0x24, 0x40, 1, 0, 0, 0}); code.Rip({0x48, 0x8d, 0x05}, programPath); code.Emit({0x48, 0x89, 0x44, 0x24, 0x48, 0x31, 0xc0, 0x48, 0x89, 0x44, 0x24, 0x50, 0x48, 0x89, 0x44, 0x24, 0x58, 0x48, 0x8d, 0x7c, 0x24, 0x40}); const auto exitCallback = code.Branch({0x48, 0x8d, 0x35}); code.Rip({0xe8}, entryRva); + code.Emit({0x89, 0x44, 0x24, 0x58}); + guestStartup.Finalize(code, guestModules, handles, guestFinished); + code.Emit({0x8b, 0x44, 0x24, 0x58}); code.Emit({0x89, 0xc1}); call("ExitProcess"); code.Emit({0x0f, 0x0b}); @@ -332,7 +349,12 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con const auto functionEnd = code.GetRva(); code.PatchBranch(exitCallback, functionEnd); + code.Emit({0x48, 0x83, 0xec, 0x28}); + guestStartup.Finalize(code, guestModules, handles, guestFinished); + code.Emit({0x48, 0x83, 0xc4, 0x28}); code.Emit({0xc3}); + const auto tlsResolver = guestStartup.EmitTlsResolver(code); + for (const auto offset : tlsResolverAddresses) code.PatchBranch(offset, tlsResolver); for (const auto index : lazyUnresolvedImports) { const auto stubRva = code.GetRva(); diff --git a/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp b/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp new file mode 100644 index 000000000..3e7f5be80 --- /dev/null +++ b/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp @@ -0,0 +1,160 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Elfpatcher { + +std::vector GuestModuleWriter::WriteWindows(const Relinker::GuestImage& guest, Domain::GuestRuntime& runtime) const { + using namespace Windows; + WindowsLoadImage image(guest.Bytes, guest.Headers); + std::vector relocations; + std::vector> tlsModules; + std::map targets; + const auto apply = [&](const std::vector& table) { + for (std::size_t offset = 0; offset < table.size(); offset += 24) { + const auto target = Io::ReadU64(table, offset); + const auto info = Io::ReadU64(table, offset + 8); + const auto addend = Io::ReadU64(table, offset + 16); + const auto type = static_cast(info); + const auto symbolIndex = info >> 32; + const auto& symbol = guest.Symbols.at(symbolIndex); + const auto rva = image.GetRva(target, 8); + for (const auto& header : guest.Headers) { + if (header.Type == 7 && target >= header.MappedAddress && target - header.MappedAddress < header.FileSize) throw Domain::RelinkerException("Windows guest TLS template relocations require explicit support", target); + } + const auto next = targets.lower_bound(target); + if ((next != targets.end() && next->first < target + 8) || (next != targets.begin() && std::prev(next)->second > target)) throw Domain::RelinkerException("Overlapping guest relocations", target); + targets.emplace(target, target + 8); + image.RequireWritable(target, 8); + if (type == 8) { + if (symbolIndex != 0) throw Domain::RelinkerException("RELATIVE guest relocation has a symbol", target); + image.WritePointer(target, image.GetRelocatedAddress(addend)); + relocations.push_back(rva); + } else if (type == 16) { + if (addend != 0 || (symbolIndex != 0 && (symbol.Info & 15) != 6)) throw Domain::RelinkerException("Invalid guest TLS module relocation", target); + if (symbolIndex != 0 && symbol.Section == 0) { + image.WritePointer(target, 0); + runtime.Imports.push_back({symbol.Name, rva, 0, 16}); + } else tlsModules.emplace_back(target, rva); + } else if (type == 17) { + if (symbolIndex != 0 && (symbol.Info & 15) != 6) throw Domain::RelinkerException("Invalid guest TLS offset relocation", target); + if (symbolIndex != 0 && symbol.Section == 0) { + image.WritePointer(target, 0); + runtime.Imports.push_back({symbol.Name, rva, addend, 17}); + } else image.WritePointer(target, symbol.Value + addend); + } else if (type == 18) { + const auto tls = std::find_if(guest.Headers.begin(), guest.Headers.end(), [](const auto& header) { return header.Type == 7; }); + if (tls == guest.Headers.end() || (symbolIndex != 0 && (symbol.Section == 0 || (symbol.Info & 15) != 6))) throw Domain::RelinkerException("Unsupported external static TLS relocation", target); + const auto size = Io::AlignUp64(tls->MemorySize, std::max(tls->Alignment, 16)); + image.WritePointer(target, symbol.Value + addend - size); + } else if (type == 1 || type == 6 || type == 7) { + if (symbolIndex == 0 || (type != 1 && addend != 0) || (symbol.Info & 15) == 6) throw Domain::RelinkerException("Invalid guest symbol relocation", target); + if (symbol.Section != 0) { + if (symbol.Section >= 0xff00) throw Domain::RelinkerException("Unsupported special guest symbol section", target); + image.WritePointer(target, image.GetRelocatedAddress(symbol.Value) + addend); + relocations.push_back(rva); + } else { + if (symbol.Name.empty()) throw Domain::RelinkerException("Empty guest import", target); + image.WritePointer(target, 0); + runtime.Imports.push_back({symbol.Name, rva, addend}); + } + } else throw Domain::RelinkerException("Unsupported Windows guest relocation " + std::to_string(type), target); + } + }; + apply(guest.Dynamic.RelaData); + apply(guest.Dynamic.RelaPltData); + auto sections = image.BuildSections(); + auto nextRva = image.GetEndRva(); + std::array directories{}; + std::uint32_t tlsIndex = 0; + directories[9] = WindowsTlsBuilder().Build(guest.Bytes, guest.Headers, image, sections, relocations, nextRva, &tlsIndex); + for (const auto& [target, rva] : tlsModules) { + if (tlsIndex == 0) throw Domain::RelinkerException("Guest TLS relocation has no TLS block", target); + bool written = false; + for (auto& section : sections) { + if (rva < section.Rva || rva - section.Rva > section.Data.size() || section.Data.size() - (rva - section.Rva) < 8) continue; + Io::WriteU64(section.Data, rva - section.Rva, ImageBase + tlsIndex); + written = true; + break; + } + if (!written) throw Domain::RelinkerException("Guest TLS target is unmapped", target); + relocations.push_back(rva); + } + for (const auto& header : guest.Headers) { + if (header.Type != 0x6474e550) continue; + std::vector metadata(4); + Io::WriteU32(metadata, 0, image.GetRva(header.MappedAddress, header.FileSize)); + sections.push_back({".ehmeta", nextRva, SectionRead | 0x40u, std::move(metadata)}); + nextRva = AlignRva(nextRva + sections.back().Data.size()); + } + std::map exports; + PeSection tlsExports{".tlsrefs", nextRva, SectionRead | 0x40u, {}}; + for (const auto& symbol : guest.Symbols) { + if (symbol.Section == 0 || (symbol.Info >> 4) == 0 || symbol.Visibility == 1 || symbol.Visibility == 2) continue; + if (symbol.Section >= 0xff00) throw Domain::RelinkerException("Unsupported guest export section: " + symbol.Name); + std::uint32_t rva = 0; + if ((symbol.Info & 15) == 6) { + if (tlsIndex == 0) throw Domain::RelinkerException("Guest TLS export has no TLS block: " + symbol.Name); + rva = CheckedRva(tlsExports.Rva + tlsExports.Data.size()); + Io::AppendU64(tlsExports.Data, ImageBase + tlsIndex); + Io::AppendU64(tlsExports.Data, symbol.Value); + relocations.push_back(rva); + } else rva = image.GetRva(symbol.Value, std::max(symbol.Size, 1)); + if (!exports.emplace(symbol.Name, rva).second) throw Domain::RelinkerException("Duplicate guest export: " + symbol.Name); + } + if (!tlsExports.Data.empty()) { + nextRva = AlignRva(nextRva + tlsExports.Data.size()); + sections.push_back(std::move(tlsExports)); + } + if (exports.size() > 65535) throw Domain::RelinkerException("Too many guest PE exports"); + PeSection exportSection{".edata", nextRva, SectionRead | 0x40u, std::vector(40)}; + auto& data = exportSection.Data; + const auto count = CheckedRva(exports.size()); + const auto functions = data.size(); + data.resize(data.size() + count * 4); + const auto names = data.size(); + data.resize(data.size() + count * 4); + const auto ordinals = data.size(); + data.resize(data.size() + count * 2); + Io::WriteU32(data, 12, CheckedRva(nextRva + data.size())); + Io::AppendString(data, guest.OutputName); + Io::WriteU32(data, 16, 1); + Io::WriteU32(data, 20, count); + Io::WriteU32(data, 24, count); + Io::WriteU32(data, 28, CheckedRva(nextRva + functions)); + Io::WriteU32(data, 32, CheckedRva(nextRva + names)); + Io::WriteU32(data, 36, CheckedRva(nextRva + ordinals)); + std::size_t index = 0; + for (const auto& [name, rva] : exports) { + Io::WriteU32(data, functions + index * 4, rva); + Io::WriteU32(data, names + index * 4, CheckedRva(nextRva + data.size())); + Io::WriteU16(data, ordinals + index * 2, static_cast(index)); + Io::AppendString(data, name); + ++index; + } + directories[0] = {nextRva, CheckedRva(data.size())}; + nextRva = AlignRva(nextRva + data.size()); + sections.push_back(std::move(exportSection)); + auto relocationData = WindowsRelocationBuilder().BuildBaseRelocations(relocations); + if (!relocationData.empty()) { + directories[5] = {nextRva, CheckedRva(relocationData.size())}; + sections.push_back({".reloc", nextRva, SectionRead | 0x02000040u, std::move(relocationData)}); + nextRva = AlignRva(nextRva + sections.back().Data.size()); + } + const auto entry = nextRva; + sections.push_back({".dllmain", entry, SectionRead | SectionExecute | 0x20u, {0xb8, 1, 0, 0, 0, 0xc3}}); + runtime.InitRva = guest.Init == 0 ? 0 : image.GetRva(guest.Init); + runtime.FiniRva = guest.Fini == 0 ? 0 : image.GetRva(guest.Fini); + auto result = WindowsPeWriter().Write(sections, entry, directories); + const auto characteristics = Io::ReadU16(result, 0x80 + 22); + Io::WriteU16(result, 0x80 + 22, static_cast((characteristics | 0x2000) & ~1u)); + return result; +} + +} diff --git a/core/relinker/elfpatcher/src/windows/WindowsGuestStartup.cpp b/core/relinker/elfpatcher/src/windows/WindowsGuestStartup.cpp new file mode 100644 index 000000000..b5e212bc9 --- /dev/null +++ b/core/relinker/elfpatcher/src/windows/WindowsGuestStartup.cpp @@ -0,0 +1,43 @@ +#include + +namespace Elfpatcher::Windows { + +void WindowsGuestStartup::WriteImport(WindowsStubEmitter& code, const PeImport& import, const std::uint32_t handles) const { + if (import.TargetModule < 0) { + code.Rip({0x48, 0x89, 0x05}, import.TargetRva); + return; + } + code.Rip({0x48, 0x8b, 0x15}, CheckedRva(handles + static_cast(import.TargetModule) * 8)); + code.Emit({0x41, 0xb8}); + code.U32(import.TargetRva); + code.Emit({0x4a, 0x89, 0x04, 0x02}); +} + +void WindowsGuestStartup::callLifecycle(WindowsStubEmitter& code, const std::uint32_t handle, const std::uint32_t rva) const { + if (rva == 0) return; + code.Rip({0x48, 0x8b, 0x05}, handle); + code.Emit({0xb9}); + code.U32(rva); + code.Emit({0x48, 0x01, 0xc8, 0x31, 0xff, 0x31, 0xf6, 0x31, 0xd2, 0xff, 0xd0}); +} + +void WindowsGuestStartup::Initialize(WindowsStubEmitter& code, const std::vector& modules, const std::uint32_t handles) const { + for (std::size_t index = 0; index < modules.size(); ++index) callLifecycle(code, CheckedRva(handles + index * 8), modules[index].InitRva); +} + +void WindowsGuestStartup::Finalize(WindowsStubEmitter& code, const std::vector& modules, const std::uint32_t handles, const std::uint32_t finished) const { + code.Emit({0xb8, 1, 0, 0, 0}); + code.Rip({0x87, 0x05}, finished); + code.Emit({0x85, 0xc0}); + const auto skip = code.Branch({0x0f, 0x85}); + for (std::size_t index = modules.size(); index > 0; --index) callLifecycle(code, CheckedRva(handles + (index - 1) * 8), modules[index - 1].FiniRva); + code.PatchBranch(skip, code.GetRva()); +} + +std::uint32_t WindowsGuestStartup::EmitTlsResolver(WindowsStubEmitter& code) const { + const auto entry = code.GetRva(); + code.Emit({0x48, 0x8b, 0x07, 0x8b, 0x08, 0x65, 0x48, 0x8b, 0x04, 0x25, 0x58, 0, 0, 0, 0x48, 0x8b, 0x04, 0xc8, 0x48, 0x03, 0x47, 8, 0xc3}); + return entry; +} + +} diff --git a/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp b/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp index 0910e7aad..a9e5e2a64 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp @@ -78,10 +78,17 @@ std::vector WindowsPePatcher::Patch(const std::vector guestPaths; + for (std::size_t index = 0; index < dynamicSection.GuestModules.size(); ++index) { + const auto& module = dynamicSection.GuestModules[index]; + guestPaths.push_back(module.Path); + for (const auto& import : module.Imports) relocations.Imports.push_back({import.Name, import.TargetRva, import.Addend, static_cast(index), import.RelocationType}); + } + libraries.insert(libraries.begin(), guestPaths.begin(), guestPaths.end()); if (dependencyDiagnostics) writeDiagnosticsImports(relocations.Imports); - auto entry = WindowsEntryStubBuilder().Build(nextRva, image.GetEntryRva(), nativeImports, libraries, relocations.Imports, runPath, lazyBinding, dependencyDiagnostics); + auto entry = WindowsEntryStubBuilder().Build(nextRva, image.GetEntryRva(), nativeImports, libraries, relocations.Imports, runPath, lazyBinding, dependencyDiagnostics, dynamicSection.GuestModules); directories[3] = entry.ExceptionDirectory; const auto entryRva = entry.Code.Rva; sections.push_back(std::move(nativeImports.Section)); diff --git a/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp index ea58d8138..5721381f8 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp @@ -35,7 +35,7 @@ void patchAccess(std::vector& sections, const TlsAccess& access, cons } -PeDirectory WindowsTlsBuilder::Build(const std::vector& source, const std::vector& headers, const WindowsLoadImage& image, std::vector& sections, std::vector& relocations, std::uint32_t& nextRva) const { +PeDirectory WindowsTlsBuilder::Build(const std::vector& source, const std::vector& headers, const WindowsLoadImage& image, std::vector& sections, std::vector& relocations, std::uint32_t& nextRva, std::uint32_t* tlsIndexRva) const { const Domain::ProgramHeader* tls = nullptr; const Codegen::X64InstructionDecoder decoder; @@ -92,6 +92,7 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co PeSection data{".gtls", nextRva, SectionRead | SectionWrite | 0x40u, std::vector(templateOffset + blockSize + 16)}; const auto indexRva = nextRva + 40; + if (tlsIndexRva != nullptr) *tlsIndexRva = indexRva; const auto callbackTableRva = nextRva + 48; const auto templateRva = nextRva + templateOffset; const auto codeRva = AlignRva(nextRva + data.Data.size()); diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index 90f087e60..32b8c2b32 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -72,6 +73,8 @@ int main(const int argc, char* argv[]) { for (std::size_t index = 0; index < patch.Bytes.size(); ++index) sourceBytes[patch.Offset + index] = patch.Bytes[index]; } + const auto guestArtifacts = Relinker::GuestModuleBuilder().Build(args.inputPath, absPath, result.DynamicSection, args.toWindows, args.toIntel, args.skipSyscallCheck, args.lazyBinding, args.runPath); + if (args.writeRegistry) { const std::filesystem::path outFsPath(absPath); const std::string registryPath = (outFsPath.parent_path() / (outFsPath.stem().string() + ".registry.json")).string(); @@ -95,7 +98,13 @@ int main(const int argc, char* argv[]) { ); } - fileWriter.Write(absPath, patcher->Patch(sourceBytes, result.OriginalHeaders, result.DynamicSection, result.OriginalPltGotVaddr, args.runPath, args.lazyBinding, args.windowsDiagnostics)); + const auto executableBytes = patcher->Patch(sourceBytes, result.OriginalHeaders, result.DynamicSection, result.OriginalPltGotVaddr, args.runPath, args.lazyBinding, args.windowsDiagnostics); + for (const auto& artifact : guestArtifacts) { + std::filesystem::create_directories(artifact.Path.parent_path()); + fileWriter.Write(artifact.Path.string(), artifact.Bytes); + std::cout << "Guest module: " << artifact.Path.string() << '\n'; + } + fileWriter.Write(absPath, executableBytes); std::cout << "External prx references: " << result.RegistryEntries.size() << "\nOutput file: " << absPath << '\n'; if (args.autorun) return Cli::Autorun(absPath, args.toWindows); diff --git a/core/relinker/relinker/include/relinker/guest/GuestImage.hpp b/core/relinker/relinker/include/relinker/guest/GuestImage.hpp new file mode 100644 index 000000000..e001c4357 --- /dev/null +++ b/core/relinker/relinker/include/relinker/guest/GuestImage.hpp @@ -0,0 +1,49 @@ +#ifndef RELINKER_GUESTIMAGE_HPP +#define RELINKER_GUESTIMAGE_HPP + +#include +#include + +namespace Relinker { + +struct GuestSymbol { + std::string Name; + std::uint8_t Info; + std::uint8_t Visibility; + std::uint16_t Section; + std::uint64_t Value; + std::uint64_t Size; +}; + +struct GuestImage { + std::filesystem::path SourcePath; + std::string OutputName; + std::vector Bytes; + std::vector Headers; + std::vector Symbols; + std::vector Dependencies; + Domain::SysVDynamicSection Dynamic; + std::uint64_t Init = 0; + std::uint64_t Fini = 0; + std::uint64_t Got = 0; + bool UsePlatformTlsResolver = true; +}; + +class GuestImageReader { +public: + GuestImage Read(const std::filesystem::path& path, std::vector bytes) const; +}; + +struct GuestArtifact { + std::filesystem::path Path; + std::vector Bytes; +}; + +class GuestModuleBuilder { +public: + std::vector Build(const std::filesystem::path& inputPath, const std::filesystem::path& outputPath, Domain::SysVDynamicSection& dynamic, bool windows, bool toIntel, bool skipSyscallCheck, bool lazyBinding, const std::string& runPath) const; +}; + +} + +#endif diff --git a/core/relinker/relinker/src/guest/GuestImageReader.cpp b/core/relinker/relinker/src/guest/GuestImageReader.cpp new file mode 100644 index 000000000..816196a26 --- /dev/null +++ b/core/relinker/relinker/src/guest/GuestImageReader.cpp @@ -0,0 +1,193 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Relinker { + +GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector bytes) const { + const auto fail = [&](const std::string& message) { throw Domain::RelinkerException(path.string() + ": " + message); }; + const auto range = [&](std::uint64_t offset, std::uint64_t size) { + if (offset > bytes.size() || size > bytes.size() - offset) fail("ELF range exceeds file"); + }; + range(0, 64); + if (Io::ReadU32(bytes, 0) != 0x464c457f || bytes[4] != 2 || bytes[5] != 1 || bytes[6] != 1 || Io::ReadU16(bytes, 18) != 62 || Io::ReadU32(bytes, 20) != 1) fail("Expected little-endian ELF64 x86-64"); + const auto type = Io::ReadU16(bytes, 16); + if (type != 3 && type != 0xfe18) fail("ELF is not a shared module"); + if (Io::ReadU16(bytes, 52) != 64 || Io::ReadU16(bytes, 54) != 56 || Io::ReadU16(bytes, 56) == 0) fail("Invalid ELF program header table"); + range(Io::ReadU64(bytes, 32), static_cast(Io::ReadU16(bytes, 56)) * 56); + GuestImage image; + image.SourcePath = path; + image.OutputName = path.filename().string() + ".guest.prx"; + image.Headers = ElfReader(bytes).ReadProgramHeaders(); + const Domain::ProgramHeader* dynamic = nullptr; + const Domain::ProgramHeader* dynlib = nullptr; + const Domain::ProgramHeader* tls = nullptr; + std::map loads; + for (const auto& header : image.Headers) { + range(header.Offset, header.FileSize); + if (header.Type == 1) { + if (header.FileSize > header.MemorySize || header.MemorySize > std::numeric_limits::max() - header.MappedAddress || (header.Flags & ~7u) != 0) fail("Invalid load segment"); + if (header.Alignment > 1 && (!std::has_single_bit(header.Alignment) || header.Offset % header.Alignment != header.MappedAddress % header.Alignment)) fail("Invalid load segment alignment"); + const auto next = loads.lower_bound(header.MappedAddress); + if ((next != loads.end() && next->first < header.MappedAddress + header.MemorySize) || (next != loads.begin() && std::prev(next)->second > header.MappedAddress)) fail("Overlapping load segments"); + loads.emplace(header.MappedAddress, header.MappedAddress + header.MemorySize); + } + if (header.Type == 2) { + if (dynamic != nullptr) fail("Duplicate dynamic segment"); + dynamic = &header; + } + if (header.Type == 0x61000000) { + if (dynlib != nullptr) fail("Duplicate SCE dynamic data"); + dynlib = &header; + } + if (header.Type == 7) { + if (tls != nullptr || header.FileSize > header.MemorySize || !std::has_single_bit(header.Alignment)) fail("Invalid TLS segment"); + tls = &header; + } + } + if (loads.empty() || dynamic == nullptr || dynamic->FileSize % 16 != 0) fail("Missing or invalid ELF load/dynamic segments"); + std::map tags; + std::vector needed; + bool terminated = false; + for (std::uint64_t offset = dynamic->Offset; offset < dynamic->Offset + dynamic->FileSize; offset += 16) { + const auto tag = Io::ReadU64(bytes, offset); + const auto value = Io::ReadU64(bytes, offset + 8); + if (tag == 0) { terminated = true; break; } + if (tag == 1) needed.push_back(value); + else if (tag < 0x60000000 || tag == 0x6100003f || (tag >= 0x61000027 && tag <= 0x6100003b)) { + if (!tags.emplace(tag, value).second) fail("Duplicate dynamic tag " + std::to_string(tag)); + } + } + if (!terminated) fail("Unterminated dynamic segment"); + for (const auto unsupported : {15u, 16u, 17u, 18u, 19u, 22u, 35u, 36u, 37u}) { + if (tags.contains(unsupported)) fail("Unsupported dynamic tag " + std::to_string(unsupported)); + } + if (tags.contains(30) && (tags.at(30) & ~8ull) != 0) fail("Unsupported guest dynamic flags"); + const auto mapped = [&](std::uint64_t address, std::uint64_t size, std::uint32_t flags) { + for (const auto& header : image.Headers) { + if (header.Type == 1 && (header.Flags & flags) == flags && address >= header.MappedAddress && address - header.MappedAddress <= header.MemorySize && size <= header.MemorySize - (address - header.MappedAddress)) return; + } + fail("Unmapped or inaccessible guest address " + std::to_string(address)); + }; + const auto translate = [&](std::uint64_t address, std::uint64_t size) { + for (const auto& header : image.Headers) { + if (header.Type == 1 && address >= header.MappedAddress && address - header.MappedAddress <= header.FileSize && size <= header.FileSize - (address - header.MappedAddress)) return header.Offset + address - header.MappedAddress; + } + fail("Unmapped ELF address " + std::to_string(address)); + return std::uint64_t{}; + }; + const auto get = [&](std::uint64_t standard, std::uint64_t sce) { + if (tags.contains(standard) == tags.contains(sce)) fail("Missing or ambiguous dynamic tag " + std::to_string(standard)); + return tags.at(tags.contains(standard) ? standard : sce); + }; + const auto table = [&](std::uint64_t standard, std::uint64_t sce, std::uint64_t size) { + const auto value = get(standard, sce); + if (tags.contains(standard)) return translate(value, size); + if (dynlib == nullptr || value > dynlib->FileSize || size > dynlib->FileSize - value) fail("SCE table exceeds dynamic data segment"); + return dynlib->Offset + value; + }; + const auto strSize = get(10, 0x61000037); + const auto strOffset = table(5, 0x61000035, strSize); + const auto string = [&](std::uint64_t offset) { + if (offset >= strSize) fail("String offset out of bounds"); + const auto start = bytes.begin() + static_cast(strOffset + offset); + const auto end = bytes.begin() + static_cast(strOffset + strSize); + const auto zero = std::find(start, end, 0); + if (zero == end) fail("Unterminated ELF string"); + return std::string(start, zero); + }; + if (strSize == 0 || bytes[strOffset] != 0 || get(11, 0x6100003b) != 24) fail("Invalid string or symbol table"); + std::uint64_t symSize = 0; + if (tags.contains(0x6100003f)) symSize = tags.at(0x6100003f); + else if (tags.contains(4)) symSize = static_cast(Io::ReadU32(bytes, translate(tags.at(4), 8) + 4)) * 24; + else fail("Missing dynamic symbol count"); + if (symSize == 0 || symSize % 24 != 0 || symSize / 24 > std::numeric_limits::max()) fail("Invalid dynamic symbol count"); + const auto symOffset = table(6, 0x61000039, symSize); + image.Dynamic.DynSymData.assign(bytes.begin() + symOffset, bytes.begin() + symOffset + symSize); + image.Dynamic.DynStrData.push_back(0); + std::set exports; + for (std::uint64_t offset = 0; offset < symSize; offset += 24) { + auto name = string(Io::ReadU32(bytes, symOffset + offset)); + name = name.substr(0, name.find('#')); + const auto info = bytes[symOffset + offset + 4]; + const auto visibility = bytes[symOffset + offset + 5]; + const auto section = Io::ReadU16(bytes, symOffset + offset + 6); + const auto value = Io::ReadU64(bytes, symOffset + offset + 8); + const auto size = Io::ReadU64(bytes, symOffset + offset + 16); + if (offset == 0 && (info != 0 || visibility != 0 || section != 0 || value != 0 || size != 0 || !name.empty())) fail("Invalid null symbol"); + if (offset == 0) { + image.Symbols.push_back({{}, 0, 0, 0, 0, 0}); + continue; + } + if (offset != 0 && ((info >> 4) > 2 || (visibility & ~3u) != 0 || ((info & 15) != 0 && (info & 15) != 1 && (info & 15) != 2 && (info & 15) != 6))) fail("Unsupported symbol attributes: " + name); + if (section != 0 && (info >> 4) != 0 && visibility != 1 && visibility != 2) { + if (name.empty() || !exports.insert(name).second) fail("Duplicate or empty export after stripping #: " + name); + } + if ((info & 15) == 6 && section != 0 && (tls == nullptr || value > tls->MemorySize || size > tls->MemorySize - value)) fail("TLS symbol exceeds TLS segment: " + name); + if (section != 0 && (info & 15) != 6) { + if (section >= 0xff00) fail("Unsupported special symbol section: " + name); + mapped(value, std::max(size, 1), (info & 15) == 2 ? 1 : 0); + } + if (image.Dynamic.DynStrData.size() > std::numeric_limits::max()) fail("String table too large"); + Io::WriteU32(image.Dynamic.DynSymData, offset, static_cast(image.Dynamic.DynStrData.size())); + Io::AppendString(image.Dynamic.DynStrData, name); + image.Symbols.push_back({name, info, visibility, section, value, size}); + } + std::set dependencies; + for (const auto offset : needed) { + const auto name = string(offset); + if (name.empty() || name.find_first_of("/\\:$") != std::string::npos || !dependencies.insert(name).second) fail("Invalid or duplicate dependency: " + name); + image.Dependencies.push_back(name); + } + std::map relocationTargets; + const auto copyRelocations = [&](std::uint64_t addressTag, std::uint64_t sceAddressTag, std::uint64_t sizeTag, std::uint64_t sceSizeTag, std::vector& output, bool plt) { + if (!tags.contains(addressTag) && !tags.contains(sceAddressTag) && !tags.contains(sizeTag) && !tags.contains(sceSizeTag)) return; + const auto size = get(sizeTag, sceSizeTag); + if (size % 24 != 0) fail("Invalid relocation table size"); + const auto offset = table(addressTag, sceAddressTag, size); + output.assign(bytes.begin() + offset, bytes.begin() + offset + size); + for (std::uint64_t index = 0; index < size; index += 24) { + const auto info = Io::ReadU64(output, index + 8); + const auto relocation = static_cast(info); + if ((info >> 32) >= image.Symbols.size() || (plt && relocation != 7) || (relocation != 1 && relocation != 6 && relocation != 7 && relocation != 8 && relocation != 16 && relocation != 17 && relocation != 18)) fail("Unsupported relocation or symbol index"); + const auto target = Io::ReadU64(output, index); + const auto addend = Io::ReadU64(output, index + 16); + mapped(target, 8, 2); + const auto next = relocationTargets.lower_bound(target); + if ((next != relocationTargets.end() && next->first < target + 8) || (next != relocationTargets.begin() && std::prev(next)->second > target)) fail("Overlapping guest relocations"); + relocationTargets.emplace(target, target + 8); + if (relocation == 8) { + if ((info >> 32) != 0) fail("RELATIVE relocation has a symbol"); + mapped(addend, 0, 0); + } else if (relocation == 1 || relocation == 6 || relocation == 7) { + const auto& symbol = image.Symbols[info >> 32]; + if ((info >> 32) == 0 || (symbol.Info & 15) == 6 || (relocation != 1 && addend != 0) || (symbol.Section == 0 && symbol.Name.empty())) fail("Invalid guest symbol relocation"); + } else { + const auto& symbol = image.Symbols[info >> 32]; + if (((info >> 32) == 0 && tls == nullptr) || ((info >> 32) != 0 && (symbol.Info & 15) != 6) || (relocation == 16 && addend != 0)) fail("Invalid guest TLS relocation"); + } + } + }; + if (tags.contains(7) || tags.contains(0x6100002f)) { + if (get(9, 0x61000033) != 24) fail("Invalid RELA entry size"); + } + copyRelocations(7, 0x6100002f, 8, 0x61000031, image.Dynamic.RelaData, false); + copyRelocations(23, 0x61000029, 2, 0x6100002d, image.Dynamic.RelaPltData, true); + if (!image.Dynamic.RelaPltData.empty() && get(20, 0x6100002b) != 7) fail("Unsupported PLT relocation format"); + if (tags.contains(3) || tags.contains(0x61000027)) image.Got = get(3, 0x61000027); + if (tags.contains(12)) image.Init = tags.at(12); + if (tags.contains(13)) image.Fini = tags.at(13); + if (image.Init != 0) mapped(image.Init, 1, 1); + if (image.Fini != 0) mapped(image.Fini, 1, 1); + if ((tags.contains(27) && tags.at(27) != 0) || (tags.contains(28) && tags.at(28) != 0)) fail("Separate guest INIT_ARRAY/FINI_ARRAY is not supported"); + if (tags.contains(33) && tags.at(33) != 0 && image.Init == 0) fail("Guest PREINIT_ARRAY has no module initializer"); + image.Bytes = std::move(bytes); + return image; +} + +} diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp new file mode 100644 index 000000000..df86edccc --- /dev/null +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -0,0 +1,156 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Relinker { + +std::vector GuestModuleBuilder::Build(const std::filesystem::path& inputPath, const std::filesystem::path& outputPath, Domain::SysVDynamicSection& dynamic, const bool windows, const bool toIntel, const bool skipSyscallCheck, const bool lazyBinding, const std::string& runPath) const { + const auto root = std::filesystem::absolute(inputPath).parent_path(); + const auto singular = root / "sce_module"; + const auto plural = root / "sce_modules"; + const bool hasSingular = std::filesystem::exists(singular); + const bool hasPlural = std::filesystem::exists(plural); + if (hasSingular && hasPlural) throw Domain::RelinkerException("Both sce_module and sce_modules exist beside the input executable"); + if (!hasSingular && !hasPlural) { + std::cout << "WARNING: sce_module not found beside the input executable; it may be unnecessary or missing.\n"; + return {}; + } + const auto directory = hasSingular ? singular : plural; + if (!std::filesystem::is_directory(directory)) throw Domain::RelinkerException("Guest module path is not a directory: " + directory.string()); + std::vector paths; + for (const auto& entry : std::filesystem::directory_iterator(directory)) { + if (!entry.is_regular_file()) continue; + std::ifstream stream(entry.path(), std::ios::binary); + if (!stream) throw Domain::RelinkerException("Cannot read guest candidate: " + entry.path().string()); + char magic[4]{}; + stream.read(magic, 4); + if (stream.bad()) throw Domain::RelinkerException("Cannot read guest candidate magic: " + entry.path().string()); + if (stream.gcount() == 4 && static_cast(magic[0]) == 0x7f && magic[1] == 'E' && magic[2] == 'L' && magic[3] == 'F') paths.push_back(entry.path()); + } + std::sort(paths.begin(), paths.end()); + if (paths.empty()) return {}; + if (lazyBinding) throw Domain::RelinkerException("Guest modules require eager binding; --lazy-binding is incompatible"); + std::vector images; + std::map exports; + std::set outputNames; + Io::FileReader reader; + for (const auto& path : paths) { + auto image = GuestImageReader().Read(path, reader.Read(path.string())); + if (image.OutputName.find_first_of("$\r\n") != std::string::npos) throw Domain::RelinkerException("Unsupported guest filename: " + image.OutputName); + std::string folded = image.OutputName; + if (windows) { + for (auto& value : folded) { + if (static_cast(value) >= 128 || value == ':' || value == '$') throw Domain::RelinkerException("Unsupported Windows guest filename: " + image.OutputName); + if (value >= 'A' && value <= 'Z') value = static_cast(value + ('a' - 'A')); + } + } + if (!outputNames.insert(folded).second) throw Domain::RelinkerException("Conflicting guest output filename: " + image.OutputName); + for (const auto& symbol : image.Symbols) { + if (symbol.Section == 0 || (symbol.Info >> 4) == 0 || symbol.Visibility == 1 || symbol.Visibility == 2) continue; + const auto [existing, inserted] = exports.emplace(symbol.Name, images.size()); + if (!inserted) throw Domain::RelinkerException("Duplicate guest export after stripping #: " + symbol.Name + " in " + images.at(existing->second).SourcePath.string() + " and " + path.string()); + } + std::vector codeHeaders; + for (const auto& header : image.Headers) if (header.Type == 1 && (header.Flags & 1) != 0) codeHeaders.push_back(header); + if (toIntel) image.Bytes = Codegen::MakeAmd64OnlyConverter()->Convert(std::move(image.Bytes), codeHeaders).Bytes; + if (!skipSyscallCheck) { + const auto scanner = MakeSyscallScanner(); + for (const auto& header : codeHeaders) { + const std::vector code(image.Bytes.begin() + header.Offset, image.Bytes.begin() + header.Offset + header.FileSize); + scanner->ScanCodeSectionForSyscalls(code, header.MappedAddress, header.FileSize); + } + } + images.push_back(std::move(image)); + } + std::vector> dependencies(images.size()); + for (auto& image : images) image.UsePlatformTlsResolver = !exports.contains("vNe1w4diLCs"); + for (std::size_t index = 0; index < images.size(); ++index) { + for (const auto& symbol : images[index].Symbols) { + if (symbol.Section != 0 || symbol.Name.empty()) continue; + const auto found = exports.find(symbol.Name); + if (found != exports.end()) { + const auto& provider = images[found->second]; + const auto exported = std::find_if(provider.Symbols.begin(), provider.Symbols.end(), [&](const auto& candidate) { return candidate.Section != 0 && candidate.Name == symbol.Name && (candidate.Info >> 4) != 0 && candidate.Visibility != 1 && candidate.Visibility != 2; }); + if (exported == provider.Symbols.end() || ((symbol.Info & 15) != 0 && (symbol.Info & 15) != (exported->Info & 15))) throw Domain::RelinkerException("Guest import/export type mismatch: " + symbol.Name); + if (found->second != index) dependencies[index].insert(found->second); + } else if (windows && (symbol.Info & 15) == 6) throw Domain::RelinkerException("Windows guest TLS import requires a guest TLS export: " + symbol.Name); + } + } + std::vector order; + std::vector states(images.size()); + const std::function visit = [&](std::size_t index) { + if (states[index] == 1) throw Domain::RelinkerException("Cyclic guest initialization dependency: " + images[index].SourcePath.string()); + if (states[index] == 2) return; + states[index] = 1; + for (const auto dependency : dependencies[index]) visit(dependency); + states[index] = 2; + order.push_back(index); + }; + for (std::size_t index = 0; index < images.size(); ++index) visit(index); + std::vector hostLibraries; + std::set uniqueHosts; + const auto addHost = [&](const std::string& name) { + if (name.empty() || name.find_first_of("/\\:$") != std::string::npos) throw Domain::RelinkerException("Invalid host dependency: " + name); + if (uniqueHosts.insert(name).second) hostLibraries.push_back(name); + }; + if (dynamic.DynamicSegmentData.size() % 16 != 0) throw Domain::RelinkerException("Invalid executable dependency table"); + for (std::size_t offset = 0; offset < dynamic.DynamicSegmentData.size(); offset += 16) { + if (Io::ReadU64(dynamic.DynamicSegmentData, offset) != 1) throw Domain::RelinkerException("Unexpected executable dependency tag"); + const auto nameOffset = Io::ReadU64(dynamic.DynamicSegmentData, offset + 8); + if (nameOffset >= dynamic.DynStrData.size()) throw Domain::RelinkerException("Invalid dependency string offset"); + const auto start = dynamic.DynStrData.begin() + nameOffset; + const auto end = std::find(start, dynamic.DynStrData.end(), 0); + if (end == dynamic.DynStrData.end()) throw Domain::RelinkerException("Unterminated dependency string"); + addHost(std::string(start, end)); + } + for (const auto& image : images) for (const auto& dependency : image.Dependencies) addHost(dependency); + dynamic.DynamicSegmentData.clear(); + const auto addNeeded = [&](const std::string& name) { + Io::AppendU64(dynamic.DynamicSegmentData, 1); + Io::AppendU64(dynamic.DynamicSegmentData, dynamic.DynStrData.size()); + Io::AppendString(dynamic.DynStrData, name); + }; + const auto relativeDirectory = directory.filename().generic_string(); + for (const auto index : order) if (!windows) addNeeded("$ORIGIN/" + relativeDirectory + "/" + images[index].OutputName); + for (const auto& name : hostLibraries) addNeeded(name); + std::string guestRunPath = runPath; + if (!windows) { + if (guestRunPath == "$ORIGIN") guestRunPath = "$ORIGIN/.."; + else if (guestRunPath.starts_with("$ORIGIN/")) guestRunPath.insert(8, "../"); + else if (!std::filesystem::path(guestRunPath).is_absolute()) throw Domain::RelinkerException("Guest Linux run path must be absolute or begin with $ORIGIN"); + } + std::vector artifacts; + const auto destination = std::filesystem::absolute(outputPath).parent_path() / directory.filename(); + if (std::filesystem::exists(destination) && std::filesystem::equivalent(destination, directory)) throw Domain::RelinkerException("Guest output directory must differ from the source module directory"); + for (const auto index : order) { + const auto& image = images[index]; + const auto target = destination / image.OutputName; + if (target.lexically_normal() == std::filesystem::absolute(outputPath).lexically_normal()) throw Domain::RelinkerException("Guest output collides with the executable output"); + for (const auto& source : paths) if (std::filesystem::exists(target) && std::filesystem::equivalent(source, target)) throw Domain::RelinkerException("Guest output would overwrite an input module: " + target.string()); + if (std::filesystem::exists(target) && std::filesystem::equivalent(inputPath, target)) throw Domain::RelinkerException("Guest output would overwrite the input executable"); + Domain::GuestRuntime runtime; + runtime.UsePlatformTlsResolver = image.UsePlatformTlsResolver; + runtime.Path = relativeDirectory + "/" + image.OutputName; + std::vector output; + if (windows) output = Elfpatcher::GuestModuleWriter().WriteWindows(image, runtime); + else { + std::vector needed; + for (const auto dependency : dependencies[index]) needed.push_back("$ORIGIN/" + images[dependency].OutputName); + needed.insert(needed.end(), hostLibraries.begin(), hostLibraries.end()); + output = Elfpatcher::GuestModuleWriter().WriteLinux(image, needed, guestRunPath); + } + dynamic.GuestModules.push_back(std::move(runtime)); + artifacts.push_back({target, std::move(output)}); + } + return artifacts; +} + +} From 0c6765e6a607c9722b49a58d58ddeac9612841b2 Mon Sep 17 00:00:00 2001 From: boykopovar Date: Sat, 26 Sep 2026 15:19:40 +0300 Subject: [PATCH 65/94] fix(relinker): ignore non-loadable SCE segments outside file bounds --- core/relinker/relinker/src/guest/GuestImageReader.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/core/relinker/relinker/src/guest/GuestImageReader.cpp b/core/relinker/relinker/src/guest/GuestImageReader.cpp index 816196a26..5b3a8abc8 100644 --- a/core/relinker/relinker/src/guest/GuestImageReader.cpp +++ b/core/relinker/relinker/src/guest/GuestImageReader.cpp @@ -6,13 +6,18 @@ #include #include #include +#include namespace Relinker { GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector bytes) const { const auto fail = [&](const std::string& message) { throw Domain::RelinkerException(path.string() + ": " + message); }; const auto range = [&](std::uint64_t offset, std::uint64_t size) { - if (offset > bytes.size() || size > bytes.size() - offset) fail("ELF range exceeds file"); + if (offset > bytes.size() || size > bytes.size() - offset) { + std::ostringstream message; + message << "ELF range exceeds file: offset=0x" << std::hex << offset << ", size=0x" << size << ", fileSize=0x" << bytes.size(); + fail(message.str()); + } }; range(0, 64); if (Io::ReadU32(bytes, 0) != 0x464c457f || bytes[4] != 2 || bytes[5] != 1 || bytes[6] != 1 || Io::ReadU16(bytes, 18) != 62 || Io::ReadU32(bytes, 20) != 1) fail("Expected little-endian ELF64 x86-64"); @@ -28,7 +33,10 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector const Domain::ProgramHeader* dynlib = nullptr; const Domain::ProgramHeader* tls = nullptr; std::map loads; + constexpr std::uint32_t sceComment = 0x6fffff00; + constexpr std::uint32_t sceLibVersion = 0x6fffff01; for (const auto& header : image.Headers) { + if (header.Type == sceComment || header.Type == sceLibVersion) continue; range(header.Offset, header.FileSize); if (header.Type == 1) { if (header.FileSize > header.MemorySize || header.MemorySize > std::numeric_limits::max() - header.MappedAddress || (header.Flags & ~7u) != 0) fail("Invalid load segment"); From 6b2db1d8a8342541b0d55901994abc71dd0bdb34 Mon Sep 17 00:00:00 2001 From: boykopovar Date: Sat, 26 Sep 2026 15:33:26 +0300 Subject: [PATCH 66/94] fix(relinker): support windows guest tls relocations and accesses --- core/relinker/CMakeLists.txt | 1 + .../windows/WindowsTlsTemplateBuilder.hpp | 15 +++++++++ .../src/windows/WindowsGuestModuleWriter.cpp | 7 +++- .../src/windows/WindowsTlsBuilder.cpp | 23 ++++++++++--- .../src/windows/WindowsTlsTemplateBuilder.cpp | 32 +++++++++++++++++++ 5 files changed, 72 insertions(+), 6 deletions(-) create mode 100644 core/relinker/elfpatcher/include/elfpatcher/windows/WindowsTlsTemplateBuilder.hpp create mode 100644 core/relinker/elfpatcher/src/windows/WindowsTlsTemplateBuilder.cpp diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 66f3d02bd..95f639935 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -12,6 +12,7 @@ add_executable(relinker ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/windows/WindowsPeFormat.cpp ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/windows/WindowsLoadImage.cpp ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/windows/WindowsTlsBuilder.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/windows/WindowsTlsTemplateBuilder.cpp ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/windows/WindowsRelocationBuilder.cpp ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/windows/WindowsImportBuilder.cpp ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp diff --git a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsTlsTemplateBuilder.hpp b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsTlsTemplateBuilder.hpp new file mode 100644 index 000000000..4441a73c8 --- /dev/null +++ b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsTlsTemplateBuilder.hpp @@ -0,0 +1,15 @@ +#ifndef ELFPATCHER_WINDOWSTLSTEMPLATEBUILDER_HPP +#define ELFPATCHER_WINDOWSTLSTEMPLATEBUILDER_HPP + +#include + +namespace Elfpatcher::Windows { + +class WindowsTlsTemplateBuilder { +public: + std::vector Build(const Domain::ProgramHeader& tls, const WindowsLoadImage& image, const std::vector& sections, std::uint32_t templateRva, std::vector& relocations) const; +}; + +} + +#endif diff --git a/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp b/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp index 3e7f5be80..78b7c1d83 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp @@ -26,7 +26,12 @@ std::vector GuestModuleWriter::WriteWindows(const Relinker::GuestI const auto& symbol = guest.Symbols.at(symbolIndex); const auto rva = image.GetRva(target, 8); for (const auto& header : guest.Headers) { - if (header.Type == 7 && target >= header.MappedAddress && target - header.MappedAddress < header.FileSize) throw Domain::RelinkerException("Windows guest TLS template relocations require explicit support", target); + if (header.Type != 7 || header.FileSize == 0) continue; + const auto templateRva = image.GetRva(header.MappedAddress, header.FileSize); + const auto templateEnd = static_cast(templateRva) + header.FileSize; + if (rva >= templateEnd || static_cast(rva) + 8 <= templateRva) continue; + if (rva < templateRva || templateEnd - rva < 8) throw Domain::RelinkerException("Guest relocation crosses the TLS template boundary", target); + if (type == 16 || (symbolIndex != 0 && symbol.Section == 0)) throw Domain::RelinkerException("Runtime-bound guest TLS template relocation is not supported", target); } const auto next = targets.lower_bound(target); if ((next != targets.end() && next->first < target + 8) || (next != targets.begin() && std::prev(next)->second > target)) throw Domain::RelinkerException("Overlapping guest relocations", target); diff --git a/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp index 5721381f8..2e0d8de4c 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -14,6 +15,8 @@ namespace { struct TlsAccess { std::uint32_t Rva; std::size_t Length; + bool StoreImmediate; + std::uint32_t Immediate; }; void patchAccess(std::vector& sections, const TlsAccess& access, const std::uint32_t target) { @@ -63,9 +66,11 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co if (info.SegmentPrefix != 0) { const auto position = info.OpcodeOffset; - if (info.SegmentPrefix != 0x64 || info.RexPrefix != 0x48 || info.Length - position != 7 || bytes[position] != 0x8b || bytes[position + 1] != 0x04 || bytes[position + 2] != 0x25 || Io::ReadU32(source, header.Offset + offset + position + 3) != 0) + const bool loadPointer = info.Length == 9 && position == 2 && bytes[0] == 0x64 && bytes[1] == 0x48 && bytes[2] == 0x8b && bytes[3] == 0x04 && bytes[4] == 0x25 && Io::ReadU32(source, header.Offset + offset + 5) == 0; + const bool storeImmediate = info.Length == 12 && position == 1 && bytes[0] == 0x64 && bytes[1] == 0xc7 && bytes[2] == 0x04 && bytes[3] == 0x25 && Io::ReadU32(source, header.Offset + offset + 4) == 0x28; + if (!loadPointer && !storeImmediate) throw Domain::RelinkerException("Unsupported Windows guest TLS instruction", header.Offset + offset); - accesses.push_back({rva, info.Length}); + accesses.push_back({rva, info.Length, storeImmediate, storeImmediate ? Io::ReadU32(source, header.Offset + offset + 8) : 0}); } offset += info.Length; } @@ -89,7 +94,8 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co const auto alignment = std::max(tls->Alignment, 16); const auto blockSize = CheckedRva((tls->MemorySize + alignment - 1) & ~(alignment - 1)); const auto templateOffset = CheckedRva((64 + alignment - 1) & ~(alignment - 1)); - PeSection data{".gtls", nextRva, SectionRead | SectionWrite | 0x40u, std::vector(templateOffset + blockSize + 16)}; + constexpr std::uint32_t threadControlBlockSize = 0x30; + PeSection data{".gtls", nextRva, SectionRead | SectionWrite | 0x40u, std::vector(templateOffset + blockSize + threadControlBlockSize)}; const auto indexRva = nextRva + 40; if (tlsIndexRva != nullptr) *tlsIndexRva = indexRva; @@ -121,19 +127,26 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co throw Domain::RelinkerException("Branch enters a guest TLS instruction", *target); patchAccess(sections, access, code.GetRva()); code.Emit({0x48, 0x8d, 0x64, 0x24, 0x80, 0x51}); + if (access.StoreImmediate) code.Emit({0x50}); loadPointer(); + if (access.StoreImmediate) { + code.Emit({0xc7, 0x40, 0x28}); + code.U32(access.Immediate); + code.Emit({0x58}); + } code.Emit({0x59, 0x48, 0x8d, 0xa4, 0x24, 0x80, 0, 0, 0}); code.Rip({0xe9}, CheckedRva(access.Rva + access.Length)); } - std::copy_n(source.begin() + tls->Offset, tls->FileSize, data.Data.begin() + templateOffset); + const auto templateBytes = WindowsTlsTemplateBuilder().Build(*tls, image, sections, templateRva, relocations); + std::copy(templateBytes.begin(), templateBytes.end(), data.Data.begin() + templateOffset); const auto writeAddress = [&](const std::size_t offset, const std::uint32_t rva) { Io::WriteU64(data.Data, offset, ImageBase + rva); relocations.push_back(CheckedRva(data.Rva + offset)); }; writeAddress(0, templateRva); - writeAddress(8, CheckedRva(templateRva + blockSize + 16)); + writeAddress(8, CheckedRva(templateRva + blockSize + threadControlBlockSize)); writeAddress(16, indexRva); writeAddress(24, callbackTableRva); writeAddress(48, codeRva); diff --git a/core/relinker/elfpatcher/src/windows/WindowsTlsTemplateBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsTlsTemplateBuilder.cpp new file mode 100644 index 000000000..4ec5556f0 --- /dev/null +++ b/core/relinker/elfpatcher/src/windows/WindowsTlsTemplateBuilder.cpp @@ -0,0 +1,32 @@ +#include +#include + +namespace Elfpatcher::Windows { + +std::vector WindowsTlsTemplateBuilder::Build(const Domain::ProgramHeader& tls, const WindowsLoadImage& image, const std::vector& sections, const std::uint32_t templateRva, std::vector& relocations) const { + if (tls.FileSize == 0) return {}; + const auto sourceRva = image.GetRva(tls.MappedAddress, tls.FileSize); + const auto sourceEnd = static_cast(sourceRva) + tls.FileSize; + std::vector result; + result.reserve(static_cast(tls.FileSize)); + while (result.size() < tls.FileSize) { + const auto currentRva = static_cast(sourceRva) + result.size(); + const auto section = std::find_if(sections.begin(), sections.end(), [&](const auto& candidate) { + return currentRva >= candidate.Rva && currentRva - candidate.Rva < candidate.Data.size(); + }); + if (section == sections.end()) throw Domain::RelinkerException("TLS template is outside PE section data", currentRva); + const auto offset = static_cast(currentRva - section->Rva); + const auto size = std::min(tls.FileSize - result.size(), section->Data.size() - offset); + result.insert(result.end(), section->Data.begin() + offset, section->Data.begin() + offset + size); + } + const auto originalCount = relocations.size(); + for (std::size_t index = 0; index < originalCount; ++index) { + const auto target = static_cast(relocations[index]); + if (target >= sourceEnd || target + 8 <= sourceRva) continue; + if (target < sourceRva || sourceEnd - target < 8) throw Domain::RelinkerException("PE relocation crosses the TLS template boundary", target); + relocations.push_back(CheckedRva(static_cast(templateRva) + target - sourceRva)); + } + return result; +} + +} From fa10c6d7ebb737dfbf19bdc08d804b3306487629 Mon Sep 17 00:00:00 2001 From: boykopovar Date: Sat, 26 Sep 2026 15:41:22 +0300 Subject: [PATCH 67/94] fix(relinker): scan reachable code for windows guest tls accesses --- core/relinker/CMakeLists.txt | 1 + .../src/windows/WindowsTlsBuilder.cpp | 6 +- .../analysis/CodeInstructionCollector.hpp | 16 ++ .../src/analysis/CodeInstructionCollector.cpp | 180 ++++++++++++++++++ 4 files changed, 201 insertions(+), 2 deletions(-) create mode 100644 core/relinker/relinker/include/relinker/analysis/CodeInstructionCollector.hpp create mode 100644 core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 95f639935..0e95e87f9 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -37,6 +37,7 @@ add_executable(relinker ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/parsing/ElfReader.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/SyscallScanner.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/CallSiteResolver.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/CodeInstructionCollector.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/UnusedNidFilter.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/UnusedNidFilter/StrictReachability.cpp diff --git a/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp index 2e0d8de4c..018059e2f 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -44,6 +45,7 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co std::vector accesses; std::set branchTargets; + const auto instructions = Relinker::CodeInstructionCollector().Collect(source, headers); for (const auto& header : headers) { if (header.Type == 7) { @@ -53,7 +55,8 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co } if (header.Type != 1 || (header.Flags & 1) == 0) continue; - for (std::uint64_t offset = 0; offset < header.FileSize;) { + for (auto instruction = instructions.lower_bound(header.MappedAddress); instruction != instructions.end() && *instruction - header.MappedAddress < header.FileSize; ++instruction) { + const auto offset = *instruction - header.MappedAddress; const auto* bytes = source.data() + header.Offset + offset; const auto info = decoder.DecodeInstruction(bytes, header.FileSize - offset); const auto rva = image.GetRva(header.MappedAddress + offset, info.Length); @@ -72,7 +75,6 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co throw Domain::RelinkerException("Unsupported Windows guest TLS instruction", header.Offset + offset); accesses.push_back({rva, info.Length, storeImmediate, storeImmediate ? Io::ReadU32(source, header.Offset + offset + 8) : 0}); } - offset += info.Length; } } diff --git a/core/relinker/relinker/include/relinker/analysis/CodeInstructionCollector.hpp b/core/relinker/relinker/include/relinker/analysis/CodeInstructionCollector.hpp new file mode 100644 index 000000000..2146ab9cd --- /dev/null +++ b/core/relinker/relinker/include/relinker/analysis/CodeInstructionCollector.hpp @@ -0,0 +1,16 @@ +#ifndef RELINKER_CODEINSTRUCTIONCOLLECTOR_HPP +#define RELINKER_CODEINSTRUCTIONCOLLECTOR_HPP + +#include +#include + +namespace Relinker { + +class CodeInstructionCollector { +public: + std::set Collect(const std::vector& bytes, const std::vector& headers) const; +}; + +} + +#endif diff --git a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp new file mode 100644 index 000000000..061c35378 --- /dev/null +++ b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp @@ -0,0 +1,180 @@ +#include +#include +#include +#include +#include +#include + +namespace Relinker { + +namespace { + +class CodePointers : public UnusedNidFilter::IRelativeRelocationIndex { +public: + std::map Values; + std::optional TargetOfSlot(Domain::VirtualAddress address) const override { + const auto found = Values.find(address); + if (found == Values.end()) return std::nullopt; + return found->second; + } +}; + +} + +std::set CodeInstructionCollector::Collect(const std::vector& bytes, const std::vector& headers) const { + const auto range = [&](std::uint64_t offset, std::uint64_t size) { + if (offset > bytes.size() || size > bytes.size() - offset) throw Domain::RelinkerException("Code analysis: file range exceeds image", offset); + }; + const auto fileOffset = [&](std::uint64_t address, std::uint64_t size) { + for (const auto& header : headers) { + if (header.Type != 1 || address < header.MappedAddress || address - header.MappedAddress > header.FileSize || size > header.FileSize - (address - header.MappedAddress)) continue; + const auto offset = header.Offset + address - header.MappedAddress; + range(offset, size); + return offset; + } + throw Domain::RelinkerException("Code analysis: unmapped address", address); + }; + const auto isCode = [&](std::uint64_t address) { + for (const auto& header : headers) if (header.Type == 1 && (header.Flags & 1) != 0 && address >= header.MappedAddress && address - header.MappedAddress < header.FileSize) return true; + return false; + }; + std::map tags; + std::optional sceBase; + bool hasDynamic = false; + for (const auto& header : headers) { + if (header.Type == 0x61000000) sceBase = header.Offset; + if (header.Type != 2) continue; + if (hasDynamic || header.FileSize % 16 != 0) throw Domain::RelinkerException("Code analysis: invalid dynamic segment"); + hasDynamic = true; + range(header.Offset, header.FileSize); + bool terminated = false; + for (std::uint64_t offset = header.Offset; offset < header.Offset + header.FileSize; offset += 16) { + const auto tag = Io::ReadU64(bytes, offset); + if (tag == 0) { terminated = true; break; } + const auto value = Io::ReadU64(bytes, offset + 8); + if (tag == 1 || (tag >= 0x61000000 && tag != 0x6100003f && (tag < 0x61000027 || tag > 0x6100003b))) continue; + if (!tags.emplace(tag, value).second) throw Domain::RelinkerException("Code analysis: duplicate dynamic tag", offset); + } + if (!terminated) throw Domain::RelinkerException("Code analysis: unterminated dynamic segment"); + } + const auto value = [&](std::uint64_t standard, std::uint64_t sce) -> std::optional { + if (tags.contains(standard) && tags.contains(sce)) throw Domain::RelinkerException("Code analysis: ambiguous dynamic tag"); + if (tags.contains(standard)) return tags.at(standard); + if (tags.contains(sce)) return tags.at(sce); + return std::nullopt; + }; + const auto table = [&](std::uint64_t standard, std::uint64_t sce, std::uint64_t size) { + const auto address = value(standard, sce); + if (!address) throw Domain::RelinkerException("Code analysis: missing table"); + if (tags.contains(standard)) return fileOffset(*address, size); + if (!sceBase || *address > std::numeric_limits::max() - *sceBase) throw Domain::RelinkerException("Code analysis: invalid SCE table"); + const auto offset = *sceBase + *address; + range(offset, size); + return offset; + }; + std::set roots; + const auto addRoot = [&](std::uint64_t address) { if (isCode(address)) roots.insert(address); }; + range(0, 64); + if (const auto entry = Io::ReadU64(bytes, 24); entry != 0) addRoot(entry); + for (const auto tag : {12u, 13u}) if (const auto address = value(tag, 0x60000000 + tag); address && *address != 0) addRoot(*address); + const auto symbolBytes = value(0x6100003f, 0xffffffffffffffffull); + std::uint64_t symbolCount = 0; + if (symbolBytes) { + if (*symbolBytes % 24 != 0) throw Domain::RelinkerException("Code analysis: invalid symbol table size"); + symbolCount = *symbolBytes / 24; + } else if (tags.contains(4)) symbolCount = Io::ReadU32(bytes, fileOffset(tags.at(4), 8) + 4); + std::uint64_t symbols = 0; + if (symbolCount != 0) { + if (value(11, 0x6100003b) != 24) throw Domain::RelinkerException("Code analysis: invalid symbol entry size"); + symbols = table(6, 0x61000039, symbolCount * 24); + for (std::uint64_t index = 0; index < symbolCount; ++index) { + const auto offset = symbols + index * 24; + if ((bytes[offset + 4] & 15) == 2 && Io::ReadU16(bytes, offset + 6) != 0) addRoot(Io::ReadU64(bytes, offset + 8)); + } + } + CodePointers pointers; + std::set importSlots; + const auto readRelocations = [&](std::uint64_t addressTag, std::uint64_t sceAddressTag, std::uint64_t sizeTag, std::uint64_t sceSizeTag) { + const auto size = value(sizeTag, sceSizeTag); + if (!size) return; + if (*size % 24 != 0) throw Domain::RelinkerException("Code analysis: invalid relocation table size"); + if (*size == 0) return; + const auto start = table(addressTag, sceAddressTag, *size); + for (std::uint64_t offset = start; offset < start + *size; offset += 24) { + const auto slot = Io::ReadU64(bytes, offset); + const auto info = Io::ReadU64(bytes, offset + 8); + const auto type = static_cast(info); + const auto index = info >> 32; + const auto addend = Io::ReadU64(bytes, offset + 16); + if (type == 8) { + if (index != 0 || !pointers.Values.emplace(slot, addend).second) throw Domain::RelinkerException("Code analysis: invalid relative relocation", slot); + addRoot(addend); + } else if (type == 1 || type == 6 || type == 7) { + if (index == 0 || index >= symbolCount) throw Domain::RelinkerException("Code analysis: invalid relocation symbol", slot); + const auto symbol = symbols + index * 24; + if (Io::ReadU16(bytes, symbol + 6) == 0) importSlots.insert(slot); + else { + const auto target = Io::ReadU64(bytes, symbol + 8) + addend; + if (!pointers.Values.emplace(slot, target).second) throw Domain::RelinkerException("Code analysis: duplicate pointer relocation", slot); + addRoot(target); + } + } + } + }; + readRelocations(7, 0x6100002f, 8, 0x61000031); + readRelocations(23, 0x61000029, 2, 0x6100002d); + for (const auto& [addressTag, sizeTag] : {std::pair{25u, 27u}, std::pair{26u, 28u}, std::pair{32u, 33u}}) { + const auto address = value(addressTag, 0x60000000 + addressTag); + const auto size = value(sizeTag, 0x60000000 + sizeTag); + if (address.has_value() != size.has_value()) throw Domain::RelinkerException("Code analysis: incomplete initializer array"); + if (!size || *size == 0) continue; + if (*size % 8 != 0) throw Domain::RelinkerException("Code analysis: invalid initializer array size"); + const auto offset = fileOffset(*address, *size); + for (std::uint64_t index = 0; index < *size; index += 8) { + const auto relocated = pointers.TargetOfSlot(*address + index); + const auto target = relocated ? *relocated : Io::ReadU64(bytes, offset + index); + if (target == 0 || target == std::numeric_limits::max()) continue; + if (!isCode(target)) throw Domain::RelinkerException("Code analysis: initializer lies outside code", target); + roots.insert(target); + } + } + for (const auto& function : UnusedNidFilter::ReadExceptionFunctions(bytes, headers, pointers.Values, importSlots)) { + addRoot(function.Begin); + for (const auto target : function.ExtraTargets) addRoot(target); + } + if (roots.empty()) throw Domain::RelinkerException("Code analysis: no code entry points"); + std::set instructions; + const Codegen::X64InstructionDecoder decoder; + std::size_t previousRoots = 0; + do { + previousRoots = roots.size(); + for (const auto& header : headers) { + if (header.Type != 1 || (header.Flags & 1) == 0 || header.FileSize == 0) continue; + range(header.Offset, header.FileSize); + std::vector entries; + for (const auto root : roots) if (root >= header.MappedAddress && root - header.MappedAddress < header.FileSize) entries.push_back(root); + if (entries.empty()) continue; + const std::vector text(bytes.begin() + header.Offset, bytes.begin() + header.Offset + header.FileSize); + const auto graph = UnusedNidFilter::BuildControlFlowGraph(text, header.MappedAddress, entries.front(), entries, pointers); + for (const auto address : graph->ReachableVaddrs()) { + instructions.insert(address); + const auto offset = address - header.MappedAddress; + const auto info = decoder.DecodeInstruction(text.data() + offset, text.size() - offset); + if (info.HasBranchTarget && !info.HasRipRelativeDisp) addRoot(static_cast(static_cast(address + info.Length) + info.BranchDisp)); + } + } + } 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); + 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; + previousEnd = address + decoder.DecodeInstruction(bytes.data() + header.Offset + offset, header.FileSize - offset).Length; + break; + } + } + return instructions; +} + +} From 2cc34985c065f6c43b57edf03e34e6c925a2d670 Mon Sep 17 00:00:00 2001 From: boykopovar Date: Sat, 26 Sep 2026 15:42:55 +0300 Subject: [PATCH 68/94] fix(codegen): decode int imm8 instruction length correctly --- .../relinker/codegen/include/codegen/x86/X64OpcodeConstants.hpp | 2 ++ core/relinker/codegen/src/x86/X64InstructionDecoder.cpp | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/core/relinker/codegen/include/codegen/x86/X64OpcodeConstants.hpp b/core/relinker/codegen/include/codegen/x86/X64OpcodeConstants.hpp index 69bc3598f..237fb8bb7 100644 --- a/core/relinker/codegen/include/codegen/x86/X64OpcodeConstants.hpp +++ b/core/relinker/codegen/include/codegen/x86/X64OpcodeConstants.hpp @@ -5,6 +5,8 @@ namespace Codegen::X64OpcodeConstants { +inline constexpr std::uint8_t OneByteIntImm8 = 0xCD; + inline constexpr std::uint8_t PrefixLock = 0xF0; inline constexpr std::uint8_t PrefixRepne = 0xF2; inline constexpr std::uint8_t PrefixRep = 0xF3; diff --git a/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp b/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp index 85c42cb9c..d574fef6c 100644 --- a/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp +++ b/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp @@ -200,7 +200,7 @@ std::size_t X64InstructionDecoder::Decode(const std::uint8_t* data, std::size_t immediateSize = ImmSize32; } - if (opcode == OneByteJmpRel8 || opcode == OneByteJrcxz || + if (opcode == OneByteJmpRel8 || opcode == OneByteJrcxz || opcode == OneByteIntImm8 || (opcode >= OneByteLoop && opcode <= OneByteLoopMax) || (opcode >= OneByteInOutImm8Min && opcode <= OneByteInOutImm8Max)) { immediateSize = ImmSize8; From 337e763597c32f17a55e6c225ea4d21f2759b4a6 Mon Sep 17 00:00:00 2001 From: boykopovar Date: Sat, 26 Sep 2026 15:46:16 +0300 Subject: [PATCH 69/94] fix(relinker): support operand-size prefixes on fs:[0] access --- .../elfpatcher/src/windows/WindowsTlsBuilder.cpp | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp index 018059e2f..58ee3eb62 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp @@ -69,11 +69,18 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co if (info.SegmentPrefix != 0) { const auto position = info.OpcodeOffset; - const bool loadPointer = info.Length == 9 && position == 2 && bytes[0] == 0x64 && bytes[1] == 0x48 && bytes[2] == 0x8b && bytes[3] == 0x04 && bytes[4] == 0x25 && Io::ReadU32(source, header.Offset + offset + 5) == 0; - const bool storeImmediate = info.Length == 12 && position == 1 && bytes[0] == 0x64 && bytes[1] == 0xc7 && bytes[2] == 0x04 && bytes[3] == 0x25 && Io::ReadU32(source, header.Offset + offset + 4) == 0x28; + bool hasOperandSizePrefix = false; + bool supportedPrefixes = info.SegmentPrefix == 0x64; + for (std::size_t prefix = 0; prefix < position; ++prefix) { + const auto value = bytes[prefix]; + if (value == 0x66) hasOperandSizePrefix = true; + else if (value != 0x64 && !(prefix + 1 == position && value >= 0x40 && value <= 0x4f)) supportedPrefixes = false; + } + const bool loadPointer = supportedPrefixes && info.RexPrefix == 0x48 && info.Length - position == 7 && bytes[position] == 0x8b && bytes[position + 1] == 0x04 && bytes[position + 2] == 0x25 && Io::ReadU32(source, header.Offset + offset + position + 3) == 0; + const bool storeImmediate = supportedPrefixes && !hasOperandSizePrefix && (info.RexPrefix == 0 || info.RexPrefix == 0x40) && info.Length - position == 11 && bytes[position] == 0xc7 && bytes[position + 1] == 0x04 && bytes[position + 2] == 0x25 && Io::ReadU32(source, header.Offset + offset + position + 3) == 0x28; if (!loadPointer && !storeImmediate) throw Domain::RelinkerException("Unsupported Windows guest TLS instruction", header.Offset + offset); - accesses.push_back({rva, info.Length, storeImmediate, storeImmediate ? Io::ReadU32(source, header.Offset + offset + 8) : 0}); + accesses.push_back({rva, info.Length, storeImmediate, storeImmediate ? Io::ReadU32(source, header.Offset + offset + position + 7) : 0}); } } } From 0783e369da957982dc6b33f0eb0beb98971182ca Mon Sep 17 00:00:00 2001 From: boykopovar Date: Sat, 26 Sep 2026 15:57:46 +0300 Subject: [PATCH 70/94] feat(libkernel): add definitions for standard guest libc imports --- core/libs/prx/libkernel/CMakeLists.txt | 1 + .../libkernel/System/src/PosixImportStubs.cpp | 91 +++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 core/libs/prx/libkernel/System/src/PosixImportStubs.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 394bad513..965749d04 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -39,6 +39,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/System/src/Sanitizer.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/Rtld.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/Process.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/System/src/PosixImportStubs.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/SystemConfiguration.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/Signals.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Time/Time.cpp diff --git a/core/libs/prx/libkernel/System/src/PosixImportStubs.cpp b/core/libs/prx/libkernel/System/src/PosixImportStubs.cpp new file mode 100644 index 000000000..bfb5cd145 --- /dev/null +++ b/core/libs/prx/libkernel/System/src/PosixImportStubs.cpp @@ -0,0 +1,91 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" +#include +#include + +struct GuestSignalSet; +struct GuestResourceUsage; + +extern "C" { + +int APS5_VABI _close_nid_postfix(int descriptor) { + (void)descriptor; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +void APS5_VABI _exit_nid_postfix(int status) { + (void)status; + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _nanosleep_nid_postfix(const KernelTimespec* requested, KernelTimespec* remaining) { + (void)requested; + (void)remaining; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _open_nid_postfix(const char* path, int flags, ...) { + (void)path; + (void)flags; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +std::int64_t APS5_VABI _read_nid_postfix(int descriptor, void* buffer, std::size_t count) { + (void)descriptor; + (void)buffer; + (void)count; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _sigprocmask_nid_postfix(int how, const GuestSignalSet* set, GuestSignalSet* previousSet) { + (void)how; + (void)set; + (void)previousSet; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +std::int64_t APS5_VABI _write_nid_postfix(int descriptor, const void* buffer, std::size_t count) { + (void)descriptor; + (void)buffer; + (void)count; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI getrusage_nid_postfix(int who, GuestResourceUsage* usage) { + (void)who; + (void)usage; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_rwlock_rdlock_nid_postfix(PthreadRwlock* rwlock) { + (void)rwlock; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_rwlock_unlock_nid_postfix(PthreadRwlock* rwlock) { + (void)rwlock; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI rmdir_nid_postfix(const char* path) { + (void)path; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI unlink_nid_postfix(const char* path) { + (void)path; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} From 40101471eebaf99e5514f93cd313bd9ad02c7457 Mon Sep 17 00:00:00 2001 From: boykopovar Date: Sat, 26 Sep 2026 16:32:16 +0300 Subject: [PATCH 71/94] feat(libkernel): add new posix, pthread, semaphore, process funcs definitions --- .../prx/libkernel/DirectMemory/Export.cpp | 11 +++++ core/libs/prx/libkernel/File/src/Stdio.cpp | 25 ++++++++++++ core/libs/prx/libkernel/Module/src/Module.cpp | 11 +++++ .../libs/prx/libkernel/Pthread/Posix/Once.cpp | 14 +++++++ .../libs/prx/libkernel/Pthread/src/Thread.cpp | 9 +++++ .../libs/prx/libkernel/System/src/Process.cpp | 2 + core/libs/prx/libkernel/System/src/Rtld.cpp | 40 +++++++++++++++++++ .../libs/prx/libkernel/System/src/Signals.cpp | 11 +++++ 8 files changed, 123 insertions(+) diff --git a/core/libs/prx/libkernel/DirectMemory/Export.cpp b/core/libs/prx/libkernel/DirectMemory/Export.cpp index 0ef1a9b98..986f5616b 100644 --- a/core/libs/prx/libkernel/DirectMemory/Export.cpp +++ b/core/libs/prx/libkernel/DirectMemory/Export.cpp @@ -202,3 +202,14 @@ int APS5_VABI sceKernelBatchMap2(KernelBatchMapEntry* entries, int num_entries, } } + +extern "C" { + +int APS5_VABI sceKernelMlock_nid_postfix(void* address, std::uint64_t length) { + (void)address; + (void)length; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 714948a58..81704d09d 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -187,3 +187,28 @@ int APS5_VABI sceKernelRmdir(const char* path) { } } + +extern "C" { + +int APS5_VABI sceKernelChmod_nid_postfix(const char* path, std::uint16_t mode) { + (void)path; + (void)mode; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelTruncate_nid_postfix(const char* path, std::int64_t length) { + (void)path; + (void)length; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelUtimes_nid_postfix(const char* path, const KernelTimeval* times) { + (void)path; + (void)times; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libkernel/Module/src/Module.cpp b/core/libs/prx/libkernel/Module/src/Module.cpp index c34c4577a..24a9e86a1 100644 --- a/core/libs/prx/libkernel/Module/src/Module.cpp +++ b/core/libs/prx/libkernel/Module/src/Module.cpp @@ -52,3 +52,14 @@ int APS5_VABI sceKernelStopUnloadModule(KernelModule handle, size_t args, const } } + +extern "C" { + +int APS5_VABI __elf_phdr_match_addr_nid_postfix(ModuleInfo* module, std::uint64_t address) { + (void)module; + (void)address; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libkernel/Pthread/Posix/Once.cpp b/core/libs/prx/libkernel/Pthread/Posix/Once.cpp index bd7d8a24f..8702a17a0 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Once.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Once.cpp @@ -1,3 +1,4 @@ +#include "prx/libc/include/General.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include #include @@ -41,3 +42,16 @@ extern "C" int APS5_VABI pthread_once_nid_postfix( onceChanged.notify_all(); return 0; } + +using OnceRoutine = void (APS5_VABI *)(); + +extern "C" { + +int APS5_VABI scePthreadOnce_nid_postfix(void* onceControl, OnceRoutine initRoutine) { + (void)onceControl; + (void)initRoutine; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libkernel/Pthread/src/Thread.cpp b/core/libs/prx/libkernel/Pthread/src/Thread.cpp index b594ba7a1..7f4f857dd 100644 --- a/core/libs/prx/libkernel/Pthread/src/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Thread.cpp @@ -284,3 +284,12 @@ int APS5_VABI scePthreadSetprio(Pthread thread, int prio) { } } + +extern "C" { + +void APS5_VABI __pthread_cxa_finalize_nid_postfix(void* argument) { + (void)argument; + NotImplemented_nid_no_patch(__func__); +} + +} diff --git a/core/libs/prx/libkernel/System/src/Process.cpp b/core/libs/prx/libkernel/System/src/Process.cpp index 821e2cff3..0e235df6d 100644 --- a/core/libs/prx/libkernel/System/src/Process.cpp +++ b/core/libs/prx/libkernel/System/src/Process.cpp @@ -6,6 +6,8 @@ extern "C" { +char* __progname_nid_postfix = nullptr; + int APS5_VABI getargc_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libkernel/System/src/Rtld.cpp b/core/libs/prx/libkernel/System/src/Rtld.cpp index e8b44eae9..0d131700b 100644 --- a/core/libs/prx/libkernel/System/src/Rtld.cpp +++ b/core/libs/prx/libkernel/System/src/Rtld.cpp @@ -38,3 +38,43 @@ void APS5_VABI sceKernelSetThreadDtors(thread_dtors_func_t dtors) { } } + +using ThreadAtexitCount = int (APS5_VABI *)(KernelModule module); +using ThreadAtexitReport = void (APS5_VABI *)(KernelModule module); +using ThreadDtors = void (APS5_VABI *)(); + +extern "C" { + +void APS5_VABI _sceKernelRtldSetApplicationHeapAPI_nid_postfix(void* api[]) { + (void)api; + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _sceKernelRtldThreadAtexitDecrement_nid_postfix(std::uint64_t* counter) { + (void)counter; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _sceKernelRtldThreadAtexitIncrement_nid_postfix(std::uint64_t* counter) { + (void)counter; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +void APS5_VABI _sceKernelSetThreadAtexitCount_nid_postfix(ThreadAtexitCount callback) { + (void)callback; + NotImplemented_nid_no_patch(__func__); +} + +void APS5_VABI _sceKernelSetThreadAtexitReport_nid_postfix(ThreadAtexitReport callback) { + (void)callback; + NotImplemented_nid_no_patch(__func__); +} + +void APS5_VABI _sceKernelSetThreadDtors_nid_postfix(ThreadDtors callback) { + (void)callback; + NotImplemented_nid_no_patch(__func__); +} + +} diff --git a/core/libs/prx/libkernel/System/src/Signals.cpp b/core/libs/prx/libkernel/System/src/Signals.cpp index c662d4858..713585c60 100644 --- a/core/libs/prx/libkernel/System/src/Signals.cpp +++ b/core/libs/prx/libkernel/System/src/Signals.cpp @@ -1,3 +1,4 @@ +#include "prx/libc/include/General.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include #include @@ -58,3 +59,13 @@ int APS5_VABI raise_nid_postfix(int guest) { return result ? -1 : 0; } } + +extern "C" { + +int APS5_VABI _is_signal_return_nid_postfix(std::uint64_t programCounter) { + (void)programCounter; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} From 5887c8d6e207a9c0dbcf3e38a1432d7f485ab2b5 Mon Sep 17 00:00:00 2001 From: boykopovar Date: Sat, 26 Sep 2026 16:42:54 +0300 Subject: [PATCH 72/94] feat(libkernel): add memory segment, backtrace, heap error report definitions --- core/libs/prx/libc/CMakeLists.txt | 1 + core/libs/prx/libc/src/HeapDiagnostics.cpp | 20 +++++++++++++++++++ core/libs/prx/libkernel/Module/src/Module.cpp | 7 +++++++ .../libs/prx/libkernel/System/src/Process.cpp | 1 + docs/TechnicalDebt.md | 18 ++++++++++------- 5 files changed, 40 insertions(+), 7 deletions(-) create mode 100644 core/libs/prx/libc/src/HeapDiagnostics.cpp diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index c81a2711a..872c317cd 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -14,6 +14,7 @@ target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/GuestMemoryBacking.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/GuestHeapStorage.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/GuestHeap.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/HeapDiagnostics.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Mspace.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/ApplicationHeap.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/ApplicationProcessParameters.cpp diff --git a/core/libs/prx/libc/src/HeapDiagnostics.cpp b/core/libs/prx/libc/src/HeapDiagnostics.cpp new file mode 100644 index 000000000..fc9dd761f --- /dev/null +++ b/core/libs/prx/libc/src/HeapDiagnostics.cpp @@ -0,0 +1,20 @@ +#include "prx/libc/include/General.hpp" +#include + +extern "C" { + +// unknown signature +void APS5_VABI sceLibcInternalBacktraceForGame_nid_postfix(const char* heapName) { + (void)heapName; + NotImplemented_nid_no_patch(__func__); +} + +// unknown signature +void APS5_VABI sceLibcInternalHeapErrorReportForGame_nid_postfix(void* heap, void* block, std::uint32_t error) { + (void)heap; + (void)block; + (void)error; + NotImplemented_nid_no_patch(__func__); +} + +} diff --git a/core/libs/prx/libkernel/Module/src/Module.cpp b/core/libs/prx/libkernel/Module/src/Module.cpp index 24a9e86a1..4b0f12136 100644 --- a/core/libs/prx/libkernel/Module/src/Module.cpp +++ b/core/libs/prx/libkernel/Module/src/Module.cpp @@ -62,4 +62,11 @@ int APS5_VABI __elf_phdr_match_addr_nid_postfix(ModuleInfo* module, std::uint64_ return 0; } +// unknown signature +std::int32_t APS5_VABI sceKernelInternalMemoryGetModuleSegmentInfo_nid_postfix(void* result) { + (void)result; + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libkernel/System/src/Process.cpp b/core/libs/prx/libkernel/System/src/Process.cpp index 0e235df6d..89dca7b71 100644 --- a/core/libs/prx/libkernel/System/src/Process.cpp +++ b/core/libs/prx/libkernel/System/src/Process.cpp @@ -6,6 +6,7 @@ extern "C" { +// unknown signature char* __progname_nid_postfix = nullptr; int APS5_VABI getargc_nid_postfix(void) { diff --git a/docs/TechnicalDebt.md b/docs/TechnicalDebt.md index 922c79b78..35713ad19 100644 --- a/docs/TechnicalDebt.md +++ b/docs/TechnicalDebt.md @@ -15,14 +15,18 @@ Throughout the project, every function at every stage either **does exactly what - [libSceCommonDialog](../core/libs/prx/libSceCommonDialog/Export.cpp) - The shader recompiler [skips baryctric coordinates](../core/shader/recompiler/Recompiler.cpp) (is not even passed to SpirvTargetOptions at row 212). -### Unknown function names +### Unknown function info -- [zARR5aCmkoY](../core/libs/prx/libSceAgc/DcbFlow/src/Control.cpp) (libSceAgc) - unknown signature -- [qj7QZpgr9Uw](../core/libs/prx/libSceAgc/DcbState/src/ContextState.cpp) (libSceAgc) -- [fd5Bp5tGTgo](../core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp) (libSceAgc) -- [dolOmWH+huQ](../core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp) (libSceAgc) -- [V++UgBtQhn0](../core/libs/prx/libSceAgc/Misc/src/PacketInfo.cpp) (libSceAgc) -- [gQkqkLttcpw](../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - unknown signature +- [zARR5aCmkoY](../core/libs/prx/libSceAgc/DcbFlow/src/Control.cpp) (libSceAgc) - unknown name, signature +- [qj7QZpgr9Uw](../core/libs/prx/libSceAgc/DcbState/src/ContextState.cpp) (libSceAgc) - unknown name +- [fd5Bp5tGTgo](../core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp) (libSceAgc) - unknown name +- [dolOmWH+huQ](../core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp) (libSceAgc) - unknown name +- [V++UgBtQhn0](../core/libs/prx/libSceAgc/Misc/src/PacketInfo.cpp) (libSceAgc) - unknown name +- [gQkqkLttcpw](../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - unknown name, signature +- [sceKernelInternalMemoryGetModuleSegmentInfo](../core/libs/prx/libkernel/Module/src/Module.cpp) (libkernel) - unknown signature +- [sceLibcInternalBacktraceForGame](../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - unknown signature +- [sceLibcInternalHeapErrorReportForGame](../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - unknown signature +- [__progname](../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - unknown data export ### Functional From 630cb018c077bd268f0c46fe643ad0896a46c48c Mon Sep 17 00:00:00 2001 From: boykopovar Date: Sat, 26 Sep 2026 16:45:41 +0300 Subject: [PATCH 73/94] feat(relinker): add --skip-sce-module flag --- core/relinker/cli/include/Cli.hpp | 1 + core/relinker/cli/src/CliArgs.cpp | 4 +++- core/relinker/main.cpp | 7 ++++++- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/core/relinker/cli/include/Cli.hpp b/core/relinker/cli/include/Cli.hpp index 369cb7f60..63c1bd38b 100644 --- a/core/relinker/cli/include/Cli.hpp +++ b/core/relinker/cli/include/Cli.hpp @@ -8,6 +8,7 @@ namespace Cli { struct Args { bool skipSyscallCheck = false; + bool skipSceModule = false; bool toIntel = false; bool writeRegistry = false; bool toWindows = false; diff --git a/core/relinker/cli/src/CliArgs.cpp b/core/relinker/cli/src/CliArgs.cpp index 12b1631f0..7629f6802 100644 --- a/core/relinker/cli/src/CliArgs.cpp +++ b/core/relinker/cli/src/CliArgs.cpp @@ -13,6 +13,8 @@ Args ParseArgs(int argc, char* argv[]) { const std::string arg = argv[i]; if (arg == "--skip-syscall-check") { args.skipSyscallCheck = true; + } else if (arg == "--skip-sce-module") { + args.skipSceModule = true; } else if (arg == "--to-intel") { args.toIntel = true; } else if (arg.rfind("unused-filter=", 0) == 0) { @@ -51,7 +53,7 @@ Args ParseArgs(int argc, char* argv[]) { if (args.inputPath.empty() || args.outputPath.empty()) throw std::runtime_error( - "Usage: relinker [--windows] [--windows-diagnostics] [--skip-syscall-check] [--to-intel] [unused-filter=0|1|2] [--registry] [--rpath ] [--lazy-binding] [--autorun] \n" + "Usage: relinker [--windows] [--windows-diagnostics] [--skip-syscall-check] [--skip-sce-module] [--to-intel] [unused-filter=0|1|2] [--registry] [--rpath ] [--lazy-binding] [--autorun] \n" "Example: relinker input.elf output.elf" ); diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index 32b8c2b32..1c530c64e 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -23,6 +23,7 @@ #include #include #include +#include int main(const int argc, char* argv[]) { Cli::Args args; @@ -66,6 +67,7 @@ int main(const int argc, char* argv[]) { ); std::cout << "System: " << (args.toWindows ? "Windows" : "Linux") << "; unused-filter=" << args.unusedFilterLevel << "\n"; + std::cout << "sce_module/sce_modules processing: " << (args.skipSceModule ? "disabled (--skip-sce-module)" : "enabled") << '\n'; auto result = pipeline->Relink(sourceBytes); for (const auto& patch : result.Patches) { if (patch.Offset > sourceBytes.size() || patch.Bytes.size() > sourceBytes.size() - patch.Offset) @@ -73,7 +75,10 @@ int main(const int argc, char* argv[]) { for (std::size_t index = 0; index < patch.Bytes.size(); ++index) sourceBytes[patch.Offset + index] = patch.Bytes[index]; } - const auto guestArtifacts = Relinker::GuestModuleBuilder().Build(args.inputPath, absPath, result.DynamicSection, args.toWindows, args.toIntel, args.skipSyscallCheck, args.lazyBinding, args.runPath); + std::vector guestArtifacts; + if (!args.skipSceModule) { + guestArtifacts = Relinker::GuestModuleBuilder().Build(args.inputPath, absPath, result.DynamicSection, args.toWindows, args.toIntel, args.skipSyscallCheck, args.lazyBinding, args.runPath); + } if (args.writeRegistry) { const std::filesystem::path outFsPath(absPath); From 2e654e4f895c4b35e46aa40eec7c3628ef46bbac Mon Sep 17 00:00:00 2001 From: boykopovar Date: Sat, 26 Sep 2026 16:54:43 +0300 Subject: [PATCH 74/94] feat(relinker): scan sce modules for syscalls --- core/relinker/main.cpp | 5 +++-- .../relinker/include/relinker/guest/GuestImage.hpp | 3 ++- .../relinker/src/guest/GuestModuleBuilder.cpp | 12 ++++-------- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index 1c530c64e..64e3994e1 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -55,10 +55,11 @@ int main(const int argc, char* argv[]) { } auto elfReader = std::make_shared(sourceBytes); + const std::shared_ptr syscallScanner = args.skipSyscallCheck ? Relinker::MakeNullSyscallScanner() : Relinker::MakeSyscallScanner(); const auto pipeline = std::make_shared( elfReader, - args.skipSyscallCheck ? Relinker::MakeNullSyscallScanner() : Relinker::MakeSyscallScanner(), + syscallScanner, Relinker::MakeCallSiteResolver(), std::make_shared(), std::make_shared(), @@ -77,7 +78,7 @@ int main(const int argc, char* argv[]) { std::vector guestArtifacts; if (!args.skipSceModule) { - guestArtifacts = Relinker::GuestModuleBuilder().Build(args.inputPath, absPath, result.DynamicSection, args.toWindows, args.toIntel, args.skipSyscallCheck, args.lazyBinding, args.runPath); + guestArtifacts = Relinker::GuestModuleBuilder().Build(args.inputPath, absPath, result.DynamicSection, args.toWindows, args.toIntel, *syscallScanner, args.lazyBinding, args.runPath); } if (args.writeRegistry) { diff --git a/core/relinker/relinker/include/relinker/guest/GuestImage.hpp b/core/relinker/relinker/include/relinker/guest/GuestImage.hpp index e001c4357..a71a33289 100644 --- a/core/relinker/relinker/include/relinker/guest/GuestImage.hpp +++ b/core/relinker/relinker/include/relinker/guest/GuestImage.hpp @@ -2,6 +2,7 @@ #define RELINKER_GUESTIMAGE_HPP #include +#include #include namespace Relinker { @@ -41,7 +42,7 @@ struct GuestArtifact { class GuestModuleBuilder { public: - std::vector Build(const std::filesystem::path& inputPath, const std::filesystem::path& outputPath, Domain::SysVDynamicSection& dynamic, bool windows, bool toIntel, bool skipSyscallCheck, bool lazyBinding, const std::string& runPath) const; + std::vector Build(const std::filesystem::path& inputPath, const std::filesystem::path& outputPath, Domain::SysVDynamicSection& dynamic, bool windows, bool toIntel, ISyscallScanner& syscallScanner, bool lazyBinding, const std::string& runPath) const; }; } diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index df86edccc..c461ffb3c 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -1,5 +1,4 @@ #include -#include #include #include #include @@ -12,7 +11,7 @@ namespace Relinker { -std::vector GuestModuleBuilder::Build(const std::filesystem::path& inputPath, const std::filesystem::path& outputPath, Domain::SysVDynamicSection& dynamic, const bool windows, const bool toIntel, const bool skipSyscallCheck, const bool lazyBinding, const std::string& runPath) const { +std::vector GuestModuleBuilder::Build(const std::filesystem::path& inputPath, const std::filesystem::path& outputPath, Domain::SysVDynamicSection& dynamic, const bool windows, const bool toIntel, ISyscallScanner& syscallScanner, const bool lazyBinding, const std::string& runPath) const { const auto root = std::filesystem::absolute(inputPath).parent_path(); const auto singular = root / "sce_module"; const auto plural = root / "sce_modules"; @@ -61,12 +60,9 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path std::vector codeHeaders; for (const auto& header : image.Headers) if (header.Type == 1 && (header.Flags & 1) != 0) codeHeaders.push_back(header); if (toIntel) image.Bytes = Codegen::MakeAmd64OnlyConverter()->Convert(std::move(image.Bytes), codeHeaders).Bytes; - if (!skipSyscallCheck) { - const auto scanner = MakeSyscallScanner(); - for (const auto& header : codeHeaders) { - const std::vector code(image.Bytes.begin() + header.Offset, image.Bytes.begin() + header.Offset + header.FileSize); - scanner->ScanCodeSectionForSyscalls(code, header.MappedAddress, header.FileSize); - } + for (const auto& header : codeHeaders) { + const std::vector code(image.Bytes.begin() + header.Offset, image.Bytes.begin() + header.Offset + header.FileSize); + syscallScanner.ScanCodeSectionForSyscalls(code, header.MappedAddress, header.FileSize); } images.push_back(std::move(image)); } From 55e1c4a23872c0cbb861a841db5efe6ce62fd658 Mon Sep 17 00:00:00 2001 From: boykopovar Date: Sat, 26 Sep 2026 17:07:34 +0300 Subject: [PATCH 75/94] feat(libkernel): implement application heap and thread atexit registration --- core/libs/SceTypes.hpp | 6 +- .../prx/libkernel/Pthread/Posix/Thread.cpp | 4 +- .../Pthread/include/ThreadLifecycle.hpp | 15 +++++ .../libs/prx/libkernel/Pthread/src/Thread.cpp | 56 +++++++++++++++---- core/libs/prx/libkernel/System/src/Rtld.cpp | 32 ++++------- 5 files changed, 76 insertions(+), 37 deletions(-) create mode 100644 core/libs/prx/libkernel/Pthread/include/ThreadLifecycle.hpp diff --git a/core/libs/SceTypes.hpp b/core/libs/SceTypes.hpp index 3b149bce3..51b0a9caf 100644 --- a/core/libs/SceTypes.hpp +++ b/core/libs/SceTypes.hpp @@ -173,9 +173,9 @@ using PthreadCondattr = PthreadCondattrPrivate*; using PthreadKey = int; using pthread_entry_func_t = void* (*)(void*); using pthread_key_destructor_func_t = void (*)(void*); -using thread_dtors_func_t = void (*)(); -using get_thread_atexit_count_func_t = int (*)(KernelModule); -using thread_atexit_report_func_t = void (*)(KernelModule); +using thread_dtors_func_t = void (APS5_VABI *)(); +using get_thread_atexit_count_func_t = int (APS5_VABI *)(KernelModule); +using thread_atexit_report_func_t = void (APS5_VABI *)(KernelModule); struct FileStat { std::uint32_t st_dev; diff --git a/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp b/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp index da9083fc4..3b585b583 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp @@ -1,6 +1,7 @@ #include #include #include "SceTypes.hpp" +#include "../include/ThreadLifecycle.hpp" #include "prx/libc/include/General.hpp" @@ -32,8 +33,7 @@ int APS5_VABI pthread_detach_nid_postfix(Pthread thread) { } void APS5_VABI pthread_exit_nid_postfix(void* value) { - (void)value; - NotImplemented_nid_no_patch(__func__); + scePthreadExit(value); } int APS5_VABI pthread_getschedparam_nid_postfix(Pthread thread, int* policy, KernelSchedParam* param) { diff --git a/core/libs/prx/libkernel/Pthread/include/ThreadLifecycle.hpp b/core/libs/prx/libkernel/Pthread/include/ThreadLifecycle.hpp new file mode 100644 index 000000000..f342bb0ca --- /dev/null +++ b/core/libs/prx/libkernel/Pthread/include/ThreadLifecycle.hpp @@ -0,0 +1,15 @@ +#ifndef CORE_LIBS_PRX_LIBKERNEL_PTHREAD_THREADLIFECYCLE_HPP +#define CORE_LIBS_PRX_LIBKERNEL_PTHREAD_THREADLIFECYCLE_HPP + +#include "SceTypes.hpp" + +class ThreadLifecycle { +public: + static void SetThreadDtors(thread_dtors_func_t callback); + static void SetThreadAtexitCount(get_thread_atexit_count_func_t callback); + static void SetThreadAtexitReport(thread_atexit_report_func_t callback); +}; + +extern "C" void APS5_VABI scePthreadExit(void* retval); + +#endif diff --git a/core/libs/prx/libkernel/Pthread/src/Thread.cpp b/core/libs/prx/libkernel/Pthread/src/Thread.cpp index 7f4f857dd..6be2a0565 100644 --- a/core/libs/prx/libkernel/Pthread/src/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Thread.cpp @@ -1,4 +1,5 @@ #include "../include/Pthread.hpp" +#include "../include/ThreadLifecycle.hpp" #include "prx/libc/include/General.hpp" #include #include @@ -29,7 +30,44 @@ struct ThreadArgs { PthreadPrivate* self; }; -static void FinishThread(PthreadPrivate* self, void* retval) { +static std::atomic threadDtors{nullptr}; +static std::atomic threadAtexitCount{nullptr}; +static std::atomic threadAtexitReport{nullptr}; +static thread_local PthreadPrivate* currentThread = nullptr; +static thread_local bool threadFinishing = false; + +void ThreadLifecycle::SetThreadDtors(thread_dtors_func_t callback) { + if (!callback) + throw std::runtime_error("Thread destructor callback is null"); + thread_dtors_func_t expected = nullptr; + if (!threadDtors.compare_exchange_strong(expected, callback)) + throw std::runtime_error("Thread destructor callback is already registered"); +} + +void ThreadLifecycle::SetThreadAtexitCount(get_thread_atexit_count_func_t callback) { + if (!callback) + throw std::runtime_error("Thread atexit count callback is null"); + get_thread_atexit_count_func_t expected = nullptr; + if (!threadAtexitCount.compare_exchange_strong(expected, callback)) + throw std::runtime_error("Thread atexit count callback is already registered"); +} + +void ThreadLifecycle::SetThreadAtexitReport(thread_atexit_report_func_t callback) { + if (!callback) + throw std::runtime_error("Thread atexit report callback is null"); + thread_atexit_report_func_t expected = nullptr; + if (!threadAtexitReport.compare_exchange_strong(expected, callback)) + throw std::runtime_error("Thread atexit report callback is already registered"); +} + +static void finishThread(PthreadPrivate* self, void* retval) { + if (!self || self != currentThread) + throw std::runtime_error("Finishing an unregistered guest thread"); + if (threadFinishing) + throw std::runtime_error("Guest thread is already finishing"); + threadFinishing = true; + if (const auto callback = threadDtors.load()) + callback(); { std::unique_lock lk(self->_join_mtx); self->_retval = retval; @@ -43,13 +81,13 @@ static void RunThread(std::unique_ptr args) { const auto entry = args->entry; void* arg = args->arg; PthreadPrivate* self = args->self; + currentThread = self; args.reset(); - FinishThread(self, entry(arg)); + finishThread(self, entry(arg)); + currentThread = nullptr; } #ifdef _WIN32 -static thread_local PthreadPrivate* currentThread = nullptr; - static void ReleaseThread(PthreadPrivate* thread) { if (thread->references.fetch_sub(1, std::memory_order_acq_rel) != 1) return; @@ -183,26 +221,22 @@ int APS5_VABI scePthreadDetach(Pthread thread) { } void APS5_VABI scePthreadExit(void* retval) { -#ifdef _WIN32 if (!currentThread) throw std::runtime_error("scePthreadExit: current thread is not registered"); auto* self = currentThread; - FinishThread(self, retval); + finishThread(self, retval); currentThread = nullptr; +#ifdef _WIN32 ReleaseThread(self); _endthreadex(0); #else pthread_exit(retval); #endif - __builtin_unreachable(); + throw std::runtime_error("Native thread exit returned"); } Pthread APS5_VABI scePthreadSelf() { -#ifdef _WIN32 return currentThread; -#else - return nullptr; -#endif } void APS5_VABI scePthreadYield() { diff --git a/core/libs/prx/libkernel/System/src/Rtld.cpp b/core/libs/prx/libkernel/System/src/Rtld.cpp index 0d131700b..e47bf5b13 100644 --- a/core/libs/prx/libkernel/System/src/Rtld.cpp +++ b/core/libs/prx/libkernel/System/src/Rtld.cpp @@ -3,6 +3,7 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "prx/libc/include/ApplicationHeap.hpp" +#include "prx/libkernel/Pthread/include/ThreadLifecycle.hpp" extern "C" { @@ -23,31 +24,23 @@ int APS5_VABI sceKernelRtldThreadAtexitIncrement(uint64_t* c) { } void APS5_VABI sceKernelSetThreadAtexitCount(get_thread_atexit_count_func_t func) { - (void)func; - NotImplemented_nid_no_patch(__func__); + ThreadLifecycle::SetThreadAtexitCount(func); } void APS5_VABI sceKernelSetThreadAtexitReport(thread_atexit_report_func_t func) { - (void)func; - NotImplemented_nid_no_patch(__func__); + ThreadLifecycle::SetThreadAtexitReport(func); } void APS5_VABI sceKernelSetThreadDtors(thread_dtors_func_t dtors) { - (void)dtors; - NotImplemented_nid_no_patch(__func__); + ThreadLifecycle::SetThreadDtors(dtors); } } -using ThreadAtexitCount = int (APS5_VABI *)(KernelModule module); -using ThreadAtexitReport = void (APS5_VABI *)(KernelModule module); -using ThreadDtors = void (APS5_VABI *)(); - extern "C" { void APS5_VABI _sceKernelRtldSetApplicationHeapAPI_nid_postfix(void* api[]) { - (void)api; - NotImplemented_nid_no_patch(__func__); + ApplicationHeapRegister_nid_no_patch(api); } int APS5_VABI _sceKernelRtldThreadAtexitDecrement_nid_postfix(std::uint64_t* counter) { @@ -62,19 +55,16 @@ int APS5_VABI _sceKernelRtldThreadAtexitIncrement_nid_postfix(std::uint64_t* cou return 0; } -void APS5_VABI _sceKernelSetThreadAtexitCount_nid_postfix(ThreadAtexitCount callback) { - (void)callback; - NotImplemented_nid_no_patch(__func__); +void APS5_VABI _sceKernelSetThreadAtexitCount_nid_postfix(get_thread_atexit_count_func_t callback) { + ThreadLifecycle::SetThreadAtexitCount(callback); } -void APS5_VABI _sceKernelSetThreadAtexitReport_nid_postfix(ThreadAtexitReport callback) { - (void)callback; - NotImplemented_nid_no_patch(__func__); +void APS5_VABI _sceKernelSetThreadAtexitReport_nid_postfix(thread_atexit_report_func_t callback) { + ThreadLifecycle::SetThreadAtexitReport(callback); } -void APS5_VABI _sceKernelSetThreadDtors_nid_postfix(ThreadDtors callback) { - (void)callback; - NotImplemented_nid_no_patch(__func__); +void APS5_VABI _sceKernelSetThreadDtors_nid_postfix(thread_dtors_func_t callback) { + ThreadLifecycle::SetThreadDtors(callback); } } From dbb56adcf669093d4c47671c2f5e750ca789e7d2 Mon Sep 17 00:00:00 2001 From: boykopovar Date: Sat, 26 Sep 2026 17:24:28 +0300 Subject: [PATCH 76/94] feat(libkernel): implement process, cpu, uuid, gpi, gpo, sync funcs --- core/libs/prx/libc/include/Shutdown.hpp | 1 + core/libs/prx/libc/src/Process.cpp | 6 +- .../libs/prx/libkernel/System/src/Process.cpp | 212 +++++++++++++++--- 3 files changed, 181 insertions(+), 38 deletions(-) diff --git a/core/libs/prx/libc/include/Shutdown.hpp b/core/libs/prx/libc/include/Shutdown.hpp index 048505884..c1ff16604 100644 --- a/core/libs/prx/libc/include/Shutdown.hpp +++ b/core/libs/prx/libc/include/Shutdown.hpp @@ -3,5 +3,6 @@ extern "C" void LibcRegisterShutdown_nid_postfix(void (*callback)()); extern "C" void LibcRunShutdown_nid_postfix(); +extern "C" [[noreturn]] void LibcExit_nid_no_patch(int code); #endif diff --git a/core/libs/prx/libc/src/Process.cpp b/core/libs/prx/libc/src/Process.cpp index 01b63f769..dad4e628e 100644 --- a/core/libs/prx/libc/src/Process.cpp +++ b/core/libs/prx/libc/src/Process.cpp @@ -61,11 +61,15 @@ extern "C" { std::_Exit(code); } -void APS5_VABI exit_nid_postfix(int code) { +[[noreturn]] void LibcExit_nid_no_patch(int code) { LibcRunShutdown_nid_postfix(); std::exit(code); } +void APS5_VABI exit_nid_postfix(int code) { + LibcExit_nid_no_patch(code); +} + [[noreturn]] void abort_nid_postfix( uint64_t arg0, uint64_t arg1, uint64_t arg2, uint64_t arg3, uint64_t arg4, uint64_t arg5 diff --git a/core/libs/prx/libkernel/System/src/Process.cpp b/core/libs/prx/libkernel/System/src/Process.cpp index 89dca7b71..4fa1e7ea9 100644 --- a/core/libs/prx/libkernel/System/src/Process.cpp +++ b/core/libs/prx/libkernel/System/src/Process.cpp @@ -1,8 +1,120 @@ -#include -#include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#include +#endif + #include "SceTypes.hpp" -#include "prx/libc/include/General.hpp" +#include "prx/libc/include/ApplicationHeap.hpp" +#include "prx/libc/include/Shutdown.hpp" #include "prx/libkernel/DirectMemory/DirectMemory.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +std::atomic gpoBits{0}; +constexpr std::array openPsId{'A', 'n', 'y', 'P', 'S', '5', 'O', 'p', 'e', 'n', 'P', 's', 'I', 'd', 0, 1}; + +class ProcessArguments { +public: + ProcessArguments() { +#ifdef _WIN32 + std::array path{}; + const auto size = GetModuleFileNameA(nullptr, path.data(), static_cast(path.size())); + if (size == 0) + throw std::system_error(GetLastError(), std::system_category(), "Reading executable path"); + if (size >= path.size()) + throw std::runtime_error("Executable path exceeds the guest argument buffer"); + arguments.emplace_back(path.data(), size); +#else + std::ifstream stream("/proc/self/cmdline", std::ios::binary); + if (!stream) + throw std::runtime_error("Cannot read process arguments"); + std::string argument; + while (std::getline(stream, argument, '\0')) { + if (stream.eof()) + throw std::runtime_error("Unterminated process argument"); + arguments.push_back(argument); + } + if (stream.bad() || !stream.eof()) + throw std::runtime_error("Reading process arguments failed"); +#endif + if (arguments.empty() || arguments.size() > static_cast(std::numeric_limits::max())) + throw std::runtime_error("Invalid process argument count"); + for (const auto& argument : arguments) + pointers.push_back(argument.c_str()); + pointers.push_back(nullptr); + } + + int GetCount() const { return static_cast(arguments.size()); } + const char** GetValues() { return pointers.data(); } + +private: + std::vector arguments; + std::vector pointers; +}; + +ProcessArguments& getProcessArguments() { + static ProcessArguments arguments; + return arguments; +} + +void validateSchedulingPolicy(int policy) { + if (policy != 1 && policy != 3) + throw std::invalid_argument("Unsupported guest scheduling policy"); +} + +#ifdef _WIN32 +void syncVolumes() { + std::array name{}; + const auto first = FindFirstVolumeW(name.data(), static_cast(name.size())); + if (first == INVALID_HANDLE_VALUE) + throw std::system_error(GetLastError(), std::system_category(), "Enumerating volumes for sync"); + const std::unique_ptr search(first, &FindVolumeClose); + for (;;) { + DWORD flags = 0; + if (!GetVolumeInformationW(name.data(), nullptr, 0, nullptr, nullptr, &flags, nullptr, 0)) + throw std::system_error(GetLastError(), std::system_category(), "Reading volume properties for sync"); + if ((flags & FILE_READ_ONLY_VOLUME) == 0) { + std::wstring path(name.data()); + if (path.empty() || path.back() != L'\\') + throw std::runtime_error("Invalid volume path for sync"); + path.pop_back(); + const auto native = CreateFileW(path.c_str(), GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr); + if (native == INVALID_HANDLE_VALUE) + throw std::system_error(GetLastError(), std::system_category(), "Opening volume for sync"); + const std::unique_ptr volume(native, &CloseHandle); + if (!FlushFileBuffers(volume.get())) + throw std::system_error(GetLastError(), std::system_category(), "Flushing volume"); + } + if (FindNextVolumeW(search.get(), name.data(), static_cast(name.size()))) + continue; + const auto error = GetLastError(); + if (error != ERROR_NO_MORE_FILES) + throw std::system_error(error, std::system_category(), "Enumerating volumes for sync"); + break; + } +} +#endif + +} extern "C" { @@ -10,75 +122,101 @@ extern "C" { char* __progname_nid_postfix = nullptr; int APS5_VABI getargc_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return getProcessArguments().GetCount(); } const char** APS5_VABI getargv_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return nullptr; + return getProcessArguments().GetValues(); } int APS5_VABI getpagesize_nid_postfix(void) { - return PS5_PAGE_SIZE; + return PS5_PAGE_SIZE; } int APS5_VABI getpid_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +#ifdef _WIN32 + const auto pid = GetCurrentProcessId(); +#else + const auto pid = ::getpid(); +#endif + if (pid == 0 || static_cast(pid) > static_cast(std::numeric_limits::max())) + throw std::runtime_error("Process identifier is outside the guest range"); + return static_cast(pid); } void APS5_VABI exit_nid_postfix(int code) { - (void)code; - NotImplemented_nid_no_patch(__func__); + LibcExit_nid_no_patch(code); } int APS5_VABI sceKernelGetCurrentCpu(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +#ifdef _WIN32 + PROCESSOR_NUMBER processor{}; + GetCurrentProcessorNumberEx(&processor); + unsigned index = processor.Number; + for (WORD group = 0; group < processor.Group; ++group) { + const auto count = GetActiveProcessorCount(group); + if (count == 0) + throw std::system_error(GetLastError(), std::system_category(), "Reading processor group size"); + index += count; + } + return static_cast(index); +#else + const int cpu = ::sched_getcpu(); + if (cpu < 0) + throw std::system_error(errno, std::generic_category(), "Reading current processor"); + return cpu; +#endif } -uint64_t APS5_VABI sceKernelGetGPI(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +std::uint64_t APS5_VABI sceKernelGetGPI(void) { + return gpoBits.load(std::memory_order_relaxed); } -void APS5_VABI sceKernelSetGPO(uint32_t bits) { - (void)bits; - NotImplemented_nid_no_patch(__func__); +void APS5_VABI sceKernelSetGPO(std::uint32_t bits) { + gpoBits.store(bits, std::memory_order_relaxed); } -int APS5_VABI sceKernelGetOpenPsId(void* open_ps_id) { - (void)open_ps_id; - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceKernelGetOpenPsId(void* openPsIdOutput) { + if (!openPsIdOutput) + throw std::invalid_argument("sceKernelGetOpenPsId: null output"); + std::memcpy(openPsIdOutput, openPsId.data(), openPsId.size()); + return 0; } void* APS5_VABI sceKernelGetProcParam(void) { - NotImplemented_nid_no_patch(__func__); - return nullptr; + return const_cast(ApplicationProcessParameters_nid_no_patch()); } -int APS5_VABI sceKernelUuidCreate(uint32_t* uuid) { - (void)uuid; - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceKernelUuidCreate(std::uint32_t* uuid) { + if (!uuid) + throw std::invalid_argument("sceKernelUuidCreate: null output"); + static thread_local std::random_device device; + std::uniform_int_distribution distribution; + std::array value; + for (auto& word : value) + word = distribution(device); + value[1] = (value[1] & 0x0fffffffu) | 0x40000000u; + value[2] = (value[2] & 0xffffff3fu) | 0x80u; + std::memcpy(uuid, value.data(), sizeof(value)); + return 0; } void APS5_VABI sceKernelSync(void) { - NotImplemented_nid_no_patch(__func__); +#ifdef _WIN32 + syncVolumes(); +#else + ::sync(); +#endif } int APS5_VABI sched_get_priority_max_nid_postfix(int policy) { - (void)policy; - NotImplemented_nid_no_patch(__func__); - return 0; + validateSchedulingPolicy(policy); + return 256; } int APS5_VABI sched_get_priority_min_nid_postfix(int policy) { - (void)policy; - NotImplemented_nid_no_patch(__func__); - return 0; + validateSchedulingPolicy(policy); + return 767; } } From 9fd68163569d6b8fad8916f7b81118bed9ea682f Mon Sep 17 00:00:00 2001 From: boykopovar Date: Sat, 26 Sep 2026 17:31:13 +0300 Subject: [PATCH 77/94] fix(libkernel): store rtld allocator table separately from application heap --- core/libs/prx/libkernel/System/src/Rtld.cpp | 29 ++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libkernel/System/src/Rtld.cpp b/core/libs/prx/libkernel/System/src/Rtld.cpp index e47bf5b13..7a2f27d81 100644 --- a/core/libs/prx/libkernel/System/src/Rtld.cpp +++ b/core/libs/prx/libkernel/System/src/Rtld.cpp @@ -1,14 +1,37 @@ #include #include +#include +#include +#include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" -#include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libkernel/Pthread/include/ThreadLifecycle.hpp" +namespace { + +std::mutex rtldHeapMutex; +std::array rtldHeapApi{}; + +void registerApplicationHeapApi(void* const* api) { + if (api == nullptr) + throw std::invalid_argument("RTLD application heap: null allocator API"); + std::array replacement; + std::memcpy(replacement.data(), api, sizeof(replacement)); + if (replacement[0] == nullptr || replacement[1] == nullptr) + throw std::invalid_argument("RTLD application heap: malloc and free are required"); + std::lock_guard lock(rtldHeapMutex); + if (rtldHeapApi[0] != nullptr && rtldHeapApi != replacement) + throw std::runtime_error("RTLD application heap: cannot replace an active allocator"); + rtldHeapApi = replacement; +} + +} + extern "C" { void APS5_VABI sceKernelRtldSetApplicationHeapAPI(void* api[]) { - ApplicationHeapRegister_nid_no_patch(api); + registerApplicationHeapApi(api); } int APS5_VABI sceKernelRtldThreadAtexitDecrement(uint64_t* c) { @@ -40,7 +63,7 @@ void APS5_VABI sceKernelSetThreadDtors(thread_dtors_func_t dtors) { extern "C" { void APS5_VABI _sceKernelRtldSetApplicationHeapAPI_nid_postfix(void* api[]) { - ApplicationHeapRegister_nid_no_patch(api); + registerApplicationHeapApi(api); } int APS5_VABI _sceKernelRtldThreadAtexitDecrement_nid_postfix(std::uint64_t* counter) { From c9cb3cae39f3f9490e147a4e79a51d5b390abd32 Mon Sep 17 00:00:00 2001 From: boykopovar Date: Sat, 26 Sep 2026 17:34:30 +0300 Subject: [PATCH 78/94] feat(libkernel): implement disabled address sanitizer state + replacement tables --- core/libs/prx/libkernel/System/src/Sanitizer.cpp | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libkernel/System/src/Sanitizer.cpp b/core/libs/prx/libkernel/System/src/Sanitizer.cpp index 452a93743..15f2d3f5f 100644 --- a/core/libs/prx/libkernel/System/src/Sanitizer.cpp +++ b/core/libs/prx/libkernel/System/src/Sanitizer.cpp @@ -1,23 +1,19 @@ -#include -#include #include "SceTypes.hpp" -#include "prx/libc/include/General.hpp" extern "C" { int APS5_VABI sceKernelIsAddressSanitizerEnabled(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return 0; } MallocReplace* APS5_VABI sceKernelGetSanitizerMallocReplaceExternal(void) { - NotImplemented_nid_no_patch(__func__); - return nullptr; + static MallocReplace replacement{}; + return &replacement; } NewReplace* APS5_VABI sceKernelGetSanitizerNewReplaceExternal(void) { - NotImplemented_nid_no_patch(__func__); - return nullptr; + static NewReplace replacement{}; + return &replacement; } } From ae2e5cba7ebf85ba915f8f55df52090b0b720ffe Mon Sep 17 00:00:00 2001 From: boykopovar Date: Sat, 26 Sep 2026 17:37:29 +0300 Subject: [PATCH 79/94] feat(libc): implement heap trace info --- core/libs/prx/libSceLibcInternal/Export.cpp | 9 ++----- core/libs/prx/libc/Export.cpp | 4 +-- .../libs/prx/libc/include/HeapDiagnostics.hpp | 8 ++++++ core/libs/prx/libc/src/HeapDiagnostics.cpp | 25 +++++++++++++++++++ 4 files changed, 37 insertions(+), 9 deletions(-) create mode 100644 core/libs/prx/libc/include/HeapDiagnostics.hpp diff --git a/core/libs/prx/libSceLibcInternal/Export.cpp b/core/libs/prx/libSceLibcInternal/Export.cpp index 7541f8ed1..6597f5f5b 100644 --- a/core/libs/prx/libSceLibcInternal/Export.cpp +++ b/core/libs/prx/libSceLibcInternal/Export.cpp @@ -1,14 +1,9 @@ -#include -#include -#include "SceTypes.hpp" -#include "prx/libc/include/General.hpp" +#include "prx/libc/include/HeapDiagnostics.hpp" extern "C" { - void APS5_VABI sceLibcHeapGetTraceInfo_nid_postfix(Info* info) { - (void)info; - NotImplemented_nid_no_patch(__func__); + LibcHeapTraceInfo_nid_no_patch(info); } } diff --git a/core/libs/prx/libc/Export.cpp b/core/libs/prx/libc/Export.cpp index a7bacbfde..4e0c3813a 100644 --- a/core/libs/prx/libc/Export.cpp +++ b/core/libs/prx/libc/Export.cpp @@ -2,6 +2,7 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/HeapDiagnostics.hpp" uint32_t Need_sceLibc = 1; @@ -39,8 +40,7 @@ extern "C" { } void APS5_VABI LibcHeapGetTraceInfo_nid_postfix(LibcHeapInfo* info) { - (void)info; - NotImplemented_nid_no_patch(__func__); + LibcHeapTraceInfo_nid_no_patch(info); } int APS5_VABI LibcInternalExtCxaThreadAtexit_nid_postfix(void (*destructor)(void*), void* object, void* module_id) { diff --git a/core/libs/prx/libc/include/HeapDiagnostics.hpp b/core/libs/prx/libc/include/HeapDiagnostics.hpp new file mode 100644 index 000000000..7d3d62454 --- /dev/null +++ b/core/libs/prx/libc/include/HeapDiagnostics.hpp @@ -0,0 +1,8 @@ +#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_HEAPDIAGNOSTICS_HPP +#define CORE_LIBS_PRX_LIBC_INCLUDE_HEAPDIAGNOSTICS_HPP + +#include "SceTypes.hpp" + +extern "C" void LibcHeapTraceInfo_nid_no_patch(LibcHeapInfo* info); + +#endif diff --git a/core/libs/prx/libc/src/HeapDiagnostics.cpp b/core/libs/prx/libc/src/HeapDiagnostics.cpp index fc9dd761f..39aaf3951 100644 --- a/core/libs/prx/libc/src/HeapDiagnostics.cpp +++ b/core/libs/prx/libc/src/HeapDiagnostics.cpp @@ -1,8 +1,33 @@ #include "prx/libc/include/General.hpp" +#include "prx/libc/include/HeapDiagnostics.hpp" +#include +#include #include +#include + +namespace { + +std::uint64_t mspaceAtomicIdMask = 0; +std::array mstateTable{}; + +static_assert(sizeof(LibcHeapInfo) == 32); +static_assert(offsetof(LibcHeapInfo, mspace_atomic_id_mask) == 16); +static_assert(offsetof(LibcHeapInfo, mstate_table) == 24); + +} extern "C" { +void LibcHeapTraceInfo_nid_no_patch(LibcHeapInfo* info) { + if (info == nullptr) + throw std::invalid_argument("heap trace info: null output"); + if (info->size != sizeof(LibcHeapInfo)) + throw std::invalid_argument("heap trace info: unsupported structure size"); + info->unknown2 = 0; + info->mspace_atomic_id_mask = &mspaceAtomicIdMask; + info->mstate_table = mstateTable.data(); +} + // unknown signature void APS5_VABI sceLibcInternalBacktraceForGame_nid_postfix(const char* heapName) { (void)heapName; From a709e220f2191427f6fc9866065a61a3cab9e7de Mon Sep 17 00:00:00 2001 From: boykopovar Date: Sat, 26 Sep 2026 17:43:44 +0300 Subject: [PATCH 80/94] feat(libkernel): implement posix mutex locking and timed waits --- .../prx/libkernel/Pthread/Posix/Mutex.cpp | 64 ++--- .../prx/libkernel/Pthread/include/Mutex.hpp | 26 ++ .../prx/libkernel/Pthread/include/Pthread.hpp | 2 +- core/libs/prx/libkernel/Pthread/src/Mutex.cpp | 223 +++++++++++------- 4 files changed, 190 insertions(+), 125 deletions(-) create mode 100644 core/libs/prx/libkernel/Pthread/include/Mutex.hpp diff --git a/core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp b/core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp index 1409b3450..5482859ba 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp @@ -1,72 +1,60 @@ +#include "../include/Mutex.hpp" #include -#include -#include "SceTypes.hpp" -#include "prx/libc/include/General.hpp" +#include + +namespace { + +int toPosix(int result) { + if (result == 0) + return 0; + const auto error = static_cast(result); + if ((error & 0xffff0000u) != 0x80020000u) + throw std::runtime_error("Unexpected SCE mutex error"); + return static_cast(error & 0xffffu); +} + +} extern "C" { int APS5_VABI pthread_mutex_destroy_nid_postfix(PthreadMutex* mutex) { - (void)mutex; - NotImplemented_nid_no_patch(__func__); - return 0; + return toPosix(scePthreadMutexDestroy(mutex)); } int APS5_VABI pthread_mutex_init_nid_postfix(PthreadMutex* mutex, const PthreadMutexattr* attr) { - (void)mutex; - (void)attr; - NotImplemented_nid_no_patch(__func__); - return 0; + return toPosix(scePthreadMutexInit(mutex, attr, nullptr)); } int APS5_VABI pthread_mutex_lock_nid_postfix(PthreadMutex* mutex) { - (void)mutex; - NotImplemented_nid_no_patch(__func__); - return 0; + return toPosix(scePthreadMutexLock(mutex)); } int APS5_VABI pthread_mutex_timedlock_nid_postfix(PthreadMutex* mutex, const KernelTimespec* abstime) { - (void)mutex; - (void)abstime; - NotImplemented_nid_no_patch(__func__); - return 0; + return toPosix(MutexOperations::Timedlock(mutex, abstime)); } int APS5_VABI pthread_mutex_trylock_nid_postfix(PthreadMutex* mutex) { - (void)mutex; - NotImplemented_nid_no_patch(__func__); - return 0; + return toPosix(scePthreadMutexTrylock(mutex)); } int APS5_VABI pthread_mutex_unlock_nid_postfix(PthreadMutex* mutex) { - (void)mutex; - NotImplemented_nid_no_patch(__func__); - return 0; + return toPosix(scePthreadMutexUnlock(mutex)); } int APS5_VABI pthread_mutexattr_destroy_nid_postfix(PthreadMutexattr* attr) { - (void)attr; - NotImplemented_nid_no_patch(__func__); - return 0; + return toPosix(scePthreadMutexattrDestroy(attr)); } int APS5_VABI pthread_mutexattr_init_nid_postfix(PthreadMutexattr* attr) { - (void)attr; - NotImplemented_nid_no_patch(__func__); - return 0; + return toPosix(scePthreadMutexattrInit(attr)); } int APS5_VABI pthread_mutexattr_setprotocol_nid_postfix(PthreadMutexattr* attr, int protocol) { - (void)attr; - (void)protocol; - NotImplemented_nid_no_patch(__func__); - return 0; + return toPosix(scePthreadMutexattrSetprotocol(attr, protocol)); } int APS5_VABI pthread_mutexattr_settype_nid_postfix(PthreadMutexattr* attr, int type) { - (void)attr; - (void)type; - NotImplemented_nid_no_patch(__func__); - return 0; + return toPosix(scePthreadMutexattrSettype(attr, type)); } -} +} \ No newline at end of file diff --git a/core/libs/prx/libkernel/Pthread/include/Mutex.hpp b/core/libs/prx/libkernel/Pthread/include/Mutex.hpp new file mode 100644 index 000000000..ca734c603 --- /dev/null +++ b/core/libs/prx/libkernel/Pthread/include/Mutex.hpp @@ -0,0 +1,26 @@ +#ifndef CORE_LIBS_PRX_LIBKERNEL_PTHREAD_MUTEX_HPP +#define CORE_LIBS_PRX_LIBKERNEL_PTHREAD_MUTEX_HPP + +#include "SceTypes.hpp" + +class MutexOperations { +public: + static int Timedlock(PthreadMutex* mutex, const KernelTimespec* abstime); +}; + +extern "C" { + +int APS5_VABI scePthreadMutexattrInit(PthreadMutexattr* attr); +int APS5_VABI scePthreadMutexattrDestroy(PthreadMutexattr* attr); +int APS5_VABI scePthreadMutexattrSettype(PthreadMutexattr* attr, int type); +int APS5_VABI scePthreadMutexattrSetprotocol(PthreadMutexattr* attr, int protocol); +int APS5_VABI scePthreadMutexInit(PthreadMutex* mutex, const PthreadMutexattr* attr, const char* name); +int APS5_VABI scePthreadMutexDestroy(PthreadMutex* mutex); +int APS5_VABI scePthreadMutexLock(PthreadMutex* mutex); +int APS5_VABI scePthreadMutexUnlock(PthreadMutex* mutex); +int APS5_VABI scePthreadMutexTimedlock(PthreadMutex* mutex, KernelUseconds usec); +int APS5_VABI scePthreadMutexTrylock(PthreadMutex* mutex); + +} + +#endif diff --git a/core/libs/prx/libkernel/Pthread/include/Pthread.hpp b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp index e0adf92b8..b9b503a1b 100644 --- a/core/libs/prx/libkernel/Pthread/include/Pthread.hpp +++ b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp @@ -28,7 +28,7 @@ struct PthreadMutexPrivate { std::atomic _owner; int _count; - PthreadMutexPrivate() : _type(MutexType::Normal), _count(0) {} + PthreadMutexPrivate() : _type(MutexType::Normal), _owner(std::thread::id{}), _count(0) {} }; struct PthreadCondattrPrivate { diff --git a/core/libs/prx/libkernel/Pthread/src/Mutex.cpp b/core/libs/prx/libkernel/Pthread/src/Mutex.cpp index cc9ac1881..ccf895382 100644 --- a/core/libs/prx/libkernel/Pthread/src/Mutex.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Mutex.cpp @@ -1,113 +1,164 @@ #include "../include/Pthread.hpp" -#include "prx/libc/include/General.hpp" -#include +#include "../include/Mutex.hpp" #include +#include +#include #include -static constexpr int SCE_OK = 0; -static constexpr int SCE_KERNEL_ERROR_ENOMEM = 0x8002000C; -static constexpr int SCE_KERNEL_ERROR_EDEADLK = 0x80020023; -static constexpr int SCE_KERNEL_ERROR_EPERM = 0x80020001; +namespace { + +constexpr int sceBusy = static_cast(0x80020010u); +constexpr int sceTimedOut = static_cast(0x8002003cu); +std::mutex initializationMutex; + +PthreadMutex destroyedMutex() { + return reinterpret_cast(std::uintptr_t{2}); +} + +PthreadMutex resolveMutex(PthreadMutex* mutex, bool initialize) { + if (!mutex) + throw std::invalid_argument("Mutex pointer is null"); + std::lock_guard lock(initializationMutex); + if (*mutex == destroyedMutex()) + throw std::runtime_error("Mutex has been destroyed"); + if (reinterpret_cast(*mutex) == 1) + throw std::runtime_error("Adaptive mutex initializer is unsupported"); + if (!*mutex) { + if (!initialize) + throw std::runtime_error("Mutex is not initialized"); + *mutex = new PthreadMutexPrivate(); + } + return *mutex; +} + +template +int acquireMutex(PthreadMutex mutex, TAcquire acquire, int unavailable, bool tryOnly) { + const auto thread = std::this_thread::get_id(); + const bool owned = mutex->_owner.load(std::memory_order_acquire) == thread; + if (owned && mutex->_type != MutexType::Recursive) { + if (tryOnly) + return sceBusy; + throw std::runtime_error("Mutex is already owned by the current thread"); + } + if (owned && mutex->_count == std::numeric_limits::max()) + throw std::overflow_error("Recursive mutex lock count overflow"); + if (mutex->_type == MutexType::Recursive) { + if (!acquire(mutex->_rmtx)) + return unavailable; + ++mutex->_count; + } else { + if (!acquire(mutex->_mtx)) + return unavailable; + } + mutex->_owner.store(thread, std::memory_order_release); + return 0; +} + +} + +int MutexOperations::Timedlock(PthreadMutex* mutex, const KernelTimespec* abstime) { + if (!abstime || abstime->tv_sec < 0 || abstime->tv_nsec < 0 || abstime->tv_nsec >= 1000000000) + throw std::invalid_argument("Invalid absolute mutex timeout"); + const auto maximum = std::chrono::nanoseconds::max().count(); + if (abstime->tv_sec > (maximum - abstime->tv_nsec) / 1000000000) + throw std::overflow_error("Absolute mutex timeout exceeds the host clock range"); + const auto duration = std::chrono::nanoseconds(abstime->tv_sec * 1000000000 + abstime->tv_nsec); + if (std::chrono::duration(duration) >= std::chrono::duration(std::chrono::system_clock::duration::max())) + throw std::overflow_error("Absolute mutex timeout exceeds the host clock range"); + const auto deadline = std::chrono::system_clock::time_point(std::chrono::duration_cast(duration)); + return acquireMutex(resolveMutex(mutex, true), [&](auto& native) { return native.try_lock_until(deadline); }, sceTimedOut, false); +} extern "C" { int APS5_VABI scePthreadMutexattrInit(PthreadMutexattr* attr) { - if (!attr) throw std::runtime_error("scePthreadMutexattrInit: null attr"); - auto* p = new (std::nothrow) PthreadMutexattrPrivate{MutexType::Normal}; - if (!p) return SCE_KERNEL_ERROR_ENOMEM; - *attr = p; - return SCE_OK; + if (!attr) + throw std::invalid_argument("Mutex attribute pointer is null"); + *attr = new PthreadMutexattrPrivate{MutexType::Normal}; + return 0; } int APS5_VABI scePthreadMutexattrDestroy(PthreadMutexattr* attr) { - if (!attr || !*attr) throw std::runtime_error("scePthreadMutexattrDestroy: null attr"); + if (!attr || !*attr) + throw std::invalid_argument("Mutex attributes are not initialized"); delete *attr; *attr = nullptr; - return SCE_OK; + return 0; } int APS5_VABI scePthreadMutexattrSettype(PthreadMutexattr* attr, int type) { - if (!attr || !*attr) throw std::runtime_error("scePthreadMutexattrSettype: null attr"); + if (!attr || !*attr) + throw std::invalid_argument("Mutex attributes are not initialized"); switch (type) { case 1: (*attr)->type = MutexType::ErrorCheck; break; case 2: (*attr)->type = MutexType::Recursive; break; case 3: (*attr)->type = MutexType::Normal; break; - default: throw std::runtime_error("scePthreadMutexattrSettype: invalid type"); + default: throw std::invalid_argument("Invalid mutex type"); } - return SCE_OK; -} - -int APS5_VABI scePthreadMutexInit(PthreadMutex* mutex, const PthreadMutexattr* attr, const char*) { - if (!mutex) throw std::runtime_error("scePthreadMutexInit: null mutex"); - MutexType t = MutexType::Normal; - if (attr && *attr) t = (*attr)->type; - auto* p = new (std::nothrow) PthreadMutexPrivate(); - if (!p) return SCE_KERNEL_ERROR_ENOMEM; - p->_type = t; - *mutex = p; - return SCE_OK; -} - -int APS5_VABI scePthreadMutexDestroy(PthreadMutex* mutex) { - if (!mutex || !*mutex) throw std::runtime_error("scePthreadMutexDestroy: null mutex"); - delete *mutex; - *mutex = nullptr; - return SCE_OK; -} - -int APS5_VABI scePthreadMutexLock(PthreadMutex* mutex) { - if (!mutex || !*mutex) throw std::runtime_error("scePthreadMutexLock: null mutex"); - auto* m = *mutex; - const auto tid = std::this_thread::get_id(); - if (m->_type == MutexType::Recursive) { - m->_rmtx.lock(); - m->_owner.store(tid, std::memory_order_relaxed); - ++m->_count; - return SCE_OK; - } - if (m->_type == MutexType::ErrorCheck) { - if (m->_owner.load(std::memory_order_acquire) == tid) return SCE_KERNEL_ERROR_EDEADLK; - } - m->_mtx.lock(); - m->_owner.store(tid, std::memory_order_relaxed); - return SCE_OK; -} - -int APS5_VABI scePthreadMutexUnlock(PthreadMutex* mutex) { - if (!mutex || !*mutex) throw std::runtime_error("scePthreadMutexUnlock: null mutex"); - auto* m = *mutex; - if (m->_type == MutexType::ErrorCheck || m->_type == MutexType::Normal) { - if (m->_owner.load(std::memory_order_acquire) != std::this_thread::get_id()) - return SCE_KERNEL_ERROR_EPERM; - } - if (m->_type == MutexType::Recursive) { - if (--m->_count == 0) m->_owner.store(std::thread::id{}, std::memory_order_relaxed); - m->_rmtx.unlock(); - return SCE_OK; - } - m->_owner.store(std::thread::id{}, std::memory_order_relaxed); - m->_mtx.unlock(); - return SCE_OK; -} - -int APS5_VABI scePthreadMutexTimedlock(PthreadMutex* mutex, KernelUseconds usec) { - (void)mutex; - (void)usec; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI scePthreadMutexTrylock(PthreadMutex* mutex) { - (void)mutex; - NotImplemented_nid_no_patch(__func__); - return 0; + return 0; } int APS5_VABI scePthreadMutexattrSetprotocol(PthreadMutexattr* attr, int protocol) { - (void)attr; - (void)protocol; - NotImplemented_nid_no_patch(__func__); - return 0; + if (!attr || !*attr) + throw std::invalid_argument("Mutex attributes are not initialized"); + if (protocol != 0) + throw std::invalid_argument("Mutex priority inheritance and protection are unsupported"); + return 0; +} + +int APS5_VABI scePthreadMutexInit(PthreadMutex* mutex, const PthreadMutexattr* attr, const char*) { + if (!mutex) + throw std::invalid_argument("Mutex pointer is null"); + if (attr && !*attr) + throw std::invalid_argument("Mutex attributes are not initialized"); + auto replacement = std::make_unique(); + if (attr) + replacement->_type = (*attr)->type; + std::lock_guard lock(initializationMutex); + *mutex = replacement.release(); + return 0; +} + +int APS5_VABI scePthreadMutexDestroy(PthreadMutex* mutex) { + if (!mutex) + throw std::invalid_argument("Mutex pointer is null"); + std::lock_guard lock(initializationMutex); + if (*mutex == destroyedMutex()) + throw std::runtime_error("Mutex has already been destroyed"); + if (reinterpret_cast(*mutex) == 1) + throw std::runtime_error("Adaptive mutex initializer is unsupported"); + if (*mutex && (*mutex)->_owner.load(std::memory_order_acquire) != std::thread::id{}) + throw std::runtime_error("Cannot destroy a locked mutex"); + delete *mutex; + *mutex = destroyedMutex(); + return 0; +} + +int APS5_VABI scePthreadMutexLock(PthreadMutex* mutex) { + return acquireMutex(resolveMutex(mutex, true), [](auto& native) { native.lock(); return true; }, 0, false); +} + +int APS5_VABI scePthreadMutexUnlock(PthreadMutex* mutex) { + auto* current = resolveMutex(mutex, false); + if (current->_owner.load(std::memory_order_acquire) != std::this_thread::get_id()) + throw std::runtime_error("Cannot unlock a mutex owned by another thread"); + if (current->_type == MutexType::Recursive) { + if (--current->_count == 0) + current->_owner.store(std::thread::id{}, std::memory_order_release); + current->_rmtx.unlock(); + } else { + current->_owner.store(std::thread::id{}, std::memory_order_release); + current->_mtx.unlock(); + } + return 0; +} + +int APS5_VABI scePthreadMutexTimedlock(PthreadMutex* mutex, KernelUseconds usec) { + return acquireMutex(resolveMutex(mutex, true), [=](auto& native) { return native.try_lock_for(std::chrono::microseconds(usec)); }, sceTimedOut, false); +} + +int APS5_VABI scePthreadMutexTrylock(PthreadMutex* mutex) { + return acquireMutex(resolveMutex(mutex, true), [](auto& native) { return native.try_lock(); }, sceBusy, true); } } From 62c16792ff74a787f0cdd4f0cb97a3791c7a9381 Mon Sep 17 00:00:00 2001 From: boykopovar Date: Sat, 26 Sep 2026 18:16:47 +0300 Subject: [PATCH 81/94] feat(libkernel): impl pthread condition variables + posix thread attribute queries --- .../libs/prx/libkernel/Pthread/Posix/Cond.cpp | 55 +++--- .../prx/libkernel/Pthread/include/Cond.hpp | 26 +++ core/libs/prx/libkernel/Pthread/src/Attr.cpp | 83 ++++----- core/libs/prx/libkernel/Pthread/src/Cond.cpp | 172 ++++++++++++------ 4 files changed, 208 insertions(+), 128 deletions(-) create mode 100644 core/libs/prx/libkernel/Pthread/include/Cond.hpp diff --git a/core/libs/prx/libkernel/Pthread/Posix/Cond.cpp b/core/libs/prx/libkernel/Pthread/Posix/Cond.cpp index b860a2262..3c202b17d 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Cond.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Cond.cpp @@ -1,61 +1,52 @@ +#include "prx/libkernel/Pthread/include/Cond.hpp" #include -#include -#include "SceTypes.hpp" -#include "prx/libc/include/General.hpp" +#include + +namespace { + +int toPosix(int result) { + if (result == 0) + return 0; + const auto error = static_cast(result); + if ((error & 0xffff0000u) != 0x80020000u) + throw std::runtime_error("Unexpected SCE condition variable error"); + return static_cast(error & 0xffffu); +} + +} extern "C" { int APS5_VABI pthread_cond_broadcast_nid_postfix(PthreadCond* cond) { - (void)cond; - NotImplemented_nid_no_patch(__func__); - return 0; + return toPosix(scePthreadCondBroadcast(cond)); } int APS5_VABI pthread_cond_init_nid_postfix(PthreadCond* cond, const PthreadCondattr* attr) { - (void)cond; - (void)attr; - NotImplemented_nid_no_patch(__func__); - return 0; + return toPosix(scePthreadCondInit(cond, attr, nullptr)); } int APS5_VABI pthread_cond_signal_nid_postfix(PthreadCond* cond) { - (void)cond; - NotImplemented_nid_no_patch(__func__); - return 0; + return toPosix(scePthreadCondSignal(cond)); } int APS5_VABI pthread_cond_timedwait_nid_postfix(PthreadCond* cond, PthreadMutex* mutex, const KernelTimespec* abstime) { - (void)cond; - (void)mutex; - (void)abstime; - NotImplemented_nid_no_patch(__func__); - return 0; + return toPosix(CondOperations::AbsoluteTimedwait(cond, mutex, abstime)); } int APS5_VABI pthread_cond_wait_nid_postfix(PthreadCond* cond, PthreadMutex* mutex) { - (void)cond; - (void)mutex; - NotImplemented_nid_no_patch(__func__); - return 0; + return toPosix(scePthreadCondWait(cond, mutex)); } int APS5_VABI pthread_condattr_destroy_nid_postfix(PthreadCondattr* attr) { - (void)attr; - NotImplemented_nid_no_patch(__func__); - return 0; + return toPosix(scePthreadCondattrDestroy(attr)); } int APS5_VABI pthread_condattr_init_nid_postfix(PthreadCondattr* attr) { - (void)attr; - NotImplemented_nid_no_patch(__func__); - return 0; + return toPosix(scePthreadCondattrInit(attr)); } int APS5_VABI pthread_condattr_setclock_nid_postfix(PthreadCondattr* attr, KernelClockid clock_id) { - (void)attr; - (void)clock_id; - NotImplemented_nid_no_patch(__func__); - return 0; + return toPosix(scePthreadCondattrSetclock(attr, clock_id)); } } diff --git a/core/libs/prx/libkernel/Pthread/include/Cond.hpp b/core/libs/prx/libkernel/Pthread/include/Cond.hpp new file mode 100644 index 000000000..936b95714 --- /dev/null +++ b/core/libs/prx/libkernel/Pthread/include/Cond.hpp @@ -0,0 +1,26 @@ +#ifndef CORE_LIBS_PRX_LIBKERNEL_PTHREAD_COND_HPP +#define CORE_LIBS_PRX_LIBKERNEL_PTHREAD_COND_HPP + +#include "SceTypes.hpp" + +class CondOperations { +public: + static int AbsoluteTimedwait(PthreadCond* cond, PthreadMutex* mutex, const KernelTimespec* abstime); +}; + +extern "C" { + +int APS5_VABI scePthreadCondattrInit(PthreadCondattr* attr); +int APS5_VABI scePthreadCondattrDestroy(PthreadCondattr* attr); +int APS5_VABI scePthreadCondattrSetclock(PthreadCondattr* attr, KernelClockid clockId); +int APS5_VABI scePthreadCondInit(PthreadCond* cond, const PthreadCondattr* attr, const char* name); +int APS5_VABI scePthreadCondDestroy(PthreadCond* cond); +int APS5_VABI scePthreadCondSignal(PthreadCond* cond); +int APS5_VABI scePthreadCondBroadcast(PthreadCond* cond); +int APS5_VABI scePthreadCondSignalto(PthreadCond* cond, Pthread thread); +int APS5_VABI scePthreadCondWait(PthreadCond* cond, PthreadMutex* mutex); +int APS5_VABI scePthreadCondTimedwait(PthreadCond* cond, PthreadMutex* mutex, KernelUseconds usec); + +} + +#endif diff --git a/core/libs/prx/libkernel/Pthread/src/Attr.cpp b/core/libs/prx/libkernel/Pthread/src/Attr.cpp index 7f71adb80..d32525564 100644 --- a/core/libs/prx/libkernel/Pthread/src/Attr.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Attr.cpp @@ -1,8 +1,10 @@ -#include "../include/Pthread.hpp" +#include "prx/libkernel/Pthread/include/Pthread.hpp" #include "prx/libc/include/General.hpp" #include +#include static constexpr int SCE_OK = 0; +static constexpr int SCE_KERNEL_ERROR_EINVAL = 0x80020016; static constexpr int SCE_KERNEL_ERROR_ENOMEM = 0x8002000C; static constexpr std::size_t DEFAULT_STACK_SIZE = 1u << 20; @@ -18,7 +20,7 @@ static constexpr int SCHED_FIFO_PS5 = 1; extern "C" { int APS5_VABI scePthreadAttrInit(PthreadAttr* attr) { - if (!attr) throw std::runtime_error("scePthreadAttrInit: null attr"); + if (!attr) throw std::runtime_error(std::string(__func__) + ": null attr"); auto* p = new (std::nothrow) PthreadAttrPrivate{}; if (!p) return SCE_KERNEL_ERROR_ENOMEM; p->_stacksize = DEFAULT_STACK_SIZE; @@ -31,35 +33,35 @@ int APS5_VABI scePthreadAttrInit(PthreadAttr* attr) { } int APS5_VABI scePthreadAttrDestroy(PthreadAttr* attr) { - if (!attr || !*attr) throw std::runtime_error("scePthreadAttrDestroy: null attr"); + if (!attr || !*attr) throw std::runtime_error(std::string(__func__) + ": null attr"); delete *attr; *attr = nullptr; return SCE_OK; } int APS5_VABI scePthreadAttrSetdetachstate(PthreadAttr* attr, int detachstate) { - if (!attr || !*attr) throw std::runtime_error("scePthreadAttrSetdetachstate: null attr"); + if (!attr || !*attr) throw std::runtime_error(std::string(__func__) + ": null attr"); if (detachstate != DETACH_JOINABLE && detachstate != DETACH_DETACHED) - throw std::runtime_error("scePthreadAttrSetdetachstate: invalid state"); + throw std::runtime_error(std::string(__func__) + ": invalid state"); (*attr)->_detachstate = detachstate; return SCE_OK; } int APS5_VABI scePthreadAttrSetschedparam(PthreadAttr* attr, const KernelSchedParam* param) { if (!attr || !*attr || !param) - throw std::runtime_error("scePthreadAttrSetschedparam: null arg"); + throw std::runtime_error(std::string(__func__) + ": null arg"); (*attr)->_schedpriority = param->sched_priority; return SCE_OK; } int APS5_VABI scePthreadAttrSetstacksize(PthreadAttr* attr, std::size_t stacksize) { - if (!attr || !*attr) throw std::runtime_error("scePthreadAttrSetstacksize: null attr"); - if (stacksize < 16384) throw std::runtime_error("scePthreadAttrSetstacksize: too small"); + if (!attr || !*attr) throw std::runtime_error(std::string(__func__) + ": null attr"); + if (stacksize < 16384) throw std::runtime_error(std::string(__func__) + ": too small"); #ifdef _WIN32 SYSTEM_INFO system{}; GetSystemInfo(&system); if (stacksize % system.dwPageSize != 0 || stacksize > std::numeric_limits::max()) - throw std::runtime_error("scePthreadAttrSetstacksize: invalid Windows stack size"); + throw std::runtime_error(std::string(__func__) + ": invalid Windows stack size"); #endif (*attr)->_stacksize = stacksize; return SCE_OK; @@ -67,7 +69,7 @@ int APS5_VABI scePthreadAttrSetstacksize(PthreadAttr* attr, std::size_t stacksiz int APS5_VABI scePthreadAttrGetstack(const PthreadAttr* attr, void** stackaddr, std::size_t* stacksize) { if (!attr || !*attr || !stackaddr || !stacksize) - throw std::runtime_error("scePthreadAttrGetstack: null arg"); + throw std::runtime_error(std::string(__func__) + ": null arg"); *stackaddr = (*attr)->stackAddress; *stacksize = (*attr)->_stacksize; return SCE_OK; @@ -75,7 +77,7 @@ int APS5_VABI scePthreadAttrGetstack(const PthreadAttr* attr, void** stackaddr, int APS5_VABI scePthreadAttrGet(Pthread thread, PthreadAttr* attr) { if (!thread || !attr || !*attr) - throw std::runtime_error("scePthreadAttrGet: null arg"); + return SCE_KERNEL_ERROR_EINVAL; (*attr)->_stacksize = thread->stackSize; (*attr)->stackAddress = thread->stackAddress; (*attr)->_detachstate = thread->_detached ? DETACH_DETACHED : DETACH_JOINABLE; @@ -93,10 +95,9 @@ int APS5_VABI scePthreadAttrGetaffinity(const PthreadAttr* attr, KernelCpumask* } int APS5_VABI scePthreadAttrGetdetachstate(const PthreadAttr* attr, int* state) { - (void)attr; - (void)state; - NotImplemented_nid_no_patch(__func__); - return 0; + if (!attr || !*attr || !state) throw std::runtime_error(std::string(__func__) + ": null arg"); + *state = (*attr)->_detachstate; + return SCE_OK; } int APS5_VABI scePthreadAttrGetguardsize(const PthreadAttr* attr, size_t* guard_size) { @@ -107,10 +108,9 @@ int APS5_VABI scePthreadAttrGetguardsize(const PthreadAttr* attr, size_t* guard_ } int APS5_VABI scePthreadAttrGetschedparam(const PthreadAttr* attr, KernelSchedParam* param) { - (void)attr; - (void)param; - NotImplemented_nid_no_patch(__func__); - return 0; + if (!attr || !*attr || !param) throw std::runtime_error(std::string(__func__) + ": null arg"); + param->sched_priority = (*attr)->_schedpriority; + return SCE_OK; } int APS5_VABI scePthreadAttrGetsolosched(const PthreadAttr* attr, int* solosched) { @@ -121,17 +121,15 @@ int APS5_VABI scePthreadAttrGetsolosched(const PthreadAttr* attr, int* solosched } int APS5_VABI scePthreadAttrGetstackaddr(const PthreadAttr* attr, void** stack_addr) { - (void)attr; - (void)stack_addr; - NotImplemented_nid_no_patch(__func__); - return 0; + if (!attr || !*attr || !stack_addr) throw std::runtime_error(std::string(__func__) + ": null arg"); + *stack_addr = (*attr)->stackAddress; + return SCE_OK; } int APS5_VABI scePthreadAttrGetstacksize(const PthreadAttr* attr, size_t* stack_size) { - (void)attr; - (void)stack_size; - NotImplemented_nid_no_patch(__func__); - return 0; + if (!attr || !*attr || !stack_size) throw std::runtime_error(std::string(__func__) + ": null arg"); + *stack_size = (*attr)->_stacksize; + return SCE_OK; } int APS5_VABI scePthreadAttrSetaffinity(PthreadAttr* attr, KernelCpumask mask) { @@ -149,17 +147,15 @@ int APS5_VABI scePthreadAttrSetguardsize(PthreadAttr* attr, size_t guard_size) { } int APS5_VABI scePthreadAttrSetinheritsched(PthreadAttr* attr, int inherit_sched) { - (void)attr; - (void)inherit_sched; - NotImplemented_nid_no_patch(__func__); - return 0; + if (!attr || !*attr) throw std::runtime_error(std::string(__func__) + ": null attr"); + (*attr)->_inheritsched = inherit_sched; + return SCE_OK; } int APS5_VABI scePthreadAttrSetschedpolicy(PthreadAttr* attr, int policy) { - (void)attr; - (void)policy; - NotImplemented_nid_no_patch(__func__); - return 0; + if (!attr || !*attr) throw std::runtime_error(std::string(__func__) + ": null attr"); + (*attr)->_schedpolicy = policy; + return SCE_OK; } int APS5_VABI scePthreadAttrSetsolosched(PthreadAttr* attr, int solosched) { @@ -170,18 +166,17 @@ int APS5_VABI scePthreadAttrSetsolosched(PthreadAttr* attr, int solosched) { } int APS5_VABI scePthreadAttrSetstack(PthreadAttr* attr, void* addr, size_t size) { - (void)attr; - (void)addr; - (void)size; - NotImplemented_nid_no_patch(__func__); - return 0; + if (!attr || !*attr || !addr) throw std::runtime_error(std::string(__func__) + ": null arg"); + if (size < 16384) throw std::runtime_error(std::string(__func__) + ": too small"); + (*attr)->stackAddress = addr; + (*attr)->_stacksize = size; + return SCE_OK; } int APS5_VABI scePthreadAttrSetstackaddr(PthreadAttr* attr, void* addr) { - (void)attr; - (void)addr; - NotImplemented_nid_no_patch(__func__); - return 0; + if (!attr || !*attr || !addr) throw std::runtime_error(std::string(__func__) + ": null arg"); + (*attr)->stackAddress = addr; + return SCE_OK; } } diff --git a/core/libs/prx/libkernel/Pthread/src/Cond.cpp b/core/libs/prx/libkernel/Pthread/src/Cond.cpp index 754a076f3..93c2902fe 100644 --- a/core/libs/prx/libkernel/Pthread/src/Cond.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Cond.cpp @@ -1,85 +1,153 @@ -#include "../include/Pthread.hpp" -#include "prx/libc/include/General.hpp" +#include "prx/libkernel/Pthread/include/Pthread.hpp" +#include "prx/libkernel/Pthread/include/Mutex.hpp" +#include "prx/libkernel/Pthread/include/Cond.hpp" #include +#include #include -static constexpr int SCE_OK = 0; -static constexpr int SCE_KERNEL_ERROR_ENOMEM = 0x8002000C; -static constexpr int SCE_KERNEL_ERROR_ETIMEDOUT = 0x80020062; +namespace { + +constexpr int sceTimedOut = static_cast(0x8002003cu); +std::mutex condInitializationMutex; + +PthreadCond destroyedCond() { + return reinterpret_cast(std::uintptr_t{2}); +} + +PthreadCond resolveCond(PthreadCond* cond) { + if (!cond) + throw std::invalid_argument("Condition variable pointer is null"); + std::lock_guard lock(condInitializationMutex); + if (*cond == destroyedCond()) + throw std::runtime_error("Condition variable has been destroyed"); + if (!*cond) + *cond = new PthreadCondPrivate(); + return *cond; +} + +PthreadMutex lockedMutex(PthreadMutex* mutex) { + if (!mutex || !*mutex) + throw std::invalid_argument("Mutex pointer is null"); + auto* current = *mutex; + if (current->_owner.load(std::memory_order_acquire) != std::this_thread::get_id()) + throw std::runtime_error("Condition wait mutex is not owned by the current thread"); + return current; +} + +int waitUntil(PthreadCond* cond, PthreadMutex* mutex, const std::chrono::system_clock::time_point* deadline) { + auto* c = resolveCond(cond); + auto* m = lockedMutex(mutex); + if (m->_type == MutexType::Recursive) { + std::unique_lock lock(m->_rmtx, std::adopt_lock); + const auto previousCount = m->_count; + m->_count = 0; + m->_owner.store(std::thread::id{}, std::memory_order_release); + const bool timedOut = deadline && c->_cv.wait_until(lock, *deadline) == std::cv_status::timeout; + if (!deadline) + c->_cv.wait(lock); + m->_owner.store(std::this_thread::get_id(), std::memory_order_release); + m->_count = previousCount; + lock.release(); + return timedOut ? sceTimedOut : 0; + } + std::unique_lock lock(m->_mtx, std::adopt_lock); + m->_owner.store(std::thread::id{}, std::memory_order_release); + const bool timedOut = deadline && c->_cv.wait_until(lock, *deadline) == std::cv_status::timeout; + if (!deadline) + c->_cv.wait(lock); + m->_owner.store(std::this_thread::get_id(), std::memory_order_release); + lock.release(); + return timedOut ? sceTimedOut : 0; +} + +std::chrono::system_clock::time_point toDeadline(const KernelTimespec* abstime) { + if (!abstime || abstime->tv_sec < 0 || abstime->tv_nsec < 0 || abstime->tv_nsec >= 1000000000) + throw std::invalid_argument("Invalid absolute condition variable timeout"); + const auto maximum = std::chrono::nanoseconds::max().count(); + if (abstime->tv_sec > (maximum - abstime->tv_nsec) / 1000000000) + throw std::overflow_error("Absolute condition variable timeout exceeds the host clock range"); + const auto duration = std::chrono::nanoseconds(abstime->tv_sec * 1000000000 + abstime->tv_nsec); + if (std::chrono::duration(duration) >= std::chrono::duration(std::chrono::system_clock::duration::max())) + throw std::overflow_error("Absolute condition variable timeout exceeds the host clock range"); + return std::chrono::system_clock::time_point(std::chrono::duration_cast(duration)); +} + +} + +int CondOperations::AbsoluteTimedwait(PthreadCond* cond, PthreadMutex* mutex, const KernelTimespec* abstime) { + const auto deadline = toDeadline(abstime); + return waitUntil(cond, mutex, &deadline); +} extern "C" { int APS5_VABI scePthreadCondattrInit(PthreadCondattr* attr) { - if (!attr) throw std::runtime_error("scePthreadCondattrInit: null attr"); - auto* p = new (std::nothrow) PthreadCondattrPrivate{0}; - if (!p) return SCE_KERNEL_ERROR_ENOMEM; - *attr = p; - return SCE_OK; + if (!attr) + throw std::invalid_argument("Condition attribute pointer is null"); + *attr = new PthreadCondattrPrivate{0}; + return 0; } int APS5_VABI scePthreadCondattrDestroy(PthreadCondattr* attr) { - if (!attr || !*attr) throw std::runtime_error("scePthreadCondattrDestroy: null attr"); + if (!attr || !*attr) + throw std::invalid_argument("Condition attributes are not initialized"); delete *attr; *attr = nullptr; - return SCE_OK; + return 0; } -int APS5_VABI scePthreadCondInit(PthreadCond* cond, const PthreadCondattr*, const char*) { - if (!cond) throw std::runtime_error("scePthreadCondInit: null cond"); - auto* p = new (std::nothrow) PthreadCondPrivate{}; - if (!p) return SCE_KERNEL_ERROR_ENOMEM; - *cond = p; - return SCE_OK; +int APS5_VABI scePthreadCondattrSetclock(PthreadCondattr* attr, KernelClockid clockId) { + if (!attr || !*attr) + throw std::invalid_argument("Condition attributes are not initialized"); + (*attr)->_clockid = static_cast(clockId); + return 0; +} + +int APS5_VABI scePthreadCondInit(PthreadCond* cond, const PthreadCondattr* attr, const char*) { + if (!cond) + throw std::invalid_argument("Condition variable pointer is null"); + if (attr && !*attr) + throw std::invalid_argument("Condition attributes are not initialized"); + auto replacement = std::make_unique(); + std::lock_guard lock(condInitializationMutex); + *cond = replacement.release(); + return 0; } int APS5_VABI scePthreadCondDestroy(PthreadCond* cond) { - if (!cond || !*cond) throw std::runtime_error("scePthreadCondDestroy: null cond"); + if (!cond) + throw std::invalid_argument("Condition variable pointer is null"); + std::lock_guard lock(condInitializationMutex); + if (*cond == destroyedCond()) + throw std::runtime_error("Condition variable has already been destroyed"); delete *cond; - *cond = nullptr; - return SCE_OK; + *cond = destroyedCond(); + return 0; } int APS5_VABI scePthreadCondSignal(PthreadCond* cond) { - if (!cond || !*cond) throw std::runtime_error("scePthreadCondSignal: null cond"); - (*cond)->_cv.notify_one(); - return SCE_OK; + resolveCond(cond)->_cv.notify_one(); + return 0; } int APS5_VABI scePthreadCondBroadcast(PthreadCond* cond) { - if (!cond || !*cond) throw std::runtime_error("scePthreadCondBroadcast: null cond"); - (*cond)->_cv.notify_all(); - return SCE_OK; -} - -int APS5_VABI scePthreadCondTimedwait(PthreadCond* cond, PthreadMutex* mutex, unsigned int usec) { - if (!cond || !*cond || !mutex || !*mutex) - throw std::runtime_error("scePthreadCondTimedwait: null arg"); - auto* m = *mutex; - auto* c = *cond; - if (m->_type == MutexType::Recursive) { - std::unique_lock lk(m->_rmtx, std::adopt_lock); - auto res = c->_cv.wait_for(lk, std::chrono::microseconds(usec)); - lk.release(); - return res == std::cv_status::timeout ? SCE_KERNEL_ERROR_ETIMEDOUT : SCE_OK; - } - std::unique_lock lk(m->_mtx, std::adopt_lock); - auto res = c->_cv.wait_for(lk, std::chrono::microseconds(usec)); - lk.release(); - return res == std::cv_status::timeout ? SCE_KERNEL_ERROR_ETIMEDOUT : SCE_OK; + resolveCond(cond)->_cv.notify_all(); + return 0; } int APS5_VABI scePthreadCondSignalto(PthreadCond* cond, Pthread thread) { - (void)cond; - (void)thread; - NotImplemented_nid_no_patch(__func__); - return 0; + (void)thread; + resolveCond(cond)->_cv.notify_all(); + return 0; } int APS5_VABI scePthreadCondWait(PthreadCond* cond, PthreadMutex* mutex) { - (void)cond; - (void)mutex; - NotImplemented_nid_no_patch(__func__); - return 0; + return waitUntil(cond, mutex, nullptr); +} + +int APS5_VABI scePthreadCondTimedwait(PthreadCond* cond, PthreadMutex* mutex, KernelUseconds usec) { + const auto deadline = std::chrono::system_clock::now() + std::chrono::microseconds(usec); + return waitUntil(cond, mutex, &deadline); } } From 13c648bebbdcdc501b4b67ed01670e84647cf91d Mon Sep 17 00:00:00 2001 From: boykopovar Date: Sat, 26 Sep 2026 18:35:20 +0300 Subject: [PATCH 82/94] feat(libkernel): impl file, time, process, signal for posix --- core/libs/prx/libkernel/CMakeLists.txt | 1 - core/libs/prx/libkernel/File/src/Stdio.cpp | 61 +++++++++++- .../prx/libkernel/Pthread/Posix/Rwlock.cpp | 22 +++++ .../prx/libkernel/Pthread/include/Rwlock.hpp | 21 ++++ .../libs/prx/libkernel/Pthread/src/Rwlock.cpp | 1 + .../libkernel/System/src/PosixImportStubs.cpp | 91 ------------------ .../libs/prx/libkernel/System/src/Process.cpp | 95 +++++++++++++++++++ .../libs/prx/libkernel/System/src/Signals.cpp | 26 +++++ core/libs/prx/libkernel/Time/Time.cpp | 4 + 9 files changed, 227 insertions(+), 95 deletions(-) create mode 100644 core/libs/prx/libkernel/Pthread/include/Rwlock.hpp delete mode 100644 core/libs/prx/libkernel/System/src/PosixImportStubs.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 965749d04..394bad513 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -39,7 +39,6 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/System/src/Sanitizer.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/Rtld.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/Process.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/System/src/PosixImportStubs.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/SystemConfiguration.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/Signals.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Time/Time.cpp diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 81704d09d..c0cefcd8b 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -2,11 +2,23 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libkernel/File/include/File.hpp" +#include "prx/libkernel/File/include/FileFlags.hpp" #include "prx/libkernel/Socket/include/SocketRuntime.hpp" +#include +#include +#include #ifdef _WIN32 #include +#include +static int NativeRmdir(const std::filesystem::path& path) { + return ::_wrmdir(path.wstring().c_str()); +} #else #include +static int NativeRmdir(const std::filesystem::path& path) { + return ::rmdir(path.c_str()); +} #endif extern "C" { @@ -27,6 +39,10 @@ int APS5_VABI close_nid_postfix(int d) { #endif } +int APS5_VABI _close_nid_postfix(int descriptor) { + return close_nid_postfix(descriptor); +} + int APS5_VABI flock_nid_postfix(int d, int operation) { (void)d; (void)operation; @@ -71,6 +87,24 @@ int APS5_VABI open_nid_postfix(const char* path, int flags, int mode) { return 0; } +int APS5_VABI _open_nid_postfix(const char* path, int flags, ...) { + std::uint16_t mode = 0; + if (flags & SCE_KERNEL_O_CREAT) { +#ifdef _WIN32 + __builtin_sysv_va_list arguments; + __builtin_sysv_va_start(arguments, flags); + mode = static_cast(__builtin_va_arg(arguments, int)); + __builtin_sysv_va_end(arguments); +#else + std::va_list arguments; + va_start(arguments, flags); + mode = static_cast(va_arg(arguments, int)); + va_end(arguments); +#endif + } + return sceKernelOpen(path, flags, mode); +} + int64_t APS5_VABI pread_nid_postfix(int d, void* buf, size_t nbytes, int64_t offset) { (void)d; (void)buf; @@ -97,6 +131,10 @@ int64_t APS5_VABI read_nid_postfix(int d, void* buf, uint64_t nbytes) { return 0; } +std::int64_t APS5_VABI _read_nid_postfix(int descriptor, void* buffer, std::size_t count) { + return sceKernelRead(descriptor, buffer, count); +} + int64_t APS5_VABI write_nid_postfix(int d, const char* str, int64_t size) { (void)d; (void)str; @@ -105,6 +143,10 @@ int64_t APS5_VABI write_nid_postfix(int d, const char* str, int64_t size) { return 0; } +std::int64_t APS5_VABI _write_nid_postfix(int descriptor, const void* buffer, std::size_t count) { + return sceKernelWrite(descriptor, buffer, count); +} + int APS5_VABI stat_nid_postfix(const char* path, FileStat* sb) { (void)path; (void)sb; @@ -112,6 +154,10 @@ int APS5_VABI stat_nid_postfix(const char* path, FileStat* sb) { return 0; } +int APS5_VABI unlink_nid_postfix(const char* path) { + return sceKernelUnlink(path); +} + int APS5_VABI sceKernelCheckReachability(const char* path) { (void)path; NotImplemented_nid_no_patch(__func__); @@ -181,9 +227,18 @@ int APS5_VABI sceKernelRename(const char* from, const char* to) { } int APS5_VABI sceKernelRmdir(const char* path) { - (void)path; - NotImplemented_nid_no_patch(__func__); - return 0; + if (path == nullptr) { + throw std::invalid_argument(std::string(__func__) + ": path is null"); + } + auto native = ResolvePath_nid_no_patch(path); + if (NativeRmdir(native) != 0) { + throw std::runtime_error(std::string(__func__) + ": rmdir failed for " + native.string() + ", errno=" + std::to_string(errno)); + } + return 0; +} + +int APS5_VABI rmdir_nid_postfix(const char* path) { + return sceKernelRmdir(path); } } diff --git a/core/libs/prx/libkernel/Pthread/Posix/Rwlock.cpp b/core/libs/prx/libkernel/Pthread/Posix/Rwlock.cpp index 2f1ee6a98..52f67399e 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Rwlock.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Rwlock.cpp @@ -2,6 +2,20 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "../include/Rwlock.hpp" + +namespace { + +int toPosix(int result) { + if (result == 0) + return 0; + const auto error = static_cast(result); + if ((error & 0xffff0000u) != 0x80020000u) + throw std::runtime_error("Unexpected SCE rwlock error"); + return static_cast(error & 0xffffu); +} + +} extern "C" { @@ -18,6 +32,14 @@ int APS5_VABI pthread_rwlock_init_nid_postfix(PthreadRwlock* rwlock, const Pthre return 0; } +int APS5_VABI pthread_rwlock_rdlock_nid_postfix(PthreadRwlock* rwlock) { + return toPosix(scePthreadRwlockRdlock(rwlock)); +} + +int APS5_VABI pthread_rwlock_unlock_nid_postfix(PthreadRwlock* rwlock) { + return toPosix(scePthreadRwlockUnlock(rwlock)); +} + int APS5_VABI pthread_rwlock_wrlock_nid_postfix(PthreadRwlock* rwlock) { (void)rwlock; NotImplemented_nid_no_patch(__func__); diff --git a/core/libs/prx/libkernel/Pthread/include/Rwlock.hpp b/core/libs/prx/libkernel/Pthread/include/Rwlock.hpp new file mode 100644 index 000000000..0c9668003 --- /dev/null +++ b/core/libs/prx/libkernel/Pthread/include/Rwlock.hpp @@ -0,0 +1,21 @@ +#ifndef CORE_LIBS_PRX_LIBKERNEL_PTHREAD_RWLOCK_HPP +#define CORE_LIBS_PRX_LIBKERNEL_PTHREAD_RWLOCK_HPP + +#include "SceTypes.hpp" + +extern "C" { + +int APS5_VABI scePthreadRwlockattrInit(PthreadRwlockattr* attr); +int APS5_VABI scePthreadRwlockattrDestroy(PthreadRwlockattr* attr); +int APS5_VABI scePthreadRwlockattrSettype(PthreadRwlockattr* attr, int type); +int APS5_VABI scePthreadRwlockInit(PthreadRwlock* rwlock, const PthreadRwlockattr* attr, const char* name); +int APS5_VABI scePthreadRwlockDestroy(PthreadRwlock* rwlock); +int APS5_VABI scePthreadRwlockRdlock(PthreadRwlock* rwlock); +int APS5_VABI scePthreadRwlockTryrdlock(PthreadRwlock* rwlock); +int APS5_VABI scePthreadRwlockWrlock(PthreadRwlock* rwlock); +int APS5_VABI scePthreadRwlockTrywrlock(PthreadRwlock* rwlock); +int APS5_VABI scePthreadRwlockUnlock(PthreadRwlock* rwlock); + +} + +#endif diff --git a/core/libs/prx/libkernel/Pthread/src/Rwlock.cpp b/core/libs/prx/libkernel/Pthread/src/Rwlock.cpp index 12a6040a2..4804e2dac 100644 --- a/core/libs/prx/libkernel/Pthread/src/Rwlock.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Rwlock.cpp @@ -1,4 +1,5 @@ #include "../include/Pthread.hpp" +#include "../include/Rwlock.hpp" #include "prx/libc/include/General.hpp" extern "C" { diff --git a/core/libs/prx/libkernel/System/src/PosixImportStubs.cpp b/core/libs/prx/libkernel/System/src/PosixImportStubs.cpp deleted file mode 100644 index bfb5cd145..000000000 --- a/core/libs/prx/libkernel/System/src/PosixImportStubs.cpp +++ /dev/null @@ -1,91 +0,0 @@ -#include "SceTypes.hpp" -#include "prx/libc/include/General.hpp" -#include -#include - -struct GuestSignalSet; -struct GuestResourceUsage; - -extern "C" { - -int APS5_VABI _close_nid_postfix(int descriptor) { - (void)descriptor; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -void APS5_VABI _exit_nid_postfix(int status) { - (void)status; - NotImplemented_nid_no_patch(__func__); -} - -int APS5_VABI _nanosleep_nid_postfix(const KernelTimespec* requested, KernelTimespec* remaining) { - (void)requested; - (void)remaining; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _open_nid_postfix(const char* path, int flags, ...) { - (void)path; - (void)flags; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -std::int64_t APS5_VABI _read_nid_postfix(int descriptor, void* buffer, std::size_t count) { - (void)descriptor; - (void)buffer; - (void)count; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _sigprocmask_nid_postfix(int how, const GuestSignalSet* set, GuestSignalSet* previousSet) { - (void)how; - (void)set; - (void)previousSet; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -std::int64_t APS5_VABI _write_nid_postfix(int descriptor, const void* buffer, std::size_t count) { - (void)descriptor; - (void)buffer; - (void)count; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI getrusage_nid_postfix(int who, GuestResourceUsage* usage) { - (void)who; - (void)usage; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI pthread_rwlock_rdlock_nid_postfix(PthreadRwlock* rwlock) { - (void)rwlock; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI pthread_rwlock_unlock_nid_postfix(PthreadRwlock* rwlock) { - (void)rwlock; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI rmdir_nid_postfix(const char* path) { - (void)path; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI unlink_nid_postfix(const char* path) { - (void)path; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -} diff --git a/core/libs/prx/libkernel/System/src/Process.cpp b/core/libs/prx/libkernel/System/src/Process.cpp index 4fa1e7ea9..7fe1ad6bb 100644 --- a/core/libs/prx/libkernel/System/src/Process.cpp +++ b/core/libs/prx/libkernel/System/src/Process.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -26,6 +27,9 @@ #include #include #include +#ifndef _WIN32 +#include +#endif namespace { @@ -116,6 +120,25 @@ void syncVolumes() { } +struct GuestResourceUsage { + KernelTimeval ru_utime; + KernelTimeval ru_stime; + std::int64_t ru_maxrss; + std::int64_t ru_ixrss; + std::int64_t ru_idrss; + std::int64_t ru_isrss; + std::int64_t ru_minflt; + std::int64_t ru_majflt; + std::int64_t ru_nswap; + std::int64_t ru_inblock; + std::int64_t ru_oublock; + std::int64_t ru_msgsnd; + std::int64_t ru_msgrcv; + std::int64_t ru_nsignals; + std::int64_t ru_nvcsw; + std::int64_t ru_nivcsw; +}; + extern "C" { // unknown signature @@ -148,6 +171,10 @@ void APS5_VABI exit_nid_postfix(int code) { LibcExit_nid_no_patch(code); } +[[noreturn]] void APS5_VABI _exit_nid_postfix(int status) { + std::_Exit(status); +} + int APS5_VABI sceKernelGetCurrentCpu(void) { #ifdef _WIN32 PROCESSOR_NUMBER processor{}; @@ -219,4 +246,72 @@ int APS5_VABI sched_get_priority_min_nid_postfix(int policy) { return 767; } +int APS5_VABI getrusage_nid_postfix(int who, GuestResourceUsage* usage) { + if (usage == nullptr) + throw std::invalid_argument("getrusage: usage is null"); + if (who != 0 && who != 1) + throw std::invalid_argument("getrusage: unsupported who"); +#ifdef _WIN32 + FILETIME creation{}; + FILETIME exitTime{}; + FILETIME kernel{}; + FILETIME user{}; + if (who == 0) { + if (!GetProcessTimes(GetCurrentProcess(), &creation, &exitTime, &kernel, &user)) + throw std::system_error(GetLastError(), std::system_category(), "getrusage: GetProcessTimes failed"); + } else { + if (!GetThreadTimes(GetCurrentThread(), &creation, &exitTime, &kernel, &user)) + throw std::system_error(GetLastError(), std::system_category(), "getrusage: GetThreadTimes failed"); + } + const auto toMicros = [](const FILETIME& time) { + return ((static_cast(time.dwHighDateTime) << 32) | time.dwLowDateTime) / 10ULL; + }; + const auto userMicros = toMicros(user); + const auto kernelMicros = toMicros(kernel); + usage->ru_utime.tv_sec = static_cast(userMicros / 1000000ULL); + usage->ru_utime.tv_usec = static_cast(userMicros % 1000000ULL); + usage->ru_stime.tv_sec = static_cast(kernelMicros / 1000000ULL); + usage->ru_stime.tv_usec = static_cast(kernelMicros % 1000000ULL); + usage->ru_maxrss = 0; + usage->ru_ixrss = 0; + usage->ru_idrss = 0; + usage->ru_isrss = 0; + usage->ru_minflt = 0; + usage->ru_majflt = 0; + usage->ru_nswap = 0; + usage->ru_inblock = 0; + usage->ru_oublock = 0; + usage->ru_msgsnd = 0; + usage->ru_msgrcv = 0; + usage->ru_nsignals = 0; + usage->ru_nvcsw = 0; + usage->ru_nivcsw = 0; +#else + if (who == 1) + throw std::invalid_argument("getrusage: RUSAGE_THREAD is not supported on this platform"); + struct rusage native{}; + if (::getrusage(RUSAGE_SELF, &native) != 0) + throw std::system_error(errno, std::generic_category(), "getrusage: getrusage failed"); + usage->ru_utime.tv_sec = static_cast(native.ru_utime.tv_sec); + usage->ru_utime.tv_usec = static_cast(native.ru_utime.tv_usec); + usage->ru_stime.tv_sec = static_cast(native.ru_stime.tv_sec); + usage->ru_stime.tv_usec = static_cast(native.ru_stime.tv_usec); + usage->ru_maxrss = static_cast(native.ru_maxrss); + usage->ru_ixrss = static_cast(native.ru_ixrss); + usage->ru_idrss = static_cast(native.ru_idrss); + usage->ru_isrss = static_cast(native.ru_isrss); + usage->ru_minflt = static_cast(native.ru_minflt); + usage->ru_majflt = static_cast(native.ru_majflt); + usage->ru_nswap = static_cast(native.ru_nswap); + usage->ru_inblock = static_cast(native.ru_inblock); + usage->ru_oublock = static_cast(native.ru_oublock); + usage->ru_msgsnd = static_cast(native.ru_msgsnd); + usage->ru_msgrcv = static_cast(native.ru_msgrcv); + usage->ru_nsignals = static_cast(native.ru_nsignals); + usage->ru_nvcsw = static_cast(native.ru_nvcsw); + usage->ru_nivcsw = static_cast(native.ru_nivcsw); +#endif + return 0; +} + } diff --git a/core/libs/prx/libkernel/System/src/Signals.cpp b/core/libs/prx/libkernel/System/src/Signals.cpp index 713585c60..94a4e3be5 100644 --- a/core/libs/prx/libkernel/System/src/Signals.cpp +++ b/core/libs/prx/libkernel/System/src/Signals.cpp @@ -10,6 +10,8 @@ namespace { using GuestHandler = void (APS5_VABI *)(int); std::atomic handlers[32]{}; static_assert(std::atomic::is_always_lock_free); +std::atomic blockedMask{0}; +static_assert(std::atomic::is_always_lock_free); std::mutex registration; int NativeSignal(int guest) { switch (guest) { @@ -31,10 +33,16 @@ void Dispatch(int native) { // Preserve the guest's persistent registration across CRT delivery. std::signal(native, Dispatch); #endif + if ((blockedMask.load() & (1u << guest)) != 0) return; const auto callback = handlers[guest].load(); if (reinterpret_cast(callback) > 1) callback(guest); } } + +struct GuestSignalSet { + std::uint32_t bits[4]; +}; + extern "C" { GuestHandler APS5_VABI signal_nid_postfix(int guest, GuestHandler handler) { const auto invalid = reinterpret_cast(static_cast(-1)); @@ -58,6 +66,24 @@ int APS5_VABI raise_nid_postfix(int guest) { if (result) *__error_nid_postfix() = 22; return result ? -1 : 0; } +int APS5_VABI _sigprocmask_nid_postfix(int how, const GuestSignalSet* set, GuestSignalSet* previousSet) { + std::lock_guard lock(registration); + if (previousSet != nullptr) { + previousSet->bits[0] = blockedMask.load(); + previousSet->bits[1] = 0; + previousSet->bits[2] = 0; + previousSet->bits[3] = 0; + } + if (set != nullptr) { + switch (how) { + case 1: blockedMask.fetch_or(set->bits[0]); break; + case 2: blockedMask.fetch_and(~set->bits[0]); break; + case 3: blockedMask.store(set->bits[0]); break; + default: throw std::invalid_argument("_sigprocmask: invalid how"); + } + } + return 0; +} } extern "C" { diff --git a/core/libs/prx/libkernel/Time/Time.cpp b/core/libs/prx/libkernel/Time/Time.cpp index aeb9cafb9..7b641b016 100644 --- a/core/libs/prx/libkernel/Time/Time.cpp +++ b/core/libs/prx/libkernel/Time/Time.cpp @@ -92,6 +92,10 @@ int APS5_VABI nanosleep_nid_postfix(const KernelTimespec* rqtp, KernelTimespec* return sceKernelNanosleep(rqtp, rmtp); } +int APS5_VABI _nanosleep_nid_postfix(const KernelTimespec* rqtp, KernelTimespec* rmtp) { + return sceKernelNanosleep(rqtp, rmtp); +} + int APS5_VABI clock_gettime_nid_postfix(int clockId, KernelTimespec* tp) { if (tp == nullptr) { APS5_INVALID_ARG_EX; From 7a925c68e5d535d8d6a14d9b99542d80642ca8a9 Mon Sep 17 00:00:00 2001 From: boykopovar Date: Sat, 26 Sep 2026 18:48:40 +0300 Subject: [PATCH 83/94] fix(libkernel): handle clock_second 13 in clock_gettime and clock_getres --- core/libs/prx/libkernel/Time/Time.cpp | 32 +++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libkernel/Time/Time.cpp b/core/libs/prx/libkernel/Time/Time.cpp index 7b641b016..28cd5647e 100644 --- a/core/libs/prx/libkernel/Time/Time.cpp +++ b/core/libs/prx/libkernel/Time/Time.cpp @@ -101,7 +101,7 @@ int APS5_VABI clock_gettime_nid_postfix(int clockId, KernelTimespec* tp) { APS5_INVALID_ARG_EX; } #ifdef _WIN32 - if (clockId == 0 || clockId == 9 || clockId == 10) { + if (clockId == 0 || clockId == 9) { FILETIME ft{}; GetSystemTimePreciseAsFileTime(&ft); std::uint64_t t = (static_cast(ft.dwHighDateTime) << 32) | ft.dwLowDateTime; @@ -111,6 +111,16 @@ int APS5_VABI clock_gettime_nid_postfix(int clockId, KernelTimespec* tp) { tp->tv_nsec = static_cast(t % 1000000000ULL); return 0; } + if (clockId == 10 || clockId == 13) { + FILETIME ft{}; + GetSystemTimeAsFileTime(&ft); + std::uint64_t t = (static_cast(ft.dwHighDateTime) << 32) | ft.dwLowDateTime; + t -= 116444736000000000ULL; + t *= 100ULL; + tp->tv_sec = static_cast(t / 1000000000ULL); + tp->tv_nsec = static_cast(t % 1000000000ULL); + return 0; + } if (clockId == 4 || clockId == 1 || clockId == 5 || clockId == 7 || clockId == 8 || clockId == 11 || clockId == 12) { std::uint64_t nanos = GetMonotonicNanos(); tp->tv_sec = static_cast(nanos / 1000000000ULL); @@ -123,9 +133,16 @@ int APS5_VABI clock_gettime_nid_postfix(int clockId, KernelTimespec* tp) { switch (clockId) { case 0: case 9: - case 10: nativeId = CLOCK_REALTIME; break; + case 10: + case 13: +#ifdef CLOCK_REALTIME_COARSE + nativeId = CLOCK_REALTIME_COARSE; +#else + nativeId = CLOCK_REALTIME; +#endif + break; case 4: case 7: case 11: @@ -191,7 +208,7 @@ int APS5_VABI clock_getres_nid_postfix(int clockId, KernelTimespec* res) { res->tv_nsec = 100LL; return 0; } - if (clockId == 4 || clockId == 1 || clockId == 5 || clockId == 7 || clockId == 8 || clockId == 10 || clockId == 11 || clockId == 12) { + if (clockId == 4 || clockId == 1 || clockId == 5 || clockId == 7 || clockId == 8 || clockId == 10 || clockId == 11 || clockId == 12 || clockId == 13) { static const std::uint64_t freq = [] { LARGE_INTEGER f{}; QueryPerformanceFrequency(&f); @@ -208,9 +225,16 @@ int APS5_VABI clock_getres_nid_postfix(int clockId, KernelTimespec* res) { switch (clockId) { case 0: case 9: - case 10: nativeId = CLOCK_REALTIME; break; + case 10: + case 13: +#ifdef CLOCK_REALTIME_COARSE + nativeId = CLOCK_REALTIME_COARSE; +#else + nativeId = CLOCK_REALTIME; +#endif + break; case 4: case 7: case 11: From f5ea673c1085f6c1627273534827fe9ff618a020 Mon Sep 17 00:00:00 2001 From: boykopovar Date: Sat, 26 Sep 2026 18:58:32 +0300 Subject: [PATCH 84/94] feat(libkernel): impl posix stdio wrappers, reusing sceKernel file ops --- .../prx/libkernel/File/include/NativeStat.hpp | 1 + .../prx/libkernel/File/src/NativeStat.cpp | 32 ++- core/libs/prx/libkernel/File/src/Stdio.cpp | 207 +++++++++++++----- 3 files changed, 177 insertions(+), 63 deletions(-) diff --git a/core/libs/prx/libkernel/File/include/NativeStat.hpp b/core/libs/prx/libkernel/File/include/NativeStat.hpp index 8ea664c41..07ee81c91 100644 --- a/core/libs/prx/libkernel/File/include/NativeStat.hpp +++ b/core/libs/prx/libkernel/File/include/NativeStat.hpp @@ -7,6 +7,7 @@ namespace File { void FillFileStat(const std::filesystem::path& nativePath, FileStat* sb); +void FillFileStat(int nativeDescriptor, FileStat* sb); } diff --git a/core/libs/prx/libkernel/File/src/NativeStat.cpp b/core/libs/prx/libkernel/File/src/NativeStat.cpp index 5f92973b8..f25a72e98 100644 --- a/core/libs/prx/libkernel/File/src/NativeStat.cpp +++ b/core/libs/prx/libkernel/File/src/NativeStat.cpp @@ -10,21 +10,21 @@ using NativeStat = struct __stat64; static int DoStat(const std::filesystem::path& p, NativeStat* st) { return _wstat64(p.wstring().c_str(), st); } +static int DoFstat(int fd, NativeStat* st) { + return _fstat64(fd, st); +} #else #include using NativeStat = struct stat; static int DoStat(const std::filesystem::path& p, NativeStat* st) { return ::stat(p.c_str(), st); } +static int DoFstat(int fd, NativeStat* st) { + return ::fstat(fd, st); +} #endif -namespace File { - -void FillFileStat(const std::filesystem::path& nativePath, FileStat* sb) { - NativeStat st{}; - if (DoStat(nativePath, &st) != 0) { - throw std::runtime_error(std::string("FillFileStat: stat failed for ") + nativePath.string()); - } +static void CopyNativeStat(const NativeStat& st, FileStat* sb) { *sb = FileStat{}; sb->st_mode = static_cast(st.st_mode); sb->st_size = static_cast(st.st_size); @@ -72,4 +72,22 @@ void FillFileStat(const std::filesystem::path& nativePath, FileStat* sb) { #endif } +namespace File { + +void FillFileStat(const std::filesystem::path& nativePath, FileStat* sb) { + NativeStat st{}; + if (DoStat(nativePath, &st) != 0) { + throw std::runtime_error(std::string("FillFileStat: stat failed for ") + nativePath.string()); + } + CopyNativeStat(st, sb); +} + +void FillFileStat(int nativeDescriptor, FileStat* sb) { + NativeStat st{}; + if (DoFstat(nativeDescriptor, &st) != 0) { + throw std::runtime_error(std::string("FillFileStat: fstat failed for fd ") + std::to_string(nativeDescriptor)); + } + CopyNativeStat(st, sb); +} + } diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index c0cefcd8b..ef652c617 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -1,33 +1,115 @@ #include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "prx/libkernel/File/include/File.hpp" #include "prx/libkernel/File/include/FileFlags.hpp" +#include "prx/libkernel/File/include/NativeStat.hpp" #include "prx/libkernel/Socket/include/SocketRuntime.hpp" #include #include #include #ifdef _WIN32 +#include #include #include +#include static int NativeRmdir(const std::filesystem::path& path) { return ::_wrmdir(path.wstring().c_str()); } +static int NativeMkdir(const std::filesystem::path& path, std::uint16_t mode) { + (void)mode; + return ::_wmkdir(path.wstring().c_str()); +} +static int NativeChmod(const std::filesystem::path& path, int mode) { + return ::_wchmod(path.wstring().c_str(), mode); +} +static int NativeFtruncate(int descriptor, std::int64_t length) { + return static_cast(::_chsize_s(descriptor, length)); +} +static int NativeFlock(int descriptor, int operation) { + HANDLE handle = reinterpret_cast(::_get_osfhandle(descriptor)); + if (handle == INVALID_HANDLE_VALUE) { + return -1; + } + OVERLAPPED overlapped{}; + if (operation & 8) { + return ::UnlockFileEx(handle, 0, MAXDWORD, MAXDWORD, &overlapped) ? 0 : -1; + } + DWORD flags = 0; + if (operation & 2) flags |= LOCKFILE_EXCLUSIVE_LOCK; + if (operation & 4) flags |= LOCKFILE_FAIL_IMMEDIATELY; + return ::LockFileEx(handle, flags, 0, MAXDWORD, MAXDWORD, &overlapped) ? 0 : -1; +} +static std::int64_t NativePread(int descriptor, void* buf, std::size_t nbytes, std::int64_t offset) { + if (nbytes > static_cast(std::numeric_limits::max())) { + throw std::runtime_error("NativePread: nbytes exceeds platform limit"); + } + std::int64_t saved = ::_lseeki64(descriptor, 0, SEEK_CUR); + if (saved < 0) { + return -1; + } + if (::_lseeki64(descriptor, offset, SEEK_SET) < 0) { + return -1; + } + int n = ::_read(descriptor, buf, static_cast(nbytes)); + ::_lseeki64(descriptor, saved, SEEK_SET); + return n; +} +static std::int64_t NativePwrite(int descriptor, const void* buf, std::size_t nbytes, std::int64_t offset) { + if (nbytes > static_cast(std::numeric_limits::max())) { + throw std::runtime_error("NativePwrite: nbytes exceeds platform limit"); + } + std::int64_t saved = ::_lseeki64(descriptor, 0, SEEK_CUR); + if (saved < 0) { + return -1; + } + if (::_lseeki64(descriptor, offset, SEEK_SET) < 0) { + return -1; + } + int n = ::_write(descriptor, buf, static_cast(nbytes)); + ::_lseeki64(descriptor, saved, SEEK_SET); + return n; +} #else #include +#include +#include static int NativeRmdir(const std::filesystem::path& path) { return ::rmdir(path.c_str()); } +static int NativeMkdir(const std::filesystem::path& path, std::uint16_t mode) { + return ::mkdir(path.c_str(), static_cast(mode)); +} +static int NativeChmod(const std::filesystem::path& path, int mode) { + return ::chmod(path.c_str(), static_cast(mode)); +} +static int NativeFtruncate(int descriptor, std::int64_t length) { + return ::ftruncate(descriptor, static_cast(length)); +} +static int NativeFlock(int descriptor, int operation) { + return ::flock(descriptor, operation); +} +static std::int64_t NativePread(int descriptor, void* buf, std::size_t nbytes, std::int64_t offset) { + return static_cast(::pread(descriptor, buf, nbytes, static_cast(offset))); +} +static std::int64_t NativePwrite(int descriptor, const void* buf, std::size_t nbytes, std::int64_t offset) { + return static_cast(::pwrite(descriptor, buf, nbytes, static_cast(offset))); +} #endif extern "C" { int APS5_VABI chmod_nid_postfix(const char* path, int mode) { - (void)path; - (void)mode; - NotImplemented_nid_no_patch(__func__); - return 0; + if (path == nullptr) { + APS5_INVALID_ARG_EX; + } + auto native = ResolvePath_nid_no_patch(path); + if (NativeChmod(native, mode) != 0) { + throw std::runtime_error(std::string(__func__) + ": chmod failed for " + native.string() + ", errno=" + std::to_string(errno)); + } + return 0; } int APS5_VABI close_nid_postfix(int d) { @@ -44,47 +126,58 @@ int APS5_VABI _close_nid_postfix(int descriptor) { } int APS5_VABI flock_nid_postfix(int d, int operation) { - (void)d; - (void)operation; - NotImplemented_nid_no_patch(__func__); - return 0; + if (NativeFlock(d, operation) != 0) { +#ifdef _WIN32 + throw std::runtime_error(std::string(__func__) + ": flock failed, fd=" + std::to_string(d) + ", error=" + std::to_string(::GetLastError())); +#else + throw std::runtime_error(std::string(__func__) + ": flock failed, fd=" + std::to_string(d) + ", errno=" + std::to_string(errno)); +#endif + } + return 0; } int64_t APS5_VABI fstat_nid_disambig1_nid_postfix(int d, FileStat* sb) { - (void)d; - (void)sb; - NotImplemented_nid_no_patch(__func__); - return 0; + if (sb == nullptr) { + APS5_INVALID_ARG_EX; + } + File::FillFileStat(d, sb); + return 0; } int APS5_VABI ftruncate_nid_postfix(int d, int64_t length) { - (void)d; - (void)length; - NotImplemented_nid_no_patch(__func__); - return 0; + if (length < 0) { + APS5_INVALID_ARG_EX; + } +#ifdef _WIN32 + int error = NativeFtruncate(d, length); + if (error != 0) { + throw std::runtime_error(std::string(__func__) + ": ftruncate failed, fd=" + std::to_string(d) + ", error=" + std::to_string(error)); + } +#else + if (NativeFtruncate(d, length) != 0) { + throw std::runtime_error(std::string(__func__) + ": ftruncate failed, fd=" + std::to_string(d) + ", errno=" + std::to_string(errno)); + } +#endif + return 0; } int64_t APS5_VABI lseek_nid_postfix(int d, int64_t offset, int whence) { - (void)d; - (void)offset; - (void)whence; - NotImplemented_nid_no_patch(__func__); - return 0; + return static_cast(sceKernelLseek(d, offset, whence)); } int APS5_VABI mkdir_nid_postfix(const char* path, uint16_t mode) { - (void)path; - (void)mode; - NotImplemented_nid_no_patch(__func__); - return 0; + if (path == nullptr) { + APS5_INVALID_ARG_EX; + } + auto native = ResolvePath_nid_no_patch(path); + if (NativeMkdir(native, mode) != 0) { + throw std::runtime_error(std::string(__func__) + ": mkdir failed for " + native.string() + ", errno=" + std::to_string(errno)); + } + return 0; } int APS5_VABI open_nid_postfix(const char* path, int flags, int mode) { - (void)path; - (void)flags; - (void)mode; - NotImplemented_nid_no_patch(__func__); - return 0; + return sceKernelOpen(path, flags, static_cast(mode)); } int APS5_VABI _open_nid_postfix(const char* path, int flags, ...) { @@ -106,29 +199,35 @@ int APS5_VABI _open_nid_postfix(const char* path, int flags, ...) { } int64_t APS5_VABI pread_nid_postfix(int d, void* buf, size_t nbytes, int64_t offset) { - (void)d; - (void)buf; - (void)nbytes; - (void)offset; - NotImplemented_nid_no_patch(__func__); - return 0; + if (buf == nullptr) { + APS5_INVALID_ARG_EX; + } + if (offset < 0) { + APS5_INVALID_ARG_EX; + } + auto n = NativePread(d, buf, nbytes, offset); + if (n < 0) { + throw std::runtime_error(std::string(__func__) + ": pread failed, fd=" + std::to_string(d) + ", errno=" + std::to_string(errno)); + } + return n; } int64_t APS5_VABI pwrite_nid_disambig1_nid_postfix(int d, const void* buf, size_t nbytes, int64_t offset) { - (void)d; - (void)buf; - (void)nbytes; - (void)offset; - NotImplemented_nid_no_patch(__func__); - return 0; + if (buf == nullptr) { + APS5_INVALID_ARG_EX; + } + if (offset < 0) { + APS5_INVALID_ARG_EX; + } + auto n = NativePwrite(d, buf, nbytes, offset); + if (n < 0) { + throw std::runtime_error(std::string(__func__) + ": pwrite failed, fd=" + std::to_string(d) + ", errno=" + std::to_string(errno)); + } + return n; } int64_t APS5_VABI read_nid_postfix(int d, void* buf, uint64_t nbytes) { - (void)d; - (void)buf; - (void)nbytes; - NotImplemented_nid_no_patch(__func__); - return 0; + return sceKernelRead(d, buf, static_cast(nbytes)); } std::int64_t APS5_VABI _read_nid_postfix(int descriptor, void* buffer, std::size_t count) { @@ -136,11 +235,10 @@ std::int64_t APS5_VABI _read_nid_postfix(int descriptor, void* buffer, std::size } int64_t APS5_VABI write_nid_postfix(int d, const char* str, int64_t size) { - (void)d; - (void)str; - (void)size; - NotImplemented_nid_no_patch(__func__); - return 0; + if (size < 0) { + APS5_INVALID_ARG_EX; + } + return sceKernelWrite(d, str, static_cast(size)); } std::int64_t APS5_VABI _write_nid_postfix(int descriptor, const void* buffer, std::size_t count) { @@ -148,10 +246,7 @@ std::int64_t APS5_VABI _write_nid_postfix(int descriptor, const void* buffer, st } int APS5_VABI stat_nid_postfix(const char* path, FileStat* sb) { - (void)path; - (void)sb; - NotImplemented_nid_no_patch(__func__); - return 0; + return sceKernelStat(path, sb); } int APS5_VABI unlink_nid_postfix(const char* path) { From 9cc36a02fecfa57a875c430684efc51a0a862c8f Mon Sep 17 00:00:00 2001 From: boykopovar Date: Sat, 26 Sep 2026 19:06:03 +0300 Subject: [PATCH 85/94] feat(libkernel): add __progname str value --- core/libs/prx/libkernel/System/src/Process.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libkernel/System/src/Process.cpp b/core/libs/prx/libkernel/System/src/Process.cpp index 7fe1ad6bb..db034a210 100644 --- a/core/libs/prx/libkernel/System/src/Process.cpp +++ b/core/libs/prx/libkernel/System/src/Process.cpp @@ -141,8 +141,8 @@ struct GuestResourceUsage { extern "C" { -// unknown signature -char* __progname_nid_postfix = nullptr; +// unknown data +const char* __progname_nid_postfix = "eboot.bin"; int APS5_VABI getargc_nid_postfix(void) { return getProcessArguments().GetCount(); From bfa1ac0751d6cec02b4bc13d26ca64cd0320be1b Mon Sep 17 00:00:00 2001 From: boykopovar Date: Sun, 27 Sep 2026 09:00:11 +0300 Subject: [PATCH 86/94] fix(relinker): scan complete function ranges for windows tls accesses --- core/relinker/CMakeLists.txt | 4 +- .../src/analysis/CodeInstructionCollector.cpp | 49 +++++++- .../tests/test_tls_function_coverage.py | 116 ++++++++++++++++++ 3 files changed, 166 insertions(+), 3 deletions(-) create mode 100644 core/relinker/relinker/tests/test_tls_function_coverage.py diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 0e95e87f9..606259877 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -86,8 +86,10 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_optional_plt.py $) add_test(NAME empty_tls COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_empty_tls.py $) + add_test(NAME tls_function_coverage COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_tls_function_coverage.py $) else() - message(STATUS "Python3 interpreter not found; skipping optional_plt and empty_tls tests") + message(STATUS "Python3 interpreter not found; skipping Python relinker tests") endif() add_executable(strict_nid_filter_tests ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/StrictNidFilterTests.cpp diff --git a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp index 061c35378..0b009c39f 100644 --- a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp +++ b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp @@ -2,8 +2,11 @@ #include #include #include +#include #include #include +#include +#include namespace Relinker { @@ -73,7 +76,27 @@ std::set CodeInstructionCollector::Collect(const std::ve return offset; }; std::set roots; + std::map functions; const auto addRoot = [&](std::uint64_t address) { if (isCode(address)) roots.insert(address); }; + const auto addFunction = [&](std::uint64_t begin, std::uint64_t size) { + if (size == 0) return; + if (!isCode(begin) || size > std::numeric_limits::max() - begin) + throw Domain::RelinkerException("Code analysis: invalid function range", begin); + bool mapped = false; + for (const auto& header : headers) { + if (header.Type != 1 || (header.Flags & 1) == 0 || begin < header.MappedAddress || begin - header.MappedAddress >= header.FileSize) continue; + if (size > header.FileSize - (begin - header.MappedAddress)) + throw Domain::RelinkerException("Code analysis: function exceeds executable segment", begin); + range(header.Offset + begin - header.MappedAddress, size); + mapped = true; + break; + } + if (!mapped) throw Domain::RelinkerException("Code analysis: function is not file-backed", begin); + const auto [position, inserted] = functions.emplace(begin, begin + size); + if (!inserted && position->second != begin + size) + throw Domain::RelinkerException("Code analysis: conflicting function ranges", begin); + roots.insert(begin); + }; range(0, 64); if (const auto entry = Io::ReadU64(bytes, 24); entry != 0) addRoot(entry); for (const auto tag : {12u, 13u}) if (const auto address = value(tag, 0x60000000 + tag); address && *address != 0) addRoot(*address); @@ -89,7 +112,11 @@ std::set CodeInstructionCollector::Collect(const std::ve symbols = table(6, 0x61000039, symbolCount * 24); for (std::uint64_t index = 0; index < symbolCount; ++index) { const auto offset = symbols + index * 24; - if ((bytes[offset + 4] & 15) == 2 && Io::ReadU16(bytes, offset + 6) != 0) addRoot(Io::ReadU64(bytes, offset + 8)); + if ((bytes[offset + 4] & 15) == 2 && Io::ReadU16(bytes, offset + 6) != 0) { + const auto address = Io::ReadU64(bytes, offset + 8); + addRoot(address); + addFunction(address, Io::ReadU64(bytes, offset + 16)); + } } } CodePointers pointers; @@ -139,12 +166,30 @@ std::set CodeInstructionCollector::Collect(const std::ve } } for (const auto& function : UnusedNidFilter::ReadExceptionFunctions(bytes, headers, pointers.Values, importSlots)) { - addRoot(function.Begin); + if (function.End <= function.Begin) throw Domain::RelinkerException("Code analysis: invalid unwind function range", function.Begin); + addFunction(function.Begin, function.End - function.Begin); for (const auto target : function.ExtraTargets) addRoot(target); } if (roots.empty()) throw Domain::RelinkerException("Code analysis: no code entry points"); std::set instructions; const Codegen::X64InstructionDecoder decoder; + for (const auto& [begin, end] : functions) { + const auto offset = fileOffset(begin, end - begin); + for (auto address = begin; address < end;) { + Codegen::DecodedInstructionInfo info; + 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); + } + 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)); + address += info.Length; + } + } std::size_t previousRoots = 0; do { previousRoots = roots.size(); diff --git a/core/relinker/relinker/tests/test_tls_function_coverage.py b/core/relinker/relinker/tests/test_tls_function_coverage.py new file mode 100644 index 000000000..c0bc8145b --- /dev/null +++ b/core/relinker/relinker/tests/test_tls_function_coverage.py @@ -0,0 +1,116 @@ +import os +from pathlib import Path +import struct +import subprocess +import sys +import tempfile + +from test_optional_plt import fixture + + +TLS_LOAD = bytes.fromhex("66 66 66 64 48 8b 04 25 00 00 00 00") + + +def make_image(transfer, metadata, extent=None): + image = fixture() + image.extend(b"\x90" * 0x1000) + struct.pack_into(" Date: Sun, 27 Sep 2026 09:37:01 +0300 Subject: [PATCH 87/94] feat(libc): replace guest locale implementation with std::locale --- core/libs/CMakeLists.txt | 6 - core/libs/prx/libc/include/GuestLocale.hpp | 66 ------- core/libs/prx/libc/src/LocaleSupport.cpp | 216 ++++++++------------- core/libs/tests/GuestCharacter.cpp | 4 +- core/libs/tests/GuestLocale.cpp | 158 --------------- 5 files changed, 79 insertions(+), 371 deletions(-) delete mode 100644 core/libs/prx/libc/include/GuestLocale.hpp delete mode 100644 core/libs/tests/GuestLocale.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index ac01dac0e..933cd9093 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -361,12 +361,6 @@ add_executable(application_heap_tests EXCLUDE_FROM_ALL tests/ApplicationHeap.cpp target_include_directories(application_heap_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) set_target_properties(application_heap_tests PROPERTIES CXX_EXTENSIONS OFF) -add_executable(guest_locale_tests EXCLUDE_FROM_ALL tests/GuestLocale.cpp) -target_include_directories(guest_locale_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) -set_target_properties(guest_locale_tests PROPERTIES CXX_EXTENSIONS OFF) -target_link_libraries(guest_locale_tests PRIVATE libc) -configure_windows_unwind(guest_locale_tests) - if(MINGW) foreach(runtimeLinkFlag IN ITEMS -static-libgcc -static-libstdc++ -static) string(REPLACE "${runtimeLinkFlag}" "" CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}") diff --git a/core/libs/prx/libc/include/GuestLocale.hpp b/core/libs/prx/libc/include/GuestLocale.hpp deleted file mode 100644 index 1e46c9210..000000000 --- a/core/libs/prx/libc/include/GuestLocale.hpp +++ /dev/null @@ -1,66 +0,0 @@ -#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_GUESTLOCALE_HPP -#define CORE_LIBS_PRX_LIBC_INCLUDE_GUESTLOCALE_HPP - -#include -#include -#include "general/VabiMacros.hpp" - -namespace GuestLocale { - -struct LocinfoStorage { - alignas(std::uint64_t) unsigned char bytes[64]; -}; - -struct Facet; - -struct FacetVtable { - void (APS5_VABI *destroy)(Facet* self); - void (APS5_VABI *deleteObject)(Facet* self); - void (APS5_VABI *retain)(Facet* self); - Facet* (APS5_VABI *release)(Facet* self); -}; - -struct Facet { - const FacetVtable* vtable; - std::uint32_t references; - std::uint32_t reserved; -}; - -struct Implementation { - Facet base; - Facet** facets; - std::uint64_t facetCount; - std::uint32_t category; - bool transparent; - const char* name; -}; - -struct IosBase { - const void* vtable; - std::int64_t standardStream; - std::int32_t state; - std::int32_t exceptions; - std::int32_t flags; - std::int32_t precision; - std::int32_t width; - std::uint32_t reserved; - void* storage; - void* callbacks; - Implementation** locale; -}; - -static_assert(sizeof(Facet) == 0x10); -static_assert(offsetof(Implementation, facets) == 0x10); -static_assert(offsetof(Implementation, facetCount) == 0x18); -static_assert(offsetof(Implementation, transparent) == 0x24); -static_assert(offsetof(Implementation, name) == 0x28); -static_assert(offsetof(IosBase, state) == 0x10); -static_assert(offsetof(IosBase, storage) == 0x28); -static_assert(offsetof(IosBase, callbacks) == 0x30); -static_assert(offsetof(IosBase, locale) == 0x38); -static_assert(sizeof(IosBase) == 0x40); -static_assert(alignof(LocinfoStorage) == 8); - -} - -#endif diff --git a/core/libs/prx/libc/src/LocaleSupport.cpp b/core/libs/prx/libc/src/LocaleSupport.cpp index 0d178f899..8d3d0e7b4 100644 --- a/core/libs/prx/libc/src/LocaleSupport.cpp +++ b/core/libs/prx/libc/src/LocaleSupport.cpp @@ -1,203 +1,141 @@ +#include +#include #include -#include +#include #include #include #include -#include -#include -#include -#include -#include -#include +#include +#include #include "prx/libc/include/General.hpp" #include "prx/libc/include/ApplicationHeap.hpp" -#include "prx/libc/include/GuestLocale.hpp" namespace { -std::mutex g_localeInitMutex; -bool g_localeInitialized = false; -std::vector g_registeredFacets; - -void APS5_VABI DestroyClassicLocale(GuestLocale::Facet*) { - throw std::runtime_error("Cannot destroy the classic locale"); +void ValidateCharacter(int value) { + if (value != EOF && (value < 0 || value > UCHAR_MAX)) throw std::invalid_argument("Invalid character value"); } -void APS5_VABI RetainLocale(GuestLocale::Facet* self) { - if (self == nullptr) throw std::invalid_argument("locale retain: null facet"); - std::atomic_ref references(self->references); - auto count = references.load(); - do { - if (count == 0 || count == UINT32_MAX) throw std::runtime_error("locale retain: invalid reference count"); - } while (!references.compare_exchange_weak(count, count + 1)); +int ClassifyCharacter(int value, std::ctype_base::mask mask) { + ValidateCharacter(value); + if (value == EOF) return 0; + return std::use_facet>(std::locale::classic()).is(mask, static_cast(value)); } -GuestLocale::Facet* APS5_VABI ReleaseLocale(GuestLocale::Facet* self) { - if (self == nullptr) throw std::invalid_argument("locale release: null facet"); - std::atomic_ref references(self->references); - auto count = references.load(); - do { - if (count <= 1) throw std::runtime_error("classic locale: unbalanced release"); - } while (!references.compare_exchange_weak(count, count - 1)); - return nullptr; +int ConvertCharacter(int value, bool upper) { + ValidateCharacter(value); + if (value == EOF) return EOF; + const auto& facet = std::use_facet>(std::locale::classic()); + const auto character = static_cast(value); + return static_cast(upper ? facet.toupper(character) : facet.tolower(character)); } -const GuestLocale::FacetVtable g_localeVtable{DestroyClassicLocale, DestroyClassicLocale, RetainLocale, ReleaseLocale}; -GuestLocale::Facet* g_classicFacets[1]{}; -GuestLocale::Implementation g_classicLocale{{&g_localeVtable, 1, 0}, g_classicFacets, 1, 0, false, "C"}; - -constexpr std::array MakeClassificationTable() { - std::array table{}; - for (unsigned int value = 0; value < 256; ++value) { - short mask = 0; - if (value < 32 || value == 127) mask |= 0x80; - if (value == ' ') mask |= 0x04; - if (value >= '\t' && value <= '\r') mask |= 0x40; - if (value >= 'A' && value <= 'Z') mask |= 0x02; - if (value >= 'a' && value <= 'z') mask |= 0x10; - if (value >= '0' && value <= '9') mask |= 0x20; - if ((value >= '0' && value <= '9') || (value >= 'A' && value <= 'F') || (value >= 'a' && value <= 'f')) mask |= 0x01; - if (value >= 33 && value <= 126 && (mask & 0x232) == 0) mask |= 0x08; - table[value + 1] = mask; - } +std::array MakeCaseTable(bool upper) { + std::array table{}; + table[0] = EOF; + for (int value = 0; value <= UCHAR_MAX; ++value) table[value + 1] = ConvertCharacter(value, upper); return table; } -constexpr std::array MakeCaseTable(bool upper) { - std::array table{}; - table[0] = -1; - for (unsigned int value = 0; value < 256; ++value) { - auto converted = value; - if (upper && value >= 'a' && value <= 'z') converted -= 'a' - 'A'; - if (!upper && value >= 'A' && value <= 'Z') converted += 'a' - 'A'; - table[value + 1] = static_cast(converted); - } - return table; -} - -constexpr auto g_classificationTable = MakeClassificationTable(); -constexpr auto g_lowerTable = MakeCaseTable(false); -constexpr auto g_upperTable = MakeCaseTable(true); - } extern "C" { -// The runtime currently exposes the classic C locale. Keep byte classification -// independent of any locale selected by host-side libraries. -int APS5_VABI isupper_nid_postfix(int c) { return c >= 'A' && c <= 'Z'; } -int APS5_VABI islower_nid_postfix(int c) { return c >= 'a' && c <= 'z'; } -int APS5_VABI isalpha_nid_postfix(int c) { return isupper_nid_postfix(c) || islower_nid_postfix(c); } -int APS5_VABI isdigit_nid_postfix(int c) { return c >= '0' && c <= '9'; } -int APS5_VABI isalnum_nid_postfix(int c) { return isalpha_nid_postfix(c) || isdigit_nid_postfix(c); } -int APS5_VABI isspace_nid_postfix(int c) { return c == ' ' || (c >= '\t' && c <= '\r'); } -int APS5_VABI isblank_nid_postfix(int c) { return c == ' ' || c == '\t'; } -int APS5_VABI iscntrl_nid_postfix(int c) { return (c >= 0 && c < 32) || c == 127; } -int APS5_VABI isprint_nid_postfix(int c) { return c >= 32 && c <= 126; } -int APS5_VABI isgraph_nid_postfix(int c) { return c >= 33 && c <= 126; } -int APS5_VABI ispunct_nid_postfix(int c) { return isgraph_nid_postfix(c) && !isalnum_nid_postfix(c); } -int APS5_VABI isxdigit_nid_postfix(int c) { - return isdigit_nid_postfix(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); -} -int APS5_VABI toupper_nid_postfix(int c) { return islower_nid_postfix(c) ? c - ('a' - 'A') : c; } -int APS5_VABI tolower_nid_postfix(int c) { return isupper_nid_postfix(c) ? c + ('a' - 'A') : c; } - -std::uint64_t _ZNSt5ctypeIcE2idE_nid_postfix = 0; -std::uint64_t _ZNSt5ctypeIwE2idE_nid_postfix = 0; -std::uint64_t _ZNSt7collateIwE2idE_nid_postfix = 0; -std::uint64_t _ZNSt7num_putIcSt19ostreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix = 0; -std::uintptr_t _ZTVSt7num_putIcSt19ostreambuf_iteratorIcSt11char_traitsIcEEE_nid_postfix[12] {}; +int APS5_VABI isupper_nid_postfix(int c) { return ClassifyCharacter(c, std::ctype_base::upper); } +int APS5_VABI islower_nid_postfix(int c) { return ClassifyCharacter(c, std::ctype_base::lower); } +int APS5_VABI isalpha_nid_postfix(int c) { return ClassifyCharacter(c, std::ctype_base::alpha); } +int APS5_VABI isdigit_nid_postfix(int c) { return ClassifyCharacter(c, std::ctype_base::digit); } +int APS5_VABI isalnum_nid_postfix(int c) { return ClassifyCharacter(c, std::ctype_base::alnum); } +int APS5_VABI isspace_nid_postfix(int c) { return ClassifyCharacter(c, std::ctype_base::space); } +int APS5_VABI isblank_nid_postfix(int c) { return ClassifyCharacter(c, std::ctype_base::blank); } +int APS5_VABI iscntrl_nid_postfix(int c) { return ClassifyCharacter(c, std::ctype_base::cntrl); } +int APS5_VABI isprint_nid_postfix(int c) { return ClassifyCharacter(c, std::ctype_base::print); } +int APS5_VABI isgraph_nid_postfix(int c) { return ClassifyCharacter(c, std::ctype_base::graph); } +int APS5_VABI ispunct_nid_postfix(int c) { return ClassifyCharacter(c, std::ctype_base::punct); } +int APS5_VABI isxdigit_nid_postfix(int c) { return ClassifyCharacter(c, std::ctype_base::xdigit); } +int APS5_VABI toupper_nid_postfix(int c) { return ConvertCharacter(c, true); } +int APS5_VABI tolower_nid_postfix(int c) { return ConvertCharacter(c, false); } std::streamoff _ZSt7_BADOFF_nid_postfix = -1; -std::fpos_t _ZSt4_Fpz_nid_postfix {}; -std::int32_t _ZNSt6locale2id7_Id_cntE_nid_postfix = 0; +std::fpos_t _ZSt4_Fpz_nid_postfix{}; -GuestLocale::Implementation* _ZSt21_sceLibcClassicLocale_nid_postfix = &g_classicLocale; +const std::locale* _ZSt21_sceLibcClassicLocale_nid_postfix = &std::locale::classic(); -void APS5_VABI _ZNSt8ios_baseD2Ev_nid_postfix(GuestLocale::IosBase* self) { +void APS5_VABI _ZNSt8ios_baseD2Ev_nid_postfix(std::ios_base* self) { if (self == nullptr) throw std::invalid_argument("ios_base destructor: null object"); - if (self->standardStream != 0 || self->storage != nullptr || self->callbacks != nullptr) throw std::runtime_error("ios_base destructor: unsupported stream storage or callbacks"); - if (self->locale != &_ZSt21_sceLibcClassicLocale_nid_postfix) throw std::runtime_error("ios_base destructor: unsupported locale ownership"); - self->locale = nullptr; + self->std::ios_base::~ios_base(); } -GuestLocale::Implementation* APS5_VABI _ZNSt6locale5_InitEv_nid_postfix() { - std::lock_guard lock(g_localeInitMutex); - if (!g_localeInitialized) { - g_localeInitialized = true; - } - return &g_classicLocale; +const std::locale* APS5_VABI _ZNSt6locale5_InitEv_nid_postfix() { + return &std::locale::classic(); } -void APS5_VABI _ZNSt6locale5facet9_RegisterEv_nid_postfix(GuestLocale::Facet* self) { - if (self == nullptr || self->vtable == nullptr) throw std::invalid_argument("locale register: invalid facet"); - std::lock_guard lock(g_localeInitMutex); - for (const auto* facet : g_registeredFacets) { - if (facet == self) throw std::runtime_error("locale register: duplicate facet"); - } - g_registeredFacets.push_back(self); +void APS5_VABI _ZNSt6locale5facet9_RegisterEv_nid_postfix(std::locale::facet*) { + NotImplemented_nid_no_patch("locale::facet::_Register"); } -GuestLocale::Implementation* APS5_VABI _ZNSt6locale16_GetgloballocaleEv_nid_postfix() { - return &g_classicLocale; +const std::locale* APS5_VABI _ZNSt6locale16_GetgloballocaleEv_nid_postfix() { + thread_local std::locale locale; + locale = std::locale(); + return &locale; } -void APS5_VABI _ZNSt7collateIwE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(GuestLocale::Facet**, const GuestLocale::Implementation*) { - NotImplemented_nid_no_patch(__func__); +void APS5_VABI _ZNSt7collateIwE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(const std::locale::facet**, const std::locale*) { + NotImplemented_nid_no_patch("collate::_Getcat"); } -void APS5_VABI _ZNSt8_LocinfoC1EPKc_nid_postfix(GuestLocale::LocinfoStorage* self, const char* localeName) { - if (self == nullptr || localeName == nullptr || std::strcmp(localeName, "C") != 0) throw std::invalid_argument("_Locinfo: only the C locale is supported"); - new (self) GuestLocale::LocinfoStorage{}; +void APS5_VABI _ZNSt8_LocinfoC1EPKc_nid_postfix(std::locale* self, const char* localeName) { + if (self == nullptr || localeName == nullptr) throw std::invalid_argument("Locale constructor: null argument"); + std::construct_at(self, localeName); } -void APS5_VABI _ZNSt8_LocinfoD1Ev_nid_postfix(GuestLocale::LocinfoStorage* self) { - if (self == nullptr) throw std::invalid_argument("_Locinfo destructor: null object"); +void APS5_VABI _ZNSt8_LocinfoD1Ev_nid_postfix(std::locale* self) { + if (self == nullptr) throw std::invalid_argument("Locale destructor: null object"); + std::destroy_at(self); } -int APS5_VABI _Mbtowcx_nid_postfix(std::uint16_t* dst, const char* src, std::size_t count, mbstate_t* st) { - if (dst == nullptr || src == nullptr || st == nullptr || count == 0) throw std::invalid_argument("_Mbtowcx: invalid conversion arguments"); - wchar_t converted{}; - const auto result = std::mbrtowc(&converted, src, count, st); +std::size_t APS5_VABI _Mbtowcx_nid_postfix(wchar_t* dst, const char* src, std::size_t count, std::mbstate_t* state) { + if (dst == nullptr || src == nullptr || state == nullptr || count == 0) throw std::invalid_argument("_Mbtowcx: invalid conversion arguments"); + const auto result = std::mbrtowc(dst, src, count, state); if (result == static_cast(-1)) throw std::runtime_error("_Mbtowcx: invalid multibyte character"); if (result == static_cast(-2)) throw std::runtime_error("_Mbtowcx: incomplete multibyte character"); - if (result > static_cast(std::numeric_limits::max()) || static_cast(converted) > 0xffff) throw std::runtime_error("_Mbtowcx: conversion exceeds guest character limits"); - *dst = static_cast(converted); - return static_cast(result); + return result; } -int APS5_VABI _Wctombx_nid_postfix(char* dst, std::uint16_t src, mbstate_t* st) { - if (dst == nullptr || st == nullptr) throw std::invalid_argument("_Wctombx: invalid conversion arguments"); - const auto result = std::wcrtomb(dst, static_cast(src), st); +std::size_t APS5_VABI _Wctombx_nid_postfix(char* dst, wchar_t src, std::mbstate_t* state) { + if (dst == nullptr || state == nullptr) throw std::invalid_argument("_Wctombx: invalid conversion arguments"); + const auto result = std::wcrtomb(dst, src, state); if (result == static_cast(-1)) throw std::runtime_error("_Wctombx: invalid wide character"); - if (result > static_cast(std::numeric_limits::max())) throw std::runtime_error("_Wctombx: conversion size exceeds guest limits"); - return static_cast(result); + return result; } -const short* APS5_VABI _Getpctype_nid_postfix() { - return g_classificationTable.data() + 1; +const std::ctype::mask* APS5_VABI _Getpctype_nid_postfix() { + return std::ctype::classic_table(); } -const short* APS5_VABI _Getptolower_nid_postfix() { - return g_lowerTable.data() + 1; +const int* APS5_VABI _Getptolower_nid_postfix() { + static const auto table = MakeCaseTable(false); + return table.data() + 1; } -const short* APS5_VABI _Getptoupper_nid_postfix() { - return g_upperTable.data() + 1; +const int* APS5_VABI _Getptoupper_nid_postfix() { + static const auto table = MakeCaseTable(true); + return table.data() + 1; } -mbstate_t* APS5_VABI _Getpmbstate_nid_postfix() { - thread_local mbstate_t state {}; +std::mbstate_t* APS5_VABI _Getpmbstate_nid_postfix() { + thread_local std::mbstate_t state{}; return &state; } -mbstate_t* APS5_VABI _Getpwcstate_nid_postfix() { - thread_local mbstate_t state {}; +std::mbstate_t* APS5_VABI _Getpwcstate_nid_postfix() { + thread_local std::mbstate_t state{}; return &state; } -wint_t APS5_VABI _Towctrans_nid_postfix(wint_t c, wctrans_t desc) { +std::wint_t APS5_VABI _Towctrans_nid_postfix(std::wint_t c, std::wctrans_t desc) { + if (desc == std::wctrans_t{}) throw std::invalid_argument("_Towctrans: invalid transformation"); return std::towctrans(c, desc); } diff --git a/core/libs/tests/GuestCharacter.cpp b/core/libs/tests/GuestCharacter.cpp index 60331fc87..ab299a450 100644 --- a/core/libs/tests/GuestCharacter.cpp +++ b/core/libs/tests/GuestCharacter.cpp @@ -18,8 +18,8 @@ int APS5_VABI ispunct_nid_postfix(int); int APS5_VABI isxdigit_nid_postfix(int); int APS5_VABI toupper_nid_postfix(int); int APS5_VABI tolower_nid_postfix(int); -const short* APS5_VABI _Getptolower_nid_postfix(); -const short* APS5_VABI _Getptoupper_nid_postfix(); +const int* APS5_VABI _Getptolower_nid_postfix(); +const int* APS5_VABI _Getptoupper_nid_postfix(); } static void Require(bool value) { if (!value) std::abort(); } int main() { diff --git a/core/libs/tests/GuestLocale.cpp b/core/libs/tests/GuestLocale.cpp deleted file mode 100644 index dc1635dd9..000000000 --- a/core/libs/tests/GuestLocale.cpp +++ /dev/null @@ -1,158 +0,0 @@ -#include "prx/libc/include/GuestLocale.hpp" -#include -#include -#include -#include -#include -#include -#include -#include - -extern "C" { -extern GuestLocale::Implementation* _ZSt21_sceLibcClassicLocale_nid_postfix; -GuestLocale::Implementation* APS5_VABI _ZNSt6locale16_GetgloballocaleEv_nid_postfix(); -GuestLocale::Implementation* APS5_VABI _ZNSt6locale5_InitEv_nid_postfix(); -void APS5_VABI _ZNSt6locale5facet9_RegisterEv_nid_postfix(GuestLocale::Facet* self); -void APS5_VABI _ZNSt8_LocinfoC1EPKc_nid_postfix(GuestLocale::LocinfoStorage* self, const char* localeName); -void APS5_VABI _ZNSt8_LocinfoD1Ev_nid_postfix(GuestLocale::LocinfoStorage* self); -void APS5_VABI _ZNSt8ios_baseD2Ev_nid_postfix(GuestLocale::IosBase* self); -const short* APS5_VABI _Getpctype_nid_postfix(); -const short* APS5_VABI _Getptolower_nid_postfix(); -const short* APS5_VABI _Getptoupper_nid_postfix(); -int APS5_VABI _Mbtowcx_nid_postfix(std::uint16_t* dst, const char* src, std::size_t count, std::mbstate_t* st); -int APS5_VABI _Wctombx_nid_postfix(char* dst, std::uint16_t src, std::mbstate_t* st); -void APS5_VABI _Locksyslock_nid_postfix(); -void APS5_VABI _Unlocksyslock_nid_postfix(); - -} - -static void Require(bool condition) { - if (!condition) throw std::runtime_error("Guest locale check failed"); -} - -template -static void RequireException(TAction action) { - bool caught = false; - try { - action(); - } catch (const std::exception&) { - caught = true; - } - Require(caught); -} - -static void CheckGuestCalls() { - auto* locale = _ZSt21_sceLibcClassicLocale_nid_postfix; - Require(locale != nullptr && locale == _ZNSt6locale16_GetgloballocaleEv_nid_postfix()); - Require(_ZNSt6locale5_InitEv_nid_postfix() == locale); - Require(_ZNSt6locale5_InitEv_nid_postfix() == locale); - const std::byte* table = nullptr; - std::memcpy(&table, locale, sizeof(table)); - void (APS5_VABI *retain)(GuestLocale::Facet*) = nullptr; - GuestLocale::Facet* (APS5_VABI *release)(GuestLocale::Facet*) = nullptr; - std::memcpy(&retain, table + 0x10, sizeof(retain)); - std::memcpy(&release, table + 0x18, sizeof(release)); - const auto initial = locale->base.references; - retain(&locale->base); - Require(locale->base.references == initial + 1); - Require(release(&locale->base) == nullptr); - Require(locale->base.references == initial); - RequireException([&] { release(&locale->base); }); - RequireException([&] { retain(nullptr); }); - RequireException([&] { locale->base.vtable->deleteObject(&locale->base); }); - Require(locale->base.references == initial); - std::vector threads; - for (int thread = 0; thread < 4; ++thread) { - threads.emplace_back([&] { - for (int iteration = 0; iteration < 10000; ++iteration) { - retain(&locale->base); - release(&locale->base); - } - }); - } - for (auto& thread : threads) thread.join(); - Require(locale->base.references == initial); - Require(locale->facetCount == 1 && locale->facets[0] == nullptr); - Require(std::strcmp(locale->name, "C") == 0 && !locale->transparent); -} - -static void CheckLocinfoAlignment() { - alignas(16) std::array bytes; - bytes.fill(std::byte{0x5a}); - auto* storage = reinterpret_cast(bytes.data() + 8); - _ZNSt8_LocinfoC1EPKc_nid_postfix(storage, "C"); - for (std::size_t index = 0; index < bytes.size(); ++index) Require(bytes[index] == (index >= 8 && index < 72 ? std::byte{} : std::byte{0x5a})); - _ZNSt8_LocinfoD1Ev_nid_postfix(storage); - RequireException([&] { _ZNSt8_LocinfoC1EPKc_nid_postfix(storage, "unknown-locale"); }); - RequireException([] { _ZNSt8_LocinfoC1EPKc_nid_postfix(nullptr, "C"); }); -} - -static void CheckCharacterTables() { - const auto* classification = _Getpctype_nid_postfix(); - const auto* lower = _Getptolower_nid_postfix(); - const auto* upper = _Getptoupper_nid_postfix(); - Require(classification[-1] == 0 && lower[-1] == -1 && upper[-1] == -1); - Require(classification['A'] == 0x03 && classification['a'] == 0x11); - Require(classification['G'] == 0x02 && classification['0'] == 0x21); - Require(classification[' '] == 0x04 && classification['\t'] == 0xc0); - Require(classification['\n'] == 0xc0 && classification['!'] == 0x08); - for (int value = 0; value < 256; ++value) { - Require(lower[value] == (value >= 'A' && value <= 'Z' ? value + 32 : value)); - Require(upper[value] == (value >= 'a' && value <= 'z' ? value - 32 : value)); - if (value >= 128) Require(classification[value] == 0); - } -} - -static void CheckCharacterConversions() { - const auto* classification = _Getpctype_nid_postfix(); - for (std::uint16_t value = 0; value < 128; ++value) { - std::array bytes{}; - bytes.fill('!'); - std::mbstate_t encodeState{}; - Require(_Wctombx_nid_postfix(bytes.data(), value, &encodeState) == 1); - Require(bytes[0] == static_cast(value) && bytes[1] == '!'); - std::array wide{0xffff, 0x1234}; - std::mbstate_t decodeState{}; - Require(_Mbtowcx_nid_postfix(wide.data(), bytes.data(), 2, &decodeState) == (value == 0 ? 0 : 1)); - Require(wide[0] == value && wide[1] == 0x1234); - const bool alnum = (value >= '0' && value <= '9') || (value >= 'A' && value <= 'Z') || (value >= 'a' && value <= 'z'); - Require(((classification[static_cast(bytes[0])] & 0x232) != 0) == alnum); - } - std::mbstate_t state{}; - char byte{}; - std::uint16_t wide{}; - RequireException([&] { _Mbtowcx_nid_postfix(&wide, "]", 0, &state); }); - RequireException([&] { _Mbtowcx_nid_postfix(nullptr, "]", 1, &state); }); - RequireException([&] { _Mbtowcx_nid_postfix(&wide, nullptr, 1, &state); }); - RequireException([&] { _Mbtowcx_nid_postfix(&wide, "]", 1, nullptr); }); - RequireException([&] { _Wctombx_nid_postfix(nullptr, ']', &state); }); - RequireException([&] { _Wctombx_nid_postfix(&byte, ']', nullptr); }); -} - -static void CheckStreamDestruction() { - struct GuardedStream { - std::uint64_t before = 0x12345678; - GuestLocale::IosBase stream{}; - std::uint64_t after = 0x87654321; - } object; - object.stream.locale = &_ZSt21_sceLibcClassicLocale_nid_postfix; - _ZNSt8ios_baseD2Ev_nid_postfix(&object.stream); - Require(object.stream.locale == nullptr && object.before == 0x12345678 && object.after == 0x87654321); - object.stream.locale = &_ZSt21_sceLibcClassicLocale_nid_postfix; - object.stream.callbacks = &object; - RequireException([&] { _ZNSt8ios_baseD2Ev_nid_postfix(&object.stream); }); - Require(object.stream.locale == &_ZSt21_sceLibcClassicLocale_nid_postfix); -} - -int main() { - CheckGuestCalls(); - CheckLocinfoAlignment(); - CheckCharacterTables(); - CheckCharacterConversions(); - CheckStreamDestruction(); - _Locksyslock_nid_postfix(); - _Locksyslock_nid_postfix(); - _Unlocksyslock_nid_postfix(); - _Unlocksyslock_nid_postfix(); - std::cout << "Guest locale checks passed\n"; -} From 52b683d20845481ee44022608af98616b4f7bfe6 Mon Sep 17 00:00:00 2001 From: boykopovar Date: Sun, 27 Sep 2026 10:23:28 +0300 Subject: [PATCH 88/94] fix(relinker): skip generated guest prx files during sce module scan --- core/relinker/relinker/include/relinker/guest/GuestImage.hpp | 2 ++ core/relinker/relinker/src/guest/GuestImageReader.cpp | 2 +- core/relinker/relinker/src/guest/GuestModuleBuilder.cpp | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/core/relinker/relinker/include/relinker/guest/GuestImage.hpp b/core/relinker/relinker/include/relinker/guest/GuestImage.hpp index a71a33289..71cca90fa 100644 --- a/core/relinker/relinker/include/relinker/guest/GuestImage.hpp +++ b/core/relinker/relinker/include/relinker/guest/GuestImage.hpp @@ -7,6 +7,8 @@ namespace Relinker { +inline constexpr char GuestModuleSuffix[] = ".guest.prx"; + struct GuestSymbol { std::string Name; std::uint8_t Info; diff --git a/core/relinker/relinker/src/guest/GuestImageReader.cpp b/core/relinker/relinker/src/guest/GuestImageReader.cpp index 5b3a8abc8..d0e2afb41 100644 --- a/core/relinker/relinker/src/guest/GuestImageReader.cpp +++ b/core/relinker/relinker/src/guest/GuestImageReader.cpp @@ -27,7 +27,7 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector range(Io::ReadU64(bytes, 32), static_cast(Io::ReadU16(bytes, 56)) * 56); GuestImage image; image.SourcePath = path; - image.OutputName = path.filename().string() + ".guest.prx"; + image.OutputName = path.filename().string() + GuestModuleSuffix; image.Headers = ElfReader(bytes).ReadProgramHeaders(); const Domain::ProgramHeader* dynamic = nullptr; const Domain::ProgramHeader* dynlib = nullptr; diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index c461ffb3c..f8fd3e803 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -26,6 +26,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path if (!std::filesystem::is_directory(directory)) throw Domain::RelinkerException("Guest module path is not a directory: " + directory.string()); std::vector paths; for (const auto& entry : std::filesystem::directory_iterator(directory)) { + if (entry.path().filename().string().ends_with(GuestModuleSuffix)) continue; if (!entry.is_regular_file()) continue; std::ifstream stream(entry.path(), std::ios::binary); if (!stream) throw Domain::RelinkerException("Cannot read guest candidate: " + entry.path().string()); From e3dc9a6acc6ee85520524e98e5be468c70c77d43 Mon Sep 17 00:00:00 2001 From: boykopovar Date: Sun, 27 Sep 2026 12:41:52 +0300 Subject: [PATCH 89/94] fix(relinker): accept zero alignment for empty tls segments --- core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp | 2 +- core/relinker/relinker/src/guest/GuestImageReader.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp index 58ee3eb62..f0dd778e7 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp @@ -91,7 +91,7 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co return {}; } - if (tls->FileSize > tls->MemorySize || tls->Offset > source.size() || tls->FileSize > source.size() - tls->Offset || !std::has_single_bit(tls->Alignment) || tls->Alignment > 8192 || tls->MemorySize > 0x7fff0000u) + if (tls->FileSize > tls->MemorySize || tls->Offset > source.size() || tls->FileSize > source.size() - tls->Offset || (tls->Alignment > 1 && !std::has_single_bit(tls->Alignment)) || tls->Alignment > 8192 || tls->MemorySize > 0x7fff0000u) throw Domain::RelinkerException("Invalid or unsupported ELF TLS layout", tls->Offset); // Native homebrew linkers can emit an empty PT_TLS placeholder. // It needs no Windows TLS directory unless guest code accesses TLS. diff --git a/core/relinker/relinker/src/guest/GuestImageReader.cpp b/core/relinker/relinker/src/guest/GuestImageReader.cpp index d0e2afb41..8dec3cf8a 100644 --- a/core/relinker/relinker/src/guest/GuestImageReader.cpp +++ b/core/relinker/relinker/src/guest/GuestImageReader.cpp @@ -54,7 +54,7 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector dynlib = &header; } if (header.Type == 7) { - if (tls != nullptr || header.FileSize > header.MemorySize || !std::has_single_bit(header.Alignment)) fail("Invalid TLS segment"); + if (tls != nullptr || header.FileSize > header.MemorySize || (header.Alignment > 1 && !std::has_single_bit(header.Alignment))) fail("Invalid TLS segment"); tls = &header; } } From 43d2e8b4dc41d3bfcf7b5934e11c1ef21704b0b0 Mon Sep 17 00:00:00 2001 From: boykopovar Date: Sun, 27 Sep 2026 12:49:46 +0300 Subject: [PATCH 90/94] fix(codegen): decode xadd byte and popcnt instruction lengths fix --- .../codegen/include/codegen/x86/X64OpcodeConstants.hpp | 2 ++ core/relinker/codegen/src/x86/X64InstructionDecoder.cpp | 6 ++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/core/relinker/codegen/include/codegen/x86/X64OpcodeConstants.hpp b/core/relinker/codegen/include/codegen/x86/X64OpcodeConstants.hpp index 237fb8bb7..7448093df 100644 --- a/core/relinker/codegen/include/codegen/x86/X64OpcodeConstants.hpp +++ b/core/relinker/codegen/include/codegen/x86/X64OpcodeConstants.hpp @@ -153,6 +153,8 @@ inline constexpr std::uint8_t TwoByteShldCl = 0xA5; inline constexpr std::uint8_t TwoByteShrdImm8 = 0xAC; inline constexpr std::uint8_t TwoByteShrdCl = 0xAD; inline constexpr std::uint8_t TwoByteGrp15 = 0xAE; +inline constexpr std::uint8_t TwoBytePopcnt = 0xB8; +inline constexpr std::uint8_t TwoByteXaddRm8 = 0xC0; inline constexpr std::uint8_t TwoByteXadd = 0xC1; inline constexpr std::uint8_t TwoBytePextrw = 0xC5; inline constexpr std::uint8_t TwoByteShufpsImm8 = 0xC2; diff --git a/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp b/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp index d574fef6c..665cf6fd2 100644 --- a/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp +++ b/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp @@ -246,7 +246,8 @@ std::size_t X64InstructionDecoder::Decode(const std::uint8_t* data, std::size_t opcode == TwoByteImulRmModRm || opcode == TwoByteGrp7 || opcode == TwoByteGrp15 || - opcode == TwoByteXadd || + opcode == TwoBytePopcnt || + opcode == TwoByteXaddRm8 || opcode == TwoByteXadd || opcode == TwoByteGrp9 || opcode == TwoByteNopModRm || opcode == TwoByteEndbr) { hasModRm = true; @@ -497,7 +498,8 @@ DecodedInstructionInfo X64InstructionDecoder::DecodeInstruction( (op >= TwoBytePrefetchGrpMin && op <= TwoBytePrefetchGrpMax) || (op >= TwoByteSetccMin && op <= TwoByteSetccMax) || op == TwoByteImulRmModRm || op == TwoByteGrp7 || - op == TwoByteGrp15 || op == TwoByteXadd || + op == TwoBytePopcnt || + op == TwoByteGrp15 || op == TwoByteXaddRm8 || op == TwoByteXadd || op == TwoByteGrp9 || op == TwoByteNopModRm || op == TwoByteEndbr || op == TwoByteMovImm8ModRm || (op >= TwoByteShiftImm8Min && op <= TwoByteShiftImm8Max) || From 0722f3e5fc92ba684832842aa5d8dd88e1ee2cea Mon Sep 17 00:00:00 2001 From: Pouare514 Date: Sun, 27 Sep 2026 11:57:42 +0200 Subject: [PATCH 91/94] prx: add libRenoirCore.PS5 replacement with 23 Renoir stubs for libcohtml Minecraft Legends' libcohtml.Prospero.prx imports 23 NIDs from lib libRenoirCore.PS5_v1 (docs/legends-unresolved-nids.json). Clear names are unknown, so each export is exposed under its raw NID via APS5_EXPORT alias (_nid_no_patch_cut), which nid_patcher strips to the literal NID instead of hashing. Stubs follow KytyPS5 UnresolvedImportStub policy (log call + return 0) via APS5_LOG_OUT rather than throwing NotImplemented. --- core/libs/prx/libRenoirCore.PS5/Export.cpp | 158 +++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 core/libs/prx/libRenoirCore.PS5/Export.cpp diff --git a/core/libs/prx/libRenoirCore.PS5/Export.cpp b/core/libs/prx/libRenoirCore.PS5/Export.cpp new file mode 100644 index 000000000..a502a8535 --- /dev/null +++ b/core/libs/prx/libRenoirCore.PS5/Export.cpp @@ -0,0 +1,158 @@ +#include +#include + +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +// libRenoirCore.PS5 replacement (game-custom renderer lib, e.g. Minecraft Legends). +// +// The 23 NIDs below are imported by libcohtml.Prospero.prx (lib libRenoirCore.PS5_v1) +// and their clear names are unknown (obfuscated NIDs), so each export is exposed +// with its raw NID string via APS5_EXPORT (alias "_nid_no_patch_cut", which the +// nid_patcher strips to the literal NID instead of hashing it). +// +// Stub policy mirrors KytyPS5's UnresolvedImportStub (log the call, return 0) rather +// than the local NotImplemented_nid_no_patch helper (which throws): Renoir calls may +// happen per-frame and must not unwind through the guest. + +extern "C" { + +APS5_EXPORT("0DsDTr534gc", renoirStub00); +uint64_t APS5_VABI renoirStub00(void) { + APS5_LOG_OUT("stub NID=%s called", "0DsDTr534gc"); + return 0; +} + +APS5_EXPORT("2T56HgbPW08", renoirStub01); +uint64_t APS5_VABI renoirStub01(void) { + APS5_LOG_OUT("stub NID=%s called", "2T56HgbPW08"); + return 0; +} + +APS5_EXPORT("3HnCozh8Az8", renoirStub02); +uint64_t APS5_VABI renoirStub02(void) { + APS5_LOG_OUT("stub NID=%s called", "3HnCozh8Az8"); + return 0; +} + +APS5_EXPORT("5Ysce+s-CWU", renoirStub03); +uint64_t APS5_VABI renoirStub03(void) { + APS5_LOG_OUT("stub NID=%s called", "5Ysce+s-CWU"); + return 0; +} + +APS5_EXPORT("AdlSqA4lGS8", renoirStub04); +uint64_t APS5_VABI renoirStub04(void) { + APS5_LOG_OUT("stub NID=%s called", "AdlSqA4lGS8"); + return 0; +} + +APS5_EXPORT("bC37hk7rHqs", renoirStub05); +uint64_t APS5_VABI renoirStub05(void) { + APS5_LOG_OUT("stub NID=%s called", "bC37hk7rHqs"); + return 0; +} + +APS5_EXPORT("dfPK9R6M0qM", renoirStub06); +uint64_t APS5_VABI renoirStub06(void) { + APS5_LOG_OUT("stub NID=%s called", "dfPK9R6M0qM"); + return 0; +} + +APS5_EXPORT("fZiBbVa2ZDU", renoirStub07); +uint64_t APS5_VABI renoirStub07(void) { + APS5_LOG_OUT("stub NID=%s called", "fZiBbVa2ZDU"); + return 0; +} + +APS5_EXPORT("hkWBH8-WfW4", renoirStub08); +uint64_t APS5_VABI renoirStub08(void) { + APS5_LOG_OUT("stub NID=%s called", "hkWBH8-WfW4"); + return 0; +} + +APS5_EXPORT("hUxvGdi64Io", renoirStub09); +uint64_t APS5_VABI renoirStub09(void) { + APS5_LOG_OUT("stub NID=%s called", "hUxvGdi64Io"); + return 0; +} + +APS5_EXPORT("NxqsZxGisrA", renoirStub10); +uint64_t APS5_VABI renoirStub10(void) { + APS5_LOG_OUT("stub NID=%s called", "NxqsZxGisrA"); + return 0; +} + +APS5_EXPORT("OgZVIkaNmb0", renoirStub11); +uint64_t APS5_VABI renoirStub11(void) { + APS5_LOG_OUT("stub NID=%s called", "OgZVIkaNmb0"); + return 0; +} + +APS5_EXPORT("ojLQdRUnVgA", renoirStub12); +uint64_t APS5_VABI renoirStub12(void) { + APS5_LOG_OUT("stub NID=%s called", "ojLQdRUnVgA"); + return 0; +} + +APS5_EXPORT("s0qDfgsAgVg", renoirStub13); +uint64_t APS5_VABI renoirStub13(void) { + APS5_LOG_OUT("stub NID=%s called", "s0qDfgsAgVg"); + return 0; +} + +APS5_EXPORT("sgrd-r1Nw60", renoirStub14); +uint64_t APS5_VABI renoirStub14(void) { + APS5_LOG_OUT("stub NID=%s called", "sgrd-r1Nw60"); + return 0; +} + +APS5_EXPORT("sjTRb8YD7dQ", renoirStub15); +uint64_t APS5_VABI renoirStub15(void) { + APS5_LOG_OUT("stub NID=%s called", "sjTRb8YD7dQ"); + return 0; +} + +APS5_EXPORT("tG86XGc2VtE", renoirStub16); +uint64_t APS5_VABI renoirStub16(void) { + APS5_LOG_OUT("stub NID=%s called", "tG86XGc2VtE"); + return 0; +} + +APS5_EXPORT("umlGxOcNzgc", renoirStub17); +uint64_t APS5_VABI renoirStub17(void) { + APS5_LOG_OUT("stub NID=%s called", "umlGxOcNzgc"); + return 0; +} + +APS5_EXPORT("vPxA9OFW0os", renoirStub18); +uint64_t APS5_VABI renoirStub18(void) { + APS5_LOG_OUT("stub NID=%s called", "vPxA9OFW0os"); + return 0; +} + +APS5_EXPORT("X2qEBlOLkNo", renoirStub19); +uint64_t APS5_VABI renoirStub19(void) { + APS5_LOG_OUT("stub NID=%s called", "X2qEBlOLkNo"); + return 0; +} + +APS5_EXPORT("ynh4E+BIaqY", renoirStub20); +uint64_t APS5_VABI renoirStub20(void) { + APS5_LOG_OUT("stub NID=%s called", "ynh4E+BIaqY"); + return 0; +} + +APS5_EXPORT("YXe6ABbHZPs", renoirStub21); +uint64_t APS5_VABI renoirStub21(void) { + APS5_LOG_OUT("stub NID=%s called", "YXe6ABbHZPs"); + return 0; +} + +APS5_EXPORT("z0jsXZVz75I", renoirStub22); +uint64_t APS5_VABI renoirStub22(void) { + APS5_LOG_OUT("stub NID=%s called", "z0jsXZVz75I"); + return 0; +} + +} From 179841caff2945c6d6c4f2f77d054e47269fb27a Mon Sep 17 00:00:00 2001 From: boykopovar Date: Sun, 27 Sep 2026 13:10:06 +0300 Subject: [PATCH 92/94] feat(relinker): report full module path when windows loading fails --- .../elfpatcher/src/windows/WindowsEntryStubBuilder.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp index c0bde9668..94d25b832 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp @@ -82,6 +82,7 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con const auto loading = addString("Loading PRX: "); const auto loaded = addString(" -> OK\n"); const auto loadFailed = addString(" -> FAILED\n"); + const auto failedModule = addString("Failed to load module: "); const auto errorPrefix = addString("GetLastError: "); const auto messageSeparator = addString(" - "); const auto newline = addString("\n"); @@ -257,6 +258,9 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con const auto loadSucceeded = code.Branch({0x0f, 0x85}); captureLastError(); writeString(loadFailed, true); + writeString(failedModule, true); + writeString(resolvedPaths[index], true); + writeString(newline, true); writeLastError(); if (dependencyDiagnostics) { code.Rip({0x48, 0x8d, 0x0d}, resolvedPaths[index]); From a719359cd18dd2903e86396b882236296b0b2392 Mon Sep 17 00:00:00 2001 From: Pouare514 Date: Sun, 27 Sep 2026 15:49:18 +0200 Subject: [PATCH 93/94] prx: make libRenoirCore.PS5 stubs throw NotImplemented --- core/libs/prx/libRenoirCore.PS5/Export.cpp | 105 +++++++++------------ 1 file changed, 46 insertions(+), 59 deletions(-) diff --git a/core/libs/prx/libRenoirCore.PS5/Export.cpp b/core/libs/prx/libRenoirCore.PS5/Export.cpp index a502a8535..9130b50f3 100644 --- a/core/libs/prx/libRenoirCore.PS5/Export.cpp +++ b/core/libs/prx/libRenoirCore.PS5/Export.cpp @@ -1,158 +1,145 @@ #include -#include - #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" -// libRenoirCore.PS5 replacement (game-custom renderer lib, e.g. Minecraft Legends). -// -// The 23 NIDs below are imported by libcohtml.Prospero.prx (lib libRenoirCore.PS5_v1) -// and their clear names are unknown (obfuscated NIDs), so each export is exposed -// with its raw NID string via APS5_EXPORT (alias "_nid_no_patch_cut", which the -// nid_patcher strips to the literal NID instead of hashing it). -// -// Stub policy mirrors KytyPS5's UnresolvedImportStub (log the call, return 0) rather -// than the local NotImplemented_nid_no_patch helper (which throws): Renoir calls may -// happen per-frame and must not unwind through the guest. - extern "C" { APS5_EXPORT("0DsDTr534gc", renoirStub00); uint64_t APS5_VABI renoirStub00(void) { - APS5_LOG_OUT("stub NID=%s called", "0DsDTr534gc"); - return 0; + NotImplemented_nid_no_patch(__func__); + return 0; } APS5_EXPORT("2T56HgbPW08", renoirStub01); uint64_t APS5_VABI renoirStub01(void) { - APS5_LOG_OUT("stub NID=%s called", "2T56HgbPW08"); - return 0; + NotImplemented_nid_no_patch(__func__); + return 0; } APS5_EXPORT("3HnCozh8Az8", renoirStub02); uint64_t APS5_VABI renoirStub02(void) { - APS5_LOG_OUT("stub NID=%s called", "3HnCozh8Az8"); - return 0; + NotImplemented_nid_no_patch(__func__); + return 0; } APS5_EXPORT("5Ysce+s-CWU", renoirStub03); uint64_t APS5_VABI renoirStub03(void) { - APS5_LOG_OUT("stub NID=%s called", "5Ysce+s-CWU"); - return 0; + NotImplemented_nid_no_patch(__func__); + return 0; } APS5_EXPORT("AdlSqA4lGS8", renoirStub04); uint64_t APS5_VABI renoirStub04(void) { - APS5_LOG_OUT("stub NID=%s called", "AdlSqA4lGS8"); - return 0; + NotImplemented_nid_no_patch(__func__); + return 0; } APS5_EXPORT("bC37hk7rHqs", renoirStub05); uint64_t APS5_VABI renoirStub05(void) { - APS5_LOG_OUT("stub NID=%s called", "bC37hk7rHqs"); - return 0; + NotImplemented_nid_no_patch(__func__); + return 0; } APS5_EXPORT("dfPK9R6M0qM", renoirStub06); uint64_t APS5_VABI renoirStub06(void) { - APS5_LOG_OUT("stub NID=%s called", "dfPK9R6M0qM"); - return 0; + NotImplemented_nid_no_patch(__func__); + return 0; } APS5_EXPORT("fZiBbVa2ZDU", renoirStub07); uint64_t APS5_VABI renoirStub07(void) { - APS5_LOG_OUT("stub NID=%s called", "fZiBbVa2ZDU"); - return 0; + NotImplemented_nid_no_patch(__func__); + return 0; } APS5_EXPORT("hkWBH8-WfW4", renoirStub08); uint64_t APS5_VABI renoirStub08(void) { - APS5_LOG_OUT("stub NID=%s called", "hkWBH8-WfW4"); - return 0; + NotImplemented_nid_no_patch(__func__); + return 0; } APS5_EXPORT("hUxvGdi64Io", renoirStub09); uint64_t APS5_VABI renoirStub09(void) { - APS5_LOG_OUT("stub NID=%s called", "hUxvGdi64Io"); - return 0; + NotImplemented_nid_no_patch(__func__); + return 0; } APS5_EXPORT("NxqsZxGisrA", renoirStub10); uint64_t APS5_VABI renoirStub10(void) { - APS5_LOG_OUT("stub NID=%s called", "NxqsZxGisrA"); - return 0; + NotImplemented_nid_no_patch(__func__); + return 0; } APS5_EXPORT("OgZVIkaNmb0", renoirStub11); uint64_t APS5_VABI renoirStub11(void) { - APS5_LOG_OUT("stub NID=%s called", "OgZVIkaNmb0"); - return 0; + NotImplemented_nid_no_patch(__func__); + return 0; } APS5_EXPORT("ojLQdRUnVgA", renoirStub12); uint64_t APS5_VABI renoirStub12(void) { - APS5_LOG_OUT("stub NID=%s called", "ojLQdRUnVgA"); - return 0; + NotImplemented_nid_no_patch(__func__); + return 0; } APS5_EXPORT("s0qDfgsAgVg", renoirStub13); uint64_t APS5_VABI renoirStub13(void) { - APS5_LOG_OUT("stub NID=%s called", "s0qDfgsAgVg"); - return 0; + NotImplemented_nid_no_patch(__func__); + return 0; } APS5_EXPORT("sgrd-r1Nw60", renoirStub14); uint64_t APS5_VABI renoirStub14(void) { - APS5_LOG_OUT("stub NID=%s called", "sgrd-r1Nw60"); - return 0; + NotImplemented_nid_no_patch(__func__); + return 0; } APS5_EXPORT("sjTRb8YD7dQ", renoirStub15); uint64_t APS5_VABI renoirStub15(void) { - APS5_LOG_OUT("stub NID=%s called", "sjTRb8YD7dQ"); - return 0; + NotImplemented_nid_no_patch(__func__); + return 0; } APS5_EXPORT("tG86XGc2VtE", renoirStub16); uint64_t APS5_VABI renoirStub16(void) { - APS5_LOG_OUT("stub NID=%s called", "tG86XGc2VtE"); - return 0; + NotImplemented_nid_no_patch(__func__); + return 0; } APS5_EXPORT("umlGxOcNzgc", renoirStub17); uint64_t APS5_VABI renoirStub17(void) { - APS5_LOG_OUT("stub NID=%s called", "umlGxOcNzgc"); - return 0; + NotImplemented_nid_no_patch(__func__); + return 0; } APS5_EXPORT("vPxA9OFW0os", renoirStub18); uint64_t APS5_VABI renoirStub18(void) { - APS5_LOG_OUT("stub NID=%s called", "vPxA9OFW0os"); - return 0; + NotImplemented_nid_no_patch(__func__); + return 0; } APS5_EXPORT("X2qEBlOLkNo", renoirStub19); uint64_t APS5_VABI renoirStub19(void) { - APS5_LOG_OUT("stub NID=%s called", "X2qEBlOLkNo"); - return 0; + NotImplemented_nid_no_patch(__func__); + return 0; } APS5_EXPORT("ynh4E+BIaqY", renoirStub20); uint64_t APS5_VABI renoirStub20(void) { - APS5_LOG_OUT("stub NID=%s called", "ynh4E+BIaqY"); - return 0; + NotImplemented_nid_no_patch(__func__); + return 0; } APS5_EXPORT("YXe6ABbHZPs", renoirStub21); uint64_t APS5_VABI renoirStub21(void) { - APS5_LOG_OUT("stub NID=%s called", "YXe6ABbHZPs"); - return 0; + NotImplemented_nid_no_patch(__func__); + return 0; } APS5_EXPORT("z0jsXZVz75I", renoirStub22); uint64_t APS5_VABI renoirStub22(void) { - APS5_LOG_OUT("stub NID=%s called", "z0jsXZVz75I"); - return 0; + NotImplemented_nid_no_patch(__func__); + return 0; } } From b6dea0b51b268d084adb06986cbe41e9214be725 Mon Sep 17 00:00:00 2001 From: Pouare514 Date: Sun, 27 Sep 2026 15:53:27 +0200 Subject: [PATCH 94/94] Renoir stubs throw with NID name for progress monitoring --- core/libs/prx/libRenoirCore.PS5/Export.cpp | 46 +++++++++++----------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/core/libs/prx/libRenoirCore.PS5/Export.cpp b/core/libs/prx/libRenoirCore.PS5/Export.cpp index 9130b50f3..c0cf955a3 100644 --- a/core/libs/prx/libRenoirCore.PS5/Export.cpp +++ b/core/libs/prx/libRenoirCore.PS5/Export.cpp @@ -6,139 +6,139 @@ extern "C" { APS5_EXPORT("0DsDTr534gc", renoirStub00); uint64_t APS5_VABI renoirStub00(void) { - NotImplemented_nid_no_patch(__func__); + NotImplemented_nid_no_patch("0DsDTr534gc"); return 0; } APS5_EXPORT("2T56HgbPW08", renoirStub01); uint64_t APS5_VABI renoirStub01(void) { - NotImplemented_nid_no_patch(__func__); + NotImplemented_nid_no_patch("2T56HgbPW08"); return 0; } APS5_EXPORT("3HnCozh8Az8", renoirStub02); uint64_t APS5_VABI renoirStub02(void) { - NotImplemented_nid_no_patch(__func__); + NotImplemented_nid_no_patch("3HnCozh8Az8"); return 0; } APS5_EXPORT("5Ysce+s-CWU", renoirStub03); uint64_t APS5_VABI renoirStub03(void) { - NotImplemented_nid_no_patch(__func__); + NotImplemented_nid_no_patch("5Ysce+s-CWU"); return 0; } APS5_EXPORT("AdlSqA4lGS8", renoirStub04); uint64_t APS5_VABI renoirStub04(void) { - NotImplemented_nid_no_patch(__func__); + NotImplemented_nid_no_patch("AdlSqA4lGS8"); return 0; } APS5_EXPORT("bC37hk7rHqs", renoirStub05); uint64_t APS5_VABI renoirStub05(void) { - NotImplemented_nid_no_patch(__func__); + NotImplemented_nid_no_patch("bC37hk7rHqs"); return 0; } APS5_EXPORT("dfPK9R6M0qM", renoirStub06); uint64_t APS5_VABI renoirStub06(void) { - NotImplemented_nid_no_patch(__func__); + NotImplemented_nid_no_patch("dfPK9R6M0qM"); return 0; } APS5_EXPORT("fZiBbVa2ZDU", renoirStub07); uint64_t APS5_VABI renoirStub07(void) { - NotImplemented_nid_no_patch(__func__); + NotImplemented_nid_no_patch("fZiBbVa2ZDU"); return 0; } APS5_EXPORT("hkWBH8-WfW4", renoirStub08); uint64_t APS5_VABI renoirStub08(void) { - NotImplemented_nid_no_patch(__func__); + NotImplemented_nid_no_patch("hkWBH8-WfW4"); return 0; } APS5_EXPORT("hUxvGdi64Io", renoirStub09); uint64_t APS5_VABI renoirStub09(void) { - NotImplemented_nid_no_patch(__func__); + NotImplemented_nid_no_patch("hUxvGdi64Io"); return 0; } APS5_EXPORT("NxqsZxGisrA", renoirStub10); uint64_t APS5_VABI renoirStub10(void) { - NotImplemented_nid_no_patch(__func__); + NotImplemented_nid_no_patch("NxqsZxGisrA"); return 0; } APS5_EXPORT("OgZVIkaNmb0", renoirStub11); uint64_t APS5_VABI renoirStub11(void) { - NotImplemented_nid_no_patch(__func__); + NotImplemented_nid_no_patch("OgZVIkaNmb0"); return 0; } APS5_EXPORT("ojLQdRUnVgA", renoirStub12); uint64_t APS5_VABI renoirStub12(void) { - NotImplemented_nid_no_patch(__func__); + NotImplemented_nid_no_patch("ojLQdRUnVgA"); return 0; } APS5_EXPORT("s0qDfgsAgVg", renoirStub13); uint64_t APS5_VABI renoirStub13(void) { - NotImplemented_nid_no_patch(__func__); + NotImplemented_nid_no_patch("s0qDfgsAgVg"); return 0; } APS5_EXPORT("sgrd-r1Nw60", renoirStub14); uint64_t APS5_VABI renoirStub14(void) { - NotImplemented_nid_no_patch(__func__); + NotImplemented_nid_no_patch("sgrd-r1Nw60"); return 0; } APS5_EXPORT("sjTRb8YD7dQ", renoirStub15); uint64_t APS5_VABI renoirStub15(void) { - NotImplemented_nid_no_patch(__func__); + NotImplemented_nid_no_patch("sjTRb8YD7dQ"); return 0; } APS5_EXPORT("tG86XGc2VtE", renoirStub16); uint64_t APS5_VABI renoirStub16(void) { - NotImplemented_nid_no_patch(__func__); + NotImplemented_nid_no_patch("tG86XGc2VtE"); return 0; } APS5_EXPORT("umlGxOcNzgc", renoirStub17); uint64_t APS5_VABI renoirStub17(void) { - NotImplemented_nid_no_patch(__func__); + NotImplemented_nid_no_patch("umlGxOcNzgc"); return 0; } APS5_EXPORT("vPxA9OFW0os", renoirStub18); uint64_t APS5_VABI renoirStub18(void) { - NotImplemented_nid_no_patch(__func__); + NotImplemented_nid_no_patch("vPxA9OFW0os"); return 0; } APS5_EXPORT("X2qEBlOLkNo", renoirStub19); uint64_t APS5_VABI renoirStub19(void) { - NotImplemented_nid_no_patch(__func__); + NotImplemented_nid_no_patch("X2qEBlOLkNo"); return 0; } APS5_EXPORT("ynh4E+BIaqY", renoirStub20); uint64_t APS5_VABI renoirStub20(void) { - NotImplemented_nid_no_patch(__func__); + NotImplemented_nid_no_patch("ynh4E+BIaqY"); return 0; } APS5_EXPORT("YXe6ABbHZPs", renoirStub21); uint64_t APS5_VABI renoirStub21(void) { - NotImplemented_nid_no_patch(__func__); + NotImplemented_nid_no_patch("YXe6ABbHZPs"); return 0; } APS5_EXPORT("z0jsXZVz75I", renoirStub22); uint64_t APS5_VABI renoirStub22(void) { - NotImplemented_nid_no_patch(__func__); + NotImplemented_nid_no_patch("z0jsXZVz75I"); return 0; }