Thoughts on configure/ack_configure flow in xdg-surface and layer-surface #3206
Description
configure
/ack_configure
flow is currently used by two objects: xdg_surface
and zwlr_layer_surface_v1
. wlroots implements that flow differently for these objects and provides different APIs for them, which I believe is something that needs to be fixed.
xdg_surface
configure events are delayed and aren't sent immediately. This allows for xdg_surface
extensions/roles to be configured effectively, avoiding sending too many configure
events and sending useless events. The current implementation of this mechanism is imperfect however, as it generates a serial in advance without knowing if that serial will actually be used.
18:41:13 <ifreund> the fact that we might decide to cancel the configure is still a nasty edge case though
18:41:30 <ifreund> because with the current code serials may not actually end up being used
18:42:05 <ifreund> however they are still returned to the compositor, which has no way to know the serial won't be used
...
18:50:56 <vyivel> let's say we call set_fullscreen(false) on a fullscreen view
18:51:19 <vyivel> that's a different state, so it schedules a configure and returns a newly created serial
18:51:40 <vyivel> then we call set_fullscreen(true) without dropping to the event loop
18:51:52 <vyivel> what should it return?
18:52:54 <ifreund> the serial that will be used for the next configure that actually gets sent I believe
18:53:50 <vyivel> assuming we don't configure anything after that, compositor would receive a serial that is never going to be used
18:54:37 <ifreund> it will be used next time the compositor causes any configure to be sent
18:55:02 <ifreund> but I agree this is probably not what the compositor author would expect
On top of that, 0
serial is returned wrongly as an indication of that a configure isn't necessary, even though 0
is a perfectly valid configure serial.
zwlr_layer_surface_v1
configure events are sent immediately on wlr_layer_surface_v1_configure()
. This means that calling
wlr_layer_surface_v1_configure(new_size);
wlr_layer_surface_v1_configure(current_size);
will result in sending two configures, and, as the actual state didn't change, the client can ignore both of them.
Ref #3201 (comment)
For the purposes of unifying the API and avoiding code duplication, I propose introducing wlr_configurable
to deal with this logic.
Draft:
struct wlr_configurable {
struct wl_display *display;
const struct wlr_configurable_impl *impl;
struct wl_list sources; // wlr_configure_source::link
bool force;
struct wl_event_source *idle;
};
struct wlr_configurable_impl {
void (*send_event)(struct wlr_configurable *configurable,
uint32_t serial);
};
struct wlr_configure_source {
struct wl_list link; // wlr_configurable::sources
struct {
// Sends a pointer to serial if must wait
// for an ack, NULL otherwise
struct wl_signal configure; // uint32_t *
} events;
};
struct wlr_configure_source {
void (*needs_configure)(struct wlr_configure_source *source);
};
static void send_configure(void *data) {
struct wlr_configurable *configurable = data;
configurable->idle = NULL;
bool needs_event = false;
uint32_t serial;
if (configurable->force) {
serial = wl_display_next_serial(configurable->display);
needs_event = true;
configurable->force = false;
}
struct wlr_configure_source *source, *tmp;
wl_list_for_each_safe (source, tmp, &configurable->sources, link) {
if (source->impl->needs_configure(source)) {
if (!needs_event) {
serial = wl_display_next_serial(configurable->display);
needs_event = true;
}
// `serial` could be wrapped in a struct here
wlr_signal_emit_safe(&source->events.configure, &serial);
} else {
wlr_signal_emit_safe(&source->events.configure, NULL);
}
wl_list_remove(&source->link);
wl_list_init(&source->link);
}
if (needs_event) {
configurable->impl->send_event(configurable, serial);
}
}
void wlr_configurable_schedule(struct wlr_configurable *configurable,
struct wlr_configure_source *source) {
if (configurable->idle == NULL) {
struct wl_event_loop *loop =
wl_display_get_event_loop(configurable->display);
configurable->idle = wl_event_loop_add_idle(loop,
send_configure, configurable);
}
if (source) {
wl_list_remove(&source->link);
wl_list_insert(&configurable->sources, &source->link);
} else {
// Scheduling a configure without a source is always
// successful, can be used to e.g. respond to set_maximized
configurable->force = true;
}
}
// Example wlr usage
struct wlr_xdg_surface {
/* ... */
struct wlr_configurable configurable;
};
// wlr_xdg_surface.configurable.impl.send_event
static void xdg_surface_configurable_send_event(
struct wlr_configurable *configurable, uint32_t serial) {
struct wlr_xdg_surface *xdg_surface = wl_container_of(
configurable, xdg_surface, configurable);
xdg_surface_send_configure(xdg_surface->resource, serial);
}
struct wlr_xdg_toplevel {
/* ... */
struct wlr_configure_source configure_source;
// Listens to wlr_xdg_toplevel.configure_source.events.configure
struct wl_listener configure;
};
// wlr_xdg_toplevel.configure_source.impl.needs_configure
static void xdg_toplevel_needs_configure(
struct wlr_configure_source *source) {
struct wlr_xdg_toplevel *toplevel = wl_container_of(
source, toplevel, configure_source);
// Compare scheduled and last sent states here
return xdg_toplevel_state_changed(toplevel);
}
// wlr_xdg_toplevel.configure.notify
static void xdg_toplevel_configure_source_handle_configure(
struct wl_listener *listener, void *data) {
struct wlr_xdg_toplevel *toplevel = wl_container_of(
listener, toplevel, configure);
if (data) {
uint32_t serial = *(uint32_t *)data;
// Allocate a new wlr_xdg_toplevel_configure here,
// set its serial to `serial` and wait
xdg_toplevel_configure(toplevel->resource, width, height, states);
} else {
// A configure might or might not be sent, but
// xdg-toplevel state didn't change, so we do nothing
}
}
void wlr_xdg_toplevel_set_maximized(
struct wlr_xdg_toplevel *toplevel, bool maximized) {
toplevel->scheduled.maximized = maximized;
// Could be wrapped in wlr_xdg_toplevel_schedule_configure()
wlr_configurable_schedule(&toplevel->surface->configurable,
&toplevel->configure_source);
}
Acking a configure is done by the wlr_configure_source
implementation by listening to its ack_configure
signal, which could be moved to wlr_configurable
.
A compositor wishing to implement atomic layout updates would do the following:
- Schedule configures for all modified configure sources, mark them as "scheduled to configure"
- Wait for
wlr_configurable.idle
to be handled - Listen to
wlr_configure_source.events.configure
- If a configure source gets NULL serial pointer, mark as "ready" immediately
- Otherwise mark as "configuring"
- Wait for configures to be acked and committed, mark as "ready"
- When all configure sources are "ready", apply the transaction
cc @ifreund
wlroots has migrated to gitlab.freedesktop.org. This issue has been moved to:
https://gitlab.freedesktop.org/wlroots/wlroots/-/issues/3206