scratch/audiovisual/wayland/client.c
2025-01-17 00:32:12 -03:00

512 lines
16 KiB
C

// 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;
}