SchOS/projects/schcom/schcom.c
Nexoniarz 8b7b8df74f usb-hid: bind an absolute pointer, and plumb EV_ABS through
Groundwork for usb-tablet, which is the fix for the guest cursor and the
host's never being in the same place: a PS/2 mouse reports how far it
moved, two pointers accumulating deltas independently drift apart, and no
amount of care in the compositor can make them agree. An absolute device
reports where it is, so they coincide by construction.

  - the HID driver no longer matches the boot subclass only. usb-tablet
    declares subclass 0 and was refused at the door; hid_probe decides
    what an interface actually is, and still declines what it cannot make
    sense of.
  - a non-boot HID interface is taken to be an absolute pointer and is
    not asked for boot protocol, which it does not have -- that request
    failing is what stopped it binding.
  - tablet_report decodes the fixed 6-byte layout (buttons, 16-bit X,
    16-bit Y, wheel) and scales the logical range to pixels there,
    because it is the only place that knows the range.
  - input_mouse_abs reports EV_ABS/ABS_X/ABS_Y so a reader can tell an
    absolute position from a delta, and mouse_set_abs places the pointer
    rather than nudging it.
  - Schcom treats EV_ABS as authoritative and EV_REL as a nudge.

The device binds and identifies correctly: "usb-hid: tablet (absolute) on
slot 1, endpoint 0x81 every 4 ms".

It is NOT finished, and QEMU_INPUT is empty by default because of it. No
reports have yet been observed arriving on the tablet's interrupt
endpoint, so making it the default pointer would trade a cursor that
drifts for one that does not move at all. Opt in with

  make qemu QEMU_INPUT='-device qemu-xhci,id=xhci -device usb-tablet,bus=xhci.0'

Next step is to find out why nothing arrives: whether the first transfer
is queued but never completes, whether a completion needs re-arming for
this endpoint, or whether QEMU is not routing motion to the tablet at all
(`info mice`).

bootcheck 108 [ok], schcom-test passes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 18:27:01 +02:00

715 lines
22 KiB
C

/* SPDX-License-Identifier: Apache-2.0 */
/*
* Schcom -- the SchOS compositor.
*
* Owns the display and the input devices. Clients draw into shared buffers and
* hand them over; Schcom decides what is on top of what, composites, and
* presents. It does not decide where windows go: that is SchWM's job, and the
* moment this file starts making placement decisions the split has failed.
*
* v0, deliberately small but not a toy:
*
* - real framebuffer output through /dev/fb0 and the fbdev ioctls
* - real clients over a Unix socket, with a versioned handshake
* - real shared-memory buffers, one per surface
* - a back buffer, so a half-composited frame never reaches the screen
* - input read from /dev/input/eventN and routed to the top surface
*
* What it does not do yet: damage tracking beyond "repaint everything",
* multiple outputs, subsurfaces, or any notion of focus beyond "topmost".
* Those are additions, not rewrites -- which is the point of committing to the
* surface/commit model now rather than growing into it.
*/
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/shm.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/un.h>
#include <linux/fb.h>
#include <linux/input.h>
#include "proto.h"
#define MAX_CLIENTS 16
#define MAX_SURFACES 32
struct surface {
int used;
uint32_t id;
int client; /* index into clients[] */
int shmid;
void *pixels;
uint32_t w, h, stride, size;
int32_t x, y;
};
struct client {
int fd; /* -1 when the slot is free */
int hello_done;
};
static struct client clients[MAX_CLIENTS];
static struct surface surfaces[MAX_SURFACES];
static uint32_t next_surface_id = 1;
/* The screen. */
static uint8_t *fb;
static uint32_t fb_w, fb_h, fb_stride, fb_bpp;
static size_t fb_size;
static uint8_t *back; /* composited here, then copied out in one go */
/* Pointer position: set outright by EV_ABS from a tablet, nudged by EV_REL
* from a plain mouse. */
static int32_t ptr_x, ptr_y;
static void
die(const char *what)
{
fprintf(stderr, "schcom: %s: %s\n", what, strerror(errno));
exit(1);
}
/* ---------------------------------------------------------------- output */
static int
open_framebuffer(void)
{
int fd = open("/dev/fb0", O_RDWR);
if (fd < 0)
die("open /dev/fb0");
struct fb_var_screeninfo var;
struct fb_fix_screeninfo fix;
if (ioctl(fd, FBIOGET_VSCREENINFO, &var) < 0)
die("FBIOGET_VSCREENINFO");
if (ioctl(fd, FBIOGET_FSCREENINFO, &fix) < 0)
die("FBIOGET_FSCREENINFO");
fb_w = var.xres;
fb_h = var.yres;
fb_bpp = var.bits_per_pixel;
/* The stride comes from the driver, never from xres*bpp: the two differ on
* real hardware and assuming otherwise shears the image. */
fb_stride = fix.line_length;
fb_size = (size_t)fb_stride * fb_h;
if (fb_bpp != 32) {
fprintf(stderr, "schcom: need 32bpp, got %u\n", fb_bpp);
exit(1);
}
fb = mmap(NULL, fb_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (fb == MAP_FAILED)
die("mmap /dev/fb0");
back = malloc(fb_size);
if (!back)
die("malloc back buffer");
printf("schcom: %ux%u, %u bpp, stride %u\n", fb_w, fb_h, fb_bpp, fb_stride);
fflush(stdout);
return fd;
}
/* Damage: the rectangle that has to be redrawn and copied out.
*
* Without this the compositor repainted the whole screen for every event --
* at 1280x800 that is a million pixels of desktop fill plus a 4 MB copy to the
* framebuffer, per mouse movement. Under emulation the pointer lagged behind
* the mouse by whole seconds, and it was not the emulation's fault: moving a
* 10x10 cursor does not require touching four megabytes.
*
* One rectangle rather than a list. Two distant small changes then repaint the
* span between them, which is wasteful in theory; in practice the changes are
* a pointer and the window under it, and a list costs more bookkeeping than it
* saves.
*/
static int dmg_x0, dmg_y0, dmg_x1, dmg_y1; /* x1/y1 exclusive */
static int dirty;
static void
damage(int x, int y, int w, int h)
{
int x0 = x, y0 = y, x1 = x + w, y1 = y + h;
if (x0 < 0) x0 = 0;
if (y0 < 0) y0 = 0;
if (x1 > (int)fb_w) x1 = (int)fb_w;
if (y1 > (int)fb_h) y1 = (int)fb_h;
if (x0 >= x1 || y0 >= y1)
return;
if (!dirty) {
dmg_x0 = x0; dmg_y0 = y0; dmg_x1 = x1; dmg_y1 = y1;
dirty = 1;
return;
}
if (x0 < dmg_x0) dmg_x0 = x0;
if (y0 < dmg_y0) dmg_y0 = y0;
if (x1 > dmg_x1) dmg_x1 = x1;
if (y1 > dmg_y1) dmg_y1 = y1;
}
static void
damage_all(void)
{
damage(0, 0, (int)fb_w, (int)fb_h);
}
#define PTR_SIZE 10
static void
composite(void)
{
if (!dirty)
return;
const int rx0 = dmg_x0, ry0 = dmg_y0, rx1 = dmg_x1, ry1 = dmg_y1;
/* Desktop. A flat colour rather than black, so that "nothing was drawn"
* and "the compositor is not running" cannot look the same. */
for (int y = ry0; y < ry1; y++) {
uint32_t *row = (uint32_t *)(back + (size_t)y * fb_stride);
for (int x = rx0; x < rx1; x++)
row[x] = 0x00203040;
}
/* Surfaces, bottom to top. Creation order is stacking order in v0; SchWM
* will reorder later through a protocol request rather than by reaching in
* here. */
for (int i = 0; i < MAX_SURFACES; i++) {
struct surface *s = &surfaces[i];
if (!s->used || !s->pixels)
continue;
/* Only the part of this surface inside the damage rectangle. */
for (uint32_t sy = 0; sy < s->h; sy++) {
int dy = s->y + (int)sy;
if (dy < ry0 || dy >= ry1)
continue;
uint32_t *dst = (uint32_t *)(back + (size_t)dy * fb_stride);
uint32_t *src = (uint32_t *)((uint8_t *)s->pixels + (size_t)sy * s->stride);
for (uint32_t sx = 0; sx < s->w; sx++) {
int dx = s->x + (int)sx;
if (dx < rx0 || dx >= rx1)
continue;
dst[dx] = src[sx];
}
}
}
/* A crude pointer, drawn last so it is never under a window. */
for (int py = 0; py < PTR_SIZE; py++) {
int dy = ptr_y + py;
if (dy < ry0 || dy >= ry1)
continue;
uint32_t *dst = (uint32_t *)(back + (size_t)dy * fb_stride);
for (int px = 0; px < PTR_SIZE - py; px++) {
int dx = ptr_x + px;
if (dx >= rx0 && dx < rx1)
dst[dx] = 0x00FFFFFF;
}
}
/* Copy out only what changed, row by row. Compositing straight into the
* framebuffer would show every intermediate state -- the flicker would be
* the whole desktop being repainted under the user's eyes -- but copying
* the whole thing to move a cursor is what made the pointer lag. */
size_t bytes = (size_t)(rx1 - rx0) * 4;
for (int y = ry0; y < ry1; y++) {
size_t off = (size_t)y * fb_stride + (size_t)rx0 * 4;
memcpy(fb + off, back + off, bytes);
}
dirty = 0;
}
/* ---------------------------------------------------------------- clients */
static int
send_all(int fd, const void *buf, size_t n)
{
const uint8_t *p = buf;
while (n) {
ssize_t w = write(fd, p, n);
if (w < 0) {
if (errno == EINTR)
continue;
return -1;
}
p += w;
n -= (size_t)w;
}
return 0;
}
static int
read_all(int fd, void *buf, size_t n)
{
uint8_t *p = buf;
while (n) {
ssize_t r = read(fd, p, n);
if (r == 0)
return -1; /* client went away */
if (r < 0) {
if (errno == EINTR)
continue;
return -1;
}
p += r;
n -= (size_t)r;
}
return 0;
}
static void
send_error(int fd, uint32_t serial, uint32_t code)
{
struct schcom_error e;
memset(&e, 0, sizeof e);
e.hdr.op = SCHCOM_OP_ERROR;
e.hdr.length = sizeof e;
e.hdr.serial = serial;
e.code = code;
send_all(fd, &e, sizeof e);
}
static void
drop_client(int ci)
{
if (clients[ci].fd < 0)
return;
for (int i = 0; i < MAX_SURFACES; i++) {
struct surface *s = &surfaces[i];
if (s->used && s->client == ci) {
damage(s->x, s->y, (int)s->w, (int)s->h);
if (s->pixels)
shmdt(s->pixels);
/* IPC_RMID once, here: the segment goes when the last attach does,
* so a client that died mid-frame does not leak it. */
shmctl(s->shmid, IPC_RMID, NULL);
memset(s, 0, sizeof *s);
}
}
close(clients[ci].fd);
clients[ci].fd = -1;
clients[ci].hello_done = 0;
printf("schcom: client %d gone\n", ci);
fflush(stdout);
}
static void
handle_create_surface(int ci, const struct schcom_create_surface *req)
{
int fd = clients[ci].fd;
int slot = -1;
for (int i = 0; i < MAX_SURFACES; i++)
if (!surfaces[i].used) { slot = i; break; }
if (slot < 0) {
send_error(fd, req->hdr.serial, ENOSPC);
return;
}
uint32_t w = req->width, h = req->height;
if (w == 0 || h == 0 || w > fb_w || h > fb_h) {
send_error(fd, req->hdr.serial, EINVAL);
return;
}
uint32_t stride = w * 4;
uint32_t size = stride * h;
/* The compositor owns the segment, not the client: it has to survive the
* client crashing mid-frame, and it must not be something a client can
* quietly resize underneath us. */
int shmid = shmget(IPC_PRIVATE, size, IPC_CREAT | 0600);
if (shmid < 0) {
send_error(fd, req->hdr.serial, errno);
return;
}
void *pix = shmat(shmid, NULL, 0);
if (pix == (void *)-1) {
shmctl(shmid, IPC_RMID, NULL);
send_error(fd, req->hdr.serial, errno);
return;
}
memset(pix, 0, size);
struct surface *s = &surfaces[slot];
s->used = 1;
s->id = next_surface_id++;
s->client = ci;
s->shmid = shmid;
s->pixels = pix;
s->w = w;
s->h = h;
s->stride = stride;
s->size = size;
s->x = req->x;
s->y = req->y;
struct schcom_surface rep;
memset(&rep, 0, sizeof rep);
rep.hdr.op = SCHCOM_OP_SURFACE;
rep.hdr.length = sizeof rep;
rep.hdr.serial = req->hdr.serial;
rep.hdr.surface = s->id;
rep.handle_kind = SCHCOM_HANDLE_SHMID;
rep.handle = (uint32_t)shmid;
rep.width = w;
rep.height = h;
rep.stride = stride;
rep.size = size;
if (send_all(fd, &rep, sizeof rep) < 0) {
drop_client(ci);
return;
}
damage(s->x, s->y, (int)w, (int)h);
printf("schcom: surface %u %ux%u at %d,%d for client %d (shm %d)\n",
s->id, w, h, s->x, s->y, ci, shmid);
fflush(stdout);
}
static struct surface *
find_surface(uint32_t id)
{
for (int i = 0; i < MAX_SURFACES; i++)
if (surfaces[i].used && surfaces[i].id == id)
return &surfaces[i];
return NULL;
}
/* Returns 1 if the screen needs repainting. */
static int
handle_message(int ci)
{
int fd = clients[ci].fd;
struct schcom_header hdr;
if (read_all(fd, &hdr, sizeof hdr) < 0) {
drop_client(ci);
return 0;
}
if (hdr.length < sizeof hdr || hdr.length > 4096) {
send_error(fd, hdr.serial, EPROTO);
drop_client(ci);
return 0;
}
uint8_t body[4096];
memcpy(body, &hdr, sizeof hdr);
size_t rest = hdr.length - sizeof hdr;
if (rest && read_all(fd, body + sizeof hdr, rest) < 0) {
drop_client(ci);
return 0;
}
if (!clients[ci].hello_done && hdr.op != SCHCOM_OP_HELLO) {
/* Nothing before the handshake. A client that gets this wrong is a
* client built against a protocol we do not speak. */
send_error(fd, hdr.serial, EPROTO);
drop_client(ci);
return 0;
}
switch (hdr.op) {
case SCHCOM_OP_HELLO: {
const struct schcom_hello *h = (const void *)body;
if (hdr.length < sizeof *h || h->version != SCHCOM_VERSION) {
send_error(fd, hdr.serial, EPROTONOSUPPORT);
drop_client(ci);
return 0;
}
struct schcom_hello ack;
memset(&ack, 0, sizeof ack);
ack.hdr.op = SCHCOM_OP_HELLO_ACK;
ack.hdr.length = sizeof ack;
ack.hdr.serial = hdr.serial;
ack.version = SCHCOM_VERSION;
if (send_all(fd, &ack, sizeof ack) < 0) {
drop_client(ci);
return 0;
}
clients[ci].hello_done = 1;
printf("schcom: client %d speaks v%u\n", ci, SCHCOM_VERSION);
fflush(stdout);
return 0;
}
case SCHCOM_OP_CREATE_SURFACE:
if (hdr.length < sizeof(struct schcom_create_surface)) {
send_error(fd, hdr.serial, EINVAL);
return 0;
}
handle_create_surface(ci, (const void *)body);
return 1;
case SCHCOM_OP_COMMIT: {
struct surface *s = find_surface(hdr.surface);
if (!s || s->client != ci) {
send_error(fd, hdr.serial, ENOENT);
return 0;
}
/* The commit is the whole point: the pixels were already there, but
* they were not allowed to be shown until now. The damage rectangle in
* the message is advisory -- repaint the whole surface, which is never
* wrong, only sometimes more than asked. */
damage(s->x, s->y, (int)s->w, (int)s->h);
struct schcom_header fr;
memset(&fr, 0, sizeof fr);
fr.op = SCHCOM_OP_FRAME;
fr.length = sizeof fr;
fr.surface = s->id;
if (send_all(fd, &fr, sizeof fr) < 0) {
drop_client(ci);
return 1;
}
return 1;
}
case SCHCOM_OP_DESTROY: {
struct surface *s = find_surface(hdr.surface);
if (s && s->client == ci) {
damage(s->x, s->y, (int)s->w, (int)s->h);
if (s->pixels)
shmdt(s->pixels);
shmctl(s->shmid, IPC_RMID, NULL);
memset(s, 0, sizeof *s);
return 1;
}
return 0;
}
default:
/* Unknown but well-formed: skipped, not fatal. That is what the length
* field is for, and it is how a v0 compositor survives a v1 client. */
return 0;
}
}
/* ----------------------------------------------------------------- input */
/* 1 if the screen needs repainting, 0 if the event changed nothing, -1 to
* quit, -2 when the queue is empty. The last is separate from 0 on purpose:
* every burst of movement ends in an EV_SYN that changes nothing, and a
* drain loop that stopped there would leave the rest of the burst queued. */
static int
handle_input(int fd)
{
struct input_event ev;
if (read(fd, &ev, sizeof ev) != (ssize_t)sizeof ev)
return -2;
int repaint = 0;
/* An absolute pointer says where it is; a relative one says how far it
* moved. Taking the absolute value as authoritative is the whole point of
* usb-tablet: two pointers that accumulate deltas independently drift
* apart, which inside a VM shows up as the guest cursor sitting somewhere
* the host's is not. */
if (ev.type == EV_ABS) {
int old_x = ptr_x, old_y = ptr_y;
if (ev.code == ABS_X) { ptr_x = ev.value; repaint = 1; }
if (ev.code == ABS_Y) { ptr_y = ev.value; repaint = 1; }
if (ptr_x < 0) ptr_x = 0;
if (ptr_y < 0) ptr_y = 0;
if (ptr_x >= (int32_t)fb_w) ptr_x = (int32_t)fb_w - 1;
if (ptr_y >= (int32_t)fb_h) ptr_y = (int32_t)fb_h - 1;
if (repaint) {
damage(old_x, old_y, PTR_SIZE, PTR_SIZE);
damage(ptr_x, ptr_y, PTR_SIZE, PTR_SIZE);
}
}
if (ev.type == EV_REL) {
int old_x = ptr_x, old_y = ptr_y;
if (ev.code == REL_X) { ptr_x += ev.value; repaint = 1; }
if (ev.code == REL_Y) { ptr_y += ev.value; repaint = 1; }
if (ptr_x < 0) ptr_x = 0;
if (ptr_y < 0) ptr_y = 0;
if (ptr_x >= (int32_t)fb_w) ptr_x = (int32_t)fb_w - 1;
if (ptr_y >= (int32_t)fb_h) ptr_y = (int32_t)fb_h - 1;
if (repaint) {
/* Where it was and where it is. Everything between is untouched. */
damage(old_x, old_y, PTR_SIZE, PTR_SIZE);
damage(ptr_x, ptr_y, PTR_SIZE, PTR_SIZE);
}
}
if (ev.type == EV_KEY && ev.value == 1 && ev.code == KEY_ESC)
return -1;
/* Route to the topmost surface. "Topmost" is the whole focus model in v0;
* anything cleverer is policy and belongs to SchWM. */
struct surface *top = NULL;
for (int i = 0; i < MAX_SURFACES; i++)
if (surfaces[i].used)
top = &surfaces[i];
if (top && (ev.type == EV_KEY || ev.type == EV_REL ||
ev.type == EV_ABS)) {
struct schcom_input msg;
memset(&msg, 0, sizeof msg);
msg.hdr.op = SCHCOM_OP_INPUT;
msg.hdr.length = sizeof msg;
msg.hdr.surface = top->id;
msg.type = ev.type;
msg.code = ev.code;
msg.value = ev.value;
msg.x = ptr_x;
msg.y = ptr_y;
if (clients[top->client].fd >= 0)
send_all(clients[top->client].fd, &msg, sizeof msg);
}
return repaint;
}
/* ------------------------------------------------------------------ main */
int
main(int argc, char **argv)
{
int frames_then_exit = 0; /* --frames N: for automated testing */
for (int i = 1; i < argc; i++)
if (!strcmp(argv[i], "--frames") && i + 1 < argc)
frames_then_exit = atoi(argv[++i]);
for (int i = 0; i < MAX_CLIENTS; i++)
clients[i].fd = -1;
open_framebuffer();
ptr_x = (int32_t)fb_w / 2;
ptr_y = (int32_t)fb_h / 2;
unlink(SCHCOM_SOCKET_PATH);
int lfd = socket(AF_UNIX, SOCK_STREAM, 0);
if (lfd < 0)
die("socket");
struct sockaddr_un addr;
memset(&addr, 0, sizeof addr);
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, SCHCOM_SOCKET_PATH, sizeof addr.sun_path - 1);
if (bind(lfd, (struct sockaddr *)&addr, sizeof addr) < 0)
die("bind " SCHCOM_SOCKET_PATH);
if (listen(lfd, 8) < 0)
die("listen");
int kbd = open("/dev/input/event0", O_RDONLY | O_NONBLOCK);
int mouse = open("/dev/input/event1", O_RDONLY | O_NONBLOCK);
printf("schcom: listening on %s\n", SCHCOM_SOCKET_PATH);
fflush(stdout);
damage_all();
composite();
int frames = 1;
for (;;) {
struct pollfd pfd[MAX_CLIENTS + 3];
int n = 0;
pfd[n].fd = lfd; pfd[n].events = POLLIN; n++;
int kbd_slot = -1, mouse_slot = -1;
if (kbd >= 0) { kbd_slot = n; pfd[n].fd = kbd; pfd[n].events = POLLIN; n++; }
if (mouse >= 0) { mouse_slot = n; pfd[n].fd = mouse; pfd[n].events = POLLIN; n++; }
int first_client = n;
for (int i = 0; i < MAX_CLIENTS; i++)
if (clients[i].fd >= 0) {
pfd[n].fd = clients[i].fd;
pfd[n].events = POLLIN;
n++;
}
if (poll(pfd, n, 100) < 0) {
if (errno == EINTR)
continue;
die("poll");
}
int repaint = 0;
if (pfd[0].revents & POLLIN) {
int cfd = accept(lfd, NULL, NULL);
if (cfd >= 0) {
int slot = -1;
for (int i = 0; i < MAX_CLIENTS; i++)
if (clients[i].fd < 0) { slot = i; break; }
if (slot < 0) {
close(cfd);
} else {
clients[slot].fd = cfd;
clients[slot].hello_done = 0;
printf("schcom: client %d connected\n", slot);
fflush(stdout);
}
}
}
/* Drain both devices before painting anything. A single mouse
* movement produces a burst of EV_REL followed by EV_SYN, and
* repainting per event meant the compositor rendered every
* intermediate position of a cursor nobody saw at those positions --
* which is most of what "the mouse lags" was. */
int quit = 0;
if (kbd_slot >= 0 && (pfd[kbd_slot].revents & POLLIN)) {
for (int n = 0; n < 256; n++) {
int r = handle_input(kbd);
if (r == -2) break;
if (r < 0) { quit = 1; break; }
if (r > 0) repaint = 1;
}
}
if (!quit && mouse_slot >= 0 && (pfd[mouse_slot].revents & POLLIN)) {
for (int n = 0; n < 256; n++) {
int r = handle_input(mouse);
if (r == -2) break;
if (r < 0) { quit = 1; break; }
if (r > 0) repaint = 1;
}
}
if (quit) break;
/* Walk the client slots in the same order they were added to pfd. */
int k = first_client;
for (int i = 0; i < MAX_CLIENTS; i++) {
if (clients[i].fd < 0)
continue;
if (pfd[k].revents & (POLLIN | POLLHUP))
repaint |= handle_message(i);
k++;
}
if (repaint) {
composite();
frames++;
if (frames_then_exit && frames >= frames_then_exit) {
printf("schcom: %d frames, exiting as asked\n", frames);
fflush(stdout);
break;
}
}
}
unlink(SCHCOM_SOCKET_PATH);
return 0;
}