From 7b2391dd950ea18c8776a4e6d2f6078e7989e619 Mon Sep 17 00:00:00 2001 From: Dylan Deverill Date: Wed, 5 Aug 2026 18:51:21 -0700 Subject: [PATCH] Repeat held key bindings at the system typematic rate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Holding a paddle bound to a letter produced exactly one character. The emission loop only acts on edges, so a held paddle sent one key-down and nothing further until release. That was the correct consequence of the design rather than a fault in the send path: auto-repeat is not something Windows synthesises for a key it believes is down. A physical keyboard repeats because its own firmware keeps sending make codes while the key is held, and Windows forwards each one. Injected input has no firmware behind it, so the repeat has to be generated here. Re-send the press while the paddle is held, after the user's configured delay and at their configured rate — SPI_GETKEYBOARDDELAY and SPI_GETKEYBOARDSPEED — so a paddle behaves like the key it is bound to rather than at a rate invented here. Both are captured at press time, so the rate cannot shift mid-hold. The read loop already ticks at report rate, so this is a per-frame elapsed check with no new thread or timer. Keys only. Mice do not repeat when held, so mouse-button bindings keep their one-shot behaviour, and gamepad bindings are held state in the pad report, where a repeat would mean nothing. Co-Authored-By: Claude Opus 5 --- src/app/ControllerManager.cpp | 29 ++++++++++++++++++++++++++++- src/app/KeyInput.cpp | 18 ++++++++++++++++++ src/app/KeyInput.h | 12 ++++++++++++ 3 files changed, 58 insertions(+), 1 deletion(-) diff --git a/src/app/ControllerManager.cpp b/src/app/ControllerManager.cpp index 02dfddf..ab85151 100644 --- a/src/app/ControllerManager.cpp +++ b/src/app/ControllerManager.cpp @@ -42,6 +42,12 @@ struct ControllerManager::Slot { // again on release, so rebinding a paddle mid-hold still releases cleanly. BackButtonBinding paddleHeld[4]; + // Auto-repeat for held key bindings. When the next repeat is due, and the + // gap to use after that — both captured at press time from the user's + // keyboard settings, so the rate cannot shift mid-hold. + std::chrono::steady_clock::time_point paddleRepeatAt[4]{}; + std::chrono::milliseconds paddleRepeatGap[4]{}; + // Haptic edge-detection state for touch (movement ticks). bool hapticWasRightTouching = false; bool hapticWasLeftTouching = false; @@ -823,7 +829,12 @@ void ControllerManager::ReadLoop(Slot* slot) { const auto& e = edges[i]; if (e.cur == e.prev) continue; if (e.cur) { - if (SendPaddleInput(e.binding, true)) slot->paddleHeld[i] = e.binding; + if (SendPaddleInput(e.binding, true)) { + slot->paddleHeld[i] = e.binding; + slot->paddleRepeatGap[i] = KeyRepeatInterval(); + slot->paddleRepeatAt[i] = + std::chrono::steady_clock::now() + KeyRepeatDelay(); + } } else { // Release what the press sent, not what the binding says // now — the two differ if the user rebound mid-hold. @@ -833,6 +844,22 @@ void ControllerManager::ReadLoop(Slot* slot) { } } + // Auto-repeat held keys. A physical keyboard repeats because its own + // firmware keeps sending the key, not because Windows tracks the key as + // down, so injected input has to reproduce that itself. Keys only — + // mice do not repeat when held, and gamepad bindings are held state in + // the pad report, where a repeat would be meaningless. + for (size_t i = 0; i < std::size(slot->paddleHeld); ++i) { + const auto& held = slot->paddleHeld[i]; + if (held.kind != BackButtonBinding::Kind::Key) continue; + + const auto now = std::chrono::steady_clock::now(); + if (now < slot->paddleRepeatAt[i]) continue; + + SendKeyInput(held.code, true); + slot->paddleRepeatAt[i] = now + slot->paddleRepeatGap[i]; + } + // Button capture for the remap window (press-to-bind). // Only fires on a rising edge to avoid repeat-triggering on hold. if (m_capturing.load() && hasPrev) { diff --git a/src/app/KeyInput.cpp b/src/app/KeyInput.cpp index f322b27..80fd15f 100644 --- a/src/app/KeyInput.cpp +++ b/src/app/KeyInput.cpp @@ -151,3 +151,21 @@ void SendKeyInput(uint16_t vk, bool down) { if (!down) input.ki.dwFlags |= KEYEVENTF_KEYUP; SendInput(1, &input, sizeof(INPUT)); } + +std::chrono::milliseconds KeyRepeatDelay() { + // 0-3, meaning roughly 250ms to 1000ms in 250ms steps. + DWORD setting = 1; + if (!SystemParametersInfoW(SPI_GETKEYBOARDDELAY, 0, &setting, 0) || setting > 3) + setting = 1; + return std::chrono::milliseconds(250 * (static_cast(setting) + 1)); +} + +std::chrono::milliseconds KeyRepeatInterval() { + // 0-31, meaning roughly 2.5 to 30 repeats per second. Windows documents only + // the endpoints, so interpolate the rate between them. + DWORD setting = 31; + if (!SystemParametersInfoW(SPI_GETKEYBOARDSPEED, 0, &setting, 0) || setting > 31) + setting = 31; + const double perSecond = 2.5 + (30.0 - 2.5) * (static_cast(setting) / 31.0); + return std::chrono::milliseconds(static_cast(1000.0 / perSecond + 0.5)); +} diff --git a/src/app/KeyInput.h b/src/app/KeyInput.h index 20eadca..c840685 100644 --- a/src/app/KeyInput.h +++ b/src/app/KeyInput.h @@ -1,4 +1,5 @@ #pragma once +#include #include #include @@ -22,4 +23,15 @@ std::wstring KeyDisplayName(uint16_t vk); // Presses or releases a key. Sends a scan code rather than a virtual key, and // flags the extended keys, because software that reads scan codes directly // (games, in particular) sees nothing useful otherwise. +// +// Holding a key down does not repeat on its own: auto-repeat comes from the +// keyboard's own typematic behaviour, not from Windows noticing a key is held, +// and injected input has no firmware behind it. A caller that wants a held key +// to repeat has to re-send the press itself, paced by the two values below. void SendKeyInput(uint16_t vk, bool down); + +// The user's Windows keyboard settings: how long a key must be held before it +// starts repeating, and the gap between repeats after that. Read fresh rather +// than cached at startup so a change in Control Panel is picked up. +std::chrono::milliseconds KeyRepeatDelay(); +std::chrono::milliseconds KeyRepeatInterval();