SDL: keyboard: Search for the correct base key value when querying the keycode from a scancode

From 7cc3feeb1bcc68d4d90732f6d9cd1e1299aed3e9 Mon Sep 17 00:00:00 2001
From: Frank Praznik <[EMAIL REDACTED]>
Date: Mon, 2 Jun 2025 11:46:42 -0400
Subject: [PATCH] keyboard: Search for the correct base key value when querying
 the keycode from a scancode

When querying the keycode produced by a scancode with a certain set of modifiers, it would fall back to defaults if a key hash value with the exact set of modifiers wasn't found, which resulted in certain modifier combination returning incorrect keycodes on non-ANSI keyboard layouts. For example, querying SDL_SCANCODE_Y with the alt modifier on a QWERTZ layout returns SDLK_Y instead of SDLK_Z on most platforms, as the backends don't generate a specific entry for this key + modifier combo, so the lookup would fall back to the default ANSI layout.

Adding additional key+modifier combinations when building the keymap is one solution, but it makes an already expensive operation even more so, pushing the time needed to build the keymap into double-digit milliseconds in some cases due to the large amount of key combos that need to be queried, most of which are redundant.

Instead, falling back to searching through the shift levels for the given modifier state when querying the keymap will ensure that the most appropriate keycode is returned. This does add some overhead to lookups if the key doesn't have an entry with the exact set of modifiers, but it is minimal as hash table lookups are an inexpensive operation, and unnecessary lookups are avoided. In my own testing of an optimized build, the difference between best-case and worst-case performance (the latter of which is highly unlikely in real-world usage) is only a few hundred nanoseconds. Additionally, the unmodified keys are queried when pumping events, so there is no additional overhead in that case.
---
 src/events/SDL_keymap.c | 74 ++++++++++++++++++++++++++++++++++++-----
 1 file changed, 66 insertions(+), 8 deletions(-)

diff --git a/src/events/SDL_keymap.c b/src/events/SDL_keymap.c
index 32e4975e7df22..318fc6864be86 100644
--- a/src/events/SDL_keymap.c
+++ b/src/events/SDL_keymap.c
@@ -97,16 +97,74 @@ void SDL_SetKeymapEntry(SDL_Keymap *keymap, SDL_Scancode scancode, SDL_Keymod mo
 
 SDL_Keycode SDL_GetKeymapKeycode(SDL_Keymap *keymap, SDL_Scancode scancode, SDL_Keymod modstate)
 {
-    SDL_Keycode keycode;
+    if (keymap) {
+        const void *value;
+        const SDL_Keymod normalized_modstate = NormalizeModifierStateForKeymap(modstate);
+        Uint32 key = ((Uint32)normalized_modstate << 16) | scancode;
 
-    const Uint32 key = ((Uint32)NormalizeModifierStateForKeymap(modstate) << 16) | scancode;
-    const void *value;
-    if (keymap && SDL_FindInHashTable(keymap->scancode_to_keycode, (void *)(uintptr_t)key, &value)) {
-        keycode = (SDL_Keycode)(uintptr_t)value;
-    } else {
-        keycode = SDL_GetDefaultKeyFromScancode(scancode, modstate);
+        // First, try the requested set of modifiers.
+        if (SDL_FindInHashTable(keymap->scancode_to_keycode, (void *)(uintptr_t)key, &value)) {
+            return (SDL_Keycode)(uintptr_t)value;
+        }
+
+        // If the requested set of modifiers was not found, search for the key from the highest to lowest modifier levels.
+        if (normalized_modstate) {
+            SDL_Keymod caps_mask = normalized_modstate & SDL_KMOD_CAPS;
+
+            for (int i = caps_mask ? 2 : 1; i; --i) {
+                // Shift level 5
+                if (normalized_modstate & SDL_KMOD_LEVEL5) {
+                    const SDL_Keymod shifted_modstate = SDL_KMOD_LEVEL5 | caps_mask;
+                    key = ((Uint32)shifted_modstate << 16) | scancode;
+
+                    if (shifted_modstate != normalized_modstate && SDL_FindInHashTable(keymap->scancode_to_keycode, (void *)(uintptr_t)key, &value)) {
+                        return (SDL_Keycode)(uintptr_t)value;
+                    }
+                }
+
+                // Shift level 4 (Level 3 + Shift)
+                if ((normalized_modstate & (SDL_KMOD_MODE | SDL_KMOD_SHIFT)) == (SDL_KMOD_MODE | SDL_KMOD_SHIFT)) {
+                    const SDL_Keymod shifted_modstate = SDL_KMOD_MODE | SDL_KMOD_SHIFT | caps_mask;
+                    key = ((Uint32)shifted_modstate << 16) | scancode;
+
+                    if (shifted_modstate != normalized_modstate && SDL_FindInHashTable(keymap->scancode_to_keycode, (void *)(uintptr_t)key, &value)) {
+                        return (SDL_Keycode)(uintptr_t)value;
+                    }
+                }
+
+                // Shift level 3
+                if (normalized_modstate & SDL_KMOD_MODE) {
+                    const SDL_Keymod shifted_modstate = SDL_KMOD_MODE | caps_mask;
+                    key = ((Uint32)shifted_modstate << 16) | scancode;
+
+                    if (shifted_modstate != normalized_modstate && SDL_FindInHashTable(keymap->scancode_to_keycode, (void *)(uintptr_t)key, &value)) {
+                        return (SDL_Keycode)(uintptr_t)value;
+                    }
+                }
+
+                // Shift level 2
+                if (normalized_modstate & SDL_KMOD_SHIFT) {
+                    const SDL_Keymod shifted_modstate = SDL_KMOD_SHIFT | caps_mask;
+                    key = ((Uint32)shifted_modstate << 16) | scancode;
+
+                    if (shifted_modstate != normalized_modstate && SDL_FindInHashTable(keymap->scancode_to_keycode, (void *)(uintptr_t)key, &value)) {
+                        return (SDL_Keycode)(uintptr_t)value;
+                    }
+                }
+
+                // Shift Level 1 (unmodified)
+                key = ((Uint32)caps_mask << 16) | scancode;
+                if (SDL_FindInHashTable(keymap->scancode_to_keycode, (void *)(uintptr_t)key, &value)) {
+                    return (SDL_Keycode)(uintptr_t)value;
+                }
+
+                // Clear the capslock mask, if set.
+                caps_mask = SDL_KMOD_NONE;
+            }
+        }
     }
-    return keycode;
+
+    return SDL_GetDefaultKeyFromScancode(scancode, modstate);
 }
 
 SDL_Scancode SDL_GetKeymapScancode(SDL_Keymap *keymap, SDL_Keycode keycode, SDL_Keymod *modstate)