Schcom is gone; the desktop path is Xorg now, Wayland groundwork next

SchOS will not write its own compositor. projects/schcom, its test and its make targets are deleted. roadmap.md section 5 is
now the road to Wayland (udev/eudev, a /sys/devices tree and uevent netlink, libinput and libseat, a DRM device via the GPU
plan) and 5b is the boot manager rework (Niveus/skeuomorphic UEFI GUI). XLibre, Xorg or a Wayland compositor from Alpine,
whichever suits; Xorg for now.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EVRhxi9zpdW2uDUN4FVcao
This commit is contained in:
Nexoniarz 2026-10-26 09:35:54 +01:00
commit ed81f9a07a
21 changed files with 62 additions and 1356 deletions

View file

@ -7,8 +7,7 @@ Everything above this directory — `kernel/`, `boot/`, `lib/`, `cmd/`,
is one program that either works or does not. Everything in here is an
ordinary program that talks to it through syscalls, exactly as `toybox` does.
The split exists because the alternative gets bad quickly. A compositor,
a window manager and a desktop are each larger than the kernel will be for a
The split exists because the alternative gets bad quickly. A desktop environment is larger than the kernel will be for a
long time. Grown inside the OS tree they would blur into it: a compositor
would start reaching into kernel headers because they are right there, the
build would grow a dependency from the kernel to a GUI, and "does SchOS
@ -27,14 +26,12 @@ Concretely, code in here:
| Directory | What it is |
|------------|-----------------------------------------------------------------|
| `schcom/` | SchOS Compositor. v0 runs; the plan is for it to draw the installer, and later a desktop (SchDE) on top. |
| `schinst/` | The installer, the installation environment, and `.sip` tooling. |
| `schfs-linux/` | SchFS as a filesystem for Linux. FUSE first, a kernel module later. |
`schwm/` and `schde/` used to be here and were **deleted**: both held a README and no code. The window manager
is Schcom's own business now (a compositor is its window manager), and **SchDE**, a panel, launcher and the rest,
comes back as ordinary clients of Schcom once there is something for them to run on. Until then the desktop is
Xorg with Window Maker ([`docs/architecture/desktop.md`](../docs/architecture/desktop.md)).
`schcom/`, `schwm/` and `schde/` used to be here and were **deleted**: SchOS does not write its own compositor, window
manager or desktop. The desktop is Xorg with Window Maker ([`docs/architecture/desktop.md`](../docs/architecture/desktop.md)),
and the road to Wayland is Alpine's compositors on top of udev and a DRM device ([`docs/plan/roadmap.md`](../docs/plan/roadmap.md) §5).
**`schfs-linux/` is written to stand on its own.** It does not mention SchOS
anywhere, and that is deliberate rather than an oversight: it is intended to be
@ -51,11 +48,5 @@ by anything above it.
## Status
`schcom/` is a compositor v0, 715 lines that run, over a wire format of its own. It is **kept, with a job**: the
installer's GUI ([`docs/plan/roadmap.md`](../docs/plan/roadmap.md) §5) is its first real client, and if it grows
into something actually useful it becomes SchOS's own desktop, SchDE on top. Everything it was blocked on in the kernel
exists (Unix sockets, descriptor passing, `memfd`, evdev, framebuffer `mmap`); what it lacks is a protocol designed for
real clients and the clients themselves.
`schinst/` and `schfs-linux/` are scoped and unstarted, and are **frozen** -- see
[`docs/plan/1.0.md`](../docs/plan/1.0.md).

View file

@ -1,32 +0,0 @@
# SPDX-License-Identifier: GPL-2.0-or-later
#
# Schcom builds like any other program that runs *on* SchOS: against the
# sysroot's libc, never against kernel headers. The top-level Makefile passes
# CC, CFLAGS and DESTDIR in; nothing here reaches back up the tree.
#
# It is not part of the OS build. `make` at the top level never descends here --
# `make projects` does.
CC ?= cc
CFLAGS ?= -O2
CFLAGS += -Wall -Wextra -std=c11
DESTDIR ?= ../../sysroot
BINS = schcom schcom-demo
all: $(BINS)
schcom: schcom.c proto.h
$(CC) $(CFLAGS) -o $@ schcom.c $(LDFLAGS)
schcom-demo: schcom-demo.c proto.h
$(CC) $(CFLAGS) -o $@ schcom-demo.c $(LDFLAGS)
install: $(BINS)
mkdir -p $(DESTDIR)/bin
cp $(BINS) $(DESTDIR)/bin/
clean:
rm -f $(BINS)
.PHONY: all install clean

View file

@ -1,110 +0,0 @@
# Schcom — the SchOS Compositor
Owns the display. Every pixel that reaches the screen goes through it.
Clients do not draw to the framebuffer; they draw into their own buffers and
hand them over. Schcom decides what is on top of what, composites the result,
and presents it. It also owns input: it reads the raw event devices and sends
each event to whichever client should have it.
That is the whole job. Window *placement* is not part of it — that is SchWM's,
and Schcom exposes a protocol for it rather than implementing policy itself.
## Status
**v0 runs.** A client connects, is given a shared buffer, draws into it and
commits; the result is composited and presented. Verified end to end by
`make schcom-test`, which boots SchOS, runs the compositor and a client, then
screendumps the framebuffer and asserts the pixels **from the host** — the
compositor's own report that it composited something proves nothing. It runs
two overlapping clients and checks that the newer one wins in the overlap: a
compositor that cannot order two surfaces cannot host a window manager.
What exists:
- framebuffer output through `/dev/fb0` and the fbdev ioctls, double-buffered
- a versioned protocol over a Unix socket, with a handshake that must come first
- shared-memory buffers, one per surface, owned by the compositor
- surface / attach / **commit** semantics: nothing is shown mid-draw
- input from `/dev/input/eventN`, routed to the topmost surface
- clients that die are cleaned up, including their shm segments
- **multiple clients**, composited bottom-to-top in creation order
What does not exist yet: damage tracking beyond "repaint everything", multiple
outputs, subsurfaces, and any focus model beyond "topmost". Those are additions
rather than rewrites, which is the reason for committing to the surface/commit
model now instead of growing into it later.
## Building
Not part of the OS build. Needs `make ports` first, for the sysroot's libc:
```sh
make projects # builds and stages into sysroot/bin
make # rebuilds the image so the binaries are on it
make schcom-test # boots it and checks the pixels
```
## What it needs from the kernel
| Need | State |
|---|---|
| Shared memory (`shmget`/`shmat`/`shmdt`) | done |
| `mmap` of `/dev/fb0` | done |
| Framebuffer geometry via the fbdev ioctls | done |
| Unix domain sockets | done |
| Raw input events — key down/up with modifiers | done |
| `SCM_RIGHTS` | done — `sendmsg`/`recvmsg`, see [`docs/architecture/networking.md`](../../docs/architecture/networking.md#descriptor-passing) |
| A shareable memory object | **missing.** A Wayland buffer is a descriptor for memory; a System V segment id is not one. Item 2 of [`docs/plan/foundations.md`](../../docs/plan/foundations.md), and the one thing now standing between this directory and a Wayland compositor. |
| Mode setting and double buffering | partial — virtio-gpu binds, no modeset API yet |
> **The v0 protocol below is superseded and the code is frozen.** It runs, and
> it is kept because it is the only thing in the tree that has ever composited
> anything — but nothing more will be added to it. SchWM and SchDE are referred
> to throughout and both have been deleted; read those mentions as "the policy
> module" and "a layer-shell client". See
> [`docs/plan/wayland.md`](../../docs/plan/wayland.md).
## Design notes
- **Buffers are shared memory, not pipes.** A pipe means copying every frame; at
800×600×32 that is 1.9 MB per frame per client.
- **The compositor owns the segment, not the client.** It has to survive a
client crashing mid-frame, and it must not be something a client can resize
underneath the compositor.
- **One protocol, versioned from the start.** SchWM and SchDE are separate
programs, so this is a real compatibility surface on day one. Every message
carries its own length, so an unknown message is skipped rather than
desynchronising the stream — that is what lets a v0 compositor survive a v1
client.
- **Policy belongs to SchWM.** If Schcom starts deciding where windows go, the
split has already failed. `CREATE_SURFACE` carries x/y today only because
there is no SchWM yet to ask.
## On Wayland
> **Decided: Schcom moves onto Wayland**, replacing the protocol below while
> keeping the architecture. See [`docs/plan/wayland.md`](../../docs/plan/wayland.md).
The protocol is deliberately Wayland-shaped — surfaces rather than windows,
buffers attached and made visible only on commit, and the compositor telling
clients when to draw. Wayland is **MIT-licensed**, so it can be linked against
without any conflict with GPL-2.0-or-later (see `docs/licensing.md`), and
supporting it later is intended.
The one place v0 knowingly diverges: a buffer handle is a System V shared memory
id passed as an integer, where Wayland passes a file descriptor over
`SCM_RIGHTS`. `struct schcom_surface` carries a `handle_kind` field precisely so
that switching would be a new value rather than a protocol break.
**That field could not have saved us.** Wayland's `wl_shm_create_pool` passes a
descriptor with no alternative form, so *our* protocol being the flexible one
was never the flexibility that was needed — and for a long time this README
claimed `SCM_RIGHTS` was done when there was no `sendmsg`/`recvmsg` in the
kernel at all. An entire GUI plan was drawn on top of that line.
It is true now. Item 1 of
[`docs/plan/foundations.md`](../../docs/plan/foundations.md) is built and
tested; what a buffer still needs is item 2, a shareable memory object, because
a descriptor for a System V segment is not a thing that exists.

View file

@ -1,128 +0,0 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
* The Schcom wire protocol.
*
* Versioned from the first commit, because SchWM and SchDE are separate
* programs: the protocol is a real compatibility surface on day one, not after
* the fact. A client sends SCHCOM_OP_HELLO with the version it speaks and the
* compositor either acknowledges it or closes the connection. Nothing else is
* accepted before the handshake.
*
* The shape is deliberately Wayland's, because Wayland support is intended
* later and it is MIT-licensed, so it can be linked (see the licensing note in
* the README). Borrowed, specifically:
*
* - a surface is not a window. It is a rectangle of pixels with no policy
* attached; placement belongs to SchWM.
* - a buffer is attached to a surface and nothing appears until COMMIT.
* Atomicity is the whole point: a client that is halfway through drawing
* must never be shown.
* - the compositor tells the client when to draw again (FRAME), rather than
* the client drawing whenever it likes. That is what stops a fast client
* from burning the machine rendering frames nobody sees.
*
* Where it deliberately differs, for now: a buffer is a System V shared memory
* id passed as an integer, not a file descriptor passed over SCM_RIGHTS.
* SCM_RIGHTS is the Wayland-compatible route and the kernel supports it, but an
* integer needs no ancillary-data handling on either side and this is v0. The
* message carries a `handle_kind` field so the switch does not become a
* protocol break.
*/
#ifndef SCHCOM_PROTO_H
#define SCHCOM_PROTO_H
#include <stdint.h>
#define SCHCOM_SOCKET_PATH "/tmp/schcom.sock"
/* Bumped on any incompatible change. v0 is provisional in the SchAPI sense:
* nothing here is frozen until 1.0. */
#define SCHCOM_VERSION 0u
enum schcom_op {
/* client -> compositor */
SCHCOM_OP_HELLO = 1,
SCHCOM_OP_CREATE_SURFACE = 2,
SCHCOM_OP_COMMIT = 3,
SCHCOM_OP_DESTROY = 4,
/* compositor -> client */
SCHCOM_OP_HELLO_ACK = 128,
SCHCOM_OP_SURFACE = 129, /* the buffer for a CREATE_SURFACE */
SCHCOM_OP_FRAME = 130, /* draw again now */
SCHCOM_OP_INPUT = 131,
SCHCOM_OP_ERROR = 132,
};
/* How the pixels are shared. Only SHMID exists today; the field exists so that
* adding SCM_RIGHTS later is a new value rather than a new protocol. */
enum schcom_handle_kind {
SCHCOM_HANDLE_SHMID = 1,
SCHCOM_HANDLE_FD = 2, /* reserved: passed over SCM_RIGHTS */
};
/* Every message begins with this. `length` counts the header itself, so a
* reader can skip a message it does not understand instead of losing sync --
* which is what lets an old compositor talk to a newer client at all. */
struct schcom_header {
uint32_t op;
uint32_t length;
uint32_t serial; /* echoed in the reply, 0 for events */
uint32_t surface; /* surface id, 0 where not applicable */
};
struct schcom_hello {
struct schcom_header hdr;
uint32_t version;
uint32_t pad;
};
struct schcom_create_surface {
struct schcom_header hdr;
uint32_t width;
uint32_t height;
int32_t x; /* v0: the client asks. SchWM will own this later. */
int32_t y;
};
/* The compositor's answer: here is your surface id and where its pixels live.
* Always 32-bit XRGB8888, stride in bytes, because a client that has to
* negotiate a format before it can draw a rectangle is a client that cannot be
* written in an afternoon. Format negotiation is a v1 problem. */
struct schcom_surface {
struct schcom_header hdr;
uint32_t handle_kind;
uint32_t handle; /* shm id when handle_kind == SHMID */
uint32_t width;
uint32_t height;
uint32_t stride;
uint32_t size;
};
/* Nothing is shown until this arrives. The damage rectangle is advisory: the
* compositor may repaint more, never less. */
struct schcom_commit {
struct schcom_header hdr;
int32_t x, y;
uint32_t width, height;
};
/* Raw-ish input, already routed to the surface that should have it. Codes are
* the Linux input event codes the kernel's /dev/input/eventN already speaks --
* translating them here would mean inventing a second table to disagree with
* the first. */
struct schcom_input {
struct schcom_header hdr;
uint32_t type; /* EV_KEY, EV_REL, ... */
uint32_t code;
int32_t value;
int32_t x, y; /* pointer position at the time of the event */
};
struct schcom_error {
struct schcom_header hdr;
uint32_t code;
uint32_t pad;
};
#endif /* SCHCOM_PROTO_H */

Binary file not shown.

Binary file not shown.

View file

@ -1,149 +0,0 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
* A Schcom client, and the test for the compositor.
*
* Draws a flat rectangle of a known colour at a known position and commits it.
* Both of those are chosen so the result can be checked from outside the
* machine: `make schcom-shot` screendumps the framebuffer and a script on the
* host asserts the pixel at a given coordinate. That is the same oracle
* pattern the SchFS round-trip uses -- something with no connection to the code
* under test has to agree that it worked.
*
* schcom-demo [--colour 0xRRGGBB] [--at X Y] [--size W H] [--wait SECONDS]
*/
#define _GNU_SOURCE
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/shm.h>
#include <sys/socket.h>
#include <sys/un.h>
#include "proto.h"
static int
read_all(int fd, void *buf, size_t n)
{
unsigned char *p = buf;
while (n) {
ssize_t r = read(fd, p, n);
if (r == 0) return -1;
if (r < 0) { if (errno == EINTR) continue; return -1; }
p += r; n -= (size_t)r;
}
return 0;
}
int
main(int argc, char **argv)
{
uint32_t colour = 0x00C04020;
int32_t at_x = 100, at_y = 80;
uint32_t w = 240, h = 160;
int wait_s = 3;
for (int i = 1; i < argc; i++) {
if (!strcmp(argv[i], "--colour") && i + 1 < argc)
colour = (uint32_t)strtoul(argv[++i], NULL, 0);
else if (!strcmp(argv[i], "--at") && i + 2 < argc) {
at_x = atoi(argv[++i]); at_y = atoi(argv[++i]);
} else if (!strcmp(argv[i], "--size") && i + 2 < argc) {
w = (uint32_t)atoi(argv[++i]); h = (uint32_t)atoi(argv[++i]);
} else if (!strcmp(argv[i], "--wait") && i + 1 < argc)
wait_s = atoi(argv[++i]);
}
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (fd < 0) { perror("socket"); return 1; }
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 (connect(fd, (struct sockaddr *)&addr, sizeof addr) < 0) {
fprintf(stderr, "schcom-demo: connect %s: %s\n",
SCHCOM_SOCKET_PATH, strerror(errno));
return 1;
}
/* Handshake first: the compositor accepts nothing else before it. */
struct schcom_hello hello;
memset(&hello, 0, sizeof hello);
hello.hdr.op = SCHCOM_OP_HELLO;
hello.hdr.length = sizeof hello;
hello.hdr.serial = 1;
hello.version = SCHCOM_VERSION;
if (write(fd, &hello, sizeof hello) != (ssize_t)sizeof hello) {
perror("write hello"); return 1;
}
struct schcom_hello ack;
if (read_all(fd, &ack, sizeof ack) < 0 || ack.hdr.op != SCHCOM_OP_HELLO_ACK) {
fprintf(stderr, "schcom-demo: no hello ack\n");
return 1;
}
printf("schcom-demo: connected, protocol v%u\n", ack.version);
struct schcom_create_surface req;
memset(&req, 0, sizeof req);
req.hdr.op = SCHCOM_OP_CREATE_SURFACE;
req.hdr.length = sizeof req;
req.hdr.serial = 2;
req.width = w; req.height = h;
req.x = at_x; req.y = at_y;
if (write(fd, &req, sizeof req) != (ssize_t)sizeof req) {
perror("write create"); return 1;
}
struct schcom_surface surf;
if (read_all(fd, &surf, sizeof surf) < 0) {
fprintf(stderr, "schcom-demo: no reply to create\n");
return 1;
}
if (surf.hdr.op != SCHCOM_OP_SURFACE) {
fprintf(stderr, "schcom-demo: create refused (op %u)\n", surf.hdr.op);
return 1;
}
if (surf.handle_kind != SCHCOM_HANDLE_SHMID) {
fprintf(stderr, "schcom-demo: unexpected handle kind %u\n", surf.handle_kind);
return 1;
}
void *pix = shmat((int)surf.handle, NULL, 0);
if (pix == (void *)-1) { perror("shmat"); return 1; }
printf("schcom-demo: surface %u, %ux%u stride %u\n",
surf.hdr.surface, surf.width, surf.height, surf.stride);
/* Draw: a solid block with a one-pixel border, so a test can tell "the
* buffer arrived" from "the buffer arrived at the wrong offset". */
for (uint32_t y = 0; y < surf.height; y++) {
uint32_t *row = (uint32_t *)((unsigned char *)pix + (size_t)y * surf.stride);
for (uint32_t x = 0; x < surf.width; x++) {
int edge = (x == 0 || y == 0 || x == surf.width - 1 || y == surf.height - 1);
row[x] = edge ? 0x00FFFFFF : colour;
}
}
struct schcom_commit commit;
memset(&commit, 0, sizeof commit);
commit.hdr.op = SCHCOM_OP_COMMIT;
commit.hdr.length = sizeof commit;
commit.hdr.serial = 3;
commit.hdr.surface = surf.hdr.surface;
commit.width = surf.width; commit.height = surf.height;
if (write(fd, &commit, sizeof commit) != (ssize_t)sizeof commit) {
perror("write commit"); return 1;
}
printf("schcom-demo: committed 0x%06X at %d,%d\n", colour, at_x, at_y);
fflush(stdout);
/* Stay alive: the surface belongs to this connection, and the compositor
* drops it when the client goes. Exiting immediately would erase the very
* thing the screenshot is meant to catch. */
sleep(wait_s);
return 0;
}

View file

@ -1,715 +0,0 @@
/* 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 <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;
}