SDL: wayland: Refine key mapping

From 03cd2de3fe2e98f86b49d395f8de5946d0e9b4dc Mon Sep 17 00:00:00 2001
From: Frank Praznik <[EMAIL REDACTED]>
Date: Fri, 18 Sep 2026 14:20:15 -0400
Subject: [PATCH] wayland: Refine key mapping

Wayland can map keys beyond the legacy X mapping limitations, so use the Linux key table as a scancode source, as it mirrors the XFree86_2 table, but has additional scancode mappings for higher keys.

Looking up unknown scancodes by keysym on hardware keyboards could cause scancode collisions, causing a fixed scancode mapping to potentially be overwritten by a dynamic one. If a hardware key with an unknown scancode has a valid SDL keycode, map the key to the reserved range. This prevents existing scancodes from potentially being overwritten, and ensures that all hardware keys have a unique scancode value.
---
 src/video/wayland/SDL_waylandevents.c   | 64 +++++++++++++++++++------
 src/video/wayland/SDL_waylandevents_c.h |  1 +
 src/video/wayland/SDL_waylandsym.h      |  4 ++
 3 files changed, 54 insertions(+), 15 deletions(-)

diff --git a/src/video/wayland/SDL_waylandevents.c b/src/video/wayland/SDL_waylandevents.c
index 976c6b79fbba7..722aca62846ee 100644
--- a/src/video/wayland/SDL_waylandevents.c
+++ b/src/video/wayland/SDL_waylandevents.c
@@ -100,6 +100,12 @@
 // Scoped function declarations
 static void Wayland_SeatUpdateKeyboardGrab(SDL_WaylandSeat *seat);
 
+typedef struct
+{
+    Uint32 key;
+    SDL_Scancode scancode;
+} Wayland_ReservedKey;
+
 typedef struct
 {
     SDL_TouchID id;
@@ -1613,21 +1619,17 @@ static void Wayland_KeymapIterator(struct xkb_keymap *keymap, xkb_keycode_t key,
 
     // Look up the scancode for hardware keyboards. Virtual keyboards get the scancode from the keysym.
     if (!seat->keyboard.is_virtual) {
-        scancode = SDL_GetScancodeFromTable(SDL_SCANCODE_TABLE_XFREE86_2, (key - 8));
-        if (scancode == SDL_SCANCODE_UNKNOWN) {
-            return;
-        }
+        scancode = SDL_GetScancodeFromTable(SDL_SCANCODE_TABLE_LINUX, key - 8);
     }
 
     for (xkb_layout_index_t layout = 0; layout < seat->keyboard.xkb.num_layouts; ++layout) {
         const xkb_level_index_t num_levels = WAYLAND_xkb_keymap_num_levels_for_key(seat->keyboard.xkb.keymap, key, layout);
         for (xkb_level_index_t level = 0; level < num_levels; ++level) {
             if (WAYLAND_xkb_keymap_key_get_syms_by_level(seat->keyboard.xkb.keymap, key, layout, level, &syms) > 0) {
-                /* If the keyboard is virtual or the key didn't have a corresponding hardware scancode, try to
-                 * look it up from the keysym. If there is still no corresponding scancode, skip this mapping
-                 * for now, as it will be dynamically added with a reserved scancode on first use.
+                /* If the keyboard is virtual, try to look up the scancode from the keysym. If there is still no corresponding
+                 * scancode, skip this mapping for now, as it will be dynamically added with a reserved scancode on first use.
                  */
-                if (scancode == SDL_SCANCODE_UNKNOWN) {
+                if (scancode == SDL_SCANCODE_UNKNOWN && seat->keyboard.is_virtual) {
                     scancode = SDL_GetScancodeFromKeySym(syms[0], key);
                     if (scancode == SDL_SCANCODE_UNKNOWN) {
                         continue;
@@ -1654,6 +1656,22 @@ static void Wayland_KeymapIterator(struct xkb_keymap *keymap, xkb_keycode_t key,
 
                     SDL_Keycode keycode = SDL_GetKeyCodeFromKeySym(syms[0], key, sdl_mod);
 
+                    /* For hardware keyboards, map unknown keys with valid keycodes to the reserved scancode range.
+                     * Reserved codes are always assigned from layout zero to avoid potential overlap.
+                     */
+                    if (keycode != SDLK_UNKNOWN && scancode == SDL_SCANCODE_UNKNOWN) {
+                        scancode = SDL_GetKeymapNextReservedScancode(seat->keyboard.sdl_keymap[0]);
+                        if (level) {
+                            // Make sure the base level always has this scancode mapped, since it is a unique key.
+                            SDL_SetKeymapEntry(seat->keyboard.sdl_keymap[layout], scancode, 0, SDLK_UNKNOWN);
+                        }
+                        Wayland_ReservedKey *res = WAYLAND_wl_array_add(&seat->keyboard.reserved_scancodes, sizeof(Wayland_ReservedKey));
+                        if (res) {
+                            res->scancode = scancode;
+                            res->key = key - 8;
+                        }
+                    }
+
                     if (!keycode) {
                         switch (scancode) {
                         case SDL_SCANCODE_RETURN:
@@ -1728,6 +1746,8 @@ static void keyboard_handle_keymap(void *data, struct wl_keyboard *keyboard,
     }
     SDL_free(seat->keyboard.sdl_keymap);
     seat->keyboard.sdl_keymap = NULL;
+    WAYLAND_wl_array_release(&seat->keyboard.reserved_scancodes);
+    WAYLAND_wl_array_init(&seat->keyboard.reserved_scancodes);
     seat->keyboard.xkb.num_layouts = 0;
 
 #if SDL_XKBCOMMON_CHECK_VERSION(1, 10, 0)
@@ -1783,6 +1803,8 @@ static void keyboard_handle_keymap(void *data, struct wl_keyboard *keyboard,
             return;
         }
 
+        WAYLAND_wl_array_init(&seat->keyboard.reserved_scancodes);
+
         for (xkb_layout_index_t i = 0; i < seat->keyboard.xkb.num_layouts; ++i) {
             seat->keyboard.sdl_keymap[i] = SDL_CreateKeymap(false);
             if (!seat->keyboard.sdl_keymap[i]) {
@@ -1856,18 +1878,26 @@ static void keyboard_handle_keymap(void *data, struct wl_keyboard *keyboard,
     }
 }
 
-/*
- * Virtual keyboards can have arbitrary layouts, arbitrary scancodes/keycodes, etc...
+/* Virtual keyboards can have arbitrary layouts, arbitrary scancodes/keycodes, etc...
  * Key presses from these devices must be looked up by their keysym value.
  */
-static SDL_Scancode Wayland_GetScancodeForKey(SDL_WaylandSeat *seat, uint32_t key, const xkb_keysym_t **syms)
+static SDL_Scancode Wayland_GetScancodeForKey(SDL_WaylandSeat *seat, Uint32 key, const xkb_keysym_t **syms)
 {
     SDL_Scancode scancode = SDL_SCANCODE_UNKNOWN;
 
     if (!seat->keyboard.is_virtual) {
-        scancode = SDL_GetScancodeFromTable(SDL_SCANCODE_TABLE_XFREE86_2, key);
-    }
-    if (scancode == SDL_SCANCODE_UNKNOWN) {
+        scancode = SDL_GetScancodeFromTable(SDL_SCANCODE_TABLE_LINUX, key);
+
+        // No table entry? Check the reserved list.
+        if (scancode == SDL_SCANCODE_UNKNOWN) {
+            Wayland_ReservedKey *i;
+            wl_array_for_each(i, &seat->keyboard.reserved_scancodes) {
+                if (i->key == key) {
+                    return i->scancode;
+                }
+            }
+        }
+    } else {
         const xkb_keysym_t *keysym;
         if (WAYLAND_xkb_state_key_get_syms(seat->keyboard.xkb.state, key + 8, &keysym) > 0) {
             scancode = SDL_GetScancodeFromKeySym(keysym[0], key + 8);
@@ -2020,7 +2050,7 @@ static void Wayland_ReconcileModifiers(SDL_WaylandSeat *seat, bool key_pressed)
 
 static void Wayland_HandleModifierKeys(SDL_WaylandSeat *seat, SDL_Scancode scancode, bool pressed)
 {
-    const SDL_Keycode keycode = SDL_GetKeyFromScancode(scancode, SDL_KMOD_NONE, false);
+    const SDL_Keycode keycode = SDL_GetKeyFromScancode(scancode, seat->keyboard.pressed_modifiers | seat->keyboard.locked_modifiers, false);
     SDL_Keymod mod;
 
     /* SDL clients expect modifier state to be activated at the same time as the
@@ -2464,6 +2494,8 @@ static void Wayland_SeatDestroyKeyboard(SDL_WaylandSeat *seat)
         seat->keyboard.sdl_keymap = NULL;
     }
 
+    WAYLAND_wl_array_release(&seat->keyboard.reserved_scancodes);
+
     if (seat->keyboard.key_inhibitor) {
         zwp_keyboard_shortcuts_inhibitor_v1_destroy(seat->keyboard.key_inhibitor);
     }
@@ -2574,6 +2606,7 @@ static void seat_handle_capabilities(void *data, struct wl_seat *wl_seat, uint32
 
     if ((capabilities & WL_SEAT_CAPABILITY_KEYBOARD) && !seat->keyboard.wl_keyboard) {
         seat->keyboard.wl_keyboard = wl_seat_get_keyboard(wl_seat);
+        WAYLAND_wl_array_init(&seat->keyboard.reserved_scancodes);
         wl_keyboard_set_user_data(seat->keyboard.wl_keyboard, seat);
         wl_keyboard_add_listener(seat->keyboard.wl_keyboard, &keyboard_listener, seat);
 
@@ -3542,6 +3575,7 @@ void Wayland_DisplayCreateSeat(SDL_VideoData *display, struct wl_seat *wl_seat,
     // Keep the seats in the order in which they were added.
     WAYLAND_wl_list_insert(display->seat_list.prev, &seat->link);
 
+    WAYLAND_wl_array_init(&seat->keyboard.reserved_scancodes);
     WAYLAND_wl_list_init(&seat->touch.points);
     WAYLAND_wl_list_init(&seat->tablet.tool_list);
     seat->wl_seat = wl_seat;
diff --git a/src/video/wayland/SDL_waylandevents_c.h b/src/video/wayland/SDL_waylandevents_c.h
index 49bae2d8e671b..703a5fb140f09 100644
--- a/src/video/wayland/SDL_waylandevents_c.h
+++ b/src/video/wayland/SDL_waylandevents_c.h
@@ -137,6 +137,7 @@ typedef struct SDL_WaylandSeat
         struct zwp_keyboard_shortcuts_inhibitor_v1 *key_inhibitor;
         SDL_WindowData *focus;
         SDL_Keymap **sdl_keymap;
+        struct wl_array reserved_scancodes;
         char *current_locale;
 
         SDL_WaylandKeyboardRepeat repeat;
diff --git a/src/video/wayland/SDL_waylandsym.h b/src/video/wayland/SDL_waylandsym.h
index be15f533dfca5..2616cd90981d7 100644
--- a/src/video/wayland/SDL_waylandsym.h
+++ b/src/video/wayland/SDL_waylandsym.h
@@ -70,6 +70,10 @@ SDL_WAYLAND_SYM(void, wl_list_remove, (struct wl_list *))
 SDL_WAYLAND_SYM(int, wl_list_length, (const struct wl_list *))
 SDL_WAYLAND_SYM(int, wl_list_empty, (const struct wl_list *))
 SDL_WAYLAND_SYM(void, wl_list_insert_list, (struct wl_list *, struct wl_list *))
+SDL_WAYLAND_SYM(void, wl_array_init, (struct wl_array *))
+SDL_WAYLAND_SYM(void, wl_array_release, (struct wl_array *))
+SDL_WAYLAND_SYM(void *, wl_array_add, (struct wl_array *, size_t))
+SDL_WAYLAND_SYM(int, wl_array_copy, (struct wl_array *, struct wl_array *))
 SDL_WAYLAND_SYM(struct wl_proxy *, wl_proxy_marshal_constructor, (struct wl_proxy *, uint32_t opcode, const struct wl_interface *interface, ...))
 SDL_WAYLAND_SYM(struct wl_proxy *, wl_proxy_marshal_constructor_versioned, (struct wl_proxy *proxy, uint32_t opcode, const struct wl_interface *interface, uint32_t version, ...))
 SDL_WAYLAND_SYM(void, wl_proxy_set_tag, (struct wl_proxy *, const char * const *))