Skip to content

Commit fb79f52

Browse files
Ralph Kuepperclaude
andcommitted
fix(linux): honour WM_DELETE_WINDOW so closing the window shuts down cleanly
The X11 backend never put WM_DELETE_WINDOW in the window's WM_PROTOCOLS, so the titlebar ✕ / Alt-F4 / session logout gave the WM no polite way to ask the game to quit. It dropped the X connection instead, and Xlib's default IO-error handler called exit(1) from under the running frame: XIO: fatal IO error 62 (Timer expired) on X server ":0" after 23355 requests (23354 known processed) with 0 events remaining. `windowShouldClose()` never went true, so `closeWindow()` and everything the game does after its loop — audio teardown, save-on-exit — were skipped. Only DestroyNotify was handled, which arrives too late to be useful here. Intern the atoms once at window creation, XSetWMProtocols before mapping, and translate the resulting ClientMessage into `should_close`. Verified with the Bloom Shooter: sending a real WM_DELETE_WINDOW ClientMessage now exits 0 with no XIO error in the log (previously exit 1 + XIO fatal). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 8d6132d commit fb79f52

1 file changed

Lines changed: 38 additions & 0 deletions

File tree

native/linux/src/lib.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,13 @@ mod x11_impl {
9292
static mut WARP_CENTER_X: i32 = 0;
9393
static mut WARP_CENTER_Y: i32 = 0;
9494
static mut RELATIVE_MODE: bool = false;
95+
/// WM_PROTOCOLS / WM_DELETE_WINDOW, interned once in `create_window`.
96+
/// Cached because `poll_events` compares against them every event.
97+
static mut WM_PROTOCOLS: x11::xlib::Atom = 0;
98+
static mut WM_DELETE_WINDOW: x11::xlib::Atom = 0;
99+
100+
pub fn wm_protocols_atom() -> x11::xlib::Atom { unsafe { WM_PROTOCOLS } }
101+
pub fn wm_delete_window_atom() -> x11::xlib::Atom { unsafe { WM_DELETE_WINDOW } }
95102

96103
pub fn set_fullscreen(fullscreen: bool) {
97104
unsafe {
@@ -184,6 +191,20 @@ mod x11_impl {
184191
x11::xlib::ButtonPressMask | x11::xlib::ButtonReleaseMask |
185192
x11::xlib::PointerMotionMask | x11::xlib::StructureNotifyMask);
186193

194+
// Opt into the WM close protocol before mapping, so the titlebar
195+
// ✕ / Alt-F4 arrives as a ClientMessage we can turn into
196+
// `windowShouldClose()` instead of the WM dropping our X
197+
// connection and Xlib exit(1)-ing the game mid-frame.
198+
WM_PROTOCOLS = x11::xlib::XInternAtom(
199+
DISPLAY, b"WM_PROTOCOLS\0".as_ptr() as *const _, 0);
200+
WM_DELETE_WINDOW = x11::xlib::XInternAtom(
201+
DISPLAY, b"WM_DELETE_WINDOW\0".as_ptr() as *const _, 0);
202+
if WM_DELETE_WINDOW != 0 {
203+
let mut protocols = [WM_DELETE_WINDOW];
204+
x11::xlib::XSetWMProtocols(
205+
DISPLAY, X11_WINDOW, protocols.as_mut_ptr(), 1);
206+
}
207+
187208
if !headless {
188209
x11::xlib::XMapWindow(DISPLAY, X11_WINDOW);
189210
}
@@ -489,6 +510,23 @@ mod x11_impl {
489510
}
490511
}
491512
}
513+
x11::xlib::ClientMessage => {
514+
// The WM asking us to close (titlebar ✕, Alt-F4, or a
515+
// session logout). Without WM_DELETE_WINDOW in our
516+
// WM_PROTOCOLS the WM instead destroys the connection,
517+
// and Xlib's default IO-error handler calls exit(1)
518+
// from under the game — `windowShouldClose()` never
519+
// goes true, so `closeWindow()` and every shutdown
520+
// path after it (audio teardown, save-on-exit) are
521+
// skipped. Observed as:
522+
// XIO: fatal IO error 62 (Timer expired)
523+
if event.client_message.message_type == wm_protocols_atom()
524+
&& event.client_message.data.get_long(0)
525+
== wm_delete_window_atom() as i64
526+
{
527+
engine().should_close = true;
528+
}
529+
}
492530
x11::xlib::DestroyNotify => {
493531
engine().should_close = true;
494532
}

0 commit comments

Comments
 (0)