Skip to content

emscripten: dedupe keyboard event listeners across multiple windows#15511

Open
vittorioromeo wants to merge 1 commit intolibsdl-org:mainfrom
vittorioromeo:fix-emscripten-keyboard-listener-stacking
Open

emscripten: dedupe keyboard event listeners across multiple windows#15511
vittorioromeo wants to merge 1 commit intolibsdl-org:mainfrom
vittorioromeo:fix-emscripten-keyboard-listener-stacking

Conversation

@vittorioromeo
Copy link
Copy Markdown
Contributor

  • I confirm that I am the author of this code and release it to the SDL project under the Zlib license. This contribution does not contain code from other sources, including code generated by a Large Language Model ("AI").

DISCLAIMER: AI assistance was used to identify and understand the bug. The fix was hand-written.

Summary

On Emscripten, every browser keydown produces two SDL_EVENT_KEY_DOWN events per physical keypress when more than one SDL window has been created. The second event carries repeat=true and shares the same timestamp as the first.

The cause is a missing dedupe step in Emscripten_RegisterEventHandlers: each SDL window stacks another listener on the same DOM target.

Reproduction

MCVE: repro_emscripten_keyboard_dup.c

Compile with emcc, linking against SDL3, and run with emrun. Press any key -- prior to this PR, you should see two log lines being printed out (with identical timestamps). After this fix, only one log line should be printed.

Description

Emscripten_RegisterEventHandlers in src/video/emscripten/SDL_emscriptenevents.c is called once per SDL window. Each invocation does:

emscripten_set_keydown_callback(keyElement, data, 0, Emscripten_HandleKey);
emscripten_set_keyup_callback(keyElement, data, 0, Emscripten_HandleKey);
emscripten_set_keypress_callback(keyElement, data, 0, Emscripten_HandleKeyPress);

These funnel into emscripten's JSEvents.registerOrRemoveHandler (src/lib/libhtml5.js):

registerOrRemoveHandler(eventHandler) {
  // ...
  if (eventHandler.callbackfunc) {
    eventHandler.target.addEventListener(...);   // <-- always adds
    JSEvents.eventHandlers.push(eventHandler);   // <-- never dedupes
  } else {
    // Dedup happens ONLY when callback is null (the unregister path)
    for (/* ... */) { _removeHandler(i--); }
  }
}

When two SDL windows share the same keyboard target, the second emscripten_set_keydown_callback(...) does not replace the first -- it appends another addEventListener and another entry in JSEvents.eventHandlers.

Every browser keydown then fires the C-side Emscripten_HandleKey twice in the same JS task, which calls SDL_SendKeyboardKeyAndKeycode twice in a row. Inside SDL_SendKeyboardKeyInternal (src/events/SDL_keyboard.c):

if (down) {
    if (keyboard->keystate[scancode]) {
        // ...
        repeat = true;  // <-- second dispatch sees keystate==true
    }
    // ...
}
keyboard->keystate[scancode] = down;

The same stacking issue affects the JS-side document listener sdlEventHandlerLockKeysCheck.

Fix

Two changes to Emscripten_RegisterEventHandlers, both in src/video/emscripten/SDL_emscriptenevents.c:

  1. Before registering keyboard callbacks, call each setter with NULL first to clear any prior handler on the same target.

  2. Before assigning a new document.sdlEventHandlerLockKeysCheck closure and adding it as a keydown listener on document, remove the previous one if present.

Testing

Manually tested on Chrome via emrun with the attached repro.

Notes

I encountered this issue as part of the development of my VRSFML library, an opinionated C++23 fork of SFML that also targets Emscripten natively.

The fork maintains the same GL context model as upstream SFML: one hidden GL context (shared between all windows), and one GL context per window.

Having a shared GL context is actually nice because many resources such as textures or shaders can be reused between windows. Internally, I keep the same logic also for the Emscripten target, creating a hidden window:

SDL_CreateWindow("", 1, 1, SDL_WINDOW_OPENGL | SDL_WINDOW_HIDDEN);

The presence of this hidden window plus enabling repeated key presses led to the discovery of this bug.

@vittorioromeo
Copy link
Copy Markdown
Contributor Author

This is related to #12512 -- this PR shouldn't make the situation any worse than it is today.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant