From 9ec5dd03ba5c162df36ac55868bb297f426dcd5a Mon Sep 17 00:00:00 2001 From: baketnk Date: Sat, 26 Sep 2026 22:49:52 -0400 Subject: [PATCH] Fix paced delivery stopping after the first 24-codepoint chunk Gamescope rewrites GAMESCOPE_FOCUSED_WINDOW (same window) after an IME commit. The focus guard treated any PropertyNotify on the root focus properties as a focus change, so every paced delivery stopped after its first chunk. Re-read the property and fail only when it names a different window; real X focus moves still fail via FocusOut, and every check still re-validates both properties and X input focus. Diagnosed from delivery-debug.log on Frame: sent offset=0 bytes=24 focus invalid: GAMESCOPE_FOCUSED_WINDOW changed Co-Authored-By: Claude Opus 5.5 --- src/focus_guard.cpp | 14 ++++++++++++-- tests/focus_guard_xcb_test.cpp | 6 ++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/focus_guard.cpp b/src/focus_guard.cpp index e863c21..c8bef14 100644 --- a/src/focus_guard.cpp +++ b/src/focus_guard.cpp @@ -98,8 +98,18 @@ struct FocusGuard::Impl { if (bad) reason = "X error"; if (type == XCB_PROPERTY_NOTIFY) { const auto* e = reinterpret_cast(event); - if (e->window == root && e->atom == active_atom) { bad = true; reason = "_NET_ACTIVE_WINDOW changed"; } - if (e->window == root && e->atom == gamescope_atom) { bad = true; reason = "GAMESCOPE_FOCUSED_WINDOW changed"; } + // Gamescope rewrites its focus properties with the same window + // after IME commits; only a different value is a focus change. + // A real X focus move still reports FocusOut on the target. + xcb_window_t value = XCB_WINDOW_NONE; + if (e->window == root && e->atom == active_atom && + (!property_window(active_atom, XCB_ATOM_WINDOW, value) || value != target)) { + bad = true; reason = "_NET_ACTIVE_WINDOW changed"; + } + if (e->window == root && e->atom == gamescope_atom && + (!property_window(gamescope_atom, XCB_ATOM_CARDINAL, value) || value != target)) { + bad = true; reason = "GAMESCOPE_FOCUSED_WINDOW changed"; + } } else if (type == XCB_FOCUS_OUT) { const auto* e = reinterpret_cast(event); if (e->event == target) { bad = true; reason = "FocusOut on target"; } diff --git a/tests/focus_guard_xcb_test.cpp b/tests/focus_guard_xcb_test.cpp index 60b6207..302ba9d 100644 --- a/tests/focus_guard_xcb_test.cpp +++ b/tests/focus_guard_xcb_test.cpp @@ -50,6 +50,12 @@ int main() { assert(!stable.arm()); FocusGuard new_capture; assert(new_capture.arm() && new_capture.valid()); + // Gamescope rewrites both properties with the same window after IME commits. + xcb_change_property(c, XCB_PROP_MODE_REPLACE, root, active, XCB_ATOM_WINDOW, 32, 1, &a); + xcb_change_property(c, XCB_PROP_MODE_REPLACE, root, gamescope, XCB_ATOM_CARDINAL, 32, 1, &a); + auto sync = xcb_get_input_focus(c); + std::free(xcb_get_input_focus_reply(c, sync, nullptr)); + assert(new_capture.valid()); // same-value rewrite is not a focus change xcb_change_property(c, XCB_PROP_MODE_REPLACE, root, active, XCB_ATOM_WINDOW, 32, 1, &b); auto cookie = xcb_get_input_focus(c); Reply reply(xcb_get_input_focus_reply(c, cookie, nullptr));