From 213751e5efa4fac70c36388ff8ef5e96bb337048 Mon Sep 17 00:00:00 2001 From: foxnne Date: Fri, 9 Oct 2026 12:37:27 -0500 Subject: [PATCH 1/2] test: a fizzy suite for the Wayland driver on an older libxkbcommon testautomation_fizzy_xkb.c checks that SDL's Wayland driver loads, and builds its keymap's modifier levels right, wherever libxkbcommon is older than the headers SDL was built against. xkb_waylandDriverLoads restarts the video subsystem with the Wayland driver whenever WAYLAND_DISPLAY is set and checks that it started. Built against libxkbcommon 1.10 or newer, SDL required xkb_keymap_mod_get_mask() (new in 1.10) at load time, so on Ubuntu 24.04 (libxkbcommon 1.6) the driver reported itself unavailable. xkb_keymapModifierLevels checks the masks the keymap handler computed, through the SDL keymap they built: each level of a key is entered under the SDL modifiers its xkb mask translates to, so a wrong shift, caps lock or level 3 mask drops or misfiles the level. With FIZZY_TEST_XKB_LAYOUT=de and a compositor keyboard on the German layout it checks Shift+2 = '"', Caps+Q = 'Q', AltGr+Q = '@' and the like, and that no modifier is held. Without the variable it is skipped. fizzyedit/sdl_zig runs it under weston's X11 backend in Xvfb, on Ubuntu 24.04 and 26.04. Part of fizzyedit/fizzy#280, for fizzyedit/fizzy#289. Co-Authored-By: Claude Opus 5.5 --- test/testautomation_fizzy.h | 1 + test/testautomation_fizzy_xkb.c | 234 ++++++++++++++++++++++++++++++++ test/testfizzy.c | 1 + 3 files changed, 236 insertions(+) create mode 100644 test/testautomation_fizzy_xkb.c diff --git a/test/testautomation_fizzy.h b/test/testautomation_fizzy.h index bd8bb86fe0e0a..6f9d7c696e432 100644 --- a/test/testautomation_fizzy.h +++ b/test/testautomation_fizzy.h @@ -13,6 +13,7 @@ /* Test collections */ extern SDLTest_TestSuiteReference fizzyGPUTestSuite; +extern SDLTest_TestSuiteReference fizzyXkbTestSuite; /* Comma-separated GPU drivers ("metal,vulkan") a test must find: when one of * them is missing the test fails instead of being skipped. Set by testfizzy's diff --git a/test/testautomation_fizzy_xkb.c b/test/testautomation_fizzy_xkb.c new file mode 100644 index 0000000000000..a99226882198a --- /dev/null +++ b/test/testautomation_fizzy_xkb.c @@ -0,0 +1,234 @@ +/** + * Wayland keyboard test suite for fizzy's patches + * + * "Wayland: xkb_keymap_mod_get_mask() is optional at runtime": libxkbcommon is + * loaded at runtime, but a build against libxkbcommon 1.10 or newer required + * xkb_keymap_mod_get_mask() (new in 1.10), so the whole Wayland driver failed + * to load where the system's libxkbcommon is older (Ubuntu 24.04 has 1.6). + * + * xkb_waylandDriverLoads starts the Wayland driver whenever there is a + * compositor to connect to (WAYLAND_DISPLAY is set). xkb_keymapModifierLevels + * checks the modifier masks the keymap handler computed, through the SDL keymap + * they built: each level of a key is entered under the SDL modifiers its xkb + * modifier mask translates to, so a wrong mask drops or misfiles the level. It + * needs a compositor with a keyboard and the layout named by the + * FIZZY_TEST_XKB_LAYOUT environment variable; only "de" is known, and without + * the variable the test is skipped. + */ +#include +#include +#include "testautomation_fizzy.h" + +/* Private helpers */ + +static bool VideoDriverIsCompiled(const char *name) +{ + int i; + + for (i = 0; i < SDL_GetNumVideoDrivers(); ++i) { + if (SDL_strcmp(SDL_GetVideoDriver(i), name) == 0) { + return true; + } + } + return false; +} + +/* How many times the runner had initialized the video subsystem, and with + * which driver hint */ +static int videoRefCount = 0; +static char *videoDriverHint = NULL; + +/** + * Restarts the video subsystem with the Wayland driver. The runner initializes + * video more than once, so it is quit until it is down. The hint overrides + * SDL_VIDEO_DRIVER from the environment, which the runner may have started + * with another driver. + */ +static bool StartWaylandVideo(void) +{ + bool result; + + SDL_free(videoDriverHint); + videoDriverHint = SDL_GetHint(SDL_HINT_VIDEO_DRIVER) ? SDL_strdup(SDL_GetHint(SDL_HINT_VIDEO_DRIVER)) : NULL; + videoRefCount = 0; + while (SDL_WasInit(SDL_INIT_VIDEO) && videoRefCount < 255) { + SDL_QuitSubSystem(SDL_INIT_VIDEO); + ++videoRefCount; + } + SDL_SetHintWithPriority(SDL_HINT_VIDEO_DRIVER, "wayland", SDL_HINT_OVERRIDE); + result = SDL_InitSubSystem(SDL_INIT_VIDEO); + SDLTest_AssertPass("Call to SDL_InitSubSystem(SDL_INIT_VIDEO) with the 'wayland' driver"); + SDLTest_AssertCheck(result, "Validate that the Wayland video driver started, got error: %s", result ? "none" : SDL_GetError()); + if (result) { + const char *driver = SDL_GetCurrentVideoDriver(); + SDLTest_AssertCheck(driver && SDL_strcmp(driver, "wayland") == 0, "Validate the current video driver, expected: wayland, got: %s", driver ? driver : "(null)"); + } + return result; +} + +/** + * Puts back the video driver the runner started with, as many times over. + */ +static void RestoreVideo(void) +{ + int i; + + while (SDL_WasInit(SDL_INIT_VIDEO)) { + SDL_QuitSubSystem(SDL_INIT_VIDEO); + } + SDL_ResetHint(SDL_HINT_VIDEO_DRIVER); + if (videoDriverHint) { + SDL_SetHint(SDL_HINT_VIDEO_DRIVER, videoDriverHint); + SDL_free(videoDriverHint); + videoDriverHint = NULL; + } + for (i = 0; i < videoRefCount; ++i) { + if (!SDL_InitSubSystem(SDL_INIT_VIDEO)) { + SDLTest_LogError("Couldn't restart the video subsystem: %s", SDL_GetError()); + break; + } + } +} + +static void PumpEventsFor(Uint64 ms) +{ + const Uint64 end = SDL_GetTicks() + ms; + SDL_Event event; + + while (SDL_GetTicks() < end) { + while (SDL_PollEvent(&event)) { + } + SDL_Delay(10); + } +} + +static void CheckKey(SDL_Scancode scancode, SDL_Keymod modstate, SDL_Keycode expected, const char *why) +{ + const SDL_Keycode key = SDL_GetKeyFromScancode(scancode, modstate, false); + + SDLTest_AssertCheck(key == expected, "Validate %s with modifiers 0x%.4x (%s), expected: 0x%" SDL_PRIx32 ", got: 0x%" SDL_PRIx32, + SDL_GetScancodeName(scancode), (unsigned int)modstate, why, (Uint32)expected, (Uint32)key); +} + +/* Test case functions */ + +/** + * Starts the Wayland video driver whenever a compositor is there to connect to. + */ +static int SDLCALL xkb_waylandDriverLoads(void *arg) +{ + if (!VideoDriverIsCompiled("wayland")) { + SDLTest_Log("The Wayland video driver isn't compiled in, skipping test"); + return TEST_SKIPPED; + } + if (!SDL_getenv("WAYLAND_DISPLAY")) { + SDLTest_Log("WAYLAND_DISPLAY isn't set, so there is no compositor to connect to, skipping test"); + return TEST_SKIPPED; + } + + StartWaylandVideo(); + RestoreVideo(); + return TEST_COMPLETED; +} + +/** + * Checks the keymap the Wayland driver built from the compositor's, for the + * levels that depend on the shift, caps lock and level 3 masks. + */ +static int SDLCALL xkb_keymapModifierLevels(void *arg) +{ + const char *layout = SDL_getenv("FIZZY_TEST_XKB_LAYOUT"); + SDL_Window *window; + Uint64 deadline; + SDL_Keymod modstate; + + if (!layout) { + SDLTest_Log("FIZZY_TEST_XKB_LAYOUT isn't set, skipping test"); + return TEST_SKIPPED; + } + if (SDL_strcmp(layout, "de") != 0) { + SDLTest_Log("No expectations for the keyboard layout '%s', skipping test", layout); + return TEST_SKIPPED; + } + if (!VideoDriverIsCompiled("wayland")) { + SDLTest_Log("The Wayland video driver isn't compiled in, skipping test"); + return TEST_SKIPPED; + } + + if (!StartWaylandVideo()) { + RestoreVideo(); + return TEST_COMPLETED; + } + + window = SDL_CreateWindow("testautomation_fizzy_xkb", 320, 240, 0); + SDLTest_AssertPass("Call to SDL_CreateWindow('Title',320,240,0)"); + SDLTest_AssertCheck(window != NULL, "Validate that returned window is not NULL, got error: %s", window ? "none" : SDL_GetError()); + if (!window) { + RestoreVideo(); + return TEST_COMPLETED; + } + + /* The compositor sends the keymap as soon as the seat's keyboard is bound */ + deadline = SDL_GetTicks() + 3000; + while (!SDL_HasKeyboard() && SDL_GetTicks() < deadline) { + PumpEventsFor(10); + } + SDLTest_AssertCheck(SDL_HasKeyboard(), "Validate that the compositor's seat has a keyboard"); + PumpEventsFor(250); + + /* Shift (level 2) */ + CheckKey(SDL_SCANCODE_Y, SDL_KMOD_NONE, 'z', "the compositor's keymap is in use"); + CheckKey(SDL_SCANCODE_2, SDL_KMOD_NONE, '2', "level 1"); + CheckKey(SDL_SCANCODE_2, SDL_KMOD_LSHIFT, '"', "the shift mask"); + CheckKey(SDL_SCANCODE_3, SDL_KMOD_RSHIFT, 0xa7 /* section sign */, "the shift mask"); + + /* Caps lock (level 2 of an alphabetic key only) */ + CheckKey(SDL_SCANCODE_Q, SDL_KMOD_CAPS, 'Q', "the caps lock mask"); + CheckKey(SDL_SCANCODE_2, SDL_KMOD_CAPS, '2', "the caps lock mask"); + + /* AltGr (level 3): LevelThree, which the standard keymaps put on Mod5 */ + CheckKey(SDL_SCANCODE_Q, SDL_KMOD_MODE, '@', "the level 3 mask"); + CheckKey(SDL_SCANCODE_E, SDL_KMOD_MODE, 0x20ac /* euro sign */, "the level 3 mask"); + CheckKey(SDL_SCANCODE_7, SDL_KMOD_MODE, '{', "the level 3 mask"); + + /* Nothing is held down. Whether the window has keyboard focus, and so got + * the compositor's modifier state, depends on the compositor. */ + deadline = SDL_GetTicks() + 2000; + while (SDL_GetKeyboardFocus() != window && SDL_GetTicks() < deadline) { + PumpEventsFor(10); + } + modstate = SDL_GetModState(); + SDLTest_Log("Keyboard focus: %s, modifier state: 0x%.4x", SDL_GetKeyboardFocus() == window ? "yes" : "no", (unsigned int)modstate); + SDLTest_AssertCheck((modstate & (SDL_KMOD_SHIFT | SDL_KMOD_CTRL | SDL_KMOD_ALT | SDL_KMOD_GUI | SDL_KMOD_MODE | SDL_KMOD_LEVEL5)) == 0, + "Validate that no modifier is held, got modifier state: 0x%.4x", (unsigned int)modstate); + + SDL_DestroyWindow(window); + RestoreVideo(); + return TEST_COMPLETED; +} + +/* ================= Test References ================== */ + +/* Wayland keyboard test cases */ +static const SDLTest_TestCaseReference xkbTestWaylandDriverLoads = { + xkb_waylandDriverLoads, "xkb_waylandDriverLoads", "Starts the Wayland video driver when a compositor is there", TEST_ENABLED +}; + +static const SDLTest_TestCaseReference xkbTestKeymapModifierLevels = { + xkb_keymapModifierLevels, "xkb_keymapModifierLevels", "Checks the shift, caps lock and level 3 levels of the compositor's keymap", TEST_ENABLED +}; + +/* Sequence of Wayland keyboard test cases */ +static const SDLTest_TestCaseReference *xkbTests[] = { + &xkbTestWaylandDriverLoads, + &xkbTestKeymapModifierLevels, + NULL +}; + +/* Wayland keyboard test suite (global) */ +SDLTest_TestSuiteReference fizzyXkbTestSuite = { + "FizzyXkb", + NULL, + xkbTests, + NULL +}; diff --git a/test/testfizzy.c b/test/testfizzy.c index 42eb9bf3d93bd..411711d3f787f 100644 --- a/test/testfizzy.c +++ b/test/testfizzy.c @@ -28,6 +28,7 @@ static SDLTest_TestSuiteRunner *runner; /* All test suites */ static SDLTest_TestSuiteReference *testSuites[] = { &fizzyGPUTestSuite, + &fizzyXkbTestSuite, NULL }; From 36dda37b20cb70ce0feb79ba1340858e9cd41ac5 Mon Sep 17 00:00:00 2001 From: foxnne Date: Fri, 9 Oct 2026 12:47:40 -0500 Subject: [PATCH 2/2] wayland: Make xkb_keymap_mod_get_mask() optional at runtime Built against libxkbcommon 1.10 or newer, the Wayland driver required xkb_keymap_mod_get_mask() (new in 1.10) when it loaded libxkbcommon, so where the system's libxkbcommon is older the whole driver reported itself unavailable and SDL fell back to X11. libxkbcommon is loaded at runtime so that one binary runs across distributions, but Ubuntu 24.04 ships 1.6 and Debian 12 1.5. It only bites builds made against newer headers than the machines they run on, as fizzyedit/sdl_zig's are (1.13); distribution packages build against their own. The symbol is now SDL_WAYLAND_SYM_OPT, and the keymap handler uses it when it loaded, and otherwise falls back to the masks from xkb_keymap_mod_get_index(), which was already the path for builds against older headers. Both paths are compiled whenever the headers are 1.10 or newer. The WAYLAND_xkb_* names are function pointers in static builds too, so the pointer check needs no #ifdef. The fallback is the same as the 1.10 path for the standard keymaps: xkeyboard-config maps Alt to Mod1, Super to Mod4, NumLock to Mod2, LevelThree to Mod5 and LevelFive to Mod3. It differs where a keymap maps a virtual modifier elsewhere or to several real modifiers, and for LevelFive when nothing maps it (1.10 gives 0, the fallback Mod3), as it always has for builds against older headers. Test: test/testautomation_fizzy_xkb.c (fizzyedit/sdl_zig's test-fizzy-xkb job, Ubuntu 24.04 and 26.04). On 24.04 without this change the driver doesn't load. Upstream: not yet proposed; upstream main and release-3.4.x have the same code. Fixes fizzyedit/fizzy#289. Part of fizzyedit/fizzy#280. Co-Authored-By: Claude Opus 5.5 --- src/video/wayland/SDL_waylandevents.c | 44 +++++++++++++++------------ src/video/wayland/SDL_waylandsym.h | 3 +- 2 files changed, 27 insertions(+), 20 deletions(-) diff --git a/src/video/wayland/SDL_waylandevents.c b/src/video/wayland/SDL_waylandevents.c index 5970b18db31e5..5e27aa9fcc51a 100644 --- a/src/video/wayland/SDL_waylandevents.c +++ b/src/video/wayland/SDL_waylandevents.c @@ -1682,28 +1682,34 @@ static void keyboard_handle_keymap(void *data, struct wl_keyboard *keyboard, seat->keyboard.xkb.num_layouts = 0; #if SDL_XKBCOMMON_CHECK_VERSION(1, 10, 0) - seat->keyboard.xkb.shift_mask = WAYLAND_xkb_keymap_mod_get_mask(seat->keyboard.xkb.keymap, XKB_MOD_NAME_SHIFT); - seat->keyboard.xkb.ctrl_mask = WAYLAND_xkb_keymap_mod_get_mask(seat->keyboard.xkb.keymap, XKB_MOD_NAME_CTRL); - seat->keyboard.xkb.alt_mask = WAYLAND_xkb_keymap_mod_get_mask(seat->keyboard.xkb.keymap, XKB_VMOD_NAME_ALT); - seat->keyboard.xkb.gui_mask = WAYLAND_xkb_keymap_mod_get_mask(seat->keyboard.xkb.keymap, XKB_VMOD_NAME_SUPER); - seat->keyboard.xkb.level3_mask = WAYLAND_xkb_keymap_mod_get_mask(seat->keyboard.xkb.keymap, XKB_VMOD_NAME_LEVEL3); - seat->keyboard.xkb.level5_mask = WAYLAND_xkb_keymap_mod_get_mask(seat->keyboard.xkb.keymap, XKB_VMOD_NAME_LEVEL5); - seat->keyboard.xkb.num_mask = WAYLAND_xkb_keymap_mod_get_mask(seat->keyboard.xkb.keymap, XKB_VMOD_NAME_NUM); - seat->keyboard.xkb.caps_mask = WAYLAND_xkb_keymap_mod_get_mask(seat->keyboard.xkb.keymap, XKB_MOD_NAME_CAPS); -#else + /* The libxkbcommon loaded at runtime may be older than the headers, and lack + * xkb_keymap_mod_get_mask(), so fall back to the modifier indices without it. + */ + if (WAYLAND_xkb_keymap_mod_get_mask) { + seat->keyboard.xkb.shift_mask = WAYLAND_xkb_keymap_mod_get_mask(seat->keyboard.xkb.keymap, XKB_MOD_NAME_SHIFT); + seat->keyboard.xkb.ctrl_mask = WAYLAND_xkb_keymap_mod_get_mask(seat->keyboard.xkb.keymap, XKB_MOD_NAME_CTRL); + seat->keyboard.xkb.alt_mask = WAYLAND_xkb_keymap_mod_get_mask(seat->keyboard.xkb.keymap, XKB_VMOD_NAME_ALT); + seat->keyboard.xkb.gui_mask = WAYLAND_xkb_keymap_mod_get_mask(seat->keyboard.xkb.keymap, XKB_VMOD_NAME_SUPER); + seat->keyboard.xkb.level3_mask = WAYLAND_xkb_keymap_mod_get_mask(seat->keyboard.xkb.keymap, XKB_VMOD_NAME_LEVEL3); + seat->keyboard.xkb.level5_mask = WAYLAND_xkb_keymap_mod_get_mask(seat->keyboard.xkb.keymap, XKB_VMOD_NAME_LEVEL5); + seat->keyboard.xkb.num_mask = WAYLAND_xkb_keymap_mod_get_mask(seat->keyboard.xkb.keymap, XKB_VMOD_NAME_NUM); + seat->keyboard.xkb.caps_mask = WAYLAND_xkb_keymap_mod_get_mask(seat->keyboard.xkb.keymap, XKB_MOD_NAME_CAPS); + } else +#endif + { #define GET_MOD_INDEX(mod) \ WAYLAND_xkb_keymap_mod_get_index(seat->keyboard.xkb.keymap, XKB_MOD_NAME_##mod) - seat->keyboard.xkb.shift_mask = 1 << GET_MOD_INDEX(SHIFT); - seat->keyboard.xkb.ctrl_mask = 1 << GET_MOD_INDEX(CTRL); - seat->keyboard.xkb.alt_mask = 1 << GET_MOD_INDEX(ALT); - seat->keyboard.xkb.gui_mask = 1 << GET_MOD_INDEX(LOGO); - // Note: This is correct: Mod3 is typically level 5 shift, and Mod5 is typically level 3 shift. - seat->keyboard.xkb.level3_mask = 1 << GET_MOD_INDEX(MOD5); - seat->keyboard.xkb.level5_mask = 1 << GET_MOD_INDEX(MOD3); - seat->keyboard.xkb.num_mask = 1 << GET_MOD_INDEX(NUM); - seat->keyboard.xkb.caps_mask = 1 << GET_MOD_INDEX(CAPS); + seat->keyboard.xkb.shift_mask = 1 << GET_MOD_INDEX(SHIFT); + seat->keyboard.xkb.ctrl_mask = 1 << GET_MOD_INDEX(CTRL); + seat->keyboard.xkb.alt_mask = 1 << GET_MOD_INDEX(ALT); + seat->keyboard.xkb.gui_mask = 1 << GET_MOD_INDEX(LOGO); + // Note: This is correct: Mod3 is typically level 5 shift, and Mod5 is typically level 3 shift. + seat->keyboard.xkb.level3_mask = 1 << GET_MOD_INDEX(MOD5); + seat->keyboard.xkb.level5_mask = 1 << GET_MOD_INDEX(MOD3); + seat->keyboard.xkb.num_mask = 1 << GET_MOD_INDEX(NUM); + seat->keyboard.xkb.caps_mask = 1 << GET_MOD_INDEX(CAPS); #undef GET_MOD_INDEX -#endif + } if (seat->keyboard.xkb.state != NULL) { /* if there's already a state, throw it away rather than leaking it before diff --git a/src/video/wayland/SDL_waylandsym.h b/src/video/wayland/SDL_waylandsym.h index 4628f361aea57..a639622bb6f58 100644 --- a/src/video/wayland/SDL_waylandsym.h +++ b/src/video/wayland/SDL_waylandsym.h @@ -169,7 +169,8 @@ SDL_WAYLAND_SYM(xkb_level_index_t, xkb_state_key_get_level, (struct xkb_state *, #endif #if SDL_XKBCOMMON_CHECK_VERSION(1, 10, 0) -SDL_WAYLAND_SYM(xkb_mod_mask_t, xkb_keymap_mod_get_mask, (struct xkb_keymap *, const char *)) +// Optional, as the libxkbcommon loaded at runtime may be older than the headers. +SDL_WAYLAND_SYM_OPT(xkb_mod_mask_t, xkb_keymap_mod_get_mask, (struct xkb_keymap *, const char *)) #endif #ifdef HAVE_LIBDECOR_H