diff options
Diffstat (limited to 'video/out/wayland_common.c')
-rw-r--r-- | video/out/wayland_common.c | 2629 |
1 files changed, 2629 insertions, 0 deletions
diff --git a/video/out/wayland_common.c b/video/out/wayland_common.c new file mode 100644 index 0000000..589135f --- /dev/null +++ b/video/out/wayland_common.c @@ -0,0 +1,2629 @@ +/* + * This file is part of mpv video player. + * + * mpv is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * mpv is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +#include <errno.h> +#include <limits.h> +#include <linux/input-event-codes.h> +#include <poll.h> +#include <time.h> +#include <unistd.h> +#include <wayland-cursor.h> +#include <xkbcommon/xkbcommon.h> + +#include "common/msg.h" +#include "input/input.h" +#include "input/keycodes.h" +#include "options/m_config.h" +#include "osdep/io.h" +#include "osdep/poll_wrapper.h" +#include "osdep/timer.h" +#include "present_sync.h" +#include "wayland_common.h" +#include "win_state.h" + +// Generated from wayland-protocols +#include "idle-inhibit-unstable-v1.h" +#include "linux-dmabuf-unstable-v1.h" +#include "presentation-time.h" +#include "xdg-decoration-unstable-v1.h" +#include "xdg-shell.h" +#include "viewporter.h" + +#if HAVE_WAYLAND_PROTOCOLS_1_27 +#include "content-type-v1.h" +#include "single-pixel-buffer-v1.h" +#endif + +#if HAVE_WAYLAND_PROTOCOLS_1_31 +#include "fractional-scale-v1.h" +#endif + +#if HAVE_WAYLAND_PROTOCOLS_1_32 +#include "cursor-shape-v1.h" +#endif + +#if WAYLAND_VERSION_MAJOR > 1 || WAYLAND_VERSION_MINOR >= 22 +#define HAVE_WAYLAND_1_22 +#endif + +#ifndef CLOCK_MONOTONIC_RAW +#define CLOCK_MONOTONIC_RAW 4 +#endif + +#ifndef XDG_TOPLEVEL_STATE_SUSPENDED +#define XDG_TOPLEVEL_STATE_SUSPENDED 9 +#endif + + +static const struct mp_keymap keymap[] = { + /* Special keys */ + {XKB_KEY_Pause, MP_KEY_PAUSE}, {XKB_KEY_Escape, MP_KEY_ESC}, + {XKB_KEY_BackSpace, MP_KEY_BS}, {XKB_KEY_Tab, MP_KEY_TAB}, + {XKB_KEY_Return, MP_KEY_ENTER}, {XKB_KEY_Menu, MP_KEY_MENU}, + {XKB_KEY_Print, MP_KEY_PRINT}, {XKB_KEY_ISO_Left_Tab, MP_KEY_TAB}, + + /* Cursor keys */ + {XKB_KEY_Left, MP_KEY_LEFT}, {XKB_KEY_Right, MP_KEY_RIGHT}, + {XKB_KEY_Up, MP_KEY_UP}, {XKB_KEY_Down, MP_KEY_DOWN}, + + /* Navigation keys */ + {XKB_KEY_Insert, MP_KEY_INSERT}, {XKB_KEY_Delete, MP_KEY_DELETE}, + {XKB_KEY_Home, MP_KEY_HOME}, {XKB_KEY_End, MP_KEY_END}, + {XKB_KEY_Page_Up, MP_KEY_PAGE_UP}, {XKB_KEY_Page_Down, MP_KEY_PAGE_DOWN}, + + /* F-keys */ + {XKB_KEY_F1, MP_KEY_F + 1}, {XKB_KEY_F2, MP_KEY_F + 2}, + {XKB_KEY_F3, MP_KEY_F + 3}, {XKB_KEY_F4, MP_KEY_F + 4}, + {XKB_KEY_F5, MP_KEY_F + 5}, {XKB_KEY_F6, MP_KEY_F + 6}, + {XKB_KEY_F7, MP_KEY_F + 7}, {XKB_KEY_F8, MP_KEY_F + 8}, + {XKB_KEY_F9, MP_KEY_F + 9}, {XKB_KEY_F10, MP_KEY_F +10}, + {XKB_KEY_F11, MP_KEY_F +11}, {XKB_KEY_F12, MP_KEY_F +12}, + {XKB_KEY_F13, MP_KEY_F +13}, {XKB_KEY_F14, MP_KEY_F +14}, + {XKB_KEY_F15, MP_KEY_F +15}, {XKB_KEY_F16, MP_KEY_F +16}, + {XKB_KEY_F17, MP_KEY_F +17}, {XKB_KEY_F18, MP_KEY_F +18}, + {XKB_KEY_F19, MP_KEY_F +19}, {XKB_KEY_F20, MP_KEY_F +20}, + {XKB_KEY_F21, MP_KEY_F +21}, {XKB_KEY_F22, MP_KEY_F +22}, + {XKB_KEY_F23, MP_KEY_F +23}, {XKB_KEY_F24, MP_KEY_F +24}, + + /* Numpad independent of numlock */ + {XKB_KEY_KP_Subtract, '-'}, {XKB_KEY_KP_Add, '+'}, + {XKB_KEY_KP_Multiply, '*'}, {XKB_KEY_KP_Divide, '/'}, + {XKB_KEY_KP_Enter, MP_KEY_KPENTER}, + + /* Numpad with numlock */ + {XKB_KEY_KP_0, MP_KEY_KP0}, {XKB_KEY_KP_1, MP_KEY_KP1}, + {XKB_KEY_KP_2, MP_KEY_KP2}, {XKB_KEY_KP_3, MP_KEY_KP3}, + {XKB_KEY_KP_4, MP_KEY_KP4}, {XKB_KEY_KP_5, MP_KEY_KP5}, + {XKB_KEY_KP_6, MP_KEY_KP6}, {XKB_KEY_KP_7, MP_KEY_KP7}, + {XKB_KEY_KP_8, MP_KEY_KP8}, {XKB_KEY_KP_9, MP_KEY_KP9}, + {XKB_KEY_KP_Decimal, MP_KEY_KPDEC}, {XKB_KEY_KP_Separator, MP_KEY_KPDEC}, + + /* Numpad without numlock */ + {XKB_KEY_KP_Insert, MP_KEY_KPINS}, {XKB_KEY_KP_End, MP_KEY_KPEND}, + {XKB_KEY_KP_Down, MP_KEY_KPDOWN}, {XKB_KEY_KP_Page_Down, MP_KEY_KPPGDOWN}, + {XKB_KEY_KP_Left, MP_KEY_KPLEFT}, {XKB_KEY_KP_Begin, MP_KEY_KP5}, + {XKB_KEY_KP_Right, MP_KEY_KPRIGHT}, {XKB_KEY_KP_Home, MP_KEY_KPHOME}, + {XKB_KEY_KP_Up, MP_KEY_KPUP}, {XKB_KEY_KP_Page_Up, MP_KEY_KPPGUP}, + {XKB_KEY_KP_Delete, MP_KEY_KPDEL}, + + /* Multimedia keys */ + {XKB_KEY_XF86MenuKB, MP_KEY_MENU}, + {XKB_KEY_XF86AudioPlay, MP_KEY_PLAY}, {XKB_KEY_XF86AudioPause, MP_KEY_PAUSE}, + {XKB_KEY_XF86AudioStop, MP_KEY_STOP}, + {XKB_KEY_XF86AudioPrev, MP_KEY_PREV}, {XKB_KEY_XF86AudioNext, MP_KEY_NEXT}, + {XKB_KEY_XF86AudioRewind, MP_KEY_REWIND}, + {XKB_KEY_XF86AudioForward, MP_KEY_FORWARD}, + {XKB_KEY_XF86AudioMute, MP_KEY_MUTE}, + {XKB_KEY_XF86AudioLowerVolume, MP_KEY_VOLUME_DOWN}, + {XKB_KEY_XF86AudioRaiseVolume, MP_KEY_VOLUME_UP}, + {XKB_KEY_XF86HomePage, MP_KEY_HOMEPAGE}, {XKB_KEY_XF86WWW, MP_KEY_WWW}, + {XKB_KEY_XF86Mail, MP_KEY_MAIL}, {XKB_KEY_XF86Favorites, MP_KEY_FAVORITES}, + {XKB_KEY_XF86Search, MP_KEY_SEARCH}, {XKB_KEY_XF86Sleep, MP_KEY_SLEEP}, + {XKB_KEY_XF86Back, MP_KEY_BACK}, {XKB_KEY_XF86Tools, MP_KEY_TOOLS}, + {XKB_KEY_XF86ZoomIn, MP_KEY_ZOOMIN}, {XKB_KEY_XF86ZoomOut, MP_KEY_ZOOMOUT}, + + {0, 0} +}; + +#define OPT_BASE_STRUCT struct wayland_opts +const struct m_sub_options wayland_conf = { + .opts = (const struct m_option[]) { + {"wayland-configure-bounds", OPT_CHOICE(configure_bounds, + {"auto", -1}, {"no", 0}, {"yes", 1})}, + {"wayland-disable-vsync", OPT_BOOL(disable_vsync)}, + {"wayland-edge-pixels-pointer", OPT_INT(edge_pixels_pointer), + M_RANGE(0, INT_MAX)}, + {"wayland-edge-pixels-touch", OPT_INT(edge_pixels_touch), + M_RANGE(0, INT_MAX)}, + {0}, + }, + .size = sizeof(struct wayland_opts), + .defaults = &(struct wayland_opts) { + .configure_bounds = -1, + .edge_pixels_pointer = 16, + .edge_pixels_touch = 32, + }, +}; + +struct vo_wayland_feedback_pool { + struct wp_presentation_feedback **fback; + struct vo_wayland_state *wl; + int len; +}; + +struct vo_wayland_output { + struct vo_wayland_state *wl; + struct wl_output *output; + struct mp_rect geometry; + bool has_surface; + uint32_t id; + uint32_t flags; + int phys_width; + int phys_height; + int scale; + double refresh_rate; + char *make; + char *model; + char *name; + struct wl_list link; +}; + +static int check_for_resize(struct vo_wayland_state *wl, int edge_pixels, + enum xdg_toplevel_resize_edge *edge); +static int get_mods(struct vo_wayland_state *wl); +static int lookupkey(int key); +static int set_cursor_visibility(struct vo_wayland_state *wl, bool on); +static int spawn_cursor(struct vo_wayland_state *wl); + +static void add_feedback(struct vo_wayland_feedback_pool *fback_pool, + struct wp_presentation_feedback *fback); +static void get_shape_device(struct vo_wayland_state *wl); +static int greatest_common_divisor(int a, int b); +static void guess_focus(struct vo_wayland_state *wl); +static void prepare_resize(struct vo_wayland_state *wl, int width, int height); +static void remove_feedback(struct vo_wayland_feedback_pool *fback_pool, + struct wp_presentation_feedback *fback); +static void remove_output(struct vo_wayland_output *out); +static void request_decoration_mode(struct vo_wayland_state *wl, uint32_t mode); +static void rescale_geometry(struct vo_wayland_state *wl, double old_scale); +static void set_geometry(struct vo_wayland_state *wl, bool resize); +static void set_surface_scaling(struct vo_wayland_state *wl); +static void window_move(struct vo_wayland_state *wl, uint32_t serial); + +/* Wayland listener boilerplate */ +static void pointer_handle_enter(void *data, struct wl_pointer *pointer, + uint32_t serial, struct wl_surface *surface, + wl_fixed_t sx, wl_fixed_t sy) +{ + struct vo_wayland_state *wl = data; + + wl->pointer = pointer; + wl->pointer_id = serial; + + set_cursor_visibility(wl, wl->cursor_visible); + mp_input_put_key(wl->vo->input_ctx, MP_KEY_MOUSE_ENTER); +} + +static void pointer_handle_leave(void *data, struct wl_pointer *pointer, + uint32_t serial, struct wl_surface *surface) +{ + struct vo_wayland_state *wl = data; + mp_input_put_key(wl->vo->input_ctx, MP_KEY_MOUSE_LEAVE); +} + +static void pointer_handle_motion(void *data, struct wl_pointer *pointer, + uint32_t time, wl_fixed_t sx, wl_fixed_t sy) +{ + struct vo_wayland_state *wl = data; + + wl->mouse_x = wl_fixed_to_int(sx) * wl->scaling; + wl->mouse_y = wl_fixed_to_int(sy) * wl->scaling; + + if (!wl->toplevel_configured) + mp_input_set_mouse_pos(wl->vo->input_ctx, wl->mouse_x, wl->mouse_y); + wl->toplevel_configured = false; +} + +static void pointer_handle_button(void *data, struct wl_pointer *wl_pointer, + uint32_t serial, uint32_t time, uint32_t button, + uint32_t state) +{ + struct vo_wayland_state *wl = data; + state = state == WL_POINTER_BUTTON_STATE_PRESSED ? MP_KEY_STATE_DOWN + : MP_KEY_STATE_UP; + + if (button >= BTN_MOUSE && button < BTN_JOYSTICK) { + switch (button) { + case BTN_LEFT: + button = MP_MBTN_LEFT; + break; + case BTN_MIDDLE: + button = MP_MBTN_MID; + break; + case BTN_RIGHT: + button = MP_MBTN_RIGHT; + break; + case BTN_SIDE: + button = MP_MBTN_BACK; + break; + case BTN_EXTRA: + button = MP_MBTN_FORWARD; + break; + default: + button += MP_MBTN9 - BTN_FORWARD; + break; + } + } else { + button = 0; + } + + if (button) + mp_input_put_key(wl->vo->input_ctx, button | state | wl->mpmod); + + if (!mp_input_test_dragging(wl->vo->input_ctx, wl->mouse_x, wl->mouse_y) && + !wl->locked_size && (button == MP_MBTN_LEFT) && (state == MP_KEY_STATE_DOWN)) + { + uint32_t edges; + // Implement an edge resize zone if there are no decorations + if (!wl->vo_opts->border && check_for_resize(wl, wl->opts->edge_pixels_pointer, &edges)) { + xdg_toplevel_resize(wl->xdg_toplevel, wl->seat, serial, edges); + } else { + window_move(wl, serial); + } + // Explicitly send an UP event after the client finishes a move/resize + mp_input_put_key(wl->vo->input_ctx, button | MP_KEY_STATE_UP); + } +} + +static void pointer_handle_axis(void *data, struct wl_pointer *wl_pointer, + uint32_t time, uint32_t axis, wl_fixed_t value) +{ + struct vo_wayland_state *wl = data; + + double val = wl_fixed_to_double(value) < 0 ? -1 : 1; + switch (axis) { + case WL_POINTER_AXIS_VERTICAL_SCROLL: + if (value > 0) + mp_input_put_wheel(wl->vo->input_ctx, MP_WHEEL_DOWN | wl->mpmod, +val); + if (value < 0) + mp_input_put_wheel(wl->vo->input_ctx, MP_WHEEL_UP | wl->mpmod, -val); + break; + case WL_POINTER_AXIS_HORIZONTAL_SCROLL: + if (value > 0) + mp_input_put_wheel(wl->vo->input_ctx, MP_WHEEL_RIGHT | wl->mpmod, +val); + if (value < 0) + mp_input_put_wheel(wl->vo->input_ctx, MP_WHEEL_LEFT | wl->mpmod, -val); + break; + } +} + +static const struct wl_pointer_listener pointer_listener = { + pointer_handle_enter, + pointer_handle_leave, + pointer_handle_motion, + pointer_handle_button, + pointer_handle_axis, +}; + +static void touch_handle_down(void *data, struct wl_touch *wl_touch, + uint32_t serial, uint32_t time, struct wl_surface *surface, + int32_t id, wl_fixed_t x_w, wl_fixed_t y_w) +{ + struct vo_wayland_state *wl = data; + wl->mouse_x = wl_fixed_to_int(x_w) * wl->scaling; + wl->mouse_y = wl_fixed_to_int(y_w) * wl->scaling; + + enum xdg_toplevel_resize_edge edge; + if (!mp_input_test_dragging(wl->vo->input_ctx, wl->mouse_x, wl->mouse_y)) { + if (check_for_resize(wl, wl->opts->edge_pixels_touch, &edge)) { + xdg_toplevel_resize(wl->xdg_toplevel, wl->seat, serial, edge); + } else { + xdg_toplevel_move(wl->xdg_toplevel, wl->seat, serial); + } + } + + mp_input_set_mouse_pos(wl->vo->input_ctx, wl->mouse_x, wl->mouse_y); + mp_input_put_key(wl->vo->input_ctx, MP_MBTN_LEFT | MP_KEY_STATE_DOWN); +} + +static void touch_handle_up(void *data, struct wl_touch *wl_touch, + uint32_t serial, uint32_t time, int32_t id) +{ + struct vo_wayland_state *wl = data; + mp_input_put_key(wl->vo->input_ctx, MP_MBTN_LEFT | MP_KEY_STATE_UP); +} + +static void touch_handle_motion(void *data, struct wl_touch *wl_touch, + uint32_t time, int32_t id, wl_fixed_t x_w, wl_fixed_t y_w) +{ + struct vo_wayland_state *wl = data; + + wl->mouse_x = wl_fixed_to_int(x_w) * wl->scaling; + wl->mouse_y = wl_fixed_to_int(y_w) * wl->scaling; + + mp_input_set_mouse_pos(wl->vo->input_ctx, wl->mouse_x, wl->mouse_y); +} + +static void touch_handle_frame(void *data, struct wl_touch *wl_touch) +{ +} + +static void touch_handle_cancel(void *data, struct wl_touch *wl_touch) +{ +} + +static const struct wl_touch_listener touch_listener = { + touch_handle_down, + touch_handle_up, + touch_handle_motion, + touch_handle_frame, + touch_handle_cancel, +}; + +static void keyboard_handle_keymap(void *data, struct wl_keyboard *wl_keyboard, + uint32_t format, int32_t fd, uint32_t size) +{ + struct vo_wayland_state *wl = data; + char *map_str; + + if (format != WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1) { + close(fd); + return; + } + + map_str = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0); + if (map_str == MAP_FAILED) { + close(fd); + return; + } + + wl->xkb_keymap = xkb_keymap_new_from_buffer(wl->xkb_context, map_str, + strnlen(map_str, size), + XKB_KEYMAP_FORMAT_TEXT_V1, 0); + + munmap(map_str, size); + close(fd); + + if (!wl->xkb_keymap) { + MP_ERR(wl, "failed to compile keymap\n"); + return; + } + + wl->xkb_state = xkb_state_new(wl->xkb_keymap); + if (!wl->xkb_state) { + MP_ERR(wl, "failed to create XKB state\n"); + xkb_keymap_unref(wl->xkb_keymap); + wl->xkb_keymap = NULL; + return; + } +} + +static void keyboard_handle_enter(void *data, struct wl_keyboard *wl_keyboard, + uint32_t serial, struct wl_surface *surface, + struct wl_array *keys) +{ + struct vo_wayland_state *wl = data; + wl->has_keyboard_input = true; + guess_focus(wl); +} + +static void keyboard_handle_leave(void *data, struct wl_keyboard *wl_keyboard, + uint32_t serial, struct wl_surface *surface) +{ + struct vo_wayland_state *wl = data; + wl->has_keyboard_input = false; + wl->keyboard_code = 0; + wl->mpkey = 0; + wl->mpmod = 0; + mp_input_put_key(wl->vo->input_ctx, MP_INPUT_RELEASE_ALL); + guess_focus(wl); +} + +static void keyboard_handle_key(void *data, struct wl_keyboard *wl_keyboard, + uint32_t serial, uint32_t time, uint32_t key, + uint32_t state) +{ + struct vo_wayland_state *wl = data; + + wl->keyboard_code = key + 8; + xkb_keysym_t sym = xkb_state_key_get_one_sym(wl->xkb_state, wl->keyboard_code); + int mpkey = lookupkey(sym); + + state = state == WL_KEYBOARD_KEY_STATE_PRESSED ? MP_KEY_STATE_DOWN + : MP_KEY_STATE_UP; + + if (mpkey) { + mp_input_put_key(wl->vo->input_ctx, mpkey | state | wl->mpmod); + } else { + char s[128]; + if (xkb_keysym_to_utf8(sym, s, sizeof(s)) > 0) { + mp_input_put_key_utf8(wl->vo->input_ctx, state | wl->mpmod, bstr0(s)); + } else { + // Assume a modifier was pressed and handle it in the mod event instead. + return; + } + } + if (state == MP_KEY_STATE_DOWN) + wl->mpkey = mpkey; + if (mpkey && state == MP_KEY_STATE_UP) + wl->mpkey = 0; +} + +static void keyboard_handle_modifiers(void *data, struct wl_keyboard *wl_keyboard, + uint32_t serial, uint32_t mods_depressed, + uint32_t mods_latched, uint32_t mods_locked, + uint32_t group) +{ + struct vo_wayland_state *wl = data; + + if (wl->xkb_state) { + xkb_state_update_mask(wl->xkb_state, mods_depressed, mods_latched, + mods_locked, 0, 0, group); + wl->mpmod = get_mods(wl); + if (wl->mpkey) + mp_input_put_key(wl->vo->input_ctx, wl->mpkey | MP_KEY_STATE_DOWN | wl->mpmod); + } +} + +static void keyboard_handle_repeat_info(void *data, struct wl_keyboard *wl_keyboard, + int32_t rate, int32_t delay) +{ + struct vo_wayland_state *wl = data; + if (wl->vo_opts->native_keyrepeat) + mp_input_set_repeat_info(wl->vo->input_ctx, rate, delay); +} + +static const struct wl_keyboard_listener keyboard_listener = { + keyboard_handle_keymap, + keyboard_handle_enter, + keyboard_handle_leave, + keyboard_handle_key, + keyboard_handle_modifiers, + keyboard_handle_repeat_info, +}; + +static void seat_handle_caps(void *data, struct wl_seat *seat, + enum wl_seat_capability caps) +{ + struct vo_wayland_state *wl = data; + + if ((caps & WL_SEAT_CAPABILITY_POINTER) && !wl->pointer) { + wl->pointer = wl_seat_get_pointer(seat); + get_shape_device(wl); + wl_pointer_add_listener(wl->pointer, &pointer_listener, wl); + } else if (!(caps & WL_SEAT_CAPABILITY_POINTER) && wl->pointer) { + wl_pointer_destroy(wl->pointer); + wl->pointer = NULL; + } + + if ((caps & WL_SEAT_CAPABILITY_KEYBOARD) && !wl->keyboard) { + wl->keyboard = wl_seat_get_keyboard(seat); + wl_keyboard_add_listener(wl->keyboard, &keyboard_listener, wl); + } else if (!(caps & WL_SEAT_CAPABILITY_KEYBOARD) && wl->keyboard) { + wl_keyboard_destroy(wl->keyboard); + wl->keyboard = NULL; + } + + if ((caps & WL_SEAT_CAPABILITY_TOUCH) && !wl->touch) { + wl->touch = wl_seat_get_touch(seat); + wl_touch_set_user_data(wl->touch, wl); + wl_touch_add_listener(wl->touch, &touch_listener, wl); + } else if (!(caps & WL_SEAT_CAPABILITY_TOUCH) && wl->touch) { + wl_touch_destroy(wl->touch); + wl->touch = NULL; + } +} + +static const struct wl_seat_listener seat_listener = { + seat_handle_caps, +}; + +static void data_offer_handle_offer(void *data, struct wl_data_offer *offer, + const char *mime_type) +{ + struct vo_wayland_state *wl = data; + int score = mp_event_get_mime_type_score(wl->vo->input_ctx, mime_type); + if (score > wl->dnd_mime_score && wl->vo_opts->drag_and_drop != -2) { + wl->dnd_mime_score = score; + if (wl->dnd_mime_type) + talloc_free(wl->dnd_mime_type); + wl->dnd_mime_type = talloc_strdup(wl, mime_type); + MP_VERBOSE(wl, "Given DND offer with mime type %s\n", wl->dnd_mime_type); + } +} + +static void data_offer_source_actions(void *data, struct wl_data_offer *offer, uint32_t source_actions) +{ +} + +static void data_offer_action(void *data, struct wl_data_offer *wl_data_offer, uint32_t dnd_action) +{ + struct vo_wayland_state *wl = data; + if (dnd_action && wl->vo_opts->drag_and_drop != -2) { + if (wl->vo_opts->drag_and_drop >= 0) { + wl->dnd_action = wl->vo_opts->drag_and_drop; + } else { + wl->dnd_action = dnd_action & WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY ? + DND_REPLACE : DND_APPEND; + } + MP_VERBOSE(wl, "DND action is %s\n", + wl->dnd_action == DND_REPLACE ? "DND_REPLACE" : "DND_APPEND"); + } +} + +static const struct wl_data_offer_listener data_offer_listener = { + data_offer_handle_offer, + data_offer_source_actions, + data_offer_action, +}; + +static void data_device_handle_data_offer(void *data, struct wl_data_device *wl_ddev, + struct wl_data_offer *id) +{ + struct vo_wayland_state *wl = data; + if (wl->dnd_offer) + wl_data_offer_destroy(wl->dnd_offer); + + wl->dnd_offer = id; + wl_data_offer_add_listener(id, &data_offer_listener, wl); +} + +static void data_device_handle_enter(void *data, struct wl_data_device *wl_ddev, + uint32_t serial, struct wl_surface *surface, + wl_fixed_t x, wl_fixed_t y, + struct wl_data_offer *id) +{ + struct vo_wayland_state *wl = data; + if (wl->dnd_offer != id) { + MP_FATAL(wl, "DND offer ID mismatch!\n"); + return; + } + + if (wl->vo_opts->drag_and_drop != -2) { + wl_data_offer_set_actions(id, WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY | + WL_DATA_DEVICE_MANAGER_DND_ACTION_MOVE, + WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY); + wl_data_offer_accept(id, serial, wl->dnd_mime_type); + MP_VERBOSE(wl, "Accepting DND offer with mime type %s\n", wl->dnd_mime_type); + } + +} + +static void data_device_handle_leave(void *data, struct wl_data_device *wl_ddev) +{ + struct vo_wayland_state *wl = data; + + if (wl->dnd_offer) { + if (wl->dnd_fd != -1) + return; + wl_data_offer_destroy(wl->dnd_offer); + wl->dnd_offer = NULL; + } + + if (wl->vo_opts->drag_and_drop != -2) { + MP_VERBOSE(wl, "Releasing DND offer with mime type %s\n", wl->dnd_mime_type); + if (wl->dnd_mime_type) + TA_FREEP(&wl->dnd_mime_type); + wl->dnd_mime_score = 0; + } +} + +static void data_device_handle_motion(void *data, struct wl_data_device *wl_ddev, + uint32_t time, wl_fixed_t x, wl_fixed_t y) +{ + struct vo_wayland_state *wl = data; + wl_data_offer_accept(wl->dnd_offer, time, wl->dnd_mime_type); +} + +static void data_device_handle_drop(void *data, struct wl_data_device *wl_ddev) +{ + struct vo_wayland_state *wl = data; + + int pipefd[2]; + + if (pipe2(pipefd, O_CLOEXEC) == -1) { + MP_ERR(wl, "Failed to create dnd pipe!\n"); + return; + } + + if (wl->vo_opts->drag_and_drop != -2) { + MP_VERBOSE(wl, "Receiving DND offer with mime %s\n", wl->dnd_mime_type); + wl_data_offer_receive(wl->dnd_offer, wl->dnd_mime_type, pipefd[1]); + } + + close(pipefd[1]); + wl->dnd_fd = pipefd[0]; +} + +static void data_device_handle_selection(void *data, struct wl_data_device *wl_ddev, + struct wl_data_offer *id) +{ + struct vo_wayland_state *wl = data; + + if (wl->dnd_offer) { + wl_data_offer_destroy(wl->dnd_offer); + wl->dnd_offer = NULL; + MP_VERBOSE(wl, "Received a new DND offer. Releasing the previous offer.\n"); + } + +} + +static const struct wl_data_device_listener data_device_listener = { + data_device_handle_data_offer, + data_device_handle_enter, + data_device_handle_leave, + data_device_handle_motion, + data_device_handle_drop, + data_device_handle_selection, +}; + +static void output_handle_geometry(void *data, struct wl_output *wl_output, + int32_t x, int32_t y, int32_t phys_width, + int32_t phys_height, int32_t subpixel, + const char *make, const char *model, + int32_t transform) +{ + struct vo_wayland_output *output = data; + output->make = talloc_strdup(output->wl, make); + output->model = talloc_strdup(output->wl, model); + output->geometry.x0 = x; + output->geometry.y0 = y; + output->phys_width = phys_width; + output->phys_height = phys_height; +} + +static void output_handle_mode(void *data, struct wl_output *wl_output, + uint32_t flags, int32_t width, + int32_t height, int32_t refresh) +{ + struct vo_wayland_output *output = data; + + /* Only save current mode */ + if (!(flags & WL_OUTPUT_MODE_CURRENT)) + return; + + output->geometry.x1 = width; + output->geometry.y1 = height; + output->flags = flags; + output->refresh_rate = (double)refresh * 0.001; +} + +static void output_handle_done(void *data, struct wl_output *wl_output) +{ + struct vo_wayland_output *o = data; + struct vo_wayland_state *wl = o->wl; + + o->geometry.x1 += o->geometry.x0; + o->geometry.y1 += o->geometry.y0; + + MP_VERBOSE(o->wl, "Registered output %s %s (0x%x):\n" + "\tx: %dpx, y: %dpx\n" + "\tw: %dpx (%dmm), h: %dpx (%dmm)\n" + "\tscale: %d\n" + "\tHz: %f\n", o->make, o->model, o->id, o->geometry.x0, + o->geometry.y0, mp_rect_w(o->geometry), o->phys_width, + mp_rect_h(o->geometry), o->phys_height, o->scale, o->refresh_rate); + + /* If we satisfy this conditional, something about the current + * output must have changed (resolution, scale, etc). All window + * geometry and scaling should be recalculated. */ + if (wl->current_output && wl->current_output->output == wl_output) { + set_surface_scaling(wl); + spawn_cursor(wl); + set_geometry(wl, false); + prepare_resize(wl, 0, 0); + wl->pending_vo_events |= VO_EVENT_DPI; + } + + wl->pending_vo_events |= VO_EVENT_WIN_STATE; +} + +static void output_handle_scale(void *data, struct wl_output *wl_output, + int32_t factor) +{ + struct vo_wayland_output *output = data; + if (!factor) { + MP_ERR(output->wl, "Invalid output scale given by the compositor!\n"); + return; + } + output->scale = factor; +} + +static void output_handle_name(void *data, struct wl_output *wl_output, + const char *name) +{ + struct vo_wayland_output *output = data; + output->name = talloc_strdup(output->wl, name); +} + +static void output_handle_description(void *data, struct wl_output *wl_output, + const char *description) +{ +} + +static const struct wl_output_listener output_listener = { + output_handle_geometry, + output_handle_mode, + output_handle_done, + output_handle_scale, + output_handle_name, + output_handle_description, +}; + +static void surface_handle_enter(void *data, struct wl_surface *wl_surface, + struct wl_output *output) +{ + struct vo_wayland_state *wl = data; + if (!wl->current_output) + return; + + struct mp_rect old_output_geometry = wl->current_output->geometry; + struct mp_rect old_geometry = wl->geometry; + wl->current_output = NULL; + + struct vo_wayland_output *o; + wl_list_for_each(o, &wl->output_list, link) { + if (o->output == output) { + wl->current_output = o; + break; + } + } + + wl->current_output->has_surface = true; + bool force_resize = false; + + if (!wl->fractional_scale_manager && wl_surface_get_version(wl_surface) < 6 && + wl->scaling != wl->current_output->scale) + { + set_surface_scaling(wl); + spawn_cursor(wl); + force_resize = true; + wl->pending_vo_events |= VO_EVENT_DPI; + } + + if (!mp_rect_equals(&old_output_geometry, &wl->current_output->geometry)) { + set_geometry(wl, false); + force_resize = true; + } + + if (!mp_rect_equals(&old_geometry, &wl->geometry) || force_resize) + prepare_resize(wl, 0, 0); + + MP_VERBOSE(wl, "Surface entered output %s %s (0x%x), scale = %f, refresh rate = %f Hz\n", + o->make, o->model, o->id, wl->scaling, o->refresh_rate); + + wl->pending_vo_events |= VO_EVENT_WIN_STATE; +} + +static void surface_handle_leave(void *data, struct wl_surface *wl_surface, + struct wl_output *output) +{ + struct vo_wayland_state *wl = data; + + struct vo_wayland_output *o; + wl_list_for_each(o, &wl->output_list, link) { + if (o->output == output) { + o->has_surface = false; + wl->pending_vo_events |= VO_EVENT_WIN_STATE; + return; + } + } +} + +#ifdef HAVE_WAYLAND_1_22 +static void surface_handle_preferred_buffer_scale(void *data, + struct wl_surface *wl_surface, + int32_t scale) +{ + struct vo_wayland_state *wl = data; + double old_scale = wl->scaling; + + if (wl->fractional_scale_manager) + return; + + // dmabuf_wayland is always wl->scaling = 1 + wl->scaling = !wl->using_dmabuf_wayland ? scale : 1; + MP_VERBOSE(wl, "Obtained preferred scale, %f, from the compositor.\n", + wl->scaling); + wl->pending_vo_events |= VO_EVENT_DPI; + if (wl->current_output) { + rescale_geometry(wl, old_scale); + set_geometry(wl, false); + prepare_resize(wl, 0, 0); + } +} + +static void surface_handle_preferred_buffer_transform(void *data, + struct wl_surface *wl_surface, + uint32_t transform) +{ +} +#endif + +static const struct wl_surface_listener surface_listener = { + surface_handle_enter, + surface_handle_leave, +#ifdef HAVE_WAYLAND_1_22 + surface_handle_preferred_buffer_scale, + surface_handle_preferred_buffer_transform, +#endif +}; + +static void xdg_wm_base_ping(void *data, struct xdg_wm_base *wm_base, uint32_t serial) +{ + xdg_wm_base_pong(wm_base, serial); +} + +static const struct xdg_wm_base_listener xdg_wm_base_listener = { + xdg_wm_base_ping, +}; + +static void handle_surface_config(void *data, struct xdg_surface *surface, + uint32_t serial) +{ + xdg_surface_ack_configure(surface, serial); +} + +static const struct xdg_surface_listener xdg_surface_listener = { + handle_surface_config, +}; + +static void handle_toplevel_config(void *data, struct xdg_toplevel *toplevel, + int32_t width, int32_t height, struct wl_array *states) +{ + struct vo_wayland_state *wl = data; + struct mp_vo_opts *vo_opts = wl->vo_opts; + struct mp_rect old_geometry = wl->geometry; + + int old_toplevel_width = wl->toplevel_width; + int old_toplevel_height = wl->toplevel_height; + wl->toplevel_width = width; + wl->toplevel_height = height; + + if (!wl->configured) { + /* Save initial window size if the compositor gives us a hint here. */ + bool autofit_or_geometry = vo_opts->geometry.wh_valid || vo_opts->autofit.wh_valid || + vo_opts->autofit_larger.wh_valid || vo_opts->autofit_smaller.wh_valid; + if (width && height && !autofit_or_geometry) { + wl->initial_size_hint = true; + wl->window_size = (struct mp_rect){0, 0, width, height}; + wl->geometry = wl->window_size; + } + return; + } + + bool is_maximized = false; + bool is_fullscreen = false; + bool is_activated = false; + bool is_suspended = false; + bool is_tiled = false; + enum xdg_toplevel_state *state; + wl_array_for_each(state, states) { + switch (*state) { + case XDG_TOPLEVEL_STATE_FULLSCREEN: + is_fullscreen = true; + break; + case XDG_TOPLEVEL_STATE_RESIZING: + break; + case XDG_TOPLEVEL_STATE_ACTIVATED: + is_activated = true; + /* + * If we get an ACTIVATED state, we know it cannot be + * minimized, but it may not have been minimized + * previously, so we can't detect the exact state. + */ + vo_opts->window_minimized = false; + m_config_cache_write_opt(wl->vo_opts_cache, + &vo_opts->window_minimized); + break; + case XDG_TOPLEVEL_STATE_TILED_TOP: + case XDG_TOPLEVEL_STATE_TILED_LEFT: + case XDG_TOPLEVEL_STATE_TILED_RIGHT: + case XDG_TOPLEVEL_STATE_TILED_BOTTOM: + is_tiled = true; + break; + case XDG_TOPLEVEL_STATE_MAXIMIZED: + is_maximized = true; + break; + case XDG_TOPLEVEL_STATE_SUSPENDED: + is_suspended = true; + break; + } + } + + if (wl->hidden != is_suspended) + wl->hidden = is_suspended; + + if (vo_opts->fullscreen != is_fullscreen) { + wl->state_change = true; + vo_opts->fullscreen = is_fullscreen; + m_config_cache_write_opt(wl->vo_opts_cache, &vo_opts->fullscreen); + } + + if (vo_opts->window_maximized != is_maximized) { + wl->state_change = true; + vo_opts->window_maximized = is_maximized; + m_config_cache_write_opt(wl->vo_opts_cache, &vo_opts->window_maximized); + } + + wl->tiled = is_tiled; + + wl->locked_size = is_fullscreen || is_maximized || is_tiled; + + if (wl->requested_decoration) + request_decoration_mode(wl, wl->requested_decoration); + + if (wl->activated != is_activated) { + wl->activated = is_activated; + guess_focus(wl); + /* Just force a redraw to be on the safe side. */ + if (wl->activated) { + wl->hidden = false; + wl->pending_vo_events |= VO_EVENT_EXPOSE; + } + } + + if (wl->state_change) { + if (!wl->locked_size) { + wl->geometry = wl->window_size; + wl->state_change = false; + goto resize; + } + } + + /* Reuse old size if either of these are 0. */ + if (width == 0 || height == 0) { + if (!wl->locked_size) { + wl->geometry = wl->window_size; + } + goto resize; + } + + if (old_toplevel_width == wl->toplevel_width && + old_toplevel_height == wl->toplevel_height) + return; + + if (!wl->locked_size) { + if (vo_opts->keepaspect) { + double scale_factor = (double)width / wl->reduced_width; + width = ceil(wl->reduced_width * scale_factor); + if (vo_opts->keepaspect_window) + height = ceil(wl->reduced_height * scale_factor); + } + wl->window_size.x0 = 0; + wl->window_size.y0 = 0; + wl->window_size.x1 = round(width * wl->scaling); + wl->window_size.y1 = round(height * wl->scaling); + } + wl->geometry.x0 = 0; + wl->geometry.y0 = 0; + wl->geometry.x1 = round(width * wl->scaling); + wl->geometry.y1 = round(height * wl->scaling); + + if (mp_rect_equals(&old_geometry, &wl->geometry)) + return; + +resize: + MP_VERBOSE(wl, "Resizing due to xdg from %ix%i to %ix%i\n", + mp_rect_w(old_geometry), mp_rect_h(old_geometry), + mp_rect_w(wl->geometry), mp_rect_h(wl->geometry)); + + prepare_resize(wl, width, height); + wl->toplevel_configured = true; +} + +static void handle_toplevel_close(void *data, struct xdg_toplevel *xdg_toplevel) +{ + struct vo_wayland_state *wl = data; + mp_input_put_key(wl->vo->input_ctx, MP_KEY_CLOSE_WIN); +} + +static void handle_configure_bounds(void *data, struct xdg_toplevel *xdg_toplevel, + int32_t width, int32_t height) +{ + struct vo_wayland_state *wl = data; + wl->bounded_width = width * wl->scaling; + wl->bounded_height = height * wl->scaling; +} + +#ifdef XDG_TOPLEVEL_WM_CAPABILITIES_SINCE_VERSION +static void handle_wm_capabilities(void *data, struct xdg_toplevel *xdg_toplevel, + struct wl_array *capabilities) +{ +} +#endif + +static const struct xdg_toplevel_listener xdg_toplevel_listener = { + handle_toplevel_config, + handle_toplevel_close, + handle_configure_bounds, +#ifdef XDG_TOPLEVEL_WM_CAPABILITIES_SINCE_VERSION + handle_wm_capabilities, +#endif +}; + +#if HAVE_WAYLAND_PROTOCOLS_1_31 +static void preferred_scale(void *data, + struct wp_fractional_scale_v1 *fractional_scale, + uint32_t scale) +{ + struct vo_wayland_state *wl = data; + double old_scale = wl->scaling; + + // dmabuf_wayland is always wl->scaling = 1 + wl->scaling = !wl->using_dmabuf_wayland ? (double)scale / 120 : 1; + MP_VERBOSE(wl, "Obtained preferred scale, %f, from the compositor.\n", + wl->scaling); + wl->pending_vo_events |= VO_EVENT_DPI; + if (wl->current_output) { + rescale_geometry(wl, old_scale); + set_geometry(wl, false); + prepare_resize(wl, 0, 0); + } +} + +static const struct wp_fractional_scale_v1_listener fractional_scale_listener = { + preferred_scale, +}; +#endif + +static const char *zxdg_decoration_mode_to_str(const uint32_t mode) +{ + switch (mode) { + case ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE: + return "server-side"; + case ZXDG_TOPLEVEL_DECORATION_V1_MODE_CLIENT_SIDE: + return "client-side"; + default: + return "<unknown>"; + } +} + +static void configure_decorations(void *data, + struct zxdg_toplevel_decoration_v1 *xdg_toplevel_decoration, + uint32_t mode) +{ + struct vo_wayland_state *wl = data; + struct mp_vo_opts *opts = wl->vo_opts; + + if (wl->requested_decoration && mode != wl->requested_decoration) { + MP_DBG(wl, + "Requested %s decorations but compositor responded with %s. " + "It is likely that compositor wants us to stay in a given mode.\n", + zxdg_decoration_mode_to_str(wl->requested_decoration), + zxdg_decoration_mode_to_str(mode)); + } + + wl->requested_decoration = 0; + + if (mode == ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE) { + MP_VERBOSE(wl, "Enabling server decorations\n"); + } else { + MP_VERBOSE(wl, "Disabling server decorations\n"); + } + opts->border = mode == ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE; + m_config_cache_write_opt(wl->vo_opts_cache, &opts->border); +} + +static const struct zxdg_toplevel_decoration_v1_listener decoration_listener = { + configure_decorations, +}; + +static void pres_set_clockid(void *data, struct wp_presentation *pres, + uint32_t clockid) +{ + struct vo_wayland_state *wl = data; + + if (clockid == CLOCK_MONOTONIC || clockid == CLOCK_MONOTONIC_RAW) + wl->use_present = true; +} + +static const struct wp_presentation_listener pres_listener = { + pres_set_clockid, +}; + +static void feedback_sync_output(void *data, struct wp_presentation_feedback *fback, + struct wl_output *output) +{ +} + +static void feedback_presented(void *data, struct wp_presentation_feedback *fback, + uint32_t tv_sec_hi, uint32_t tv_sec_lo, + uint32_t tv_nsec, uint32_t refresh_nsec, + uint32_t seq_hi, uint32_t seq_lo, + uint32_t flags) +{ + struct vo_wayland_feedback_pool *fback_pool = data; + struct vo_wayland_state *wl = fback_pool->wl; + + if (fback) + remove_feedback(fback_pool, fback); + + wl->refresh_interval = (int64_t)refresh_nsec; + + // Very similar to oml_sync_control, in this case we assume that every + // time the compositor receives feedback, a buffer swap has been already + // been performed. + // + // Notes: + // - tv_sec_lo + tv_sec_hi is the equivalent of oml's ust + // - seq_lo + seq_hi is the equivalent of oml's msc + // - these values are updated every time the compositor receives feedback. + + int64_t sec = (uint64_t) tv_sec_lo + ((uint64_t) tv_sec_hi << 32); + int64_t ust = MP_TIME_S_TO_NS(sec) + (uint64_t) tv_nsec; + int64_t msc = (uint64_t) seq_lo + ((uint64_t) seq_hi << 32); + present_sync_update_values(wl->present, ust, msc); +} + +static void feedback_discarded(void *data, struct wp_presentation_feedback *fback) +{ + struct vo_wayland_feedback_pool *fback_pool = data; + if (fback) + remove_feedback(fback_pool, fback); +} + +static const struct wp_presentation_feedback_listener feedback_listener = { + feedback_sync_output, + feedback_presented, + feedback_discarded, +}; + +static const struct wl_callback_listener frame_listener; + +static void frame_callback(void *data, struct wl_callback *callback, uint32_t time) +{ + struct vo_wayland_state *wl = data; + + if (callback) + wl_callback_destroy(callback); + + wl->frame_callback = wl_surface_frame(wl->callback_surface); + wl_callback_add_listener(wl->frame_callback, &frame_listener, wl); + + if (wl->use_present) { + struct wp_presentation_feedback *fback = wp_presentation_feedback(wl->presentation, wl->callback_surface); + add_feedback(wl->fback_pool, fback); + wp_presentation_feedback_add_listener(fback, &feedback_listener, wl->fback_pool); + } + + wl->frame_wait = false; + wl->hidden = false; +} + +static const struct wl_callback_listener frame_listener = { + frame_callback, +}; + +static void done(void *data, + struct zwp_linux_dmabuf_feedback_v1 *zwp_linux_dmabuf_feedback_v1) +{ +} + +static void format_table(void *data, + struct zwp_linux_dmabuf_feedback_v1 *zwp_linux_dmabuf_feedback_v1, + int32_t fd, + uint32_t size) +{ + struct vo_wayland_state *wl = data; + + void *map = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0); + close(fd); + + if (map != MAP_FAILED) { + wl->format_map = map; + wl->format_size = size; + } +} + +static void main_device(void *data, + struct zwp_linux_dmabuf_feedback_v1 *zwp_linux_dmabuf_feedback_v1, + struct wl_array *device) +{ +} + +static void tranche_done(void *data, + struct zwp_linux_dmabuf_feedback_v1 *zwp_linux_dmabuf_feedback_v1) +{ +} + +static void tranche_target_device(void *data, + struct zwp_linux_dmabuf_feedback_v1 *zwp_linux_dmabuf_feedback_v1, + struct wl_array *device) +{ +} + +static void tranche_formats(void *data, + struct zwp_linux_dmabuf_feedback_v1 *zwp_linux_dmabuf_feedback_v1, + struct wl_array *indices) +{ +} + +static void tranche_flags(void *data, + struct zwp_linux_dmabuf_feedback_v1 *zwp_linux_dmabuf_feedback_v1, + uint32_t flags) +{ +} + +static const struct zwp_linux_dmabuf_feedback_v1_listener dmabuf_feedback_listener = { + done, + format_table, + main_device, + tranche_done, + tranche_target_device, + tranche_formats, + tranche_flags, +}; + +static void registry_handle_add(void *data, struct wl_registry *reg, uint32_t id, + const char *interface, uint32_t ver) +{ + int found = 1; + struct vo_wayland_state *wl = data; + + if (!strcmp(interface, wl_compositor_interface.name) && (ver >= 4) && found++) { +#ifdef HAVE_WAYLAND_1_22 + ver = MPMIN(ver, 6); /* Cap at 6 in case new events are added later. */ +#else + ver = 4; +#endif + wl->compositor = wl_registry_bind(reg, id, &wl_compositor_interface, ver); + wl->surface = wl_compositor_create_surface(wl->compositor); + wl->video_surface = wl_compositor_create_surface(wl->compositor); + wl->osd_surface = wl_compositor_create_surface(wl->compositor); + + /* never accept input events on anything besides the main surface */ + struct wl_region *region = wl_compositor_create_region(wl->compositor); + wl_surface_set_input_region(wl->osd_surface, region); + wl_surface_set_input_region(wl->video_surface, region); + wl_region_destroy(region); + + wl->cursor_surface = wl_compositor_create_surface(wl->compositor); + wl_surface_add_listener(wl->surface, &surface_listener, wl); + } + + if (!strcmp(interface, wl_subcompositor_interface.name) && (ver >= 1) && found++) { + wl->subcompositor = wl_registry_bind(reg, id, &wl_subcompositor_interface, 1); + } + + if (!strcmp (interface, zwp_linux_dmabuf_v1_interface.name) && (ver >= 4) && found++) { + wl->dmabuf = wl_registry_bind(reg, id, &zwp_linux_dmabuf_v1_interface, 4); + wl->dmabuf_feedback = zwp_linux_dmabuf_v1_get_default_feedback(wl->dmabuf); + zwp_linux_dmabuf_feedback_v1_add_listener(wl->dmabuf_feedback, &dmabuf_feedback_listener, wl); + } + + if (!strcmp (interface, wp_viewporter_interface.name) && (ver >= 1) && found++) { + wl->viewporter = wl_registry_bind (reg, id, &wp_viewporter_interface, 1); + } + + if (!strcmp(interface, wl_data_device_manager_interface.name) && (ver >= 3) && found++) { + wl->dnd_devman = wl_registry_bind(reg, id, &wl_data_device_manager_interface, 3); + } + + if (!strcmp(interface, wl_output_interface.name) && (ver >= 2) && found++) { + struct vo_wayland_output *output = talloc_zero(wl, struct vo_wayland_output); + + output->wl = wl; + output->id = id; + output->scale = 1; + output->name = ""; + + ver = MPMIN(ver, 4); /* Cap at 4 in case new events are added later. */ + output->output = wl_registry_bind(reg, id, &wl_output_interface, ver); + wl_output_add_listener(output->output, &output_listener, output); + wl_list_insert(&wl->output_list, &output->link); + } + + if (!strcmp(interface, wl_seat_interface.name) && found++) { + wl->seat = wl_registry_bind(reg, id, &wl_seat_interface, 1); + wl_seat_add_listener(wl->seat, &seat_listener, wl); + } + + if (!strcmp(interface, wl_shm_interface.name) && found++) { + wl->shm = wl_registry_bind(reg, id, &wl_shm_interface, 1); + } + +#if HAVE_WAYLAND_PROTOCOLS_1_27 + if (!strcmp(interface, wp_content_type_manager_v1_interface.name) && found++) { + wl->content_type_manager = wl_registry_bind(reg, id, &wp_content_type_manager_v1_interface, 1); + } + + if (!strcmp(interface, wp_single_pixel_buffer_manager_v1_interface.name) && found++) { + wl->single_pixel_manager = wl_registry_bind(reg, id, &wp_single_pixel_buffer_manager_v1_interface, 1); + } +#endif + +#if HAVE_WAYLAND_PROTOCOLS_1_31 + if (!strcmp(interface, wp_fractional_scale_manager_v1_interface.name) && found++) { + wl->fractional_scale_manager = wl_registry_bind(reg, id, &wp_fractional_scale_manager_v1_interface, 1); + } +#endif + +#if HAVE_WAYLAND_PROTOCOLS_1_32 + if (!strcmp(interface, wp_cursor_shape_manager_v1_interface.name) && found++) { + wl->cursor_shape_manager = wl_registry_bind(reg, id, &wp_cursor_shape_manager_v1_interface, 1); + } +#endif + + if (!strcmp(interface, wp_presentation_interface.name) && found++) { + wl->presentation = wl_registry_bind(reg, id, &wp_presentation_interface, 1); + wp_presentation_add_listener(wl->presentation, &pres_listener, wl); + } + + if (!strcmp(interface, xdg_wm_base_interface.name) && found++) { + ver = MPMIN(ver, 6); /* Cap at 6 in case new events are added later. */ + wl->wm_base = wl_registry_bind(reg, id, &xdg_wm_base_interface, ver); + xdg_wm_base_add_listener(wl->wm_base, &xdg_wm_base_listener, wl); + } + + if (!strcmp(interface, zxdg_decoration_manager_v1_interface.name) && found++) { + wl->xdg_decoration_manager = wl_registry_bind(reg, id, &zxdg_decoration_manager_v1_interface, 1); + } + + if (!strcmp(interface, zwp_idle_inhibit_manager_v1_interface.name) && found++) { + wl->idle_inhibit_manager = wl_registry_bind(reg, id, &zwp_idle_inhibit_manager_v1_interface, 1); + } + + if (found > 1) + MP_VERBOSE(wl, "Registered for protocol %s\n", interface); +} + +static void registry_handle_remove(void *data, struct wl_registry *reg, uint32_t id) +{ + struct vo_wayland_state *wl = data; + struct vo_wayland_output *output, *tmp; + wl_list_for_each_safe(output, tmp, &wl->output_list, link) { + if (output->id == id) { + remove_output(output); + return; + } + } +} + +static const struct wl_registry_listener registry_listener = { + registry_handle_add, + registry_handle_remove, +}; + +/* Static functions */ +static void check_dnd_fd(struct vo_wayland_state *wl) +{ + if (wl->dnd_fd == -1) + return; + + struct pollfd fdp = { wl->dnd_fd, POLLIN | POLLHUP, 0 }; + if (poll(&fdp, 1, 0) <= 0) + return; + + if (fdp.revents & POLLIN) { + ptrdiff_t offset = 0; + size_t data_read = 0; + const size_t chunk_size = 1; + uint8_t *buffer = ta_zalloc_size(wl, chunk_size); + if (!buffer) + goto end; + + while ((data_read = read(wl->dnd_fd, buffer + offset, chunk_size)) > 0) { + offset += data_read; + buffer = ta_realloc_size(wl, buffer, offset + chunk_size); + memset(buffer + offset, 0, chunk_size); + if (!buffer) + goto end; + } + + MP_VERBOSE(wl, "Read %td bytes from the DND fd\n", offset); + + struct bstr file_list = bstr0(buffer); + mp_event_drop_mime_data(wl->vo->input_ctx, wl->dnd_mime_type, + file_list, wl->dnd_action); + talloc_free(buffer); +end: + if (wl->dnd_mime_type) + talloc_free(wl->dnd_mime_type); + + if (wl->dnd_action >= 0 && wl->dnd_offer) + wl_data_offer_finish(wl->dnd_offer); + + wl->dnd_action = -1; + wl->dnd_mime_type = NULL; + wl->dnd_mime_score = 0; + } + + if (fdp.revents & (POLLIN | POLLERR | POLLHUP)) { + close(wl->dnd_fd); + wl->dnd_fd = -1; + } +} + +static int check_for_resize(struct vo_wayland_state *wl, int edge_pixels, + enum xdg_toplevel_resize_edge *edge) +{ + if (wl->vo_opts->fullscreen || wl->vo_opts->window_maximized) + return 0; + + int pos[2] = { wl->mouse_x, wl->mouse_y }; + int left_edge = pos[0] < edge_pixels; + int top_edge = pos[1] < edge_pixels; + int right_edge = pos[0] > (mp_rect_w(wl->geometry) - edge_pixels); + int bottom_edge = pos[1] > (mp_rect_h(wl->geometry) - edge_pixels); + + if (left_edge) { + *edge = XDG_TOPLEVEL_RESIZE_EDGE_LEFT; + if (top_edge) + *edge = XDG_TOPLEVEL_RESIZE_EDGE_TOP_LEFT; + else if (bottom_edge) + *edge = XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_LEFT; + } else if (right_edge) { + *edge = XDG_TOPLEVEL_RESIZE_EDGE_RIGHT; + if (top_edge) + *edge = XDG_TOPLEVEL_RESIZE_EDGE_TOP_RIGHT; + else if (bottom_edge) + *edge = XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_RIGHT; + } else if (top_edge) { + *edge = XDG_TOPLEVEL_RESIZE_EDGE_TOP; + } else if (bottom_edge) { + *edge = XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM; + } else { + *edge = 0; + return 0; + } + + return 1; +} + +static bool create_input(struct vo_wayland_state *wl) +{ + wl->xkb_context = xkb_context_new(XKB_CONTEXT_NO_FLAGS); + + if (!wl->xkb_context) { + MP_ERR(wl, "failed to initialize input: check xkbcommon\n"); + return 1; + } + + return 0; +} + +static int create_viewports(struct vo_wayland_state *wl) +{ + if (wl->viewporter) { + wl->viewport = wp_viewporter_get_viewport(wl->viewporter, wl->surface); + wl->osd_viewport = wp_viewporter_get_viewport(wl->viewporter, wl->osd_surface); + wl->video_viewport = wp_viewporter_get_viewport(wl->viewporter, wl->video_surface); + } + + if (wl->viewporter && (!wl->viewport || !wl->osd_viewport || !wl->video_viewport)) { + MP_ERR(wl, "failed to create viewport interfaces!\n"); + return 1; + } + return 0; +} + +static int create_xdg_surface(struct vo_wayland_state *wl) +{ + wl->xdg_surface = xdg_wm_base_get_xdg_surface(wl->wm_base, wl->surface); + xdg_surface_add_listener(wl->xdg_surface, &xdg_surface_listener, wl); + + wl->xdg_toplevel = xdg_surface_get_toplevel(wl->xdg_surface); + xdg_toplevel_add_listener(wl->xdg_toplevel, &xdg_toplevel_listener, wl); + + if (!wl->xdg_surface || !wl->xdg_toplevel) { + MP_ERR(wl, "failed to create xdg_surface and xdg_toplevel!\n"); + return 1; + } + return 0; +} + +static void add_feedback(struct vo_wayland_feedback_pool *fback_pool, + struct wp_presentation_feedback *fback) +{ + for (int i = 0; i < fback_pool->len; ++i) { + if (!fback_pool->fback[i]) { + fback_pool->fback[i] = fback; + break; + } else if (i == fback_pool->len - 1) { + // Shouldn't happen in practice. + wp_presentation_feedback_destroy(fback_pool->fback[i]); + fback_pool->fback[i] = fback; + } + } +} + +static void do_minimize(struct vo_wayland_state *wl) +{ + if (!wl->xdg_toplevel) + return; + if (wl->vo_opts->window_minimized) + xdg_toplevel_set_minimized(wl->xdg_toplevel); +} + +static char **get_displays_spanned(struct vo_wayland_state *wl) +{ + char **names = NULL; + int displays_spanned = 0; + struct vo_wayland_output *output; + wl_list_for_each(output, &wl->output_list, link) { + if (output->has_surface) { + char *name = output->name ? output->name : output->model; + MP_TARRAY_APPEND(NULL, names, displays_spanned, + talloc_strdup(NULL, name)); + } + } + MP_TARRAY_APPEND(NULL, names, displays_spanned, NULL); + return names; +} + +static int get_mods(struct vo_wayland_state *wl) +{ + static char* const mod_names[] = { + XKB_MOD_NAME_SHIFT, + XKB_MOD_NAME_CTRL, + XKB_MOD_NAME_ALT, + XKB_MOD_NAME_LOGO, + }; + + static const int mods[] = { + MP_KEY_MODIFIER_SHIFT, + MP_KEY_MODIFIER_CTRL, + MP_KEY_MODIFIER_ALT, + MP_KEY_MODIFIER_META, + }; + + int modifiers = 0; + + for (int n = 0; n < MP_ARRAY_SIZE(mods); n++) { + xkb_mod_index_t index = xkb_keymap_mod_get_index(wl->xkb_keymap, mod_names[n]); + if (!xkb_state_mod_index_is_consumed(wl->xkb_state, wl->keyboard_code, index) + && xkb_state_mod_index_is_active(wl->xkb_state, index, + XKB_STATE_MODS_DEPRESSED)) + modifiers |= mods[n]; + } + return modifiers; +} + +static void get_shape_device(struct vo_wayland_state *wl) +{ +#if HAVE_WAYLAND_PROTOCOLS_1_32 + if (!wl->cursor_shape_device && wl->cursor_shape_manager) { + wl->cursor_shape_device = wp_cursor_shape_manager_v1_get_pointer(wl->cursor_shape_manager, + wl->pointer); + } +#endif +} + +static int greatest_common_divisor(int a, int b) +{ + int rem = a % b; + if (rem == 0) + return b; + return greatest_common_divisor(b, rem); +} + +static void guess_focus(struct vo_wayland_state *wl) +{ + // We can't actually know if the window is focused or not in wayland, + // so just guess it with some common sense. Obviously won't work if + // the user has no keyboard. + if ((!wl->focused && wl->activated && wl->has_keyboard_input) || + (wl->focused && !wl->activated)) + { + wl->focused = !wl->focused; + wl->pending_vo_events |= VO_EVENT_FOCUS; + } +} + +static struct vo_wayland_output *find_output(struct vo_wayland_state *wl) +{ + int index = 0; + struct mp_vo_opts *opts = wl->vo_opts; + int screen_id = opts->fullscreen ? opts->fsscreen_id : opts->screen_id; + char *screen_name = opts->fullscreen ? opts->fsscreen_name : opts->screen_name; + struct vo_wayland_output *output = NULL; + struct vo_wayland_output *fallback_output = NULL; + wl_list_for_each(output, &wl->output_list, link) { + if (index == 0) + fallback_output = output; + if (screen_id == -1 && !screen_name) + return output; + if (screen_id == -1 && screen_name && !strcmp(screen_name, output->name)) + return output; + if (screen_id == -1 && screen_name && !strcmp(screen_name, output->model)) + return output; + if (screen_id == index++) + return output; + } + if (!fallback_output) { + MP_ERR(wl, "No screens could be found!\n"); + return NULL; + } else if (screen_id >= 0) { + MP_WARN(wl, "Screen index %i not found/unavailable! Falling back to screen 0!\n", screen_id); + } else if (screen_name && screen_name[0]) { + MP_WARN(wl, "Screen name %s not found/unavailable! Falling back to screen 0!\n", screen_name); + } + return fallback_output; +} + +static int lookupkey(int key) +{ + const char *passthrough_keys = " -+*/<>`~!@#$%^&()_{}:;\"\',.?\\|=[]"; + + int mpkey = 0; + if ((key >= 'a' && key <= 'z') || (key >= 'A' && key <= 'Z') || + (key >= '0' && key <= '9') || + (key > 0 && key < 256 && strchr(passthrough_keys, key))) + mpkey = key; + + if (!mpkey) + mpkey = lookup_keymap_table(keymap, key); + + return mpkey; +} + +static void prepare_resize(struct vo_wayland_state *wl, int width, int height) +{ + if (!width) + width = mp_rect_w(wl->geometry) / wl->scaling; + if (!height) + height = mp_rect_h(wl->geometry) / wl->scaling; + xdg_surface_set_window_geometry(wl->xdg_surface, 0, 0, width, height); + wl->pending_vo_events |= VO_EVENT_RESIZE; +} + +static void request_decoration_mode(struct vo_wayland_state *wl, uint32_t mode) +{ + wl->requested_decoration = mode; + zxdg_toplevel_decoration_v1_set_mode(wl->xdg_toplevel_decoration, mode); +} + +static void rescale_geometry(struct vo_wayland_state *wl, double old_scale) +{ + double factor = old_scale / wl->scaling; + wl->window_size.x1 /= factor; + wl->window_size.y1 /= factor; + wl->geometry.x1 /= factor; + wl->geometry.y1 /= factor; +} + +static void clean_feedback_pool(struct vo_wayland_feedback_pool *fback_pool) +{ + for (int i = 0; i < fback_pool->len; ++i) { + if (fback_pool->fback[i]) { + wp_presentation_feedback_destroy(fback_pool->fback[i]); + fback_pool->fback[i] = NULL; + } + } +} + +static void remove_feedback(struct vo_wayland_feedback_pool *fback_pool, + struct wp_presentation_feedback *fback) +{ + for (int i = 0; i < fback_pool->len; ++i) { + if (fback_pool->fback[i] == fback) { + wp_presentation_feedback_destroy(fback); + fback_pool->fback[i] = NULL; + break; + } + } +} + +static void remove_output(struct vo_wayland_output *out) +{ + if (!out) + return; + + MP_VERBOSE(out->wl, "Deregistering output %s %s (0x%x)\n", out->make, + out->model, out->id); + wl_list_remove(&out->link); + wl_output_destroy(out->output); + talloc_free(out->make); + talloc_free(out->model); + talloc_free(out); + return; +} + +static void set_content_type(struct vo_wayland_state *wl) +{ + if (!wl->content_type_manager) + return; +#if HAVE_WAYLAND_PROTOCOLS_1_27 + // handle auto; + if (wl->vo_opts->content_type == -1) { + wp_content_type_v1_set_content_type(wl->content_type, wl->current_content_type); + } else { + wp_content_type_v1_set_content_type(wl->content_type, wl->vo_opts->content_type); + } +#endif +} + +static void set_cursor_shape(struct vo_wayland_state *wl) +{ +#if HAVE_WAYLAND_PROTOCOLS_1_32 + wp_cursor_shape_device_v1_set_shape(wl->cursor_shape_device, wl->pointer_id, + WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DEFAULT); +#endif +} + +static int set_cursor_visibility(struct vo_wayland_state *wl, bool on) +{ + wl->cursor_visible = on; + if (on) { + if (wl->cursor_shape_device) { + set_cursor_shape(wl); + } else { + if (spawn_cursor(wl)) + return VO_FALSE; + struct wl_cursor_image *img = wl->default_cursor->images[0]; + struct wl_buffer *buffer = wl_cursor_image_get_buffer(img); + if (!buffer) + return VO_FALSE; + int scale = MPMAX(wl->scaling, 1); + wl_pointer_set_cursor(wl->pointer, wl->pointer_id, wl->cursor_surface, + img->hotspot_x / scale, img->hotspot_y / scale); + wl_surface_set_buffer_scale(wl->cursor_surface, scale); + wl_surface_attach(wl->cursor_surface, buffer, 0, 0); + wl_surface_damage_buffer(wl->cursor_surface, 0, 0, img->width, img->height); + } + wl_surface_commit(wl->cursor_surface); + } else { + wl_pointer_set_cursor(wl->pointer, wl->pointer_id, NULL, 0, 0); + } + return VO_TRUE; +} + +static void set_geometry(struct vo_wayland_state *wl, bool resize) +{ + struct vo *vo = wl->vo; + if (!wl->current_output) + return; + + struct vo_win_geometry geo; + struct mp_rect screenrc = wl->current_output->geometry; + vo_calc_window_geometry2(vo, &screenrc, wl->scaling, &geo); + vo_apply_window_geometry(vo, &geo); + + int gcd = greatest_common_divisor(vo->dwidth, vo->dheight); + wl->reduced_width = vo->dwidth / gcd; + wl->reduced_height = vo->dheight / gcd; + + if (!wl->initial_size_hint) + wl->window_size = (struct mp_rect){0, 0, vo->dwidth, vo->dheight}; + wl->initial_size_hint = false; + + if (resize) { + if (!wl->locked_size) + wl->geometry = wl->window_size; + prepare_resize(wl, 0, 0); + } +} + +static void set_input_region(struct vo_wayland_state *wl, bool passthrough) +{ + if (passthrough) { + struct wl_region *region = wl_compositor_create_region(wl->compositor); + wl_surface_set_input_region(wl->surface, region); + wl_region_destroy(region); + } else { + wl_surface_set_input_region(wl->surface, NULL); + } +} + +static int set_screensaver_inhibitor(struct vo_wayland_state *wl, int state) +{ + if (!wl->idle_inhibit_manager) + return VO_NOTIMPL; + if (state == (!!wl->idle_inhibitor)) + return VO_TRUE; + if (state) { + MP_VERBOSE(wl, "Enabling idle inhibitor\n"); + struct zwp_idle_inhibit_manager_v1 *mgr = wl->idle_inhibit_manager; + wl->idle_inhibitor = zwp_idle_inhibit_manager_v1_create_inhibitor(mgr, wl->surface); + } else { + MP_VERBOSE(wl, "Disabling the idle inhibitor\n"); + zwp_idle_inhibitor_v1_destroy(wl->idle_inhibitor); + wl->idle_inhibitor = NULL; + } + return VO_TRUE; +} + +static void set_surface_scaling(struct vo_wayland_state *wl) +{ + if (wl->fractional_scale_manager) + return; + + // dmabuf_wayland is always wl->scaling = 1 + double old_scale = wl->scaling; + wl->scaling = !wl->using_dmabuf_wayland ? wl->current_output->scale : 1; + + rescale_geometry(wl, old_scale); + wl_surface_set_buffer_scale(wl->surface, wl->scaling); +} + +static void set_window_bounds(struct vo_wayland_state *wl) +{ + // If the user has set geometry/autofit and the option is auto, + // don't use these. + if (wl->opts->configure_bounds == -1 && (wl->vo_opts->geometry.wh_valid || + wl->vo_opts->autofit.wh_valid || wl->vo_opts->autofit_larger.wh_valid || + wl->vo_opts->autofit_smaller.wh_valid)) + { + return; + } + + if (wl->bounded_width && wl->bounded_width < wl->window_size.x1) + wl->window_size.x1 = wl->bounded_width; + if (wl->bounded_height && wl->bounded_height < wl->window_size.y1) + wl->window_size.y1 = wl->bounded_height; +} + +static int spawn_cursor(struct vo_wayland_state *wl) +{ + /* Don't use this if we have cursor-shape. */ + if (wl->cursor_shape_device) + return 0; + /* Reuse if size is identical */ + if (!wl->pointer || wl->allocated_cursor_scale == wl->scaling) + return 0; + else if (wl->cursor_theme) + wl_cursor_theme_destroy(wl->cursor_theme); + + const char *xcursor_theme = getenv("XCURSOR_THEME"); + const char *size_str = getenv("XCURSOR_SIZE"); + int size = 24; + if (size_str != NULL) { + errno = 0; + char *end; + long size_long = strtol(size_str, &end, 10); + if (!*end && !errno && size_long > 0 && size_long <= INT_MAX) + size = (int)size_long; + } + + wl->cursor_theme = wl_cursor_theme_load(xcursor_theme, size*wl->scaling, wl->shm); + if (!wl->cursor_theme) { + MP_ERR(wl, "Unable to load cursor theme!\n"); + return 1; + } + + wl->default_cursor = wl_cursor_theme_get_cursor(wl->cursor_theme, "left_ptr"); + if (!wl->default_cursor) { + MP_ERR(wl, "Unable to load cursor theme!\n"); + return 1; + } + + wl->allocated_cursor_scale = wl->scaling; + + return 0; +} + +static void toggle_fullscreen(struct vo_wayland_state *wl) +{ + if (!wl->xdg_toplevel) + return; + wl->state_change = true; + bool specific_screen = wl->vo_opts->fsscreen_id >= 0 || wl->vo_opts->fsscreen_name; + if (wl->vo_opts->fullscreen && !specific_screen) { + xdg_toplevel_set_fullscreen(wl->xdg_toplevel, NULL); + } else if (wl->vo_opts->fullscreen && specific_screen) { + struct vo_wayland_output *output = find_output(wl); + xdg_toplevel_set_fullscreen(wl->xdg_toplevel, output->output); + } else { + xdg_toplevel_unset_fullscreen(wl->xdg_toplevel); + } +} + +static void toggle_maximized(struct vo_wayland_state *wl) +{ + if (!wl->xdg_toplevel) + return; + wl->state_change = true; + if (wl->vo_opts->window_maximized) { + xdg_toplevel_set_maximized(wl->xdg_toplevel); + } else { + xdg_toplevel_unset_maximized(wl->xdg_toplevel); + } +} + +static void update_app_id(struct vo_wayland_state *wl) +{ + if (!wl->xdg_toplevel) + return; + xdg_toplevel_set_app_id(wl->xdg_toplevel, wl->vo_opts->appid); +} + +static int update_window_title(struct vo_wayland_state *wl, const char *title) +{ + if (!wl->xdg_toplevel) + return VO_NOTAVAIL; + /* The xdg-shell protocol requires that the title is UTF-8. */ + void *tmp = talloc_new(NULL); + struct bstr b_title = bstr_sanitize_utf8_latin1(tmp, bstr0(title)); + xdg_toplevel_set_title(wl->xdg_toplevel, bstrto0(tmp, b_title)); + talloc_free(tmp); + return VO_TRUE; +} + +static void window_move(struct vo_wayland_state *wl, uint32_t serial) +{ + if (wl->xdg_toplevel) + xdg_toplevel_move(wl->xdg_toplevel, wl->seat, serial); +} + +static void wayland_dispatch_events(struct vo_wayland_state *wl, int nfds, int64_t timeout_ns) +{ + if (wl->display_fd == -1) + return; + + struct pollfd fds[2] = { + {.fd = wl->display_fd, .events = POLLIN }, + {.fd = wl->wakeup_pipe[0], .events = POLLIN }, + }; + + while (wl_display_prepare_read(wl->display) != 0) + wl_display_dispatch_pending(wl->display); + wl_display_flush(wl->display); + + mp_poll(fds, nfds, timeout_ns); + + if (fds[0].revents & POLLIN) { + wl_display_read_events(wl->display); + } else { + wl_display_cancel_read(wl->display); + } + + if (fds[0].revents & (POLLERR | POLLHUP | POLLNVAL)) { + MP_FATAL(wl, "Error occurred on the display fd\n"); + wl->display_fd = -1; + mp_input_put_key(wl->vo->input_ctx, MP_KEY_CLOSE_WIN); + } + + if (fds[1].revents & POLLIN) + mp_flush_wakeup_pipe(wl->wakeup_pipe[0]); + + wl_display_dispatch_pending(wl->display); +} + +/* Non-static */ +int vo_wayland_allocate_memfd(struct vo *vo, size_t size) +{ +#if !HAVE_MEMFD_CREATE + return VO_ERROR; +#else + int fd = memfd_create("mpv", MFD_CLOEXEC | MFD_ALLOW_SEALING); + if (fd < 0) { + MP_ERR(vo, "Failed to allocate memfd: %s\n", mp_strerror(errno)); + return VO_ERROR; + } + + fcntl(fd, F_ADD_SEALS, F_SEAL_SHRINK | F_SEAL_SEAL); + + if (posix_fallocate(fd, 0, size) == 0) + return fd; + + close(fd); + MP_ERR(vo, "Failed to allocate memfd: %s\n", mp_strerror(errno)); + + return VO_ERROR; +#endif +} + +bool vo_wayland_check_visible(struct vo *vo) +{ + struct vo_wayland_state *wl = vo->wl; + bool render = !wl->hidden || wl->vo_opts->force_render; + wl->frame_wait = true; + return render; +} + +int vo_wayland_control(struct vo *vo, int *events, int request, void *arg) +{ + struct vo_wayland_state *wl = vo->wl; + struct mp_vo_opts *opts = wl->vo_opts; + wl_display_dispatch_pending(wl->display); + + switch (request) { + case VOCTRL_CHECK_EVENTS: { + check_dnd_fd(wl); + *events |= wl->pending_vo_events; + if (*events & VO_EVENT_RESIZE) { + *events |= VO_EVENT_EXPOSE; + wl->frame_wait = false; + wl->timeout_count = 0; + wl->hidden = false; + } + wl->pending_vo_events = 0; + return VO_TRUE; + } + case VOCTRL_VO_OPTS_CHANGED: { + void *opt; + while (m_config_cache_get_next_changed(wl->vo_opts_cache, &opt)) { + if (opt == &opts->appid) + update_app_id(wl); + if (opt == &opts->border) + { + // This is stupid but the value of border shouldn't be written + // unless we get a configure event. Change it back to its old + // value and let configure_decorations handle it after the request. + if (wl->xdg_toplevel_decoration) { + int requested_border_mode = opts->border; + opts->border = !opts->border; + m_config_cache_write_opt(wl->vo_opts_cache, + &opts->border); + request_decoration_mode( + wl, requested_border_mode ? + ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE : + ZXDG_TOPLEVEL_DECORATION_V1_MODE_CLIENT_SIDE); + } else { + opts->border = false; + m_config_cache_write_opt(wl->vo_opts_cache, + &wl->vo_opts->border); + } + } + if (opt == &opts->content_type) + set_content_type(wl); + if (opt == &opts->cursor_passthrough) + set_input_region(wl, opts->cursor_passthrough); + if (opt == &opts->fullscreen) + toggle_fullscreen(wl); + if (opt == &opts->hidpi_window_scale) + set_geometry(wl, true); + if (opt == &opts->window_maximized) + toggle_maximized(wl); + if (opt == &opts->window_minimized) + do_minimize(wl); + if (opt == &opts->geometry || opt == &opts->autofit || + opt == &opts->autofit_smaller || opt == &opts->autofit_larger) + { + set_geometry(wl, true); + } + } + return VO_TRUE; + } + case VOCTRL_CONTENT_TYPE: { +#if HAVE_WAYLAND_PROTOCOLS_1_27 + wl->current_content_type = *(enum mp_content_type *)arg; + set_content_type(wl); +#endif + return VO_TRUE; + } + case VOCTRL_GET_FOCUSED: { + *(bool *)arg = wl->focused; + return VO_TRUE; + } + case VOCTRL_GET_DISPLAY_NAMES: { + *(char ***)arg = get_displays_spanned(wl); + return VO_TRUE; + } + case VOCTRL_GET_UNFS_WINDOW_SIZE: { + int *s = arg; + if (wl->vo_opts->window_maximized || wl->tiled) { + s[0] = mp_rect_w(wl->geometry); + s[1] = mp_rect_h(wl->geometry); + } else { + s[0] = mp_rect_w(wl->window_size); + s[1] = mp_rect_h(wl->window_size); + } + return VO_TRUE; + } + case VOCTRL_SET_UNFS_WINDOW_SIZE: { + int *s = arg; + wl->window_size.x0 = 0; + wl->window_size.y0 = 0; + wl->window_size.x1 = s[0]; + wl->window_size.y1 = s[1]; + if (!wl->vo_opts->fullscreen && !wl->tiled) { + if (wl->vo_opts->window_maximized) { + xdg_toplevel_unset_maximized(wl->xdg_toplevel); + wl_display_dispatch_pending(wl->display); + /* Make sure the compositor let us unmaximize */ + if (wl->vo_opts->window_maximized) + return VO_TRUE; + } + wl->geometry = wl->window_size; + prepare_resize(wl, 0, 0); + } + return VO_TRUE; + } + case VOCTRL_GET_DISPLAY_FPS: { + struct vo_wayland_output *out; + if (wl->current_output) { + out = wl->current_output; + } else { + out = find_output(wl); + } + if (!out) + return VO_NOTAVAIL; + *(double *)arg = out->refresh_rate; + return VO_TRUE; + } + case VOCTRL_GET_DISPLAY_RES: { + struct vo_wayland_output *out; + if (wl->current_output) { + out = wl->current_output; + } else { + out = find_output(wl); + } + if (!out) + return VO_NOTAVAIL; + ((int *)arg)[0] = out->geometry.x1; + ((int *)arg)[1] = out->geometry.y1; + return VO_TRUE; + } + case VOCTRL_GET_HIDPI_SCALE: { + if (!wl->scaling) + return VO_NOTAVAIL; + *(double *)arg = wl->scaling; + return VO_TRUE; + } + case VOCTRL_UPDATE_WINDOW_TITLE: + return update_window_title(wl, (const char *)arg); + case VOCTRL_SET_CURSOR_VISIBILITY: + if (!wl->pointer) + return VO_NOTAVAIL; + return set_cursor_visibility(wl, *(bool *)arg); + case VOCTRL_KILL_SCREENSAVER: + return set_screensaver_inhibitor(wl, true); + case VOCTRL_RESTORE_SCREENSAVER: + return set_screensaver_inhibitor(wl, false); + } + + return VO_NOTIMPL; +} + +void vo_wayland_handle_fractional_scale(struct vo_wayland_state *wl) +{ + if (wl->fractional_scale_manager && wl->viewport) + wp_viewport_set_destination(wl->viewport, + round(mp_rect_w(wl->geometry) / wl->scaling), + round(mp_rect_h(wl->geometry) / wl->scaling)); +} + +bool vo_wayland_init(struct vo *vo) +{ + vo->wl = talloc_zero(NULL, struct vo_wayland_state); + struct vo_wayland_state *wl = vo->wl; + + *wl = (struct vo_wayland_state) { + .display = wl_display_connect(NULL), + .vo = vo, + .log = mp_log_new(wl, vo->log, "wayland"), + .bounded_width = 0, + .bounded_height = 0, + .refresh_interval = 0, + .scaling = 1, + .wakeup_pipe = {-1, -1}, + .display_fd = -1, + .dnd_fd = -1, + .cursor_visible = true, + .vo_opts_cache = m_config_cache_alloc(wl, vo->global, &vo_sub_opts), + }; + wl->vo_opts = wl->vo_opts_cache->opts; + wl->using_dmabuf_wayland = !strcmp(wl->vo->driver->name, "dmabuf-wayland"); + + wl_list_init(&wl->output_list); + + if (!wl->display) + goto err; + + if (create_input(wl)) + goto err; + + wl->registry = wl_display_get_registry(wl->display); + wl_registry_add_listener(wl->registry, ®istry_listener, wl); + + /* Do a roundtrip to run the registry */ + wl_display_roundtrip(wl->display); + + if (!wl->surface) { + MP_FATAL(wl, "Compositor doesn't support %s (ver. 4)\n", + wl_compositor_interface.name); + goto err; + } + + if (!wl->wm_base) { + MP_FATAL(wl, "Compositor doesn't support the required %s protocol!\n", + xdg_wm_base_interface.name); + goto err; + } + + if (!wl_list_length(&wl->output_list)) { + MP_FATAL(wl, "No outputs found or compositor doesn't support %s (ver. 2)\n", + wl_output_interface.name); + goto err; + } + + /* Can't be initialized during registry due to multi-protocol dependence */ + if (create_viewports(wl)) + goto err; + + if (create_xdg_surface(wl)) + goto err; + + if (wl->subcompositor) { + wl->osd_subsurface = wl_subcompositor_get_subsurface(wl->subcompositor, wl->osd_surface, wl->video_surface); + wl->video_subsurface = wl_subcompositor_get_subsurface(wl->subcompositor, wl->video_surface, wl->surface); + } + +#if HAVE_WAYLAND_PROTOCOLS_1_27 + if (wl->content_type_manager) { + wl->content_type = wp_content_type_manager_v1_get_surface_content_type(wl->content_type_manager, wl->surface); + } else { + MP_VERBOSE(wl, "Compositor doesn't support the %s protocol!\n", + wp_content_type_manager_v1_interface.name); + } + + if (!wl->single_pixel_manager) { + MP_VERBOSE(wl, "Compositor doesn't support the %s protocol!\n", + wp_single_pixel_buffer_manager_v1_interface.name); + } +#endif + +#if HAVE_WAYLAND_PROTOCOLS_1_31 + if (wl->fractional_scale_manager) { + wl->fractional_scale = wp_fractional_scale_manager_v1_get_fractional_scale(wl->fractional_scale_manager, wl->surface); + wp_fractional_scale_v1_add_listener(wl->fractional_scale, &fractional_scale_listener, wl); + } else { + MP_VERBOSE(wl, "Compositor doesn't support the %s protocol!\n", + wp_fractional_scale_manager_v1_interface.name); + } +#endif + + if (wl->dnd_devman && wl->seat) { + wl->dnd_ddev = wl_data_device_manager_get_data_device(wl->dnd_devman, wl->seat); + wl_data_device_add_listener(wl->dnd_ddev, &data_device_listener, wl); + } else if (!wl->dnd_devman) { + MP_VERBOSE(wl, "Compositor doesn't support the %s (ver. 3) protocol!\n", + wl_data_device_manager_interface.name); + } + + if (wl->presentation) { + wl->fback_pool = talloc_zero(wl, struct vo_wayland_feedback_pool); + wl->fback_pool->wl = wl; + wl->fback_pool->len = VO_MAX_SWAPCHAIN_DEPTH; + wl->fback_pool->fback = talloc_zero_array(wl->fback_pool, struct wp_presentation_feedback *, + wl->fback_pool->len); + wl->present = mp_present_initialize(wl, wl->vo_opts, VO_MAX_SWAPCHAIN_DEPTH); + } else { + MP_VERBOSE(wl, "Compositor doesn't support the %s protocol!\n", + wp_presentation_interface.name); + } + + if (wl->xdg_decoration_manager) { + wl->xdg_toplevel_decoration = zxdg_decoration_manager_v1_get_toplevel_decoration(wl->xdg_decoration_manager, wl->xdg_toplevel); + zxdg_toplevel_decoration_v1_add_listener(wl->xdg_toplevel_decoration, &decoration_listener, wl); + request_decoration_mode( + wl, wl->vo_opts->border ? + ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE : + ZXDG_TOPLEVEL_DECORATION_V1_MODE_CLIENT_SIDE); + } else { + wl->vo_opts->border = false; + m_config_cache_write_opt(wl->vo_opts_cache, + &wl->vo_opts->border); + MP_VERBOSE(wl, "Compositor doesn't support the %s protocol!\n", + zxdg_decoration_manager_v1_interface.name); + } + + if (!wl->idle_inhibit_manager) { + MP_VERBOSE(wl, "Compositor doesn't support the %s protocol!\n", + zwp_idle_inhibit_manager_v1_interface.name); + } + + wl->opts = mp_get_config_group(wl, wl->vo->global, &wayland_conf); + wl->display_fd = wl_display_get_fd(wl->display); + + update_app_id(wl); + mp_make_wakeup_pipe(wl->wakeup_pipe); + + wl->callback_surface = wl->using_dmabuf_wayland ? wl->video_surface : wl->surface; + wl->frame_callback = wl_surface_frame(wl->callback_surface); + wl_callback_add_listener(wl->frame_callback, &frame_listener, wl); + wl_surface_commit(wl->surface); + + /* Do another roundtrip to ensure all of the above is initialized + * before mpv does anything else. */ + wl_display_roundtrip(wl->display); + + return true; + +err: + vo_wayland_uninit(vo); + return false; +} + +bool vo_wayland_reconfig(struct vo *vo) +{ + struct vo_wayland_state *wl = vo->wl; + + MP_VERBOSE(wl, "Reconfiguring!\n"); + + if (!wl->current_output) { + wl->current_output = find_output(wl); + if (!wl->current_output) + return false; + set_surface_scaling(wl); + wl->pending_vo_events |= VO_EVENT_DPI; + } + + if (wl->vo_opts->auto_window_resize || !wl->configured) + set_geometry(wl, false); + + if (wl->opts->configure_bounds) + set_window_bounds(wl); + + if (!wl->configured || !wl->locked_size) { + wl->geometry = wl->window_size; + wl->configured = true; + } + + if (wl->vo_opts->cursor_passthrough) + set_input_region(wl, true); + + if (wl->vo_opts->fullscreen) + toggle_fullscreen(wl); + + if (wl->vo_opts->window_maximized) + toggle_maximized(wl); + + if (wl->vo_opts->window_minimized) + do_minimize(wl); + + prepare_resize(wl, 0, 0); + + return true; +} + +void vo_wayland_set_opaque_region(struct vo_wayland_state *wl, bool alpha) +{ + const int32_t width = mp_rect_w(wl->geometry); + const int32_t height = mp_rect_h(wl->geometry); + if (!alpha) { + struct wl_region *region = wl_compositor_create_region(wl->compositor); + wl_region_add(region, 0, 0, width, height); + wl_surface_set_opaque_region(wl->surface, region); + wl_region_destroy(region); + } else { + wl_surface_set_opaque_region(wl->surface, NULL); + } +} + +void vo_wayland_uninit(struct vo *vo) +{ + struct vo_wayland_state *wl = vo->wl; + if (!wl) + return; + + mp_input_put_key(wl->vo->input_ctx, MP_INPUT_RELEASE_ALL); + + if (wl->compositor) + wl_compositor_destroy(wl->compositor); + + if (wl->subcompositor) + wl_subcompositor_destroy(wl->subcompositor); + +#if HAVE_WAYLAND_PROTOCOLS_1_32 + if (wl->cursor_shape_device) + wp_cursor_shape_device_v1_destroy(wl->cursor_shape_device); + + if (wl->cursor_shape_manager) + wp_cursor_shape_manager_v1_destroy(wl->cursor_shape_manager); +#endif + + if (wl->cursor_surface) + wl_surface_destroy(wl->cursor_surface); + + if (wl->cursor_theme) + wl_cursor_theme_destroy(wl->cursor_theme); + +#if HAVE_WAYLAND_PROTOCOLS_1_27 + if (wl->content_type) + wp_content_type_v1_destroy(wl->content_type); + + if (wl->content_type_manager) + wp_content_type_manager_v1_destroy(wl->content_type_manager); +#endif + + if (wl->dnd_ddev) + wl_data_device_destroy(wl->dnd_ddev); + + if (wl->dnd_devman) + wl_data_device_manager_destroy(wl->dnd_devman); + + if (wl->dnd_offer) + wl_data_offer_destroy(wl->dnd_offer); + + if (wl->fback_pool) + clean_feedback_pool(wl->fback_pool); + +#if HAVE_WAYLAND_PROTOCOLS_1_31 + if (wl->fractional_scale) + wp_fractional_scale_v1_destroy(wl->fractional_scale); + + if (wl->fractional_scale_manager) + wp_fractional_scale_manager_v1_destroy(wl->fractional_scale_manager); +#endif + + if (wl->frame_callback) + wl_callback_destroy(wl->frame_callback); + + if (wl->idle_inhibitor) + zwp_idle_inhibitor_v1_destroy(wl->idle_inhibitor); + + if (wl->idle_inhibit_manager) + zwp_idle_inhibit_manager_v1_destroy(wl->idle_inhibit_manager); + + if (wl->keyboard) + wl_keyboard_destroy(wl->keyboard); + + if (wl->pointer) + wl_pointer_destroy(wl->pointer); + + if (wl->presentation) + wp_presentation_destroy(wl->presentation); + + if (wl->registry) + wl_registry_destroy(wl->registry); + + if (wl->viewporter) + wp_viewporter_destroy(wl->viewporter); + + if (wl->viewport) + wp_viewport_destroy(wl->viewport); + + if (wl->osd_viewport) + wp_viewport_destroy(wl->osd_viewport); + + if (wl->video_viewport) + wp_viewport_destroy(wl->video_viewport); + + if (wl->dmabuf) + zwp_linux_dmabuf_v1_destroy(wl->dmabuf); + + if (wl->dmabuf_feedback) + zwp_linux_dmabuf_feedback_v1_destroy(wl->dmabuf_feedback); + + if (wl->seat) + wl_seat_destroy(wl->seat); + + if (wl->shm) + wl_shm_destroy(wl->shm); + +#if HAVE_WAYLAND_PROTOCOLS_1_27 + if (wl->single_pixel_manager) + wp_single_pixel_buffer_manager_v1_destroy(wl->single_pixel_manager); +#endif + + if (wl->surface) + wl_surface_destroy(wl->surface); + + if (wl->osd_surface) + wl_surface_destroy(wl->osd_surface); + + if (wl->osd_subsurface) + wl_subsurface_destroy(wl->osd_subsurface); + + if (wl->video_surface) + wl_surface_destroy(wl->video_surface); + + if (wl->video_subsurface) + wl_subsurface_destroy(wl->video_subsurface); + + if (wl->wm_base) + xdg_wm_base_destroy(wl->wm_base); + + if (wl->xdg_decoration_manager) + zxdg_decoration_manager_v1_destroy(wl->xdg_decoration_manager); + + if (wl->xdg_toplevel) + xdg_toplevel_destroy(wl->xdg_toplevel); + + if (wl->xdg_toplevel_decoration) + zxdg_toplevel_decoration_v1_destroy(wl->xdg_toplevel_decoration); + + if (wl->xdg_surface) + xdg_surface_destroy(wl->xdg_surface); + + if (wl->xkb_context) + xkb_context_unref(wl->xkb_context); + + if (wl->xkb_keymap) + xkb_keymap_unref(wl->xkb_keymap); + + if (wl->xkb_state) + xkb_state_unref(wl->xkb_state); + + struct vo_wayland_output *output, *tmp; + wl_list_for_each_safe(output, tmp, &wl->output_list, link) + remove_output(output); + + if (wl->display) + wl_display_disconnect(wl->display); + + munmap(wl->format_map, wl->format_size); + + for (int n = 0; n < 2; n++) + close(wl->wakeup_pipe[n]); + talloc_free(wl); + vo->wl = NULL; +} + +void vo_wayland_wait_frame(struct vo_wayland_state *wl) +{ + int64_t vblank_time = 0; + /* We need some vblank interval to use for the timeout in + * this function. The order of preference of values to use is: + * 1. vsync duration from presentation time + * 2. refresh interval reported by presentation time + * 3. refresh rate of the output reported by the compositor + * 4. make up crap if vblank_time is still <= 0 (better than nothing) */ + + if (wl->use_present && wl->present->head) + vblank_time = wl->present->head->vsync_duration; + + if (vblank_time <= 0 && wl->refresh_interval > 0) + vblank_time = wl->refresh_interval; + + if (vblank_time <= 0 && wl->current_output->refresh_rate > 0) + vblank_time = 1e9 / wl->current_output->refresh_rate; + + // Ideally you should never reach this point. + if (vblank_time <= 0) + vblank_time = 1e9 / 60; + + // Completely arbitrary amount of additional time to wait. + vblank_time += 0.05 * vblank_time; + int64_t finish_time = mp_time_ns() + vblank_time; + + while (wl->frame_wait && finish_time > mp_time_ns()) { + int64_t poll_time = finish_time - mp_time_ns(); + if (poll_time < 0) { + poll_time = 0; + } + wayland_dispatch_events(wl, 1, poll_time); + } + + /* If the compositor does not have presentation time, we cannot be sure + * that this wait is accurate. Do a hacky block with wl_display_roundtrip. */ + if (!wl->use_present && !wl_display_get_error(wl->display)) + wl_display_roundtrip(wl->display); + + /* Only use this heuristic if the compositor doesn't support the suspended state. */ + if (wl->frame_wait && xdg_toplevel_get_version(wl->xdg_toplevel) < 6) { + // Only consider consecutive missed callbacks. + if (wl->timeout_count > 1) { + wl->hidden = true; + return; + } else { + wl->timeout_count += 1; + return; + } + } + + wl->timeout_count = 0; +} + +void vo_wayland_wait_events(struct vo *vo, int64_t until_time_ns) +{ + struct vo_wayland_state *wl = vo->wl; + + int64_t wait_ns = until_time_ns - mp_time_ns(); + int64_t timeout_ns = MPCLAMP(wait_ns, 0, MP_TIME_S_TO_NS(10)); + + wayland_dispatch_events(wl, 2, timeout_ns); +} + +void vo_wayland_wakeup(struct vo *vo) +{ + struct vo_wayland_state *wl = vo->wl; + (void)write(wl->wakeup_pipe[1], &(char){0}, 1); +} |