Skip to content

Spud v1.0.5

Latest

Choose a tag to compare

@github-actions github-actions released this 08 Jun 20:37

Spud v1.0.5

Overview

  • Reduced input latency across all platforms by removing polling delays and unnecessary allocations.
  • X11 capture now blocks on events instead of busy-waiting, and auto-repeat no longer causes phantom key releases.
  • Wayland pointer-lock polling timeout reduced from 50ms to 5ms.
  • Linux evdev injection no longer allocates per wheel event.
  • Windows diagonal scroll now batched into a single syscall.
  • macOS injector thread sleeps efficiently when idle, and key-repeat pulses no longer cause phantom releases in games.
  • Server UDP path no longer copies unencrypted payloads.
  • Client no longer leaks sent_keys or sends duplicate releases on disconnect.

Changes

Linux (X11)

  • Fixed reconnect hotkey grab failure (Access error): When reconnecting, the old X11 input thread was still alive, blocked in wait_for_event() and holding the key grab. The new thread then failed to grab the same hotkey with an Access error. The event loop now uses poll_for_event() followed by libc::poll() on the X11 file descriptor with a 10 ms timeout. This blocks efficiently when idle (no added latency) but allows the old thread to detect when the stream is dropped and exit promptly, releasing the grab before the new connection attempts its own.
  • Fixed stuck modifier keys / lost events during auto-repeat detection: The X11 backend used poll_for_event() to peek at the next event after a KeyRelease to suppress auto-repeat pairs. If the next event was not the expected autorepeat KeyPress (e.g. a mouse motion or another key release), it was silently discarded. This caused modifier key releases to get stuck on the server, which on macOS manifested as F exiting fullscreen and Escape producing a system beep. Non-autorepeat events peeked from the queue are now buffered and re-processed on the next loop iteration instead of being dropped.
  • Fixed phantom key release during X11 auto-repeat: When X11 auto-repeat is enabled and the event queue is empty after a KeyRelease, the backend could incorrectly treat the release as real (sending a phantom KeyUp) because the matching KeyPress had not yet arrived. It now queries the physical keyboard bitmap via XQueryKeymap as a fallback: if the key is still physically down, the release is treated as auto-repeat and swallowed.
  • Ungrab stale hotkey grabs before re-grabbing: As a defensive measure, the X11 backend still calls ungrab_key for all modifier combinations before attempting to grab, handling cases where a previous process crashed without cleaning up.

Linux (Wayland)

  • Faster compositor polling: Reduced the libc::poll timeout in the Wayland event loop from 50ms to 5ms. This was adding unnecessary lag when waiting for pointer lock / confinement state changes from the compositor.

Linux (evdev injection)

  • Zero-allocation wheel events: emit_wheel() previously allocated a temporary Vec on every wheel event. Replaced with fixed-size stack arrays, removing allocator pressure from the hot path.

Windows (injection)

  • Batched diagonal scroll: Diagonal scroll (both horizontal and vertical) previously triggered two separate SendInput syscalls. Now submitted as a single two-element SendInput call, cutting the kernel transition overhead in half.

macOS (injection)

  • Idle thread efficiency: The injector worker thread used a fixed 50ms recv_timeout even when no keys were held, waking up 20 times per second to do nothing. It now uses a 1-second timeout when idle and only switches to 50ms when keys are actively held for repeat generation.
  • Faster modifier check: Super+Space detection used an O(n) iterator scan over held keys. Replaced with two HashMap::contains_key calls.
  • Set modifier flags on every injected keyboard event: Injected CGEvent keyboard events were posted with CGEventFlags::Null, so macOS had no way to know which modifiers were held. This caused modifier state to desync when events arrived out of order or were dropped. Every injected key event now computes the correct modifier flags from the injector's own held_keys state and sets them via event.set_flags() before posting.
  • Fixed phantom key release in games: The injector pulsed KeyUp + KeyDown every 50ms for held keys to reset macOS's accent-menu timer. This caused raw-input games to see a brief key release. The pulse now posts to CGEventTapLocation::Session (application-local) instead of HID (global). Games monitoring the HID stream no longer see the release blip, while the Text Input system still sees the events and resets the accent-menu timer.

Server (all platforms)

  • Zero-copy UDP decoding: The server path for unencrypted UDP packets was copying the payload into a new Vec before decoding. It now decodes directly from the packet buffer slice.
  • Accurate decrypt failure tracking: Restructured the decrypt path so decrypt_event failures correctly increment the per-session failure counter instead of falling through to a false-positive success path.
  • Fixed stuck keys after disconnect / lost release events: The KeyTracker returned synthetic release actions for duplicate key-downs (indicating a lost key-up), but the server discarded them with let _actions = ... and never injected them. Additionally, when a session disconnected, any keys still held in the tracker were not released, so they stayed stuck in the macOS injector's state forever. Both issues are fixed: tracker actions are now injected, and disconnecting triggers a release_all() flush to the injector.

Client (all platforms)

  • Release held keys on disconnect: Disconnect and ConnectionLost handlers cleared the local pressed_keys set without first sending KeyUp/MouseButton release events. If the network dropped while keys were held, the server would never receive the releases and those keys would remain stuck. The client now flushes release events before clearing state.
  • Track all sent keys for release on disconnect: The client only tracked keys currently in pressed_keys for the disconnect-release flush. If a KeyUp event was lost in transit, the key was no longer in pressed_keys and would not be released. Added a sent_keys set that tracks every key ever sent as KeyDown; disconnect now releases all sent_keys instead of just pressed_keys.
  • Fixed sent_keys leak causing duplicate releases: Keys were added to sent_keys on KeyDown but never removed on a successful KeyUp. This caused release_all_held() to send duplicate KeyUp events for keys that had already been released cleanly.
  • Display X11 backend errors in UI: BackendError messages from the input backend (e.g. "could not grab hotkey: Access error") were silently ignored. They now appear as a visible error message in the client UI.

Known Issues & Limitations

  • The Wayland capture backend still requires compositor support for pointer-constraints and relative-pointer protocols; minimal compositors may not support these.
  • macOS input capture requires manual permission grants in System Settings before the app can capture input. Rebuilding or re-signing the binary may revoke previously granted Accessibility permissions.