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>
715 lines
22 KiB
C
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;
|
|
}
|