Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 46 additions & 3 deletions src/input.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ extern "C" {
#include <bitset>
#include <chrono>
#include <cmath>
#include <cstddef>
#include <cstring>
#include <functional>
#include <list>
Expand Down Expand Up @@ -68,6 +69,16 @@ namespace input {
constexpr auto VKEY_LMENU = 0xA4; ///< Windows virtual-key code for lmenu.
constexpr auto VKEY_RMENU = 0xA5; ///< Windows virtual-key code for rmenu.

constexpr auto SS_KEY_UNDO = 0x0100; ///< Non-normalized client keycode for undo.
constexpr auto SS_KEY_CUT = 0x0101; ///< Non-normalized client keycode for cut.
constexpr auto SS_KEY_COPY = 0x0102; ///< Non-normalized client keycode for copy.
constexpr auto SS_KEY_PASTE = 0x0103; ///< Non-normalized client keycode for paste.

constexpr auto SS_SCANCODE_UNDO = 0x7A; ///< SDL scancode delivered for undo when non-normalized.
constexpr auto SS_SCANCODE_CUT = 0x7B; ///< SDL scancode delivered for cut when non-normalized.
constexpr auto SS_SCANCODE_COPY = 0x7C; ///< SDL scancode delivered for copy when non-normalized.
constexpr auto SS_SCANCODE_PASTE = 0x7D; ///< SDL scancode delivered for paste when non-normalized.

/**
* @brief Enumerates supported button state options.
*/
Expand Down Expand Up @@ -1196,12 +1207,42 @@ namespace input {
}

auto release = util::endian::little(packet->header.magic) == KEY_UP_EVENT_MAGIC;
auto keyCode = packet->keyCode & 0x00FF;
// Keycodes carrying the legacy 0x8000 flag are normalized Windows virtual-key
// codes for which only the low byte is significant. Non-normalized keycodes
// (such as the Undo/Cut/Copy/Paste editing keys) are interpreted as-is.
const bool normalized_prefix = (packet->keyCode & 0x8000) != 0;
auto keyCode = normalized_prefix ? (packet->keyCode & 0x00FF) : (packet->keyCode & 0xFFFF);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Preserve the full editing keycode when releasing held keys

Keeping 0x0100–0x0103 here introduces a cleanup mismatch: reset_keyboard_keys() still calls map_keycode(vk_from_kpid(key) & 0x00FF). If a client sends Paste down and disconnects before Paste up, Sunshine emits down for 0x0103, then cleanup emits up for 0x03 and marks the tracked key released. The pinned Linux backend rejects that release code, leaving Paste held and potentially repeating after disconnect. Undo/Cut/Copy and remapped editing keys have the same problem.

In src/input.cpp, inside reset_keyboard_keys(), remove the byte mask while retaining the configured remap and the existing flags/extended arguments after rebasing:

map_keycode(vk_from_kpid(key))

Add disconnect coverage for both the scancode aliases and dedicated keycodes, plus a remapped editing key. For example, append this test to tests/unit/test_keyboard.cpp, using its existing recorder fixture:

TEST_F(KeyboardPassthroughTest, ReleasesHeldEditingKeysWhenClientDisconnects) {
  const std::array<std::uint16_t, 8> client_keys {
    0x7A, 0x7B, 0x7C, 0x7D, 0x0100, 0x0101, 0x0102, 0x0103
  };
  for (const auto client_key : client_keys) {
    SCOPED_TRACE(client_key);
    press(client_key, 0, 0x01);
    input::testing::release_held_keys();
    const auto events = taken_events();
    ASSERT_EQ(events.size(), 2);
    EXPECT_GE(events[0].key_code, 0x0100);
    EXPECT_FALSE(events[0].release);
    EXPECT_EQ(events[1].key_code, events[0].key_code);
    EXPECT_TRUE(events[1].release);
    EXPECT_EQ(events[1].flags, 0x01);
  }
}

Run with the repository's MSYS2 UCRT64 prefix and cmake-build-<name>/tests/test_sunshine --gtest_filter=KeyboardPassthroughTest.ReleasesHeldEditingKeysWhenClientDisconnects. The cleanup correction was validated in the focused extracted-function harness; this fixture-level snippet has not been run in a full Sunshine build.

Review completed by Codex | Model: gpt-6.1-sol | Thinking: xhigh


int modifiers = packet->modifiers;

// Clients deliver the Undo/Cut/Copy/Paste editing keys as non-normalized
// keycodes, either as the SDL scancode or a dedicated value. Normalize the
// scancode form to the canonical keycode that the platform backend maps to
// the native editing key. Requiring the absence of the 0x8000 prefix keeps
// the F11-F14 virtual keys (which some clients send with the non-normalized
// flag set) from being reinterpreted as editing keys.
if (!normalized_prefix && (static_cast<std::byte>(packet->flags) & static_cast<std::byte>(SS_KBE_FLAG_NON_NORMALIZED)) != std::byte {}) {
switch (keyCode) {
case SS_SCANCODE_UNDO:
keyCode = SS_KEY_UNDO;
break;
case SS_SCANCODE_CUT:
keyCode = SS_KEY_CUT;
break;
case SS_SCANCODE_COPY:
keyCode = SS_KEY_COPY;
break;
case SS_SCANCODE_PASTE:
keyCode = SS_KEY_PASTE;
break;
default:
break;
}
}

update_modifier_state(*input, keyCode, release);

// Right-alt maps to meta, so it must not also register as ALT
int modifiers = packet->modifiers;
if (config::input.key_rightalt_to_key_win && input->alt_keys.right_pressed && !input->alt_keys.left_pressed) {
modifiers &= ~MODIFIER_ALT;
}
Expand Down Expand Up @@ -2287,7 +2328,9 @@ namespace input {
// 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), state.extended);
// The full 16-bit code is preserved so the Undo/Cut/Copy/Paste keycodes
// (0x0100-0x0103) are released with the same code they were pressed with.
emit_keyboard_update(map_keycode(vk_from_kpid(key)), true, flags_from_kpid(key), state.extended);
state.pressed = false;
}
}
Expand Down
68 changes: 68 additions & 0 deletions tests/unit/test_input.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -642,3 +642,71 @@ TEST_F(InputGamepadSessionTest, RetargetsRelativeMouseFromLatestStreamGeometry)
EXPECT_EQ(mouse_event.y, 9);
EXPECT_EQ(context().mouse_viewport.offset_x, 0);
}

TEST_F(InputGamepadSessionTest, TranslatesEditingKeysToCanonicalKeycodes) {
config::input.keyboard = true;
config::input.key_repeat_delay = std::chrono::milliseconds {0};

auto mail = std::make_shared<safe::mail_raw_t>();
auto session = input::alloc(mail, "editing-keys-client");
ASSERT_NE(session, nullptr);
ASSERT_NE(context().keyboard, nullptr);

constexpr std::uint8_t non_normalized = 0x01; // SS_KBE_FLAG_NON_NORMALIZED

struct editing_key_case {
std::uint16_t client_key; ///< Keycode sent by the client.
std::uint16_t expected_key; ///< Canonical keycode forwarded to the backend.
};

const std::array<editing_key_case, 8> cases {{
{0x7A, 0x0100}, // SDL scancode Undo
{0x7B, 0x0101}, // SDL scancode Cut
{0x7C, 0x0102}, // SDL scancode Copy
{0x7D, 0x0103}, // SDL scancode Paste
{0x0100, 0x0100}, // Dedicated Undo
{0x0101, 0x0101}, // Dedicated Cut
{0x0102, 0x0102}, // Dedicated Copy
{0x0103, 0x0103}, // Dedicated Paste
}};

for (const auto &editing_key : cases) {
const auto before = context().keyboard->submit_count();

input::testing::send_keyboard_packet(session, editing_key.client_key, 0, non_normalized, false);
EXPECT_EQ(context().keyboard->submit_count(), before + 1);
const auto press_event = context().keyboard->last_submitted_event();
EXPECT_EQ(press_event.key_code, editing_key.expected_key);
EXPECT_TRUE(press_event.pressed);

input::testing::send_keyboard_packet(session, editing_key.client_key, 0, non_normalized, true);
EXPECT_EQ(context().keyboard->submit_count(), before + 2);
const auto release_event = context().keyboard->last_submitted_event();
EXPECT_EQ(release_event.key_code, editing_key.expected_key);
EXPECT_FALSE(release_event.pressed);
}

// Without the non-normalized flag, 0x7A-0x7D are the F11-F14 virtual keys and
// must pass through untranslated.
const auto before_fn = context().keyboard->submit_count();
input::testing::send_keyboard_packet(session, 0x7A, 0, 0, false);
EXPECT_EQ(context().keyboard->submit_count(), before_fn + 1);
const auto fn_event = context().keyboard->last_submitted_event();
EXPECT_EQ(fn_event.key_code, 0x7A);
EXPECT_TRUE(fn_event.pressed);
input::testing::send_keyboard_packet(session, 0x7A, 0, 0, true);

// A normalized F11-F14 keycode (0x807A-0x807D) that also carries the
// non-normalized flag must remain a function key and not be aliased to an
// editing key. Only the low byte reaches the backend after prefix stripping.
const std::array<std::uint16_t, 4> normalized_function_keys {0x807A, 0x807B, 0x807C, 0x807D};
for (const auto normalized_key : normalized_function_keys) {
const auto before = context().keyboard->submit_count();
input::testing::send_keyboard_packet(session, normalized_key, 0, non_normalized, false);
EXPECT_EQ(context().keyboard->submit_count(), before + 1);
const auto event = context().keyboard->last_submitted_event();
EXPECT_EQ(event.key_code, normalized_key & 0x00FF);
EXPECT_TRUE(event.pressed);
input::testing::send_keyboard_packet(session, normalized_key, 0, non_normalized, true);
}
}
60 changes: 60 additions & 0 deletions tests/unit/test_keyboard.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -991,6 +991,66 @@ TEST_F(KeyboardPassthroughTest, ReleasesTheRemappedHostKeyWhenTheClientDisconnec
EXPECT_EQ(taken(), (std::vector<std::string> {released(VKEY_LWIN)}));
}

TEST_F(KeyboardPassthroughTest, ReleasesHeldEditingKeysWhenClientDisconnects) {
constexpr unsigned non_normalized = 0x01; // SS_KBE_FLAG_NON_NORMALIZED

struct editing_key_case_t {
std::uint16_t client_key; ///< Keycode sent by the client.
std::uint16_t host_key; ///< Canonical keycode emitted toward the platform.
};

constexpr std::array<editing_key_case_t, 8> cases {{
{0x7A, 0x0100}, // SDL scancode Undo
{0x7B, 0x0101}, // SDL scancode Cut
{0x7C, 0x0102}, // SDL scancode Copy
{0x7D, 0x0103}, // SDL scancode Paste
{0x0100, 0x0100}, // Dedicated Undo
{0x0101, 0x0101}, // Dedicated Cut
{0x0102, 0x0102}, // Dedicated Copy
{0x0103, 0x0103}, // Dedicated Paste
}};

for (const auto &editing_key : cases) {
SCOPED_TRACE(hex(editing_key.client_key));

press(editing_key.client_key, 0, non_normalized);
EXPECT_EQ(taken(), (std::vector<std::string> {pressed(editing_key.host_key)}));

input::testing::release_held_keys();

// The disconnect cleanup must release the full 16-bit code. Masking it to the low byte
// would release 0x03 instead of 0x0103, leaving the host holding the key down.
const auto events = taken_events();
ASSERT_EQ(events.size(), 1U);
EXPECT_EQ(events[0].key_code, editing_key.host_key);
EXPECT_TRUE(events[0].release);
EXPECT_EQ(events[0].flags, non_normalized);
}
}

TEST_F(KeyboardPassthroughTest, ReleasesTheRemappedHostKeyForHeldEditingKeysWhenClientDisconnects) {
constexpr unsigned non_normalized = 0x01; // SS_KBE_FLAG_NON_NORMALIZED
config::input.keybindings = {{0x0103, VKEY_F1}, {0x0100, VKEY_F1 + 1}};

// A dedicated keycode and a scancode alias both resolve to a canonical code before remapping.
press(0x0103, 0, non_normalized);
press(0x7A, 0, non_normalized);
auto pressed_keys = taken();
std::ranges::sort(pressed_keys);
std::vector expected_pressed {pressed(VKEY_F1), pressed(VKEY_F1 + 1)};
std::ranges::sort(expected_pressed);
EXPECT_EQ(pressed_keys, expected_pressed);

input::testing::release_held_keys();

// key_press is an unordered_map, so the release order is unspecified.
auto released_keys = taken();
std::ranges::sort(released_keys);
std::vector expected_released {released(VKEY_F1), released(VKEY_F1 + 1)};
std::ranges::sort(expected_released);
EXPECT_EQ(released_keys, expected_released);
}

TEST_F(KeyboardPassthroughTest, ReleasesTheRemappedRightAltForKeyRightaltToKeyWin) {
// Mirrors what config parsing installs for the shipped key_rightalt_to_key_win option.
config::input.key_rightalt_to_key_win = true;
Expand Down