Skip to content

Lurch strafe: lock-free hot path + strict key-state tracking - #36

Merged
fL0P1337 merged 1 commit into
masterfrom
devin/1779471275-lurch-fastpath-strict
May 22, 2026
Merged

fL0P1337 merged 1 commit into
masterfrom
devin/1779471275-lurch-fastpath-strict

Conversation

@devin-ai-integration

Copy link
Copy Markdown
Contributor

Summary

Targets two related concerns on the lurch-strafe (Spam) pipeline that you raised: (1) performance of the input → spam-thread hot path, and (2) strict key-state tracking to eliminate ghost inputs and instability.

Performance — lock-free hot path

  • Lurch state is now a single atomic. g_activeSpamKeys (std::vector<int>), g_activeKeysMutex, and g_spamKeysEpoch collapse into one std::atomic<uint32_t> g_lurchState (low 4 bits = WASD bitmask, high 24 bits = epoch). Hook thread and spam thread no longer take a mutex to publish/consume the active key set. The publisher uses a CAS loop that only bumps the epoch when the key bits actually change.
  • Backend status counters are atomic. KbdHookBackend::statusMutex_ is gone — the LL-hook callback path is now mutex-free, eliminating a real risk of Windows silently unregistering the hook under contention (it enforces a 300 ms timeout by default). Same change applied to InterceptionBackend.
  • WASD scancode + flag cache in SpamLogic. SendKeyInputBatch no longer calls MapVirtualKeyW per cycle; the per-VK scan code and KEYEVENTF_EXTENDEDKEY template are cached on first use.
  • Stack-allocated INPUT buffer. SendKeyInputBatch uses std::array<INPUT, 4> (WASD bound) instead of a heap vector, removing per-cycle allocator churn.
  • KeybindManager state tracker moves from std::map<int,bool> + std::mutex to std::array<std::atomic<bool>, 256>. Same observable semantics, no allocation, no global lock per keystroke.

Strict key-state tracking — ghost-input & instability

  • Edge dedup at dispatch. HandleFeatureKeyEvent now short-circuits any event that does not change physicalKeyDown. This drops the Windows autorepeat storm (1 DOWN per ~30 ms while a key is held), which previously drove the full feature chain (axis mutex, publish, CV wake) on every repeat. Behaviour is preserved — every downstream consumer already deduped autorepeats internally.
  • Per-VK debounce. New Config::DebounceUs (debounce_us in config.cfg, default 500 µs). Opposite-edge transitions arriving within the window of the previous edge are dropped as bounce / driver echo. The default sits below human dexterity (~10 ms) and above typical mechanical-switch debounce (~50 µs). Set to 0 to disable.
  • Memory ordering audit. Acquire/release pair on physicalKeyDown, g_isSpamActive, and g_lurchState so observers on the spam thread / GUI thread can never see a partial state on weakly-ordered architectures (latent on x86, real on ARM).
  • Re-validation before SendInput. Spam thread re-checks the lurch epoch and g_isSpamActive immediately before injecting. Closes the snapshot→inject race that previously emitted ghost DOWN events for just-released keys.
  • CleanupSpamState uses the same CAS protocol as the publisher; the two operations interleave safely instead of fighting over the vector.

Scope intentionally NOT in this PR

  • Focus-loss reconciliation (reconcile physicalKeyDown against GetAsyncKeyState on WM_ACTIVATEAPP / idle tick).
  • Synthetic-echo defense (per-VK pending counters to ignore reflected injections from co-resident drivers).
  • Logger off the hook thread (queue + worker thread for Logger::Log).
  • High-resolution waitable timer replacing timeBeginPeriod(1) + busy-spin in SpamLogic.

These are behaviour-changing or larger refactors; happy to follow up with separate PRs once you confirm direction.

Review & Testing Checklist for Human

  • Lurch behaviour in-game. Hold A, hold both A+D, swap which is "winner" (SnapTap on), release while spamming. Spam should not emit ghost DOWN after release; the previously-active key should not stay stuck in the target app.
  • Debounce default. Confirm 500 µs default doesn't suppress real fast taps. If you ever notice missed inputs on rapid double-taps, set debounce_us = 0 in config.cfg and retry — that isolates whether debounce is the culprit.
  • Backend switch. Toggle between KbdHook and Interception while the spam trigger is active; spam keys should release cleanly across the switch.
  • Stats panel in GUI. "Spamming: …" should still reflect the current active set when the trigger is held.
  • Rebind flow. Bind the spam trigger to a new key while a key is already held; the new bind shouldn't immediately fire spam.

Notes

  • Risk: yellow. The hot path is restructured but every public API and config key is preserved; g_activeSpamKeys was internal-only. The atomic-bitmask layout is documented inline in Globals.h.
  • KeyState's deleted copy/move ops are a small correctness improvement: the struct was only ever used as elements of g_KeyInfo[256], so disallowing copy/move is closer to intent and removes the maintenance burden of keeping the manual copy/move ops in sync with new fields like lastEdgeNs.
  • The analysis document with the full tier breakdown (Tiers 3 / 4 deferred) is attached to the originating session.

Link to Devin session: https://app.devin.ai/sessions/aefc79c6e4004edfac6d5aef0487ec4b
Requested by: @fL0P1337

Performance

- Replace g_activeSpamKeys vector + g_activeKeysMutex with a single std::atomic<uint32_t> g_lurchState (WASD bitmask in low 4 bits + 24-bit epoch). Hook thread and spam thread no longer take a mutex to publish/consume the active key set.

- Drop statusMutex_ from KbdHookBackend; per-counter atomics so the LL-hook callback never blocks. Mirror the change in InterceptionBackend.

- Cache per-VK scan code + flag template in SpamLogic so SendKeyInputBatch no longer calls MapVirtualKeyW on every spam cycle. Switch batch buffer to stack std::array<INPUT,4>.

- KeybindManager previous-state tracker: std::map<int,bool> + std::mutex -> std::array<std::atomic<bool>,256>. Removes allocation + per-keystroke mutex.

Strict key-state tracking

- Edge dedup in HandleFeatureKeyEvent: ignore events that don't change physicalKeyDown (drops OS autorepeat storm).

- Configurable per-VK debounce window (debounce_us, default 500) rejects bounce / driver echo.

- Acquire/release ordering on shared atomics (physicalKeyDown, g_isSpamActive, g_lurchState) so observers can't see partial state on weakly-ordered architectures.

- Spam thread re-validates the lurch epoch and g_isSpamActive immediately before SendInput, closing the snapshot->inject race that emitted ghost DOWN events for just-released keys.

- Cleanup path uses the same CAS protocol as the publisher, so cleanup and publish interleave safely.
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

@fL0P1337
fL0P1337 merged commit 849436e into master May 22, 2026
4 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant