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/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/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/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; 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); }