From b1702f3a209817190a8aec42717b7c62a300b177 Mon Sep 17 00:00:00 2001 From: Xnick417x Date: Tue, 7 Jul 2026 02:31:51 +0000 Subject: [PATCH 1/2] Fix gamepad input drops in the fake-input ring - Publish ring events and state snapshots with a store-store fence so the guest-side reader can no longer observe the sequence word before the payload it publishes; unfenced stores could surface as rare random stale or lost inputs under load. - Grow the per-slot event ring 512 -> 4096 events so a stalled reader loses history far later; a press/release pair inside the overwritten window was silently dropped. - Restage libfakeinput.so into the imagefs on every launch so the guest reader can never run against a newer ring layout than it understands. --- app/src/main/cpp/CMakeLists.txt | 1 + app/src/main/cpp/winlator/fakeinput.cpp | 2 +- app/src/main/cpp/winlator/ring_fence.c | 13 ++++++++++ .../GuestProgramLauncherComponent.java | 26 +++++++++---------- .../input/controls/FakeInputWriter.java | 18 ++++++++++--- 5 files changed, 42 insertions(+), 18 deletions(-) create mode 100644 app/src/main/cpp/winlator/ring_fence.c diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index 001fd003d..4f006c89e 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -138,6 +138,7 @@ add_library(winlator SHARED winlator/drawable.c winlator/native_content_io.cpp winlator/gpu_image.c + winlator/ring_fence.c winlator/sync_fence.c winlator/sysvshared_memory.c winlator/xconnector_epoll.c diff --git a/app/src/main/cpp/winlator/fakeinput.cpp b/app/src/main/cpp/winlator/fakeinput.cpp index 5f474c2e8..855d5c76e 100644 --- a/app/src/main/cpp/winlator/fakeinput.cpp +++ b/app/src/main/cpp/winlator/fakeinput.cpp @@ -49,7 +49,7 @@ static constexpr uint8_t GAMEPAD_BUTTON_COUNT = 11; static constexpr uint32_t FAKE_INPUT_RING_MAGIC = 0x46494252; static constexpr uint32_t FAKE_INPUT_RING_VERSION = 2; static constexpr uint32_t FAKE_INPUT_EVENT_SIZE = sizeof(struct input_event); -static constexpr uint32_t FAKE_INPUT_RING_CAPACITY = 512; +static constexpr uint32_t FAKE_INPUT_RING_CAPACITY = 4096; static constexpr unsigned int FAKE_INPUT_MAJOR = 13; static constexpr unsigned int FAKE_INPUT_EVENT_MINOR_BASE = 64; static constexpr unsigned int FAKE_INPUT_JS_MINOR_BASE = 0; diff --git a/app/src/main/cpp/winlator/ring_fence.c b/app/src/main/cpp/winlator/ring_fence.c new file mode 100644 index 000000000..634f3a0a7 --- /dev/null +++ b/app/src/main/cpp/winlator/ring_fence.c @@ -0,0 +1,13 @@ +#include +#include + +// Store-store barrier (dmb ish on arm64) so ring event/snapshot payload stores +// are visible to the guest-side reader before the sequence word that publishes +// them. Pairs with the __ATOMIC_ACQUIRE loads in fakeinput.cpp. +JNIEXPORT void JNICALL +Java_com_winlator_cmod_runtime_input_controls_FakeInputWriter_nativeStoreFence( + JNIEnv *env, jclass clazz) { + (void)env; + (void)clazz; + atomic_thread_fence(memory_order_release); +} diff --git a/app/src/main/runtime/display/environment/components/GuestProgramLauncherComponent.java b/app/src/main/runtime/display/environment/components/GuestProgramLauncherComponent.java index 58ac5f6ae..4ee3a33e6 100644 --- a/app/src/main/runtime/display/environment/components/GuestProgramLauncherComponent.java +++ b/app/src/main/runtime/display/environment/components/GuestProgramLauncherComponent.java @@ -1007,20 +1007,20 @@ private int execGuestProgram() { Log.d("GuestLauncher", "nativeLibDir: " + nativeLibDir); Log.d("GuestLauncher", "fakeinputSrc exists: " + fakeinputSrc.exists()); Log.d("GuestLauncher", "fakeinputDest: " + fakeinputDest.getAbsolutePath()); - if (!fakeinputDest.exists()) { - try { - if (fakeinputSrc.exists()) { - FileUtils.copy(fakeinputSrc, fakeinputDest); - Log.d("GuestLauncher", "Copied libfakeinput.so to imagefs"); - } else { - Log.e( - "GuestLauncher", - "libfakeinput.so NOT FOUND in APK: " + fakeinputSrc.getAbsolutePath()); - } - } catch (Exception e) { - Log.e("GuestLauncher", "Failed to copy libfakeinput.so: " + e.getMessage()); - e.printStackTrace(); + try { + if (fakeinputSrc.exists()) { + // Refresh every launch so the guest reader can never skew from the + // app-side ring writer (a same-size rebuild defeats length checks). + FileUtils.copy(fakeinputSrc, fakeinputDest); + Log.d("GuestLauncher", "Copied libfakeinput.so to imagefs"); + } else if (!fakeinputDest.exists()) { + Log.e( + "GuestLauncher", + "libfakeinput.so NOT FOUND in APK: " + fakeinputSrc.getAbsolutePath()); } + } catch (Exception e) { + Log.e("GuestLauncher", "Failed to copy libfakeinput.so: " + e.getMessage()); + e.printStackTrace(); } Log.d("GuestLauncher", "fakeinputDest exists after copy: " + fakeinputDest.exists()); diff --git a/app/src/main/runtime/input/controls/FakeInputWriter.java b/app/src/main/runtime/input/controls/FakeInputWriter.java index 721b88632..c24b81424 100644 --- a/app/src/main/runtime/input/controls/FakeInputWriter.java +++ b/app/src/main/runtime/input/controls/FakeInputWriter.java @@ -21,7 +21,7 @@ public class FakeInputWriter { private static final int BUFFER_SIZE = 768; private static final int EVENT_SIZE = 24; private static final int MAX_FAKE_INPUT_SLOTS = 4; - private static final int RING_CAPACITY_EVENTS = 512; + private static final int RING_CAPACITY_EVENTS = 4096; private static final int RING_HEADER_SIZE = 64; private static final int RING_SIZE = RING_HEADER_SIZE + (RING_CAPACITY_EVENTS * EVENT_SIZE); private static final int RING_MAGIC = 0x46494252; // FIBR @@ -49,6 +49,12 @@ public class FakeInputWriter { private static final String TAG = "FakeInputWriter"; private static final Object RING_LOCK = new Object(); private static final RingSlot[] RING_SLOTS = new RingSlot[MAX_FAKE_INPUT_SLOTS]; + + static { + System.loadLibrary("winlator"); + } + + private static native void nativeStoreFence(); private final File eventFile; private final int slot; private int prevHatX; @@ -363,15 +369,15 @@ private boolean flushBufferToRing() { // Publish the resulting absolute state. prev* now hold the post-update // values, i.e. exactly the state the events just written transition to. writeSnapshotLocked(ring); + nativeStoreFence(); ring.putLong(RING_WRITE_SEQ_OFFSET, writeSeq); } return true; } // Publishes the full absolute controller state for the native reader to replay - // as a keyframe. seqlock: bump to odd, write fields, bump to even. Mirrors the - // write_seq publication model (plain mapped-buffer stores); the reader retries - // on a torn read. + // as a keyframe. seqlock: bump to odd, write fields, bump to even, with + // store-store fences so the reader's acquire loads see consistent payloads. private void writeSnapshotLocked(ByteBuffer ring) { int buttons = 0; for (int i = 0; i < BUTTON_MAP.length; i++) { @@ -381,6 +387,7 @@ private void writeSnapshotLocked(ByteBuffer ring) { } long seq = ring.getLong(RING_SNAPSHOT_SEQ_OFFSET); ring.putLong(RING_SNAPSHOT_SEQ_OFFSET, seq + 1); // odd: write in progress + nativeStoreFence(); ring.putInt(RING_SNAPSHOT_BUTTONS_OFFSET, buttons); // Axis order must match the native kSnapshotAxisCodes: // X, Y, RX, RY, GAS(=triggerR), BRAKE(=triggerL), HAT0X, HAT0Y. @@ -392,6 +399,7 @@ private void writeSnapshotLocked(ByteBuffer ring) { ring.putShort(RING_SNAPSHOT_AXES_OFFSET + 10, clampShort(this.prevTriggerL)); ring.putShort(RING_SNAPSHOT_AXES_OFFSET + 12, clampShort(this.prevHatX)); ring.putShort(RING_SNAPSHOT_AXES_OFFSET + 14, clampShort(this.prevHatY)); + nativeStoreFence(); ring.putLong(RING_SNAPSHOT_SEQ_OFFSET, seq + 2); // even: write complete } @@ -408,10 +416,12 @@ private static short clampShort(int value) { private static void clearSnapshotLocked(ByteBuffer ring) { long seq = ring.getLong(RING_SNAPSHOT_SEQ_OFFSET); ring.putLong(RING_SNAPSHOT_SEQ_OFFSET, seq + 1); + nativeStoreFence(); ring.putInt(RING_SNAPSHOT_BUTTONS_OFFSET, 0); for (int i = 0; i < 8; i++) { ring.putShort(RING_SNAPSHOT_AXES_OFFSET + (i * 2), (short) 0); } + nativeStoreFence(); ring.putLong(RING_SNAPSHOT_SEQ_OFFSET, seq + 2); } From 981a69e8fed4644a987b70a4dda1fe1ba3e23ac5 Mon Sep 17 00:00:00 2001 From: Xnick417x Date: Tue, 7 Jul 2026 02:58:03 +0000 Subject: [PATCH 2/2] Release held gamepad inputs when the in-session menu opens While the menu drawer is open it consumes all controller events for navigation, so a release performed there never reaches the input service: the guest keeps the input applied while the menu is open, and the delta-encoded writer then swallows the next press of that same button because its tracked state never saw the release. Zero every connected controller's tracked state and push it once on drawer open so the guest releases everything and post-close presses register again. --- .../display/XServerDisplayActivity.java | 3 ++ .../display/winhandler/WinHandler.java | 29 +++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/app/src/main/runtime/display/XServerDisplayActivity.java b/app/src/main/runtime/display/XServerDisplayActivity.java index cf4a7d7f4..2b27c94e3 100644 --- a/app/src/main/runtime/display/XServerDisplayActivity.java +++ b/app/src/main/runtime/display/XServerDisplayActivity.java @@ -4869,6 +4869,9 @@ public void onDrawerSlide() { @Override public void onDrawerOpened() { releasePointerCapture(); + if (winHandler != null) { + winHandler.neutralizeControllers(); + } renderDrawerMenu(); if (drawerStateHolder != null) { drawerStateHolder.resetMenuNav(); diff --git a/app/src/main/runtime/display/winhandler/WinHandler.java b/app/src/main/runtime/display/winhandler/WinHandler.java index 48711cd73..31962af81 100644 --- a/app/src/main/runtime/display/winhandler/WinHandler.java +++ b/app/src/main/runtime/display/winhandler/WinHandler.java @@ -731,6 +731,35 @@ public void sendGamepadState(ExternalController controller) { if (xServer != null && xServer.getRenderer() != null) xServer.getRenderer().requestRenderCoalesced(); } + // Menu owns the controller while open; zero tracked state and push it once so nothing stays held in the guest. + public void neutralizeControllers() { + for (ExternalController controller : this.controllers.values()) { + if (controller == null) { + continue; + } + clearGamepadState(controller.state); + clearGamepadState(controller.remappedState); + int slot = assignSlot(controller.getDeviceId()); + if (slot >= 0 && this.writers[slot] != null) { + try { + this.writers[slot].writeGamepadState(controller.state); + } catch (IOException ignored) { + } + } + } + } + + private static void clearGamepadState(GamepadState state) { + state.thumbLX = 0; + state.thumbLY = 0; + state.thumbRX = 0; + state.thumbRY = 0; + state.triggerL = 0; + state.triggerR = 0; + state.buttons = 0; + state.dpad[0] = state.dpad[1] = state.dpad[2] = state.dpad[3] = false; + } + private void writeControllerGamepadState( ExternalController controller, boolean applyGyroOverlay) { ExternalController profileController;