Skip to content

fix(input): Windows Shift/Ctrl were dead + injected keys made no edges#99

Merged
proggeramlug merged 2 commits into
mainfrom
fix/windows-modifier-keys-and-injection-edges
Jul 15, 2026
Merged

fix(input): Windows Shift/Ctrl were dead + injected keys made no edges#99
proggeramlug merged 2 commits into
mainfrom
fix/windows-modifier-keys-and-injection-edges

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Two input bugs, both of which silently disabled whole features rather than
failing loudly.

1. Shift/Ctrl/Alt never registered on Windows

map_keycode only knew the side-specific modifier codes (VK_LSHIFT 0xA0,
VK_LCONTROL 0xA2, …). But Windows delivers the generic VK in
WM_KEYDOWN/UPVK_SHIFT (0x10) for either shift, VK_CONTROL (0x11) for
either ctrl. The side-specific codes come back only from GetKeyState, never in
the message wParam. So map_keycode(0x10) hit _ => 0 and the press was
dropped: every Shift and Ctrl vanished before reaching isKeyDown().

Consequence in the shooter: sprint (Shift) and dodge (Ctrl) silently never
fired on Windows.
Only letters/arrows/F-keys ever worked — which is why WASD,
Space, R and Q were fine and nobody spotted it.

resolve_modifier_vk() recovers the side from lParam (scancode for shift —
LShift 0x2A / RShift 0x36; the extended-key bit for ctrl/alt) and runs before
map_keycode in both wndprocs (standalone + embedded subclass).

2. Injected keys never produced an isKeyPressed edge

keys_pressed is computed in begin_frame as keys_down && !prev_keys_down,
but prev_keys_down is copied in end_frame. A real key arrives in
poll_events()before begin_frame — so it gets an edge. An injected
key is raised from the game loop, after begin_frame, so end_frame folded it
into prev_keys_down before begin_frame ever looked. isKeyDown() saw it;
isKeyPressed() never did — making the injection API useless for anything
edge-triggered (every menu/UI in any game on this engine).

Injected keys are now staged in pending_key_down/up and applied at the top
of begin_frame, before the edge loop. Real input is untouched.

Verification

  • Real OS keypress, not injection: SendInput with KEYEVENTF_SCANCODE 0x2A
    produces a genuine WM_KEYDOWN with wParam=0x10; a probe latch then reads
    isKeyDown(LEFT_SHIFT)=1 (it read 0 before this change). In-game, holding
    Shift takes the player 4.50 → 9.00 m/s.
  • Injection: a shooter harness drives MAIN → SETTINGS → ESC-back → PLAY
    entirely through injectKeyDown, asserting menu state at each step.

Pairs with Bloom-Engine/shooter#20.

https://claude.ai/code/session_01J1UWgMcrTNvwWeXcJ1T3Fp

Summary by CodeRabbit

  • Bug Fixes
    • Fixed injected keyboard events so key-press and key-release states are detected correctly on the following frame.
    • Improved Windows keyboard handling to distinguish left and right Shift, Ctrl, and Alt keys.
    • Prevented modifier-based actions from being missed when Windows reports generic modifier key codes.

Ralph Kuepper added 2 commits July 15, 2026 17:29
keys_pressed is computed in begin_frame as (keys_down && !prev_keys_down), but
prev_keys_down is copied from keys_down in end_frame. A REAL key arrives in
poll_events(), which runs BEFORE begin_frame, so its edge is computed. An
INJECTED key (bloom_inject_key_down) is raised from the game loop — AFTER
begin_frame — so end_frame folded it into prev_keys_down before begin_frame ever
looked at it, and the edge was never computed at all.

isKeyDown() saw injected keys; isKeyPressed() never did. That silently made the
injection API useless for anything edge-triggered — every menu and every UI in
any game on this engine, which is the real cause behind "synthetic input doesn't
reach menus".

Injected keys are now staged in pending_key_down/up and applied at the TOP of
begin_frame, before the edge loop. Real input (set_key_down/up from the message
pump) is untouched. An injection on frame N now reads exactly like a real press
on frame N+1.

Verified: a shooter harness drives MAIN -> SETTINGS -> ESC-back -> PLAY entirely
through injectKeyDown and asserts the menu state at each step.

Claude-Session: https://claude.ai/code/session_01J1UWgMcrTNvwWeXcJ1T3Fp
…odifier VK

map_keycode only knew the SIDE-SPECIFIC modifier codes (VK_LSHIFT 0xA0,
VK_LCONTROL 0xA2, ...). But Windows delivers the GENERIC VK in WM_KEYDOWN/UP —
VK_SHIFT (0x10) for either shift, VK_CONTROL (0x11) for either ctrl, VK_MENU
(0x12) for alt. The side-specific codes come back only from GetKeyState, never in
the message wParam. So map_keycode(0x10) hit `_ => 0` and the press was dropped:
every Shift and Ctrl vanished before it reached isKeyDown().

Consequence in the shooter: sprint (Shift) and dodge (Ctrl) silently never fired
on Windows. Only letters/arrows/F-keys ever worked — which is why WASD, Space, R
and Q were fine and nobody spotted it.

resolve_modifier_vk() recovers the side from lParam — scancode for shift (LShift
0x2A / RShift 0x36), the extended-key bit for ctrl/alt (the right-hand ones are
extended) — and runs before map_keycode in BOTH wndprocs (standalone + embedded
subclass).

Verified with a real OS keypress, not injection: SendInput with
KEYEVENTF_SCANCODE 0x2A produces a genuine WM_KEYDOWN with wParam=0x10, and a
probe latch then reads isKeyDown(LEFT_SHIFT)=1 (it read 0 before this change).
In-game: holding Shift now takes the player 4.50 -> 9.00 m/s.

Claude-Session: https://claude.ai/code/session_01J1UWgMcrTNvwWeXcJ1T3Fp
@proggeramlug proggeramlug merged commit 671c914 into main Jul 15, 2026
7 of 10 checks passed
@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2f0c17cd-f95e-4692-ad97-d549f448c727

📥 Commits

Reviewing files that changed from the base of the PR and between 1aedf68 and 3eb0414.

📒 Files selected for processing (3)
  • native/shared/src/ffi_core/game_loop.rs
  • native/shared/src/input.rs
  • native/windows/src/lib.rs

📝 Walkthrough

Walkthrough

Keyboard injection now stages events until frame start so pressed/released edges are computed correctly. Windows keyboard handlers resolve generic Shift, Ctrl, and Alt messages to side-specific keys before mapping them.

Changes

Keyboard input handling

Layer / File(s) Summary
Staged synthetic key injection
native/shared/src/input.rs, native/shared/src/ffi_core/game_loop.rs
InputState buffers injected key changes, applies them during begin_frame, computes edge states afterward, and routes FFI injection calls through the staged APIs.
Windows modifier key resolution
native/windows/src/lib.rs
Top-level and embedded Windows procedures resolve generic modifier virtual keys using lParam before Bloom key translation.

Estimated code review effort: 2 (Simple) | ~10 minutes

Sequence Diagram(s)

sequenceDiagram
  participant FFI
  participant InputState
  participant FrameLoop
  FFI->>InputState: Inject synthetic key event
  InputState->>InputState: Store pending key state
  FrameLoop->>InputState: Begin frame
  InputState->>InputState: Apply pending state
  InputState-->>FrameLoop: Compute pressed/released edges
Loading
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/windows-modifier-keys-and-injection-edges

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@proggeramlug proggeramlug deleted the fix/windows-modifier-keys-and-injection-edges branch July 15, 2026 15:30
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