move wayland experiments here

This commit is contained in:
tocariimaa 2025-01-17 00:32:12 -03:00
parent 4b925dcb0d
commit 85e5323ee1
6 changed files with 662 additions and 0 deletions

View file

@ -0,0 +1,19 @@
# Wayland experiments
Just a bunch of programs I wrote to teach myself about Wayland using the [Wayland book](https://wayland-book.com/) as reference.
Building this requires [Meson](https://mesonbuild.com/).
The build script will take care of creating the interfacing and glue code for you.
```
meson setup --buildtype=release build
meson compile -C build
```
## client.c
A Wayland client that draws a barberpole-like animation; implementing the XDG shell protocol, interfaces with
`wl_seat` and `wl_keyboard` too, so it will listen keyboard events and print them on the console.
Because of this, libxkbcommon is required.
You can close it by pressing the escape key or just how you would close any window (see below).
This client also implements the XDG decoration protocol and uses server-side decorations.

View file

@ -0,0 +1,512 @@
// Wayland client example, made following the https://wayland-book.com/
#include <assert.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <err.h>
#include <sys/mman.h>
#include <unistd.h>
#include <wayland-client-core.h>
#include <wayland-client-protocol.h>
#include <wayland-client.h>
#include <wayland-util.h>
#include <xkbcommon/xkbcommon-keysyms.h>
#include <xkbcommon/xkbcommon.h>
#include "shm_helper.h"
#include "xdg-shell-client-protocol.h"
#include "xdg-decoration-unstable-v1-client-protocol.h"
#define _DBGPRINT_
#include "dbgpri.h"
#define COMPOSITOR_INTERFACE_VER 4u
#define TOPLEVEL_TITLE "Fubuland"
#define TOPLEVEL_APP_ID "net.tocariimaa.fubuland"
struct ClientState
{
struct wl_display *display;
struct wl_compositor *compositor;
struct wl_registry *registry;
struct wl_shm *shm;
struct wl_seat *seat;
struct wl_keyboard *keyboard;
struct xdg_wm_base *xdg_wm_base;
struct wl_surface *wl_surface;
struct xdg_surface *xdg_surface;
struct xdg_toplevel *xdg_toplevel;
struct zxdg_decoration_manager_v1 *xdg_decoration_mgr;
struct zxdg_toplevel_decoration_v1 *xdg_toplevel_deco;
float offset;
uint32_t last_frame;
int32_t width, height;
bool closed;
struct {
struct wl_buffer *buffer;
uint32_t *pool_data;
size_t pool_size;
} memory;
struct {
struct xkb_context *ctx;
struct xkb_state *state;
struct xkb_keymap *keymap;
} xkb;
};
static inline void draw_frame(struct ClientState *state);
static void surface_frame_done(void *data, struct wl_callback *cb, uint32_t time);
static inline void
draw_frame(struct ClientState *state)
{
// Macro helper to index the 1D buffer as a 2D lattice (buffer 2D index)
#define b2di(data, width, y, x) (data[y * width + x])
const int bar1_color = 0xe7f5c5;
const int bar2_color = 0xa2dd99;
const int bar1_thickness = 64; // px
const int bar2_thickness = 64;
int offset = (int)state->offset % bar2_thickness;
for (int y = 0; y < state->height; ++y) {
for (int x = 0; x < state->width; ++x) {
if (((x + offset) + (y + offset)) % bar1_thickness < bar2_thickness / 2)
b2di(state->memory.pool_data, state->width, y, x) = bar1_color;
else
b2di(state->memory.pool_data, state->width, y, x) = bar2_color;
}
}
#undef b2di
}
static void
create_memory_pool(struct ClientState *state)
{
int width = state->width, height = state->height;
int stride = width * 4; // 4 bytes per pixel; for WL_SHM_FORMAT_XRGB8888
int pool_size = height * stride;
const int32_t offset = 0;
int shm_fd;
if ((shm_fd = allocate_shm_file(pool_size)) < 0)
err(EXIT_FAILURE, "couldn't create shared memory object");
uint32_t *pool_data = mmap(NULL, pool_size, PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0);
if (pool_data == MAP_FAILED) {
warn("mmap failed");
close(shm_fd);
return;
}
#ifdef __linux__
// Request the kernel to use hugepages for shared memory if possible, improves performance by reducing page faults.
// This only will work (on Linux) if transparent hugepages are enabled for shmem on your system.
// To see/enable this check /sys/kernel/mm/transparent_hugepage/shmem_enabled.
// Related: https://stackoverflow.com/questions/73278608/can-mmaps-performance-be-improved-for-shared-memory
if (madvise(pool_data, pool_size, MADV_HUGEPAGE) < 0)
warn("madvise hugepage");
#endif
// Might help...
if (madvise(pool_data, pool_size, MADV_WILLNEED) < 0)
warn("madvise willneed");
//dbgpri("w: %d h: %d\n", state->width, state->height);
struct wl_shm_pool *pool = wl_shm_create_pool(state->shm, shm_fd, pool_size);
struct wl_buffer *buffer = wl_shm_pool_create_buffer(pool, offset, width, height, stride, WL_SHM_FORMAT_XRGB8888);
wl_shm_pool_destroy(pool);
close(shm_fd);
state->memory.pool_data = pool_data;
state->memory.pool_size = pool_size;
state->memory.buffer = buffer;
}
/*
static void
buffer_release(void *data, struct wl_buffer *wl_buf)
{
(void)data;
dbgpri("buffer release event");
wl_buffer_destroy(wl_buf);
}
static const struct wl_buffer_listener buffer_listener = {
.release = buffer_release,
};
*/
static void
destroy_memory_pool(struct ClientState *state)
{
munmap(state->memory.pool_data, state->memory.pool_size);
state->memory.pool_data = NULL;
wl_buffer_destroy(state->memory.buffer);
/* This event never gets called for some reason, leaking memory, so I just call wl_buffer_destroy manually */
/* wl_buffer_add_listener(state->memory.buffer, &buffer_listener, NULL); */
}
static void
xdg_toplevel_configure(void *data, struct xdg_toplevel *xdgtpl, int32_t width, int32_t height, struct wl_array *states)
{
(void)xdgtpl;
// the compositor is asking us to give it an initial size
if (width == 0 || height == 0)
return;
struct ClientState *state = data;
state->width = width;
state->height = height;
dbgpri("XDG toplevel configure: ");
enum xdg_toplevel_state *xdg_state;
wl_array_for_each(xdg_state, states) {
putchar('\t');
switch (*xdg_state) {
case XDG_TOPLEVEL_STATE_MAXIMIZED:
puts("maximized");
break;
case XDG_TOPLEVEL_STATE_FULLSCREEN:
puts("fullscreen");
break;
case XDG_TOPLEVEL_STATE_RESIZING:
puts("resizing");
break;
case XDG_TOPLEVEL_STATE_ACTIVATED:
puts("activated");
break;
default:
printf("xdgstate: %d\n", *xdg_state);
}
}
}
static void
xdg_toplevel_close(void *data, struct xdg_toplevel *xdgtpl)
{
(void)xdgtpl;
((struct ClientState *)data)->closed = true;
}
static void
xdg_surface_configure(void *data, struct xdg_surface *xdg_surface, uint32_t serial)
{
struct ClientState *state = data;
// Acknowledge to the server that we've configured the surface
xdg_surface_ack_configure(xdg_surface, serial);
dbgpri("XDG surface configure event");
if (state->memory.pool_data != NULL)
destroy_memory_pool(state);
// The surface has a new size now, so create a new memory pool with the proper size
// TODO: we can optimize for the case of the new surface size being smaller than the current
// buffer, avoiding creating a new buffer.
create_memory_pool(state);
// We have to draw here as well, otherwise we will send an empty frame
draw_frame(state);
wl_surface_attach(state->wl_surface, state->memory.buffer, 0, 0);
wl_surface_commit(state->wl_surface);
}
// Reply to a ping event from the compositor, so it does not think that we're dead.
static void
xdg_wm_base_ping(void *data, struct xdg_wm_base *xdg_wm_base, uint32_t serial)
{
(void)data;
xdg_wm_base_pong(xdg_wm_base, serial);
}
static const struct xdg_surface_listener xdg_surface_listener = {
.configure = xdg_surface_configure,
};
static const struct xdg_wm_base_listener xdg_wm_base_listener = {
.ping = xdg_wm_base_ping,
};
static const struct xdg_toplevel_listener xdg_toplevel_listener = {
.configure = xdg_toplevel_configure,
.close = xdg_toplevel_close,
};
static const struct wl_callback_listener wl_surface_frame_listener = {
.done = surface_frame_done,
};
// This function will be called each time the server requests a frame to be drawn
static void
surface_frame_done(void *data, struct wl_callback *cb, uint32_t time)
{
struct ClientState *state = data;
wl_callback_destroy(cb);
// Add this function itself to be called on the next frame event
cb = wl_surface_frame(state->wl_surface);
wl_callback_add_listener(cb, &wl_surface_frame_listener, state);
if (state->last_frame != 0) {
int elapsed = time - state->last_frame;
state->offset += elapsed / 1000.0 * 60; // move 60px per second
}
draw_frame(state);
wl_surface_attach(state->wl_surface, state->memory.buffer, 0, 0);
// To "damage" a surface means to update it
wl_surface_damage_buffer(state->wl_surface, 0, 0, state->width, state->height);
// We're done with this frame, send it
wl_surface_commit(state->wl_surface);
state->last_frame = time;
}
static void
keyboard_keymap(void *data, struct wl_keyboard *keyboard, uint32_t format, int32_t fd, uint32_t size)
{
(void)keyboard;
assert(format == WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1);
struct ClientState *state = data;
// Release previous xkb state, if any
xkb_keymap_unref(state->xkb.keymap);
xkb_state_unref(state->xkb.state);
// must be mapped as MAP_PRIVATE since wl_keyboard version 7
char *map_shm = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0);
if (map_shm == MAP_FAILED) {
warn("mmap keyboard keymap");
return;
}
state->xkb.keymap = xkb_keymap_new_from_string(
state->xkb.ctx, map_shm, XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_COMPILE_NO_FLAGS);
munmap(map_shm, size);
close(fd);
state->xkb.state = xkb_state_new(state->xkb.keymap);
}
static void
keyboard_decode_key(struct ClientState *state, uint32_t keycode, uint32_t kstate)
{
const char *key_state = kstate == WL_KEYBOARD_KEY_STATE_PRESSED ? "press" : "release";
char key_name[255], keyutf8[255];
xkb_keysym_t sym = xkb_state_key_get_one_sym(state->xkb.state, keycode);
xkb_keysym_get_name(sym, key_name, sizeof(key_name));
xkb_state_key_get_utf8(state->xkb.state, keycode, keyutf8, sizeof(keyutf8));
dbgpri("Key %s: name: %s; code: %d; utf8: %s", key_state, key_name, sym, keyutf8);
if (sym == XKB_KEY_Escape)
state->closed = true;
}
static void
keyboard_enter(void *data, struct wl_keyboard *keyboard, uint32_t serial, struct wl_surface *surface,
struct wl_array *keys)
{
(void)keyboard, (void)serial, (void)surface;
dbgpri("Got keyboard enter event");
uint32_t *key;
wl_array_for_each(key, keys) {
keyboard_decode_key(data, *key + 8, 0);
}
}
static void
keyboard_leave(void *data, struct wl_keyboard *keyboard, uint32_t serial, struct wl_surface *surface)
{
dbgpri("Keyboard focus leave event");
(void)data, (void)keyboard, (void)serial, (void)surface;
}
static void
keyboard_key(void *data, struct wl_keyboard *keyboard, uint32_t serial, uint32_t time,
uint32_t key, uint32_t kstate)
{
(void)keyboard, (void)serial, (void)time;
const uint32_t keycode = key + 8; // evdev codes to xkb codes
keyboard_decode_key(data, keycode, kstate);
}
static void
keyboard_modifiers(void *data, struct wl_keyboard *keyboard, uint32_t serial, uint32_t mod_pressed,
uint32_t mod_latchd, uint32_t mod_lockd, uint32_t group)
{
(void)serial, (void)keyboard;
struct ClientState *state = data;
xkb_state_update_mask(state->xkb.state, mod_pressed, mod_latchd, mod_lockd,
0, 0, group);
}
static void
keyboard_repeat_info(void *data, struct wl_keyboard *keyboard, int32_t rate, int32_t delay)
{
(void)data, (void)keyboard, (void)rate, (void)delay;
}
static const struct wl_keyboard_listener wl_keyboard_listener = {
.keymap = keyboard_keymap,
.enter = keyboard_enter,
.leave = keyboard_leave,
.key = keyboard_key,
.modifiers = keyboard_modifiers,
.repeat_info = keyboard_repeat_info,
};
static void
seat_capabilities(void *data, struct wl_seat *seat, uint32_t capabilities)
{
struct ClientState *state = data;
bool keycap = capabilities & WL_SEAT_CAPABILITY_KEYBOARD;
if (keycap && state->keyboard == NULL) {
dbgpri("we have a keyboard");
state->keyboard = wl_seat_get_keyboard(seat);
wl_keyboard_add_listener(state->keyboard, &wl_keyboard_listener, data);
} else if (!keycap && state->keyboard != NULL) {
wl_keyboard_release(state->keyboard);
state->keyboard = NULL;
}
if (capabilities & WL_SEAT_CAPABILITY_POINTER)
dbgpri("we have a pointer");
if (capabilities & WL_SEAT_CAPABILITY_TOUCH)
dbgpri("we have a touchscreen");
}
static void
seat_name(void *data, struct wl_seat *seat, const char *name)
{
(void)data, (void)seat;
dbgpri("seat id: %s", name);
}
static const struct wl_seat_listener wl_seat_listener = {
.capabilities = seat_capabilities,
.name = seat_name,
};
void xdg_toplevel_deco_cfg(void *data, struct zxdg_toplevel_decoration_v1 *deco, uint32_t mode)
{
dbgpri("Deco cfg: %d", mode);
/* We're supposed to send an ack_configure, however, where do we get the serial?
* xdg_surface_ack_configure(((struct ClientState *)data)->xdg_surface, ?);
*/
}
static const struct zxdg_toplevel_decoration_v1_listener xdg_deco_listener = {
.configure = xdg_toplevel_deco_cfg,
};
// On this callback we bind objects with an interface
void reg_handle_global(void *data, struct wl_registry *reg, uint32_t name, const char *interface, uint32_t ver)
{
(void)ver;
struct ClientState *state = data;
// dbgpri("Interface: '%s': ver.: %u, name: %u", interface, ver, name);
if (strcmp(interface, wl_compositor_interface.name) == 0) {
dbgpri("Bound compositor interface: %s", interface);
state->compositor = wl_registry_bind(reg, name, &wl_compositor_interface, COMPOSITOR_INTERFACE_VER);
} else if (strcmp(interface, wl_shm_interface.name) == 0) {
dbgpri("Bound shared memory interface");
state->shm = wl_registry_bind(reg, name, &wl_shm_interface, 1);
} else if (strcmp(interface, xdg_wm_base_interface.name) == 0) {
dbgpri("Bound XDG WM base interface");
state->xdg_wm_base = wl_registry_bind(reg, name, &xdg_wm_base_interface, 1);
xdg_wm_base_add_listener(state->xdg_wm_base, &xdg_wm_base_listener, state);
} else if (strcmp(interface, wl_seat_interface.name) == 0) {
dbgpri("Bound seat interface");
state->seat = wl_registry_bind(reg, name, &wl_seat_interface, 8);
wl_seat_add_listener(state->seat, &wl_seat_listener, state);
} else if (strcmp(interface, zxdg_decoration_manager_v1_interface.name) == 0) {
dbgpri("Bound XDG decoration protocol");
state->xdg_decoration_mgr = wl_registry_bind(reg, name, &zxdg_decoration_manager_v1_interface, 1);
}
}
void reg_handle_global_rm(void *data, struct wl_registry *reg, uint32_t name)
{
(void)data, (void)reg, (void)name;
dbgpri("unbind event: %u", name);
}
static const struct wl_registry_listener reg_listener = {
.global = reg_handle_global, // object bind event
.global_remove = reg_handle_global_rm, // '' removal ''
};
int main()
{
struct ClientState state = {.width = 640, .height = 480};
state.display = wl_display_connect(NULL);
if (state.display == NULL)
errx(EXIT_FAILURE, "could not open display");
dbgpri("Connected to Wayland server; display fd: %d", wl_display_get_fd(state.display));
// get the object registry
state.registry = wl_display_get_registry(state.display);
wl_registry_add_listener(state.registry, &reg_listener, &state);
// Do a roundtrip, block until the server replies to all of our requests
wl_display_roundtrip(state.display);
if (state.compositor == NULL || state.shm == NULL || state.registry == NULL)
errx(EXIT_FAILURE, "Failed to retrieve essential interfaces (comp shm reg)");
if ((state.xkb.ctx = xkb_context_new(XKB_CONTEXT_NO_FLAGS)) == NULL)
errx(EXIT_FAILURE, "xkb_context_new");
state.wl_surface = wl_compositor_create_surface(state.compositor);
state.xdg_surface = xdg_wm_base_get_xdg_surface(state.xdg_wm_base, state.wl_surface);
xdg_surface_add_listener(state.xdg_surface, &xdg_surface_listener, &state);
state.xdg_toplevel = xdg_surface_get_toplevel(state.xdg_surface);
state.xdg_toplevel_deco = zxdg_decoration_manager_v1_get_toplevel_decoration(
state.xdg_decoration_mgr, state.xdg_toplevel
);
/* Set up the toplevel decoration listener.
* Note that this must be done before the setup of the xdg_toplevel attaches any
* buffer to it. */
zxdg_toplevel_decoration_v1_add_listener(
state.xdg_toplevel_deco,
&xdg_deco_listener,
&state
);
xdg_toplevel_add_listener(state.xdg_toplevel, &xdg_toplevel_listener, &state);
xdg_toplevel_set_title(state.xdg_toplevel, TOPLEVEL_TITLE);
xdg_toplevel_set_app_id(state.xdg_toplevel, TOPLEVEL_APP_ID);
/* Tell the compositor to add decorations for us */
zxdg_toplevel_decoration_v1_set_mode(state.xdg_toplevel_deco, ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE);
struct wl_callback *cb = wl_surface_frame(state.wl_surface);
// Bootstrap the frame drawing event sequence
wl_callback_add_listener(cb, &wl_surface_frame_listener, &state);
wl_surface_commit(state.wl_surface);
// Event dispatch loop
while (!state.closed && wl_display_dispatch(state.display) != -1) {
}
dbgpri("Closing connection and destroying objects");
/* Clean up */
xkb_keymap_unref(state.xkb.keymap);
xkb_state_unref(state.xkb.state);
xkb_context_unref(state.xkb.ctx);
destroy_memory_pool(&state);
zxdg_decoration_manager_v1_destroy(state.xdg_decoration_mgr);
zxdg_toplevel_decoration_v1_destroy(state.xdg_toplevel_deco);
xdg_toplevel_destroy(state.xdg_toplevel);
xdg_surface_destroy(state.xdg_surface);
xdg_wm_base_destroy(state.xdg_wm_base);
wl_seat_destroy(state.seat);
wl_surface_destroy(state.wl_surface);
wl_registry_destroy(state.registry);
wl_shm_destroy(state.shm);
wl_compositor_destroy(state.compositor);
wl_display_disconnect(state.display);
return 0;
}

View file

@ -0,0 +1,33 @@
#ifndef _dbpri_h_
#define _dbpri_h_
#include <stdarg.h>
#include <stdio.h>
#include <time.h>
#ifdef _DBGPRINT_
#define dbgpri(...) dbgpri_impl(__VA_ARGS__)
#else
#define dbgpri(...)
#endif
static void dbgpri_impl(const char *fmt, ...)
{
va_list args;
struct timespec now;
char msg[2048];
va_start(args, fmt);
clock_gettime(CLOCK_MONOTONIC_RAW, &now);
vsnprintf(msg, sizeof(msg), fmt, args);
fprintf(stderr, "[%ld.%-9ld] %s\n", now.tv_sec, now.tv_nsec, msg);
va_end(args);
}
static void dbgput(const char *str)
{
fprintf(stderr, "%s\n", str);
}
#endif

View file

@ -0,0 +1,42 @@
project('wayland-client-test', 'c', default_options: ['warning_level=2'])
wl_scanner = dependency('wayland-scanner', native: true)
wl_scanner_prog = find_program(wl_scanner.get_variable('wayland_scanner'), native: true)
wl_scanner_pcode = generator(
wl_scanner_prog,
output: '@BASENAME@-protocol.c',
arguments: ['private-code', '@INPUT@', '@OUTPUT@']
)
wl_scanner_client = generator(
wl_scanner_prog,
output: '@BASENAME@-client-protocol.h',
arguments: ['client-header', '@INPUT@', '@OUTPUT@']
)
wl_stable_proto_dir = '/usr/share/wayland-protocols/stable/'
wl_unstable_proto_dir = '/usr/share/wayland-protocols/unstable/'
wl_protocols = [
wl_stable_proto_dir / 'xdg-shell/xdg-shell.xml',
wl_unstable_proto_dir / 'xdg-decoration/xdg-decoration-unstable-v1.xml'
]
wl_protocol_srcs = []
foreach filename : wl_protocols
wl_protocol_srcs += wl_scanner_pcode.process(filename)
wl_protocol_srcs += wl_scanner_client.process(filename)
endforeach
if get_option('buildtype') == 'release'
add_project_arguments('-march=native', '-fno-plt', '-flto', language: 'c')
endif
cc = meson.get_compiler('c')
deps = [
cc.find_library('rt', required: false), # for shm_* functions
dependency('wayland-client'),
dependency('xkbcommon'),
]
srcs = ['client.c', 'shm_helper.c', wl_protocol_srcs]
executable('client', srcs, dependencies: deps)

View file

@ -0,0 +1,53 @@
// taken from wayland-book.com (public domain)
#define _POSIX_C_SOURCE 200112L
#include <errno.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <time.h>
#include <unistd.h>
static void
randname(char *buf)
{
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
long r = ts.tv_nsec;
for (int i = 0; i < 6; ++i) {
buf[i] = 'A'+(r&15)+(r&16)*2;
r >>= 5;
}
}
static int
create_shm_file(void)
{
int retries = 100;
do {
char name[] = "/wl_shm-XXXXXX";
randname(name + sizeof(name) - 7);
--retries;
int fd = shm_open(name, O_RDWR | O_CREAT | O_EXCL, 0600);
if (fd >= 0) {
shm_unlink(name);
return fd;
}
} while (retries > 0 && errno == EEXIST);
return -1;
}
int
allocate_shm_file(size_t size)
{
int fd = create_shm_file();
if (fd < 0)
return -1;
int ret;
do {
ret = ftruncate(fd, size);
} while (ret < 0 && errno == EINTR);
if (ret < 0) {
close(fd);
return -1;
}
return fd;
}

View file

@ -0,0 +1,3 @@
#include <stddef.h>
int allocate_shm_file(size_t size);