Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 52 additions & 37 deletions app/src/main/cpp/winlator/fakeinput.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -379,54 +379,75 @@ 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<unsigned long long>(fake.read_seq),
static_cast<unsigned long long>(ring_write_seq(fake.ring)),
fake.keyframe_buttons, fake.keyframe_axes[0],
fake.keyframe_axes[1], fake.keyframe_axes[2],
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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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<uint8_t *>(buf);
Expand Down
1 change: 1 addition & 0 deletions app/src/main/runtime/input/ui/InputControlsView.java
Original file line number Diff line number Diff line change
Expand Up @@ -904,6 +904,7 @@ public boolean onTouchEvent(MotionEvent event) {
}

releaseStaleCaptures(event);
if (touchpadView != null) touchpadView.reconcileScreenTouchStick(event);
}
return true;
}
Expand Down
17 changes: 17 additions & 0 deletions app/src/main/runtime/input/ui/ScreenTouchStick.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<Int>) {
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)
}
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/runtime/input/ui/TouchpadView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -652,6 +652,10 @@ class TouchpadView(
pointerIdsToIgnore.addAll(ids)
}

fun reconcileScreenTouchStick(event: MotionEvent) {
screenTouchStick.reconcile(event, pointerIdsToIgnore)
}

var tapToClickEnabled = true

fun setMouseEnabled(enabled: Boolean) {
Expand Down