Compare commits

...

12 commits

Author SHA1 Message Date
Daniel Kondor
15382861f3 Merge branch 'foreign_toplevel_appmenu' into '0.18'
Draft: wlr-foreign-toplevel-management: add support for relaying information about global menus

See merge request wlroots/wlroots!4986
2025-10-26 08:23:42 +00:00
Simon Ser
f0ae9dffdb build: bump version to 0.18.3 2025-10-23 12:00:25 +02:00
Simon Ser
1cd9d3bec5 ci: fix VKMS lookup after faux bus migration
VKMS has been migrated to the new faux bus. This causes breakage
in CI, because we used the platform bus to find the right device.

udev hasn't been updated yet to support the faux bus, so just use
sysfs instead.

(cherry picked from commit 03e7966650)
2025-10-21 21:31:04 -04:00
DreamMaoMao
de06f60a29 render/pass: Ensure the precision is consistent during comparison
(cherry picked from commit a08acfcee0)
2025-10-21 22:25:46 +00:00
Alistair Buxton
d8f110c5bc Fix Meson version required for C23 support
Attempting to build with Meson 1.3.2 (current version in Ubuntu 24.04 LTS) gives the following error:

    meson.build:1:0: ERROR: Unknown C std ['c23'].

This is because C23 support was not added until Meson 1.4.0.

See:

https://github.com/mesonbuild/meson/blob/1.3.2/mesonbuild/compilers/c.py#L59
https://github.com/mesonbuild/meson/blob/1.4.0/mesonbuild/compilers/c.py#L49
(cherry picked from commit 71cc47b859)
2025-10-21 22:25:46 +00:00
M Stoeckl
f02d3ef28b wlr_cursor: use default shape if requested shape missing
(cherry picked from commit b97106ddcb)
2025-10-21 22:25:46 +00:00
Simon Ser
38f18cd684 backend/drm: fix enabling an output with a custom mode set
Since 5567aefb1c ("backend/drm: Don't add pollute fixed modes
list with custom modes"), when a custom mode is set on an output,
current_mode will be NULL.

Instead of checking current_mode, check width/height.

Closes: https://gitlab.freedesktop.org/wlroots/wlroots/-/issues/3946
(cherry picked from commit 94cb8e2bc7)
2025-10-21 22:25:46 +00:00
Simon Ser
bcba7cb12b backend/drm: handle custom modes in connect_drm_connector()
On startup, some connectors might be already lit up with a custom
mode. We weren't correctly populating the current output state in
this case.

(cherry picked from commit ca1f9f86e6)
2025-10-21 22:25:46 +00:00
liupeng
1a6702938d screencopy-v1: drop output_enable listener
(cherry picked from commit 31f9d6bb97)
2025-10-21 22:25:46 +00:00
Kenny Levinsen
f57e7e40d3 scene/surface: Do not use buffer dimensions for clip
The surface's buffer dimensions were used to scale the clip's x/y
offset. If a surface had a larger buffer than src_box, the calculations
to scale the x/y portion of the clip would be incorrect, yielding
graphical glitches.

This was noticed with Chromium in sway, which during resize uses a
viewport with a src_box to avoid immediate buffer reallocation. While
the viewport was in use, the surface would be shifted so that too much
content was cropped in the upper left, and damage glitching was visible
in the lower right.

Use the buffer source box dimensions instead.

(cherry picked from commit dc7dba8b1f)
2025-10-21 22:25:46 +00:00
Kondor Dániel
34d780bf75 wlr-foreign-toplevel-management: add support for relaying DBus annotation 2025-02-16 14:21:09 +01:00
Kondor Dániel
52dac9bd09 wlr-foreign-toplevel-management: update protocol 2025-02-16 14:21:03 +01:00
11 changed files with 276 additions and 46 deletions

View file

@ -41,9 +41,10 @@ tasks:
cd wlroots/build-gcc/tinywl
sudo modprobe vkms
udevadm settle
card="/dev/dri/$(ls /sys/devices/faux/vkms/drm/ | grep ^card)"
export WLR_BACKENDS=drm
export WLR_RENDERER=pixman
export WLR_DRM_DEVICES=/dev/dri/by-path/platform-vkms-card
export WLR_DRM_DEVICES="$card"
export UBSAN_OPTIONS=halt_on_error=1
sudo chmod ugo+rw /dev/dri/by-path/platform-vkms-card
sudo chmod ugo+rw "$card"
sudo -E seatd-launch -- ./tinywl -s 'kill $PPID' || [ $? = 143 ]

View file

@ -796,13 +796,12 @@ static bool drm_connector_prepare(struct wlr_drm_connector_state *conn_state, bo
return false;
}
if ((state->committed & WLR_OUTPUT_STATE_ENABLED) && state->enabled) {
if (output->current_mode == NULL &&
!(state->committed & WLR_OUTPUT_STATE_MODE)) {
wlr_drm_conn_log(conn, WLR_DEBUG,
"Can't enable an output without a mode");
return false;
}
if ((state->committed & WLR_OUTPUT_STATE_ENABLED) && state->enabled &&
output->width == 0 && output->height == 0 &&
!(state->committed & WLR_OUTPUT_STATE_MODE)) {
wlr_drm_conn_log(conn, WLR_DEBUG,
"Can't enable an output without a mode");
return false;
}
if ((state->committed & WLR_OUTPUT_STATE_ADAPTIVE_SYNC_ENABLED) &&
@ -1582,6 +1581,7 @@ static bool connect_drm_connector(struct wlr_drm_connector *wlr_conn,
wlr_log(WLR_INFO, "Detected modes:");
bool found_current_mode = false;
for (int i = 0; i < drm_conn->count_modes; ++i) {
if (drm_conn->modes[i].flags & DRM_MODE_FLAG_INTERLACE) {
continue;
@ -1600,14 +1600,7 @@ static bool connect_drm_connector(struct wlr_drm_connector *wlr_conn,
if (current_modeinfo != NULL && memcmp(&mode->drm_mode,
current_modeinfo, sizeof(*current_modeinfo)) == 0) {
wlr_output_state_set_mode(&state, &mode->wlr_mode);
uint64_t mode_id = 0;
get_drm_prop(drm->fd, wlr_conn->crtc->id,
wlr_conn->crtc->props.mode_id, &mode_id);
wlr_conn->crtc->own_mode_id = false;
wlr_conn->crtc->mode_id = mode_id;
wlr_conn->refresh = calculate_refresh_rate(current_modeinfo);
found_current_mode = true;
}
wlr_log(WLR_INFO, " %"PRId32"x%"PRId32" @ %.3f Hz %s",
@ -1618,6 +1611,23 @@ static bool connect_drm_connector(struct wlr_drm_connector *wlr_conn,
wl_list_insert(modes.prev, &mode->wlr_mode.link);
}
if (current_modeinfo != NULL) {
int32_t refresh = calculate_refresh_rate(current_modeinfo);
if (!found_current_mode) {
wlr_output_state_set_custom_mode(&state,
current_modeinfo->hdisplay, current_modeinfo->vdisplay, refresh);
}
uint64_t mode_id = 0;
get_drm_prop(drm->fd, wlr_conn->crtc->id,
wlr_conn->crtc->props.mode_id, &mode_id);
wlr_conn->crtc->own_mode_id = false;
wlr_conn->crtc->mode_id = mode_id;
wlr_conn->refresh = refresh;
}
free(current_modeinfo);
wlr_output_init(output, &drm->backend, &output_impl, drm->session->event_loop, &state);

View file

@ -45,6 +45,13 @@ struct wlr_foreign_toplevel_handle_v1_output {
struct wl_listener output_destroy;
};
struct wlr_foreign_toplevel_handle_v1_dbus_annotation {
struct wl_list link;
char *interface;
char *bus_name;
char *object_path;
};
struct wlr_foreign_toplevel_handle_v1 {
struct wlr_foreign_toplevel_manager_v1 *manager;
struct wl_list resources;
@ -57,6 +64,9 @@ struct wlr_foreign_toplevel_handle_v1 {
struct wl_list outputs; // wlr_foreign_toplevel_v1_output.link
uint32_t state; // enum wlr_foreign_toplevel_v1_state
struct wl_list client_dbus_annotations; // wlr_foreign_toplevel_handle_v1_dbus_annotation.link
struct wl_list surface_dbus_annotations; // wlr_foreign_toplevel_handle_v1_dbus_annotation.link
struct {
// struct wlr_foreign_toplevel_handle_v1_maximized_event
struct wl_signal request_maximize;
@ -149,5 +159,34 @@ void wlr_foreign_toplevel_handle_v1_set_parent(
struct wlr_foreign_toplevel_handle_v1 *toplevel,
struct wlr_foreign_toplevel_handle_v1 *parent);
/**
* Add or update a DBus annotation for the client associated with this
* toplevel. Arguements should not be NULL.
*/
void wlr_foreign_toplevel_handle_v1_add_client_dbus_annotation(
struct wlr_foreign_toplevel_handle_v1 *toplevel, const char *interface,
const char *bus_name, const char *object_path);
/**
* Remove an existing DBus annotation for the client associated with
* this toplevel.
*/
void wlr_foreign_toplevel_handle_v1_remove_client_dbus_annotation(
struct wlr_foreign_toplevel_handle_v1 *toplevel, const char *interface);
/**
* Add or update a DBus annotation for this toplevel. Arguements should not be NULL.
*/
void wlr_foreign_toplevel_handle_v1_add_surface_dbus_annotation(
struct wlr_foreign_toplevel_handle_v1 *toplevel, const char *interface,
const char *bus_name, const char *object_path);
/**
* Remove an existing DBus annotation for this toplevel.
*/
void wlr_foreign_toplevel_handle_v1_remove_surface_dbus_annotation(
struct wlr_foreign_toplevel_handle_v1 *toplevel, const char *interface);
#endif

View file

@ -52,7 +52,6 @@ struct wlr_screencopy_frame_v1 {
struct wlr_output *output;
struct wl_listener output_commit;
struct wl_listener output_destroy;
struct wl_listener output_enable;
void *data;
};

View file

@ -1,11 +1,11 @@
project(
'wlroots',
'c',
version: '0.18.2',
version: '0.18.3',
license: 'MIT',
meson_version: '>=0.59.0',
default_options: [
'c_std=' + (meson.version().version_compare('>=1.3.0') ? 'c23,c11' : 'c11'),
'c_std=' + (meson.version().version_compare('>=1.4.0') ? 'c23,c11' : 'c11'),
'warning_level=2',
'werror=true',
],

View file

@ -25,7 +25,7 @@
THIS SOFTWARE.
</copyright>
<interface name="zwlr_foreign_toplevel_manager_v1" version="3">
<interface name="zwlr_foreign_toplevel_manager_v1" version="4">
<description summary="list and control opened apps">
The purpose of this protocol is to enable the creation of taskbars
and docks by providing them with a list of opened applications and
@ -58,7 +58,7 @@
</description>
</request>
<event name="finished">
<event name="finished" type="destructor">
<description summary="the compositor has finished with the toplevel manager">
This event indicates that the compositor is done sending events to the
zwlr_foreign_toplevel_manager_v1. The server will destroy the object
@ -68,7 +68,7 @@
</event>
</interface>
<interface name="zwlr_foreign_toplevel_handle_v1" version="3">
<interface name="zwlr_foreign_toplevel_handle_v1" version="4">
<description summary="an opened toplevel">
A zwlr_foreign_toplevel_handle_v1 object represents an opened toplevel
window. Each app may have multiple opened toplevels.
@ -266,5 +266,43 @@
</description>
<arg name="parent" type="object" interface="zwlr_foreign_toplevel_handle_v1" allow-null="true"/>
</event>
<!-- Version 4 additions -->
<event name="client_dbus_annotation" since="4">
<description summary="DBus interface available for the client">
This event is emitted when the client associated with this surface
announces that it implements a specific DBus interface, available
at the given DBus name and object path. The DBus interface announced
by this event is not tied to any specific surface, but to a client app.
If an interface is available, both bus_name and object_path will be
non-null. bus_name will typically refer to an unique name, but can
potentially be a well-known name as well. If a previously announced
interface is no longer available, the event will be emitted for it
with bus_name and object_path set to null.
</description>
<arg name="interface" type="string" allow-null="false" summary="The name of the supported DBus interface" />
<arg name="bus_name" type="string" allow-null="true" summary="The bus name where the interface is available" />
<arg name="object_path" type="string" allow-null="true" summary="The DBus object path that implements the interface" />
</event>
<event name="surface_dbus_annotation" since="4">
<description summary="DBus interface available for the client">
This event is emitted when the client associated with this surface
announces that it implements a specific DBus interface, available
at the given DBus name and object path. The DBus interface announced
by this event is specific to this toplevel.
If an interface is available, both bus_name and object_path will be
non-null. bus_name will typically refer to an unique name, but can
potentially be a well-known name as well. If a previously announced
interface is no longer available, the event will be emitted for it
with bus_name and object_path set to null.
</description>
<arg name="interface" type="string" allow-null="false" summary="The name of the supported DBus interface" />
<arg name="bus_name" type="string" allow-null="true" summary="The bus name where the interface is available" />
<arg name="object_path" type="string" allow-null="true" summary="The DBus object path that implements the interface" />
</event>
</interface>
</protocol>

View file

@ -21,8 +21,8 @@ void wlr_render_pass_add_texture(struct wlr_render_pass *render_pass,
if (!wlr_fbox_empty(&options->src_box)) {
const struct wlr_fbox *box = &options->src_box;
assert(box->x >= 0 && box->y >= 0 &&
box->x + box->width <= options->texture->width &&
box->y + box->height <= options->texture->height);
(uint32_t)(box->x + box->width) <= options->texture->width &&
(uint32_t)(box->y + box->height) <= options->texture->height);
}
render_pass->impl->add_texture(render_pass, options);

View file

@ -128,8 +128,8 @@ static void surface_reconfigure(struct wlr_scene_surface *scene_surface) {
buffer_width, buffer_height);
wlr_output_transform_coords(state->transform, &buffer_width, &buffer_height);
src_box.x += (double)(clip->x * buffer_width) / state->width;
src_box.y += (double)(clip->y * buffer_height) / state->height;
src_box.x += (double)(clip->x * src_box.width) / state->width;
src_box.y += (double)(clip->y * src_box.height) / state->height;
src_box.width *= (double)width / state->width;
src_box.height *= (double)height / state->height;

View file

@ -574,9 +574,15 @@ static void cursor_output_cursor_update(struct wlr_cursor_output_cursor *output_
wlr_xcursor_manager_load(manager, scale);
struct wlr_xcursor *xcursor = wlr_xcursor_manager_get_xcursor(manager, name, scale);
if (xcursor == NULL) {
wlr_log(WLR_DEBUG, "XCursor theme is missing '%s' cursor", name);
wlr_output_cursor_set_buffer(output_cursor->output_cursor, NULL, 0, 0);
return;
/* Try the default cursor: better the wrong image than an invisible
* (and therefore practically unusable) cursor */
wlr_log(WLR_DEBUG, "XCursor theme is missing '%s' cursor, falling back to 'default'", name);
xcursor = wlr_xcursor_manager_get_xcursor(manager, "default", scale);
if (xcursor == NULL) {
wlr_log(WLR_DEBUG, "XCursor theme is missing a 'default' cursor");
wlr_output_cursor_set_buffer(output_cursor->output_cursor, NULL, 0, 0);
return;
}
}
output_cursor->xcursor = xcursor;

View file

@ -8,7 +8,7 @@
#include <wlr/util/log.h>
#include "wlr-foreign-toplevel-management-unstable-v1-protocol.h"
#define FOREIGN_TOPLEVEL_MANAGEMENT_V1_VERSION 3
#define FOREIGN_TOPLEVEL_MANAGEMENT_V1_VERSION 4
static const struct zwlr_foreign_toplevel_handle_v1_interface toplevel_handle_impl;
@ -486,6 +486,130 @@ void wlr_foreign_toplevel_handle_v1_set_parent(
toplevel_update_idle_source(toplevel);
}
static void toplevel_add_annotation(struct wl_list *list, const char *interface,
const char *bus_name, const char *object_path) {
assert ( (interface != NULL) && (bus_name != NULL) && (object_path != NULL) );
bool found = false;
struct wlr_foreign_toplevel_handle_v1_dbus_annotation *toplevel_annot = NULL;
wl_list_for_each(toplevel_annot, list, link) {
if (!strcmp(toplevel_annot->interface, interface)) {
found = true;
free(toplevel_annot->bus_name);
free(toplevel_annot->object_path);
toplevel_annot->bus_name = strdup(bus_name);
toplevel_annot->object_path = strdup(object_path);
break;
}
}
if (!found) {
// this interface did not exist before
toplevel_annot = calloc(1, sizeof(struct wlr_foreign_toplevel_handle_v1_dbus_annotation));
toplevel_annot->interface = strdup(interface);
toplevel_annot->bus_name = strdup(bus_name);
toplevel_annot->object_path = strdup(object_path);
wl_list_insert(list, &toplevel_annot->link);
}
}
void wlr_foreign_toplevel_handle_v1_add_client_dbus_annotation(
struct wlr_foreign_toplevel_handle_v1 *toplevel, const char *interface,
const char *bus_name, const char *object_path) {
toplevel_add_annotation(&toplevel->client_dbus_annotations,
interface, bus_name, object_path);
struct wl_resource *resource;
wl_resource_for_each(resource, &toplevel->resources) {
if (wl_resource_get_version(resource) >=
ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_CLIENT_DBUS_ANNOTATION_SINCE_VERSION) {
zwlr_foreign_toplevel_handle_v1_send_client_dbus_annotation(
resource, interface, bus_name, object_path);
}
}
toplevel_update_idle_source(toplevel);
}
void wlr_foreign_toplevel_handle_v1_add_surface_dbus_annotation(
struct wlr_foreign_toplevel_handle_v1 *toplevel, const char *interface,
const char *bus_name, const char *object_path) {
toplevel_add_annotation(&toplevel->surface_dbus_annotations,
interface, bus_name, object_path);
struct wl_resource *resource;
wl_resource_for_each(resource, &toplevel->resources) {
if (wl_resource_get_version(resource) >=
ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_SURFACE_DBUS_ANNOTATION_SINCE_VERSION) {
zwlr_foreign_toplevel_handle_v1_send_surface_dbus_annotation(
resource, interface, bus_name, object_path);
}
}
toplevel_update_idle_source(toplevel);
}
static void toplevel_dbus_annotation_destroy(
struct wlr_foreign_toplevel_handle_v1_dbus_annotation *annot) {
wl_list_remove(&annot->link);
free(annot->interface);
free(annot->bus_name);
free(annot->object_path);
free(annot);
}
void wlr_foreign_toplevel_handle_v1_remove_client_dbus_annotation(
struct wlr_foreign_toplevel_handle_v1 *toplevel, const char *interface) {
assert (interface != NULL);
bool found = false;
struct wlr_foreign_toplevel_handle_v1_dbus_annotation *toplevel_annot, *tmp;
wl_list_for_each_safe(toplevel_annot, tmp, &toplevel->client_dbus_annotations, link) {
if (!strcmp(toplevel_annot->interface, interface)) {
toplevel_dbus_annotation_destroy(toplevel_annot);
found = true;
break;
}
}
if (found) {
struct wl_resource *resource;
wl_resource_for_each(resource, &toplevel->resources) {
if (wl_resource_get_version(resource) >=
ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_CLIENT_DBUS_ANNOTATION_SINCE_VERSION) {
zwlr_foreign_toplevel_handle_v1_send_client_dbus_annotation(
resource, interface, NULL, NULL);
}
}
}
}
void wlr_foreign_toplevel_handle_v1_remove_surface_dbus_annotation(
struct wlr_foreign_toplevel_handle_v1 *toplevel, const char *interface) {
assert (interface != NULL);
bool found = false;
struct wlr_foreign_toplevel_handle_v1_dbus_annotation *toplevel_annot, *tmp;
wl_list_for_each_safe(toplevel_annot, tmp, &toplevel->surface_dbus_annotations, link) {
if (!strcmp(toplevel_annot->interface, interface)) {
toplevel_dbus_annotation_destroy(toplevel_annot);
found = true;
break;
}
}
if (found) {
struct wl_resource *resource;
wl_resource_for_each(resource, &toplevel->resources) {
if (wl_resource_get_version(resource) >=
ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_SURFACE_DBUS_ANNOTATION_SINCE_VERSION) {
zwlr_foreign_toplevel_handle_v1_send_surface_dbus_annotation(
resource, interface, NULL, NULL);
}
}
}
}
void wlr_foreign_toplevel_handle_v1_destroy(
struct wlr_foreign_toplevel_handle_v1 *toplevel) {
if (!toplevel) {
@ -525,6 +649,14 @@ void wlr_foreign_toplevel_handle_v1_destroy(
wlr_foreign_toplevel_handle_v1_set_parent(tl, NULL);
}
}
struct wlr_foreign_toplevel_handle_v1_dbus_annotation *toplevel_annot, *tmp4;
wl_list_for_each_safe(toplevel_annot, tmp4, &toplevel->client_dbus_annotations, link) {
toplevel_dbus_annotation_destroy(toplevel_annot);
}
wl_list_for_each_safe(toplevel_annot, tmp4, &toplevel->surface_dbus_annotations, link) {
toplevel_dbus_annotation_destroy(toplevel_annot);
}
free(toplevel->title);
free(toplevel->app_id);
@ -568,6 +700,8 @@ wlr_foreign_toplevel_handle_v1_create(
wl_list_init(&toplevel->resources);
wl_list_init(&toplevel->outputs);
wl_list_init(&toplevel->client_dbus_annotations);
wl_list_init(&toplevel->surface_dbus_annotations);
wl_signal_init(&toplevel->events.request_maximize);
wl_signal_init(&toplevel->events.request_minimize);
@ -637,6 +771,20 @@ static void toplevel_send_details_to_toplevel_resource(
toplevel_resource_send_parent(resource, toplevel->parent);
if (wl_resource_get_version(resource) >=
ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_CLIENT_DBUS_ANNOTATION_SINCE_VERSION) {
struct wlr_foreign_toplevel_handle_v1_dbus_annotation *annot;
wl_list_for_each(annot, &toplevel->client_dbus_annotations, link) {
zwlr_foreign_toplevel_handle_v1_send_surface_dbus_annotation(
resource, annot->interface, annot->bus_name, annot->object_path);
}
wl_list_for_each(annot, &toplevel->surface_dbus_annotations, link) {
zwlr_foreign_toplevel_handle_v1_send_surface_dbus_annotation(
resource, annot->interface, annot->bus_name, annot->object_path);
}
}
zwlr_foreign_toplevel_handle_v1_send_done(resource);
}

View file

@ -146,7 +146,6 @@ static void frame_destroy(struct wlr_screencopy_frame_v1 *frame) {
wl_list_remove(&frame->link);
wl_list_remove(&frame->output_commit.link);
wl_list_remove(&frame->output_destroy.link);
wl_list_remove(&frame->output_enable.link);
// Make the frame resource inert
wl_resource_set_user_data(frame->resource, NULL);
wlr_buffer_unlock(frame->buffer);
@ -286,6 +285,10 @@ static void frame_handle_output_commit(struct wl_listener *listener,
struct wlr_output_event_commit *event = data;
struct wlr_output *output = frame->output;
if (event->state->committed & WLR_OUTPUT_STATE_ENABLED && !output->enabled) {
goto err;
}
if (!(event->state->committed & WLR_OUTPUT_STATE_BUFFER)) {
return;
}
@ -338,16 +341,6 @@ err:
frame_destroy(frame);
}
static void frame_handle_output_enable(struct wl_listener *listener,
void *data) {
struct wlr_screencopy_frame_v1 *frame =
wl_container_of(listener, frame, output_enable);
if (!frame->output->enabled) {
zwlr_screencopy_frame_v1_send_failed(frame->resource);
frame_destroy(frame);
}
}
static void frame_handle_output_destroy(struct wl_listener *listener,
void *data) {
struct wlr_screencopy_frame_v1 *frame =
@ -439,9 +432,6 @@ static void frame_handle_copy(struct wl_client *wl_client,
wl_signal_add(&output->events.commit, &frame->output_commit);
frame->output_commit.notify = frame_handle_output_commit;
wl_signal_add(&output->events.destroy, &frame->output_enable);
frame->output_enable.notify = frame_handle_output_enable;
// Request a frame because we can't assume that the current front buffer is still usable. It may
// have been released already, and we shouldn't lock it here because compositors want to render
// into the least damaged buffer.
@ -526,7 +516,6 @@ static void capture_output(struct wl_client *wl_client,
wl_list_insert(&client->manager->frames, &frame->link);
wl_list_init(&frame->output_commit.link);
wl_list_init(&frame->output_enable.link);
wl_signal_add(&output->events.destroy, &frame->output_destroy);
frame->output_destroy.notify = frame_handle_output_destroy;