LookingGlass/client/displayservers/Wayland/wayland.c

1559 lines
40 KiB
C
Raw Normal View History

/*
Looking Glass - KVM FrameRelay (KVMFR) Client
Copyright (C) 2017-2021 Geoffrey McRae <geoff@hostfission.com>
https://looking-glass.hostfission.com
This program is free software; you can redistribute it and/or modify it under
the terms of the GNU General Public License as published by the Free Software
Foundation; either version 2 of the License, or (at your option) any later
version.
This program 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 General Public License for more details.
You should have received a copy of the GNU General Public License along with
this program; if not, write to the Free Software Foundation, Inc., 59 Temple
Place, Suite 330, Boston, MA 02111-1307 USA
*/
#define _GNU_SOURCE
#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <stdbool.h>
#include <sys/mman.h>
#include <unistd.h>
#include <linux/input.h>
#include <poll.h>
#include <sys/epoll.h>
#include <SDL2/SDL.h>
#include <wayland-client.h>
#if defined(ENABLE_EGL) || defined(ENABLE_OPENGL)
# include <wayland-egl.h>
# include "egl_dynprocs.h"
# include <EGL/eglext.h>
#endif
#include "app.h"
#include "common/debug.h"
#include "common/locking.h"
#include "common/countedbuffer.h"
#include "wayland-xdg-shell-client-protocol.h"
#include "wayland-xdg-decoration-unstable-v1-client-protocol.h"
#include "wayland-keyboard-shortcuts-inhibit-unstable-v1-client-protocol.h"
#include "wayland-pointer-constraints-unstable-v1-client-protocol.h"
#include "wayland-relative-pointer-unstable-v1-client-protocol.h"
#include "wayland-idle-inhibit-unstable-v1-client-protocol.h"
#define EPOLL_EVENTS 10 // Maximum number of fds we can process at once in waylandWait
typedef void (*WaylandPollCallback)(uint32_t events, void * opaque);
struct WaylandPoll
{
int fd;
bool removed;
WaylandPollCallback callback;
void * opaque;
struct wl_list link;
};
struct WaylandDSState
{
bool pointerGrabbed;
bool keyboardGrabbed;
struct wl_display * display;
struct wl_surface * surface;
struct wl_registry * registry;
struct wl_seat * seat;
struct wl_shm * shm;
struct wl_compositor * compositor;
int32_t width, height;
bool fullscreen;
uint32_t resizeSerial;
bool configured;
bool warpSupport;
double cursorX, cursorY;
#ifdef ENABLE_EGL
struct wl_egl_window * eglWindow;
#endif
#ifdef ENABLE_OPENGL
EGLDisplay glDisplay;
EGLConfig glConfig;
EGLSurface glSurface;
#endif
struct xdg_wm_base * xdgWmBase;
struct xdg_surface * xdgSurface;
struct xdg_toplevel * xdgToplevel;
struct zxdg_decoration_manager_v1 * xdgDecorationManager;
struct zxdg_toplevel_decoration_v1 * xdgToplevelDecoration;
struct wl_surface * cursor;
struct wl_data_device_manager * dataDeviceManager;
struct wl_data_device * dataDevice;
uint32_t capabilities;
struct wl_keyboard * keyboard;
struct zwp_keyboard_shortcuts_inhibit_manager_v1 * keyboardInhibitManager;
struct zwp_keyboard_shortcuts_inhibitor_v1 * keyboardInhibitor;
uint32_t keyboardEnterSerial;
struct wl_pointer * pointer;
struct zwp_relative_pointer_manager_v1 * relativePointerManager;
struct zwp_pointer_constraints_v1 * pointerConstraints;
struct zwp_relative_pointer_v1 * relativePointer;
struct zwp_confined_pointer_v1 * confinedPointer;
bool showPointer;
uint32_t pointerEnterSerial;
struct zwp_idle_inhibit_manager_v1 * idleInhibitManager;
struct zwp_idle_inhibitor_v1 * idleInhibitor;
struct wl_list poll; // WaylandPoll::link
struct wl_list pollFree; // WaylandPoll::link
LG_Lock pollLock;
LG_Lock pollFreeLock;
int epollFd;
int displayFd;
};
struct WCBTransfer
{
struct CountedBuffer * data;
const char ** mimetypes;
};
struct ClipboardRead
{
int fd;
size_t size;
size_t numRead;
uint8_t * buf;
enum LG_ClipboardData type;
struct wl_data_offer * offer;
};
struct WCBState
{
char lgMimetype[64];
enum LG_ClipboardData pendingType;
char * pendingMimetype;
bool isSelfCopy;
enum LG_ClipboardData stashedType;
uint8_t * stashedContents;
ssize_t stashedSize;
bool haveRequest;
LG_ClipboardData type;
struct ClipboardRead * currentRead;
};
static struct WaylandDSState wm;
static struct WCBState wcb;
static const uint32_t cursorBitmap[] = {
0x000000, 0x000000, 0x000000, 0x000000,
0x000000, 0xFFFFFF, 0xFFFFFF, 0x000000,
0x000000, 0xFFFFFF, 0xFFFFFF, 0x000000,
0x000000, 0x000000, 0x000000, 0x000000,
};
static struct wl_buffer * createCursorBuffer(void)
{
int fd = memfd_create("lg-cursor", 0);
if (fd < 0)
{
DEBUG_ERROR("Failed to create cursor shared memory: %d", errno);
return NULL;
}
struct wl_buffer * result = NULL;
if (ftruncate(fd, sizeof cursorBitmap) < 0)
{
DEBUG_ERROR("Failed to ftruncate cursor shared memory: %d", errno);
goto fail;
}
void * shm_data = mmap(NULL, sizeof cursorBitmap, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (shm_data == MAP_FAILED)
{
DEBUG_ERROR("Failed to map memory for cursor: %d", errno);
goto fail;
}
struct wl_shm_pool * pool = wl_shm_create_pool(wm.shm, fd, sizeof cursorBitmap);
result = wl_shm_pool_create_buffer(pool, 0, 4, 4, 16, WL_SHM_FORMAT_XRGB8888);
wl_shm_pool_destroy(pool);
memcpy(shm_data, cursorBitmap, sizeof cursorBitmap);
munmap(shm_data, sizeof cursorBitmap);
fail:
close(fd);
return result;
}
// XDG WM base listeners.
static void xdgWmBasePing(void * data, struct xdg_wm_base * xdgWmBase, uint32_t serial)
{
xdg_wm_base_pong(xdgWmBase, serial);
}
static const struct xdg_wm_base_listener xdgWmBaseListener = {
.ping = xdgWmBasePing,
};
// Registry-handling listeners.
static void registryGlobalHandler(void * data, struct wl_registry * registry,
uint32_t name, const char * interface, uint32_t version)
{
if (!strcmp(interface, wl_seat_interface.name) && !wm.seat)
wm.seat = wl_registry_bind(wm.registry, name, &wl_seat_interface, 1);
else if (!strcmp(interface, wl_shm_interface.name))
wm.shm = wl_registry_bind(wm.registry, name, &wl_shm_interface, 1);
else if (!strcmp(interface, wl_compositor_interface.name))
wm.compositor = wl_registry_bind(wm.registry, name, &wl_compositor_interface, 4);
else if (!strcmp(interface, xdg_wm_base_interface.name))
wm.xdgWmBase = wl_registry_bind(wm.registry, name, &xdg_wm_base_interface, 1);
else if (!strcmp(interface, zxdg_decoration_manager_v1_interface.name))
wm.xdgDecorationManager = wl_registry_bind(wm.registry, name,
&zxdg_decoration_manager_v1_interface, 1);
else if (!strcmp(interface, zwp_relative_pointer_manager_v1_interface.name))
wm.relativePointerManager = wl_registry_bind(wm.registry, name,
&zwp_relative_pointer_manager_v1_interface, 1);
else if (!strcmp(interface, zwp_pointer_constraints_v1_interface.name))
wm.pointerConstraints = wl_registry_bind(wm.registry, name,
&zwp_pointer_constraints_v1_interface, 1);
else if (!strcmp(interface, zwp_keyboard_shortcuts_inhibit_manager_v1_interface.name))
wm.keyboardInhibitManager = wl_registry_bind(wm.registry, name,
&zwp_keyboard_shortcuts_inhibit_manager_v1_interface, 1);
else if (!strcmp(interface, wl_data_device_manager_interface.name))
wm.dataDeviceManager = wl_registry_bind(wm.registry, name,
&wl_data_device_manager_interface, 3);
else if (!strcmp(interface, zwp_idle_inhibit_manager_v1_interface.name))
wm.idleInhibitManager = wl_registry_bind(wm.registry, name,
&zwp_idle_inhibit_manager_v1_interface, 1);
}
static void registryGlobalRemoveHandler(void * data,
struct wl_registry * registry, uint32_t name)
{
// Do nothing.
}
static const struct wl_registry_listener registryListener = {
.global = registryGlobalHandler,
.global_remove = registryGlobalRemoveHandler,
};
// Mouse-handling listeners.
static void pointerMotionHandler(void * data, struct wl_pointer * pointer,
uint32_t serial, wl_fixed_t sxW, wl_fixed_t syW)
{
wm.cursorX = wl_fixed_to_double(sxW);
wm.cursorY = wl_fixed_to_double(syW);
app_updateCursorPos(wm.cursorX, wm.cursorY);
if (!wm.warpSupport && !wm.relativePointer)
[client] spice/wayland: improve cursor tracking logic One of the major issues with the old tracking code is a data race between the cursor thread updating g_cursor.guest and the app_handleMouseBasic function. Specifically, the latter may have sent mouse input via spice that has not been processed by the guest and updated g_cursor.guest, but the guest may overwrite g_cursor.guest to a previous state before the input is processed. This causes some movements to be doubled. Eventually, the cursor positions will synchronize, but this nevertheless causes a lot of jitter. In this commit, we introduce a new field g_cursor.projected, which is unambiguously the position of the cursor after taking into account all the input already sent via spice. This is synced up to the guest cursor upon entering the window and when the host restarts. Afterwards, all mouse movements will be based on this position. This eliminates all cursor jitter as far as I could tell. Also, the cursor is now synced to the host position when exiting capture mode. A downside of this commit is that if the 1:1 movement patch is not correctly applied, the cursor position would be wildly off instead of simply jittering, but that is an unsupported configuration and should not matter. Also unsupported is when an application in guest moves the cursor programmatically and bypassing spice. When using those applications, capture mode must be on. Before this commit, we try to move the guest cursor back to where it should be, but it's inherently fragile and may lead to scenarios such as wild movements in first-person shooters.
2021-01-21 02:05:50 +00:00
app_handleMouseBasic();
}
static void pointerEnterHandler(void * data, struct wl_pointer * pointer,
uint32_t serial, struct wl_surface * surface, wl_fixed_t sxW,
wl_fixed_t syW)
{
app_handleEnterEvent(true);
wl_pointer_set_cursor(pointer, serial, wm.showPointer ? wm.cursor : NULL, 0, 0);
wm.pointerEnterSerial = serial;
wm.cursorX = wl_fixed_to_double(sxW);
wm.cursorY = wl_fixed_to_double(syW);
app_updateCursorPos(wm.cursorX, wm.cursorY);
if (wm.warpSupport)
{
app_handleMouseRelative(0.0, 0.0, 0.0, 0.0);
return;
}
if (wm.relativePointer)
return;
[client] spice/wayland: improve cursor tracking logic One of the major issues with the old tracking code is a data race between the cursor thread updating g_cursor.guest and the app_handleMouseBasic function. Specifically, the latter may have sent mouse input via spice that has not been processed by the guest and updated g_cursor.guest, but the guest may overwrite g_cursor.guest to a previous state before the input is processed. This causes some movements to be doubled. Eventually, the cursor positions will synchronize, but this nevertheless causes a lot of jitter. In this commit, we introduce a new field g_cursor.projected, which is unambiguously the position of the cursor after taking into account all the input already sent via spice. This is synced up to the guest cursor upon entering the window and when the host restarts. Afterwards, all mouse movements will be based on this position. This eliminates all cursor jitter as far as I could tell. Also, the cursor is now synced to the host position when exiting capture mode. A downside of this commit is that if the 1:1 movement patch is not correctly applied, the cursor position would be wildly off instead of simply jittering, but that is an unsupported configuration and should not matter. Also unsupported is when an application in guest moves the cursor programmatically and bypassing spice. When using those applications, capture mode must be on. Before this commit, we try to move the guest cursor back to where it should be, but it's inherently fragile and may lead to scenarios such as wild movements in first-person shooters.
2021-01-21 02:05:50 +00:00
app_resyncMouseBasic();
app_handleMouseBasic();
}
static void pointerLeaveHandler(void * data, struct wl_pointer * pointer,
uint32_t serial, struct wl_surface * surface)
{
app_handleEnterEvent(false);
}
static void pointerAxisHandler(void * data, struct wl_pointer * pointer,
uint32_t serial, uint32_t axis, wl_fixed_t value)
{
int button = value > 0 ?
5 /* SPICE_MOUSE_BUTTON_DOWN */ :
4 /* SPICE_MOUSE_BUTTON_UP */;
app_handleButtonPress(button);
app_handleButtonRelease(button);
}
static int mapWaylandToSpiceButton(uint32_t button)
{
switch (button)
{
case BTN_LEFT:
return 1; // SPICE_MOUSE_BUTTON_LEFT
case BTN_MIDDLE:
return 2; // SPICE_MOUSE_BUTTON_MIDDLE
case BTN_RIGHT:
return 3; // SPICE_MOUSE_BUTTON_RIGHT
case BTN_SIDE:
return 6; // SPICE_MOUSE_BUTTON_SIDE
case BTN_EXTRA:
return 7; // SPICE_MOUSE_BUTTON_EXTRA
}
return 0; // SPICE_MOUSE_BUTTON_INVALID
}
static void pointerButtonHandler(void *data, struct wl_pointer *pointer,
uint32_t serial, uint32_t time, uint32_t button, uint32_t stateW)
{
button = mapWaylandToSpiceButton(button);
if (stateW == WL_POINTER_BUTTON_STATE_PRESSED)
app_handleButtonPress(button);
else
app_handleButtonRelease(button);
}
static const struct wl_pointer_listener pointerListener = {
.enter = pointerEnterHandler,
.leave = pointerLeaveHandler,
.motion = pointerMotionHandler,
.button = pointerButtonHandler,
.axis = pointerAxisHandler,
};
static void relativePointerMotionHandler(void * data,
struct zwp_relative_pointer_v1 *pointer, uint32_t timeHi, uint32_t timeLo,
wl_fixed_t dxW, wl_fixed_t dyW, wl_fixed_t dxUnaccelW,
wl_fixed_t dyUnaccelW)
{
wm.cursorX += wl_fixed_to_double(dxW);
wm.cursorY += wl_fixed_to_double(dyW);
app_updateCursorPos(wm.cursorX, wm.cursorY);
app_handleMouseRelative(
wl_fixed_to_double(dxW),
wl_fixed_to_double(dyW),
wl_fixed_to_double(dxUnaccelW),
wl_fixed_to_double(dyUnaccelW));
}
static const struct zwp_relative_pointer_v1_listener relativePointerListener = {
.relative_motion = relativePointerMotionHandler,
};
static void waylandInhibitIdle(void)
{
if (wm.idleInhibitManager && !wm.idleInhibitor)
wm.idleInhibitor = zwp_idle_inhibit_manager_v1_create_inhibitor(
wm.idleInhibitManager, wm.surface);
}
static void waylandUninhibitIdle(void)
{
if (wm.idleInhibitor)
{
zwp_idle_inhibitor_v1_destroy(wm.idleInhibitor);
wm.idleInhibitor = NULL;
}
}
// Keyboard-handling listeners.
static void keyboardKeymapHandler(void * data, struct wl_keyboard * keyboard,
uint32_t format, int fd, uint32_t size)
{
close(fd);
}
static void keyboardEnterHandler(void * data, struct wl_keyboard * keyboard,
uint32_t serial, struct wl_surface * surface, struct wl_array * keys)
{
app_handleFocusEvent(true);
wm.keyboardEnterSerial = serial;
uint32_t * key;
wl_array_for_each(key, keys)
app_handleKeyPress(*key);
}
static void keyboardLeaveHandler(void * data, struct wl_keyboard * keyboard,
uint32_t serial, struct wl_surface * surface)
{
app_handleFocusEvent(false);
}
static void keyboardKeyHandler(void * data, struct wl_keyboard * keyboard,
uint32_t serial, uint32_t time, uint32_t key, uint32_t state)
{
if (state == WL_KEYBOARD_KEY_STATE_PRESSED)
app_handleKeyPress(key);
else
app_handleKeyRelease(key);
}
static void keyboardModifiersHandler(void * data,
struct wl_keyboard * keyboard, uint32_t serial, uint32_t modsDepressed,
uint32_t modsLatched, uint32_t modsLocked, uint32_t group)
{
// Do nothing.
}
static const struct wl_keyboard_listener keyboardListener = {
.keymap = keyboardKeymapHandler,
.enter = keyboardEnterHandler,
.leave = keyboardLeaveHandler,
.key = keyboardKeyHandler,
.modifiers = keyboardModifiersHandler,
};
// Seat-handling listeners.
static void handlePointerCapability(uint32_t capabilities)
{
bool hasPointer = capabilities & WL_SEAT_CAPABILITY_POINTER;
if (!hasPointer && wm.pointer)
{
wl_pointer_destroy(wm.pointer);
wm.pointer = NULL;
}
else if (hasPointer && !wm.pointer)
{
wm.pointer = wl_seat_get_pointer(wm.seat);
wl_pointer_add_listener(wm.pointer, &pointerListener, NULL);
}
}
static void handleKeyboardCapability(uint32_t capabilities)
{
bool hasKeyboard = capabilities & WL_SEAT_CAPABILITY_KEYBOARD;
if (!hasKeyboard && wm.keyboard)
{
wl_keyboard_destroy(wm.keyboard);
wm.keyboard = NULL;
}
else if (hasKeyboard && !wm.keyboard)
{
wm.keyboard = wl_seat_get_keyboard(wm.seat);
wl_keyboard_add_listener(wm.keyboard, &keyboardListener, NULL);
}
}
static void seatCapabilitiesHandler(void * data, struct wl_seat * seat,
uint32_t capabilities)
{
wm.capabilities = capabilities;
handlePointerCapability(capabilities);
handleKeyboardCapability(capabilities);
}
static void seatNameHandler(void * data, struct wl_seat * seat,
const char * name)
{
// Do nothing.
}
static const struct wl_seat_listener seatListener = {
.capabilities = seatCapabilitiesHandler,
.name = seatNameHandler,
};
// Surface-handling listeners.
static void xdgSurfaceConfigure(void * data, struct xdg_surface * xdgSurface,
uint32_t serial)
{
if (wm.configured)
wm.resizeSerial = serial;
else
{
xdg_surface_ack_configure(xdgSurface, serial);
wm.configured = true;
}
}
static const struct xdg_surface_listener xdgSurfaceListener = {
.configure = xdgSurfaceConfigure,
};
static void xdgToplevelConfigure(void * data, struct xdg_toplevel * xdgToplevel,
int32_t width, int32_t height, struct wl_array * states)
{
wm.width = width;
wm.height = height;
wm.fullscreen = false;
enum xdg_toplevel_state * state;
wl_array_for_each(state, states)
{
if (*state == XDG_TOPLEVEL_STATE_FULLSCREEN)
wm.fullscreen = true;
}
}
static void xdgToplevelClose(void * data, struct xdg_toplevel * xdgToplevel)
{
app_handleCloseEvent();
}
static void waylandEpollRemoveNode(struct WaylandPoll * node)
{
INTERLOCKED_SECTION(wm.pollLock,
{
wl_list_remove(&node->link);
});
}
static bool waylandEpollRegister(int fd, WaylandPollCallback callback, void * opaque, uint32_t events)
{
struct WaylandPoll * node = malloc(sizeof(struct WaylandPoll));
if (!node)
return false;
node->fd = fd;
node->removed = false;
node->callback = callback;
node->opaque = opaque;
INTERLOCKED_SECTION(wm.pollLock,
{
wl_list_insert(&wm.poll, &node->link);
});
if (epoll_ctl(wm.epollFd, EPOLL_CTL_ADD, fd, &(struct epoll_event) {
.events = events,
.data = (epoll_data_t) { .ptr = node },
}) < 0)
{
waylandEpollRemoveNode(node);
free(node);
return false;
}
return true;
}
static bool waylandEpollUnregister(int fd)
{
struct WaylandPoll * node = NULL;
INTERLOCKED_SECTION(wm.pollLock,
{
wl_list_for_each(node, &wm.poll, link)
{
if (node->fd == fd)
break;
}
});
if (!node)
{
DEBUG_ERROR("Attempt to unregister a fd that was not registered: %d", fd);
return false;
}
node->removed = true;
if (epoll_ctl(wm.epollFd, EPOLL_CTL_DEL, fd, NULL) < 0)
{
DEBUG_ERROR("Failed to unregistered from epoll: %s", strerror(errno));
return false;
}
waylandEpollRemoveNode(node);
INTERLOCKED_SECTION(wm.pollFreeLock,
{
wl_list_insert(&wm.pollFree, &node->link);
});
return true;
}
static const struct xdg_toplevel_listener xdgToplevelListener = {
.configure = xdgToplevelConfigure,
.close = xdgToplevelClose,
};
static bool waylandEarlyInit(void)
{
// Request to receive EPIPE instead of SIGPIPE when one end of a pipe
// disconnects while a write is pending. This is useful to the Wayland
// clipboard backend, where an arbitrary application is on the other end of
// that pipe.
signal(SIGPIPE, SIG_IGN);
return true;
}
static bool waylandProbe(void)
{
return getenv("WAYLAND_DISPLAY") != NULL;
}
static EGLDisplay waylandGetEGLDisplay(void);
static void waylandDisplayCallback(uint32_t events, void * opaque);
static bool waylandInit(const LG_DSInitParams params)
{
memset(&wm, 0, sizeof(wm));
wm.epollFd = epoll_create1(EPOLL_CLOEXEC);
if (wm.epollFd < 0)
{
DEBUG_ERROR("Failed to initialize epoll: %s", strerror(errno));
return false;
}
wm.display = wl_display_connect(NULL);
wm.registry = wl_display_get_registry(wm.display);
wl_list_init(&wm.poll);
wl_list_init(&wm.pollFree);
LG_LOCK_INIT(wm.pollLock);
LG_LOCK_INIT(wm.pollFreeLock);
wm.displayFd = wl_display_get_fd(wm.display);
if (!waylandEpollRegister(wm.displayFd, waylandDisplayCallback, NULL, EPOLLIN))
{
DEBUG_ERROR("Failed register display to epoll: %s", strerror(errno));
return false;
}
wl_registry_add_listener(wm.registry, &registryListener, NULL);
wl_display_roundtrip(wm.display);
if (!wm.seat || !wm.dataDeviceManager || !wm.compositor || !wm.xdgWmBase)
{
DEBUG_ERROR("Compositor missing a required interface, will not proceed");
return false;
}
if (!wm.relativePointerManager)
DEBUG_WARN("zwp_relative_pointer_manager_v1 not exported by compositor, "
"mouse will not be captured");
if (!wm.pointerConstraints)
DEBUG_WARN("zwp_pointer_constraints_v1 not exported by compositor, mouse "
"will not be captured");
if (!wm.keyboardInhibitManager)
DEBUG_WARN("zwp_keyboard_shortcuts_inhibit_manager_v1 not exported by "
"compositor, keyboard will not be grabbed");
if (!wm.idleInhibitManager)
DEBUG_WARN("zwp_idle_inhibit_manager_v1 not exported by compositor, will "
"not be able to suppress idle states");
if (wm.warpSupport && (!wm.relativePointerManager || !wm.pointerConstraints))
{
DEBUG_WARN("Cursor warp is requested, but cannot be honoured due to lack "
"of zwp_relative_pointer_manager_v1 or zwp_pointer_constraints_v1");
wm.warpSupport = false;
}
wl_seat_add_listener(wm.seat, &seatListener, NULL);
xdg_wm_base_add_listener(wm.xdgWmBase, &xdgWmBaseListener, NULL);
wl_display_roundtrip(wm.display);
wm.dataDevice = wl_data_device_manager_get_data_device(
wm.dataDeviceManager, wm.seat);
if (wm.warpSupport)
{
wm.relativePointer =
zwp_relative_pointer_manager_v1_get_relative_pointer(
wm.relativePointerManager, wm.pointer);
zwp_relative_pointer_v1_add_listener(wm.relativePointer,
&relativePointerListener, NULL);
}
wm.surface = wl_compositor_create_surface(wm.compositor);
wm.eglWindow = wl_egl_window_create(wm.surface, params.w, params.h);
wm.xdgSurface = xdg_wm_base_get_xdg_surface(wm.xdgWmBase, wm.surface);
xdg_surface_add_listener(wm.xdgSurface, &xdgSurfaceListener, NULL);
wm.xdgToplevel = xdg_surface_get_toplevel(wm.xdgSurface);
xdg_toplevel_add_listener(wm.xdgToplevel, &xdgToplevelListener, NULL);
xdg_toplevel_set_title(wm.xdgToplevel, params.title);
xdg_toplevel_set_app_id(wm.xdgToplevel, "looking-glass-client");
if (params.fullscreen)
xdg_toplevel_set_fullscreen(wm.xdgToplevel, NULL);
if (params.maximize)
xdg_toplevel_set_maximized(wm.xdgToplevel);
wl_surface_commit(wm.surface);
if (wm.xdgDecorationManager)
{
wm.xdgToplevelDecoration = zxdg_decoration_manager_v1_get_toplevel_decoration(
wm.xdgDecorationManager, wm.xdgToplevel);
if (wm.xdgToplevelDecoration)
{
zxdg_toplevel_decoration_v1_set_mode(wm.xdgToplevelDecoration,
params.borderless ?
ZXDG_TOPLEVEL_DECORATION_V1_MODE_CLIENT_SIDE :
ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE);
}
}
struct wl_buffer * cursorBuffer = createCursorBuffer();
if (cursorBuffer)
{
wm.cursor = wl_compositor_create_surface(wm.compositor);
wl_surface_attach(wm.cursor, cursorBuffer, 0, 0);
wl_surface_commit(wm.cursor);
}
#ifdef ENABLE_OPENGL
if (params.opengl)
{
EGLint attr[] =
{
EGL_BUFFER_SIZE , 24,
EGL_CONFORMANT , EGL_OPENGL_BIT,
EGL_RENDERABLE_TYPE , EGL_OPENGL_BIT,
EGL_COLOR_BUFFER_TYPE, EGL_RGB_BUFFER,
EGL_RED_SIZE , 8,
EGL_GREEN_SIZE , 8,
EGL_BLUE_SIZE , 8,
EGL_SAMPLE_BUFFERS , 0,
EGL_SAMPLES , 0,
EGL_NONE
};
wm.glDisplay = waylandGetEGLDisplay();
int maj, min;
if (!eglInitialize(wm.glDisplay, &maj, &min))
{
DEBUG_ERROR("Unable to initialize EGL");
return false;
}
if (wm.glDisplay == EGL_NO_DISPLAY)
{
DEBUG_ERROR("Failed to get EGL display (eglError: 0x%x)", eglGetError());
return false;
}
EGLint num_config;
if (!eglChooseConfig(wm.glDisplay, attr, &wm.glConfig, 1, &num_config))
{
DEBUG_ERROR("Failed to choose config (eglError: 0x%x)", eglGetError());
return false;
}
wm.glSurface = eglCreateWindowSurface(wm.glDisplay, wm.glConfig, wm.eglWindow, NULL);
if (wm.glSurface == EGL_NO_SURFACE)
{
DEBUG_ERROR("Failed to create EGL surface (eglError: 0x%x)", eglGetError());
return false;
}
}
#endif
wm.width = params.w;
wm.height = params.h;
return true;
}
static void waylandStartup(void)
{
}
static void waylandShutdown(void)
{
}
#ifdef ENABLE_EGL
static EGLNativeWindowType waylandGetEGLNativeWindow(void)
{
return (EGLNativeWindowType) wm.eglWindow;
}
#endif
#if defined(ENABLE_EGL) || defined(ENABLE_OPENGL)
static EGLDisplay waylandGetEGLDisplay(void)
{
EGLNativeDisplayType native = (EGLNativeDisplayType) wm.display;
const char *early_exts = eglQueryString(NULL, EGL_EXTENSIONS);
if (strstr(early_exts, "EGL_KHR_platform_wayland") != NULL &&
g_egl_dynProcs.eglGetPlatformDisplay)
{
DEBUG_INFO("Using eglGetPlatformDisplay");
return g_egl_dynProcs.eglGetPlatformDisplay(EGL_PLATFORM_WAYLAND_KHR, native, NULL);
}
if (strstr(early_exts, "EGL_EXT_platform_wayland") != NULL &&
g_egl_dynProcs.eglGetPlatformDisplayEXT)
{
DEBUG_INFO("Using eglGetPlatformDisplayEXT");
return g_egl_dynProcs.eglGetPlatformDisplayEXT(EGL_PLATFORM_WAYLAND_EXT, native, NULL);
}
DEBUG_INFO("Using eglGetDisplay");
return eglGetDisplay(native);
}
static void waylandEGLSwapBuffers(EGLDisplay display, EGLSurface surface)
{
eglSwapBuffers(display, surface);
if (wm.resizeSerial)
{
wl_egl_window_resize(wm.eglWindow, wm.width, wm.height, 0, 0);
struct wl_region * region = wl_compositor_create_region(wm.compositor);
wl_region_add(region, 0, 0, wm.width, wm.height);
wl_surface_set_opaque_region(wm.surface, region);
wl_region_destroy(region);
app_handleResizeEvent(wm.width, wm.height, (struct Border) {0, 0, 0, 0});
xdg_surface_ack_configure(wm.xdgSurface, wm.resizeSerial);
wm.resizeSerial = 0;
}
}
#endif
#ifdef ENABLE_OPENGL
static LG_DSGLContext waylandGLCreateContext(void)
{
eglBindAPI(EGL_OPENGL_API);
return eglCreateContext(wm.glDisplay, wm.glConfig, EGL_NO_CONTEXT, NULL);
}
static void waylandGLDeleteContext(LG_DSGLContext context)
{
eglDestroyContext(wm.glDisplay, context);
}
static void waylandGLMakeCurrent(LG_DSGLContext context)
{
eglMakeCurrent(wm.glDisplay, wm.glSurface, wm.glSurface, context);
}
static void waylandGLSetSwapInterval(int interval)
{
eglSwapInterval(wm.glDisplay, interval);
}
static void waylandGLSwapBuffers(void)
{
waylandEGLSwapBuffers(wm.glDisplay, wm.glSurface);
}
#endif
static void waylandShowPointer(bool show)
{
wm.showPointer = show;
wl_pointer_set_cursor(wm.pointer, wm.pointerEnterSerial, show ? wm.cursor : NULL, 0, 0);
}
static void waylandWait(unsigned int time)
{
while (wl_display_prepare_read(wm.display))
wl_display_dispatch_pending(wm.display);
wl_display_flush(wm.display);
struct epoll_event events[EPOLL_EVENTS];
int count;
if ((count = epoll_wait(wm.epollFd, events, EPOLL_EVENTS, time)) < 0)
{
if (errno != EINTR)
DEBUG_INFO("epoll failed: %s", strerror(errno));
wl_display_cancel_read(wm.display);
return;
}
bool sawDisplay = false;
for (int i = 0; i < count; ++i) {
struct WaylandPoll * poll = events[i].data.ptr;
if (!poll->removed)
poll->callback(events[i].events, poll->opaque);
if (poll->fd == wm.displayFd)
sawDisplay = true;
}
if (!sawDisplay)
wl_display_cancel_read(wm.display);
INTERLOCKED_SECTION(wm.pollFreeLock,
{
struct WaylandPoll * node;
struct WaylandPoll * temp;
wl_list_for_each_safe(node, temp, &wm.pollFree, link)
{
wl_list_remove(&node->link);
free(node);
}
});
}
static void waylandDisplayCallback(uint32_t events, void * opaque)
{
if (events & EPOLLERR)
wl_display_cancel_read(wm.display);
else
wl_display_read_events(wm.display);
wl_display_dispatch_pending(wm.display);
}
static void waylandSetWindowSize(int x, int y)
{
// FIXME: implement.
}
static void waylandSetFullscreen(bool fs)
{
if (fs)
xdg_toplevel_set_fullscreen(wm.xdgToplevel, NULL);
else
xdg_toplevel_unset_fullscreen(wm.xdgToplevel);
}
static bool waylandGetFullscreen(void)
{
return wm.fullscreen;
}
static void waylandGrabPointer(void)
{
if (!wm.relativePointerManager || !wm.pointerConstraints)
return;
if (!wm.warpSupport && !wm.relativePointer)
{
wm.relativePointer =
zwp_relative_pointer_manager_v1_get_relative_pointer(
wm.relativePointerManager, wm.pointer);
zwp_relative_pointer_v1_add_listener(wm.relativePointer,
&relativePointerListener, NULL);
}
if (!wm.confinedPointer)
{
wm.confinedPointer = zwp_pointer_constraints_v1_confine_pointer(
wm.pointerConstraints, wm.surface, wm.pointer, NULL,
ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_PERSISTENT);
}
}
static void waylandUngrabPointer(void)
{
if (wm.confinedPointer)
{
zwp_confined_pointer_v1_destroy(wm.confinedPointer);
wm.confinedPointer = NULL;
}
[client] spice/wayland: improve cursor tracking logic One of the major issues with the old tracking code is a data race between the cursor thread updating g_cursor.guest and the app_handleMouseBasic function. Specifically, the latter may have sent mouse input via spice that has not been processed by the guest and updated g_cursor.guest, but the guest may overwrite g_cursor.guest to a previous state before the input is processed. This causes some movements to be doubled. Eventually, the cursor positions will synchronize, but this nevertheless causes a lot of jitter. In this commit, we introduce a new field g_cursor.projected, which is unambiguously the position of the cursor after taking into account all the input already sent via spice. This is synced up to the guest cursor upon entering the window and when the host restarts. Afterwards, all mouse movements will be based on this position. This eliminates all cursor jitter as far as I could tell. Also, the cursor is now synced to the host position when exiting capture mode. A downside of this commit is that if the 1:1 movement patch is not correctly applied, the cursor position would be wildly off instead of simply jittering, but that is an unsupported configuration and should not matter. Also unsupported is when an application in guest moves the cursor programmatically and bypassing spice. When using those applications, capture mode must be on. Before this commit, we try to move the guest cursor back to where it should be, but it's inherently fragile and may lead to scenarios such as wild movements in first-person shooters.
2021-01-21 02:05:50 +00:00
if (!wm.warpSupport)
{
if (!wm.relativePointer)
{
wm.relativePointer =
zwp_relative_pointer_manager_v1_get_relative_pointer(
wm.relativePointerManager, wm.pointer);
zwp_relative_pointer_v1_add_listener(wm.relativePointer,
&relativePointerListener, NULL);
}
app_resyncMouseBasic();
app_handleMouseBasic();
}
}
static void waylandGrabKeyboard(void)
{
if (wm.keyboardInhibitManager && !wm.keyboardInhibitor)
{
wm.keyboardInhibitor = zwp_keyboard_shortcuts_inhibit_manager_v1_inhibit_shortcuts(
wm.keyboardInhibitManager, wm.surface, wm.seat);
}
}
static void waylandUngrabKeyboard(void)
{
if (wm.keyboardInhibitor)
{
zwp_keyboard_shortcuts_inhibitor_v1_destroy(wm.keyboardInhibitor);
wm.keyboardInhibitor = NULL;
}
}
static void waylandWarpPointer(int x, int y, bool exiting)
{
if (x < 0) x = 0;
else if (x >= wm.width) x = wm.width - 1;
if (y < 0) y = 0;
else if (y >= wm.height) y = wm.height - 1;
struct wl_region * region = wl_compositor_create_region(wm.compositor);
wl_region_add(region, x, y, 1, 1);
if (wm.confinedPointer)
{
zwp_confined_pointer_v1_set_region(wm.confinedPointer, region);
wl_surface_commit(wm.surface);
zwp_confined_pointer_v1_set_region(wm.confinedPointer, NULL);
}
else
{
struct zwp_confined_pointer_v1 * confine;
confine = zwp_pointer_constraints_v1_confine_pointer(
wm.pointerConstraints, wm.surface, wm.pointer, region,
ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_PERSISTENT);
wl_surface_commit(wm.surface);
zwp_confined_pointer_v1_destroy(confine);
}
wl_surface_commit(wm.surface);
wl_region_destroy(region);
}
static void waylandRealignPointer(void)
{
if (!wm.warpSupport)
app_resyncMouseBasic();
}
static bool waylandIsValidPointerPos(int x, int y)
{
return x >= 0 && x < wm.width && y >= 0 && y < wm.height;
}
static void waylandFree(void)
{
waylandUngrabPointer();
if (wm.idleInhibitManager)
{
waylandUninhibitIdle();
zwp_idle_inhibit_manager_v1_destroy(wm.idleInhibitManager);
}
wl_surface_destroy(wm.surface);
wl_pointer_destroy(wm.pointer);
wl_seat_destroy(wm.seat);
wl_registry_destroy(wm.registry);
wl_display_disconnect(wm.display);
}
static bool waylandGetProp(LG_DSProperty prop, void * ret)
{
if (prop == LG_DS_WARP_SUPPORT)
{
*(enum LG_DSWarpSupport*)ret = wm.warpSupport ? LG_DS_WARP_SURFACE : LG_DS_WARP_NONE;
return true;
}
return false;
}
static const char * textMimetypes[] =
{
"text/plain",
"text/plain;charset=utf-8",
"TEXT",
"STRING",
"UTF8_STRING",
NULL,
};
static const char * pngMimetypes[] =
{
"image/png",
NULL,
};
static const char * bmpMimetypes[] =
{
"image/bmp",
"image/x-bmp",
"image/x-MS-bmp",
"image/x-win-bitmap",
NULL,
};
static const char * tiffMimetypes[] =
{
"image/tiff",
NULL,
};
static const char * jpegMimetypes[] =
{
"image/jpeg",
NULL,
};
static const char ** cbTypeToMimetypes(enum LG_ClipboardData type)
{
switch (type)
{
case LG_CLIPBOARD_DATA_TEXT:
return textMimetypes;
case LG_CLIPBOARD_DATA_PNG:
return pngMimetypes;
case LG_CLIPBOARD_DATA_BMP:
return bmpMimetypes;
case LG_CLIPBOARD_DATA_TIFF:
return tiffMimetypes;
case LG_CLIPBOARD_DATA_JPEG:
return jpegMimetypes;
default:
DEBUG_ERROR("invalid clipboard type");
abort();
}
}
static bool containsMimetype(const char ** mimetypes,
const char * needle)
{
for (const char ** mimetype = mimetypes; *mimetype; mimetype++)
if (!strcmp(needle, *mimetype))
return true;
return false;
}
static bool mimetypeEndswith(const char * mimetype, const char * what)
{
size_t mimetypeLen = strlen(mimetype);
size_t whatLen = strlen(what);
if (mimetypeLen < whatLen)
return false;
return !strcmp(mimetype + mimetypeLen - whatLen, what);
}
static bool isTextMimetype(const char * mimetype)
{
if (containsMimetype(textMimetypes, mimetype))
return true;
char * text = "text/";
if (!strncmp(mimetype, text, strlen(text)))
return true;
if (mimetypeEndswith(mimetype, "script") ||
mimetypeEndswith(mimetype, "xml") ||
mimetypeEndswith(mimetype, "yaml"))
return true;
if (strstr(mimetype, "json"))
return true;
return false;
}
static enum LG_ClipboardData mimetypeToCbType(const char * mimetype)
{
if (isTextMimetype(mimetype))
return LG_CLIPBOARD_DATA_TEXT;
if (containsMimetype(pngMimetypes, mimetype))
return LG_CLIPBOARD_DATA_PNG;
if (containsMimetype(bmpMimetypes, mimetype))
return LG_CLIPBOARD_DATA_BMP;
if (containsMimetype(tiffMimetypes, mimetype))
return LG_CLIPBOARD_DATA_TIFF;
if (containsMimetype(jpegMimetypes, mimetype))
return LG_CLIPBOARD_DATA_JPEG;
return LG_CLIPBOARD_DATA_NONE;
}
// Destination client handlers.
static void dataOfferHandleOffer(void * data, struct wl_data_offer * offer,
const char * mimetype)
{
enum LG_ClipboardData type = mimetypeToCbType(mimetype);
// We almost never prefer text/html, as that's used to represent rich text.
// Since we can't copy or paste rich text, we should instead prefer actual
// images or plain text.
if (type != LG_CLIPBOARD_DATA_NONE &&
(wcb.pendingType == LG_CLIPBOARD_DATA_NONE ||
strstr(wcb.pendingMimetype, "html")))
{
wcb.pendingType = type;
if (wcb.pendingMimetype)
free(wcb.pendingMimetype);
wcb.pendingMimetype = strdup(mimetype);
}
if (!strcmp(mimetype, wcb.lgMimetype))
wcb.isSelfCopy = true;
}
static void dataOfferHandleSourceActions(void * data,
struct wl_data_offer * offer, uint32_t sourceActions)
{
// Do nothing.
}
static void dataOfferHandleAction(void * data, struct wl_data_offer * offer,
uint32_t dndAction)
{
// Do nothing.
}
static const struct wl_data_offer_listener dataOfferListener = {
.offer = dataOfferHandleOffer,
.source_actions = dataOfferHandleSourceActions,
.action = dataOfferHandleAction,
};
static void dataDeviceHandleDataOffer(void * data,
struct wl_data_device * dataDevice, struct wl_data_offer * offer)
{
wcb.pendingType = LG_CLIPBOARD_DATA_NONE;
wcb.isSelfCopy = false;
wl_data_offer_add_listener(offer, &dataOfferListener, NULL);
}
static void clipboardReadCancel(struct ClipboardRead * data, bool freeBuf)
{
waylandEpollUnregister(data->fd);
close(data->fd);
wl_data_offer_destroy(data->offer);
if (freeBuf)
free(data->buf);
free(data);
wcb.currentRead = NULL;
}
static void clipboardReadCallback(uint32_t events, void * opaque)
{
struct ClipboardRead * data = opaque;
if (events & EPOLLERR)
{
clipboardReadCancel(data, true);
return;
}
ssize_t result = read(data->fd, data->buf + data->numRead, data->size - data->numRead);
if (result < 0)
{
DEBUG_ERROR("Failed to read from clipboard: %s", strerror(errno));
clipboardReadCancel(data, true);
return;
}
if (result == 0)
{
data->buf[data->numRead] = 0;
wcb.stashedType = data->type;
wcb.stashedSize = data->numRead;
wcb.stashedContents = data->buf;
clipboardReadCancel(data, false);
app_clipboardNotify(wcb.stashedType, 0);
return;
}
data->numRead += result;
if (data->numRead >= data->size)
{
data->size *= 2;
void * nbuf = realloc(data->buf, data->size);
if (!nbuf) {
DEBUG_ERROR("Failed to realloc clipboard buffer: %s", strerror(errno));
clipboardReadCancel(data, true);
return;
}
data->buf = nbuf;
}
}
static void dataDeviceHandleSelection(void * opaque,
struct wl_data_device * dataDevice, struct wl_data_offer * offer)
{
if (wcb.pendingType == LG_CLIPBOARD_DATA_NONE || wcb.isSelfCopy || !offer)
return;
if (wcb.currentRead)
clipboardReadCancel(wcb.currentRead, true);
int fds[2];
if (pipe(fds) < 0)
{
DEBUG_ERROR("Failed to get a clipboard pipe: %s", strerror(errno));
abort();
}
wl_data_offer_receive(offer, wcb.pendingMimetype, fds[1]);
close(fds[1]);
free(wcb.pendingMimetype);
wcb.pendingMimetype = NULL;
wl_display_roundtrip(wm.display);
if (wcb.stashedContents)
{
free(wcb.stashedContents);
wcb.stashedContents = NULL;
}
struct ClipboardRead * data = malloc(sizeof(struct ClipboardRead));
if (!data)
{
DEBUG_ERROR("Failed to allocate memory to read clipboard");
close(fds[0]);
return;
}
data->fd = fds[0];
data->size = 4096;
data->numRead = 0;
data->buf = malloc(data->size);
data->offer = offer;
data->type = wcb.pendingType;
if (!data->buf)
{
DEBUG_ERROR("Failed to allocate memory to receive clipboard data");
close(data->fd);
free(data);
return;
}
if (!waylandEpollRegister(data->fd, clipboardReadCallback, data, EPOLLIN))
{
DEBUG_ERROR("Failed to register clipboard read into epoll: %s", strerror(errno));
close(data->fd);
free(data->buf);
free(data);
}
wcb.currentRead = data;
}
static const struct wl_data_device_listener dataDeviceListener = {
.data_offer = dataDeviceHandleDataOffer,
.selection = dataDeviceHandleSelection,
};
static void waylandCBRequest(LG_ClipboardData type)
{
// We only notified once, so it must be this.
assert(type == wcb.stashedType);
app_clipboardData(wcb.stashedType, wcb.stashedContents, wcb.stashedSize);
}
static bool waylandCBInit(void)
{
memset(&wcb, 0, sizeof(wcb));
wcb.stashedType = LG_CLIPBOARD_DATA_NONE;
wl_data_device_add_listener(wm.dataDevice, &dataDeviceListener, NULL);
snprintf(wcb.lgMimetype, sizeof(wcb.lgMimetype),
"application/x-looking-glass-copy;pid=%d", getpid());
return true;
}
struct ClipboardWrite
{
int fd;
size_t pos;
struct CountedBuffer * buffer;
};
static void clipboardWriteCallback(uint32_t events, void * opaque)
{
struct ClipboardWrite * data = opaque;
if (events & EPOLLERR)
goto error;
ssize_t written = write(data->fd, data->buffer->data + data->pos, data->buffer->size - data->pos);
if (written < 0)
{
if (errno != EPIPE)
DEBUG_ERROR("Failed to write clipboard data: %s", strerror(errno));
goto error;
}
data->pos += written;
if (data->pos < data->buffer->size)
return;
error:
waylandEpollUnregister(data->fd);
close(data->fd);
countedBufferRelease(&data->buffer);
free(data);
}
static void dataSourceHandleSend(void * data, struct wl_data_source * source,
const char * mimetype, int fd)
{
struct WCBTransfer * transfer = (struct WCBTransfer *) data;
if (containsMimetype(transfer->mimetypes, mimetype))
{
// Consider making this do non-blocking sends to not stall the Wayland
// event loop if it becomes a problem. This is "fine" in the sense that
// wl-copy also stalls like this, but it's not necessary.
fcntl(fd, F_SETFL, 0);
struct ClipboardWrite * data = malloc(sizeof(struct ClipboardWrite));
if (!data)
{
DEBUG_ERROR("Out of memory trying to allocate ClipboardWrite");
goto error;
}
data->fd = fd;
data->pos = 0;
data->buffer = transfer->data;
countedBufferAddRef(transfer->data);
waylandEpollRegister(fd, clipboardWriteCallback, data, EPOLLOUT);
return;
}
error:
close(fd);
}
static void dataSourceHandleCancelled(void * data,
struct wl_data_source * source)
{
struct WCBTransfer * transfer = (struct WCBTransfer *) data;
countedBufferRelease(&transfer->data);
free(transfer);
wl_data_source_destroy(source);
}
static const struct wl_data_source_listener dataSourceListener = {
.send = dataSourceHandleSend,
.cancelled = dataSourceHandleCancelled,
};
static void waylandCBReplyFn(void * opaque, LG_ClipboardData type,
uint8_t * data, uint32_t size)
{
struct WCBTransfer * transfer = malloc(sizeof(struct WCBTransfer));
if (!transfer)
{
DEBUG_ERROR("Out of memory when allocating WCBTransfer");
return;
}
transfer->mimetypes = cbTypeToMimetypes(type);
transfer->data = countedBufferNew(size);
if (!transfer->data)
{
DEBUG_ERROR("Out of memory when allocating clipboard buffer");
free(transfer);
return;
}
memcpy(transfer->data->data, data, size);
struct wl_data_source * source =
wl_data_device_manager_create_data_source(wm.dataDeviceManager);
wl_data_source_add_listener(source, &dataSourceListener, transfer);
for (const char ** mimetype = transfer->mimetypes; *mimetype; mimetype++)
wl_data_source_offer(source, *mimetype);
wl_data_source_offer(source, wcb.lgMimetype);
wl_data_device_set_selection(wm.dataDevice, source,
wm.keyboardEnterSerial);
}
static void waylandCBNotice(LG_ClipboardData type)
{
wcb.haveRequest = true;
wcb.type = type;
app_clipboardRequest(waylandCBReplyFn, NULL);
}
static void waylandCBRelease(void)
{
wcb.haveRequest = false;
}
struct LG_DisplayServerOps LGDS_Wayland =
{
.probe = waylandProbe,
.earlyInit = waylandEarlyInit,
.init = waylandInit,
.startup = waylandStartup,
.shutdown = waylandShutdown,
.free = waylandFree,
.getProp = waylandGetProp,
#ifdef ENABLE_EGL
.getEGLDisplay = waylandGetEGLDisplay,
.getEGLNativeWindow = waylandGetEGLNativeWindow,
.eglSwapBuffers = waylandEGLSwapBuffers,
#endif
#ifdef ENABLE_OPENGL
.glCreateContext = waylandGLCreateContext,
.glDeleteContext = waylandGLDeleteContext,
.glMakeCurrent = waylandGLMakeCurrent,
.glSetSwapInterval = waylandGLSetSwapInterval,
.glSwapBuffers = waylandGLSwapBuffers,
#endif
.showPointer = waylandShowPointer,
.grabPointer = waylandGrabPointer,
.ungrabPointer = waylandUngrabPointer,
.grabKeyboard = waylandGrabKeyboard,
.ungrabKeyboard = waylandUngrabKeyboard,
.warpPointer = waylandWarpPointer,
.realignPointer = waylandRealignPointer,
.isValidPointerPos = waylandIsValidPointerPos,
.inhibitIdle = waylandInhibitIdle,
.uninhibitIdle = waylandUninhibitIdle,
.wait = waylandWait,
.setWindowSize = waylandSetWindowSize,
.setFullscreen = waylandSetFullscreen,
.getFullscreen = waylandGetFullscreen,
.cbInit = waylandCBInit,
.cbNotice = waylandCBNotice,
.cbRelease = waylandCBRelease,
.cbRequest = waylandCBRequest
};