fix(capture): throttle WaitingForAck Enter re-sends to one per 50ms#437
Open
jondkinney wants to merge 2 commits intofeschber:mainfrom
Open
fix(capture): throttle WaitingForAck Enter re-sends to one per 50ms#437jondkinney wants to merge 2 commits intofeschber:mainfrom
jondkinney wants to merge 2 commits intofeschber:mainfrom
Conversation
Originally every `CaptureEvent::Input` arriving while the cross was in `State::WaitingForAck` got converted into a repeat `Enter` packet. The intent was reliability — if the first `Enter` was dropped, the user's next motion would re-trigger it. In practice on a LAN the round-trip-to-Ack is sub-millisecond and the user's mouse fires at 1000 Hz, so each cross sent 2–10 `Enter` packets inside the gap. Each one made the receiver run its 'Enter' handler again — re-running `ReleaseNotify` and (with the CursorPos work) re-warping the cursor on its entry edge. The user sees that cluster of warps as a visible stutter and reads it as "the cross got rejected." Throttle re-sends to at most one per 50 ms: the first `Enter` always fires immediately, subsequent motion events inside that window are dropped, and a stalled handshake recovers on the next motion event after 50 ms. 50 ms is short enough to be well inside human reaction time and long enough that a healthy LAN never re-sends at all. No protocol change. The Ack path additionally clears the re-send timer so subsequent crosses start fresh.
Windows clippy flags `loop { let Some(...) = get_msg() else { break } }`
as while-let-loop. Rewrite to `while let Some(msg) = get_msg() { … }`.
The inner `break` for `RequestType::Exit` still breaks the surrounding
while-let, so semantics are unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced May 7, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes a sub-millisecond protocol pile-up that produces visible cursor stutter on cross-machine handoffs.
Root cause
When a cross begins, capture transitions to
State::WaitingForAckand sends anEnterpacket. The original logic also converted every subsequentCaptureEvent::Input(motion event) arriving in that window into a repeatEnter— intended as a reliability hedge against a dropped firstEnter.On a typical LAN the round-trip-to-Ack is 0.4–0.7 ms and the user's mouse fires at 1000 Hz, so each cross was sending 2–10
Enterpackets in the gap. The receiver'sEnterhandler runs once per packet — re-runningReleaseNotify, re-acking, re-warping the cursor on the entry edge. The user sees that cluster of warps as a visible stutter and reads it as "the cross got rejected."Confirmed via instrumentation:
client 0 ACK in 0.7ms after 2 Enter event(s)was a typical line, with the receiver seeing the second EnterΔ 0.1ms since last Enter.Fix
Throttle re-sends to at most one per 50 ms:
Enter(onBegin) always fires immediatelyAck, the next motion re-sendsEnter50 ms is short enough to be well inside human reaction time on a stalled handshake and long enough that a healthy LAN never re-sends at all.
Note: fix is per-sender
The throttle runs on the sending side, so the user-visible stutter only fully resolves once both peers carry this change. A mixed pair (one upgraded, one not) still sees the stutter when crossing from the unpatched peer. This is worth calling out for anyone shipping or backporting in stages.
Verification (Linux ↔ macOS, real LAN, ~50 crosses each direction)
Captured with diagnostic instrumentation (sender logs ACK latency + Enter count per cross; receiver logs inter-Enter Δ per peer). Diagnostics not included in this PR.
Linux → macOS (sender): 120/120 ACKs after exactly 1 Enter event, latency 0.4–1.0 ms. Zero multi-Enter handshakes.
macOS → Linux (receiver): post-fix sessions show minimum inter-Enter Δ of 470 ms across all crosses — well above the 50 ms throttle window. Pre-fix sessions on the same hardware showed Δ as low as 0.1 ms (the original bug signature, exactly matching the root-cause analysis).
Cursor stutter no longer reproducible after both peers upgraded.
Test plan
🤖 Generated with Claude Code
Related PRs
This PR is part of an effort to split #418 into focused, independently-reviewable pieces.
The split stack (each builds on the previous; review in order):
Plus:
fix(capture): throttle WaitingForAck Enter re-sends to one per 50ms — small standalone fix offmain, independent of the stack