diff --git a/Makefile b/Makefile
index 27d149c..60251f4 100644
--- a/Makefile
+++ b/Makefile
@@ -10,6 +10,7 @@ SHELL := /bin/bash
APP_DEFINITIONS ?=
APP_INCLUDE_PATHS ?=
APP_STATIC_ARCHIVES ?=
+APP_SDK_ARCHIVES ?=
APP_RUNTIME_MODULES ?=
PACBREW_PACKAGES ?=
PACBREW_INCLUDE_PATHS ?=
@@ -29,7 +30,7 @@ HOST_TEST_CXXFLAGS ?= -std=c++20 -O2 -Wall -Wextra -Wpedantic -Werror \
-ffunction-sections -fdata-sections
HOST_TEST_LDFLAGS ?= -Wl,--gc-sections
GTEST_ARGS ?=
-export APP_DEFINITIONS APP_INCLUDE_PATHS APP_STATIC_ARCHIVES APP_RUNTIME_MODULES
+export APP_DEFINITIONS APP_INCLUDE_PATHS APP_STATIC_ARCHIVES APP_SDK_ARCHIVES APP_RUNTIME_MODULES
export PACBREW_PACKAGES PACBREW_INCLUDE_PATHS PACBREW_STATIC_ARCHIVES
export PS5_HOST FTP_PORT DEPLOY_FORMAT PS5_FTP_USER PS5_FTP_PASSWORD DEPLOY_DRY_RUN
export TITLE_ID APP_NAME APP_CATEGORY CONTENT_SUFFIX
@@ -200,7 +201,7 @@ help:
'make packages Build folder, .ffpkg, and .ffpfsc outputs' \
'make deploy PS5_HOST=
Build and FTP-deploy the app folder' \
'make undeploy PS5_HOST= Remove this title from /data/homebrew' \
- 'Build variables: APP_DEFINITIONS, APP_INCLUDE_PATHS, APP_STATIC_ARCHIVES, APP_RUNTIME_MODULES' \
+ 'Build variables: APP_DEFINITIONS, APP_INCLUDE_PATHS, APP_STATIC_ARCHIVES, APP_SDK_ARCHIVES, APP_RUNTIME_MODULES' \
'PacBrew variables: PACBREW_PACKAGES, PACBREW_INCLUDE_PATHS, PACBREW_STATIC_ARCHIVES' \
'Deploy variables: FTP_PORT=2121, DEPLOY_FORMAT=folder|ffpfsc|ffpkg, DEPLOY_DRY_RUN=0|1' \
'Local defaults: Copy .env.example to the ignored .env file' \
diff --git a/docs/LRPS2_PORT.md b/docs/LRPS2_PORT.md
index 28ad76c..7d94349 100644
--- a/docs/LRPS2_PORT.md
+++ b/docs/LRPS2_PORT.md
@@ -75,7 +75,7 @@ driver rounds are in ../PS5_Vulkan/docs/LRPS2_GAPS.md.
| Executable memory | A mapping asked for with `PROT_EXEC` does not execute; one mapped read-write and then `mprotect`ed executable does (Dolphin and PPSSPP). | The code area is mapped that way at its hint. |
| Flexible memory | About 450 MB for the whole title, with Dolphin running leaving roughly 238 MB free. Anonymous mappings are charged in full when made. | 305 MiB of code does not fit beside RetroArch. I will measure what a game really uses, then size the areas to fit (they are upstream's generous defaults) or place code in direct memory if the console executes it. Either way the result is in the log. |
| Page size | 16 KiB. | `__pagesize` 16 KiB for the PS5, using the vtlb's coalescing path. That path so far has run only on AArch64 Apple, so the x86 recompilers' page-protection users (`mmap_MarkCountedRamPage`, the mVU dispatcher's size) need checking against it. |
-| Faults | A fault on a reserved, unmapped page can arrive as SIGBUS. The console's machine context is the FreeBSD layout six words further on than the SDK header places it (measured under Dolphin and PPSSPP). | Take both signals, and read the context at the measured offset. |
+| Faults | A fault on a reserved, unmapped page can arrive as SIGBUS. The console's machine context has 48 bytes before `uc_mcontext` that FreeBSD's lacks (measured by the platform probe). | Take both signals. My SDK fork's `sys/_ucontext.h` declares the 48 bytes, so the FreeBSD branch's `uc_mcontext.mc_rip` is right as written. |
| Threads, timers, audio, input, FTP-visible files, 120 Hz pacing | Solved by the title for Dolphin and PPSSPP: `core_threads_ps5` (sampler registration and stack sizes), `audio_ps5`, `input_ps5`, `permissions_ps5`, and the pacer. | Reused as they are; the CPU thread and MTVU get the sampler hooks. |
| Content | The Ratchet & Clank demo is a `.7z`, which LRPS2 does not open and the frontend will not unpack for it. The FFX demo is the European release, so PAL at 50 Hz. God of War II's demo is an ISO. | I convert the 7z once, on the console's own storage, beside the original and leaving it untouched. Full speed for FFX is 50 fps, and its pacing on the 120 Hz output gets its own check. |
diff --git a/docs/PHASE_LOG.md b/docs/PHASE_LOG.md
index 8548a19..4730413 100644
--- a/docs/PHASE_LOG.md
+++ b/docs/PHASE_LOG.md
@@ -3608,3 +3608,70 @@ Everything the runs created is reachable over FTP (0777/0666). The shared
memory cards and empty per-core folders they left outside the test folder
were removed afterwards; my own saves, cards and configurations were not
touched.
+
+## 2026-09-26 — The payload SDK fork and its platform layer, adopted
+
+The title and every project now build with my fork of the payload SDK
+(../PS5_PayloadSDK, pinned in tools/setup-native-dependencies.sh): the v0.42
+release binaries with the fork's headers and its platform layer installed over
+them (docs/PLATFORM_FIRMWARE_ANALYSIS.md has what the layer rests on). What
+moved into it, and out of the title and the cores:
+
+- **The machine context.** The fork's `sys/_ucontext.h` declares the console's
+ 48 extra bytes, so `uc_mcontext.mc_rip` is the faulting instruction. The
+ crash reporter, the sampler and the Dolphin, PPSSPP and LRPS2 ports read the
+ header's fields; the six-word offset each carried is gone. The probe built
+ with the fork's header finds rip at its field.
+- **The libc gaps.** `src/ps5_directory.cpp` and most of `src/libc_shims.c`
+ are the platform's `ps5_*` functions now. `src/platform_wraps.c` binds the
+ title's `--wrap` links to them, and tools/core-imports.py binds a core's
+ imports (the whole `*at` family, `utimensat`, `futimens`,
+ `clock_nanosleep`, `arc4random*`, `statvfs`/`fstatvfs`). The directory
+ fixture test moved to the fork with the code. The platform's `statvfs`
+ refuses a path that does not exist, as a desktop build sees it; the title's
+ answered for any path.
+- **Executable code.** Dolphin's `AllocateExecutableMemory`, PPSSPP's, and
+ LRPS2's code area come from the platform's executable direct memory, near
+ each core's own code, instead of anonymous flexible memory. The code caches
+ are upstream's sizes again: Dolphin's near and far caches 128 + 64 MiB (32
+ + 16 before), LRPS2's recompilers 305 MiB (161 before). Dolphin frees its
+ code blocks through `FreeMemoryPages` off iOS, so that returns platform
+ memory too.
+- **Guest memory.** Dolphin's `MemArena`, PPSSPP's arena (a shared-memory
+ object before, every view charged to flexible memory) and LRPS2's memshm are
+ the platform's direct-memory objects, views and reserved ranges. PPSSPP maps
+ its VRAM view 80 KiB into its guest memory; the platform first refused a view
+ off the 64 KiB allocation unit (PPSSPP's memory init failed). The probe then
+ measured a view at a 16 KiB page offset mapping and aliasing correctly, and
+ the platform now takes page offsets.
+- **Reporting.** A profiled run's memory line is followed by the platform's:
+ `memory: platform exec=/ shm=/ views=
+ ranges=/`.
+
+Measured on the console with this build, against the runs before it:
+
+| | before | now |
+|---|---|---|
+| Dolphin, flexible memory free in game (RE4, Melee, Wind Waker) | 243,712 KiB | 303,104 KiB |
+| Dolphin, JIT code | 48 MiB of flexible memory | one 200 MiB direct region (288 MiB with the MMU: Rogue Leader) |
+| LRPS2, code area | 161 MiB, 163,840 KiB flexible left | 305 MiB, 328,704 KiB flexible left |
+| PPSSPP, guest memory | a shared-memory object, views in flexible memory | 144 MiB of direct memory, 19 views |
+
+- **Dolphin, Profiles 0 and 1** on Melee, RE4 and Wind Waker: no crash, every
+ steady window at 100% but one Melee window at 98% in each profile.
+- **Dolphin, Profile 9** (Melee, four reloads): no crash; the platform holds
+ one code region, one guest-memory object and one arena after each reload,
+ so each reload returns them; flexible memory drops 2 MiB once and holds; the
+ driver's direct mappings are flat at the reloads (180 each).
+- **Rogue Leader** from boot: no crash, no FIFO alert, no unknown opcode; the
+ attract sequence's windows are as before (its heavy section read a few
+ points lower on this run).
+- **PPSSPP, God of War: Ghost of Sparta** from its state: full speed (480,000
+ of 480,000 frames every window after the first), 1,200 presents per 10 s,
+ no crash.
+- **LRPS2** boots its BIOS with the new code area; the hardware renderer still
+ stops at the driver's first-vertex refusal, the next driver round.
+- When content closes, the platform's counters go back to zero.
+
+Still to run on this build: Profile 10 (save-state stress), whose test harness
+I have to rebuild, and a second Rogue Leader run for its heavy section.
diff --git a/docs/PLATFORM_FIRMWARE_ANALYSIS.md b/docs/PLATFORM_FIRMWARE_ANALYSIS.md
new file mode 100644
index 0000000..cc69482
--- /dev/null
+++ b/docs/PLATFORM_FIRMWARE_ANALYSIS.md
@@ -0,0 +1,123 @@
+# The 10.01 system modules: what they tell us
+
+I have an unpacked copy of a console's system modules on this machine,
+outside every repository (a folder named for firmware 10.01). This page records
+what comparing their export tables with our SDK and our shims established, and
+whether the copy is worth using. It names exported functions and modules only.
+Nothing from the copy is in any repository, and the reader I used lives in my
+scratch space.
+
+## What is there
+
+332 files, all x86-64 ELF images with the FreeBSD ABI and readable dynamic
+tables:
+
+| Folder | Contents |
+|---|---|
+| `common/lib` | 277 modules: libkernel in three variants (`libkernel`, `libkernel_web`, `libkernel_sys`), `libSceLibcInternal`, and the Sce libraries (VideoOut, AudioOut, Pad, Agc, Sysmodule, ...) |
+| `priv/lib` | 26 system-private modules |
+| `sys` | system processes |
+| `vsh` | system applications |
+
+Each module's symbols are named by NID: an 11-character hash of the function
+name, the same derivation our own ELF writer uses for our imports
+(tooling/native/sce_module_writer.cpp). With the module's library table, an
+export reads as "NID, library, module". I computed NIDs for every name in the
+payload SDK's stub libraries, the SDK headers,
+tooling/native/runtime/imports.txt and the functions our shims replace, and
+looked each up.
+
+**Versions.** The copy is 10.01. My console runs 12.09:
+`sceKernelGetSystemSwVersion` reports `12.090.001` in our own capability probe
+(../PS5_PayloadSDK, platform/evidence/probe-2026-09-25), and
+`kern.sdk_version` reports
+0x10010000. So everything below is a hint, and each behaviour that matters was
+measured on the console.
+
+## What it shows that the SDK does not
+
+**The SDK's stubs are almost all real.** Every one of the 3,015 names in the
+SDK's `libSceLibcInternal` stub is exported by the console's libc. Of the 1,111
+names in the `libkernel_web` stub, all but `sceKernelGetPrefixVersion` and
+`sceKernelIccGetThermalAlert` are exported (the same two are missing from the
+other variants). The other stubs we could link are complete except for
+system-only functions nobody here calls: six system-cursor functions in
+VideoOut, 17 crash-report functions in SystemService, 40 settings getters in
+UserService and two device-control functions in AudioOut.
+`libScePosixForWebKit` and `libSceGLSlimVSH` are not in the copy, so their
+stubs cannot be judged from it; the SDK routes `arc4random`,
+`arc4random_buf`, `getaddrinfo`, `freeaddrinfo`, `opendir` and `readdir`
+through the former, and our titles measured `arc4random` resolving to nothing.
+
+**Why our libc gaps are gaps.** For the functions our shims replace
+(src/libc_shims.c, src/ps5_directory.cpp, tooling/lrps2/ps5-libc-shims.cpp,
+the cores' patches):
+
+| Function | Exported by | So |
+|---|---|---|
+| `gmtime_r`, `localtime_r`, `utimensat`, `futimens`, `dirfd`, `faccessat`, `readlinkat`, `clock_nanosleep`, `if_nameindex`, `arc4random`, `arc4random_buf`, `arc4random_uniform` | no module in the copy | no stub fix is possible: shims |
+| `umask`, `statfs`, `fstatfs`, `openat`, `unlinkat`, `fchmodat`, `fstatat`, `mkdirat`, `renameat`, `fchownat`, `linkat`, `symlinkat` | `libkernel_sys` only | our titles import `libkernel_web` by design (docs/PLAN.md), so these resolve to nothing; shims |
+| `statvfs`, `fstatvfs` | libc | exported, but libc builds them on `statfs`, which only `libkernel_sys` carries; our titles measured `statvfs` faulting inside libc (2026-09-24): shim |
+| `opendir`, `fdopendir`, `readdir`, `closedir`, `rewinddir` | libc | exported; our titles measured the console refusing `opendir`, so enumeration goes through `getdents`, which `libkernel_web` exports |
+| `gmtime`, `posix_memalign`, `aligned_alloc` | libc | real exports |
+
+So the question has a clear answer: no gap is a stub routing mistake that
+a stub fix would replace. Each is either absent from the system, present only
+in the variant our titles do not import, or exported but faulting or refused.
+The shims stay, once, in the platform layer.
+
+**The memory interface.** Everything the platform layer needs is exported by
+all three libkernel variants and named in the SDK's libkernel stubs, but not
+one `sceKernel` function is declared in the SDK's headers:
+
+- direct memory: `sceKernelAllocateDirectMemory`,
+ `sceKernelAllocateMainDirectMemory`, `sceKernelReleaseDirectMemory`,
+ `sceKernelCheckedReleaseDirectMemory`, `sceKernelMapDirectMemory`,
+ `sceKernelMapDirectMemory2`, `sceKernelMapNamedDirectMemory`,
+ `sceKernelGetDirectMemorySize`, `sceKernelAvailableDirectMemorySize`,
+ `sceKernelDirectMemoryQuery`, `sceKernelGetDirectMemoryType`;
+- protection: `sceKernelMprotect`, `sceKernelMtypeprotect`,
+ `sceKernelQueryMemoryProtection`, `sceKernelVirtualQuery`, and POSIX
+ `mprotect`;
+- virtual ranges: `sceKernelReserveVirtualRange`,
+ `sceKernelSetVirtualRangeName`, `sceKernelMmap`, `sceKernelMunmap`,
+ `sceKernelBatchMap`, `sceKernelBatchMap2`;
+- memory pools: `sceKernelMemoryPoolExpand`, `...Reserve`, `...Commit`,
+ `...Decommit`, `...Batch`, `...Move`, `...GetBlockStats`;
+- flexible memory: `sceKernelMapFlexibleMemory`,
+ `sceKernelMapNamedFlexibleMemory`, `sceKernelReleaseFlexibleMemory`,
+ `sceKernelAvailableFlexibleMemorySize`,
+ `sceKernelConfiguredFlexibleMemorySize`;
+- shared memory for JIT code: `sceKernelJitCreateSharedMemory`,
+ `sceKernelJitCreateAliasOfSharedMemory`, `sceKernelJitMapSharedMemory`,
+ `sceKernelJitGetSharedMemoryInfo`. In `libkernel_web` these are exported
+ only under the `libkernel_jvm`, `libkernel_ps2emu` and `libkernel_psmkit`
+ libraries. Our probe measured `sceKernelJitCreateSharedMemory` refused to
+ the title (0x80020001), so executable code does not go this way.
+
+Across our repositories the kernel functions we call are all among these, and
+nine files plus two core patches declared their own prototypes for them. The
+machine-context offset was carried in five places: the crash reporter, the
+sampler, and the Dolphin, PPSSPP and LRPS2 ports.
+
+## Verdict
+
+Useful, as an index, for three things: deciding which exported functions are
+worth probing, explaining why an import resolves to nothing (which module and
+variant exports it), and confirming that the SDK stubs are not the problem.
+Not useful for behaviour: the copy is two firmware versions behind my console,
+and an export says nothing about whether a title may use it --
+`sceKernelJitCreateSharedMemory` is exported and refused, `opendir` is
+exported and refused. Every behaviour the platform layer relies on is
+established by our own probe instead (../PS5_PayloadSDK, platform/docs/PROBE.md).
+
+## Where the platform layer lives
+
+In my fork of the payload SDK, `../PS5_PayloadSDK`, which every project pins by
+revision. The fork's `sys/_ucontext.h` declares the console's 48 extra bytes,
+so `uc_mcontext.mc_rip` is the faulting instruction and no project carries an
+offset. Its `platform/` builds `libps5platform.a` and installs
+`include/ps5platform` with the SDK. That covers the kernel declarations, the
+libc gaps above, the executable-code allocator and direct-memory shared memory,
+with host tests. Its `platform/tools/setup-sdk.sh` installs the fork's revision
+over the v0.42 release binaries.
diff --git a/patches/dolphin/ps5-port.patch b/patches/dolphin/ps5-port.patch
index ac5e74b..6d12907 100644
--- a/patches/dolphin/ps5-port.patch
+++ b/patches/dolphin/ps5-port.patch
@@ -41,64 +41,54 @@ index 01b03ecee5..30a51970cc 100644
add_definitions(-DHAVE_CPPIPC)
endif()
diff --git a/Source/Core/Common/MemArena.h b/Source/Core/Common/MemArena.h
-index 1a3093cc4d..82fb1fa1a5 100644
+index 1a3093cc4d..22a59f7a47 100644
--- a/Source/Core/Common/MemArena.h
+++ b/Source/Core/Common/MemArena.h
-@@ -152,6 +152,11 @@ private:
+@@ -14,6 +14,10 @@
+ #include "Common/CommonTypes.h"
+ #include "Common/DynamicLibrary.h"
+
++#ifdef __PROSPERO__
++#include
++#endif
++
+ namespace Common
+ {
+ #ifdef _WIN32
+@@ -152,6 +156,11 @@ private:
int m_shm_fd = 0;
void* m_reserved_region = nullptr;
std::size_t m_reserved_region_size = 0;
+#ifdef __PROSPERO__
-+ // PS5: guest memory is one direct-memory allocation (MemArenaUnix.cpp).
-+ int64_t m_direct_start = -1;
-+ std::size_t m_direct_size = 0;
++ // PS5: guest memory is one direct-memory object of the platform layer
++ // (MemArenaUnix.cpp, ps5platform/shm.h).
++ ps5_shm m_shm{};
+#endif
#endif
};
diff --git a/Source/Core/Common/MemArenaUnix.cpp b/Source/Core/Common/MemArenaUnix.cpp
-index 26a123c081..7410b63117 100644
+index 26a123c081..b8c89961af 100644
--- a/Source/Core/Common/MemArenaUnix.cpp
+++ b/Source/Core/Common/MemArenaUnix.cpp
-@@ -21,6 +21,45 @@
+@@ -21,6 +21,24 @@
#include "Common/Logging/Log.h"
#include "Common/MsgHandler.h"
+#ifdef __PROSPERO__
-+extern "C" {
-+int32_t sceKernelReserveVirtualRange(void** addr, size_t len, int flags, size_t alignment);
-+int64_t sceKernelGetDirectMemorySize(void);
-+int32_t sceKernelAllocateDirectMemory(int64_t search_start, int64_t search_end, size_t bytes,
-+ size_t alignment, int type, int64_t* start);
-+int32_t sceKernelMapDirectMemory(void** address, size_t bytes, int protection, int flags,
-+ int64_t start, size_t alignment);
-+int32_t sceKernelReleaseDirectMemory(int64_t start, size_t bytes);
-+}
-+
-+// PS5: guest memory is direct memory rather than a shared-memory object. Every
-+// view of a shared-memory object was charged to the title's flexible memory
-+// (about 450 MB for the whole title), and Wind Waker's fake-VMEM mirrors - one
-+// 32 MiB region mapped at up to 28 logical addresses - exhausted it: the mappings
-+// failed and a fastmem access faulted through a missing view (2026-09-24).
-+// Direct memory is a separate pool of several GiB, and one allocation can be
-+// mapped at any number of virtual addresses, which is what a mirror is.
++// PS5: guest memory is direct memory, through the platform layer (my payload SDK
++// fork, ps5platform/shm.h), rather than a shared-memory object. Every view of a
++// shared-memory object was charged to the title's flexible memory (448 MiB for
++// the whole title), and Wind Waker's fake-VMEM mirrors - one 32 MiB region
++// mapped at up to 28 logical addresses - exhausted it: the mappings failed and a
++// fastmem access faulted through a missing view (2026-09-24). A direct-memory
++// object is charged to nothing but the 12 GiB direct pool, and can be mapped at
++// any number of addresses, which is what a mirror is.
+namespace
+{
-+// CPU read and write; the type the title's own overflow heap uses.
-+constexpr int ps5_direct_type = 12;
-+constexpr int ps5_prot_read = 0x01;
-+constexpr int ps5_prot_write = 0x02;
-+// The kernel's fixed-address flag (the FreeBSD MAP_FIXED value).
-+constexpr int ps5_map_fixed = 0x10;
-+constexpr size_t ps5_direct_alignment = 0x10000;
-+// Views asked for without an address go here: above the GPU window
-+// (0x2_0000_0000 - 0x2_FFFF_FFFF, which the console's Vulkan driver needs) and
-+// below libkernel's modules at 0x8_0000_0000.
-+constexpr uintptr_t ps5_view_hint = 0x4'0000'0000;
-+
+int ps5_protection(bool writeable)
+{
-+ return ps5_prot_read | (writeable ? ps5_prot_write : 0);
++ return PS5_SHM_READ | (writeable ? PS5_SHM_WRITE : 0);
+}
+} // namespace
+#endif
@@ -106,37 +96,26 @@ index 26a123c081..7410b63117 100644
namespace Common
{
MemArena::MemArena() = default;
-@@ -28,6 +67,21 @@ MemArena::~MemArena() = default;
+@@ -28,6 +46,13 @@ MemArena::~MemArena() = default;
void MemArena::GrabSHMSegment(size_t size, std::string_view base_name)
{
+#ifdef __PROSPERO__
-+ const size_t bytes = (size + ps5_direct_alignment - 1) & ~(ps5_direct_alignment - 1);
-+ int64_t start = -1;
-+ const int32_t result = sceKernelAllocateDirectMemory(
-+ 0, sceKernelGetDirectMemorySize(), bytes, ps5_direct_alignment, ps5_direct_type, &start);
++ const int result = ps5_shm_create(size, &m_shm);
+ if (result != 0)
-+ {
+ PanicAlertFmt("PS5: allocating {:#x} bytes of direct memory for guest RAM failed: {:#010x}",
-+ bytes, static_cast(result));
-+ return;
-+ }
-+ m_direct_start = start;
-+ m_direct_size = bytes;
++ size, static_cast(result));
+ return;
+#endif
const std::string file_name = fmt::format("/{}.{}", base_name, getpid());
m_shm_fd = shm_open(file_name.c_str(), O_RDWR | O_CREAT | O_EXCL, 0600);
if (m_shm_fd == -1)
-@@ -42,11 +96,30 @@ void MemArena::GrabSHMSegment(size_t size, std::string_view base_name)
+@@ -42,11 +67,28 @@ void MemArena::GrabSHMSegment(size_t size, std::string_view base_name)
void MemArena::ReleaseSHMSegment()
{
+#ifdef __PROSPERO__
-+ if (m_direct_start >= 0)
-+ sceKernelReleaseDirectMemory(m_direct_start, m_direct_size);
-+ m_direct_start = -1;
-+ m_direct_size = 0;
++ ps5_shm_destroy(&m_shm);
+ return;
+#endif
close(m_shm_fd);
@@ -145,47 +124,52 @@ index 26a123c081..7410b63117 100644
void* MemArena::CreateView(s64 offset, size_t size)
{
+#ifdef __PROSPERO__
-+ void* address = reinterpret_cast(ps5_view_hint);
-+ const int32_t result = sceKernelMapDirectMemory(&address, size, ps5_protection(true), 0,
-+ m_direct_start + offset, 0x4000);
++ // Anywhere outside the Vulkan driver's GPU window.
++ void* view = nullptr;
++ const int result = ps5_shm_map(&m_shm, static_cast(offset), size, nullptr,
++ ps5_protection(true), 0, &view);
+ if (result != 0)
+ {
+ ERROR_LOG_FMT(MEMMAP, "PS5: mapping a view of {:#x} bytes at offset {:#x} failed: {:#010x}",
+ size, offset, static_cast(result));
+ return nullptr;
+ }
-+ return address;
++ return view;
+#endif
void* retval = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, m_shm_fd, offset);
if (retval == MAP_FAILED)
{
-@@ -66,6 +139,48 @@ void MemArena::ReleaseView(void* view, size_t size)
+@@ -61,11 +103,45 @@ void* MemArena::CreateView(s64 offset, size_t size)
+
+ void MemArena::ReleaseView(void* view, size_t size)
+ {
++#ifdef __PROSPERO__
++ ps5_shm_unmap(view, size, 0);
++ return;
++#endif
+ munmap(view, size);
+ }
u8* MemArena::ReserveMemoryRegion(size_t memory_size)
{
+#ifdef __PROSPERO__
+ // PS5: an anonymous PROT_NONE mapping of this size is charged to the title's
-+ // small flexible-memory budget and fails. The kernel's virtual-range
-+ // reservation holds the range without backing it, and the views are mapped
-+ // into it with MAP_FIXED as upstream does.
-+ //
-+ // The hint keeps the range away from 0x2_0000_0000-0x2_FFFF_FFFF, the window
-+ // the console's Vulkan driver needs for GPU-visible mappings (an unhinted
-+ // reservation landed there in the PPSSPP port), and from PPSSPP's own range at
-+ // 0x6_0000_0000.
++ // small flexible-memory budget and fails. The platform layer reserves the
++ // range without backing it, outside the Vulkan driver's GPU window, and the
++ // views are mapped into it at fixed addresses as upstream does.
+ //
+ // RetroArch unloads a core when content closes. A reservation an earlier
-+ // instance left behind (a core that did not reach ReleaseMemoryRegion) still
-+ // holds the range; the range is this arena's alone, so it is released and
-+ // reserved again.
-+ constexpr uintptr_t ps5_arena_hint = 0x10'0000'0000;
-+ void* address = reinterpret_cast(ps5_arena_hint);
-+ int32_t result = sceKernelReserveVirtualRange(&address, memory_size, 0, 0x10000);
-+ if (result != 0)
++ // instance left behind (a core that did not reach ReleaseMemoryRegion) holds
++ // the hint; the range is this arena's alone, so it is released and asked for
++ // again rather than taken elsewhere.
++ void* const ps5_arena_hint = reinterpret_cast(0x10'0000'0000);
++ void* address = nullptr;
++ int result = ps5_vrange_reserve(memory_size, ps5_arena_hint, 0x10000, &address);
++ if (result == 0 && address != ps5_arena_hint)
+ {
-+ munmap(reinterpret_cast(ps5_arena_hint), memory_size);
-+ address = reinterpret_cast(ps5_arena_hint);
-+ result = sceKernelReserveVirtualRange(&address, memory_size, 0, 0x10000);
++ ps5_vrange_release(address, memory_size);
++ ps5_vrange_release(ps5_arena_hint, memory_size);
++ result = ps5_vrange_reserve(memory_size, ps5_arena_hint, 0x10000, &address);
+ }
+ if (result != 0)
+ {
@@ -193,13 +177,6 @@ index 26a123c081..7410b63117 100644
+ static_cast(result));
+ return nullptr;
+ }
-+ const uintptr_t start = reinterpret_cast(address);
-+ if (start < 0x3'0000'0000 && start + memory_size > 0x2'0000'0000)
-+ {
-+ PanicAlertFmt("PS5: the address-space reservation landed at {} in the GPU window", address);
-+ munmap(address, memory_size);
-+ return nullptr;
-+ }
+ INFO_LOG_FMT(MEMMAP, "PS5: fastmem arena reserved at {} ({:#x} bytes)", address, memory_size);
+ m_reserved_region = address;
+ m_reserved_region_size = memory_size;
@@ -208,27 +185,38 @@ index 26a123c081..7410b63117 100644
const int flags = MAP_ANON | MAP_PRIVATE;
void* base = mmap(nullptr, memory_size, PROT_NONE, flags, -1, 0);
if (base == MAP_FAILED)
-@@ -89,6 +204,19 @@ void MemArena::ReleaseMemoryRegion()
+@@ -82,13 +158,30 @@ void MemArena::ReleaseMemoryRegion()
+ {
+ if (m_reserved_region)
+ {
++#ifdef __PROSPERO__
++ ps5_vrange_release(m_reserved_region, m_reserved_region_size);
++#else
+ munmap(m_reserved_region, m_reserved_region_size);
++#endif
+ m_reserved_region = nullptr;
+ }
+ }
void* MemArena::MapInMemoryRegion(s64 offset, size_t size, void* base, bool writeable)
{
+#ifdef __PROSPERO__
+ // Fixed, over the reservation (ReserveMemoryRegion) or a view unmapped from it.
-+ void* address = base;
-+ const int32_t result = sceKernelMapDirectMemory(&address, size, ps5_protection(writeable),
-+ ps5_map_fixed, m_direct_start + offset, 0x4000);
-+ if (result != 0 || address != base)
++ void* view = nullptr;
++ const int result = ps5_shm_map(&m_shm, static_cast(offset), size, base,
++ ps5_protection(writeable), PS5_SHM_FIXED, &view);
++ if (result != 0)
+ {
+ ERROR_LOG_FMT(MEMMAP, "PS5: mapping {:#x} bytes of guest RAM at {} failed: {:#010x}", size,
+ base, static_cast(result));
+ return nullptr;
+ }
-+ return address;
++ return view;
+#endif
int prot = PROT_READ;
if (writeable)
prot |= PROT_WRITE;
-@@ -96,7 +224,8 @@ void* MemArena::MapInMemoryRegion(s64 offset, size_t size, void* base, bool writ
+@@ -96,7 +189,8 @@ void* MemArena::MapInMemoryRegion(s64 offset, size_t size, void* base, bool writ
void* retval = mmap(base, size, prot, MAP_SHARED | MAP_FIXED, m_shm_fd, offset);
if (retval == MAP_FAILED)
{
@@ -238,16 +226,14 @@ index 26a123c081..7410b63117 100644
return nullptr;
}
else
-@@ -119,9 +248,17 @@ bool MemArena::ChangeMappingProtection(void* view, size_t size, bool writeable)
+@@ -119,9 +213,15 @@ bool MemArena::ChangeMappingProtection(void* view, size_t size, bool writeable)
void MemArena::UnmapFromMemoryRegion(void* view, size_t size)
{
+#ifdef __PROSPERO__
-+ // The hole is reserved again, so nothing else is mapped into the arena.
-+ munmap(view, size);
-+ void* address = view;
-+ if (sceKernelReserveVirtualRange(&address, size, ps5_map_fixed, 0) != 0)
-+ ERROR_LOG_FMT(MEMMAP, "PS5: reserving the unmapped view at {} again failed", view);
++ // The hole stays reserved, so nothing else is mapped into the arena.
++ if (ps5_shm_unmap(view, size, PS5_SHM_KEEP_RESERVED) != 0)
++ ERROR_LOG_FMT(MEMMAP, "PS5: unmapping the view at {} failed", view);
+ return;
+#endif
void* retval = mmap(view, size, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0);
@@ -258,7 +244,7 @@ index 26a123c081..7410b63117 100644
size_t MemArena::GetPageSize() const
diff --git a/Source/Core/Common/MemoryUtil.cpp b/Source/Core/Common/MemoryUtil.cpp
-index 5d5085aa09..28533fea76 100644
+index 5d5085aa09..48afb09fac 100644
--- a/Source/Core/Common/MemoryUtil.cpp
+++ b/Source/Core/Common/MemoryUtil.cpp
@@ -3,6 +3,8 @@
@@ -270,68 +256,64 @@ index 5d5085aa09..28533fea76 100644
#include
#include
-@@ -68,6 +70,10 @@ namespace Common
- static JITMemoryTracker g_jit_memory_tracker;
- #endif
+@@ -10,6 +12,10 @@
+ #include "Common/Logging/Log.h"
+ #include "Common/MsgHandler.h"
+#ifdef __PROSPERO__
-+extern "C" int sceKernelAvailableFlexibleMemorySize(size_t* size);
++#include
+#endif
+
- void* AllocateExecutableMemory(size_t size)
- {
- #if defined(IPHONEOS) && defined(__LIBRETRO__)
-@@ -103,7 +109,50 @@ void* AllocateExecutableMemory(size_t size)
+ #ifdef _WIN32
+ #include
+ #include "Common/StringUtil.h"
+@@ -103,7 +109,20 @@ void* AllocateExecutableMemory(size_t size)
int map_prot = PROT_READ | PROT_WRITE | PROT_EXEC;
+#ifdef __PROSPERO__
-+ // PS5: a mapping asked for with PROT_EXEC is returned but does not execute;
-+ // one mapped read-write and then made executable with mprotect does (the
-+ // PPSSPP port's x86-64 JIT runs from such memory).
-+ //
-+ // The hint keeps JIT code out of 0x2_0000_0000 - 0x2_FFFF_FFFF, the window the
-+ // console's Vulkan driver needs for GPU-visible memory: without it the first
-+ // run's code landed at 0x2_2902_xxxx.
-+ // The console refuses a hint it cannot place (ENOMEM for 0x7_0000_0000) rather
-+ // than choosing elsewhere; 0x3_0000_0000, just above the GPU window, is
-+ // accepted, and the others are fallbacks.
-+ size_t flexible_before = 0;
-+ sceKernelAvailableFlexibleMemorySize(&flexible_before);
-+ void* ptr = MAP_FAILED;
-+ for (const uintptr_t hint : {uintptr_t{0x3'0000'0000}, uintptr_t{0x4'8000'0000},
-+ uintptr_t{0x5'0000'0000}})
-+ {
-+ ptr = mmap(reinterpret_cast(hint), size, PROT_READ | PROT_WRITE, map_flags, -1, 0);
-+ if (ptr != MAP_FAILED)
-+ break;
-+ }
-+ const uintptr_t start = reinterpret_cast(ptr);
-+ if (ptr != MAP_FAILED && start < 0x3'0000'0000 && start + size > 0x2'0000'0000)
-+ {
-+ munmap(ptr, size);
-+ ptr = MAP_FAILED;
-+ }
-+ if (ptr != MAP_FAILED && mprotect(ptr, size, map_prot) != 0)
-+ {
-+ fprintf(stderr, "dolphin ps5: mprotect for code failed (errno %d)\n", errno);
-+ munmap(ptr, size);
-+ ptr = MAP_FAILED;
-+ }
-+ // Code comes out of the title's flexible memory, which a large buffer can
-+ // hold (a 260 MB save state being read did, 2026-09-25): say how much there
-+ // was, since what fails next is the first write to the code.
-+ if (ptr == MAP_FAILED)
-+ fprintf(stderr,
-+ "dolphin ps5: no executable memory for %zu bytes (errno %d, %zu bytes of flexible "
-+ "memory free)\n",
-+ size, errno, flexible_before);
++ // PS5: code lives in direct memory through the platform layer (my payload SDK
++ // fork, ps5platform/exec.h): read, write and execute at once, within a 32-bit
++ // displacement of this core's own code, and out of the 12 GiB direct pool
++ // rather than the title's 448 MiB of flexible memory -- which is why the code
++ // caches are upstream's size again (Jit64Constants.h, FarCodeCache.h).
++ (void)map_prot;
++ (void)map_flags;
++ void* ptr = ps5_exec_allocate(size, reinterpret_cast(&AllocateExecutableMemory));
++ if (ptr == nullptr)
++ fprintf(stderr, "dolphin ps5: no executable memory for %zu bytes\n", size);
+#else
void* ptr = mmap(nullptr, size, map_prot, map_flags, -1, 0);
+#endif
if (ptr == MAP_FAILED)
ptr = nullptr;
#endif
+@@ -236,6 +255,13 @@ bool FreeMemoryPages(void* ptr, size_t size)
+ {
+ if (ptr)
+ {
++#ifdef __PROSPERO__
++ // Code blocks free their executable memory here (CodeBlock.h, off iOS):
++ // what the platform layer handed out goes back to it, direct memory and
++ // all, rather than being only unmapped.
++ if (ps5_exec_release(ptr) == 0)
++ return true;
++#endif
+ #ifdef _WIN32
+ if (!VirtualFree(ptr, 0, MEM_RELEASE))
+ {
+@@ -436,6 +462,11 @@ void FreeExecutableMemory(void* ptr, size_t size)
+ return;
+ }
+ }
++#endif
++#ifdef __PROSPERO__
++ (void)size;
++ ps5_exec_release(ptr);
++ return;
+ #endif
+ munmap(ptr, size);
+ }
diff --git a/Source/Core/Common/Thread.cpp b/Source/Core/Common/Thread.cpp
index 41afff087b..a5750cca86 100644
--- a/Source/Core/Common/Thread.cpp
@@ -469,7 +451,7 @@ index f0c62d4e3f..bbb4cb7791 100644
if (m_FileLoadedCb)
diff --git a/Source/Core/Core/MemTools.cpp b/Source/Core/Core/MemTools.cpp
-index 871bb3f615..ae7a6c16ab 100644
+index 871bb3f615..bad7c5d42a 100644
--- a/Source/Core/Core/MemTools.cpp
+++ b/Source/Core/Core/MemTools.cpp
@@ -260,14 +260,17 @@ bool IsExceptionHandlerSupported()
@@ -492,20 +474,7 @@ index 871bb3f615..ae7a6c16ab 100644
&& sig != SIGBUS
#endif
)
-@@ -287,6 +290,12 @@ static void sigsegv_handler(int sig, siginfo_t* info, void* raw_context)
- // Get all the information we can out of the context.
- #ifdef __OpenBSD__
- SContext* const ctx = context;
-+#elif defined(__PROSPERO__)
-+ // PS5: the console's machine context is the FreeBSD layout six words further on
-+ // than the SDK header places it (measured on a console fault: rip, cs, rflags,
-+ // rsp and ss at words 26-30; the PPSSPP port reads it the same way).
-+ SContext* const ctx = reinterpret_cast(
-+ reinterpret_cast(&context->uc_mcontext) + 6);
- #elif defined(__APPLE__)
- // `uc_mcontext` is already a pointer here.
- SContext* const ctx = context->uc_mcontext;
-@@ -298,7 +307,7 @@ static void sigsegv_handler(int sig, siginfo_t* info, void* raw_context)
+@@ -298,7 +301,7 @@ static void sigsegv_handler(int sig, siginfo_t* info, void* raw_context)
// If JIT didn't handle the signal, restore the original handler and invoke it.
const auto& old_sa =
@@ -514,7 +483,7 @@ index 871bb3f615..ae7a6c16ab 100644
(sig == SIGBUS) ? old_sa_bus :
#endif
old_sa_segv;
-@@ -324,7 +333,7 @@ void InstallExceptionHandler()
+@@ -324,7 +327,7 @@ void InstallExceptionHandler()
sa.sa_flags = SA_SIGINFO;
sigemptyset(&sa.sa_mask);
sigaction(SIGSEGV, &sa, &old_sa_segv);
@@ -523,7 +492,7 @@ index 871bb3f615..ae7a6c16ab 100644
sigaction(SIGBUS, &sa, &old_sa_bus);
#endif
}
-@@ -339,7 +348,7 @@ void UninstallExceptionHandler()
+@@ -339,7 +342,7 @@ void UninstallExceptionHandler()
free(old_stack.ss_sp);
}
sigaction(SIGSEGV, &old_sa_segv, nullptr);
@@ -532,39 +501,6 @@ index 871bb3f615..ae7a6c16ab 100644
sigaction(SIGBUS, &old_sa_bus, nullptr);
#endif
}
-diff --git a/Source/Core/Core/PowerPC/Jit64Common/FarCodeCache.h b/Source/Core/Core/PowerPC/Jit64Common/FarCodeCache.h
-index 08cfe3f8ab..c59a731446 100644
---- a/Source/Core/Core/PowerPC/Jit64Common/FarCodeCache.h
-+++ b/Source/Core/Core/PowerPC/Jit64Common/FarCodeCache.h
-@@ -8,7 +8,12 @@
-
- // a bit of a hack; the MMU results in a vast amount more code ending up in the far cache,
- // mostly exception handling, so give it a whole bunch more space if the MMU is on.
-+// PS5: sized with the near code cache (Jit64Constants.h).
-+#ifdef __PROSPERO__
-+constexpr size_t FARCODE_SIZE = 1024 * 1024 * 16;
-+#else
- constexpr size_t FARCODE_SIZE = 1024 * 1024 * 64;
-+#endif
- constexpr size_t FARCODE_SIZE_MMU = 1024 * 1024 * 128;
-
- // A place to throw blocks of code we don't want polluting the cache, e.g. rarely taken
-diff --git a/Source/Core/Core/PowerPC/Jit64Common/Jit64Constants.h b/Source/Core/Core/PowerPC/Jit64Common/Jit64Constants.h
-index 39e16c7eea..0ed09e9550 100644
---- a/Source/Core/Core/PowerPC/Jit64Common/Jit64Constants.h
-+++ b/Source/Core/Core/PowerPC/Jit64Common/Jit64Constants.h
-@@ -20,4 +20,11 @@ constexpr Gen::X64Reg RMEM = Gen::RBX;
- // to address as much as possible in a one-byte offset form.
- constexpr Gen::X64Reg RPPCSTATE = Gen::RBP;
-
-+// PS5: anonymous mappings draw on the title's flexible memory, about 450 MB for
-+// the whole title, so the code cache is Dolphin's former 32 MiB; a full cache is
-+// cleared as on any platform.
-+#ifdef __PROSPERO__
-+constexpr size_t CODE_SIZE = 1024 * 1024 * 32;
-+#else
- constexpr size_t CODE_SIZE = 1024 * 1024 * 128;
-+#endif
diff --git a/Source/Core/DolphinLibretro/Boot.cpp b/Source/Core/DolphinLibretro/Boot.cpp
index c53281053f..885cdfac84 100644
--- a/Source/Core/DolphinLibretro/Boot.cpp
diff --git a/patches/lrps2/ps5-port.patch b/patches/lrps2/ps5-port.patch
index 005f110..5002c73 100644
--- a/patches/lrps2/ps5-port.patch
+++ b/patches/lrps2/ps5-port.patch
@@ -54,19 +54,10 @@ index 614ffb9f2..01ccd70c8 100644
#else
#include // alloca
diff --git a/libretro/libretro-common/faulthandler/faulthandler.c b/libretro/libretro-common/faulthandler/faulthandler.c
-index e4b25206d..70ffc2a64 100644
+index e4b25206d..ae5e046b9 100644
--- a/libretro/libretro-common/faulthandler/faulthandler.c
+++ b/libretro/libretro-common/faulthandler/faulthandler.c
-@@ -23,6 +23,8 @@
- #define _GNU_SOURCE 1 /* REG_RIP in */
- #endif
-
-+#include
-+#include
- #include
- #include
- #include
-@@ -83,7 +85,7 @@ static retro_atomic_int_t s_slot_inhandler[RETRO_FAULT_MAX_THREADS];
+@@ -83,7 +83,7 @@ static retro_atomic_int_t s_slot_inhandler[RETRO_FAULT_MAX_THREADS];
#if defined(_WIN32)
static void *s_veh_handle;
#else
@@ -75,7 +66,7 @@ index e4b25206d..70ffc2a64 100644
static struct sigaction s_old_sigbus;
#endif
#if !defined(__APPLE__) || defined(__aarch64__)
-@@ -214,7 +216,7 @@ static LONG __stdcall fault_veh(EXCEPTION_POINTERS *eps)
+@@ -214,7 +214,7 @@ static LONG __stdcall fault_veh(EXCEPTION_POINTERS *eps)
/* Chain to whatever handled this signal before us. */
static void fault_chain(int sig, siginfo_t *si, void *ctx)
{
@@ -84,20 +75,7 @@ index e4b25206d..70ffc2a64 100644
#if !defined(__APPLE__) || defined(__aarch64__)
const struct sigaction *sa = (sig == SIGBUS) ? &s_old_sigbus : &s_old_sigsegv;
#else
-@@ -263,6 +265,12 @@ static void fault_filter(int sig, siginfo_t *si, void *ctx)
- pc = (void*)((ucontext_t*)ctx)->uc_mcontext->__ss.__rip;
- #elif defined(__APPLE__) && defined(__aarch64__)
- pc = (void*)((ucontext_t*)ctx)->uc_mcontext->__ss.__pc;
-+#elif defined(__PROSPERO__)
-+ /* The PS5's machine context is FreeBSD's, six words further on than the
-+ * SDK's header places it: rip, cs, rflags, rsp and ss were measured at
-+ * words 26-30 (the PS5 RetroArch title's Dolphin and PPSSPP ports). */
-+ pc = (void*)((const uint64_t*)&((ucontext_t*)ctx)->uc_mcontext)
-+ [offsetof(mcontext_t, mc_rip) / sizeof(uint64_t) + 6];
- #elif defined(__FreeBSD__) && defined(__x86_64__)
- pc = (void*)((ucontext_t*)ctx)->uc_mcontext.mc_rip;
- #elif defined(__x86_64__)
-@@ -318,9 +326,10 @@ bool retro_faulthandler_install(retro_fault_handler_t handler)
+@@ -318,9 +318,10 @@ bool retro_faulthandler_install(retro_fault_handler_t handler)
* old one must be able to raise it again. */
sa.sa_flags |= SA_NODEFER;
#endif
@@ -110,7 +88,7 @@ index e4b25206d..70ffc2a64 100644
if (sigaction(SIGBUS, &sa, &s_old_sigbus) != 0)
ok = false;
#endif
-@@ -375,7 +384,7 @@ void retro_faulthandler_remove(retro_fault_handler_t handler)
+@@ -375,7 +376,7 @@ void retro_faulthandler_remove(retro_fault_handler_t handler)
#else
{
struct sigaction sa;
@@ -120,61 +98,38 @@ index e4b25206d..70ffc2a64 100644
#endif
#if !defined(__APPLE__) || defined(__aarch64__)
diff --git a/libretro/libretro-common/memmap/memmap.c b/libretro/libretro-common/memmap/memmap.c
-index f67ff39df..b7390cac0 100644
+index f67ff39df..9c113f490 100644
--- a/libretro/libretro-common/memmap/memmap.c
+++ b/libretro/libretro-common/memmap/memmap.c
-@@ -25,6 +25,50 @@
+@@ -25,6 +25,27 @@
#include /* strlen, memcpy, memset */
#include
+#if defined(__PROSPERO__)
-+/* PS5: shared memory is libkernel's direct memory. Every mapping of an
-+ * anonymous or shared-memory object is charged to the title's small
-+ * flexible-memory budget, while direct memory is a separate pool of
-+ * several GiB, one allocation of which can be mapped at any number of
-+ * addresses -- which is what a shared-memory object is for here (the
-+ * PS5 RetroArch title's Dolphin port measured both). A reservation is
-+ * the kernel's virtual-range reservation, which holds addresses without
-+ * backing them. Views and reservations are placed above the GPU-visible
++/* PS5: shared memory is direct memory, through the platform layer (my
++ * payload SDK fork, ps5platform/shm.h). Every mapping of an anonymous or
++ * shared-memory object is charged to the title's 448 MiB of flexible
++ * memory; one direct-memory object is charged to nothing but the 12 GiB
++ * direct pool and maps at any number of addresses -- which is what a
++ * shared-memory object is for here. A reservation holds addresses without
++ * backing them. Views and reservations are placed outside the GPU-visible
+ * window at 0x2_0000_0000 - 0x2_FFFF_FFFF, which the console's Vulkan
-+ * driver needs, and below libkernel's modules at 0x8_0000_0000. */
++ * driver needs. */
+#include
-+int64_t sceKernelGetDirectMemorySize(void);
-+int32_t sceKernelAllocateDirectMemory(int64_t search_start, int64_t search_end,
-+ size_t bytes, size_t alignment, int type, int64_t *start);
-+int32_t sceKernelMapDirectMemory(void **address, size_t bytes, int protection,
-+ int flags, int64_t start, size_t alignment);
-+int32_t sceKernelReleaseDirectMemory(int64_t start, size_t bytes);
-+int32_t sceKernelReserveVirtualRange(void **address, size_t bytes, int flags,
-+ size_t alignment);
-+#define MEMMAP_PS5_ALIGN ((size_t)0x10000)
-+#define MEMMAP_PS5_PAGE ((size_t)0x4000)
-+/* CPU read and write: the type the title's own direct-memory heap uses. */
-+#define MEMMAP_PS5_TYPE 12
-+/* The kernel's fixed-address flag (FreeBSD's MAP_FIXED value). */
-+#define MEMMAP_PS5_FIXED 0x10
-+#define MEMMAP_PS5_VIEW_HINT ((uintptr_t)0x400000000ull)
-+#define MEMMAP_PS5_AREA_HINT ((uintptr_t)0x1400000000ull)
-+#define MEMMAP_PS5_GPU_LOW ((uintptr_t)0x200000000ull)
-+#define MEMMAP_PS5_GPU_HIGH ((uintptr_t)0x300000000ull)
-+
-+typedef struct
-+{
-+ int64_t start;
-+ size_t len;
-+} memshm_ps5_t;
++#include
++#define MEMMAP_PS5_AREA_HINT ((void*)0x1400000000ull)
+
+/* Direct memory maps for the CPU with read and write only. */
+static int memshm_ps5_prot(int prot)
+{
-+ return prot & (PROT_READ | PROT_WRITE);
++ return (prot & PROT_READ ? PS5_SHM_READ : 0) | (prot & PROT_WRITE ? PS5_SHM_WRITE : 0);
+}
+#endif
+
#ifndef PROT_READ
#define PROT_READ 0x1 /* Page can be read */
#endif
-@@ -483,6 +527,59 @@ void memshm_unmap(void *addr, size_t len)
+@@ -483,6 +504,50 @@ void memshm_unmap(void *addr, size_t len)
/* Gated on MAP_SHARED, as reserve/commit gates on its constants: DJGPP
* defines __unix__ and HAVE_MMAN but ships a stub with no
* MAP_SHARED and no shm_open, and falls through to the stubs below. */
@@ -182,45 +137,36 @@ index f67ff39df..b7390cac0 100644
+
+void *memshm_create(const char *name, size_t len)
+{
-+ memshm_ps5_t *h;
-+ int64_t start = -1;
-+ size_t bytes = (len + MEMMAP_PS5_ALIGN - 1) & ~(MEMMAP_PS5_ALIGN - 1);
++ struct ps5_shm *h = (struct ps5_shm*)calloc(1, sizeof(*h));
+ (void)name;
-+ if (!bytes || sceKernelAllocateDirectMemory(0, sceKernelGetDirectMemorySize(),
-+ bytes, MEMMAP_PS5_ALIGN, MEMMAP_PS5_TYPE, &start) != 0)
-+ return NULL;
-+ h = (memshm_ps5_t*)malloc(sizeof(*h));
+ if (!h)
++ return NULL;
++ if (!len || ps5_shm_create(len, h) != 0)
+ {
-+ sceKernelReleaseDirectMemory(start, bytes);
++ free(h);
+ return NULL;
+ }
-+ h->start = start;
-+ h->len = bytes;
+ return h;
+}
+
+void memshm_destroy(void *handle)
+{
-+ memshm_ps5_t *h = (memshm_ps5_t*)handle;
++ struct ps5_shm *h = (struct ps5_shm*)handle;
+ if (!h)
+ return;
-+ sceKernelReleaseDirectMemory(h->start, h->len);
++ ps5_shm_destroy(h);
+ free(h);
+}
+
-+/* A hint is where the kernel starts looking, as mmap's is: the caller
-+ * compares what it got, as on the other platforms. No hint means above
++/* A hint is where the view is asked for, as mmap's is: the caller compares
++ * what it got, as on the other platforms. No hint means anywhere outside
+ * the GPU window. */
+void *memshm_map(void *handle, size_t offset, void *hint, size_t len, int prot)
+{
-+ memshm_ps5_t *h = (memshm_ps5_t*)handle;
-+ void *addr;
-+ if (!h || !len || offset > h->len || len > h->len - offset)
-+ return NULL;
-+ addr = hint ? hint : (void*)MEMMAP_PS5_VIEW_HINT;
-+ if (sceKernelMapDirectMemory(&addr, len, memshm_ps5_prot(prot), 0,
-+ h->start + (int64_t)offset, MEMMAP_PS5_PAGE) != 0)
++ void *addr = NULL;
++ if (!handle || !len ||
++ ps5_shm_map((const struct ps5_shm*)handle, offset, len, hint,
++ memshm_ps5_prot(prot), 0, &addr) != 0)
+ return NULL;
+ return addr;
+}
@@ -228,13 +174,13 @@ index f67ff39df..b7390cac0 100644
+void memshm_unmap(void *addr, size_t len)
+{
+ if (addr)
-+ munmap(addr, len);
++ ps5_shm_unmap(addr, len, 0);
+}
+
#elif defined(HAVE_MMAN) && !defined(__EMSCRIPTEN__) && defined(MAP_SHARED)
#include
-@@ -859,6 +956,30 @@ static void memshm_legacy_release(memshm_area_t *a, size_t first, size_t count)
+@@ -859,6 +924,25 @@ static void memshm_legacy_release(memshm_area_t *a, size_t first, size_t count)
memshm_area_t *memshm_area_create(size_t len)
{
@@ -244,18 +190,13 @@ index f67ff39df..b7390cac0 100644
+ * At the hint first, anywhere else if it is taken, never in the GPU
+ * window. */
+ memshm_area_t *a;
-+ void *addr = (void*)MEMMAP_PS5_AREA_HINT;
-+ if (sceKernelReserveVirtualRange(&addr, len, 0, MEMMAP_PS5_ALIGN) != 0)
++ void *addr = NULL;
++ if (ps5_vrange_reserve(len, MEMMAP_PS5_AREA_HINT, 0x10000, &addr) != 0)
+ return NULL;
-+ if ((uintptr_t)addr < MEMMAP_PS5_GPU_HIGH && (uintptr_t)addr + len > MEMMAP_PS5_GPU_LOW)
-+ {
-+ munmap(addr, len);
-+ return NULL;
-+ }
+ a = (memshm_area_t*)calloc(1, sizeof(*a));
+ if (!a)
+ {
-+ munmap(addr, len);
++ ps5_vrange_release(addr, len);
+ return NULL;
+ }
+ a->base = (unsigned char*)addr;
@@ -265,7 +206,7 @@ index f67ff39df..b7390cac0 100644
#if (defined(_WIN32) && !defined(_XBOX)) || (defined(HAVE_MMAN) && !defined(__EMSCRIPTEN__) && defined(MAP_ANONYMOUS))
memshm_area_t *a;
#endif
-@@ -994,6 +1115,7 @@ memshm_area_t *memshm_area_create(size_t len)
+@@ -994,6 +1078,7 @@ memshm_area_t *memshm_area_create(size_t len)
(void)len;
return NULL;
#endif
@@ -273,46 +214,43 @@ index f67ff39df..b7390cac0 100644
}
void memshm_area_free(memshm_area_t *area)
-@@ -1019,6 +1141,9 @@ void memshm_area_free(memshm_area_t *area)
+@@ -1019,6 +1104,9 @@ void memshm_area_free(memshm_area_t *area)
}
/* Every placeholder and every mapping goes with the reservation. */
VirtualFree(area->base, 0, MEM_RELEASE);
+#elif defined(__PROSPERO__)
+ /* The reservation and every mapping made into it. */
-+ munmap(area->base, area->len);
++ ps5_vrange_release(area->base, area->len);
#elif defined(HAVE_MMAN) && !defined(__EMSCRIPTEN__) && defined(MAP_FIXED) && defined(MAP_ANONYMOUS)
munmap(area->base, area->len);
#endif
-@@ -1134,6 +1259,17 @@ unsigned char *memshm_area_map(memshm_area_t *area, void *handle,
+@@ -1134,6 +1222,16 @@ unsigned char *memshm_area_map(memshm_area_t *area, void *handle,
#else
return NULL;
#endif
+#elif defined(__PROSPERO__)
-+ memshm_ps5_t *h = (memshm_ps5_t*)handle;
-+ void *addr = at;
-+ if (!area || !h || !at || !len || offset > h->len || len > h->len - offset)
++ void *addr = NULL;
++ if (!area || !handle || !at || !len)
+ return NULL;
+ /* Fixed, over the reservation or a hole memshm_area_unmap reserved again. */
-+ if (sceKernelMapDirectMemory(&addr, len, memshm_ps5_prot(prot), MEMMAP_PS5_FIXED,
-+ h->start + (int64_t)offset, MEMMAP_PS5_PAGE) != 0 || addr != at)
++ if (ps5_shm_map((const struct ps5_shm*)handle, offset, len, at, memshm_ps5_prot(prot),
++ PS5_SHM_FIXED, &addr) != 0)
+ return NULL;
+ area->mappings++;
+ return (unsigned char*)addr;
#elif defined(HAVE_MMAN) && !defined(__EMSCRIPTEN__) && defined(MAP_FIXED) && defined(MAP_ANONYMOUS)
void *p;
if (!area || !at || !len)
-@@ -1226,6 +1362,17 @@ bool memshm_area_unmap(memshm_area_t *area, void *at, size_t len)
+@@ -1226,6 +1324,15 @@ bool memshm_area_unmap(memshm_area_t *area, void *at, size_t len)
#else
return false;
#endif
+#elif defined(__PROSPERO__)
-+ void *addr = at;
+ if (!area || !at || !len)
+ return false;
+ /* The mapping goes and the hole is reserved again, so nothing else is
+ * placed inside the area. */
-+ munmap(at, len);
-+ if (sceKernelReserveVirtualRange(&addr, len, MEMMAP_PS5_FIXED, 0) != 0)
++ if (ps5_shm_unmap(at, len, PS5_SHM_KEEP_RESERVED) != 0)
+ return false;
+ area->mappings--;
+ return true;
@@ -695,21 +633,20 @@ index b57a0c8e3..166bdfa1c 100644
}
#endif
diff --git a/pcsx2/Memory.cpp b/pcsx2/Memory.cpp
-index 1adeb1c31..f1f7159bc 100644
+index 1adeb1c31..04bed82bc 100644
--- a/pcsx2/Memory.cpp
+++ b/pcsx2/Memory.cpp
-@@ -55,6 +55,10 @@ namespace HostMemoryMap
- } // namespace HostMemoryMap
-
- /// Attempts to find a spot near static variables for the main memory
+@@ -46,6 +46,9 @@ BIOS
+ #include "ps2/HwInternal.h"
+ #include "ps2/BiosTools.h"
+ #include "SPU2/spu2.h"
+#ifdef __PROSPERO__
-+extern "C" int sceKernelAvailableFlexibleMemorySize(size_t* size);
++#include
+#endif
-+
- static VirtualMemoryManagerPtr AllocateVirtualMemory(const char* name, size_t size, size_t offset_from_base)
+
+ namespace HostMemoryMap
{
- #if defined(_WIN32)
-@@ -80,6 +84,52 @@ static VirtualMemoryManagerPtr AllocateVirtualMemory(const char* name, size_t si
+@@ -80,6 +83,53 @@ static VirtualMemoryManagerPtr AllocateVirtualMemory(const char* name, size_t si
if (mgr->IsOk())
return mgr;
}
@@ -745,9 +682,10 @@ index 1adeb1c31..f1f7159bc 100644
+ (start < 0x300000000ull && start + span > 0x200000000ull))
+ continue;
+ VirtualMemoryManagerPtr mgr = std::make_shared(name, base, size, /*upper_bounds=*/0, /*strict=*/true);
-+ /* The code area is anonymous memory, which comes out of the title's
-+ * flexible memory (main memory is direct memory, memmap.c): what is
-+ * left of it is what the rest of the core allocates from. */
++ /* Main memory and the code area are direct memory (memmap.c and
++ * VirtualMemory.cpp, through the platform layer); what is left of the
++ * title's flexible memory is what the rest of the core allocates
++ * from. */
+ size_t flexible_free = 0;
+ sceKernelAvailableFlexibleMemorySize(&flexible_free);
+ log_cb(mgr->IsOk() ? RETRO_LOG_INFO : RETRO_LOG_WARN,
@@ -763,30 +701,19 @@ index 1adeb1c31..f1f7159bc 100644
return std::make_shared(name, 0, size);
}
diff --git a/pcsx2/Memory.h b/pcsx2/Memory.h
-index 5b2f26275..8ba12740b 100644
+index 5b2f26275..51b4d67a5 100644
--- a/pcsx2/Memory.h
+++ b/pcsx2/Memory.h
-@@ -79,32 +79,57 @@ namespace HostMemoryMap
+@@ -79,32 +79,43 @@ namespace HostMemoryMap
//////////////////////////////////////////////////////////////////////////
// Code
//////////////////////////////////////////////////////////////////////////
- static const u32 CodeSize = 0x13100000; // 305 mb
-+ // Each recompiler's code cache, one after another. A cache that fills is
-+ // cleared and refilled, so a smaller one costs the occasional reset and
-+ // nothing else.
-+#if defined(__PROSPERO__)
-+ // PS5: the code area is anonymous memory, charged in full to the title's
-+ // flexible-memory budget when it is mapped -- about 450MB for the whole
-+ // title, RetroArch and the core image included -- where upstream's 305MB
-+ // would not fit. VU0's microprograms are small; VU1's are what games run.
-+ static const u32 EErecSize = 0x02800000; // 40mb
-+ static const u32 IOPrecSize = 0x00C00000; // 12mb
-+ static const u32 VIFrecSize = 0x00600000; // 6mb each
-+ static const u32 mVU0recSize = 0x01000000; // 16mb
-+ static const u32 mVU1recSize = 0x03000000; // 48mb
-+ static const u32 VIFUnpackRecSize = 0x00100000; // 1mb
-+ static const u32 SWrecSize = 0x02000000; // 32mb
-+#else
+-
+- // EE recompiler code cache area (64mb)
++ // Each recompiler's code cache, one after another, at upstream's sizes (305
++ // MiB together). On the PS5 the area is executable direct memory from the
++ // platform layer (VirtualMemory.cpp), out of the 12 GiB pool.
+ static const u32 EErecSize = 0x04000000; // 64mb
+ static const u32 IOPrecSize = 0x02000000; // 32mb
+ static const u32 VIFrecSize = 0x00800000; // 8mb each
@@ -794,9 +721,7 @@ index 5b2f26275..8ba12740b 100644
+ static const u32 mVU1recSize = 0x04000000; // 64mb
+ static const u32 VIFUnpackRecSize = 0x00100000; // 1mb
+ static const u32 SWrecSize = 0x04000000; // 64mb
-+#endif
-
-- // EE recompiler code cache area (64mb)
++
+ // EE recompiler code cache area
static const u32 EErecOffset = 0x00000000;
@@ -841,6 +766,92 @@ index 5b2f26275..8ba12740b 100644
}
// --------------------------------------------------------------------------------------
+diff --git a/pcsx2/VirtualMemory.cpp b/pcsx2/VirtualMemory.cpp
+index e642655f6..05c476254 100644
+--- a/pcsx2/VirtualMemory.cpp
++++ b/pcsx2/VirtualMemory.cpp
+@@ -65,6 +65,20 @@ VirtualMemoryManager::VirtualMemoryManager(const char* file_mapping_name, uptr b
+ }
+ else
+ {
++#if defined(__PROSPERO__)
++ /* PS5: the code area is direct memory through the platform layer,
++ * read, write and execute at once: out of the 12 GiB direct pool
++ * rather than the title's 448 MiB of flexible memory, so the
++ * recompilers' caches are upstream's sizes (Memory.h). It is placed
++ * at base only if that range is free (PS5_EXEC_AT): the caller tries
++ * places near the core's code in turn (Memory.cpp). */
++ ps5_exec_request request = {};
++ request.bytes = reserved_bytes;
++ request.address = base;
++ request.flags = base ? PS5_EXEC_AT : 0;
++ if (ps5_exec_alloc(&request, &m_exec) == 0)
++ m_baseptr = static_cast(m_exec.base);
++#else
+ PageProtectionMode mode;
+ mode.m_read = true;
+ mode.m_write = true;
+@@ -81,6 +95,7 @@ VirtualMemoryManager::VirtualMemoryManager(const char* file_mapping_name, uptr b
+ if (base)
+ m_baseptr = static_cast(host_mmap(0, reserved_bytes, mode));
+ }
++#endif
+ }
+
+ bool fulfillsRequirements = true;
+@@ -101,7 +116,11 @@ VirtualMemoryManager::VirtualMemoryManager(const char* file_mapping_name, uptr b
+ }
+ else
+ {
++#if defined(__PROSPERO__)
++ ps5_exec_free(&m_exec);
++#else
+ host_munmap(m_baseptr, reserved_bytes);
++#endif
+ m_baseptr = 0;
+ }
+ }
+@@ -136,7 +155,11 @@ VirtualMemoryManager::~VirtualMemoryManager()
+ #endif
+ }
+ else
++#if defined(__PROSPERO__)
++ ps5_exec_free(&m_exec);
++#else
+ host_munmap(m_baseptr, bytes);
++#endif
+ }
+ if (m_file_handle)
+ memshm_destroy(m_file_handle);
+diff --git a/pcsx2/VirtualMemory.h b/pcsx2/VirtualMemory.h
+index f73eabc0e..c755c7b8b 100644
+--- a/pcsx2/VirtualMemory.h
++++ b/pcsx2/VirtualMemory.h
+@@ -18,6 +18,12 @@
+ #include
+ #include "../common/General.h"
+
++#if defined(__PROSPERO__)
++/* PS5: the code area is the platform layer's executable direct memory (my
++ * payload SDK fork, ps5platform/exec.h; VirtualMemory.cpp). */
++#include
++#endif
++
+ /* The code reserve below holds a manager pointer without ever completing the
+ * type, so C needs a name for it and nothing more. */
+ #ifdef __cplusplus
+@@ -47,6 +53,11 @@ class VirtualMemoryManager
+ // reserved memory (in pages)
+ u32 m_pages_reserved;
+
++#if defined(__PROSPERO__)
++ // PS5: the code area's region, when this manager holds one.
++ ps5_exec_region m_exec{};
++#endif
++
+ public:
+ // If upper_bounds is nonzero and the OS fails to allocate memory that is below it,
+ // calls to IsOk() will return false and Alloc() will always return null pointers
diff --git a/pcsx2/microVU/microVU_Structs.h b/pcsx2/microVU/microVU_Structs.h
index 1f76f1128..191de2846 100644
--- a/pcsx2/microVU/microVU_Structs.h
diff --git a/patches/ppsspp/ps5-port.patch b/patches/ppsspp/ps5-port.patch
index 0251b52..8795e9a 100644
--- a/patches/ppsspp/ps5-port.patch
+++ b/patches/ppsspp/ps5-port.patch
@@ -90,23 +90,6 @@ index f45ef3ce5..03c07091b 100644
#endif
if (logical_cpu_count <= 0)
logical_cpu_count = 1;
-diff --git a/Common/ExceptionHandlerSetup.cpp b/Common/ExceptionHandlerSetup.cpp
-index f1b977888..8c9df7168 100644
---- a/Common/ExceptionHandlerSetup.cpp
-+++ b/Common/ExceptionHandlerSetup.cpp
-@@ -239,6 +239,12 @@ static void sigsegv_handler(int sig, siginfo_t* info, void* raw_context) {
- // Get all the information we can out of the context.
- #ifdef __OpenBSD__
- ucontext_t* ctx = context;
-+#elif PPSSPP_PLATFORM(PS5)
-+ // The console's machine context is the FreeBSD layout six words further on
-+ // than the SDK header places it: measured on a console fault, rip, cs (0x43),
-+ // rflags, rsp and ss (0x3b) sat at words 26-30, the trap number (12), the
-+ // fault address and the error code at 22, 23 and 25, rbp beside rsp at 15.
-+ mcontext_t* ctx = (mcontext_t *)((uint64_t *)&context->uc_mcontext + 6);
- #else
- mcontext_t* ctx = &context->uc_mcontext;
- #endif
diff --git a/Common/GPU/Vulkan/thin3d_vulkan.cpp b/Common/GPU/Vulkan/thin3d_vulkan.cpp
index d0f2fd37f..248bc9402 100644
--- a/Common/GPU/Vulkan/thin3d_vulkan.cpp
@@ -136,40 +119,101 @@ index d0f2fd37f..248bc9402 100644
caps_.fragmentShaderInt32Supported = true;
caps_.textureNPOTFullySupported = true;
caps_.fragmentShaderDepthWriteSupported = true;
+diff --git a/Common/MemArena.h b/Common/MemArena.h
+index 9a905b134..0b3e806cc 100644
+--- a/Common/MemArena.h
++++ b/Common/MemArena.h
+@@ -25,8 +25,13 @@
+ #include
+ #endif
+
++#include "ppsspp_config.h"
+ #include "Common/CommonTypes.h"
+
++#if PPSSPP_PLATFORM(PS5)
++#include
++#endif
++
+ // This class lets you create a block of anonymous RAM, and then arbitrarily map views into it.
+ // Multiple views can mirror the same section of the block, which makes it very convient for emulating
+ // memory mirrors.
+@@ -55,4 +60,9 @@ class MemArena {
+ #else
+ int fd = -1;
+ #endif
++#if PPSSPP_PLATFORM(PS5)
++ // PS5: guest memory is one direct-memory object of the platform layer
++ // (MemArenaPosix.cpp, ps5platform/shm.h).
++ ps5_shm shm_{};
++#endif
+ };
diff --git a/Common/MemArenaPosix.cpp b/Common/MemArenaPosix.cpp
-index a413f4eaf..bbd68c9d1 100644
+index a413f4eaf..6c064fcca 100644
--- a/Common/MemArenaPosix.cpp
+++ b/Common/MemArenaPosix.cpp
-@@ -51,7 +51,28 @@ bool MemArena::NeedsProbing() {
+@@ -51,7 +51,31 @@ bool MemArena::NeedsProbing() {
return false;
}
+#if PPSSPP_PLATFORM(PS5)
-+extern "C" int32_t sceKernelReserveVirtualRange(void **addr, size_t len, int flags, size_t alignment);
++// PS5: guest memory is direct memory, through the platform layer (my payload
++// SDK fork, ps5platform/shm.h). Every view of a shared-memory object is charged
++// to the title's 448 MiB of flexible memory; one direct-memory object is
++// charged to nothing but the 12 GiB direct pool, and maps at any number of
++// addresses, which is what PPSSPP's mirrors are.
++static void *ps5_reserved = nullptr;
++static const size_t ps5_reserved_bytes = 0x100000000ULL;
++
++static bool ps5_in_reserved(const void *view, size_t size) {
++ const uintptr_t at = (uintptr_t)view, base = (uintptr_t)ps5_reserved;
++ return ps5_reserved && at >= base && at + size <= base + ps5_reserved_bytes;
++}
+#endif
+
bool MemArena::GrabMemSpace(size_t size) {
+#if PPSSPP_PLATFORM(PS5)
-+ // PS5: an anonymous shared-memory object is the backing every view maps; the
-+ // console has no /dev/shm or /tmp, and named objects are not needed.
-+ fd = shm_open(SHM_ANON, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
-+ if (fd < 0) {
-+ ERROR_LOG(Log::MemMap, "PS5: shm_open(SHM_ANON) failed: %s", strerror(errno));
++ const int result = ps5_shm_create(size, &shm_);
++ if (result != 0) {
++ ERROR_LOG(Log::MemMap, "PS5: %08x bytes of direct memory for guest memory failed: %08x", (int)size, (unsigned)result);
+ return false;
+ }
-+ if (ftruncate(fd, size) != 0) {
-+ ERROR_LOG(Log::MemMap, "PS5: ftruncate(%d, %08x) failed: %s", fd, (int)size, strerror(errno));
-+ close(fd);
-+ fd = -1;
-+ return false;
-+ }
-+ INFO_LOG(Log::MemMap, "PS5: guest memory is an anonymous shared object of %08x bytes (fd %d)", (int)size, fd);
++ INFO_LOG(Log::MemMap, "PS5: guest memory is %08x bytes of direct memory", (int)size);
+ return true;
+#endif
#ifndef NO_MMAP
constexpr mode_t mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH;
-@@ -122,7 +143,7 @@ void *MemArena::CreateView(s64 offset, size_t size, void *base)
+@@ -104,6 +128,10 @@ bool MemArena::GrabMemSpace(size_t size) {
+ }
+
+ void MemArena::ReleaseSpace() {
++#if PPSSPP_PLATFORM(PS5)
++ ps5_shm_destroy(&shm_);
++ return;
++#endif
+ #ifndef NO_MMAP
+ close(fd);
+ #endif
+@@ -111,6 +139,18 @@ void MemArena::ReleaseSpace() {
+
+ void *MemArena::CreateView(s64 offset, size_t size, void *base)
+ {
++#if PPSSPP_PLATFORM(PS5)
++ // At base, inside the reserved 4 GiB (Find4GBBase); with none, anywhere
++ // outside the Vulkan driver's GPU window.
++ void *view = nullptr;
++ const int result = ps5_shm_map(&shm_, (size_t)offset, size, base, PS5_SHM_READ | PS5_SHM_WRITE,
++ base ? PS5_SHM_FIXED : 0, &view);
++ if (result != 0) {
++ ERROR_LOG(Log::MemMap, "PS5: view %08x+%08x at %p failed: %08x", (int)offset, (int)size, base, (unsigned)result);
++ return 0;
++ }
++ return view;
++#endif
+ #ifdef NO_MMAP
+ return (void*) base;
+ #else
+@@ -122,7 +162,7 @@ void *MemArena::CreateView(s64 offset, size_t size, void *base)
((base == 0) ? 0 : MAP_FIXED), fd, offset);
if (retval == MAP_FAILED) {
@@ -178,23 +222,33 @@ index a413f4eaf..bbd68c9d1 100644
return 0;
}
return retval;
-@@ -136,6 +157,42 @@ void MemArena::ReleaseView(s64 offset, void* view, size_t size) {
+@@ -130,12 +170,52 @@ void *MemArena::CreateView(s64 offset, size_t size, void *base)
+ }
+
+ void MemArena::ReleaseView(s64 offset, void* view, size_t size) {
++#if PPSSPP_PLATFORM(PS5)
++ // A view in the reserved 4 GiB leaves its hole reserved, so nothing else is
++ // mapped there before the next instance maps its views again.
++ ps5_shm_unmap(view, size, ps5_in_reserved(view, size) ? PS5_SHM_KEEP_RESERVED : 0);
++ return;
++#endif
+ #ifndef NO_MMAP
+ munmap(view, size);
+ #endif
}
u8* MemArena::Find4GBBase() {
+#if PPSSPP_PLATFORM(PS5)
+ // PS5: an anonymous PROT_NONE mapping of that size is charged to the title's
+ // small flexible-memory budget and fails, and the hardcoded fallback below
-+ // would map over whatever lives there. The kernel's own virtual-range
-+ // reservation holds 4 GiB without backing it; views are then mapped into it
-+ // with MAP_FIXED. It is kept while the core is loaded and reused.
-+ // The address asked for is 0x6_0000_0000: outside the 4 GiB window at high
-+ // word 2, which the console's Vulkan driver needs for every GPU-visible
-+ // mapping (a first run without a hint landed at 0x2_0227_0000, in it).
-+ static void *reserved = nullptr;
-+ if (!reserved) {
-+ void *address = (void *)0x600000000ULL;
-+ int32_t result = sceKernelReserveVirtualRange(&address, 0x100000000ULL, 0, 0x10000);
++ // would map over whatever lives there. The platform layer reserves 4 GiB
++ // without backing it, outside the Vulkan driver's GPU window; views are then
++ // mapped into it at fixed addresses. It is kept while the core is loaded and
++ // reused. The address asked for is 0x6_0000_0000.
++ if (!ps5_reserved) {
++ void *const hint = (void *)0x600000000ULL;
++ void *address = nullptr;
++ int result = ps5_vrange_reserve(ps5_reserved_bytes, hint, 0x10000, &address);
+ // The static above lives in the core image, and RetroArch unloads the
+ // core when content closes: a reloaded core starts with it null while
+ // the previous instance's reservation (and any view it left mapped)
@@ -202,10 +256,10 @@ index a413f4eaf..bbd68c9d1 100644
+ // ("Memory init failed", 2026-09-24). The range is this arena's alone,
+ // so whatever an earlier instance left there is released and the
+ // reservation made again.
-+ if (result != 0) {
-+ munmap((void *)0x600000000ULL, 0x100000000ULL);
-+ address = (void *)0x600000000ULL;
-+ result = sceKernelReserveVirtualRange(&address, 0x100000000ULL, 0, 0x10000);
++ if (result == 0 && address != hint) {
++ ps5_vrange_release(address, ps5_reserved_bytes);
++ ps5_vrange_release(hint, ps5_reserved_bytes);
++ result = ps5_vrange_reserve(ps5_reserved_bytes, hint, 0x10000, &address);
+ if (result == 0)
+ INFO_LOG(Log::MemMap, "PS5: released the previous instance's 4 GiB and reserved it again");
+ }
@@ -213,30 +267,42 @@ index a413f4eaf..bbd68c9d1 100644
+ ERROR_LOG(Log::MemMap, "PS5: reserving 4 GiB failed: %08x", (unsigned)result);
+ return nullptr;
+ }
-+ reserved = address;
-+ INFO_LOG(Log::MemMap, "PS5: guest address space reserved at %p (4 GiB)", reserved);
++ ps5_reserved = address;
++ INFO_LOG(Log::MemMap, "PS5: guest address space reserved at %p (4 GiB)", ps5_reserved);
+ }
-+ return static_cast(reserved);
++ return static_cast(ps5_reserved);
+#endif
// Now, create views in high memory where there's plenty of space.
#if PPSSPP_ARCH(64BIT) && !defined(USE_ASAN) && !defined(NO_MMAP)
// We should probably just go look in /proc/self/maps for some free space.
diff --git a/Common/MemoryUtil.cpp b/Common/MemoryUtil.cpp
-index be2a14ddb..a48cae66d 100644
+index be2a14ddb..dbd608bd1 100644
--- a/Common/MemoryUtil.cpp
+++ b/Common/MemoryUtil.cpp
-@@ -189,7 +189,19 @@ void *AllocateExecutableMemory(size_t size) {
+@@ -43,6 +43,10 @@
+
+ #ifndef _WIN32
+ #include
++
++#if PPSSPP_PLATFORM(PS5)
++#include
++#endif
+ #endif
+ static int hint_location;
+ #ifdef __APPLE__
+@@ -189,7 +193,20 @@ void *AllocateExecutableMemory(size_t size) {
if (PlatformIsWXExclusive())
prot = PROT_READ | PROT_WRITE; // POST_EXEC is added later in this case.
+#if PPSSPP_PLATFORM(PS5)
-+ // PS5: a mapping asked for with PROT_EXEC is returned but does not execute;
-+ // one mapped read-write and then made executable with mprotect does (console
-+ // probes of this platform). So the protection is added in a second step.
-+ void* ptr = mmap(map_hint, size, PROT_READ | PROT_WRITE, MAP_ANON | MAP_PRIVATE, -1, 0);
-+ if (ptr != MAP_FAILED && prot != (PROT_READ | PROT_WRITE) && mprotect(ptr, size, prot) != 0) {
-+ ERROR_LOG(Log::MemMap, "Failed to make %d bytes executable, errno=%d", (int)size, errno);
-+ munmap(ptr, size);
++ // PS5: code lives in direct memory through the platform layer (my payload
++ // SDK fork, ps5platform/exec.h): read, write and execute at once, within a
++ // 32-bit displacement of this core's own code, and out of the 12 GiB direct
++ // pool rather than the title's 448 MiB of flexible memory.
++ (void)map_hint;
++ void* ptr = ps5_exec_allocate(size, (uintptr_t)&AllocateExecutableMemory);
++ if (!ptr) {
++ ERROR_LOG(Log::MemMap, "PS5: no executable memory for %d bytes", (int)size);
+ ptr = MAP_FAILED;
+ }
+#else
@@ -245,6 +311,19 @@ index be2a14ddb..a48cae66d 100644
if (ptr == MAP_FAILED) {
ptr = nullptr;
+@@ -276,6 +293,12 @@ void FreeMemoryPages(void *ptr, size_t size) {
+ }
+
+ void FreeExecutableMemory(void *ptr, size_t size) {
++#if PPSSPP_PLATFORM(PS5)
++ if (ptr) {
++ ps5_exec_release(ptr);
++ return;
++ }
++#endif
+ FreeMemoryPages(ptr, size);
+ }
+
diff --git a/Common/Thread/ThreadUtil.cpp b/Common/Thread/ThreadUtil.cpp
index e842a0312..12a87d20b 100644
--- a/Common/Thread/ThreadUtil.cpp
diff --git a/src/crash_report.cpp b/src/crash_report.cpp
index 885ac6a..cf5b38f 100644
--- a/src/crash_report.cpp
+++ b/src/crash_report.cpp
@@ -26,6 +26,8 @@
#include
#include
+#include
+
extern "C" int sceKernelAvailableFlexibleMemorySize(std::size_t *size);
namespace
@@ -41,12 +43,10 @@ void report(int signal, siginfo_t *info, void *context_pointer)
funlockfile(stderr);
}
const ucontext_t *context = static_cast(context_pointer);
- /* The console's mcontext is not the SDK header's FreeBSD layout: measured on
- * a call through a null pointer (FBNeo, 2026-09-23), rip, cs (0x43), rflags,
- * rsp and ss (0x3b) sit at words 26-30, six words past the header's mc_rip. */
- const auto *words = reinterpret_cast(&context->uc_mcontext);
- const std::uintptr_t rip = static_cast(words[26]);
- const std::uintptr_t rsp = static_cast(words[29]);
+ /* The SDK fork's ucontext_t is the console's layout (ps5platform/context.h
+ * refuses any other), so the registers are the header's own fields. */
+ const std::uintptr_t rip = static_cast(context->uc_mcontext.mc_rip);
+ const std::uintptr_t rsp = static_cast(context->uc_mcontext.mc_rsp);
/* After a call through a bad pointer the return address is on top of the
* stack; otherwise the word is only a hint, and is read only when rsp looks
* like a stack address. */
diff --git a/src/frontend_ps5.cpp b/src/frontend_ps5.cpp
index b22785d..bfa9055 100644
--- a/src/frontend_ps5.cpp
+++ b/src/frontend_ps5.cpp
@@ -11,7 +11,7 @@ extern "C"
#include
#include
#include
-#include "ps5_directory.h"
+#include
#include
#include
diff --git a/src/libc_shims.c b/src/libc_shims.c
index 7240404..e2c4c89 100644
--- a/src/libc_shims.c
+++ b/src/libc_shims.c
@@ -1,111 +1,26 @@
/*
- * PS5 RetroArch - libc functions a core imports that the console's SDK does not
- * resolve, or resolves to something that faults: gmtime_r, arc4random,
- * statvfs and utimensat.
+ * PS5 RetroArch - the libc functions the title's own code and its libc++ link
+ * by their standard names, where the console has none that works: gmtime_r
+ * and utimensat. The implementations are the platform layer's (my payload SDK
+ * fork, include/ps5platform/libc.h); these give them the standard names inside
+ * the title. A core imports them through tools/core-imports.py instead.
*
* Copyright (C) 2026 Mihawk
* SPDX-License-Identifier: GPL-3.0-or-later
- *
- * PPSSPP's FFmpeg (libavutil's time formatting) calls the reentrant form; the
- * SDK has the plain gmtime, whose result lives in one static buffer. The copy is
- * taken under a lock, so concurrent callers each get their own conversion.
*/
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-
-uint64_t sceKernelReadTsc(void);
-
-static pthread_mutex_t gmtime_lock = PTHREAD_MUTEX_INITIALIZER;
+#include
+/* PPSSPP's FFmpeg (libavutil's time formatting) calls the reentrant form; the
+ * console's libc has only gmtime. */
struct tm *gmtime_r(const time_t *time, struct tm *result)
{
- pthread_mutex_lock(&gmtime_lock);
- const struct tm *shared = gmtime(time);
- if (shared)
- memcpy(result, shared, sizeof(*result));
- pthread_mutex_unlock(&gmtime_lock);
- return shared ? result : NULL;
+ return ps5_gmtime_r(time, result);
}
-/* arc4random: FFmpeg's configure finds it on a FreeBSD target, and libavutil
- * seeds its random helper with it; the console resolves the import to nothing.
- * Nothing here needs cryptographic strength, so it is a splitmix64 generator
- * seeded once from the timestamp counter. Bound to cores by tools/core-imports.py
- * under its own name: a title-defined arc4random would be an export, which the
- * title converter refuses. */
-static uint64_t random_state;
-
-uint32_t ps5_arc4random(void)
-{
- uint64_t state = __atomic_load_n(&random_state, __ATOMIC_RELAXED);
- if (state == 0)
- state = sceKernelReadTsc() | 1;
- state += UINT64_C(0x9e3779b97f4a7c15);
- __atomic_store_n(&random_state, state, __ATOMIC_RELAXED);
- uint64_t z = state;
- z = (z ^ (z >> 30)) * UINT64_C(0xbf58476d1ce4e5b9);
- z = (z ^ (z >> 27)) * UINT64_C(0x94d049bb133111eb);
- return (uint32_t)(z ^ (z >> 31));
-}
-
-/* statvfs: the console libc's own jumps to an unmapped address (0x10_002629b6)
- * from inside libc -- it is built on statfs, which only libkernel_sys carries.
- * PPSSPP asks for the free space of its save directory whenever a game checks
- * that its save data fits (sceUtilitySavedata GETSIZE), and Yu-Gi-Oh! GX Tag
- * Force crashed there at every start (2026-09-24). No query this title can make
- * reports the data partition's free space, so this answers a writable
- * filesystem with 16 GiB free, which is what those checks need: room.
- * Bound to cores by tools/core-imports.py under its own name. */
-int ps5_statvfs(const char *path, struct statvfs *result)
-{
- (void)path;
- if (result == NULL)
- return -1;
- memset(result, 0, sizeof(*result));
- result->f_bsize = 32768;
- result->f_frsize = 32768;
- result->f_blocks = (fsblkcnt_t)(UINT64_C(64) << 30) / 32768;
- result->f_bfree = (fsblkcnt_t)(UINT64_C(16) << 30) / 32768;
- result->f_bavail = result->f_bfree;
- result->f_namemax = 255;
- return 0;
-}
-
-/* utimensat: libc++'s std::filesystem::last_write_time(path, time) sets a file's
- * times with it, and Dolphin's core links that setter (its file utilities), so
- * the title's libc++ archive needs it; the SDK exports no such symbol. The
- * console's libkernel has utimes, which takes microseconds. Only what those
- * callers pass is supported: paths relative to the working directory
- * (AT_FDCWD), both times given or both left to "now". */
+/* libc++'s std::filesystem::last_write_time(path, time) sets a file's times
+ * with it, and Dolphin's file utilities link that setter. */
int utimensat(int directory, const char *path, const struct timespec times[2], int flags)
{
- (void)flags;
- if (directory != AT_FDCWD)
- {
- errno = ENOSYS;
- return -1;
- }
- if (times == NULL || (times[0].tv_nsec == UTIME_NOW && times[1].tv_nsec == UTIME_NOW))
- return utimes(path, NULL);
- if (times[0].tv_nsec == UTIME_OMIT || times[1].tv_nsec == UTIME_OMIT ||
- times[0].tv_nsec == UTIME_NOW || times[1].tv_nsec == UTIME_NOW)
- {
- errno = ENOSYS;
- return -1;
- }
- struct timeval values[2];
- for (int index = 0; index < 2; ++index)
- {
- values[index].tv_sec = times[index].tv_sec;
- values[index].tv_usec = times[index].tv_nsec / 1000;
- }
- return utimes(path, values);
+ return ps5_utimensat(directory, path, times, flags);
}
diff --git a/src/main.cpp b/src/main.cpp
index db0223f..d365e39 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -44,6 +44,8 @@
#include
#include
+#include
+#include
#include "trace.hpp"
#include "memory_diagnostics.hpp"
@@ -77,9 +79,11 @@ namespace
constexpr const char *config_path = "/app0/config/retroarch.cfg";
/* A profiled test run (the driver's /app0/ps5vk-profile.txt survived the stale
- * test file sweep) also reports the flexible memory left every ten seconds:
- * with the driver's direct_live, a soak's leak is a line that only moves one
- * way. Set once main has swept. */
+ * test file sweep) also reports the flexible memory left every ten seconds,
+ * and what the platform layer holds: the cores' executable code, their
+ * direct-memory arenas and the ranges reserved for them. With the driver's
+ * direct_live, a soak's leak is a line that only moves one way. Set once main
+ * has swept. */
std::atomic g_memory_report{false};
/* Writes every buffered output stream out -- the trace file and RetroArch's
@@ -95,6 +99,9 @@ void *log_flusher(void *)
std::size_t flexible = 0;
sceKernelAvailableFlexibleMemorySize(&flexible);
std::fprintf(stderr, "memory: flexible_free=%zu KiB\n", flexible >> 10);
+ char platform[192];
+ ps5_platform_report(platform, sizeof(platform));
+ std::fprintf(stderr, "memory: %s\n", platform);
}
std::fflush(nullptr);
}
@@ -108,6 +115,45 @@ void start_log_flusher()
pthread_detach(thread);
}
+/* The platform probe (the SDK fork's platform/src/probe.c):
+ * /app0/platform-probe.txt, a test file of one launch like the others, runs the
+ * console capability probe in this process -- the one whose flexible budget,
+ * imports, GPU window and core loader the cores live with -- and ends the
+ * launch. Its words select the optional tests: "large" (4 GiB of guest memory
+ * beside 1 GiB of code), "huge" (10 GiB mapped and written at once), "full"
+ * (the largest direct allocation mapped and written) and "jit" (the
+ * shared-memory JIT interface, last, since a refused import would stop the
+ * process). Every line goes to the trace as it is measured. */
+static void probe_line(void *, const char *line)
+{
+ ps5::debug::mark(line);
+}
+
+static bool run_platform_probe()
+{
+ std::FILE *const file = std::fopen("/app0/platform-probe.txt", "r");
+ if (!file)
+ return false;
+ char words[128] = {};
+ const size_t read = std::fread(words, 1, sizeof(words) - 1, file);
+ words[read] = '\0';
+ std::fclose(file);
+ std::remove("/app0/platform-probe.txt");
+ unsigned flags = 0;
+ if (std::strstr(words, "large"))
+ flags |= PS5_PROBE_LARGE;
+ if (std::strstr(words, "huge"))
+ flags |= PS5_PROBE_HUGE;
+ if (std::strstr(words, "full"))
+ flags |= PS5_PROBE_FULL;
+ if (std::strstr(words, "jit"))
+ flags |= PS5_PROBE_JIT_API;
+ const int failures = ps5_platform_probe(probe_line, nullptr, flags);
+ ps5::debug::mark_value("platform probe: failures", failures);
+ std::fflush(stderr);
+ return true;
+}
+
/* Profile 10's entry state (docs/PHASE_LOG.md): /app0/state-copy.txt names a
* source and a destination, a line each, and a test run copies the one to the
* other before RetroArch starts. The save-state stress keeps its states in a
@@ -333,6 +379,7 @@ int main()
"/app0/dolphin-options.txt",
"/app0/dolphin-debug.txt",
"/app0/state-copy.txt",
+ "/app0/platform-probe.txt",
};
unsigned removed = 0;
for (const char *const path : test_files)
@@ -342,6 +389,8 @@ int main()
}
copy_test_file();
std::remove("/app0/state-copy.txt");
+ if (run_platform_probe())
+ return 0;
if (std::FILE *profile = std::fopen("/app0/ps5vk-profile.txt", "rb"))
{
std::fclose(profile);
diff --git a/src/permissions_ps5.cpp b/src/permissions_ps5.cpp
index 0636253..16ac515 100644
--- a/src/permissions_ps5.cpp
+++ b/src/permissions_ps5.cpp
@@ -34,7 +34,7 @@
#include
#include
-#include "ps5_directory.h"
+#include
#include "trace.hpp"
extern "C" int __real_mkdir(const char *path, mode_t mode);
diff --git a/src/platform_wraps.c b/src/platform_wraps.c
new file mode 100644
index 0000000..7493785
--- /dev/null
+++ b/src/platform_wraps.c
@@ -0,0 +1,61 @@
+/*
+ * PS5 RetroArch - the title's links to the directory functions, through the
+ * platform layer (my payload SDK fork, include/ps5platform/libc.h).
+ *
+ * Copyright (C) 2026 Mihawk
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * tools/build-title.sh links the title with --wrap for each of these. A
+ * title's opendir is refused and the *at functions resolve to nothing (only
+ * libkernel_sys exports them), so the frontend's and libc++'s calls land here.
+ * A core's own imports of the same names are bound to the ps5_ functions by
+ * tools/core-imports.py.
+ */
+
+#include
+#include
+
+#include
+
+int __wrap_openat(int directory, const char *name, int flags, ...)
+{
+ int mode = 0;
+ if (flags & O_CREAT)
+ {
+ va_list arguments;
+ va_start(arguments, flags);
+ mode = va_arg(arguments, int);
+ va_end(arguments);
+ }
+ return ps5_openat(directory, name, flags, mode);
+}
+
+int __wrap_unlinkat(int directory, const char *name, int flags)
+{
+ return ps5_unlinkat(directory, name, flags);
+}
+
+int __wrap_fchmodat(int directory, const char *name, mode_t mode, int flags)
+{
+ return ps5_fchmodat(directory, name, mode, flags);
+}
+
+DIR *__wrap_fdopendir(int fd)
+{
+ return ps5_fdopendir(fd);
+}
+
+DIR *__wrap_opendir(const char *path)
+{
+ return ps5_opendir(path);
+}
+
+struct dirent *__wrap_readdir(DIR *directory)
+{
+ return ps5_readdir(directory);
+}
+
+int __wrap_closedir(DIR *directory)
+{
+ return ps5_closedir(directory);
+}
diff --git a/src/ps5_directory.cpp b/src/ps5_directory.cpp
deleted file mode 100644
index 467a6b7..0000000
--- a/src/ps5_directory.cpp
+++ /dev/null
@@ -1,326 +0,0 @@
-/* PS5 RetroArch - native directory enumeration without libc's denied opendir.
- * Copyright (C) 2026 Mihawk
- * SPDX-License-Identifier: GPL-3.0-or-later
- */
-#include "ps5_directory.h"
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-
-extern "C" int getdents(int fd, char *buffer, int bytes);
-
-namespace
-{
-struct Directory
-{
- int fd = -1;
- size_t offset = 0, bytes = 0;
- bool finished = false;
- // Mounted title directories require a larger read than the synthetic root.
- char buffer[64 * 1024];
- struct dirent entry{};
-};
-} // namespace
-
-extern "C" DIR *ps5_fdopendir(int fd)
-{
- struct stat status;
- if (fstat(fd, &status) != 0)
- return nullptr;
- if (!S_ISDIR(status.st_mode))
- {
- errno = ENOTDIR;
- return nullptr;
- }
- auto *directory = new (std::nothrow) Directory;
- if (!directory)
- {
- errno = ENOMEM;
- return nullptr;
- }
- directory->fd = fd;
- return reinterpret_cast(directory);
-}
-
-extern "C" DIR *ps5_opendir(const char *path)
-{
- if (!path || !*path)
- {
- errno = ENOENT;
- return nullptr;
- }
- const int fd = open(path, O_RDONLY | O_DIRECTORY);
- if (fd < 0)
- return nullptr;
- auto *directory = new (std::nothrow) Directory;
- if (!directory)
- {
- close(fd);
- errno = ENOMEM;
- return nullptr;
- }
- directory->fd = fd;
- return reinterpret_cast(directory);
-}
-
-extern "C" struct dirent *ps5_readdir(DIR *opaque)
-{
- auto *directory = reinterpret_cast(opaque);
- if (!directory)
- {
- errno = EBADF;
- return nullptr;
- }
- while (!directory->finished)
- {
- if (directory->offset == directory->bytes)
- {
- const int count = getdents(directory->fd, directory->buffer, sizeof(directory->buffer));
- if (count <= 0)
- {
- directory->finished = true;
- return nullptr;
- }
- if (size_t(count) > sizeof(directory->buffer))
- {
- directory->finished = true;
- errno = EIO;
- return nullptr;
- }
- directory->bytes = size_t(count);
- directory->offset = 0;
- }
- // Public SDK FreeBSD dirent wire layout: inode32, reclen16, type8, namlen8.
- const size_t remaining = directory->bytes - directory->offset;
- const char *record = directory->buffer + directory->offset;
- uint32_t inode = 0;
- uint16_t length = 0;
- if (remaining >= 8)
- {
- std::memcpy(&inode, record, 4);
- std::memcpy(&length, record + 4, 2);
- }
- const size_t name_length = remaining >= 8 ? uint8_t(record[7]) : 0;
- if (remaining < 8 || length < 8 + name_length + 1 || length > remaining ||
- name_length >= sizeof(directory->entry.d_name) || record[8 + name_length] != 0)
- {
- directory->finished = true;
- errno = EIO;
- return nullptr;
- }
- directory->offset += length;
- if (!inode)
- continue;
- directory->entry = {};
- directory->entry.d_type = uint8_t(record[6]);
- std::memcpy(directory->entry.d_name, record + 8, name_length + 1);
- return &directory->entry;
- }
- return nullptr;
-}
-
-extern "C" void ps5_rewinddir(DIR *opaque)
-{
- auto *directory = reinterpret_cast(opaque);
- if (!directory)
- {
- errno = EBADF;
- return;
- }
- if (lseek(directory->fd, 0, SEEK_SET) < 0)
- return;
- directory->offset = directory->bytes = 0;
- directory->finished = false;
-}
-
-/* The descriptor under a directory stream: a core's libretro VFS stats entries
- * with fstatat(dirfd(dir), ...), and libc's dirfd would read its own DIR layout
- * from this one (tools/core-imports.py binds a core's dirfd here). */
-extern "C" int ps5_dirfd(DIR *opaque)
-{
- auto *directory = reinterpret_cast(opaque);
- if (!directory)
- {
- errno = EINVAL;
- return -1;
- }
- return directory->fd;
-}
-
-extern "C" int ps5_closedir(DIR *opaque)
-{
- auto *directory = reinterpret_cast(opaque);
- if (!directory)
- {
- errno = EBADF;
- return -1;
- }
- const int result = close(directory->fd);
- delete directory;
- return result;
-}
-
-/* The *at family for the title's own libc++.
- *
- * std::filesystem in the title's libc++ archive (the one every core's C++ uses)
- * walks directories with openat, fdopendir and unlinkat (remove_all) and sets
- * permissions with fchmodat. The console's libkernel exports none of them - the
- * SDK stub lists them, and the imports resolve to null, so Dolphin's first
- * remove_all called address 0 (2026-09-24) - and libc's opendir is refused
- * outright (see above). The title link wraps all of them (tools/build-title.sh),
- * so libc++ reaches these instead.
- *
- * A path relative to a directory descriptor is resolved against the path that
- * descriptor was opened with, which openat records. The record is checked
- * against the descriptor's device and inode before use, so a descriptor number
- * that was closed and reused elsewhere is never resolved to a stale path. */
-namespace
-{
-struct DirectoryPath
-{
- int fd = -1;
- dev_t device = 0;
- ino_t inode = 0;
- char path[1024];
-};
-
-constexpr int kTrackedDirectories = 64;
-std::mutex directory_paths_lock;
-DirectoryPath directory_paths[kTrackedDirectories];
-
-void remember_directory(int fd, const char *path)
-{
- struct stat status;
- if (fstat(fd, &status) != 0 || !S_ISDIR(status.st_mode) ||
- std::strlen(path) >= sizeof(DirectoryPath::path))
- return;
- std::lock_guard hold(directory_paths_lock);
- DirectoryPath *slot = nullptr;
- for (auto &entry : directory_paths)
- if (entry.fd == fd || (!slot && entry.fd < 0))
- slot = &entry;
- if (!slot)
- slot = &directory_paths[fd % kTrackedDirectories];
- slot->fd = fd;
- slot->device = status.st_dev;
- slot->inode = status.st_ino;
- std::strcpy(slot->path, path);
-}
-
-void forget_directory(int fd)
-{
- std::lock_guard hold(directory_paths_lock);
- for (auto &entry : directory_paths)
- if (entry.fd == fd)
- entry.fd = -1;
-}
-
-/* The path of name relative to directory, in out; false (errno set) when the
- * directory descriptor is not one openat recorded. */
-bool resolve_at(int directory, const char *name, char *out, size_t size)
-{
- if (!name)
- {
- errno = EFAULT;
- return false;
- }
- if (directory == AT_FDCWD || name[0] == '/')
- {
- if (std::strlen(name) >= size)
- {
- errno = ENAMETOOLONG;
- return false;
- }
- std::strcpy(out, name);
- return true;
- }
- struct stat status;
- if (fstat(directory, &status) != 0)
- return false;
- std::lock_guard hold(directory_paths_lock);
- for (const auto &entry : directory_paths)
- {
- if (entry.fd != directory || entry.device != status.st_dev || entry.inode != status.st_ino)
- continue;
- const size_t base = std::strlen(entry.path);
- const bool slash = base > 0 && entry.path[base - 1] == '/';
- if (base + (slash ? 0 : 1) + std::strlen(name) >= size)
- {
- errno = ENAMETOOLONG;
- return false;
- }
- std::strcpy(out, entry.path);
- if (!slash)
- std::strcat(out, "/");
- std::strcat(out, name);
- return true;
- }
- errno = ENOSYS;
- return false;
-}
-} // namespace
-
-extern "C" int __wrap_openat(int directory, const char *name, int flags, ...)
-{
- int mode = 0;
- if (flags & O_CREAT)
- {
- va_list arguments;
- va_start(arguments, flags);
- mode = va_arg(arguments, int);
- va_end(arguments);
- }
- char path[1024];
- if (!resolve_at(directory, name, path, sizeof(path)))
- return -1;
- const int fd = open(path, flags, mode);
- if (fd >= 0)
- remember_directory(fd, path);
- return fd;
-}
-
-extern "C" int __wrap_unlinkat(int directory, const char *name, int flags)
-{
- char path[1024];
- if (!resolve_at(directory, name, path, sizeof(path)))
- return -1;
- return (flags & AT_REMOVEDIR) ? rmdir(path) : unlink(path);
-}
-
-extern "C" int __wrap_fchmodat(int directory, const char *name, mode_t mode, int flags)
-{
- (void)flags;
- char path[1024];
- if (!resolve_at(directory, name, path, sizeof(path)))
- return -1;
- return chmod(path, mode);
-}
-
-extern "C" DIR *__wrap_fdopendir(int fd)
-{
- return ps5_fdopendir(fd);
-}
-
-extern "C" DIR *__wrap_opendir(const char *path)
-{
- return ps5_opendir(path);
-}
-
-extern "C" struct dirent *__wrap_readdir(DIR *directory)
-{
- return ps5_readdir(directory);
-}
-
-extern "C" int __wrap_closedir(DIR *directory)
-{
- auto *native = reinterpret_cast(directory);
- if (native)
- forget_directory(native->fd);
- return ps5_closedir(directory);
-}
diff --git a/src/ps5_directory.h b/src/ps5_directory.h
deleted file mode 100644
index d6c504a..0000000
--- a/src/ps5_directory.h
+++ /dev/null
@@ -1,16 +0,0 @@
-/* PS5 RetroArch - public SDK directory enumeration adapter. */
-#pragma once
-#include
-#ifdef __cplusplus
-extern "C"
-{
-#endif
- DIR *ps5_opendir(const char *path);
- DIR *ps5_fdopendir(int fd);
- struct dirent *ps5_readdir(DIR *directory);
- void ps5_rewinddir(DIR *directory);
- int ps5_dirfd(DIR *directory);
- int ps5_closedir(DIR *directory);
-#ifdef __cplusplus
-}
-#endif
diff --git a/src/sampler_ps5.cpp b/src/sampler_ps5.cpp
index a874eec..8deeb35 100644
--- a/src/sampler_ps5.cpp
+++ b/src/sampler_ps5.cpp
@@ -33,6 +33,8 @@
#include
#include
+#include
+
extern "C"
{
/* ../PS5_Vulkan's debug API; the title always links the driver. */
@@ -74,12 +76,10 @@ constexpr std::uint64_t kStackSpan = 16ull * 1024 * 1024;
void on_sample(int, siginfo_t *, void *context_pointer)
{
const auto *context = static_cast(context_pointer);
- /* The console's mcontext is the SDK header's shifted by six words
- * (src/crash_report.cpp): rip at 26, rsp at 29, and so rbp at 15. */
- const auto *words = reinterpret_cast(&context->uc_mcontext);
+ const mcontext_t ®isters = context->uc_mcontext;
const std::uint32_t at = g_head.fetch_add(1, std::memory_order_relaxed);
std::uint64_t *const sample = g_ring[at & (kRingSize - 1)];
- const std::uint64_t rsp = words[29];
+ const std::uint64_t rsp = static_cast(registers.mc_rsp);
unsigned thread = 0;
const pthread_t self = pthread_self();
const unsigned count = g_thread_count.load(std::memory_order_acquire);
@@ -87,9 +87,9 @@ void on_sample(int, siginfo_t *, void *context_pointer)
if (pthread_equal(g_threads[index], self))
thread = index;
sample[0] = (g_stall.load(std::memory_order_relaxed) ? 1u : 0u) | (std::uint64_t{thread} << 8);
- sample[1] = words[26];
+ sample[1] = static_cast(registers.mc_rip);
sample[2] = (rsp & 7) == 0 && rsp != 0 ? *reinterpret_cast(rsp) : 0;
- std::uint64_t frame = words[15];
+ std::uint64_t frame = static_cast(registers.mc_rbp);
for (unsigned depth = 3; depth <= kFrames; ++depth)
{
if ((frame & 7) != 0 || frame < rsp || frame + 16 > rsp + kStackSpan)
diff --git a/tests/ps5_directory_test.cpp b/tests/ps5_directory_test.cpp
deleted file mode 100644
index c198234..0000000
--- a/tests/ps5_directory_test.cpp
+++ /dev/null
@@ -1,115 +0,0 @@
-/* Clock-free SDK getdents fixture: actual adapter, including malformed records. */
-#include
-#include
-#include
-#include "../src/ps5_directory.cpp"
-
-namespace
-{
-std::vector> batches;
-size_t batch = 0;
-int closed = 0;
-bool seek_fails = false;
-std::vector record(uint32_t inode, unsigned type, const std::string &name)
-{
- uint16_t length = uint16_t((8 + name.size() + 1 + 3) & ~3u);
- std::vector bytes(length);
- std::memcpy(bytes.data(), &inode, 4);
- std::memcpy(bytes.data() + 4, &length, 2);
- bytes[6] = char(type);
- bytes[7] = char(name.size());
- std::memcpy(bytes.data() + 8, name.c_str(), name.size() + 1);
- return bytes;
-}
-} // namespace
-extern "C" int __wrap_open(const char *path, int flags, ...)
-{
- assert(flags == (O_RDONLY | O_DIRECTORY));
- if (std::strcmp(path, "/denied") == 0)
- {
- errno = EACCES;
- return -1;
- }
- return 0; // fd zero is valid and must be closed too.
-}
-extern "C" int __wrap_close(int fd)
-{
- assert(fd == 0);
- ++closed;
- return 0;
-}
-extern "C" off_t lseek(int fd, off_t offset, int whence)
-{
- assert(fd == 0 && offset == 0 && whence == SEEK_SET);
- if (seek_fails)
- {
- errno = EINVAL;
- return -1;
- }
- batch = 0;
- return 0;
-}
-extern "C" int getdents(int fd, char *buffer, int size)
-{
- assert(fd == 0);
- // Model the mounted filesystem that rejects small directory reads.
- if (size < 64 * 1024)
- {
- errno = EINVAL;
- return -1;
- }
- if (batch == batches.size())
- return 0;
- const auto &data = batches[batch++];
- assert(data.size() <= size_t(size));
- std::memcpy(buffer, data.data(), data.size());
- return int(data.size());
-}
-int main()
-{
- assert(ps5_opendir("/denied") == nullptr && errno == EACCES);
- assert(ps5_opendir(nullptr) == nullptr && errno == ENOENT);
- auto first = record(0, DT_REG, "deleted");
- auto folder = record(2, DT_DIR, "cores");
- first.insert(first.end(), folder.begin(), folder.end());
- batches = {first, record(3, DT_REG, "Example ROM.bin")};
- DIR *dir = ps5_opendir("/app0");
- assert(dir);
- auto *entry = ps5_readdir(dir);
- assert(entry && entry->d_type == DT_DIR && std::string(entry->d_name) == "cores");
- entry = ps5_readdir(dir);
- assert(entry && entry->d_type == DT_REG && std::string(entry->d_name) == "Example ROM.bin");
- errno = 0;
- assert(!ps5_readdir(dir) && errno == 0 && !ps5_readdir(dir));
- seek_fails = true;
- ps5_rewinddir(dir);
- assert(errno == EINVAL && !ps5_readdir(dir));
- seek_fails = false;
- ps5_rewinddir(dir);
- entry = ps5_readdir(dir);
- assert(entry && std::string(entry->d_name) == "cores");
- ps5_rewinddir(dir); // Reset a partially consumed batch too.
- entry = ps5_readdir(dir);
- assert(entry && std::string(entry->d_name) == "cores");
- assert(ps5_closedir(dir) == 0 && closed == 1);
- // Truncated headers, oversized/zero records and missing name terminators fail closed.
- auto oversized = record(1, DT_DIR, "x");
- oversized[4] = char(255);
- auto zero = record(1, DT_DIR, "x");
- zero[4] = 0;
- auto unterminated = record(1, DT_DIR, "x");
- unterminated[9] = 'x';
- for (const auto &bad : {std::vector(7), oversized, zero, unterminated})
- {
- batches = {bad};
- batch = 0;
- dir = ps5_opendir("/app0");
- errno = 0;
- assert(!ps5_readdir(dir) && errno == EIO);
- assert(!ps5_readdir(dir));
- ps5_closedir(dir);
- }
- assert(closed == 5);
- std::puts(
- "ps5_directory: SDK record parsing, multiple batches, EOF, denial and corruption PASS");
-}
diff --git a/tests/test_platform_paths.py b/tests/test_platform_paths.py
index 1c75d98..c61eb28 100644
--- a/tests/test_platform_paths.py
+++ b/tests/test_platform_paths.py
@@ -12,21 +12,20 @@ class PlatformPaths(unittest.TestCase):
with tempfile.TemporaryDirectory() as td:
binary = str(Path(td) / 'paths-test')
functions = ['fopen', 'stat', 'mkdir', 'chmod', 'rename', 'remove']
+ # The platform layer's headers, alone: the SDK's FreeBSD tree is
+ # not a host compiler's.
+ platform = Path(td) / 'platform-include'
+ platform.mkdir()
+ (platform / 'ps5platform').symlink_to(
+ ROOT / '.deps/native/ps5-payload-sdk/target/include/ps5platform')
subprocess.run(['c++', '-std=c++17', '-O2', '-Wall', '-Wextra', '-Werror',
'-Ivendor/retroarch', '-Ivendor/retroarch/libretro-common/include',
+ f'-I{platform}',
'tests/frontend_ps5_test.cpp', '-o', binary,
*[f'-Wl,--wrap={name}' for name in functions]], cwd=ROOT, check=True)
subprocess.run([binary, str(Path(td) / 'filesystem')], cwd=ROOT,
check=True, timeout=10)
- def test_native_directory_records(self):
- with tempfile.TemporaryDirectory() as td:
- binary = str(Path(td) / 'directory-test')
- subprocess.run(['c++', '-std=c++17', '-O2', '-Wall', '-Wextra', '-Werror',
- 'tests/ps5_directory_test.cpp', '-o', binary,
- '-Wl,--wrap=open', '-Wl,--wrap=close'], cwd=ROOT, check=True)
- subprocess.run([binary], cwd=ROOT, check=True, timeout=10)
-
def test_application_path_avoids_procfs(self):
import importlib.util
import re
diff --git a/tools/build-title.sh b/tools/build-title.sh
index ab26536..a0698cf 100755
--- a/tools/build-title.sh
+++ b/tools/build-title.sh
@@ -49,6 +49,10 @@ case "${1:-}" in
esac
sdk="$root/.deps/native/ps5-payload-sdk"
+# The pinned SDK (my fork; tools/setup-native-dependencies.sh) is installed
+# before anything is compiled against it, and the cores' stamps include its
+# revision.
+bash "$root/tools/setup-native-dependencies.sh" >/dev/null
[[ -x $sdk/bin/prospero-lld ]] || {
echo "error: no SDK at $sdk; run this project's dependency bootstrap first" >&2
exit 2
@@ -223,8 +227,9 @@ PY
# The title's libc++ (std::filesystem) calls libc's opendir, which the console
# refuses, and openat/fdopendir/unlinkat/fchmodat, which its libkernel does not
-# export; src/ps5_directory.cpp implements all of them (a core's own imports of
-# the directory calls are bound to the same functions by tools/core-imports.py).
+# export; the platform layer implements all of them (libps5platform.a, from my
+# payload SDK fork), and src/platform_wraps.c binds these links to it. A core's
+# own imports of the same calls are bound to it by tools/core-imports.py.
directory_wrap_flags="--wrap=opendir --wrap=readdir --wrap=closedir --wrap=fdopendir --wrap=openat --wrap=unlinkat --wrap=fchmodat"
# Folders the title or a core creates are 0777 and files at least 0666, so FTP,
# which runs as another user, can reach them (src/permissions_ps5.cpp).
@@ -237,6 +242,7 @@ PYTHONPATH="$root/tooling/pystub${PYTHONPATH:+:$PYTHONPATH}" \
APP_DEFINITIONS="${title_definition_names[*]}" \
APP_INCLUDE_PATHS="build/ra-conf build vendor/retroarch build/ra-conf/libretro-common/include vendor/retroarch/deps vendor/retroarch/deps/stb" \
APP_STATIC_ARCHIVES="build/ra/libretroarch.a" \
+APP_SDK_ARCHIVES="libps5platform.a" \
APP_VULKAN_ARCHIVES="${vulkan_archives[*]}" \
APP_EXTRA_OBJECTS="${vulkan_objects[*]}" \
APP_LINK_FLAGS="$vulkan_flags --wrap=malloc --wrap=calloc --wrap=realloc --wrap=free $memory_wrap_flags $directory_wrap_flags" \
diff --git a/tools/build.sh b/tools/build.sh
index e5143d2..9fa58d0 100644
--- a/tools/build.sh
+++ b/tools/build.sh
@@ -119,12 +119,14 @@ done
definitions=()
includes=()
archives=()
+sdk_archives=()
pacbrew_packages=()
pacbrew_includes=()
pacbrew_archives=()
[[ -z ${APP_DEFINITIONS:-} ]] || read -r -a definitions <<< "$APP_DEFINITIONS"
[[ -z ${APP_INCLUDE_PATHS:-} ]] || read -r -a includes <<< "$APP_INCLUDE_PATHS"
[[ -z ${APP_STATIC_ARCHIVES:-} ]] || read -r -a archives <<< "$APP_STATIC_ARCHIVES"
+[[ -z ${APP_SDK_ARCHIVES:-} ]] || read -r -a sdk_archives <<< "$APP_SDK_ARCHIVES"
[[ -z ${PACBREW_PACKAGES:-} ]] || read -r -a pacbrew_packages <<< "$PACBREW_PACKAGES"
[[ -z ${PACBREW_INCLUDE_PATHS:-} ]] || read -r -a pacbrew_includes <<< "$PACBREW_INCLUDE_PATHS"
[[ -z ${PACBREW_STATIC_ARCHIVES:-} ]] || read -r -a pacbrew_archives <<< "$PACBREW_STATIC_ARCHIVES"
@@ -272,6 +274,13 @@ for archive in "${archives[@]}"; do
}
link_inputs+=("$root/$archive")
done
+# Archives the SDK installs (its platform layer): named, found in target/lib.
+for archive in "${sdk_archives[@]}"; do
+ [[ $archive =~ ^lib[A-Za-z0-9_.+-]+\.a$ && -f $sdk_root/target/lib/$archive ]] || {
+ echo "invalid SDK archive: $archive" >&2; exit 2;
+ }
+ link_inputs+=("$sdk_root/target/lib/$archive")
+done
if (( ${#pacbrew_libs[@]} > 0 )); then
link_inputs+=(--start-group "${pacbrew_libs[@]}" --end-group)
fi
diff --git a/tools/core-imports.py b/tools/core-imports.py
index 0f2a6bc..6eeaedb 100644
--- a/tools/core-imports.py
+++ b/tools/core-imports.py
@@ -27,8 +27,19 @@ def collect_imports(cores):
def generate(imports):
lines = ['// Generated by tools/core-imports.py; never edit.', '#include ']
+ # The platform layer (my payload SDK fork, include/ps5platform/libc.h) for
+ # what the console's libc lacks or refuses: a title's opendir is refused,
+ # the *at functions resolve to nothing (only libkernel_sys has them), and
+ # arc4random, utimensat, futimens and clock_nanosleep have no export at all.
aliases = {'opendir': 'ps5_opendir', 'readdir': 'ps5_readdir', 'closedir': 'ps5_closedir',
- 'dirfd': 'ps5_dirfd',
+ 'dirfd': 'ps5_dirfd', 'fdopendir': 'ps5_fdopendir',
+ 'openat': 'ps5_openat', 'unlinkat': 'ps5_unlinkat', 'fchmodat': 'ps5_fchmodat',
+ 'fstatat': 'ps5_fstatat', 'mkdirat': 'ps5_mkdirat', 'renameat': 'ps5_renameat',
+ 'utimensat': 'ps5_utimensat', 'futimens': 'ps5_futimens',
+ 'clock_nanosleep': 'ps5_clock_nanosleep',
+ 'arc4random_buf': 'ps5_arc4random_buf',
+ 'arc4random_uniform': 'ps5_arc4random_uniform',
+ 'fstatvfs': 'ps5_fstatvfs',
'localtime_r': 'rtime_localtime', 'rewinddir': 'ps5_rewinddir',
# src/core_threads_ps5.cpp: libkernel cannot attribute a thread
# created from a core's stack, so a factory thread creates it.
@@ -46,10 +57,10 @@ def generate(imports):
'posix_memalign': 'ps5_core_posix_memalign', 'strdup': 'ps5_core_strdup',
'_Znwm': 'ps5_core_new', '_Znam': 'ps5_core_new',
'_ZnwmRKSt9nothrow_t': 'ps5_core_new_nothrow',
- # src/libc_shims.c: the SDK resolves this import to nothing.
+ # The platform layer: the SDK resolves this import to nothing.
'arc4random': 'ps5_arc4random',
- # src/libc_shims.c: the console libc's statvfs faults (it rests
- # on statfs, which only libkernel_sys carries).
+ # The platform layer: the console libc's statvfs faults (it
+ # rests on statfs, which only libkernel_sys carries).
'statvfs': 'ps5_statvfs'}
for index, (name, kind) in enumerate(sorted(imports.items())):
target = aliases.get(name, name)
diff --git a/tools/core-stamp.sh b/tools/core-stamp.sh
index f7ecb68..c383789 100644
--- a/tools/core-stamp.sh
+++ b/tools/core-stamp.sh
@@ -8,8 +8,9 @@
# cores). The stamp is a hash of everything the core's build reads that this
# repository controls: its build script (which pins the revision and the archive
# digests), its patches and port tooling, the shared native loader files, the
-# ABI checker and the SDK compiler wrappers. When the stamp matches and every
-# staged output is present, the build is skipped. PS5_FORCE_CORES=1 builds anyway.
+# ABI checker, the SDK compiler wrappers and the SDK fork's revision. When the
+# stamp matches and every staged output is present, the build is skipped.
+# PS5_FORCE_CORES=1 builds anyway.
core_stamp_dir="$root/build/cores/stamps"
@@ -38,7 +39,8 @@ core_stamp_skip() {
"$root/tooling/native/ps5-core.ld" "$root/tooling/native/core_cxx_runtime.cpp"
"$root/tools/check-core.py" "$root/tools/core-stamp.sh" "$root/tooling/prospero-clang18"
"$root/.deps/native/ps5-payload-sdk/bin/prospero-clang"
- "$root/.deps/native/ps5-payload-sdk/bin/prospero-clang++")
+ "$root/.deps/native/ps5-payload-sdk/bin/prospero-clang++"
+ "$root/.deps/native/ps5-payload-sdk/.ps5-sdk-revision")
core_stamp_value=$(core_stamp_compute "${inputs[@]}")
core_stamp_name=$name
[[ ${PS5_FORCE_CORES:-0} == 1 ]] && return 0
diff --git a/tools/run_clang_tidy.sh b/tools/run_clang_tidy.sh
index c2d5d05..4b6f90a 100644
--- a/tools/run_clang_tidy.sh
+++ b/tools/run_clang_tidy.sh
@@ -23,12 +23,33 @@ mapfile -d '' host_sources < <(find "$root/tooling/native" -maxdepth 1 \
"$tidy" "${host_sources[@]}" --quiet --warnings-as-errors='*' -- \
-std=c++20 -I"$zlib"
-gtest=$(bash "$root/tools/setup-test-dependencies.sh")
-mapfile -d '' test_sources < <(find "$root/tests" -maxdepth 1 -type f -name '*.cpp' -print0)
-if (( ${#test_sources[@]} )); then
- "$tidy" "${test_sources[@]}" --quiet --warnings-as-errors='*' -- \
- -std=c++20 -I"$root/src" -isystem "$gtest/googletest/include"
-fi
+# Each C++ test with the flags its tests/test_*.py builds it with: they include
+# the sources they test, and those include RetroArch's headers or the platform
+# layer's. input_ps5_test.cpp includes a header its test extracts from
+# RetroArch's source at run time, so it has no standalone build to check.
+platform_include=$(mktemp -d)
+trap 'rm -rf -- "$platform_include"' EXIT
+ln -s "$sdk/target/include/ps5platform" "$platform_include/ps5platform"
+vendor="-I$root/vendor/retroarch -I$root/vendor/retroarch/libretro-common/include"
+declare -A test_flags=(
+ [audio_ps5_test.cpp]="-std=c++17 $vendor"
+ [frontend_ps5_test.cpp]="-std=c++17 $vendor -I$platform_include"
+ [memory_diagnostics_test.cpp]="-std=c++20 -I$root -DPS5_MEMORY_DIAGNOSTICS"
+ [memory_ps5_test.cpp]="-std=c++17 -I$root"
+ [memory_vulkan_test.cpp]="-std=c++20 -I$root -I$root/vendor/retroarch -DPS5_MEMORY_DIAGNOSTICS"
+ [menu_memory_test.cpp]="-std=c++17 -I$root"
+ [input_ps5_test.cpp]="skip"
+)
+for source in "$root"/tests/*.cpp; do
+ name=$(basename -- "$source")
+ [[ -n ${test_flags[$name]:-} ]] || {
+ echo "tests/$name has no tidy flags in tools/run_clang_tidy.sh" >&2
+ exit 2
+ }
+ [[ ${test_flags[$name]} == skip ]] && continue
+ read -r -a flags <<< "${test_flags[$name]}"
+ "$tidy" "$source" --quiet --warnings-as-errors='*' -- "${flags[@]}"
+done
mapfile -d '' app_c_sources < <(find "$root/src" -type f -name '*.c' -print0)
if (( ${#app_c_sources[@]} )); then
diff --git a/tools/setup-native-dependencies.sh b/tools/setup-native-dependencies.sh
index a20263b..d9052f4 100644
--- a/tools/setup-native-dependencies.sh
+++ b/tools/setup-native-dependencies.sh
@@ -3,7 +3,8 @@
# Copyright (C) 2026 BlackBearReloaded
# SPDX-License-Identifier: GPL-3.0-or-later
#
-# Fetches the public PS5 payload SDK and static zlib into the ignored cache.
+# Installs the PS5 payload SDK (my fork, pinned below) and static zlib into the
+# ignored cache.
set -euo pipefail
@@ -16,8 +17,13 @@ zlib_version="1.3.2"
zlib_source="$zlib_directory/zlib-$zlib_version"
zlib_archive="$zlib_directory/zlib-$zlib_version.tar.gz"
zlib_stamp="$zlib_root/.source-version"
-sdk_url="https://github.com/ps5-payload-dev/sdk/releases/download/v0.42/ps5-payload-sdk.zip"
-sdk_hash="8cfbc7cd5811e719eb4f0c47eea668d3dc7b40bc8ab11c4a5031d40c23ec02da"
+# The payload SDK is my fork, ../PS5_PayloadSDK: the upstream v0.42 release with
+# the PS5 platform layer (libps5platform.a, include/ps5platform) and the
+# console's machine-context layout installed over it (platform/README.md
+# there). The pinned revision is exported with git archive, so a build never
+# depends on the fork's working tree, and the SDK directory records it.
+sdk_fork="${PS5_PAYLOAD_SDK_FORK:-$root/../PS5_PayloadSDK}"
+sdk_revision=a5bd474a50e5eb50e4d5bfef86e76c8ffc1d81ed
zlib_url="https://zlib.net/fossils/zlib-$zlib_version.tar.gz"
zlib_hash="bb329a0a2cd0274d05519d61c667c062e06990d72e125ee2dfa8de64f0119d16"
skip_sdk=false
@@ -29,7 +35,7 @@ elif [[ $# -ne 0 ]]; then
exit 2
fi
-for command in wget unzip sha256sum tar make; do
+for command in git wget unzip sha256sum tar make; do
command -v "$command" >/dev/null || {
echo "missing required command: $command" >&2
exit 2
@@ -44,13 +50,15 @@ ranlib=$(command -v llvm-ranlib-18 || command -v llvm-ranlib || command -v ranli
}
mkdir -p "$cache"
-if ! $skip_sdk && [[ ! -x "$sdk/bin/prospero-lld" ]]; then
- archive="$cache/ps5-payload-sdk.zip"
- temporary="$archive.download"
- wget -q "$sdk_url" -O "$temporary"
- printf '%s %s\n' "$sdk_hash" "$temporary" | sha256sum --check --strict
- mv "$temporary" "$archive"
- unzip -q -o "$archive" -d "$cache"
+if ! $skip_sdk && [[ ! -f $sdk/.ps5-sdk-revision || $(<"$sdk/.ps5-sdk-revision") != "$sdk_revision" ]]; then
+ git -C "$sdk_fork" cat-file -e "$sdk_revision^{commit}" 2>/dev/null || {
+ echo "the payload SDK fork at $sdk_fork does not have $sdk_revision" >&2
+ exit 2
+ }
+ sdk_tree=$(mktemp -d)
+ git -C "$sdk_fork" archive "$sdk_revision" | tar -x -C "$sdk_tree"
+ bash "$sdk_tree/platform/tools/setup-sdk.sh" "$sdk" "$sdk_revision" "$cache" >&2
+ rm -rf -- "$sdk_tree"
fi
if ! $skip_sdk && [[ ! -x "$sdk/bin/prospero-lld" || ! -d "$sdk/target/include" ]]; then
echo "the pinned PS5 payload SDK is incomplete" >&2