diff --git a/.github/workflows/emscripten-libraries.yml b/.github/workflows/emscripten-libraries.yml new file mode 100644 index 00000000000..790bf599a3c --- /dev/null +++ b/.github/workflows/emscripten-libraries.yml @@ -0,0 +1,43 @@ +name: Emscripten library portability + +on: + pull_request: + paths: + - .github/workflows/emscripten-libraries.yml + - core/libs/** + push: + branches: [main] + paths: + - .github/workflows/emscripten-libraries.yml + - core/libs/** + +permissions: + contents: read + +jobs: + library-portability: + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + - name: Compile native library sources and test guest long doubles + run: | + cmake -S core/libs/tests/emscripten -B build-native -DCMAKE_BUILD_TYPE=Release + cmake --build build-native --parallel 2 + ctest --test-dir build-native --output-on-failure + - uses: actions/setup-node@v6 + with: + node-version: '26' + - name: Compile memory64 libraries and test conversions and module errors + shell: bash + run: | + ANYPS5_TEST_NODE="$(command -v node)" + git clone --depth 1 https://github.com/emscripten-core/emsdk.git "$RUNNER_TEMP/emsdk" + "$RUNNER_TEMP/emsdk/emsdk" install 6.0.12 + "$RUNNER_TEMP/emsdk/emsdk" activate 6.0.12 + source "$RUNNER_TEMP/emsdk/emsdk_env.sh" + emcmake cmake -S core/libs/tests/emscripten -B build-portable -DCMAKE_BUILD_TYPE=Release -DANYPS5_NODE="$ANYPS5_TEST_NODE" + cmake --build build-portable --parallel 2 + ctest --test-dir build-portable --output-on-failure diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 6feaf0d5248..2103f34a7ce 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #endif diff --git a/core/libs/prx/libSceSysmodule/Export.cpp b/core/libs/prx/libSceSysmodule/Export.cpp index c95e6793c0a..72f6ab0f89e 100644 --- a/core/libs/prx/libSceSysmodule/Export.cpp +++ b/core/libs/prx/libSceSysmodule/Export.cpp @@ -17,7 +17,7 @@ #include #include #include -#else +#elif !defined(__EMSCRIPTEN__) #include "prx/libc/include/specifics/linux/HostMapping.hpp" #endif @@ -149,6 +149,8 @@ bool fillModuleInfoForUnwind(std::uint64_t addr, ModuleInfoForUnwind* info) { info->seg0_addr = region; info->seg0_size = size; return true; +#elif defined(__EMSCRIPTEN__) + return sceKernelGetModuleInfoForUnwind(addr, 0, info) == 0; #else const auto mapping = HostMapping::Find(addr, "sceSysmoduleGetModuleInfoForUnwind"); if (!mapping) { diff --git a/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp b/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp index 555617fb169..39a987f0262 100644 --- a/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp +++ b/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp @@ -18,13 +18,17 @@ #include #include "prx/libSceAgcDriver/Execution/include/VideoOutput.hpp" #include "prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp" +#ifndef __EMSCRIPTEN__ #include "prx/libSceVideoOut/include/DisplayWindow.hpp" -#include "prx/libSceVideoOut/include/BufferReuseTracker.hpp" #include "prx/libSceVideoOut/include/HostHdrProbe.hpp" +#endif +#include "prx/libSceVideoOut/include/BufferReuseTracker.hpp" #include "prx/libc/include/Shutdown.hpp" #include "prx/libkernel/Time/include/TimedWait.hpp" +#ifndef __EMSCRIPTEN__ #include "SDL.h" +#endif #include "SceTypes.hpp" static constexpr int VIDEO_OUT_ERROR_INVALID_VALUE = -2144796671; @@ -255,8 +259,10 @@ class VideoOutDriver { std::array, VIDEO_OUT_NUM_MAX> outputs; std::shared_ptr flipQueue = std::make_shared(); +#ifndef __EMSCRIPTEN__ DisplayWindow window; HostHdrProbe hdrProbe; +#endif std::uint64_t hdrGeneration = 0; std::atomic displayRefreshRate{0}; std::atomic displayResolution{VIDEO_OUT_RESOLUTION_1080P}; diff --git a/core/libs/prx/libc/include/GuestLongDouble.hpp b/core/libs/prx/libc/include/GuestLongDouble.hpp new file mode 100644 index 00000000000..aac64777166 --- /dev/null +++ b/core/libs/prx/libc/include/GuestLongDouble.hpp @@ -0,0 +1,54 @@ +#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_GUESTLONGDOUBLE_HPP +#define CORE_LIBS_PRX_LIBC_INCLUDE_GUESTLONGDOUBLE_HPP + +#include +#include +#include + +namespace LibcDetail { + +inline constexpr std::size_t GuestLongDoubleBytes = 16; + +static_assert(std::numeric_limits::digits == 64 || std::numeric_limits::digits == 113); + +inline long double GuestLongDoubleLoad(const void* source) { + long double value; + static_assert(sizeof(value) == GuestLongDoubleBytes); + if constexpr (std::numeric_limits::digits == 64) { + std::memcpy(&value, source, sizeof(value)); + } else { + std::uint64_t mantissa; + std::uint16_t signExponent; + std::memcpy(&mantissa, source, sizeof(mantissa)); + std::memcpy(&signExponent, static_cast(source) + 8, sizeof(signExponent)); + const std::uint64_t fraction = mantissa & 0x7FFF'FFFF'FFFF'FFFFull; + const std::uint64_t low = fraction << 49; + const std::uint64_t high = (fraction >> 15) | (static_cast(signExponent & 0x7FFF) << 48) | (static_cast(signExponent >> 15) << 63); + std::memcpy(&value, &low, sizeof(low)); + std::memcpy(reinterpret_cast(&value) + 8, &high, sizeof(high)); + } + return value; +} + +inline void GuestLongDoubleStore(void* target, long double value) { + if constexpr (std::numeric_limits::digits == 64) { + std::memcpy(target, &value, sizeof(value)); + } else { + std::uint64_t low; + std::uint64_t high; + std::memcpy(&low, &value, sizeof(low)); + std::memcpy(&high, reinterpret_cast(&value) + 8, sizeof(high)); + const auto exponent = static_cast((high >> 48) & 0x7FFF); + const std::uint64_t fraction = ((high & 0xFFFF'FFFF'FFFFull) << 15) | (low >> 49); + const std::uint64_t mantissa = fraction | (exponent != 0 ? 0x8000'0000'0000'0000ull : 0); + const auto signExponent = static_cast(exponent | ((high >> 63) << 15)); + unsigned char bytes[GuestLongDoubleBytes] = {}; + std::memcpy(bytes, &mantissa, sizeof(mantissa)); + std::memcpy(bytes + 8, &signExponent, sizeof(signExponent)); + std::memcpy(target, bytes, sizeof(bytes)); + } +} + +} + +#endif diff --git a/core/libs/prx/libc/include/WindowsFormatting.hpp b/core/libs/prx/libc/include/WindowsFormatting.hpp index 928635470a6..306705573d2 100644 --- a/core/libs/prx/libc/include/WindowsFormatting.hpp +++ b/core/libs/prx/libc/include/WindowsFormatting.hpp @@ -2,6 +2,7 @@ #define CORE_LIBS_PRX_LIBC_INCLUDE_WINDOWSFORMATTING_HPP #include "SceTypes.hpp" +#include "prx/libc/include/GuestLongDouble.hpp" #include #include #include @@ -87,9 +88,13 @@ class FormatArguments { args.overflow_arg_area = static_cast(args.overflow_arg_area) + 8; } } - T value; - std::memcpy(&value, address, sizeof(value)); - return value; + if constexpr (std::is_same_v) { + return GuestLongDoubleLoad(address); + } else { + T value; + std::memcpy(&value, address, sizeof(value)); + return value; + } } }; @@ -664,8 +669,6 @@ inline int FormatWindows(char* buffer, size_t size, const char* format, const vo output.Value(spec + "ll" + conversion, value); } else if (std::strchr("aAeEfFgG", conversion)) { if (length == "L") { - static_assert(sizeof(long double) == 16); - static_assert(std::numeric_limits::digits == 64); const long double value = args.Next(); if (std::isnan(value)) { output.Value(PaddedStringSpec(spec), NanText(spec, std::signbit(value), conversion < 'a').c_str()); diff --git a/core/libs/prx/libc/include/general/ExportMacros.hpp b/core/libs/prx/libc/include/general/ExportMacros.hpp index c175c74f2bf..6f6f833d1d9 100644 --- a/core/libs/prx/libc/include/general/ExportMacros.hpp +++ b/core/libs/prx/libc/include/general/ExportMacros.hpp @@ -1,7 +1,9 @@ #ifndef CORE_LIBS_PRX_LIBC_INCLUDE_GENERAL_EXPORTMACROS_HPP #define CORE_LIBS_PRX_LIBC_INCLUDE_GENERAL_EXPORTMACROS_HPP -#if defined(__APPLE__) +#if defined(__EMSCRIPTEN__) && !defined(APS5_WASM_EXPORT_SCAN) + #define APS5_EXPORT(exportName, funcName) +#elif defined(__APPLE__) #define APS5_EXPORT(exportName, funcName) \ __asm__(".globl \"_" exportName "_nid_no_patch_cut\"\n\t" \ ".set \"_" exportName "_nid_no_patch_cut\", _" #funcName) diff --git a/core/libs/prx/libc/include/general/VabiMacros.hpp b/core/libs/prx/libc/include/general/VabiMacros.hpp index 1eed45e459b..49994ae1e48 100644 --- a/core/libs/prx/libc/include/general/VabiMacros.hpp +++ b/core/libs/prx/libc/include/general/VabiMacros.hpp @@ -3,6 +3,8 @@ #ifdef _WIN32 #define APS5_VABI __attribute__((sysv_abi)) +#elif defined(APS5_WASM_EXPORT_SCAN) +#define APS5_VABI APS5_VABI #else #define APS5_VABI #endif diff --git a/core/libs/prx/libc/include/specifics/itanium/CxxAbi.hpp b/core/libs/prx/libc/include/specifics/itanium/CxxAbi.hpp index f4957fd7b3e..b38b463e14b 100644 --- a/core/libs/prx/libc/include/specifics/itanium/CxxAbi.hpp +++ b/core/libs/prx/libc/include/specifics/itanium/CxxAbi.hpp @@ -3,6 +3,12 @@ #include +#ifdef __EMSCRIPTEN__ +namespace __cxxabiv1 { +extern "C" int __cxa_thread_atexit(void (*)(void*), void*, void*) noexcept; +} +#endif + #ifdef _LIBCPPABI_VERSION #include diff --git a/core/libs/prx/libc/src/ApplicationProcessParameters.cpp b/core/libs/prx/libc/src/ApplicationProcessParameters.cpp index 183aa320376..ed5f1fb309f 100644 --- a/core/libs/prx/libc/src/ApplicationProcessParameters.cpp +++ b/core/libs/prx/libc/src/ApplicationProcessParameters.cpp @@ -10,7 +10,7 @@ #define NOMINMAX #endif #include -#elif defined(__linux__) || defined(__APPLE__) +#elif defined(__linux__) || defined(__APPLE__) || defined(__EMSCRIPTEN__) #include "prx/libc/include/specifics/linux/ElfTypes.hpp" #include #endif @@ -88,7 +88,7 @@ const void* ApplicationProcessParameters_nid_no_patch() { } if (!found) throw std::runtime_error("application heap: process metadata is missing; relink the executable"); return result; -#elif defined(__linux__) || defined(__APPLE__) +#elif defined(__linux__) || defined(__APPLE__) || defined(__EMSCRIPTEN__) struct Search { const void* parameters = nullptr; bool invalid = false; diff --git a/core/libs/prx/libc/src/General.cpp b/core/libs/prx/libc/src/General.cpp index 972d2f2070b..9ed57a5fb06 100644 --- a/core/libs/prx/libc/src/General.cpp +++ b/core/libs/prx/libc/src/General.cpp @@ -581,6 +581,9 @@ std::atomic g_hostStandardError{-1}; } extern "C" int KeepHostStandardError_nid_no_patch() { +#ifdef __EMSCRIPTEN__ + return 2; +#else const std::lock_guard lock(g_hostStandardErrorMutex); if (const int kept = g_hostStandardError.load(); kept >= 0) return kept; const int kept = ::fcntl(2, F_DUPFD_CLOEXEC, 3); @@ -599,6 +602,7 @@ extern "C" int KeepHostStandardError_nid_no_patch() { stderr = stream; g_hostStandardError.store(kept); return kept; +#endif } extern "C" int HostStandardError_nid_no_patch() { @@ -611,6 +615,9 @@ std::atomic g_hostStandardOutput{-1}; } extern "C" int KeepHostStandardOutput_nid_no_patch() { +#ifdef __EMSCRIPTEN__ + NotImplemented_nid_no_patch(__func__); +#else const std::lock_guard lock(g_hostStandardOutputMutex); if (const int kept = g_hostStandardOutput.load(); kept >= 0) return kept; const int kept = ::fcntl(1, F_DUPFD_CLOEXEC, 3); @@ -629,6 +636,7 @@ extern "C" int KeepHostStandardOutput_nid_no_patch() { stdout = stream; g_hostStandardOutput.store(kept); return kept; +#endif } extern "C" int DuplicateHostDescriptor_nid_no_patch(int descriptor) { diff --git a/core/libs/prx/libc/src/GuestAllocations.cpp b/core/libs/prx/libc/src/GuestAllocations.cpp index cca11ea1e4b..e0988c8c62c 100644 --- a/core/libs/prx/libc/src/GuestAllocations.cpp +++ b/core/libs/prx/libc/src/GuestAllocations.cpp @@ -103,7 +103,7 @@ void recordChange(void* mutation, const void* pointer, std::size_t bytes) { if (mutation != nullptr && bytes != 0) static_cast(mutation)->changed.emplace_back(reinterpret_cast(pointer), bytes); } -#if !defined(_WIN32) && !defined(__APPLE__) +#if !defined(_WIN32) && !defined(__APPLE__) && !defined(__EMSCRIPTEN__) std::vector> fileBackedWritableImage() { const auto image = std::filesystem::read_symlink("/proc/self/exe").string(); std::ifstream maps("/proc/self/maps"); @@ -631,7 +631,7 @@ struct RangePatch { }; RangePatch replaceRange(const void* pointer, std::size_t bytes, bool remove, bool readable, bool writable, bool gpu) { - const auto address = reinterpret_cast(pointer); + const std::uint64_t address = reinterpret_cast(pointer); require(bytes != 0 && bytes <= std::numeric_limits::max() - address, "invalid guest protection or unmap range"); require(!writable || readable, "writable guest allocation must be readable"); const auto end = address + bytes; diff --git a/core/libs/prx/libc/src/GuestArena.cpp b/core/libs/prx/libc/src/GuestArena.cpp index bbe4fda6b94..948c8d841a7 100644 --- a/core/libs/prx/libc/src/GuestArena.cpp +++ b/core/libs/prx/libc/src/GuestArena.cpp @@ -17,6 +17,7 @@ #include #endif #include +#include #include #include #include @@ -56,6 +57,11 @@ constexpr std::uintptr_t SystemReservedStart = 0x00000007FFFFC000ull + GuestAren constexpr std::uintptr_t SystemReservedEnd = 0x0000001000000000ull + GuestArenaOffset; constexpr std::uintptr_t ApplicationAreaEnd = 0x000000FC00000000ull + GuestArenaOffset; +#ifdef __EMSCRIPTEN__ +constexpr std::size_t EmscriptenArenaAlignment = std::size_t{1} << 32; +constexpr std::size_t EmscriptenArenaBytes = std::size_t{8} << 30; +#endif + std::uintptr_t alignUp(std::uintptr_t value, std::size_t alignment) { return (value + alignment - 1) & ~(static_cast(alignment) - 1); } @@ -152,6 +158,9 @@ class Arena { std::lock_guard lock(_lock); const auto start = reinterpret_cast(pointer); const auto end = start + bytes; +#ifdef __EMSCRIPTEN__ + NoteDirtyLocked(start, end); +#endif auto it = _used.upper_bound(start); if (it != _used.begin()) --it; while (it != _used.end() && it->first < end) { @@ -172,7 +181,12 @@ class Arena { private: Arena() { -#if defined(__linux__) || defined(__APPLE__) +#if defined(__EMSCRIPTEN__) + void* const region = std::aligned_alloc(EmscriptenArenaAlignment, EmscriptenArenaBytes); + if (region == nullptr) throw std::runtime_error("guest arena: cannot reserve the guest range in WebAssembly memory"); + _base = reinterpret_cast(region); + _end = _base + EmscriptenArenaBytes; +#elif defined(__linux__) || defined(__APPLE__) SeedHostUsedRanges(); #elif defined(_WIN32) _writeWatched = std::getenv("APS5_NO_WRITE_WATCH") == nullptr; @@ -308,6 +322,46 @@ class Arena { } #endif +#ifdef __EMSCRIPTEN__ + void NoteDirtyLocked(std::uintptr_t start, std::uintptr_t end) { + auto it = _dirty.upper_bound(start); + if (it != _dirty.begin() && std::prev(it)->second >= start) --it; + while (it != _dirty.end() && it->first <= end) { + start = std::min(start, it->first); + end = std::max(end, it->second); + it = _dirty.erase(it); + } + _dirty.emplace(start, end); + } + +public: + void NoteDirty(const void* pointer, std::size_t bytes) { + std::lock_guard lock(_lock); + const auto start = reinterpret_cast(pointer); + NoteDirtyLocked(start, start + bytes); + } + + void ZeroDirty(void* pointer, std::size_t bytes) { + std::lock_guard lock(_lock); + const auto start = reinterpret_cast(pointer); + const auto end = start + bytes; + auto it = _dirty.upper_bound(start); + if (it != _dirty.begin() && std::prev(it)->second > start) --it; + while (it != _dirty.end() && it->first < end) { + const auto [dirtyStart, dirtyEnd] = *it; + const auto first = std::max(dirtyStart, start); + const auto last = std::min(dirtyEnd, end); + std::memset(reinterpret_cast(first), 0, last - first); + it = _dirty.erase(it); + if (dirtyStart < first) _dirty.emplace(dirtyStart, first); + if (dirtyEnd > last) it = _dirty.emplace(last, dirtyEnd).first; + } + } + +private: + std::map _dirty; +#endif + std::mutex _lock; std::map _used; std::vector> _holes; @@ -353,6 +407,9 @@ class Arena { #ifndef _WIN32 void ReleaseHostRegion(const void* pointer, std::size_t bytes) { +#ifdef __EMSCRIPTEN__ + throw std::runtime_error("guest arena: host region release is unsupported in WebAssembly"); +#else Release(pointer, bytes); std::lock_guard lock(_lock); const auto start = reinterpret_cast(pointer); @@ -368,6 +425,7 @@ class Arena { } _hostRegions = std::move(kept); static_cast(ReserveNoReplace(start, bytes)); +#endif } #endif }; @@ -615,6 +673,24 @@ void GuestArenaReset_nid_postfix(void* pointer, std::size_t bytes) { if (!Arena::Get().Contains(pointer, bytes)) throw OutsideArena("reset", pointer, bytes); if (PlaceAt(pointer, bytes, 0x01) == MAP_FAILED) throw std::system_error(errno, std::generic_category(), "guest arena reset failed"); } +#elif defined(__EMSCRIPTEN__) +void GuestArenaSetProtection_nid_postfix(std::uintptr_t address, std::size_t bytes, std::uint32_t protection) { + static_cast(protection); + if (!Arena::Get().Contains(reinterpret_cast(address), bytes)) throw OutsideArena("set protection", reinterpret_cast(address), bytes); +} + +void GuestArenaCommit_nid_postfix(void* pointer, std::size_t bytes, std::uint32_t protection, std::size_t granule) { + static_cast(protection); + static_cast(granule); + if (!Arena::Get().Contains(pointer, bytes)) throw OutsideArena("commit", pointer, bytes); + Arena::Get().MarkUsedForCommit(pointer, bytes); + Arena::Get().ZeroDirty(pointer, bytes); +} + +void GuestArenaReset_nid_postfix(void* pointer, std::size_t bytes) { + if (!Arena::Get().Contains(pointer, bytes)) throw OutsideArena("reset", pointer, bytes); + Arena::Get().NoteDirty(pointer, bytes); +} #endif bool GuestArenaWriteWatched_nid_postfix() { diff --git a/core/libs/prx/libc/src/ScanningWide.cpp b/core/libs/prx/libc/src/ScanningWide.cpp index cccfa535b14..2d7ba443b09 100644 --- a/core/libs/prx/libc/src/ScanningWide.cpp +++ b/core/libs/prx/libc/src/ScanningWide.cpp @@ -1,4 +1,5 @@ #include "prx/libc/include/Api.hpp" +#include "prx/libc/include/GuestLongDouble.hpp" #include #include #include @@ -366,7 +367,6 @@ class WideScanner { } Outcome Float(const Directive& directive) { - static_assert(sizeof(long double) == 16 && std::numeric_limits::digits == 64); if (!directive.length.empty() && directive.length != "l" && directive.length != "L") Fail("invalid floating length modifier " + directive.length); void* target = nullptr; @@ -378,8 +378,7 @@ class WideScanner { if (target != nullptr) { const int savedError = errno; if (directive.length == "L") { - const long double value = std::strtold(field.c_str(), nullptr); - std::memcpy(target, &value, sizeof(value)); + LibcDetail::GuestLongDoubleStore(target, std::strtold(field.c_str(), nullptr)); } else if (directive.length == "l") { const double value = std::strtod(field.c_str(), nullptr); std::memcpy(target, &value, sizeof(value)); diff --git a/core/libs/prx/libc/src/Strings.cpp b/core/libs/prx/libc/src/Strings.cpp index 95439954308..f86f66de3f7 100644 --- a/core/libs/prx/libc/src/Strings.cpp +++ b/core/libs/prx/libc/src/Strings.cpp @@ -831,8 +831,10 @@ float APS5_VABI wcstof_nid_postfix(const char16_t* str, char16_t** endptr) { } long double APS5_VABI wcstold_nid_postfix(const char16_t* str, char16_t** endptr) { +#ifndef __EMSCRIPTEN__ static_assert(sizeof(long double) == 16); static_assert(std::numeric_limits::digits == 64); +#endif return ParseAsciiPrefix(str, endptr, [](const char* text, char** end) { return ParseFloat(text, end, std::strtold); }); } diff --git a/core/libs/prx/libc/src/specifics/linux/DlIteratePhdr.cpp b/core/libs/prx/libc/src/specifics/linux/DlIteratePhdr.cpp index 516a53933a9..0feae7ca9e6 100644 --- a/core/libs/prx/libc/src/specifics/linux/DlIteratePhdr.cpp +++ b/core/libs/prx/libc/src/specifics/linux/DlIteratePhdr.cpp @@ -8,7 +8,7 @@ extern "C" { int APS5_VABI dl_iterate_phdr_nid_postfix(int (*callback)(dl_phdr_info*, std::size_t, void*), void* data) { -#if defined(__linux__) || defined(__APPLE__) +#if defined(__linux__) || defined(__APPLE__) || defined(__EMSCRIPTEN__) return dl_iterate_phdr(callback, data); #else (void)callback; diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp index 72f44ac6db5..fd6e14827b5 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp @@ -31,6 +31,10 @@ #include #include #include +#elif defined(__EMSCRIPTEN__) +#include +#include +#include #else #include #include "prx/libc/include/WindowsDescriptor.hpp" @@ -380,7 +384,9 @@ class SectionPool { class PhysicalBacking { public: PhysicalBacking(std::size_t bytes, int memoryType, std::uint64_t start) : memoryType(memoryType), bytes(bytes) { -#ifdef _WIN32 +#if defined(__EMSCRIPTEN__) + static_cast(start); +#elif defined(_WIN32) static_cast(start); const auto size = static_cast(bytes); if (SectionPoolEnabled()) section = SectionPool::Get().Take(bytes); @@ -415,7 +421,8 @@ class PhysicalBacking { #endif ~PhysicalBacking() { -#ifdef _WIN32 +#if defined(__EMSCRIPTEN__) +#elif defined(_WIN32) if (!SectionPoolEnabled()) { GuestArena::GuestArenaForgetSection_nid_postfix(section); CloseHandle(section); @@ -431,7 +438,11 @@ class PhysicalBacking { PhysicalBacking& operator=(const PhysicalBacking&) = delete; void Map(std::uintptr_t address, std::size_t bytes, std::uint64_t offset, int protection) const { -#ifdef _WIN32 +#if defined(__EMSCRIPTEN__) + static_cast(offset); + static_cast(protection); + GuestArena::GuestArenaCommit_nid_postfix(reinterpret_cast(address), bytes, 0x04, PS5_PAGE_SIZE); +#elif defined(_WIN32) GuestArena::GuestArenaMapDirect_nid_postfix(reinterpret_cast(address), bytes, section, offset, WinProtFromPosix(protection)); #else if (::mmap(reinterpret_cast(address), bytes, protection, MAP_SHARED | MAP_FIXED, file, static_cast(fileOffset + offset)) == MAP_FAILED) throw std::system_error(errno, std::generic_category(), "map direct memory backing"); @@ -441,7 +452,8 @@ class PhysicalBacking { private: int memoryType; std::size_t bytes; -#ifdef _WIN32 +#if defined(__EMSCRIPTEN__) +#elif defined(_WIN32) HANDLE section = nullptr; #else int file = -1; @@ -707,6 +719,9 @@ bool WriteSharedBacking(std::uintptr_t address, const void* source, std::size_t void AddMapping(std::uintptr_t address, std::size_t len, std::uint64_t phys, int nativeProt, bool records = true) { ValidatePhysicalRange(phys, len); if (records) EraseMappings(address, address + len); +#ifdef __EMSCRIPTEN__ + if (records && !MappingsSharingPhys(phys, phys + len).empty()) throw std::system_error(ENOTSUP, std::generic_category(), "WebAssembly memory cannot map one direct memory range at two addresses"); +#endif const auto last = phys + len; auto it = g_physBlocks.upper_bound(phys); --it; @@ -779,6 +794,9 @@ bool RemapFixedIntoRegistered(GuestAllocations::Mutation& mutation, void* addr, #ifdef _WIN32 GuestArena::GuestArenaReset_nid_postfix(addr, len); if (nativeProtection != PROT_NONE) CommitArenaRange(addr, len, WinProtFromPosix(nativeProtection)); +#elif defined(__EMSCRIPTEN__) + GuestArena::GuestArenaReset_nid_postfix(addr, len); + GuestArena::GuestArenaCommit_nid_postfix(addr, len, 0x04, PS5_PAGE_SIZE); #else if (::mmap(addr, len, nativeProtection, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0) == MAP_FAILED) throw std::system_error(errno, std::generic_category(), "remap anonymous memory"); GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(addr, len); @@ -789,11 +807,17 @@ bool RemapFixedIntoRegistered(GuestAllocations::Mutation& mutation, void* addr, return true; } -#if defined(__linux__) +#if defined(__linux__) || defined(__EMSCRIPTEN__) bool ArenaAvailable() { return GuestArena::GuestArenaAvailable_nid_postfix(); } void* ArenaCommit(void* addr, size_t len, int prot) { +#ifdef __EMSCRIPTEN__ + static_cast(prot); + GuestArena::GuestArenaCommit_nid_postfix(addr, len, 0x04, PS5_PAGE_SIZE); + return addr; +#else return mmap(addr, len, prot, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0); +#endif } void ArenaForget(void* addr, size_t len) { @@ -802,7 +826,7 @@ void ArenaForget(void* addr, size_t len) { #endif void Unmap(void* addr, size_t len) { -#if defined(__linux__) +#if defined(__linux__) || defined(__EMSCRIPTEN__) if (ArenaAvailable() && GuestArena::GuestArenaContains_nid_postfix(addr, len)) GuestArena::GuestArenaReset_nid_postfix(addr, len); else if (munmap(addr, len) != 0) throw std::system_error(errno, std::generic_category(), "munmap failed"); @@ -854,7 +878,7 @@ void* MapPlaced(void* addr, size_t len, int prot, int flags, size_t alignment) { } } #endif -#if defined(__linux__) +#if defined(__linux__) || defined(__EMSCRIPTEN__) if (ArenaAvailable() && !GuestArena::GuestArenaContains_nid_postfix(addr, len)) throw std::invalid_argument("fixed mapping is outside the guest address space arena"); if (ArenaAvailable()) { @@ -875,7 +899,7 @@ void* MapPlaced(void* addr, size_t len, int prot, int flags, size_t alignment) { return result; } if (addr) { -#if defined(__linux__) +#if defined(__linux__) || defined(__EMSCRIPTEN__) if (ArenaAvailable()) { void* placed = GuestArena::GuestArenaAllocateFromHint_nid_postfix( reinterpret_cast(addr), len, alignment); @@ -883,7 +907,11 @@ void* MapPlaced(void* addr, size_t len, int prot, int flags, size_t alignment) { ArenaForget(placed, len); throw std::system_error(errno, std::generic_category(), "Arena mapping failed"); } +#ifdef __EMSCRIPTEN__ + throw std::runtime_error("the guest address space arena is unavailable"); +#else return MapAtOrAbove(reinterpret_cast(addr), len, prot, alignment); +#endif #else return mmap_aligned(len, prot, alignment, reinterpret_cast(addr)); #endif @@ -891,7 +919,7 @@ void* MapPlaced(void* addr, size_t len, int prot, int flags, size_t alignment) { #ifdef _WIN32 return mmap_aligned(len, prot, alignment); #endif -#if defined(__linux__) +#if defined(__linux__) || defined(__EMSCRIPTEN__) if (ArenaAvailable()) { void* placed = GuestArena::GuestArenaAllocate_nid_postfix(len, alignment); if (void* committed = ArenaCommit(placed, len, prot); committed != MAP_FAILED) return committed; @@ -1010,7 +1038,7 @@ void UnmapRegistered(GuestAllocations::Mutation& mutation, void* addr, size_t le auto* pieceAddress = const_cast(piece); std::lock_guard lock(g_directLock); EraseMappings(reinterpret_cast(piece), reinterpret_cast(piece) + pieceBytes); -#if defined(__linux__) +#if defined(__linux__) || defined(__EMSCRIPTEN__) Unmap(pieceAddress, pieceBytes); #else if (KernelArena::Get().Contains(pieceAddress, pieceBytes)) munmap(pieceAddress, pieceBytes); @@ -1191,6 +1219,13 @@ int DoMapFileBacked_nid_no_patch(void** addr, size_t len, int prot, int flags, i RecordProtection(mapped, len, prot); Trace("map file %p+0x%zx offset=0x%llx prot=0x%x flags=0x%x", mapped, len, static_cast(offset), prot, flags); return 0; +#elif defined(__EMSCRIPTEN__) + static_cast(prot); + static_cast(flags); + static_cast(descriptor); + static_cast(offset); + static_cast(alignment); + throw std::system_error(ENOTSUP, std::generic_category(), "file-backed guest mmap is not supported in WebAssembly"); #else if ((flags & 3) != 1) throw std::system_error(ENOTSUP, std::generic_category(), "map file: a private file mapping is not mapped from the file on Windows"); const HANDLE file = WindowsDescriptorHandle_nid_no_patch(descriptor); @@ -1248,11 +1283,14 @@ int DoMapShared(void** addr, size_t len, int prot, HostSharedMemory object) { GuestAllocations::Mutation mutation; void* mapped = MapPlaced(*addr, len, PROT_NONE, 0, PS5_PAGE_SIZE); try { -#ifdef _WIN32 +#if defined(_WIN32) GuestArena::GuestArenaMap_nid_postfix(mapped, len, object, 0, WinProtFromPosix(nativeProtection)); #elif defined(__APPLE__) if (::mmap(mapped, len, nativeProtection & ~PROT_EXEC, MAP_SHARED | MAP_FIXED, object, 0) == MAP_FAILED) throw std::system_error(errno, std::generic_category(), "map shared memory"); if ((nativeProtection & PROT_EXEC) != 0 && ::mprotect(mapped, len, nativeProtection) != 0) throw std::system_error(errno, std::generic_category(), "make shared memory executable"); +#elif defined(__EMSCRIPTEN__) + static_cast(object); + throw std::system_error(ENOTSUP, std::generic_category(), "shared memory mappings are not supported in WebAssembly"); #else if (::mmap(mapped, len, nativeProtection, MAP_SHARED | MAP_FIXED, object, 0) == MAP_FAILED) throw std::system_error(errno, std::generic_category(), "map shared memory"); #endif @@ -1373,10 +1411,12 @@ int DoMprotect(const void* addr, size_t len, int prot) { const bool protectedPages = error == 0; #else bool protectedPages = mprotect(insidePointer, insideBytes, nativeProtection) == 0; +#if defined(__linux__) if (!protectedPages && errno == ENOMEM) { MapUnmappedPages(insidePointer, static_cast((stackEnd & ~hostPageMask) - inside)); protectedPages = mprotect(insidePointer, insideBytes, nativeProtection) == 0; } +#endif #endif if (!protectedPages) throw std::system_error(errno, std::generic_category(), "mprotect failed"); if (inside != requested) RecordProtection(requestedPointer, static_cast(inside - requested), prot); diff --git a/core/libs/prx/libkernel/Equeue/src/FileEvent.cpp b/core/libs/prx/libkernel/Equeue/src/FileEvent.cpp index aaf4d1b5700..ba078db53b6 100644 --- a/core/libs/prx/libkernel/Equeue/src/FileEvent.cpp +++ b/core/libs/prx/libkernel/Equeue/src/FileEvent.cpp @@ -23,6 +23,8 @@ #include #include #include +#elif defined(__EMSCRIPTEN__) +#include #else #include #include @@ -68,6 +70,7 @@ class VnodeWatch { stopSignal = ends[1]; ::fcntl(stopEvent, F_SETFD, FD_CLOEXEC); ::fcntl(stopSignal, F_SETFD, FD_CLOEXEC); +#elif defined(__EMSCRIPTEN__) #else stopEvent = ::eventfd(0, EFD_CLOEXEC); if (stopEvent < 0) { @@ -465,6 +468,19 @@ int OpenHostWatch(int descriptor, std::uint32_t, HostWatch& host) { return 0; } +#elif defined(__EMSCRIPTEN__) + +struct HostWatch {}; + +void CloseHostWatch(HostWatch&) {} + +void RunHostWatch(std::shared_ptr, HostWatch) {} + +int OpenHostWatch(int, std::uint32_t, HostWatch&) { + NotImplemented_nid_no_patch("EVFILT_VNODE in WebAssembly"); + return GUEST_EINVAL; +} + #else struct HostWatch { diff --git a/core/libs/prx/libkernel/File/src/NativeStat.cpp b/core/libs/prx/libkernel/File/src/NativeStat.cpp index 2902da11524..100bf5a9a3e 100644 --- a/core/libs/prx/libkernel/File/src/NativeStat.cpp +++ b/core/libs/prx/libkernel/File/src/NativeStat.cpp @@ -138,7 +138,7 @@ static void CopyNativeStat(const NativeStat& st, FileStat* sb) { #if defined(__APPLE__) sb->st_birthtim.tv_sec = static_cast(st.st_birthtimespec.tv_sec); sb->st_birthtim.tv_nsec = static_cast(st.st_birthtimespec.tv_nsec); -#elif defined(__linux__) +#elif defined(__linux__) || defined(__EMSCRIPTEN__) sb->st_birthtim = sb->st_ctim; #else sb->st_birthtim.tv_sec = static_cast(st.st_birthtim.tv_sec); diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index f858af87e23..5c611806cbc 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -1290,7 +1290,11 @@ int APS5_VABI sceKernelGetdirentries(int fd, char* buf, int nbytes, int64_t* bas if (base < 0) return SceErrorFromErrno(errno); if (basep != nullptr) *basep = static_cast(base); std::vector host(static_cast(nbytes) < 4096 ? 4096 : static_cast(nbytes)); +#ifdef __EMSCRIPTEN__ + const auto read = ::getdents(fd, reinterpret_cast(host.data()), host.size()); +#else const auto read = ::syscall(SYS_getdents64, fd, host.data(), host.size()); +#endif if (read < 0) return SceErrorFromErrno(errno); std::size_t written = 0; off_t resume = base; diff --git a/core/libs/prx/libkernel/Module/src/Module.cpp b/core/libs/prx/libkernel/Module/src/Module.cpp index f1921c47526..2cff3fbf727 100644 --- a/core/libs/prx/libkernel/Module/src/Module.cpp +++ b/core/libs/prx/libkernel/Module/src/Module.cpp @@ -23,7 +23,7 @@ #include #include #include -#else +#elif !defined(__EMSCRIPTEN__) #include #include "prx/libc/include/specifics/linux/HostMapping.hpp" #endif @@ -111,7 +111,7 @@ extern "C" void ForgetUnloadedUnwindImages_nid_no_patch() { std::lock_guard lock(unwindImagesMutex); for (const auto base : unloaded) UnwindImages().erase(base); } -#elif !defined(__APPLE__) +#elif !defined(__APPLE__) && !defined(__EMSCRIPTEN__) namespace { struct ImageSearch { @@ -181,7 +181,10 @@ int APS5_VABI sceKernelDlsym(KernelModule handle, const char* symbol, void** add int APS5_VABI sceKernelGetModuleInfoForUnwind(uint64_t addr, int flags, ModuleInfoForUnwind* info) { (void)flags; if (!info) return SCE_KERNEL_ERROR_EFAULT; -#ifdef _WIN32 +#if defined(__EMSCRIPTEN__) + static_cast(addr); + NotImplemented_nid_no_patch(__func__); +#elif defined(_WIN32) if (FillKnownUnwindImage(addr, info)) return 0; MEMORY_BASIC_INFORMATION mbi{}; if (!VirtualQuery(reinterpret_cast(addr), &mbi, sizeof(mbi))) return SCE_KERNEL_ERROR_ESRCH; diff --git a/core/libs/prx/libkernel/System/src/Process.cpp b/core/libs/prx/libkernel/System/src/Process.cpp index 2519d2c3530..f288fbcd595 100644 --- a/core/libs/prx/libkernel/System/src/Process.cpp +++ b/core/libs/prx/libkernel/System/src/Process.cpp @@ -713,9 +713,11 @@ int APS5_VABI getresgid_nid_postfix(std::uint32_t* rgid, std::uint32_t* egid, st return 0; } +#ifndef __EMSCRIPTEN__ void APS5_VABI exit_nid_postfix(int code) { LibcExit_nid_no_patch(code); } +#endif [[noreturn]] void APS5_VABI _exit_nid_postfix(int status) { LibcTerminate_nid_no_patch(status); @@ -1230,7 +1232,7 @@ int APS5_VABI getdtablesize_nid_postfix(void) { getrlimit_nid_postfix(guestRlimitNofile, &limit); std::int64_t tableSize = limit.rlim_cur; -#ifndef _WIN32 +#if !defined(_WIN32) && !defined(__EMSCRIPTEN__) tableSize = std::min(tableSize, static_cast(::getdtablesize())); #endif if (tableSize > std::numeric_limits::max()) diff --git a/core/libs/prx/libkernel/System/src/UserContext.cpp b/core/libs/prx/libkernel/System/src/UserContext.cpp index 1445055be51..ea580d7132f 100644 --- a/core/libs/prx/libkernel/System/src/UserContext.cpp +++ b/core/libs/prx/libkernel/System/src/UserContext.cpp @@ -8,8 +8,33 @@ #include #include #include +#ifdef __EMSCRIPTEN__ +#include "prx/libc/include/General.hpp" +#else #include +#endif + +#ifdef __EMSCRIPTEN__ +extern "C" { +int APS5_VABI getcontext_nid_postfix(UserUcontext* context) { + static_cast(context); + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI setcontext_nid_postfix(const UserUcontext* context) { + static_cast(context); + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI swapcontext_nid_postfix(UserUcontext* saved, const UserUcontext* next) { + static_cast(saved); + static_cast(next); + NotImplemented_nid_no_patch(__func__); +} + +} +#else namespace { struct RegisterFrame { @@ -281,3 +306,4 @@ int APS5_VABI Aps5SwapcontextFinish_nid_no_patch(UserUcontext* saved, const User } } +#endif diff --git a/core/libs/tests/EmscriptenModuleUnwind.cpp b/core/libs/tests/EmscriptenModuleUnwind.cpp new file mode 100644 index 00000000000..9e3710221e2 --- /dev/null +++ b/core/libs/tests/EmscriptenModuleUnwind.cpp @@ -0,0 +1,41 @@ +#include "prx/libkernel/include/Api.hpp" +#include "prx/libkernel/KernelErrors.hpp" +#include "prx/libSceSysmodule/include/Api.hpp" +#include +#include +#include +#include +#include + +namespace { + +struct UnsupportedPath { + const char* function; +}; + +} + +extern "C" [[noreturn]] void NotImplemented_nid_no_patch(const char* function) { + throw UnsupportedPath{function}; +} + +int main() { + for (const auto query : {&sceKernelGetModuleInfoForUnwind, &sceSysmoduleGetModuleInfoForUnwind}) { + for (const auto address : {std::uint64_t{0}, std::uint64_t{0x10000}, std::numeric_limits::max()}) { + for (const int flags : {0, 1, -1}) { + ModuleInfoForUnwind info; + std::memset(&info, 0xa5, sizeof(info)); + std::array before; + std::memcpy(before.data(), &info, sizeof(info)); + try { + query(address, flags, &info); + std::abort(); + } catch (const UnsupportedPath& error) { + if (std::strcmp(error.function, "sceKernelGetModuleInfoForUnwind") != 0 || + std::memcmp(before.data(), &info, sizeof(info)) != 0) std::abort(); + } + } + } + } + if (sceKernelGetModuleInfoForUnwind(0, 0, nullptr) != SCE_KERNEL_ERROR_EFAULT) std::abort(); +} diff --git a/core/libs/tests/GuestLongDouble.cpp b/core/libs/tests/GuestLongDouble.cpp new file mode 100644 index 00000000000..5f0b78cb130 --- /dev/null +++ b/core/libs/tests/GuestLongDouble.cpp @@ -0,0 +1,41 @@ +#include "prx/libc/include/GuestLongDouble.hpp" +#include +#include +#include +#include +#include + +int main() { + struct Case { + std::uint64_t mantissa; + std::uint16_t signExponent; + long double expected; + }; + const Case cases[] = { + {0, 0, 0.0L}, {0, 0x8000, -0.0L}, + {0x8000000000000000ull, 0x3fff, 1.0L}, {0xc000000000000000ull, 0xbfff, -1.5L}, + {0x8000000000000000ull, 1, std::ldexp(1.0L, -16382)}, + {1, 0, std::ldexp(1.0L, -16445)}, + }; + for (const auto& item : cases) { + std::array guest{}; + std::memcpy(guest.data(), &item.mantissa, sizeof(item.mantissa)); + std::memcpy(guest.data() + 8, &item.signExponent, sizeof(item.signExponent)); + const auto loaded = LibcDetail::GuestLongDoubleLoad(guest.data()); + if (loaded != item.expected || std::signbit(loaded) != std::signbit(item.expected)) std::abort(); + std::array stored{}; + LibcDetail::GuestLongDoubleStore(stored.data(), loaded); + if (std::memcmp(stored.data(), guest.data(), 10) != 0) std::abort(); + } + for (const auto mantissa : {0x8000000000000000ull, 0xc000000000000123ull}) { + std::array guest{}; + const std::uint16_t exponent = 0x7fff; + std::memcpy(guest.data(), &mantissa, sizeof(mantissa)); + std::memcpy(guest.data() + 8, &exponent, sizeof(exponent)); + const auto loaded = LibcDetail::GuestLongDoubleLoad(guest.data()); + if (mantissa == 0x8000000000000000ull ? !std::isinf(loaded) : !std::isnan(loaded)) std::abort(); + std::array stored{}; + LibcDetail::GuestLongDoubleStore(stored.data(), loaded); + if (std::memcmp(stored.data(), guest.data(), 10) != 0) std::abort(); + } +} diff --git a/core/libs/tests/emscripten/CMakeLists.txt b/core/libs/tests/emscripten/CMakeLists.txt new file mode 100644 index 00000000000..e978296a617 --- /dev/null +++ b/core/libs/tests/emscripten/CMakeLists.txt @@ -0,0 +1,51 @@ +cmake_minimum_required(VERSION 3.20) +project(AnyPS5LibraryPortability CXX) +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +enable_testing() +get_filename_component(libs "${CMAKE_CURRENT_SOURCE_DIR}/../.." ABSOLUTE) +add_executable(guest_long_double_tests ../GuestLongDouble.cpp) +target_include_directories(guest_long_double_tests PRIVATE ${libs}) +add_library(library_portability_sources OBJECT + ${libs}/prx/libSceNet/Export.cpp + ${libs}/prx/libSceSysmodule/Export.cpp + ${libs}/prx/libc/src/ApplicationProcessParameters.cpp + ${libs}/prx/libc/src/Formatting.cpp + ${libs}/prx/libc/src/General.cpp + ${libs}/prx/libc/src/GuestAllocations.cpp + ${libs}/prx/libc/src/GuestArena.cpp + ${libs}/prx/libc/src/Interfaces.cpp + ${libs}/prx/libc/src/ScanningWide.cpp + ${libs}/prx/libc/src/Strings.cpp + ${libs}/prx/libc/src/specifics/linux/DlIteratePhdr.cpp + ${libs}/prx/libkernel/DirectMemory/DirectMemory.cpp + ${libs}/prx/libkernel/Equeue/src/FileEvent.cpp + ${libs}/prx/libkernel/File/src/NativeStat.cpp + ${libs}/prx/libkernel/File/src/Stdio.cpp + ${libs}/prx/libkernel/Module/src/Module.cpp + ${libs}/prx/libkernel/System/src/Process.cpp + ${libs}/prx/libkernel/System/src/UserContext.cpp +) +target_include_directories(library_portability_sources PRIVATE ${libs}) +if(EMSCRIPTEN) + set(flags --target=wasm64 -pthread -fwasm-exceptions -sSUPPORT_LONGJMP=wasm) + target_compile_options(guest_long_double_tests PRIVATE ${flags}) + target_link_options(guest_long_double_tests PRIVATE ${flags} -sENVIRONMENT=node -sEXIT_RUNTIME) + set_target_properties(guest_long_double_tests PROPERTIES SUFFIX .js) + set(ANYPS5_NODE node CACHE FILEPATH "Node.js 26 or newer for memory64") + add_test(NAME guest_long_double COMMAND ${ANYPS5_NODE} $) + target_compile_options(library_portability_sources PRIVATE ${flags}) + add_executable(emscripten_module_unwind_tests + ../EmscriptenModuleUnwind.cpp + ${libs}/prx/libkernel/Module/src/Module.cpp + ${libs}/prx/libSceSysmodule/Export.cpp) + target_include_directories(emscripten_module_unwind_tests PRIVATE ${libs}) + target_compile_options(emscripten_module_unwind_tests PRIVATE ${flags}) + target_link_options(emscripten_module_unwind_tests PRIVATE ${flags} -sENVIRONMENT=node -sEXIT_RUNTIME) + set_target_properties(emscripten_module_unwind_tests PROPERTIES SUFFIX .js) + add_test(NAME emscripten_module_unwind COMMAND ${ANYPS5_NODE} $) + set_tests_properties(emscripten_module_unwind PROPERTIES TIMEOUT 20) +else() + add_test(NAME guest_long_double COMMAND guest_long_double_tests) +endif() +set_tests_properties(guest_long_double PROPERTIES TIMEOUT 20) diff --git a/docs/dev/debt/libSceSysmodule.md b/docs/dev/debt/libSceSysmodule.md index 7049c717ee0..91a505db390 100644 --- a/docs/dev/debt/libSceSysmodule.md +++ b/docs/dev/debt/libSceSysmodule.md @@ -14,4 +14,5 @@ ### Functional +- [sceSysmoduleGetModuleInfoForUnwind](../../../core/libs/prx/libSceSysmodule/Export.cpp) (WebAssembly) throws through the unsupported `sceKernelGetModuleInfoForUnwind` path instead of consulting host process mappings - [sceSysmoduleGetModuleInfoForUnwind](../../../core/libs/prx/libSceSysmodule/Export.cpp) (libSceSysmodule) - on Windows the region and file name of an address inside a loaded image are cached after the first lookup, because `VirtualQuery` on a large image section is slow. An entry is used only while the loader still reports the same image for the address (same base, time stamp and size). A page protection change inside the image after the first lookup, which would split or merge the region `VirtualQuery` reports, is not noticed. Addresses outside loaded images are queried every time diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 659473ea0c6..c609013dbc4 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -104,6 +104,10 @@ - [modf and modff](../../../core/libs/prx/libc/src/Math.cpp) (libc) - native PS5 execution, Windows and unmasked exceptions remain unverified. Linux comparisons with bundled libraries from PPSA03766 and PPSA07923 cover result and integral bits, errno, SSE/x87 flags and control state. Signaling NaNs retain their payload and quiet bit; the measured float path raises SSE invalid while the double path leaves flags unchanged. - [_FSinh and _FCosh](../../../core/libs/prx/libc/src/Math.cpp) (libc) finite results are not verified bit for bit against the console: the scaled product uses the host's binary64 hyperbolic function before rounding to binary32, rather than the float rational and `_FExp` sequence in PRAGMATA's bundled `libc.prx`. NaN preference and payloads, signed zeros, infinite operands and the range-error thresholds follow that binary's instructions; the system `libSceLibcInternal` implementation and nondefault flush-to-zero modes are unverified. Against `libSceLibcInternal.sprx` on a PS5 (firmware 13.60), 12 of 36 measured results differ by one unit in the last place (`_FSinh(0.5, y)` and `_FCosh(1, y)`, `_FCosh(10, y)` for y of 0.5, 1 and 2); zeros, the overflow to infinity at 89 and 90 and the tiny-argument results matched - [_Divide, _FDivide, _Recip and _FRecip](../../../core/libs/prx/libc/src/DinkumwareInternals.cpp) (libc) follow the Dinkumware CRT division and reciprocal contracts with IEEE-754 signed-zero, infinity and NaN behavior. The exact console results, signaling-NaN handling and floating-point exception behavior are not verified. +- [Host stdout preservation](../../../core/libs/prx/libc/src/General.cpp) (WebAssembly) - redirecting stdout with `dup2` throws because Emscripten's stdout pointer cannot be rebound to a retained host stream +- [Host stderr preservation](../../../core/libs/prx/libc/src/General.cpp) (WebAssembly) uses descriptor 2 directly, so host diagnostics do not retain an independent stream across guest stderr redirection +- [Guest arena](../../../core/libs/prx/libc/src/GuestArena.cpp) (WebAssembly) is limited to an 8 GiB reservation, WebAssembly linear memory does not enforce guest page read, write or execute protections, and host region release throws because host virtual-address reservations are unavailable +- [Guest long double conversions](../../../core/libs/prx/libc/include/GuestLongDouble.hpp) (WebAssembly) - storing a binary128 value discards fraction bits beyond x87 precision instead of rounding them; noncanonical x87 encodings are unverified - [sscanf_s and vsscanf_s](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) return `EOF` when a reached conversion has a null destination, stopping before further assignments, following C11 K.3.5.3.7 and K.3.5.3.14. Earlier assignments are retained. The constraint handler is not invoked, and console behaviour is unverified. - [memmem and memccpy](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - `memmem` searches for needle occurrences in memory buffers and `memccpy` copies buffers until a character match, following FreeBSD and POSIX specifications; behavior is unverified on console. Neither is exported by `libSceLibcInternal.sprx` on a PS5 (firmware 13.60), so a payload process cannot call them there. diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 2f0c5521502..9c99cbfc79e 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -141,6 +141,7 @@ ### Functional +- [sceKernelGetModuleInfoForUnwind](../../../core/libs/prx/libkernel/Module/src/Module.cpp) (WebAssembly) throws because guest module unwind metadata is not implemented for linear memory; it does not query the host's process mappings - [scePthreadYield](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) on Windows calls Sleep(1) after 512 consecutive yields that find no other thread ready on the current processor and arrive less than ~10,000 TSC ticks apart (so the span varies with host CPU TSC rate); FreeBSD sched_yield never sleeps. - [__tls_get_addr](../../../core/libs/prx/libkernel/Module/src/Tls.cpp) (libkernel, NID `vNe1w4diLCs`) throws on macOS, where the relinker binds guest modules to `Aps5GuestTlsGetAddr_nid_no_patch` ([GuestTlsMacOs.cpp](../../../core/libs/prx/libkernel/Pthread/src/GuestTlsMacOs.cpp)) instead; it also throws for a null index, module 0 and a Windows thread without a block for the module. It reads the module word as the relinker writes it for `R_X86_64_DTPMOD64`: on Windows the address of the module's PE `_tls_index`, resolved through the thread's `ThreadLocalStoragePointer` as in [WindowsGuestStartup.cpp](../../../core/relinker/elfpatcher/src/windows/WindowsGuestStartup.cpp); on Linux the glibc module ID, passed to glibc's `__tls_get_addr`. Guest modules' imports are bound to the host resolver, so this export is reached only through a lookup by NID. - [sceKernelGetTscFrequency](../../../core/libs/prx/libkernel/Time/Time.cpp) (libkernel) pins its thread to the CPU it runs on for the 20 ms measurement, so that both TSC reads come from one counter. If the pin can't be set (`sched_getcpu` or `pthread_getaffinity_np` fails on Linux, `SetThreadAffinityMask` fails on Windows), the measurement runs unpinned. On a host whose CPU TSCs differ, the result can then be wrong as before. Neither call is known to fail for the calling thread's own CPU, which is always in its allowed set. macOS never pins. @@ -195,6 +196,10 @@ - [pselect](../../../core/libs/prx/libkernel/Socket/src/Pselect.cpp) (libkernel) sets the signal mask, calls `select` and restores the mask, as FreeBSD 11 `kern_pselect` does. A pending signal that the new mask unblocks has its handler run during the swap, and `pselect` then returns `EINTR` unless a descriptor is already ready, the timeout is zero or an argument is invalid, as FreeBSD's `kern_select` decides before it sleeps. A signal raised during the wait, including one sent by another thread, does not interrupt it, because `select` has no `EINTR` path; and the mask is the process-wide one, so it applies to every thread for the duration of the call - [getcontext and setcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) and [makecontext](../../../core/libs/prx/libSceLibcInternal/UserContext.cpp) (libSceLibcInternal) use FreeBSD 11's amd64 `ucontext_t` (`mcontext` at 0x10, `mc_len` 800, `uc_link` at 0x330, `uc_stack` at 0x338, 0x370 bytes), the layout homebrew built against FreeBSD's headers passes: PPSA99039's OpenSSL `ASYNC` (`crypto/async/arch/async_posix.c`) captures into a 0x370-byte stack buffer. `getcontext` writes the 0x330 bytes before `uc_link`, as FreeBSD's `UC_COPY_SIZE`. The console's own layout for these functions is not verified; the exception handler's context is a different one (see `sceKernelRaiseException`) - [ksem_init, ksem_post, ksem_wait, ksem_trywait, ksem_timedwait, ksem_getvalue and ksem_destroy](../../../core/libs/prx/libkernel/Semaphore/src/Ksem.cpp) (libkernel) - `dup` of a ksem id, or `dup2` from one, throws, where FreeBSD shares the semaphore, and a guest signal does not interrupt a wait with `EINTR`. Otherwise they follow FreeBSD 11 `uipc_sem.c`: each semaphore is kept in a table keyed by a descriptor reserved on the host null device, as a ksem id is a descriptor there; a guest `close`, or `dup2` over the id, drops it through libkernel's close notification; `ksem_init` fails with `ENOSPC` when no descriptor is left or 30 semaphores (`SEM_MAX`) exist +- [getcontext, setcontext and swapcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (WebAssembly) throw because switching guest x86-64 register contexts on a WebAssembly host is not implemented +- [Guest memory mappings](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (WebAssembly) throw for file-backed or shared mappings and for mapping one direct-memory range at two addresses; WebAssembly linear memory has no host mapping primitive for these aliases +- [Direct memory unmap](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (WebAssembly) loses the contents of a direct-memory range when it is unmapped and mapped again: the pages come back zero-filled, where FreeBSD keeps them +- [EVFILT_VNODE](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) (WebAssembly) throws because host file-change notification is not implemented - [getcontext and setcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) keep the 512-byte FXSAVE image in `mc_fpstate` (x87, MXCSR, XMM0-15), so the upper halves of YMM0-15 are neither saved nor restored, and `getcontext` leaves `mc_xfpustate` and `mc_xfpustate_len` 0. `getcontext` sets `mc_fpformat` and `mc_ownedfp` to FreeBSD's `_MC_FPFMT_XMM` and `_MC_FPOWNED_FPU`, `mc_cs` and `mc_ss` to the host selectors, and `mc_flags`, `mc_fsbase` and `mc_gsbase` to 0. `setcontext` loads `mc_fpstate` whatever `mc_fpformat` and `mc_ownedfp` say, and restores neither the segment selectors, `mc_fsbase` and `mc_gsbase` nor the extended state, which FreeBSD restores when `mc_flags` asks for them - [setcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) and [longjmp](../../../core/libs/prx/libc/src/JumpBuffer.cpp) (libc) on Windows set the thread's stack bounds in the TEB to the stack they switch to: a `makecontext` stack, or else the committed region around the target stack pointer. A switch to any other guest stack, such as a context built by hand, gets the bounds of that region only - [scePthreadAttrGet](../../../core/libs/prx/libkernel/Pthread/src/Attr.cpp) and [pthread_attr_get_np](../../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) - the answer for a thread that has ended is not measured on the console; the implementation follows FreeBSD 11.4's [`_pthread_attr_get_np`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_attr.c#L139-L172) through `_thr_find_thread`. On Windows a host thread adopted by `scePthreadSelf` that ends without `scePthreadExit` keeps answering 0. Monster Hunter Wilds (PPSA24191) reuses a slot of its thread table only when `scePthreadAttrGet` returns `0x80020003`