| git.druid.rocks | index | militantprogrammer | astral_canvas | src/ | platform/ | x11.c |
src/platform/x11.c
#include "platform.h"
#include "../input/input.h"
#include "shm.h"
#include <errno.h>
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <xcb/xcb.h>
#include <xcb/shm.h>
#include <xcb/xkb.h>
#include <xcb/xcb_cursor.h>
#include <xcb/xcb_icccm.h>
#include <xkbcommon/xkbcommon-x11.h>
#define VK_USE_PLATFORM_XCB_KHR
#include <vulkan/vulkan.h>
/*
* X11/xcb backend, stored in Platform.impl. Owns the connection, window,
* graphics context, MIT-SHM (or SysV / PutImage fallback) pixel buffer,
* and xkbcommon-x11 keyboard state.
*
* Buffer attach order: memfd + xcb_shm_attach_fd, else SysV shmget +
* xcb_shm_attach, else a malloc'd buffer uploaded with PutImage in
* strips. shm_fd / shmid are mutually exclusive; the unused one is -1.
*/
typedef struct {
Platform *platform;
xcb_connection_t *connection;
xcb_screen_t *screen;
xcb_window_t window;
xcb_gcontext_t graphicsContext;
uint8_t depth;
xcb_atom_t wmProtocols;
xcb_atom_t wmDeleteWindow;
xcb_atom_t netWmName;
xcb_atom_t utf8String;
xcb_atom_t netWmPid;
xcb_cursor_context_t *cursorContext;
xcb_cursor_t cursor;
int shmFileDescriptor;
int shmId;
size_t shmSize;
xcb_shm_seg_t shmSegment;
bool useShm;
InputXkb xkb;
uint8_t xkbBaseEvent;
int32_t xkbDeviceId;
bool keyDown[256];
} X11;
static xcb_atom_t intern_atom(xcb_connection_t *conn, const char *name)
{
xcb_intern_atom_cookie_t cookie = xcb_intern_atom(conn, 0, (uint16_t)strlen(name), name);
xcb_intern_atom_reply_t *reply = xcb_intern_atom_reply(conn, cookie, NULL);
xcb_atom_t atom = reply ? reply->atom : XCB_ATOM_NONE;
free(reply);
return atom;
}
static void x11Push(X11 *x11, Event ev)
{
eventQueuePush(&x11->platform->events, ev);
}
static void x11FreeBuffer(X11 *x11)
{
if (x11->useShm && x11->shmSegment) {
xcb_shm_detach(x11->connection, x11->shmSegment);
x11->shmSegment = 0;
}
if (x11->shmId >= 0) {
if (x11->platform->pixels)
shmdt(x11->platform->pixels);
x11->shmId = -1;
} else if (x11->shmFileDescriptor >= 0) {
shm_free(x11->shmFileDescriptor, x11->platform->pixels, x11->shmSize);
x11->shmFileDescriptor = -1;
} else {
free(x11->platform->pixels);
}
x11->platform->pixels = NULL;
x11->shmSize = 0;
}
static int x11AllocBuffer(X11 *x11, int width, int height)
{
if (width < 1)
width = 1;
if (height < 1)
height = 1;
x11FreeBuffer(x11);
size_t size = (size_t)width * (size_t)height * 4;
x11->platform->width = width;
x11->platform->height = height;
x11->platform->bufferWidth = width;
x11->platform->bufferHeight = height;
x11->platform->stride = width * 4;
if (x11->useShm) {
x11->shmSegment = xcb_generate_id(x11->connection);
int fd = shm_alloc(size, (void **)&x11->platform->pixels);
if (fd >= 0) {
int send_fd = dup(fd);
if (send_fd >= 0) {
xcb_void_cookie_t cookie = xcb_shm_attach_fd_checked(x11->connection, x11->shmSegment, send_fd, 0);
xcb_generic_error_t *err = xcb_request_check(x11->connection, cookie);
if (!err) {
x11->shmFileDescriptor = fd;
x11->shmSize = size;
x11->shmId = -1;
return 0;
}
free(err);
}
shm_free(fd, x11->platform->pixels, size);
x11->platform->pixels = NULL;
}
int shmid = shmget(IPC_PRIVATE, size, IPC_CREAT | 0600);
if (shmid >= 0) {
void *map = shmat(shmid, NULL, 0);
shmctl(shmid, IPC_RMID, NULL);
if (map != (void *)-1) {
xcb_void_cookie_t cookie = xcb_shm_attach_checked(x11->connection, x11->shmSegment, (uint32_t)shmid, 0);
xcb_generic_error_t *err = xcb_request_check(x11->connection, cookie);
if (!err) {
memset(map, 0, size);
x11->platform->pixels = (uint32_t *)map;
x11->shmId = shmid;
x11->shmSize = size;
x11->shmFileDescriptor = -1;
return 0;
}
free(err);
shmdt(map);
}
}
fprintf(stderr, "x11: shm attach failed, falling back to PutImage\n");
x11->useShm = false;
x11->shmSegment = 0;
}
x11->platform->pixels = (uint32_t*)calloc(1, size);
if (!x11->platform->pixels)
return -1;
x11->shmSize = size;
x11->shmFileDescriptor = -1;
x11->shmId = -1;
return 0;
}
static int x11ReloadKeymap(X11 *x11)
{
struct xkb_keymap *keymap = xkb_x11_keymap_new_from_device(
x11->xkb.context, x11->connection, x11->xkbDeviceId, XKB_KEYMAP_COMPILE_NO_FLAGS);
if (!keymap)
return -1;
struct xkb_state *state = xkb_x11_state_new_from_device(keymap, x11->connection, x11->xkbDeviceId);
if (!state) {
xkb_keymap_unref(keymap);
return -1;
}
inputXkbSetKeymap(&x11->xkb, keymap, state);
x11->platform->modifiers = inputModifiers(&x11->xkb);
return 0;
}
static int x11SetupXkb(X11 *x11)
{
uint8_t base_error = 0;
if (!xkb_x11_setup_xkb_extension(x11->connection,
XKB_X11_MIN_MAJOR_XKB_VERSION,
XKB_X11_MIN_MINOR_XKB_VERSION,
XKB_X11_SETUP_XKB_EXTENSION_NO_FLAGS,
NULL, NULL, &x11->xkbBaseEvent, &base_error)) {
fprintf(stderr, "x11: XKB extension unavailable\n");
return -1;
}
x11->xkbDeviceId = xkb_x11_get_core_keyboard_device_id(x11->connection);
if (x11->xkbDeviceId == -1)
return -1;
if (inputXkbInit(&x11->xkb) < 0)
return -1;
if (x11ReloadKeymap(x11) < 0)
return -1;
const uint16_t events =
XCB_XKB_EVENT_TYPE_NEW_KEYBOARD_NOTIFY |
XCB_XKB_EVENT_TYPE_MAP_NOTIFY |
XCB_XKB_EVENT_TYPE_STATE_NOTIFY;
xcb_xkb_select_events(x11->connection, (xcb_xkb_device_spec_t)x11->xkbDeviceId,
events, 0, events, 0, 0, NULL);
xcb_xkb_per_client_flags(x11->connection, (xcb_xkb_device_spec_t)x11->xkbDeviceId,
XCB_XKB_PER_CLIENT_FLAG_DETECTABLE_AUTO_REPEAT,
XCB_XKB_PER_CLIENT_FLAG_DETECTABLE_AUTO_REPEAT,
0, 0, 0);
return 0;
}
static uint32_t x11ScrollButton(uint8_t detail, PointerEvent *pe)
{
switch (detail) {
case 4:
pe->axis = AXIS_VERTICAL;
pe->axisValue = -1.0;
pe->axisSteps = -1;
return 1;
case 5:
pe->axis = AXIS_VERTICAL;
pe->axisValue = 1.0;
pe->axisSteps = 1;
return 1;
case 6:
pe->axis = AXIS_HORIZONTAL;
pe->axisValue = -1.0;
pe->axisSteps = -1;
return 1;
case 7:
pe->axis = AXIS_HORIZONTAL;
pe->axisValue = 1.0;
pe->axisSteps = 1;
return 1;
default:
return 0;
}
}
static uint32_t x11DetailToButton(uint8_t detail)
{
switch (detail) {
case 1: return BUTTON_LEFT;
case 2: return BUTTON_MIDDLE;
case 3: return BUTTON_RIGHT;
case 8: return BUTTON_BACK;
case 9: return BUTTON_FORWARD;
default: return 0;
}
}
static void x11PointerEvent(X11 *x11, uint32_t kind, uint32_t time, int16_t px, int16_t py)
{
Event ev = {0};
ev.kind = EVENT_POINTER;
ev.pointer.kind = kind;
ev.pointer.timeMilliseconds = time;
ev.pointer.x = (double)px;
ev.pointer.y = (double)py;
ev.pointer.buttons = x11->platform->buttons;
ev.pointer.modifiers = x11->platform->modifiers;
x11->platform->mouseX = ev.pointer.x;
x11->platform->mouseY = ev.pointer.y;
x11Push(x11, ev);
}
static void x11HandleXkb(X11 *x11, xcb_generic_event_t *ge)
{
xcb_xkb_state_notify_event_t *ne = (xcb_xkb_state_notify_event_t *)ge;
switch (ne->xkbType) {
case XCB_XKB_NEW_KEYBOARD_NOTIFY: {
xcb_xkb_new_keyboard_notify_event_t *e = (xcb_xkb_new_keyboard_notify_event_t *)ge;
if (e->deviceID == x11->xkbDeviceId && (e->changed & XCB_XKB_NKN_DETAIL_KEYCODES))
x11ReloadKeymap(x11);
break;
}
case XCB_XKB_MAP_NOTIFY:
x11ReloadKeymap(x11);
break;
case XCB_XKB_STATE_NOTIFY:
if (x11->xkb.state) {
xkb_state_update_mask(x11->xkb.state,
ne->baseMods, ne->latchedMods, ne->lockedMods,
(uint32_t)ne->baseGroup, (uint32_t)ne->latchedGroup,
ne->lockedGroup);
x11->platform->modifiers = inputModifiers(&x11->xkb);
}
break;
default:
break;
}
}
static void x11HandleEvent(X11 *x11, xcb_generic_event_t *ge)
{
uint8_t type = ge->response_type & ~0x80;
if (type == 0) {
xcb_generic_error_t *err = (xcb_generic_error_t *)ge;
fprintf(stderr, "x11 error: code=%u major=%u minor=%u\n",
err->error_code, err->major_code, err->minor_code);
return;
}
if (type == x11->xkbBaseEvent) {
x11HandleXkb(x11, ge);
return;
}
switch (type) {
case XCB_EXPOSE:
x11Push(x11, (Event){ .kind = EVENT_EXPOSE });
break;
case XCB_CONFIGURE_NOTIFY: {
xcb_configure_notify_event_t *e = (xcb_configure_notify_event_t *)ge;
if (e->window != x11->window)
break;
if (e->width == x11->platform->width && e->height == x11->platform->height)
break;
if (x11AllocBuffer(x11, e->width, e->height) < 0)
break;
Event ev = { .kind = EVENT_RESIZE };
ev.resize.width = x11->platform->width;
ev.resize.height = x11->platform->height;
ev.resize.scale = x11->platform->scale;
x11Push(x11, ev);
break;
}
case XCB_CLIENT_MESSAGE: {
xcb_client_message_event_t *e = (xcb_client_message_event_t *)ge;
if (e->data.data32[0] == x11->wmDeleteWindow) {
x11->platform->running = false;
x11Push(x11, (Event){ .kind = EVENT_QUIT });
}
break;
}
case XCB_FOCUS_IN:
case XCB_FOCUS_OUT: {
x11->platform->focused = (type == XCB_FOCUS_IN);
Event ev = { .kind = EVENT_FOCUS };
ev.focus.focused = x11->platform->focused;
x11Push(x11, ev);
break;
}
case XCB_ENTER_NOTIFY: {
xcb_enter_notify_event_t *e = (xcb_enter_notify_event_t *)ge;
x11->platform->pointerInside = true;
x11PointerEvent(x11, POINTER_ENTER, e->time, e->event_x, e->event_y);
break;
}
case XCB_LEAVE_NOTIFY: {
xcb_leave_notify_event_t *e = (xcb_leave_notify_event_t *)ge;
x11->platform->pointerInside = false;
x11PointerEvent(x11, POINTER_LEAVE, e->time, e->event_x, e->event_y);
break;
}
case XCB_MOTION_NOTIFY: {
xcb_motion_notify_event_t *e = (xcb_motion_notify_event_t *)ge;
x11PointerEvent(x11, POINTER_MOTION, e->time, e->event_x, e->event_y);
break;
}
case XCB_BUTTON_PRESS:
case XCB_BUTTON_RELEASE: {
xcb_button_press_event_t *e = (xcb_button_press_event_t *)ge;
bool pressed = (type == XCB_BUTTON_PRESS);
PointerEvent pe = {0};
pe.timeMilliseconds = e->time;
pe.x = e->event_x;
pe.y = e->event_y;
pe.modifiers = x11->platform->modifiers;
x11->platform->mouseX = pe.x;
x11->platform->mouseY = pe.y;
if (pressed && x11ScrollButton(e->detail, &pe)) {
pe.kind = POINTER_AXIS;
pe.axisSource = AXIS_SOURCE_WHEEL;
pe.buttons = x11->platform->buttons;
Event ev = { .kind = EVENT_POINTER };
ev.pointer = pe;
x11Push(x11, ev);
break;
}
uint32_t button = x11DetailToButton(e->detail);
if (!button)
break;
uint8_t mask = buttonMask(button);
if (pressed)
x11->platform->buttons |= mask;
else
x11->platform->buttons &= (uint8_t)~mask;
pe.kind = POINTER_BUTTON;
pe.button = button;
pe.pressed = pressed;
pe.buttons = x11->platform->buttons;
Event ev = { .kind = EVENT_POINTER };
ev.pointer = pe;
x11Push(x11, ev);
break;
}
case XCB_KEY_PRESS:
case XCB_KEY_RELEASE: {
xcb_key_press_event_t *e = (xcb_key_press_event_t *)ge;
xkb_keycode_t keycode = e->detail;
bool pressed = (type == XCB_KEY_PRESS);
bool repeat = false;
if (keycode < 256) {
if (pressed && x11->keyDown[keycode])
repeat = true;
x11->keyDown[keycode] = pressed;
}
Event ev = { .kind = EVENT_KEY };
input_fill_key(&x11->xkb, &ev.key, keycode, pressed, repeat);
ev.key.timeMilliseconds = e->time;
x11->platform->modifiers = ev.key.modifiers;
x11Push(x11, ev);
break;
}
case XCB_MAPPING_NOTIFY:
break;
default:
break;
}
}
static int x11Fd(Platform *platform)
{
X11 *x11 = (X11*)platform->implementation;
return xcb_get_file_descriptor(x11->connection);
}
static int x11Flush(Platform *platform)
{
X11 *x11 = (X11*)platform->implementation;
return xcb_flush(x11->connection) ? 0 : -1;
}
static int x11Dispatch(Platform *platform)
{
X11 *x11 = (X11*)platform->implementation;
if (xcb_connection_has_error(x11->connection)) {
platform->running = false;
x11Push(x11, (Event){ .kind = EVENT_QUIT });
return -1;
}
xcb_generic_event_t *ge;
while ((ge = xcb_poll_for_event(x11->connection))) {
x11HandleEvent(x11, ge);
free(ge);
}
return 0;
}
static int x11PollEvents(Platform *platform, int timeoutMilliseconds)
{
X11 *x11 = (X11*)platform->implementation;
xcb_flush(x11->connection);
struct pollfd fd = {
.fd = xcb_get_file_descriptor(x11->connection),
.events = POLLIN,
};
int n = poll(&fd, 1, timeoutMilliseconds);
if (n < 0 && errno != EINTR)
return -1;
return x11Dispatch(platform);
}
static int x11_create_vk_surface(Platform *platform, void *vulkanInstance, void *outSurface)
{
X11 *x11 = (X11*)platform->implementation;
VkXcbSurfaceCreateInfoKHR ci = {
.sType = VK_STRUCTURE_TYPE_XCB_SURFACE_CREATE_INFO_KHR,
.connection = x11->connection,
.window = x11->window,
};
VkSurfaceKHR surface = VK_NULL_HANDLE;
VkResult result = vkCreateXcbSurfaceKHR((VkInstance)vulkanInstance, &ci, NULL, &surface);
if (result != VK_SUCCESS) {
fprintf(stderr, "x11: vkCreateXcbSurfaceKHR failed (%d)\n", (int)result);
return -1;
}
*(VkSurfaceKHR *)outSurface = surface;
return 0;
}
static int x11_present(Platform *platform)
{
X11 *x11 = (X11*)platform->implementation;
if (platform->gpuPresent)
return 0;
if (!platform->pixels)
return -1;
if (x11->useShm) {
xcb_shm_put_image(x11->connection, x11->window, x11->graphicsContext,
(uint16_t)platform->bufferWidth, (uint16_t)platform->bufferHeight,
0, 0, (uint16_t)platform->bufferWidth, (uint16_t)platform->bufferHeight,
0, 0, x11->depth, XCB_IMAGE_FORMAT_Z_PIXMAP,
0, x11->shmSegment, 0);
} else {
/* PutImage max request is limited; send in strips. */
int max_rows = 16384 / (platform->bufferWidth * 4);
if (max_rows < 1)
max_rows = 1;
for (int y = 0; y < platform->bufferHeight; y += max_rows) {
int rows = platform->bufferHeight - y;
if (rows > max_rows)
rows = max_rows;
xcb_put_image(x11->connection, XCB_IMAGE_FORMAT_Z_PIXMAP, x11->window, x11->graphicsContext,
(uint16_t)platform->bufferWidth, (uint16_t)rows, 0, (int16_t)y, 0,
x11->depth, (uint32_t)(platform->bufferWidth * rows * 4),
(const uint8_t *)(platform->pixels + y * platform->bufferWidth));
}
}
xcb_flush(x11->connection);
return 0;
}
static void x11_destroy(Platform *platform)
{
X11 *x11 = (X11*)platform->implementation;
if (!x11) return;
x11FreeBuffer(x11);
inputXkbFinish(&x11->xkb);
if (x11->cursor)
xcb_free_cursor(x11->connection, x11->cursor);
if (x11->cursorContext)
xcb_cursor_context_free(x11->cursorContext);
if (x11->graphicsContext)
xcb_free_gc(x11->connection, x11->graphicsContext);
if (x11->window)
xcb_destroy_window(x11->connection, x11->window);
if (x11->connection)
xcb_disconnect(x11->connection);
free(x11);
platform->implementation = NULL;
platform->pixels = NULL;
}
int platformX11Open(Platform *platform, const char *title, int width, int height)
{
X11 *x11 = (X11*)calloc(1, sizeof(*x11));
if (!x11)
return -1;
x11->platform = platform;
x11->shmFileDescriptor = -1;
x11->shmId = -1;
platform->implementation = x11;
platform->backend = BACKEND_X11;
platform->fileDescriptor = x11Fd;
platform->flush = x11Flush;
platform->dispatch = x11Dispatch;
platform->pollEvents = x11PollEvents;
platform->present = x11_present;
platform->destroy = x11_destroy;
platform->vulkanSurfaceExtension = VK_KHR_XCB_SURFACE_EXTENSION_NAME;
platform->createVulkanSurface = x11_create_vk_surface;
int screen_n = 0;
x11->connection = xcb_connect(NULL, &screen_n);
if (!x11->connection || xcb_connection_has_error(x11->connection)) {
fprintf(stderr, "x11: connect failed\n");
x11_destroy(platform);
return -1;
}
const xcb_setup_t *setup = xcb_get_setup(x11->connection);
xcb_screen_iterator_t it = xcb_setup_roots_iterator(setup);
for (int i = 0; i < screen_n; i++)
xcb_screen_next(&it);
x11->screen = it.data;
x11->depth = x11->screen->root_depth;
const xcb_query_extension_reply_t *shm_ext = xcb_get_extension_data(x11->connection, &xcb_shm_id);
x11->useShm = shm_ext && shm_ext->present;
uint32_t mask = XCB_CW_BACK_PIXEL | XCB_CW_EVENT_MASK;
uint32_t values[2] = {
x11->screen->black_pixel,
XCB_EVENT_MASK_KEY_PRESS | XCB_EVENT_MASK_KEY_RELEASE |
XCB_EVENT_MASK_BUTTON_PRESS | XCB_EVENT_MASK_BUTTON_RELEASE |
XCB_EVENT_MASK_POINTER_MOTION | XCB_EVENT_MASK_BUTTON_MOTION |
XCB_EVENT_MASK_ENTER_WINDOW | XCB_EVENT_MASK_LEAVE_WINDOW |
XCB_EVENT_MASK_STRUCTURE_NOTIFY | XCB_EVENT_MASK_EXPOSURE |
XCB_EVENT_MASK_FOCUS_CHANGE,
};
x11->window = xcb_generate_id(x11->connection);
xcb_create_window(x11->connection, XCB_COPY_FROM_PARENT, x11->window, x11->screen->root,
0, 0, (uint16_t)width, (uint16_t)height, 0,
XCB_WINDOW_CLASS_INPUT_OUTPUT, x11->screen->root_visual,
mask, values);
x11->graphicsContext = xcb_generate_id(x11->connection);
xcb_create_gc(x11->connection, x11->graphicsContext, x11->window, 0, NULL);
x11->wmProtocols = intern_atom(x11->connection, "WM_PROTOCOLS");
x11->wmDeleteWindow = intern_atom(x11->connection, "WM_DELETE_WINDOW");
x11->netWmName = intern_atom(x11->connection, "_NET_WM_NAME");
x11->utf8String = intern_atom(x11->connection, "UTF8_STRING");
x11->netWmPid = intern_atom(x11->connection, "_NET_WM_PID");
if (!title)
title = "terminal";
xcb_change_property(x11->connection, XCB_PROP_MODE_REPLACE, x11->window,
XCB_ATOM_WM_NAME, XCB_ATOM_STRING, 8,
(uint32_t)strlen(title), title);
xcb_change_property(x11->connection, XCB_PROP_MODE_REPLACE, x11->window,
x11->netWmName, x11->utf8String, 8,
(uint32_t)strlen(title), title);
xcb_icccm_set_wm_protocols(x11->connection, x11->window, x11->wmProtocols, 1, &x11->wmDeleteWindow);
static const char wm_class[] = "terminal\0Terminal";
xcb_icccm_set_wm_class(x11->connection, x11->window, sizeof(wm_class), wm_class);
xcb_size_hints_t hints;
memset(&hints, 0, sizeof(hints));
xcb_icccm_size_hints_set_min_size(&hints, 160, 80);
xcb_icccm_set_wm_normal_hints(x11->connection, x11->window, &hints);
uint32_t pid = (uint32_t)getpid();
xcb_change_property(x11->connection, XCB_PROP_MODE_REPLACE, x11->window,
x11->netWmPid, XCB_ATOM_CARDINAL, 32, 1, &pid);
if (xcb_cursor_context_new(x11->connection, x11->screen, &x11->cursorContext) == 0) {
x11->cursor = xcb_cursor_load_cursor(x11->cursorContext, "xterm");
if (!x11->cursor)
x11->cursor = xcb_cursor_load_cursor(x11->cursorContext, "left_ptr");
if (x11->cursor) {
uint32_t cid = x11->cursor;
xcb_change_window_attributes(x11->connection, x11->window, XCB_CW_CURSOR, &cid);
}
}
if (x11SetupXkb(x11) < 0) {
x11_destroy(platform);
return -1;
}
if (x11AllocBuffer(x11, width, height) < 0) {
x11_destroy(platform);
return -1;
}
xcb_map_window(x11->connection, x11->window);
xcb_flush(x11->connection);
platform->focused = true;
return 0;
}