/* SPDX-License-Identifier: GPL-2.0-or-later */ /* * 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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; }