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 <noreply@anthropic.com>
This commit is contained in:
baketnkandClaude Opus 5.5 committed 2026-09-26 22:49:52 -04:00
1 parent 045cd4f45f
commit 9ec5dd03ba
2 files changed
+18 -2

No files matched your search

+12 -2
View File
@@ -98,8 +98,18 @@ struct FocusGuard::Impl {
if (bad) reason = "X error";
if (type == XCB_PROPERTY_NOTIFY) {
const auto* e = reinterpret_cast<xcb_property_notify_event_t*>(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<xcb_focus_out_event_t*>(event);
if (e->event == target) { bad = true; reason = "FocusOut on target"; }
+6
View File
@@ -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<xcb_get_input_focus_reply_t> reply(xcb_get_input_focus_reply(c, cookie, nullptr));