The guest polls cellPad at its own rate -- 33ms at 30fps -- and a press plus its release are two separate snapshot pushes with nothing between them. A press shorter than one poll interval lands entirely between polls and the game never sees it. Steady presses always span a poll, which is why a button works everywhere except during a rapid mash, and why a held button (crouch) keeps working while everything tapped alongside it does not. GestureLayer already knew this and worked around it locally with a 40ms hold in pulse(); ordinary touch taps and physical controller presses had no equivalent. Reported on Iron Man's quick-time event, where circle must be pressed repeatedly and does not register, and again as 'only the crouch button works' with both on-screen controls and a PS5 pad. Delaying a too-short release would NOT fix it and would look identical: in a mash, press N's deferred release collides with press N+1 and the game sees one long press instead of several, while a QTE counts presses. Each transition therefore gets its own slot -- press visible for 40ms, release visible for 40ms, then the next press -- so a mash arrives as distinct presses rather than a hold. A press with nothing queued ahead of it still goes through immediately, so normal input takes no added latency; only the release of a tap shorter than 40ms is deferred, and only up to 40ms. Queue depth is capped so a mash cannot accumulate while the guest is not consuming. 40ms matches the gesture path and clears one 60Hz sample. NEEDS FIGHTING-GAME TESTING: it also paces the d-pad, and 40ms is about 2.4 frames at 60fps, so frame-tight directional input is the case most likely to feel different.
ARMSX3
Uses the latest RPCS3 upstream code (the recent ARM64 improvements included).
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