1
0
Fork 0
mirror of https://github.com/yshui/picom.git synced 2025-04-07 17:44:04 -04:00

wm: wire up the new wm_tree structure to X events

To let it process some events to validate that it is working. The output
of wm_tree is not yet used besides debugging logs.

Note because we don't quite setup event subscription the way wm_tree
expects, there will be some errors logged, which is normal.

Signed-off-by: Yuxuan Shui <yshuiv7@gmail.com>
This commit is contained in:
Yuxuan Shui 2024-06-04 19:39:45 +01:00
parent 5ecac66185
commit c96ca0a40a
No known key found for this signature in database
GPG key ID: D3A4405BE6CC17F4
7 changed files with 420 additions and 29 deletions

View file

@ -203,6 +203,8 @@ static inline void ev_focus_out(session_t *ps, xcb_focus_out_event_t *ev) {
}
static inline void ev_create_notify(session_t *ps, xcb_create_notify_event_t *ev) {
wm_import_incomplete(ps->wm, ev->window, ev->parent);
if (ev->parent == ps->c.screen_info->root) {
wm_stack_add_top(ps->wm, ev->window);
ps->pending_updates = true;
@ -294,6 +296,8 @@ static inline void ev_configure_notify(session_t *ps, xcb_configure_notify_event
}
static inline void ev_destroy_notify(session_t *ps, xcb_destroy_notify_event_t *ev) {
wm_destroy(ps->wm, ev->window);
auto subwin = wm_subwin_find(ps->wm, ev->window);
if (subwin) {
wm_subwin_remove(ps->wm, subwin);
@ -370,8 +374,11 @@ static inline void ev_unmap_notify(session_t *ps, xcb_unmap_notify_event_t *ev)
}
static inline void ev_reparent_notify(session_t *ps, xcb_reparent_notify_event_t *ev) {
log_debug("Window %#010x has new parent: %#010x, override_redirect: %d",
ev->window, ev->parent, ev->override_redirect);
log_debug("Window %#010x has new parent: %#010x, override_redirect: %d, "
"send_event: %#010x",
ev->window, ev->parent, ev->override_redirect, ev->event);
wm_reparent(ps->wm, ev->window, ev->parent);
auto old_toplevel = wm_find_by_client(ps->wm, ev->window);
auto old_w = wm_find(ps->wm, ev->window);

View file

@ -1572,6 +1572,8 @@ static void handle_queued_x_events(EV_P attr_unused, ev_prepare *w, int revents
}
static void handle_new_windows(session_t *ps) {
wm_complete_import(ps->wm, &ps->c, ps->atoms);
list_foreach_safe(struct win, w, wm_stack_end(ps->wm), stack_neighbour) {
if (w->is_new) {
auto new_w = maybe_allocate_managed_win(ps, w);
@ -1633,11 +1635,36 @@ static void handle_pending_updates(EV_P_ struct session *ps, double delta_t) {
// Catching up with X server
handle_queued_x_events(EV_A, &ps->event_check, 0);
if (ps->pending_updates) {
if (ps->pending_updates || wm_has_incomplete_imports(ps->wm) ||
wm_has_tree_changes(ps->wm)) {
log_debug("Delayed handling of events, entering critical section");
// Process new windows, and maybe allocate struct managed_win for them
handle_new_windows(ps);
while (true) {
auto wm_change = wm_dequeue_change(ps->wm);
if (wm_change.type == WM_TREE_CHANGE_NONE) {
break;
}
switch (wm_change.type) {
case WM_TREE_CHANGE_TOPLEVEL_NEW:
log_debug("New window %#010x",
wm_ref_win_id(wm_change.toplevel));
break;
case WM_TREE_CHANGE_TOPLEVEL_KILLED:
log_debug("Destroying window %#010x",
wm_ref_win_id(wm_change.toplevel));
break;
case WM_TREE_CHANGE_CLIENT:
log_debug("Client message for window %#010x changed from "
"%#010x to %#010x",
wm_ref_win_id(wm_change.toplevel),
wm_change.client.old.x, wm_change.client.new_.x);
break;
default: break;
}
}
// Handle screen changes
// This HAS TO be called before refresh_windows, as handle_root_flags
// could call configure_root, which will release images and mark them
@ -2473,6 +2500,9 @@ static session_t *session_init(int argc, char **argv, Display *dpy,
#endif
}
ps->wm = wm_new();
wm_import_incomplete(ps->wm, ps->c.screen_info->root, XCB_NONE);
e = xcb_request_check(ps->c.c, xcb_grab_server_checked(ps->c.c));
if (e) {
log_fatal_x_error(e, "Failed to grab X server");
@ -2500,7 +2530,6 @@ static session_t *session_init(int argc, char **argv, Display *dpy,
ps->server_grabbed = false;
ps->wm = wm_new();
if (query_tree_reply) {
xcb_window_t *children;
int nchildren;

View file

@ -170,8 +170,8 @@ static inline void dynarr_remove_swap_impl(size_t size, void *arr, size_t idx) {
#define dynarr_foreach_rev(arr, i) \
for (typeof(arr)(i) = dynarr_end(arr) - 1; (i) >= (arr); (i)--)
/// Find the index of an element in the array by using trivial comparison, returns -1 if
/// not found.
/// Find the index of the first appearance of an element in the array by using trivial
/// comparison, returns -1 if not found.
#define dynarr_find_pod(arr, needle) \
({ \
ptrdiff_t dynarr_find_ret = -1; \

View file

@ -125,8 +125,7 @@ struct wm_tree_change wm_tree_dequeue_change(struct wm_tree *tree) {
/// Return the next node in the subtree after `node` in a pre-order traversal. Returns
/// NULL if `node` is the last node in the traversal.
static struct wm_tree_node *
wm_tree_next(struct wm_tree_node *node, struct wm_tree_node *subroot) {
struct wm_tree_node *wm_tree_next(struct wm_tree_node *node, struct wm_tree_node *subroot) {
if (!list_is_empty(&node->children)) {
// Descend if there are children
return list_entry(node->children.next, struct wm_tree_node, siblings);

View file

@ -1,16 +1,20 @@
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) Yuxuan Shui
#include <stddef.h>
#include <uthash.h>
#include <xcb/xproto.h>
#include "common.h"
#include "log.h"
#include "utils/dynarr.h"
#include "utils/list.h"
#include "utils/uthash_extra.h"
#include "x.h"
#include "win.h"
#include "wm.h"
#include "wm_internal.h"
struct wm {
/// Current window generation, start from 1. 0 is reserved for using as
@ -39,6 +43,16 @@ struct wm {
/// subsidiary window detection.
xcb_window_t active_leader;
struct subwin *subwins;
struct wm_tree tree;
struct wm_tree_node *root;
/// Incomplete imports. See `wm_import_incomplete` for an explanation.
/// This is a dynarr.
struct wm_tree_node **incompletes;
/// Tree nodes that we have chosen to forget, but we might still receive some
/// events from, we keep them here to ignore those events.
struct wm_tree_node **masked;
};
unsigned int wm_get_window_count(struct wm *wm) {
@ -49,11 +63,41 @@ unsigned int wm_get_window_count(struct wm *wm) {
}
return count;
}
// TODO(yshui): this is a bit weird and I am not decided on it yet myself. Maybe we can
// expose `wm_tree_node` directly. But maybe we want to bundle some additional data with
// it. Anyway, this is probably easy to get rid of if we want to.
/// A wrapper of `wm_tree_node`. This points to the `siblings` `struct list_node` in a
/// `struct wm_tree_node`.
struct wm_ref {
struct list_node inner;
};
static_assert(offsetof(struct wm_ref, inner) == 0, "wm_cursor should be usable as a "
"wm_tree_node");
static_assert(alignof(struct wm_ref) == alignof(struct list_node),
"wm_cursor should have the same alignment as wm_tree_node");
static inline const struct wm_tree_node *to_tree_node(const struct wm_ref *cursor) {
return cursor != NULL ? list_entry(&cursor->inner, struct wm_tree_node, siblings)
: NULL;
}
xcb_window_t wm_ref_win_id(const struct wm_ref *cursor) {
return to_tree_node(cursor)->id.x;
}
struct managed_win *wm_active_win(struct wm *wm) {
return wm->active_win;
}
static ptrdiff_t wm_find_masked(struct wm *wm, xcb_window_t wid) {
dynarr_foreach(wm->masked, m) {
if ((*m)->id.x == wid) {
return m - wm->masked;
}
}
return -1;
}
void wm_set_active_win(struct wm *wm, struct managed_win *w) {
wm->active_win = w;
}
@ -341,6 +385,9 @@ struct wm *wm_new(void) {
auto wm = ccalloc(1, struct wm);
list_init_head(&wm->window_stack);
wm->generation = 1;
wm_tree_init(&wm->tree);
wm->incompletes = dynarr_new(struct wm_tree_node *, 4);
wm->masked = dynarr_new(struct wm_tree_node *, 8);
return wm;
}
@ -361,6 +408,218 @@ void wm_free(struct wm *wm, struct x_connection *c) {
HASH_ITER(hh, wm->subwins, subwin, next_subwin) {
wm_subwin_remove_and_unsubscribe(wm, c, subwin);
}
wm_tree_clear(&wm->tree);
dynarr_free_pod(wm->incompletes);
dynarr_free_pod(wm->masked);
free(wm);
}
void wm_destroy(struct wm *wm, xcb_window_t wid) {
auto masked = wm_find_masked(wm, wid);
if (masked != -1) {
free(wm->masked[masked]);
dynarr_remove_swap(wm->masked, (size_t)masked);
return;
}
struct wm_tree_node *node = wm_tree_find(&wm->tree, wid);
if (!node) {
log_error("Trying to destroy window %#010x, but it's not in the tree. "
"Expect malfunction.",
wid);
return;
}
auto index = dynarr_find_pod(wm->incompletes, node);
if (index != -1) {
dynarr_remove_swap(wm->incompletes, (size_t)index);
}
wm_tree_destroy_window(&wm->tree, node);
}
void wm_reparent(struct wm *wm, xcb_window_t wid, xcb_window_t parent) {
auto window = wm_tree_find(&wm->tree, wid);
auto new_parent = wm_tree_find(&wm->tree, parent);
// We delete the window here if parent is not found, or if the parent is
// an incomplete import. We will rediscover this window later in
// `wm_complete_import`. Keeping it around will only confuse us.
bool should_forget =
new_parent == NULL || dynarr_find_pod(wm->incompletes, new_parent) != -1;
if (window == NULL) {
log_debug("Reparenting window %#010x which is not in our tree. Assuming "
"it came from fog of war.",
wid);
if (!should_forget) {
wm_import_incomplete(wm, wid, parent);
}
return;
}
if (should_forget) {
log_debug("Window %#010x reparented to window %#010x which is "
"%s, forgetting and masking instead.",
window->id.x, parent,
new_parent == NULL ? "not in our tree" : "an incomplete import");
wm_tree_detach(&wm->tree, window);
for (auto curr = window; curr != NULL;) {
auto next = wm_tree_next(curr, window);
HASH_DEL(wm->tree.nodes, curr);
auto incomplete_index = dynarr_find_pod(wm->incompletes, curr);
if (incomplete_index != -1) {
dynarr_remove_swap(wm->incompletes, (size_t)incomplete_index);
// Incomplete imports cannot have children.
assert(list_is_empty(&curr->children));
// Incomplete imports won't have event masks set, so we
// don't need to mask them.
free(curr);
} else {
dynarr_push(wm->masked, curr);
}
curr = next;
}
return;
}
wm_tree_reparent(&wm->tree, window, new_parent);
}
void wm_import_incomplete(struct wm *wm, xcb_window_t wid, xcb_window_t parent) {
auto masked = wm_find_masked(wm, wid);
if (masked != -1) {
// A new window created with the same wid means the window we chose to
// forget has been deleted (without us knowing), and its ID was then
// reused.
free(wm->masked[masked]);
dynarr_remove_swap(wm->masked, (size_t)masked);
}
auto parent_cursor = NULL;
if (parent != XCB_NONE) {
parent_cursor = wm_tree_find(&wm->tree, parent);
if (parent_cursor == NULL) {
log_error("Importing window %#010x, but its parent %#010x is not "
"in our tree, ignoring. Expect malfunction.",
wid, parent);
return;
}
}
log_debug("Importing window %#010x with parent %#010x", wid, parent);
auto new = wm_tree_new_window(&wm->tree, wid, parent_cursor);
dynarr_push(wm->incompletes, new);
if (parent == XCB_NONE) {
BUG_ON(wm->root != NULL); // Can't have more than one root
wm->root = new;
}
}
static const xcb_event_mask_t WM_IMPORT_EV_MASK =
XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY | XCB_EVENT_MASK_PROPERTY_CHANGE;
static void wm_complete_import_single(struct wm *wm, struct x_connection *c,
struct atom *atoms, struct wm_tree_node *node) {
log_debug("Finishing importing window %#010x with parent %#010lx.", node->id.x,
node->parent != NULL ? node->parent->id.x : XCB_NONE);
set_cant_fail_cookie(
c, xcb_change_window_attributes(c->c, node->id.x, XCB_CW_EVENT_MASK,
(const uint32_t[]){WM_IMPORT_EV_MASK}));
if (wid_has_prop(c->c, node->id.x, atoms->aWM_STATE)) {
wm_tree_set_wm_state(&wm->tree, node, true);
}
}
static void wm_complete_import_subtree(struct wm *wm, struct x_connection *c,
struct atom *atoms, struct wm_tree_node *subroot) {
wm_complete_import_single(wm, c, atoms, subroot);
for (auto curr = subroot; curr != NULL; curr = wm_tree_next(curr, subroot)) {
if (!list_is_empty(&curr->children)) {
log_error("Newly imported subtree root at %#010x already has "
"children. "
"Expect malfunction.",
curr->id.x);
}
xcb_query_tree_reply_t *tree = XCB_AWAIT(xcb_query_tree, c->c, curr->id.x);
if (!tree) {
log_error("Disappearing window subtree rooted at %#010x. Expect "
"malfunction.",
curr->id.x);
continue;
}
auto children = xcb_query_tree_children(tree);
auto children_len = xcb_query_tree_children_length(tree);
for (int i = 0; i < children_len; i++) {
// `children` goes from bottom to top, and `wm_tree_new_window`
// puts the new window at the top of the stacking order, which
// means the windows will naturally be in the correct stacking
// order.
auto existing = wm_tree_find(&wm->tree, children[i]);
if (existing != NULL) {
// This should never happen: we haven't subscribed to
// child creation events yet, and any window reparented to
// an incomplete is deleted. report an error and try to
// recover.
auto index = dynarr_find_pod(wm->incompletes, existing);
if (index != -1) {
dynarr_remove_swap(wm->incompletes, (size_t)index);
}
log_error("Window %#010x already exists in the tree, but "
"it appeared again as a child of window "
"%#010x. Deleting the old one, but expect "
"malfunction.",
children[i], curr->id.x);
wm_tree_destroy_window(&wm->tree, existing);
}
existing = wm_tree_new_window(&wm->tree, children[i], curr);
wm_complete_import_single(wm, c, atoms, existing);
}
free(tree);
}
}
void wm_complete_import(struct wm *wm, struct x_connection *c, struct atom *atoms) {
// Unveil the fog of war
dynarr_foreach(wm->masked, m) {
free(*m);
}
dynarr_clear_pod(wm->masked);
while (!dynarr_is_empty(wm->incompletes)) {
auto i = dynarr_pop(wm->incompletes);
// This function modifies `wm->incompletes`, so we can't use
// `dynarr_foreach`.
wm_complete_import_subtree(wm, c, atoms, i);
}
}
bool wm_has_incomplete_imports(const struct wm *wm) {
return !dynarr_is_empty(wm->incompletes);
}
bool wm_has_tree_changes(const struct wm *wm) {
return !list_is_empty(&wm->tree.changes);
}
struct wm_change wm_dequeue_change(struct wm *wm) {
auto tree_change = wm_tree_dequeue_change(&wm->tree);
struct wm_change ret = {
.type = tree_change.type,
.toplevel = NULL,
};
switch (tree_change.type) {
case WM_TREE_CHANGE_CLIENT:
ret.client.old = tree_change.client.old;
ret.client.new_ = tree_change.client.new_;
ret.toplevel = (struct wm_ref *)&tree_change.client.toplevel->siblings;
break;
case WM_TREE_CHANGE_TOPLEVEL_KILLED:
ret.toplevel = (struct wm_ref *)&tree_change.killed->siblings;
break;
case WM_TREE_CHANGE_TOPLEVEL_NEW:
ret.toplevel = (struct wm_ref *)&tree_change.new_->siblings;
break;
default: break;
}
return ret;
}

View file

@ -12,6 +12,9 @@
#pragma once
#include <assert.h>
#include <stdalign.h>
#include <uthash.h>
#include <xcb/xproto.h>
@ -57,6 +60,44 @@ enum wm_tree_change_type {
WM_TREE_CHANGE_NONE,
};
typedef struct wm_treeid {
/// The generation of the window ID. This is used to detect if the window ID is
/// reused. Inherited from the wm_tree at cr
uint64_t gen;
/// The X window ID.
xcb_window_t x;
/// Explicit padding
char padding[4];
} wm_treeid;
static const wm_treeid WM_TREEID_NONE = {.gen = 0, .x = XCB_NONE};
static_assert(sizeof(wm_treeid) == 16, "wm_treeid size is not 16 bytes");
static_assert(alignof(wm_treeid) == 8, "wm_treeid alignment is not 8 bytes");
struct wm_change {
enum wm_tree_change_type type;
/// The toplevel window this change is about. For
/// `WM_TREE_CHANGE_TOPLEVEL_KILLED`, this is the zombie window left in place of
/// the killed toplevel. For `WM_TREE_CHANGE_TOPLEVEL_RESTACKED`, this is NULL.
struct wm_ref *toplevel;
struct {
wm_treeid old;
wm_treeid new_;
} client;
};
/// Reference to a window in the `struct wm`. Most of wm operations operate on wm_refs. If
/// the referenced window is managed, a `struct window` can be retrieved by
/// `wm_ref_deref`.
struct wm_ref;
struct atom;
static inline bool wm_treeid_eq(wm_treeid a, wm_treeid b) {
return a.gen == b.gen && a.x == b.x;
}
struct wm *wm_new(void);
void wm_free(struct wm *wm, struct x_connection *c);
@ -124,3 +165,64 @@ void wm_subwin_remove_and_unsubscribe(struct wm *wm, struct x_connection *c,
/// Remove all subwins associated with a toplevel window
void wm_subwin_remove_and_unsubscribe_for_toplevel(struct wm *wm, struct x_connection *c,
xcb_window_t toplevel);
xcb_window_t attr_pure wm_ref_win_id(const struct wm_ref *cursor);
/// Destroy a window. Children of this window should already have been destroyed. This
/// will cause a `WM_TREE_CHANGE_TOPLEVEL_KILLED` event to be generated, and a zombie
/// window to be placed where the window was.
void wm_destroy(struct wm *wm, xcb_window_t wid);
void wm_reparent(struct wm *wm, xcb_window_t wid, xcb_window_t parent);
/// Create a tree node for `wid`, with `parent` as its parent. The parent must already
/// be in the window tree. This function creates a placeholder tree node, without
/// contacting the X server, thus can be called outside of the X critical section. The
/// expectation is that the caller will later call `wm_complete_import` inside the
/// X critical section to complete the import.
///
/// ## NOTE
///
/// The reason for this complicated dance is because we want to catch all windows ever
/// created on X server's side. For a newly created windows, we will setup a
/// SubstructureNotify event mask to catch any new windows created under it. But between
/// the time we received the creation event and the time we setup the event mask, if any
/// windows were created under the new window, we will miss them. Therefore we have to
/// scan the new windows in X critical section so they won't change as we scan.
///
/// On the other hand, we can't push everything to the X critical section, because
/// updating the window stack requires knowledge of all windows in the stack. Think
/// ConfigureNotify, if we don't know about the `above_sibling` window, we don't know
/// where to put the window. So we need to create an incomplete import first.
///
/// But wait, this is actually way more involved. Because we choose to not set up event
/// masks for incomplete imports (we can also choose otherwise, but that also has its own
/// set of problems), there is a whole subtree of windows we don't know about. And those
/// windows might involve in reparent events. To handle that, we essentially put "fog of
/// war" under any incomplete imports, anything reparented into the fog is lost, and will
/// be rediscovered later during subtree scans. If a window is reparented out of the fog,
/// then it's treated as if a brand new window was created.
///
/// But wait again, there's more. We can delete "lost" windows on our side and unset event
/// masks, but again because this is racy, we might still receive some events for those
/// windows. So we have to keep a list of "lost" windows, and correctly ignore events sent
/// for them. (And since we are actively ignoring events from these windows, we might as
/// well not unset their event masks, saves us a trip to the X server).
///
/// (Now you have a glimpse of how much X11 sucks.)
void wm_import_incomplete(struct wm *wm, xcb_window_t wid, xcb_window_t parent);
/// Check if there are any incomplete imports in the window tree.
bool wm_has_incomplete_imports(const struct wm *wm);
/// Check if there are tree change events
bool wm_has_tree_changes(const struct wm *wm);
/// Complete the previous incomplete imports by querying the X server. This function will
/// recursively import all children of previously created placeholders, and add them to
/// the window tree. This function must be called from the X critical section. This
/// function also subscribes to SubstructureNotify events for all newly imported windows,
/// as well as for the (now completed) placeholder windows.
void wm_complete_import(struct wm *wm, struct x_connection *c, struct atom *atoms);
bool wm_is_wid_masked(struct wm *wm, xcb_window_t wid);
struct wm_change wm_dequeue_change(struct wm *wm);

View file

@ -21,22 +21,6 @@ struct wm_tree {
struct list_node free_changes;
};
typedef struct wm_treeid {
/// The generation of the window ID. This is used to detect if the window ID is
/// reused. Inherited from the wm_tree at cr
uint64_t gen;
/// The X window ID.
xcb_window_t x;
/// Explicit padding
char padding[4];
} wm_treeid;
static const wm_treeid WM_TREEID_NONE = {.gen = 0, .x = XCB_NONE};
static_assert(sizeof(wm_treeid) == 16, "wm_treeid size is not 16 bytes");
static_assert(alignof(wm_treeid) == 8, "wm_treeid alignment is not 8 bytes");
struct wm_tree_node {
UT_hash_handle hh;
@ -84,19 +68,30 @@ struct wm_tree_change {
/// Free all tree nodes and changes, without generating any change events. Used when
/// shutting down.
void wm_tree_clear(struct wm_tree *tree);
struct wm_tree_node *wm_tree_find(struct wm_tree *tree, xcb_window_t id);
struct wm_tree_node *wm_tree_find(const struct wm_tree *tree, xcb_window_t id);
struct wm_tree_node *wm_tree_next(struct wm_tree_node *node, struct wm_tree_node *subroot);
/// Create a new window node in the tree, with X window ID `id`, and parent `parent`. If
/// `parent` is NULL, the new node will be the root window. Only one root window is
/// permitted, and the root window cannot be destroyed once created, until
/// `wm_tree_clear` is called. If `parent` is not NULL, the new node will be put at the
/// top of the stacking order among its siblings.
struct wm_tree_node *
wm_tree_new_window(struct wm_tree *tree, xcb_window_t id, struct wm_tree_node *parent);
void wm_tree_destroy_window(struct wm_tree *tree, struct wm_tree_node *node);
struct wm_tree_node *wm_tree_find_toplevel_for(struct wm_tree_node *node);
/// Detach the subtree rooted at `subroot` from `tree`. The subtree root is removed from
/// its parent, and the disconnected tree nodes won't be able to be found via
/// `wm_tree_find`. Relevant events will be generated.
void wm_tree_detach(struct wm_tree *tree, struct wm_tree_node *subroot);
void wm_tree_reparent(struct wm_tree *tree, struct wm_tree_node *node,
struct wm_tree_node *new_parent);
void wm_tree_move_to_end(struct wm_tree *tree, struct wm_tree_node *node, bool to_bottom);
struct wm_tree_change wm_tree_dequeue_change(struct wm_tree *tree);
void wm_tree_reap_zombie(struct wm_tree_node *zombie);
void wm_tree_set_wm_state(struct wm_tree *tree, struct wm_tree_node *node, bool has_wm_state);
static inline void wm_tree_init(struct wm_tree *tree) {
tree->nodes = NULL;
list_init_head(&tree->changes);
list_init_head(&tree->free_changes);
}
static inline bool wm_treeid_eq(wm_treeid a, wm_treeid b) {
return a.gen == b.gen && a.x == b.x;
}