Linux and Web on SDL 2, and a Linux window that works with tiling window managers - #91
Merged
Merged
Conversation
On Hyprland the engine's window did whatever the window manager made of it. It was tiled whatever size the game asked for, and floating it (SUPER+T) brought back an 800x600 that no preference named: InitOpenGL created the window from $pref::Video::resolution, which nothing sets before the canvas exists, and a tiling window manager remembers the size a window first appeared at. The window is now created at the size canvas.cs is about to ask for, and $pref::Video::windowedRes says whether that size matters. Empty means it does not: the window opens resizable at defaultResolution and a tiler is free to tile it. A size means exactly that size. The window is created fixed-size, so its minimum and maximum size hints are in place before it first maps, which is what makes Hyprland -- and i3 and sway -- float it. The hints are held for 500 ms and then cleared straight on the X server, so the player can still resize it and no texture is reloaded to do it. Clearing them at once loses a race: Hyprland decides a moment after the map, and a window whose hints are gone by then is tiled. Holding them 100 ms was already enough. Following the window manager took a second X connection. Genuine SDL 1.2 draws into a child of the window the window manager manages, so anything read off SDL_SysWMinfo's window was reading the wrong one; wmwindow is the toplevel. And sdl12-compat, which distributions now ship as SDL 1.2, passes on no X events at all -- not even the resizes while the window manager has the window fullscreen. The back-end now selects structure and property events on the toplevel itself, and settles a resize or a _NET_WM_STATE change the way a drag was already settled. SUPER+F is followed into and out of fullscreen: under sdl12-compat by going fullscreen with the window manager, since a windowed SDL_SetVideoMode there asks to leave again; under genuine SDL 1.2 as a plain resize, because its SDL_FULLSCREEN puts up an override-redirect window of its own that the window manager never sees. canvas.cs now honours $pref::Video::fullScreen on Linux, keyed on $platformUnixType because the web build calls itself x86UNIX too. That is right under sdl12-compat and wrong under genuine SDL 1.2 on Wayland, where the game draws unscaled in a corner. This branch moves the engine to SDL 2 before it merges, and SDL 2 does fullscreen through the window manager everywhere. The editor's preferences leave windowedRes empty, so the editor tiles; new projects keep AppCore's 1024x768 and float at it. Windows and macOS read windowedRes when they create the window too, so both now fall back to defaultResolution for an empty one rather than opening at a placeholder and resizing. Video::getDesktopResolution on Linux also had width and height the wrong way round. Tested on Hyprland against both SDL 1.2s -- a hand-built genuine 1.2.15 and Arch's sdl12-compat 1.2.68 -- with an empty and an explicit windowedRes, SUPER+F in and out, and SUPER+T. Not tested on a stacking desktop, and the Windows and macOS changes have only been compiled by CI. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019qUbGQ6PD6Mqtvnxn67CLp
The Linux back-end was written against SDL 1.2, which now comes in two implementations that disagree about exactly what a window manager cares about. Genuine 1.2.15 draws into a child of its toplevel and goes fullscreen with an override-redirect window of its own; the sdl12-compat shim that distributions ship as SDL 1.2 passes on none of the window manager's events, and not even its resizes while it holds the window fullscreen. This week's tiling work needed a second X connection just to see what Hyprland had done to the window. It now runs on SDL 2.32.10, vendored in engine/lib/sdl from Torque3D's tree (without SDL's 51 MB of tests) and built as a static library, so the engine still ships as one executable. It is the same version Emscripten's port gives the Web build, which moves to SDL 2 next. - One window and one GL context for the life of the process. Resizes, tiling and fullscreen go through SDL_SetWindowSize and desktop SDL_SetWindowFullscreen around the context already there, so the 150 ms settle delay and the texture reload on every mode change are gone, with the second X connection and the check for which SDL 1.2 was linked. - The exact-size rule stays. A window with an explicit windowedRes is created fixed-size and made resizable after 500 ms; the phase-0 prototype showed Hyprland needs the hold (released at 0 ms, the window tiled three runs in three). - SDL 2 never flags a fullscreen the window manager imposes, only the resize, so _NET_WM_STATE is read through SDL's own X display to keep isFullScreen() and $pref::Video::fullScreen true. toggleFullScreen() no longer demands that a fullscreen size be in the resolution list, which refused whatever size a drag or a tiling layout had left. - One SDL_PollEvent loop a frame; keyboard, text and mouse events go to UInputManager::processEvent. - engine/source/platformSDL, shared with the Web back-end: Torque3D's scancode key table, sized to SDL's scancode range (Torque3D's 256 entries let media keys read past the end); modifiers reported per side (Torque3D reports a left Shift as both); key names and bind-by-character through the current layout, Torque3D's a78235dd fix without its detour through key names; the SDL_TEXTINPUT bridge, posting each UTF-16 unit as a character-only KEY_NULL event and decoding the UTF-8 properly (Torque3D copies the bytes into 16-bit slots), with text input switched at the end of the frame as Torque3D switches it; SDL's clipboard (freed, where Torque3D leaks it); and Torque3D's message box, whose Retry button returned OK. - The mouse wheel is SDL_MOUSEWHEEL, because SDL 2 gives buttons 4 and 5 to the side buttons. - x86UNIXMessageBox, the Xlib clipboard and the Xlib keymap reader are deleted. CI and the build scripts install SDL's X11 build headers instead of libsdl1.2-dev; libxext-dev is the one SDL insists on. Tested: 348 unit tests pass, 16 of them new (the key table, modifiers, and UTF-8 decoding including surrogate pairs and malformed bytes). On Hyprland the editor tiles with its own preferences, floats centred at an explicit 1024x768 and stays resizable, and follows SUPER+F in and out, SUPER+T, and a fullscreen start. Typing, the clipboard and alert dialogs were not driven on the desktop. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019qUbGQ6PD6Mqtvnxn67CLp
GuiTextEditCtrl keeps its text as UTF-8 with the caret as a byte offset, but inserted a key's character as string(1, event.ascii): one byte cut from a UTF-16 code unit. 'e'-acute became the lone byte 0xE9, and anything past U+00FF kept only its low byte. A keyboard event's ascii is now treated as one UTF-16 code unit and inserted as its UTF-8, through the engine's own oneUTF32toUTF8 (so a character beyond the BMP pairs across two events and comes out as U+FFFD, as everywhere else in the engine). The pair is combined here rather than by oneUTF16toUTF32, whose pairing omits the 0x10000 offset and reads U+1F600 as U+F600; and oneUTF8toUTF32 is always handed a unitsWalked pointer, because its ASCII fast path writes through it without the NULL check the rest of the function has. The caret, Backspace, Delete, the arrows, Shift-selection, overwrite mode and click positioning all step whole UTF-8 characters, so nothing lands inside one. Both input models work. A key-down carrying its character (Windows, macOS, the SDL 1.2 Linux back-end) is typed as before; a key-down with ascii 0 still does what the key does and types nothing; and a character-only event -- keyCode KEY_NULL, the model the SDL 2 Linux and Web back-ends will use and Windows IME already uses -- goes straight to insertion, never through the Ctrl/Alt shortcut handling. AltGr (left Ctrl + right Alt with a printable character, as Windows reports it) is typed rather than treated as a Ctrl shortcut, and control codes are never inserted, which also stops Ctrl+Shift+letter typing one on Windows. KEY_NULL character events are kept out of the script onKeyDown/onKeyUp callbacks (they have no key name, and asking for one logs an error per character) and out of GuiInputCtrl's key capture. The pure parts are statics -- isTypedCharacter, isCharacterEvent, isAltGrCharacter, composeTypedCharacter, and GuiTextEditSelection's next/previousCharacterStart -- tested in guiTextInputTests.cc along with the editing paths through onKeyDown; the font check is a virtual the test subclass stands in for, since the unit tests cannot load a font. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019qUbGQ6PD6Mqtvnxn67CLp
The Web back-end ran on Emscripten's JavaScript SDL 1.2, which reports keys but not the characters they type. Text came from a US-only table that dropped anything outside printable ASCII, so "café naïve" arrived as "caf nave". The wheel handler was commented out, the canvas never followed the browser, and getDesktopResolution() answered from a 1024x768 desktop made up in platform.js. It now runs on Emscripten's SDL 2 port -- 2.32.10, the version the Linux build vendors -- and shares the Linux build's input: platformSDL's scancode table and SDL_TEXTINPUT bridge, and the same input manager. - One canvas and one WebGL context, made by SDL 2 for the life of the page. LEGACY_GL_EMULATION hooks the context SDL creates through Emscripten's EGL, which renders identically (the port's phase-0 prototype passed 12 of 12 pixel checks), and the build stays on WebGL 1, which the emulation requires. - A page that sizes #canvas with CSS decides its size, and the canvas follows the browser window; otherwise it opens at windowedRes or defaultResolution, as on Linux. The desktop resolution is the real screen. - One SDL_PollEvent loop. The copied event list and its re-entrancy workaround, TORQUE_SETVIDEOMODE and the unused alert helpers are gone, and so are two 200 ms background sleeps that SDL 2's focus-loss events would now wake: in a page, a sleep is a busy-wait. - SDL 2 really hides the pointer, where the JavaScript SDL 1.2 did not, so only Input::setCursorState hides it -- while the canvas draws its own. - Message boxes stay in EmscriptenAlerts.cpp and platform.js: SDL 2 has no SDL_ShowMessageBox in a browser. - Gamma, minimize and the accumulation-buffer attributes are gone as no-ops, and EmscriptenEvents.cpp, which held only stubs, is deleted. Release wasm grows from 3,161,157 to 3,527,632 bytes, 103 KB gzipped; the preloaded .data is unchanged. Tested in headless Chromium through the DevTools protocol, with real key, mouse and wheel events: the editor boots to the Project Manager in Debug and Release; a typed echo(...) runs and "café naïve" arrives intact; three wheel notches scroll a list 90 px; resizing the browser window resizes the canvas and the GUI. The Linux build still builds, and its 372 unit tests pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019qUbGQ6PD6Mqtvnxn67CLp
Two faults stood in front of every message box on the SDL 2 Linux build.
ProcessControlInit ignored SIGCHLD, so that a child the engine started
would not linger as a zombie. That also has the kernel reap every child
the moment it exits, before anything can wait for it -- and SDL shows its
X11 message box from a forked child and waits for it. The wait failed,
and a debug build stopped on SDL's assertion (SDL_x11messagebox.c:857,
rc == pid) and exited with status 42 as soon as a script called
messageBox(). SIGCHLD is now left at the default: everything that starts
a child waits for it, and the one thing that didn't,
Platform::openWebBrowser, now does.
Behind that, SDL's X11 box is drawn in the X server's core fonts and asks
for one 12-point size of them, which a Wayland-first desktop may not
install at all; this Omarchy machine has none. SDL can show a box with
zenity instead -- its Wayland driver does -- but it only turns to another
driver's box before its video has started, never with a window open. So
when SDL_ShowMessageBox fails, sdlMsgBox.cpp runs zenity itself, adapted
from SDL 2.32's SDL_waylandmessagebox.c, and prints the alert only if that
fails too.
openWebBrowser goes through SDL_OpenURL (xdg-open), as Torque3D does, and
still honours $Pref::Unix::WebBrowser, starting that browser from a child
that exits at once so there is nothing to wait for. Every browser it used
to try was started with argv[0] = NULL, which ends the argument list
before the address.
Tested with a Release build on Hyprland: messageBox(..., "OkCancel")
shows the zenity box and Return answers 1; gotoWebPage() with
$Pref::Unix::WebBrowser set to a stand-in script hands it the address;
and no child is left a zombie. A standalone SDL 2.32.10 program shows the
same X11 failure without the engine ("No message system available"), and
"msgbox child process failed" once SIGCHLD is ignored.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019qUbGQ6PD6Mqtvnxn67CLp
Under SDL 2 a key press arrives without its character -- the character follows as an event of its own -- so a text box that took its text from the key press no longer had a reason to consume the key. It passed every key on, and the game's action maps fired while the player typed: q in a chat box typed a q and fired whatever was bound to q. A key that types now belongs to the text box that has the keyboard (letters, digits, space, punctuation and the number pad, alone, with Shift or with AltGr); keys that type nothing and Ctrl or Alt shortcuts go on as before; and a release is never consumed, since a map only fires a release for a press it took. The desktop test that found it also found text input left on after the console closed. Text input was tied to any awake text field rather than to the keyboard: a focused field that went to sleep was never told it had lost the keyboard, and GuiCanvas::setFirstResponder(NULL) -- which popDialog calls when the last dialog goes -- did nothing at all, so the old control kept the canvas's pointer. Text input now follows the keyboard alone, as in Torque3D, which never had the count. Hiding or deactivating a control that holds the keyboard, directly or anywhere above it, lets go through the canvas so the control hears it; sleep and removal let go silently, because the callback runs script mid-pop; and the canvas drops a responder that isn't awake and visible before sending it a key. None of these gaps is new -- on Windows and SDL 1.2 a hidden field still holding the pointer would have been typed into as well -- SDL 2 only made them visible. Tab told the old field it had lost the keyboard a second time, after the new one had taken it, so its AltCommand ran twice and, on SDL, its request to turn text input off came last and the box Tab moved to couldn't type. The extra calls are gone, as they are in Torque3D. A key bound in the GlobalActionMap works while a text box has the keyboard, and types nothing -- Torque3D's rule. The text box leaves it to the map, and on SDL the text queued behind the key is thrown away by turning text input off for a moment (ActionMap::getGlobalMap and isAction find the binding as processButton would). Torque3D also skips every other action map while text input is on; that part isn't copied, because the editor's Ctrl+` console toggle is an ordinary ActionMap. Tested: 383 unit tests pass, 11 of them new (guiTextInputTests.cc and guiFirstResponderTests.cc). On Hyprland, before and after: z pressed with the console closed produced text, and now produces none; q in a Toy Box value field typed a q and fired a q binding, and now only types; Tab then 7 lost the 7, and now types it; a key bound in the GlobalActionMap fires and types nothing. The Web input manager compiles with emcc; the Web build itself was not re-run. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019qUbGQ6PD6Mqtvnxn67CLp
…features SDL's configure leaves out each X11 extension whose headers it can't find, and says nothing: on an Ubuntu box with only libx11-dev and libxext-dev the engine built and ran, but its SDL had no XRandR, Xcursor, XInput2, XFixes or screensaver support, and getDesktopResolution() answered 3840x1080 for two 1920x1080 monitors. The packages were listed in the build scripts and CI, but nothing required them. engine/lib/CMakeLists.txt now looks for every one of those headers before SDL does and stops with the missing packages named, and the commands that install them on Debian/Ubuntu, Arch and Fedora. It uses the result variables SDL's own find_file calls use, so SDL reuses what is found, and one left NOTFOUND is searched for again on the next configure: installing the package and configuring again is enough. Tested: a fresh configure on Ubuntu 22.04 without the five packages stops and names exactly those five. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VQuhage1yEjBVuCFyB5Edm
Two conflicts, both where development and this branch changed the same lines. GuiControl::onSleep keeps development's reset of mFittedWidth and this branch's quiet clearFirstResponder(this), which lets go of the keyboard without running script from the middle of a sleep. CHANGELOG.md keeps both sets of Fixed entries. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VQuhage1yEjBVuCFyB5Edm
The merge of development stored guiControl.cc with LF endings where development stores CRLF, so the whole file -- 3,000 lines -- read as changed against development for the 65 that were. The content is the merge's exactly; only the endings are development's again. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VfbP8wSFrZusjvSHHfv5z5
android-actions/setup-android@v3 installs "tools platform-tools" by default, and the legacy "tools" package is gone from the SDK repository: the action's own sdkmanager call fails with "Failed to find package 'tools'" before the build starts. Nothing in the engine is involved -- the same workflow passed on this branch on 12 September, and this was the repository's first CI run since. The action's v4 installs platform-tools alone (v4.0.2, "Fix for removed tools package"), on node24 and a current cmdline-tools. The NDK and CMake are still installed by the step after it, which needs only sdkmanager on the path. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VQuhage1yEjBVuCFyB5Edm
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This moves the Linux and Web back-ends from SDL 1.2 to SDL 2, and makes the Linux window work with tiling window managers such as Hyprland without breaking stacking desktops.
Engine/lib/sdl, minus SDL's 51 MB of tests. Seeengine/lib/sdl/README-TORQUE2D.md.engine/source/platformSDL: the key table, typed-text bridge, clipboard and message box. It is adapted from Torque3D's, following the aim of keeping the two engines close.sdl12-compatshim distributions ship. They disagreed about exactly what a window manager cares about: windows, events and fullscreen.Commits
221ede88windowedResdecides floating vs tiled; the engine follows the window manager's fullscreen; Linux honours$pref::Video::fullScreen. (On SDL 1.2.)09a71466platformSDL; CI and scripts install SDL's X11 build headers instead oflibsdl1.2-dev.d169ad89KEY_NULLevent (SDL 2).a9bc93586d797bbdSIGCHLDis no longer ignored, and there is azenityfallback where SDL's X11 box has no core fonts.gotoWebPage()goes throughSDL_OpenURL.6d2423f7CHANGELOG.md[Unreleased]has every user-visible change, written for someone building a game.What changes for a game
$pref::Video::windowedReslets a tiling window manager size the window. A set size opens the window floating at exactly that size, still resizable. The editor now leaves it empty; AppCore keeps1024 768. Windows and macOS fall back todefaultResolutionfor an empty value.isFullScreen().GlobalActionMapworks while a text box has the keyboard and types nothing, as in Torque3D.$pref::Input::MouseWheelSpeed(120 by default, as Windows reports it) instead of 10; GUI scrolling is unchanged. The wheel works on the Web for the first time.onLoseFirstResponderandonBlur, as in Torque3D.Testing
6d2423f7(Windows ×4, Linux x86_64, Linux 32-bit, macOS, iOS, Android).café naïveinto the console after choosing a project;Not tested:
tests/run.ps1).Notes for reviewers
engine/lib/sdlis 28 MB of vendored SDL source. Filter it out when reading the diff.Follow-ups, not in this PR
engine/source/string/unicode.cc, worked around rather than fixed.oneUTF16toUTF32drops the 0x10000 offset,oneUTF8toUTF32writes through a NULL out-pointer on ASCII, andisSurrogateRangeexcludes both ends.windowedResrule, the Linux build dependencies, and global key bindings in text boxes.getDoubleClickTimeanswers 3,000,000 ms.🤖 Generated with Claude Code
https://claude.ai/code/session_019qUbGQ6PD6Mqtvnxn67CLp