From a16df44e5e307be24f978a4ec069625e033f7284 Mon Sep 17 00:00:00 2001 From: Xnick417x Date: Wed, 1 Jul 2026 05:58:59 +0000 Subject: [PATCH 1/3] Fake input: keep last-known-good snapshot instead of a fabricated neutral keyframe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The seqlock snapshot reader gave up after 8 non-yielding spins and returned all-zeros, which capture_keyframe then streamed to the guest as a full neutral controller state — a one-frame input drop under contention (worst with on-screen touch controls). read_snapshot now spins up to 128 attempts with a yield hint on each retry and reports success/failure; on a failed read capture_keyframe keeps the previous keyframe instead of publishing zeros. Reader-side only; ring layout and protocol unchanged. --- app/src/main/cpp/winlator/fakeinput.cpp | 66 +++++++++++++++---------- 1 file changed, 39 insertions(+), 27 deletions(-) diff --git a/app/src/main/cpp/winlator/fakeinput.cpp b/app/src/main/cpp/winlator/fakeinput.cpp index 5f474c2e8..b080d6a69 100644 --- a/app/src/main/cpp/winlator/fakeinput.cpp +++ b/app/src/main/cpp/winlator/fakeinput.cpp @@ -379,31 +379,39 @@ struct SnapshotState { static long long monotonic_ms(); -// Read the authoritative absolute-state snapshot using the writer's seqlock. -// Retries on a torn read (snapshot_seq odd or changed mid-read); after a few -// failed attempts returns the neutral baseline rather than spinning. This -// mirrors the publication model already used for write_seq. -__attribute__((visibility("hidden"))) static SnapshotState -read_snapshot(const FakeInputRingHeader *ring) { - SnapshotState out; - for (int attempt = 0; attempt < 8; attempt++) { +static constexpr int kSnapshotReadAttempts = 128; + +static inline void snapshot_cpu_relax() { +#if defined(__aarch64__) + __asm__ __volatile__("yield" ::: "memory"); +#else + __asm__ __volatile__("" ::: "memory"); +#endif +} + +// Reads the writer's seqlock snapshot, relaxing between torn/in-progress reads. +// Returns false on exhaustion, leaving out untouched (caller keeps last-known-good). +__attribute__((visibility("hidden"))) static bool +read_snapshot(const FakeInputRingHeader *ring, SnapshotState &out) { + for (int attempt = 0; attempt < kSnapshotReadAttempts; attempt++) { uint64_t s1 = __atomic_load_n(&ring->snapshot_seq, __ATOMIC_ACQUIRE); - if (s1 & 1ULL) - continue; // a write is in progress - uint32_t buttons = ring->snapshot_buttons; - int16_t axes[8]; - for (int i = 0; i < 8; i++) - axes[i] = ring->snapshot_axes[i]; - __atomic_thread_fence(__ATOMIC_ACQUIRE); - uint64_t s2 = __atomic_load_n(&ring->snapshot_seq, __ATOMIC_RELAXED); - if (s1 == s2) { - out.buttons = buttons; + if (!(s1 & 1ULL)) { + uint32_t buttons = ring->snapshot_buttons; + int16_t axes[8]; for (int i = 0; i < 8; i++) - out.axes[i] = axes[i]; // sign-extend to int32 for the event value - return out; + axes[i] = ring->snapshot_axes[i]; + __atomic_thread_fence(__ATOMIC_ACQUIRE); + uint64_t s2 = __atomic_load_n(&ring->snapshot_seq, __ATOMIC_RELAXED); + if (s1 == s2) { + out.buttons = buttons; + for (int i = 0; i < 8; i++) + out.axes[i] = axes[i]; // sign-extend to int32 + return true; + } } + snapshot_cpu_relax(); } - return out; + return false; } // Capture the current absolute state into the controller so it can be streamed @@ -412,14 +420,18 @@ read_snapshot(const FakeInputRingHeader *ring) { // frame is never restarted mid-stream. __attribute__((visibility("hidden"))) static void capture_keyframe(FakeController &fake, const char *reason, int fd) { - SnapshotState snap = read_snapshot(fake.ring); - fake.keyframe_buttons = snap.buttons; - for (int i = 0; i < 8; i++) - fake.keyframe_axes[i] = snap.axes[i]; + SnapshotState snap; + // On a failed read keep the previous keyframe rather than publishing a neutral. + bool fresh = read_snapshot(fake.ring, snap); + if (fresh) { + fake.keyframe_buttons = snap.buttons; + for (int i = 0; i < 8; i++) + fake.keyframe_axes[i] = snap.axes[i]; + } fake.keyframe_remaining = kNeutralEventCount; - Logger::log("Fake input keyframe reason=%s fd=%d slot=%d read_seq=%llu " + Logger::log("Fake input keyframe reason=%s fd=%d slot=%d snapshot=%s read_seq=%llu " "write_seq=%llu buttons=0x%03x axes=[%d,%d,%d,%d,%d,%d,%d,%d]\n", - reason ? reason : "unknown", fd, fake.slot, + reason ? reason : "unknown", fd, fake.slot, fresh ? "fresh" : "stale", static_cast(fake.read_seq), static_cast(ring_write_seq(fake.ring)), fake.keyframe_buttons, fake.keyframe_axes[0], From b39848a0e889dacd61b36b14b5900b8413aa39b7 Mon Sep 17 00:00:00 2001 From: Xnick417x Date: Thu, 2 Jul 2026 05:54:59 +0000 Subject: [PATCH 2/3] Fix stuck gamepad stick after an input stutter Touch: recover ScreenTouchStick when its pointer is captured by a control it drifted onto or is no longer down; reconcile from the controls view on every event so a missed or unforwarded UP no longer strands the right stick. Reader: on ring overflow only arm a keyframe from a fresh snapshot. A fresh keyframe now discards the surviving ring window so an older push can't overwrite a newer release, and a failed snapshot read replays the window instead of re-asserting a stale absolute frame. --- app/src/main/cpp/winlator/fakeinput.cpp | 28 +++++++++++-------- .../runtime/input/ui/InputControlsView.java | 1 + .../main/runtime/input/ui/ScreenTouchStick.kt | 17 +++++++++++ app/src/main/runtime/input/ui/TouchpadView.kt | 4 +++ 4 files changed, 39 insertions(+), 11 deletions(-) diff --git a/app/src/main/cpp/winlator/fakeinput.cpp b/app/src/main/cpp/winlator/fakeinput.cpp index b080d6a69..65dd1c5fd 100644 --- a/app/src/main/cpp/winlator/fakeinput.cpp +++ b/app/src/main/cpp/winlator/fakeinput.cpp @@ -418,17 +418,17 @@ read_snapshot(const FakeInputRingHeader *ring, SnapshotState &out) { // as a keyframe independently of the ring. Idempotent w.r.t. an in-flight // keyframe: callers guard on keyframe_remaining == 0 so a partially delivered // frame is never restarted mid-stream. -__attribute__((visibility("hidden"))) static void +__attribute__((visibility("hidden"))) static bool capture_keyframe(FakeController &fake, const char *reason, int fd) { SnapshotState snap; - // On a failed read keep the previous keyframe rather than publishing a neutral. + // Only a fresh snapshot arms a keyframe; a failed read stays unarmed so the caller replays the ring window instead of a stale frame. bool fresh = read_snapshot(fake.ring, snap); if (fresh) { fake.keyframe_buttons = snap.buttons; for (int i = 0; i < 8; i++) fake.keyframe_axes[i] = snap.axes[i]; + fake.keyframe_remaining = kNeutralEventCount; } - fake.keyframe_remaining = kNeutralEventCount; Logger::log("Fake input keyframe reason=%s fd=%d slot=%d snapshot=%s read_seq=%llu " "write_seq=%llu buttons=0x%03x axes=[%d,%d,%d,%d,%d,%d,%d,%d]\n", reason ? reason : "unknown", fd, fake.slot, fresh ? "fresh" : "stale", @@ -439,6 +439,18 @@ capture_keyframe(FakeController &fake, const char *reason, int fd) { fake.keyframe_axes[3], fake.keyframe_axes[4], fake.keyframe_axes[5], fake.keyframe_axes[6], fake.keyframe_axes[7]); + return fresh; +} + +// Overflow recovery: a fresh keyframe holds the full state so drop the stale window; on a failed read replay the window instead. +__attribute__((visibility("hidden"))) static void +handle_overflow(FakeController &fake, uint64_t write_seq, int fd) { + fake.read_seq = write_seq - FAKE_INPUT_RING_CAPACITY; + bool discard = true; + if (fake.keyframe_remaining == 0) + discard = capture_keyframe(fake, "overflow", fd); + if (discard) + fake.read_seq = write_seq; } // Resolve the value a keyframe event should carry from the captured snapshot. @@ -541,10 +553,7 @@ fake_fd_has_unread_data(int fd) { if (write_seq < fake.read_seq) fake.read_seq = write_seq; if (write_seq - fake.read_seq > FAKE_INPUT_RING_CAPACITY) { - fake.read_seq = write_seq - FAKE_INPUT_RING_CAPACITY; - if (fake.keyframe_remaining == 0) { - capture_keyframe(fake, "overflow", fd); - } + handle_overflow(fake, write_seq, fd); } // A pending keyframe counts as readable so poll/blocking reads wake to finish // flushing it even after the ring itself has drained. @@ -1086,10 +1095,7 @@ EXPORT ssize_t read(int fd, void *buf, size_t count) { uint64_t write_seq = ring_write_seq(fake.ring); if (write_seq - fake.read_seq > FAKE_INPUT_RING_CAPACITY) { - fake.read_seq = write_seq - FAKE_INPUT_RING_CAPACITY; - if (fake.keyframe_remaining == 0) { - capture_keyframe(fake, "overflow", fd); - } + handle_overflow(fake, write_seq, fd); } uint8_t *out = static_cast(buf); diff --git a/app/src/main/runtime/input/ui/InputControlsView.java b/app/src/main/runtime/input/ui/InputControlsView.java index 89b0473ba..ebb71757a 100644 --- a/app/src/main/runtime/input/ui/InputControlsView.java +++ b/app/src/main/runtime/input/ui/InputControlsView.java @@ -904,6 +904,7 @@ public boolean onTouchEvent(MotionEvent event) { } releaseStaleCaptures(event); + if (touchpadView != null) touchpadView.reconcileScreenTouchStick(event); } return true; } diff --git a/app/src/main/runtime/input/ui/ScreenTouchStick.kt b/app/src/main/runtime/input/ui/ScreenTouchStick.kt index 249d6830b..678a21317 100644 --- a/app/src/main/runtime/input/ui/ScreenTouchStick.kt +++ b/app/src/main/runtime/input/ui/ScreenTouchStick.kt @@ -70,6 +70,23 @@ class ScreenTouchStick(context: Context, private val xServer: XServer) { } } + // Release a stranded stick: pointer stolen by a control it drifted onto, or no longer down (missed UP). + fun reconcile(event: MotionEvent, controlOwnedPointerIds: Set) { + if (activePointerId == -1) return + if (controlOwnedPointerIds.contains(activePointerId)) { + releaseAll() + return + } + val liftingIndex = when (event.actionMasked) { + MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP -> event.actionIndex + else -> -1 + } + for (p in 0 until event.pointerCount) { + if (p != liftingIndex && event.getPointerId(p) == activePointerId) return + } + releaseAll() + } + private fun push(x: Float, y: Float) { xServer.winHandler?.setScreenTouchRightStick(x, y) } diff --git a/app/src/main/runtime/input/ui/TouchpadView.kt b/app/src/main/runtime/input/ui/TouchpadView.kt index 9d39e6b04..1b0443a4c 100644 --- a/app/src/main/runtime/input/ui/TouchpadView.kt +++ b/app/src/main/runtime/input/ui/TouchpadView.kt @@ -652,6 +652,10 @@ class TouchpadView( pointerIdsToIgnore.addAll(ids) } + fun reconcileScreenTouchStick(event: MotionEvent) { + screenTouchStick.reconcile(event, pointerIdsToIgnore) + } + var tapToClickEnabled = true fun setMouseEnabled(enabled: Boolean) { From c475cd2bda5641fe9ee8cb9aa442dbe669fa8237 Mon Sep 17 00:00:00 2001 From: Xnick417x Date: Thu, 2 Jul 2026 13:46:50 +0000 Subject: [PATCH 3/3] Fix held stick freezing the other stick during an input flood On ring overflow the reader dropped the surviving window by default, so while a keyframe was mid-stream it discarded fresh delta events without refreshing the full state. A quiet axis whose last event had aged out of the window then stayed pinned to the stale keyframe value until the flooding axis stopped. Replay the window by default and drop it only when a fresh full keyframe (a true superset) was captured this call. --- app/src/main/cpp/winlator/fakeinput.cpp | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/app/src/main/cpp/winlator/fakeinput.cpp b/app/src/main/cpp/winlator/fakeinput.cpp index 65dd1c5fd..36c283f15 100644 --- a/app/src/main/cpp/winlator/fakeinput.cpp +++ b/app/src/main/cpp/winlator/fakeinput.cpp @@ -442,14 +442,11 @@ capture_keyframe(FakeController &fake, const char *reason, int fd) { return fresh; } -// Overflow recovery: a fresh keyframe holds the full state so drop the stale window; on a failed read replay the window instead. +// Overflow recovery: only a freshly captured keyframe is a superset, so drop the window only then; otherwise replay it so a quiet axis' delta is never lost. __attribute__((visibility("hidden"))) static void handle_overflow(FakeController &fake, uint64_t write_seq, int fd) { fake.read_seq = write_seq - FAKE_INPUT_RING_CAPACITY; - bool discard = true; - if (fake.keyframe_remaining == 0) - discard = capture_keyframe(fake, "overflow", fd); - if (discard) + if (fake.keyframe_remaining == 0 && capture_keyframe(fake, "overflow", fd)) fake.read_seq = write_seq; }