From d0a53cadca82bc1ff7a6fc93ff490dab1c3aae89 Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Sun, 27 Sep 2026 17:15:11 -0400 Subject: [PATCH] fix(input): honor extended keyboard modifier --- src/input.cpp | 46 ++++++++++++------- src/input.h | 1 + src/platform/common.h | 5 +- src/platform/virtualhid_input.cpp | 11 +++-- src/platform/virtualhid_input.h | 3 +- tests/unit/platform/test_virtualhid_input.cpp | 7 +++ tests/unit/test_keyboard.cpp | 29 ++++++++++++ third-party/libvirtualhid | 2 +- 8 files changed, 79 insertions(+), 25 deletions(-) diff --git a/src/input.cpp b/src/input.cpp index b30352205b8..0c07a915e30 100644 --- a/src/input.cpp +++ b/src/input.cpp @@ -163,7 +163,16 @@ namespace input { } static task_pool_util::TaskPool::task_id_t key_press_repeat_id {}; - static std::unordered_map key_press {}; + + /** + * @brief Press state retained so releases and repeats use the key's original identity. + */ + struct key_press_state_t { + bool pressed = false; ///< Whether the key is held. + bool extended = false; ///< Whether its press identified an extended key. + }; + + static std::unordered_map key_press {}; static std::array mouse_press {}; static platf::input_t platf_input; @@ -1093,15 +1102,16 @@ namespace input { * @param key_code Platform keycode to emit. * @param release Whether the key event is a release. * @param flags Bit flags that modify the requested operation. + * @param extended Whether the client identified an extended key. */ - void emit_keyboard_update(uint16_t key_code, bool release, uint8_t flags) { + void emit_keyboard_update(uint16_t key_code, bool release, uint8_t flags, bool extended = false) { #ifdef SUNSHINE_TESTS if (keyboard_sink()) { - keyboard_sink()(testing::keyboard_event_t {key_code, release, flags}); + keyboard_sink()(testing::keyboard_event_t {key_code, release, flags, extended}); return; } #endif - platf::keyboard_update(platf_input, key_code, release, flags); + platf::keyboard_update(platf_input, key_code, release, flags, extended); } /** @@ -1111,8 +1121,9 @@ namespace input { * @param release Whether the key or button event is a release. * @param flags Bit flags that modify the requested operation. * @param synthetic_modifiers Synthetic modifiers. + * @param extended Whether the client identified an extended key. */ - void send_key_and_modifiers(uint16_t key_code, bool release, uint8_t flags, uint8_t synthetic_modifiers) { + void send_key_and_modifiers(uint16_t key_code, bool release, uint8_t flags, uint8_t synthetic_modifiers, bool extended) { if (!release) { // Press any synthetic modifiers required for this key if (synthetic_modifiers & MODIFIER_SHIFT) { @@ -1126,7 +1137,7 @@ namespace input { } } - emit_keyboard_update(map_keycode(key_code), release, flags); + emit_keyboard_update(map_keycode(key_code), release, flags, extended); if (!release) { // Raise any synthetic modifier keys we pressed @@ -1151,12 +1162,13 @@ namespace input { */ void repeat_key(uint16_t key_code, uint8_t flags, uint8_t synthetic_modifiers) { // If key no longer pressed, stop repeating - if (!key_press[make_kpid(key_code, flags)]) { + const auto state = key_press[make_kpid(key_code, flags)]; + if (!state.pressed) { key_press_repeat_id = nullptr; return; } - send_key_and_modifiers(key_code, false, flags, synthetic_modifiers); + send_key_and_modifiers(key_code, false, flags, synthetic_modifiers, state.extended); key_press_repeat_id = task_pool.pushDelayed(repeat_key, config::input.key_repeat_period, key_code, flags, synthetic_modifiers).task_id; } @@ -1198,8 +1210,8 @@ namespace input { } } - auto &pressed = key_press[make_kpid(keyCode, packet->flags)]; - if (!pressed) { + auto &state = key_press[make_kpid(keyCode, packet->flags)]; + if (!state.pressed) { if (!release) { // A new key has been pressed down, we need to check for key combo's // If a key-combo has been pressed down, don't pass it through @@ -1207,6 +1219,8 @@ namespace input { return; } + state.extended = (modifiers & MODIFIER_EXTENDED) != 0; + if (key_press_repeat_id) { task_pool.cancel(key_press_repeat_id); } @@ -1223,9 +1237,9 @@ namespace input { return; } - pressed = !release; + state.pressed = !release; - send_key_and_modifiers(keyCode, release, packet->flags, synthetic_modifiers); + send_key_and_modifiers(keyCode, release, packet->flags, synthetic_modifiers, state.extended); // Track the modifier state the client is holding, not the remapped host key. // This is compared against packet->modifiers above, which is client-side, so a @@ -2243,13 +2257,13 @@ namespace input { * @brief Release every pressed keyboard key tracked by Sunshine. */ void reset_keyboard_keys() { - for (auto &[key, pressed] : key_press) { - if (pressed) { + for (auto &[key, state] : key_press) { + if (state.pressed) { // key_press is keyed on the client's unmapped virtual-key code, but the press was // emitted through map_keycode(). Release the host key that actually went down, // otherwise a remapped modifier stays latched after the client disconnects. - emit_keyboard_update(map_keycode(vk_from_kpid(key) & 0x00FF), true, flags_from_kpid(key)); - pressed = false; + emit_keyboard_update(map_keycode(vk_from_kpid(key) & 0x00FF), true, flags_from_kpid(key), state.extended); + state.pressed = false; } } } diff --git a/src/input.h b/src/input.h index 6d78447e53b..fc744b06d4e 100644 --- a/src/input.h +++ b/src/input.h @@ -139,6 +139,7 @@ namespace input { std::uint16_t key_code; ///< Platform keycode after the configured keybinding remap. bool release; ///< Whether the event releases the key. std::uint8_t flags; ///< Bit flags carried by the client keyboard packet. + bool extended = false; ///< Whether the client positively identified an extended key. }; /** diff --git a/src/platform/common.h b/src/platform/common.h index e8aff70ec2c..e19b38f74a5 100644 --- a/src/platform/common.h +++ b/src/platform/common.h @@ -1189,11 +1189,12 @@ namespace platf { * @brief Press or release a virtual keyboard key. * * @param input Platform input backend that receives the event. - * @param modcode Modifier key code to update. + * @param modcode Portable key code to update. * @param release Whether the key or button event is a release. * @param flags Bit flags that modify the requested operation. + * @param extended Whether the client positively identified an extended key. */ - void keyboard_update(input_t &input, uint16_t modcode, bool release, uint8_t flags); + void keyboard_update(input_t &input, uint16_t modcode, bool release, uint8_t flags, bool extended = false); void gamepad_update(input_t &input, int nr, const gamepad_state_t &gamepad_state); /** * @brief Submit UTF-8 text input to the keyboard backend. diff --git a/src/platform/virtualhid_input.cpp b/src/platform/virtualhid_input.cpp index db710dae6d4..dafb4628f70 100644 --- a/src/platform/virtualhid_input.cpp +++ b/src/platform/virtualhid_input.cpp @@ -304,10 +304,11 @@ namespace platf::virtualhid { }; } - lvh::KeyboardEvent keyboard_event(std::uint16_t modcode, bool release, std::uint8_t flags) { + lvh::KeyboardEvent keyboard_event(std::uint16_t modcode, bool release, std::uint8_t flags, bool extended) { lvh::KeyboardEvent event { .key_code = modcode, .pressed = !release, + .extended = extended, }; #ifdef _WIN32 @@ -837,9 +838,9 @@ namespace platf::virtualhid { } } - void keyboard_update(input_context_t &context, std::uint16_t modcode, bool release, std::uint8_t flags) { + void keyboard_update(input_context_t &context, std::uint16_t modcode, bool release, std::uint8_t flags, bool extended) { if (context.keyboard) { - log_failure("submit libvirtualhid keyboard input"sv, context.keyboard->submit(keyboard_event(modcode, release, flags))); + log_failure("submit libvirtualhid keyboard input"sv, context.keyboard->submit(keyboard_event(modcode, release, flags, extended))); } } @@ -1065,8 +1066,8 @@ namespace platf { virtualhid::hscroll(virtualhid::get_input_context(input), high_res_distance); } - void keyboard_update(input_t &input, uint16_t modcode, bool release, uint8_t flags) { - virtualhid::keyboard_update(virtualhid::get_input_context(input), modcode, release, flags); + void keyboard_update(input_t &input, uint16_t modcode, bool release, uint8_t flags, bool extended) { + virtualhid::keyboard_update(virtualhid::get_input_context(input), modcode, release, flags, extended); } void unicode(input_t &input, const char *utf8, int size) { diff --git a/src/platform/virtualhid_input.h b/src/platform/virtualhid_input.h index 967b3bbb246..7f0037f5ba9 100644 --- a/src/platform/virtualhid_input.h +++ b/src/platform/virtualhid_input.h @@ -281,8 +281,9 @@ namespace platf::virtualhid { * @param modcode Portable key code. * @param release Whether the key was released. * @param flags Bit flags that modify the requested operation. + * @param extended Whether the client positively identified an extended key. */ - void keyboard_update(input_context_t &context, std::uint16_t modcode, bool release, std::uint8_t flags); + void keyboard_update(input_context_t &context, std::uint16_t modcode, bool release, std::uint8_t flags, bool extended = false); /** * @brief Submit UTF-8 text input. diff --git a/tests/unit/platform/test_virtualhid_input.cpp b/tests/unit/platform/test_virtualhid_input.cpp index bbdcd710e17..b8e9bf13913 100644 --- a/tests/unit/platform/test_virtualhid_input.cpp +++ b/tests/unit/platform/test_virtualhid_input.cpp @@ -745,6 +745,7 @@ TEST_F(VirtualHidDeviceTest, TranslatesMouseAndKeyboardInput) { auto keyboard_event = context()->keyboard->last_submitted_event(); EXPECT_EQ(keyboard_event.key_code, 0x41); EXPECT_TRUE(keyboard_event.pressed); + EXPECT_FALSE(keyboard_event.extended); #ifdef _WIN32 EXPECT_TRUE(keyboard_event.uses_normalized_key_code); EXPECT_TRUE(keyboard_event.prefer_native_scan_code); @@ -758,6 +759,10 @@ TEST_F(VirtualHidDeviceTest, TranslatesMouseAndKeyboardInput) { keyboard_event = context()->keyboard->last_submitted_event(); #endif EXPECT_FALSE(keyboard_event.pressed); + platf::virtualhid::keyboard_update(*context(), 0x0D, false, 0, true); + keyboard_event = context()->keyboard->last_submitted_event(); + EXPECT_EQ(keyboard_event.key_code, 0x0D); + EXPECT_TRUE(keyboard_event.extended); const auto keyboard_submit_count = context()->keyboard->submit_count(); const std::string text = "Sunshine \u{2600}"; @@ -950,6 +955,8 @@ TEST_F(VirtualHidDeviceTest, PlatformWrappersForwardToVirtualHidContext) { EXPECT_EQ(platform_context.mouse->last_submitted_event().kind, lvh::MouseEventKind::horizontal_scroll); platf::keyboard_update(platform_input, 0x41, false, 0); EXPECT_EQ(platform_context.keyboard->last_submitted_event().key_code, 0x41); + platf::keyboard_update(platform_input, 0x0D, false, 0, true); + EXPECT_TRUE(platform_context.keyboard->last_submitted_event().extended); const std::string text = "wrapper"; const auto keyboard_count = platform_context.keyboard->submit_count(); platf::unicode(platform_input, text.data(), static_cast(text.size())); diff --git a/tests/unit/test_keyboard.cpp b/tests/unit/test_keyboard.cpp index 060341bc4fa..dd52fad5502 100644 --- a/tests/unit/test_keyboard.cpp +++ b/tests/unit/test_keyboard.cpp @@ -451,6 +451,35 @@ TEST_F(KeyboardPassthroughTest, ForwardsEveryUnmodifiedKeyUnchanged) { } } +TEST_F(KeyboardPassthroughTest, PreservesExtendedEnterOnReleaseAndDisconnect) { + press(VKEY_RETURN); + release(VKEY_RETURN); + auto events = taken_events(); + ASSERT_EQ(events.size(), 2U); + EXPECT_FALSE(events[0].extended); + EXPECT_FALSE(events[1].extended); + + press(VKEY_RETURN, MODIFIER_EXTENDED | MODIFIER_SHIFT); + events = taken_events(); + ASSERT_EQ(events.size(), 3U); + EXPECT_FALSE(events[0].extended); + EXPECT_TRUE(events[1].extended); + EXPECT_FALSE(events[2].extended); + + // The bit may be absent on key-up; release the key as it was pressed. + release(VKEY_RETURN); + events = taken_events(); + ASSERT_EQ(events.size(), 1U); + EXPECT_TRUE(events[0].extended); + + press(VKEY_RETURN, MODIFIER_EXTENDED); + taken_events(); + input::testing::release_held_keys(); + events = taken_events(); + ASSERT_EQ(events.size(), 1U); + EXPECT_TRUE(events[0].extended); +} + TEST_F(KeyboardPassthroughTest, CoversTheWholePrintableAsciiRange) { // Guard the table itself: every printable ASCII character must be reachable, exactly once. std::set covered; diff --git a/third-party/libvirtualhid b/third-party/libvirtualhid index 52d788119aa..2db2cc2d7f4 160000 --- a/third-party/libvirtualhid +++ b/third-party/libvirtualhid @@ -1 +1 @@ -Subproject commit 52d788119aa4ba32bb1d6e9f68d347a47ce3dffe +Subproject commit 2db2cc2d7f428a53a480985fed041d77dc121f51