Skip to content

fix(input): track/release client keycode for modifiers - #5558

Merged
ReenigneArcher merged 6 commits into
LizardByte:masterfrom
sethdmoore:fix/input-bugs
Sep 1, 2026
Merged

fix(input): track/release client keycode for modifiers#5558
ReenigneArcher merged 6 commits into
LizardByte:masterfrom
sethdmoore:fix/input-bugs

Conversation

@sethdmoore

@sethdmoore sethdmoore commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Description

Fix two related bugs in the keybindings path. Also add tests.

Please let me know if any of the changes are unacceptable. This change set fixes bugs I
encountered while streaming OSX Taho with Sunshine to Linux (via Moonlight).

Tested and working after these patches. Wayland Arch client (moonlight) -> OSX host (sunshine).

Both bugs stem from the same issue: key_press and shortcutFlags were stored on
the client's unmapped virtual-key code. Key presses were emitted through
map_keycode(), so the host holds a different key than Sunshine's perspective.

1. Synthetic modifier fix (src/input.cpp, passthrough)

shortcutFlags is compared against packet->modifiers, a client-side bitmask. It was
updated from the remapped host keycode.

Any keybindings entry that moves Alt off
VKEY_MENU/VKEY_LMENU/VKEY_RMENU caused the ALT bit to clear while the client still
reported MODIFIER_ALT. send_key_and_modifiers() wrapped every following key in a real
VKEY_MENU press/release.

keybindings = [0x5B, 0xA4, 0x5C, 0xA5, 0xA4, 0x5B, 0xA5, 0x5C]

Before the fix, Alt+Space from a client (linux) arrived as Cmd+Option+Space on Sunshine host (OSX).
This would cause annoyances like: "Search This Mac" appearing when attempting to spawn Spotlight.
It also made simple things like such as option-as-alt in terminals to be unusable.

The bug affected every key pressed while the remapped Alt was held, not just Space.

The synthetic-modifier path exists for clients that report modifiers in the bitmask without
sending discrete modifier key events, so both sides of that comparison should be client-side.
apply_shortcut() in the combo check already receives the unmapped keycode, so this also
makes the two halves of that check agree.

2. Remapped key left latched on disconnect (src/input.cpp, reset_keyboard_keys)

The reset path pulled the unmapped VK back out of key_press and released those unmapped keys.
The remapped key that was never released. With the keybindings config above, a disconnect
released Option and left Command held.

This bug also affects the shippedkey_rightalt_to_key_win option, which follows the same remap / unmap flow.

3. Tests

platf::keyboard_update is a per-platform free function, and libvirtualhid's FakeKeyboard
discards submit(), so ordered keyboard output was not observable. The nine emit sites now go
through one emit_keyboard_update() forwarder whose sink check is inside
#ifdef SUNSHINE_TESTS; non-test builds compile to a plain forwarder.

18 tests: 45 held-modifier combinations (3 side variants x 15 non-empty subsets of
Shift/Ctrl/Alt/Meta), all 7 synthetic-injection subsets, 91 keys forwarded verbatim, a
self-verifying table proving all 95 printable ASCII characters are reachable and exercised
plain and shifted, flags propagation, press/release dedup, the shortcut-swallow path, and
regression coverage for both fixes. One test clears the recorder and asserts through
lvh::Keyboard::submit_count() so a broken delivery path can't hide behind the sink.

Both fixes were verified by reverting them individually and confirming the tests fail:
fix 1 -> 3 failures (expected {+0x20, -0x20}, got {+0x12, +0x20, -0x12, -0x20});
fix 2 -> 2 failures (released -0xA4 instead of -0x5B). A no-keybindings control test
passes either way, showing fix 2 doesn't over-map.

Native VK -> native keycode translation is deliberately not tested here. It moved to
libvirtualhid, whose backend tables live in anonymous namespaces in translation units that
only compile on their own platform, and which are already covered by TranslatesKeyboardKeys
in that project's per-platform test jobs. Nothing here is platform-gated, so every CI job runs
all 18.

Screenshot

N/A - not a UI change.

Issues Fixed or Closed

None open that I could find. Fix 1 is the same root cause as #4531, which #5318 fixed for the
key_rightalt_to_key_win path only. That special is likely redundant. Unmapped
tracking VKEY_RMENU applies the ALT bit on its own.

Roadmap Issues

None.

Type of Change

  • feat: New feature (non-breaking change which adds functionality)
  • fix: Bug fix (non-breaking change which fixes an issue)
  • docs: Documentation only changes
  • style: Changes that do not affect the meaning of the code (white-space, formatting, missing semicolons, etc.)
  • refactor: Code change that neither fixes a bug nor adds a feature
  • perf: Code change that improves performance
  • test: Adding missing tests or correcting existing tests
  • build: Changes that affect the build system or external dependencies
  • ci: Changes to CI configuration files and scripts
  • chore: Other changes that don't modify src or test files
  • revert: Reverts a previous commit
  • BREAKING CHANGE: Introduces a breaking change (can be combined with any type above)

Checklist

  • Code follows the style guidelines of this project
  • Code has been self-reviewed
  • Code has been commented, particularly in hard-to-understand areas
  • Code docstring/documentation-blocks for new or existing methods/components have been added or updated
  • Unit tests have been added or updated for any new or modified functionality

AI Usage

See our AI usage policy.

  • None: No AI tools were used in creating this PR
  • Light: AI provided minor assistance (formatting, simple suggestions)
  • Moderate: AI helped with code generation or debugging specific parts
  • Heavy: AI generated most or all of the code changes

Fixed, tested and working on my branch. Added tests to prevent
regressions.

shortcutFlags is compared against packet->modifiers, a client-side
bitmask, but was updated from the host keycode from map_keycode().

- A keybindings entry moving Alt off VKEY_*MENU left the ALT bit clear,
  so send_key_and_modifiers() wrapped every following key in a real
  VKEY_MENU press
  - Alt->Meta on a macOS host turned Cmd+Space into Cmd+Option+Space
- apply_shortcut() already gets the unmapped keycode, so both halves of
  the combo check now agree
- Generalizes the key_rightalt_to_key_win case from LizardByte#5318

fix(input): release the remapped key when the client disconnects

reset_keyboard_keys() emitted an unmapped virtual-key code. Keypresses
go are sent via map_keycode(). Remapped keys stay pressed on the
host after a disconnect.

- With keybindings 0xA4 -> 0x5B, Option was released while Command was
  left held down
- Also affected key_rightalt_to_key_win option

test(input): cover keyboard passthrough with a recording sink

platf::keyboard_update is a per-platform function and lvh's
FakeKeyboard discards submit(), so ordered keyboard output could not be
observed from a test.

- Route the nine emit sites through emit_keyboard_update(), whose sink
  check sits inside #ifdef SUNSHINE_TESTS
- Cover 45 held-modifier combinations, all 7 synthetic-injection
  subsets, and every printable ASCII character plain and shifted
- Pin the spurious VKEY_MENU injection fixed in the previous commit
- Native VK translation stays in libvirtualhid, which covers each
  backend table in its own platform's CI job
- Return the test keyboard recorder from an accessor, so the file adds no
  non-const variable at namespace scope
- Build assertion tokens and the stream name with std::format instead of
  output manipulators and manual concatenation
- Hold modifier masks in unsigned, and cast once where the packet helper
  needs a byte
- Move the modifier selection loop into select_modifiers(), which drops
  the combination test to three levels of nesting
- Make the fixture stream private, add using enum for side_e, and use
  std::ranges::sort with class template argument deduction
@Kishi85

Kishi85 commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

I'll test this as I've seen modifiers getting stuck sometimes myself on Linux host + Linux client. Will report back.

@Kishi85

Kishi85 commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

So far I've not seen a single stuck modifier with this PR applied. Neither have there been any other problems related to keyboard input so LGTM from a practical standpoint.

@ReenigneArcher

Copy link
Copy Markdown
Member

Thanks for working on this. There might be an edge case in the modifier tracking:

  1. Hold Left Alt.
  2. Hold Right Alt with key_rightalt_to_key_win enabled.
  3. Release Right Alt while continuing to hold Left Alt.
  4. Press another key with MODIFIER_ALT.

Releasing Right Alt currently clears the shared shortcutFlags::ALT bit even though Left Alt remains held. The next key therefore gets wrapped in an unnecessary synthetic Alt press/release. Could you update the tracking so the aggregate modifier bit is cleared only when no corresponding left/right/generic modifier key remains pressed? Please also extend KeepsRealAltWhileRightAltMapsToKeyWin by pressing a key after releasing Right Alt but before releasing Left Alt, verifying that no synthetic Alt events are emitted.

Left, right, and side-less Alt all share the same shortcutFlags::ALT
bit. update_shortcutFlags() cleared that bit on any Alt release, so
releasing Right Alt while Left Alt was still down (e.g. Right Alt
mapped to Meta via key_rightalt_to_key_win) dropped the bit even
though a real Alt key remained pressed. The next key then got wrapped
in an unnecessary synthetic Alt press/release.

- Track a side-less Alt key (VKEY_MENU) the same way Left/Right Alt
  are already tracked
- Clear the aggregate ALT bit on release only when none of
  left/right/generic Alt remain pressed

Extends KeepsRealAltWhileRightAltMapsToKeyWin to press a key after
releasing Right Alt but before releasing Left Alt, asserting no
synthetic Alt events are emitted.

Addresses review feedback from @ReenigneArcher on LizardByte#5558.
@sethdmoore

Copy link
Copy Markdown
Contributor Author

I've rebased and added another commit+tests that should (hopefully) address your concerns @ReenigneArcher .
I've recompiled and Sunshine is so far working (with remapped modifiers).

@ReenigneArcher

Copy link
Copy Markdown
Member

I probably didn't describe the issue properly, but it applied to more modifiers than just ALT. I pushed another commit to fully address it.

@codecov

codecov Bot commented Sep 1, 2026

Copy link
Copy Markdown

Bundle Report

Bundle size has no change ✅

@sonarqubecloud

sonarqubecloud Bot commented Sep 1, 2026

Copy link
Copy Markdown

@codecov

codecov Bot commented Sep 1, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 35.18%. Comparing base (f111d6a) to head (422bddb).
✅ All tests successful. No failed tests found.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #5558      +/-   ##
==========================================
+ Coverage   34.55%   35.18%   +0.62%     
==========================================
  Files         104      104              
  Lines       25314    25409      +95     
  Branches    11194    11211      +17     
==========================================
+ Hits         8748     8939     +191     
+ Misses      14460    14366      -94     
+ Partials     2106     2104       -2     
Flag Coverage Δ
Archlinux 0.00% <ø> (ø)
FreeBSD-amd64 20.79% <97.53%> (+1.05%) ⬆️
Homebrew-macos-14 32.48% <98.75%> (+1.36%) ⬆️
Homebrew-macos-15 33.12% <98.75%> (+1.35%) ⬆️
Homebrew-macos-26 33.22% <98.75%> (+1.35%) ⬆️
Homebrew-ubuntu-24.04 21.87% <93.90%> (+0.86%) ⬆️
Linux-AppImage 21.29% <93.90%> (+0.88%) ⬆️
Windows-AMD64 27.09% <ø> (+0.04%) ⬆️
Windows-ARM64 22.88% <ø> (+0.06%) ⬆️
macOS-arm64 27.44% <97.53%> (+1.45%) ⬆️
macOS-x86_64 28.08% <97.53%> (+1.45%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/input.cpp 44.31% <100.00%> (+13.13%) ⬆️
src/input.h 50.00% <ø> (ø)

... and 2 files with indirect coverage changes


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update f111d6a...422bddb. Read the comment docs.

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Last Updated 2026-09-01 22:58:54 UTC
Source Run CI Run #5231
Commit 422bddb0f310440cb14e43c8358b0a9694d4e9f1

Screenshot Comparison

PR #5558 screenshots vs screenshots baseline.

Matrix: AppImage

Image Baseline PR
sunshine_tray_initial.png
sunshine_tray_menu.png
sunshine_tray_pairing_request.png
sunshine_tray_paused.png
sunshine_tray_stopped.png
sunshine_tray_streaming.png
sunshine_tray_tooltip.png

Matrix: Windows-AMD64

Image Baseline PR
sunshine_tray_initial.png
sunshine_tray_menu.png
sunshine_tray_pairing_request.png
sunshine_tray_paused.png
sunshine_tray_stopped.png
sunshine_tray_streaming.png
sunshine_tray_tooltip.png

Matrix: Windows-ARM64

Image Baseline PR
sunshine_tray_initial.png
sunshine_tray_menu.png
sunshine_tray_pairing_request.png
sunshine_tray_paused.png
sunshine_tray_stopped.png
sunshine_tray_streaming.png
sunshine_tray_tooltip.png

Matrix: macOS-arm64

Image Baseline PR
sunshine_tray_initial.png
sunshine_tray_menu.png
sunshine_tray_pairing_request.png
sunshine_tray_paused.png
sunshine_tray_stopped.png
sunshine_tray_streaming.png
sunshine_tray_tooltip.png

Matrix: macOS-x86_64

Image Baseline PR
sunshine_tray_initial.png
sunshine_tray_menu.png
sunshine_tray_pairing_request.png
sunshine_tray_paused.png
sunshine_tray_stopped.png
sunshine_tray_streaming.png
sunshine_tray_tooltip.png

@ReenigneArcher
ReenigneArcher merged commit 9f7daa5 into LizardByte:master Sep 1, 2026
69 checks passed
@sethdmoore

sethdmoore commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

Oh thank you so much for merging this @ReenigneArcher!. Sorry you had to fix the PR on top of everything else :( . Sunshine is an awesome piece of technology that I use all throughout my household, so I appreciate your time and attention.

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.

3 participants