While chasing an unrelated SPURS hang I noticed the JIT publishes freshly written code on ARM64 with no instruction-cache maintenance at all. A grep for clear_cache or flushInstructionCache over the JIT layer comes back empty. The branch-rewrite sites only issue ISB; DSB ISH, which performs no D-cache clean or I-cache invalidation and is ordered backwards for self-modifying code besides. On ARMv8 a correct publication needs the DC CVAU / IC IVAU broadcast sequence; x86 has a coherent instruction cache, so none of this was ever visible there. All sites use the bundled asmjit::VirtMem::flushInstructionCache(), which emits that sequence portably across toolchains. This covers every publication path I could find: - MemoryManager1::finalizeMemory() and MemoryManager2::finalizeMemory() were both no-ops. RuntimeDyld calls finalizeMemory() after writing code and relies on it for cache maintenance, so LLVM emitted PPU and SPU code was never flushed. MemoryManager1 serves the primary PPU JIT, MemoryManager2 the SPU JIT and auxiliary engines. Both managers now record code section allocations and flush them on finalize. I confirmed at runtime that the MemoryManager2 path executes (about 12800 calls per cold boot). - jit_runtime_base::_add() copies asmjit output into executable memory with no flush. - jit_runtime::finalize() restores an executable code snapshot in place during emulator restart with only the ISB/DSB pair. - spu_runtime::rebuild_ubertrampoline() publishes a hand-written trampoline via CAS with no flush; the flush now happens before the publication. - spu_runtime::make_branch_patchpoint() writes a patchpoint byte by byte and returns it with only the ISB/DSB pair. - Both 16-byte branch-site rewrites (dispatch and branch) atomically overwrite live code and only issued the ISB/DSB pair. The ISB/DSB pairs adjacent to the new flushes are removed along with their misleading "flush all cache lines" comments: the flush helper already issues the trailing barriers, and the pairs never performed any cache maintenance in the first place. I want to be upfront that this was not the cause of the hang I was debugging (a same-item compilation race, fixed separately), and I have not observed a failure that this change alone fixes. It is a latent correctness issue on any ARM64 host: nothing prevents another core from fetching stale instruction bytes for freshly published code.
ARMSX3
Proof of concept Android port of RPCS3.
Uses the latest RPCS3 upstream code (the recent ARM64 improvements included).
Status
From my testing, I only tried Skate 3. It boots, loads and reaches gameplay at roughly 20 to 30 fps on a Snapdragon 8 Gen 2. Rendering, audio, touch controls and physical controllers work. Almost nothing else has been tested. So the main stop gap at the moment is performance/speed.
Differences from upstream RPCS3
Some of the fixes here are not in upstream and affect any ARM64 build, not only Android:
-
Shaders declared runtime sized arrays inside uniform blocks, which requires VK_EXT_shader_uniform_buffer_unsized_array. Adreno does not support that extension, so every game pipeline failed to compile and nothing rendered. Concrete array bounds are emitted when the extension is missing.
-
The ARM64 SPU block verification checksum folded two thirds of every block through an absolute difference. That collides on the near identical job binaries an SPU job manager streams through the same local store address, so a cached block could end up running against another job's code. It sums now.
-
Thread affinity was compiled out on Android, and the core had no ARM big.LITTLE topology, so SPU and RSX threads were never placed on the fast cores.
-
The LLVM JIT target was pinned to cortex-a34, an in order core from 2016. It detects the host now.
Building
Only arm64-v8a is supported. You need the Android SDK with NDK r27 or newer, CMake 3.30 or newer, and a JDK 17. Android Studio ships all of these.
Clone with submodules, then fetch the two third party checkouts that are not submodules:
git clone --recursive https://github.com/ARMSX2/ARMSX3.git
cd ARMSX3
git clone https://github.com/SnowflakePowered/librashader 3rdparty/librashader
git clone https://github.com/bylaws/libadrenotools android/armsx3-ui/app/src/main/cpp/libadrenotools
Build the core. This is the long part and produces an unstripped library of around 1.3 GB:
export ANDROID_HOME=$HOME/Library/Android/sdk
cmake -B build-android -G Ninja \
-DCMAKE_TOOLCHAIN_FILE=$ANDROID_HOME/ndk/<version>/build/cmake/android.toolchain.cmake \
-DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM=android-31 \
-DCMAKE_BUILD_TYPE=RelWithDebInfo
cmake --build build-android --target rpcsx-android -j8
Strip it and put it where the app expects it:
llvm-strip --strip-unneeded build-android/android/libarmsx3-core.so
cp build-android/android/libarmsx3-core.so \
android/armsx3-ui/app/src/main/jniLibs/arm64-v8a/
Then build the app:
cd android/armsx3-ui
export JAVA_HOME="/Applications/Android Studio.app/Contents/jbr/Contents/Home"
./gradlew :app:assembleRelease
The apk lands in app/build/outputs/apk/release/.
Note that the core library has to be rebuilt and copied again whenever anything under rpcs3/ or android/src/ changes. Gradle does not build it for you.
The Discord Social SDK is proprietary and is not redistributed here. Get it from Discord's developer portal and drop it in app/libs/ and app/src/main/cpp/discord_sdk/ if you want that feature. The build skips it otherwise.
Running it needs PS3 firmware, which is not included. License
GPL-2.0-only, the same as RPCS3. See LICENSE. Some files may be licensed differently, check the file headers.
Based on RPCS3, https://github.com/RPCS3/rpcs3