From ee5f480fc9135300fbc9d3f59badfcce5e6a2f7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 15 Oct 2026 03:57:37 +0200 Subject: [PATCH] Mac ARM64: Switch JIT memory between writable and executable per thread JIT memory is now mapped with MAP_JIT, and pthread_jit_write_protect_np() switches the calling thread's view of it, in place of two mprotect() calls for every block compiled and every exit linked. This makes compiling cheaper, which matters mostly in the first seconds after a game or state loads. A hardened runtime without the JIT entitlement refuses the mapping, and then it's mprotect() as before. Co-Authored-By: Claude Opus 5.5 (1M context) --- Common/CodeBlock.h | 4 +-- Common/MemoryUtil.cpp | 67 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/Common/CodeBlock.h b/Common/CodeBlock.h index 04642f16b4..853c15c149 100644 --- a/Common/CodeBlock.h +++ b/Common/CodeBlock.h @@ -99,8 +99,8 @@ public: // If not WX Exclusive, no need to call ProtectMemoryPages because we never change the protection from RWX. PoisonMemory(offset); ResetCodePtr(offset); - if (PlatformIsWXExclusive() && offset != 0) { - // Need to re-protect the part we didn't clear. + if (PlatformIsWXExclusive()) { + // Need to re-protect the part we didn't clear (with MAP_JIT, this thread's view of it all). ProtectMemoryPages(region, offset, MEM_PROT_READ | MEM_PROT_EXEC); } } diff --git a/Common/MemoryUtil.cpp b/Common/MemoryUtil.cpp index 7817c37db3..335a280b67 100644 --- a/Common/MemoryUtil.cpp +++ b/Common/MemoryUtil.cpp @@ -41,6 +41,14 @@ #include #endif +#if PPSSPP_PLATFORM(MAC) && PPSSPP_ARCH(ARM64) +#include +#include +#include +#include +#define USE_MAP_JIT +#endif + #ifndef _WIN32 #include #endif @@ -183,7 +191,43 @@ void *AllocateExecutableMemory(size_t size) { #else // Non-Windows platforms +#ifdef USE_MAP_JIT +// With MAP_JIT, a thread switches all of its view of the JIT memory between writable and executable +// with pthread_jit_write_protect_np(), far cheaper than an mprotect() for every block compiled and +// linked. A hardened runtime without the JIT entitlement refuses the mapping: then mprotect() it is. +static std::mutex g_mapJitLock; +static int g_mapJitState; // 0 untried, 1 in use, -1 refused. +static std::vector> g_mapJitRanges; + +static bool IsMapJitMemory(const void *ptr) { + std::lock_guard guard(g_mapJitLock); + for (const auto &range : g_mapJitRanges) { + if ((uintptr_t)ptr >= range.first && (uintptr_t)ptr < range.second) + return true; + } + return false; +} +#endif + void *AllocateExecutableMemory(size_t size) { +#ifdef USE_MAP_JIT + { + std::lock_guard guard(g_mapJitLock); + if (g_mapJitState >= 0) { + void *ptr = mmap(nullptr, size, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_ANON | MAP_PRIVATE | MAP_JIT, -1, 0); + if (ptr != MAP_FAILED) { + g_mapJitState = 1; + g_mapJitRanges.emplace_back((uintptr_t)ptr, (uintptr_t)ptr + size); + return ptr; + } + if (g_mapJitState == 0) { + WARN_LOG(Log::MemMap, "MAP_JIT refused (errno=%d), using mprotect", errno); + g_mapJitState = -1; + } + } + } +#endif + static char *map_hint = nullptr; #if PPSSPP_ARCH(AMD64) @@ -339,6 +383,19 @@ void FreeMemoryPages(void *ptr, size_t size) { } void FreeExecutableMemory(void *ptr, size_t size) { +#ifdef USE_MAP_JIT + if (IsMapJitMemory(ptr)) { + std::lock_guard guard(g_mapJitLock); + for (size_t i = 0; i < g_mapJitRanges.size(); i++) { + if (g_mapJitRanges[i].first == (uintptr_t)ptr) { + g_mapJitRanges.erase(g_mapJitRanges.begin() + i); + break; + } + } + // Back to executable, which the protection change before freeing undid. + pthread_jit_write_protect_np(1); + } +#endif FreeMemoryPages(ptr, size); } @@ -374,6 +431,16 @@ bool ProtectMemoryPages(const void* ptr, size_t size, uint32_t memProtFlags) { _assert_msg_(false, "Bad memory protect flags %d: W^X is in effect, can't both write and exec", memProtFlags); } } +#ifdef USE_MAP_JIT + if (IsMapJitMemory(ptr)) { + _assert_msg_((memProtFlags & (MEM_PROT_WRITE | MEM_PROT_EXEC)) != 0, "MAP_JIT memory is writable or executable"); + pthread_jit_write_protect_np((memProtFlags & MEM_PROT_WRITE) == 0); + return true; + } +#endif + if (size == 0) + return true; + // Note - VirtualProtect will affect the full pages containing the requested range. // mprotect does not seem to, at least not on Android unless I made a mistake somewhere, so we manually round. #ifdef _WIN32