Files
LibretroAdmin a4d4ef8c2c macOS on Apple silicon: the ARM64 recompilers (SH4, ARM7, DSP)
The arm64 macOS build (platform=osx arch=arm) used the interpreters.
It now has the same recompilers as arm64 Linux, made to follow Apple's
rules for code written at run time:

- The code goes in memory mapped with MAP_JIT (vmem_platform_alloc_jit()),
  once for the SH4, once for the ARM7 and once for the DSP, at start-up,
  and kept for every game after that. MAP_JIT cannot be given an address
  (no MAP_FIXED), so the static arrays in the library's text are not
  used there (and are not built in: no 17 MB of zeroes in the dylib).
  The memory is asked for right after the core's own __TEXT, then below
  it, so that the core's functions are within one branch's reach
  (128 MB); where the system puts it elsewhere, it is taken anyway.
- Such memory is writable or executable per thread
  (pthread_jit_write_protect_np()). Everything that writes code -
  compiling a block, relinking one, the main loop, the fast-memory
  rewrite in the fault handler, the ARM7 and DSP compilers, clearing the
  cache - is wrapped in vmem_jit_write_begin()/end(), a per-thread count,
  so nested writers are fine and other threads go on running code. No
  lock. On every other platform the two are empty.
- Calls and branches from the code to the core: when the target is out
  of reach, through a register (x16/x17, vixl's scratch registers). The
  three block-linking routines find their caller by the address after
  the call, which has to be inside the block as linked: for those a
  block calls a trampoline beside the main loop instead (one bl, as
  today). Where the code is in reach - always, on every other platform -
  exactly the same instructions are emitted as before.
- ngen_arm64.S is ELF assembly: for macOS its routines are in
  rec_arm64.cpp in Apple's syntax (underscored names, .private_extern,
  .alt_entry where one routine runs into the next), and the ARM7's
  dispatcher in arm64.cpp is written for both assemblers.
- No JIT memory (a hardened frontend without the
  com.apple.security.cs.allow-jit entitlement): the SH4 and ARM7
  interpreters are used, and the DSP is left out (no effects), instead
  of a crash.
- PAGE_SIZE is 16 KB on Apple's ARM systems: with 4 KB, mprotect() of the
  page protections (textures, the SH4's code pages) was refused there, a
  die() - that was already so for the interpreter build, and for iOS and
  tvOS, which keep the interpreters.

Linux and Android arm64 are unchanged in what they emit; iOS and tvOS
stay on the interpreters (HOST_APPLE_JIT is for a macOS target only),
and macOS x86-64 keeps its x64 recompiler as before. No save-state
change.

Verified here (Linux, no Mac):
- full cross-build and link of parallel_dc_libretro.dylib for
  arm64-apple-macos (clang 18, MacOSX 13.3 SDK, ld64.lld): links with
  every symbol resolved against libSystem, libc++ and OpenGL; the
  assembly routines come out in order and the fall-through symbols are
  marked alt_entry
- full cross-build and link for x86_64-apple-macos (x64 recompiler)
- the changed files compile for arm64 iOS and tvOS targets, with
  HOST_APPLE_JIT off there
- Linux aarch64 (platform=arm64) with the same recompilers:
  tools/threads/headless.sh passes under qemu-aarch64
- the same, with every call and branch to the core forced through a
  register and the link trampolines on (the code macOS runs when its JIT
  memory lands out of reach): headless.sh passes too

Not verified: anything on a real Mac - MAP_JIT, the per-thread write
protection, the placement near the core's code, the fault handler and
the fallback without the entitlement. Unverified until GitLab CI builds
it and someone runs it on Apple silicon.
2026-10-23 10:32:12 +02:00

443 lines
9.0 KiB
C++

#pragma once
#include "types.h"
#include <stdlib.h>
#include <vector>
#include <string.h>
#include <rthreads/rthreads.h>
#ifndef TARGET_NO_THREADS
#include <retro_atomic.h>
#include <rthreads/retro_eventcount.h>
#endif
#ifdef _ANDROID
#include <sys/mman.h>
#endif
/* The core's page: what it protects memory by and divides it into. A
* multiple of the system's, so it may be larger than the system's own.
* Android's NDK r28 on no longer defines PAGE_SIZE on 64-bit processors,
* whose devices may have 16 KB pages: 16 KB there, on 4 KB devices too.
* Apple's ARM systems have 16 KB pages, and mprotect() refuses an address
* that is not at the start of one; their headers may define PAGE_SIZE as
* a variable, which is not what is wanted here. */
#if defined(__APPLE__) && defined(__aarch64__)
#undef PAGE_SIZE
#endif
#ifndef PAGE_SIZE
#if defined(_ANDROID) && (defined(__aarch64__) || defined(__x86_64__))
#define PAGE_SIZE 16384
#elif defined(__APPLE__) && defined(__aarch64__)
#define PAGE_SIZE 16384
#else
#define PAGE_SIZE 4096
#endif
#endif
#undef PAGE_MASK
#define PAGE_MASK (PAGE_SIZE-1)
//Commonly used classes across the project
//Simple Array class for helping me out ;P
template<class T>
class Array
{
public:
T* data;
u32 Size;
Array(T* Source,u32 ellements)
{
//initialise array
data=Source;
Size=ellements;
}
Array(u32 ellements)
{
//initialise array
data=0;
Resize(ellements,false);
Size=ellements;
}
Array(u32 ellements,bool zero)
{
//initialise array
data=0;
Resize(ellements,zero);
Size=ellements;
}
Array()
{
//initialise array
data=0;
Size=0;
}
~Array()
{
if (data)
{
#ifdef MEM_ALLOC_TRACE
printf("WARNING : DESTRUCTOR WITH NON FREED ARRAY [arrayid:%d]\n",id);
#endif
Free();
}
}
void SetPtr(T* Source,u32 ellements)
{
//initialise array
Free();
data=Source;
Size=ellements;
}
T* Resize(u32 size,bool bZero)
{
if (size==0)
{
if (data)
{
#ifdef MEM_ALLOC_TRACE
printf("Freeing data -> resize to zero[Array:%d]\n",id);
#endif
Free();
}
}
if (!data)
data=(T*)malloc(size*sizeof(T));
else
data=(T*)realloc(data,size*sizeof(T));
//TODO : Optimise this
//if we allocated more , Zero it out
if (bZero)
{
if (size>Size)
{
for (u32 i=Size;i<size;i++)
{
u8*p =(u8*)&data[i];
for (size_t j=0;j<sizeof(T);j++)
{
p[j]=0;
}
}
}
}
Size=size;
return data;
}
void Zero()
{
memset(data,0,sizeof(T)*Size);
}
void Free()
{
if (Size != 0)
{
if (data)
free(data);
data = NULL;
}
}
INLINE T& operator [](const u32 i)
{
#ifdef MEM_BOUND_CHECK
if (i>=Size)
{
printf("Error: Array %d , index out of range (%d>%d)\n",id,i,Size-1);
MEM_DO_BREAK;
}
#endif
return data[i];
}
INLINE T& operator [](const s32 i)
{
#ifdef MEM_BOUND_CHECK
if (!(i>=0 && i<(s32)Size))
{
printf("Error: Array %d , index out of range (%d > %d)\n",id,i,Size-1);
MEM_DO_BREAK;
}
#endif
return data[i];
}
};
//Threads
#if !defined(HOST_NO_THREADS)
typedef void* ThreadEntryFP(void* param);
typedef void* THREADHANDLE;
class cThread
{
private:
ThreadEntryFP* Entry;
void* param;
public :
sthread_t *hThread;
cThread(ThreadEntryFP* function,void* param);
~cThread() { WaitToEnd(); }
void Start();
void WaitToEnd();
};
#endif
//Wait Events
#ifndef TARGET_NO_THREADS
/* Where a thread sleeps until another one changes something it is waiting
* for. The waiter checks its own condition between Prepare() and Commit();
* whoever changes the condition calls Notify() afterwards. Notify() with
* nobody asleep is two atomic operations: no lock, no system call. */
class cEventCount
{
retro_eventcount_t ec;
public :
cEventCount()
{
if (!retro_eventcount_init(&ec))
abort();
}
~cEventCount() { retro_eventcount_free(&ec); }
void Notify() { retro_eventcount_notify(&ec); }
int Prepare() { return retro_eventcount_prepare_wait(&ec); }
void Cancel() { retro_eventcount_cancel_wait(&ec); }
void Commit(int key) { retro_eventcount_commit_wait(&ec, key); }
//Returns false if the bound expired
bool Commit(int key, int64_t timeout_us)
{
return retro_eventcount_commit_wait_timeout(&ec, key, timeout_us);
}
};
#endif
/* Auto-reset event. Set() is one store, plus a wake when a thread is
* asleep in Wait(); nothing here takes a lock. */
class cResetEvent
{
#ifndef TARGET_NO_THREADS
cEventCount ec;
retro_atomic_int_t state;
bool Consume()
{
return retro_atomic_load_acquire_int(&state)
&& retro_atomic_exchange_int(&state, 0);
}
#else
bool state;
#endif
public :
cResetEvent()
{
#ifndef TARGET_NO_THREADS
retro_atomic_int_init(&state, 0);
#else
state = false;
#endif
}
//Set state to signaled
void Set()
{
#ifndef TARGET_NO_THREADS
retro_atomic_store_release_int(&state, 1);
ec.Notify();
#else
state = true;
#endif
}
//Set state to non signaled
void Reset()
{
#ifndef TARGET_NO_THREADS
retro_atomic_store_release_int(&state, 0);
#else
state = false;
#endif
}
//Wait for signal, then reset. Returns false if timeout expired, true otherwise
bool Wait(u32 msec)
{
#ifndef TARGET_NO_THREADS
/* A wake that finds the event unset is a stray one; wait again,
* a few times at most so the call stays bounded. */
for (int lap = 0; ; lap++)
{
int key;
if (Consume())
return true;
key = ec.Prepare();
if (Consume())
{
ec.Cancel();
return true;
}
if (!ec.Commit(key, (int64_t)msec * 1000) || lap == 3)
return Consume();
}
#else
bool ret = state;
state = false;
return ret;
#endif
}
//Wait for signal, then reset
void Wait()
{
#ifndef TARGET_NO_THREADS
for (;;)
{
int key;
if (Consume())
return;
key = ec.Prepare();
if (Consume())
{
ec.Cancel();
return;
}
ec.Commit(key);
}
#else
state = false;
#endif
}
};
//Set the path !
void set_user_config_dir(const std::string& dir);
//subpath format: /data/fsca-table.bit
std::string get_writable_data_path(const std::string& filename);
std::string get_writable_vmu_path(const char *logical_port);
bool mem_region_lock(void *start, std::size_t len);
bool mem_region_unlock(void *start, std::size_t len);
bool mem_region_noaccess(void *start, std::size_t len);
bool mem_region_set_exec(void *start, std::size_t len);
void *mem_region_reserve(void *start, std::size_t len);
bool mem_region_release(void *start, std::size_t len);
void *mem_region_map_file(void *file_handle, void *dest, std::size_t len, std::size_t offset, bool readwrite);
bool mem_region_unmap_file(void *start, std::size_t len);
class VArray2
{
public:
u8* data;
u32 size;
//void Init(void* data,u32 sz);
//void Term();
#ifdef TARGET_NO_EXCEPTIONS
void UnLockRegion(u32 offset,u32 size) {}
#else
void UnLockRegion(u32 offset,u32 size);
#endif
void Zero()
{
UnLockRegion(0,size);
memset(data,0,size);
}
INLINE u8& operator [](const u32 i)
{
#ifdef MEM_BOUND_CHECK
if (i>=size)
{
printf("Error: VArray2 , index out of range (%d>%d)\n",i,size-1);
MEM_DO_BREAK;
}
#endif
return data[i];
}
};
int ExeptionHandler(u32 dwCode, void* pExceptionPointers);
int msgboxf(const char* text,unsigned int type,...);
#define MBX_OK 0x00000000L
#define MBX_OKCANCEL 0x00000001L
#define MBX_ABORTRETRYIGNORE 0x00000002L
#define MBX_YESNOCANCEL 0x00000003L
#define MBX_YESNO 0x00000004L
#define MBX_RETRYCANCEL 0x00000005L
#define MBX_ICONHAND 0x00000010L
#define MBX_ICONQUESTION 0x00000020L
#define MBX_ICONEXCLAMATION 0x00000030L
#define MBX_ICONASTERISK 0x00000040L
#define MBX_USERICON 0x00000080L
#define MBX_ICONWARNING MBX_ICONEXCLAMATION
#define MBX_ICONERROR MBX_ICONHAND
#define MBX_ICONINFORMATION MBX_ICONASTERISK
#define MBX_ICONSTOP MBX_ICONHAND
#define MBX_DEFBUTTON1 0x00000000L
#define MBX_DEFBUTTON2 0x00000100L
#define MBX_DEFBUTTON3 0x00000200L
#define MBX_DEFBUTTON4 0x00000300L
#define MBX_APPLMODAL 0x00000000L
#define MBX_SYSTEMMODAL 0x00001000L
#define MBX_TASKMODAL 0x00002000L
#define MBX_HELP 0x00004000L // Help Button
#define MBX_NOFOCUS 0x00008000L
#define MBX_SETFOREGROUND 0x00010000L
#define MBX_DEFAULT_DESKTOP_ONLY 0x00020000L
#define MBX_TOPMOST 0x00040000L
#define MBX_RIGHT 0x00080000L
#define MBX_RTLREADING 0x00100000L
#define MBX_RV_OK 1
#define MBX_RV_CANCEL 2
#define MBX_RV_ABORT 3
#define MBX_RV_RETRY 4
#define MBX_RV_IGNORE 5
#define MBX_RV_YES 6
#define MBX_RV_NO 7
static inline std::string trim_trailing_ws(const std::string& str,
const std::string& whitespace = " ")
{
const auto strEnd = str.find_last_not_of(whitespace);
if (strEnd == std::string::npos)
return "";
return str.substr(0, strEnd + 1);
}