diff --git a/app/src/main/cpp/winlator/fakeinput.cpp b/app/src/main/cpp/winlator/fakeinput.cpp index 5f474c2e8..36c283f15 100644 --- a/app/src/main/cpp/winlator/fakeinput.cpp +++ b/app/src/main/cpp/winlator/fakeinput.cpp @@ -379,47 +379,59 @@ 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 // 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 = read_snapshot(fake.ring); - 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 " + SnapshotState snap; + // 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; + } + 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], @@ -427,6 +439,15 @@ 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: 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; + if (fake.keyframe_remaining == 0 && capture_keyframe(fake, "overflow", fd)) + fake.read_seq = write_seq; } // Resolve the value a keyframe event should carry from the captured snapshot. @@ -529,10 +550,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. @@ -1074,10 +1092,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) {