diff --git a/Makefile b/Makefile index e8c8ecb..044a872 100644 --- a/Makefile +++ b/Makefile @@ -449,6 +449,31 @@ cmd: $(CMDBINS) ports: $(MAKE) -C ports +# projects/ -- things that run *on* SchOS rather than things that *are* SchOS. +# +# Deliberately not a dependency of anything above: `make` never descends here, +# and the kernel build does not know these exist. They are ordinary programs +# built against the sysroot's libc, exactly as toybox is, and the boundary +# between them and the OS is syscalls and protocols rather than a shared header +# nobody meant to share. See projects/README.md. +# +# Staged into $(SYSROOT) so the next image build picks them up; run +# `make projects && make` to get them onto a disk. +PROJECTS ?= schcom + +projects: + @test -d $(MUSLPREFIX)/include || { \ + echo "projects: no musl in $(MUSLPREFIX) -- run 'make ports' first" >&2; \ + exit 1; } + @for p in $(PROJECTS); do \ + echo "==> projects/$$p"; \ + $(MAKE) -C projects/$$p \ + CC='$(PORTCC)' \ + CFLAGS='-O2 -static -D__SCHOS__ -I$(MUSLPREFIX)/include -fno-stack-protector' \ + LDFLAGS='-static -L$(MUSLPREFIX)/lib' \ + DESTDIR='$(abspath $(SYSROOT))' install || exit 1; \ + done + # ----------------------------------------------------------------------- # Bootable ISO # @@ -885,6 +910,15 @@ $(BUILD)/fs.img: $(MKFS) $(BUILD)/sysroot.stamp # it gets worse -- because stage2 loads the kernel by PIO one sector at a time, # and under KVM every one of those port accesses is a VM exit, where under TCG # it is cheap. +# Schcom, end to end, checked from outside the machine. +# +# Boots, starts the compositor and a client, and then screendumps the +# framebuffer and asserts the pixels on the host. That last part is the whole +# value: the compositor saying "I composited it" proves nothing, and a +# screenshot nobody inspects proves less. Needs `make projects` first. +schcom-test: schos.img $(BUILD)/schfs.img + sh tools/posix/schcom_test.sh + qemu-kvm: schos.img $(BUILD)/schfs.img $(QEMU) -enable-kvm -serial mon:stdio $(QEMU_DISPLAY) -cpu host \ -hda schos.img -hdb $(BUILD)/schfs.img \ @@ -989,7 +1023,7 @@ qrtest: $(QRTEST) qrcheck: $(QRTEST) $(PYTHON) tools/python/qrcheck.py -.PHONY: all kernel ramroot cmd ports sysroot-install qemu qemu-kvm \ +.PHONY: all kernel ramroot cmd ports projects sysroot-install qemu qemu-kvm schcom-test \ schfs-test schfs-objtest schfs-image-test schfs-check mkschfs \ schfs-fsck crashshot qemu-schfs iso qemu-iso clean distclean \ logo qrtest qrcheck crashqr usb-ids diff --git a/projects/schcom/Makefile b/projects/schcom/Makefile new file mode 100644 index 0000000..9b89a82 --- /dev/null +++ b/projects/schcom/Makefile @@ -0,0 +1,32 @@ +# SPDX-License-Identifier: Apache-2.0 +# +# 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 diff --git a/projects/schcom/README.md b/projects/schcom/README.md index 76087d5..5f03b4d 100644 --- a/projects/schcom/README.md +++ b/projects/schcom/README.md @@ -12,31 +12,75 @@ and Schcom exposes a protocol for it rather than implementing policy itself. ## Status -Not started. Scoped only. +**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. -## What it needs from the kernel first +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 + +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**, with `SCM_RIGHTS` | **missing** — `socket()` is a `NO_LINUX` stub | -| **Raw input events** — key down/up with modifiers | **missing** — input only arrives cooked, through the tty line discipline | -| Mode setting and double buffering | **missing** — needs the virtio-gpu driver | +| Unix domain sockets | done | +| Raw input events — key down/up with modifiers | done | +| `SCM_RIGHTS` | done, and **not yet used** — see below | +| Mode setting and double buffering | partial — virtio-gpu binds, no modeset API yet | -The two marked missing are hard blockers, not inconveniences. A client and -the compositor have to pass a buffer handle between them, and a pipe cannot -carry one. And a compositor that reads the tty gets line-buffered text -delivered to a single foreground process, which is not input in any sense it -can use. +## Design notes -## Design notes, so far - -- **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. shm exists for this. +- **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 the protocol is a real compatibility surface from day one, not - after the fact. + 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. + split has already failed. `CREATE_SURFACE` carries x/y today only because + there is no SchWM yet to ask. + +## On Wayland + +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 Apache-2.0 (see `docs/architecture/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`. The kernel supports `SCM_RIGHTS` already; an integer simply needs +no ancillary-data handling on either side. `struct schcom_surface` carries a +`handle_kind` field precisely so that switching is a new value rather than a +protocol break. diff --git a/projects/schcom/proto.h b/projects/schcom/proto.h new file mode 100644 index 0000000..f8925ee --- /dev/null +++ b/projects/schcom/proto.h @@ -0,0 +1,128 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +/* + * 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 + +#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 */ diff --git a/projects/schcom/schcom b/projects/schcom/schcom new file mode 100644 index 0000000..ae9f2c5 Binary files /dev/null and b/projects/schcom/schcom differ diff --git a/projects/schcom/schcom-demo b/projects/schcom/schcom-demo new file mode 100644 index 0000000..d6cdb49 Binary files /dev/null and b/projects/schcom/schcom-demo differ diff --git a/projects/schcom/schcom-demo.c b/projects/schcom/schcom-demo.c new file mode 100644 index 0000000..bde9dbb --- /dev/null +++ b/projects/schcom/schcom-demo.c @@ -0,0 +1,149 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +/* + * 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 +#include +#include +#include +#include +#include +#include +#include + +#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; +} diff --git a/projects/schcom/schcom.c b/projects/schcom/schcom.c new file mode 100644 index 0000000..38902d7 --- /dev/null +++ b/projects/schcom/schcom.c @@ -0,0 +1,605 @@ +/* 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 +#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, moved by EV_REL from the 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; +} + +static void +composite(void) +{ + /* Desktop. A flat colour rather than black, so that "nothing was drawn" + * and "the compositor is not running" cannot look the same. */ + for (uint32_t y = 0; y < fb_h; y++) { + uint32_t *row = (uint32_t *)(back + (size_t)y * fb_stride); + for (uint32_t x = 0; x < fb_w; 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; + + for (uint32_t sy = 0; sy < s->h; sy++) { + int32_t dy = s->y + (int32_t)sy; + if (dy < 0 || dy >= (int32_t)fb_h) + 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++) { + int32_t dx = s->x + (int32_t)sx; + if (dx < 0 || dx >= (int32_t)fb_w) + continue; + dst[dx] = src[sx]; + } + } + } + + /* A crude pointer, drawn last so it is never under a window. */ + for (int32_t py = 0; py < 10; py++) { + int32_t dy = ptr_y + py; + if (dy < 0 || dy >= (int32_t)fb_h) + continue; + uint32_t *dst = (uint32_t *)(back + (size_t)dy * fb_stride); + for (int32_t px = 0; px < 10 - py; px++) { + int32_t dx = ptr_x + px; + if (dx >= 0 && dx < (int32_t)fb_w) + dst[dx] = 0x00FFFFFF; + } + } + + /* One copy to the screen. Compositing straight into the framebuffer would + * show every intermediate state -- the flicker would be the whole desktop + * being repainted under the user's eyes. */ + memcpy(fb, back, fb_size); +} + +/* ---------------------------------------------------------------- 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) { + 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; + } + 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. */ + 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) { + 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 */ + +/* Returns 1 if the screen needs repainting, -1 to quit. */ +static int +handle_input(int fd) +{ + struct input_event ev; + if (read(fd, &ev, sizeof ev) != (ssize_t)sizeof ev) + return 0; + + int repaint = 0; + + if (ev.type == EV_REL) { + 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 (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)) { + 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); + + 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); + } + } + } + + if (kbd_slot >= 0 && (pfd[kbd_slot].revents & POLLIN)) { + int r = handle_input(kbd); + if (r < 0) break; + repaint |= r; + } + if (mouse_slot >= 0 && (pfd[mouse_slot].revents & POLLIN)) { + int r = handle_input(mouse); + if (r < 0) break; + repaint |= r; + } + + /* 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; +} diff --git a/tools/posix/schcom_test.sh b/tools/posix/schcom_test.sh new file mode 100644 index 0000000..4de3c29 --- /dev/null +++ b/tools/posix/schcom_test.sh @@ -0,0 +1,117 @@ +#!/bin/sh +# SPDX-License-Identifier: Apache-2.0 +# +# Run Schcom and a client inside SchOS, then check the actual pixels from the +# host with QEMU's screendump. The host has no connection to the compositor, so +# it is a real oracle: if the block is the right colour in the right place, the +# buffer really was shared, composited and presented. +# +# One display device on purpose (-vga std, no virtio-gpu): with two, screendump +# captures whichever QEMU considers active, which need not be the one /dev/fb0 +# maps. +set -e +BUILD=${BUILD:-build} +mkdir -p "$BUILD" + +LOG=$BUILD/schcom-test.log +PPM=${1:-$BUILD/schcom.ppm} +rm -f "$LOG" "$PPM" + +PORT=$(python3 - <<'EOF' +import socket +s=socket.socket(); s.bind(('127.0.0.1',0)); print(s.getsockname()[1]); s.close() +EOF +) + +( + sleep 26 + printf 'schcom --frames 8 &\n'; sleep 6 + printf 'schcom-demo --colour 0x00C04020 --at 100 80 --size 240 160 --wait 25\n'; sleep 14 + printf '\n' + sleep 40 +) | timeout -k 5 120 qemu-system-x86_64 \ + -machine pc-i440fx-9.2 -cpu qemu64,+rdtscp \ + -hda schos.img -hdb build/schfs.img -m 1G -smp 1 \ + -display none -vga std \ + -monitor tcp:127.0.0.1:$PORT,server,nowait \ + -chardev stdio,id=c0,logfile="$LOG" -serial chardev:c0 \ + >/dev/null 2>&1 & +QPID=$! + +# Wait until the client says it committed, then photograph. +for i in $(seq 1 60); do + grep -aq 'committed' "$LOG" 2>/dev/null && break + sleep 1 +done +sleep 2 + +python3 - "$PORT" "$PPM" <<'EOF' +import socket, sys, time +port, ppm = int(sys.argv[1]), sys.argv[2] +s = socket.create_connection(('127.0.0.1', port), timeout=10) +time.sleep(0.5) +s.sendall(b'screendump ' + ppm.encode() + b'\n') +time.sleep(2.0) +s.close() +EOF + +sleep 1 +kill -9 $QPID 2>/dev/null || true +wait 2>/dev/null || true + +echo "=== guest output ===" +sed 's/\x1b\[[0-9;=]*[A-Za-z]//g' "$LOG" | tr -d '\r' | grep -aE 'schcom|surface|committed|listening' | head -12 + +echo "=== pixel check (host oracle) ===" +python3 - "$PPM" <<'EOF' +import sys +path = sys.argv[1] +try: + data = open(path, 'rb').read() +except FileNotFoundError: + print("FAIL: no screendump produced"); sys.exit(1) + +# Binary PPM: P6, whitespace-separated width height maxval, then RGB triples. +def token(buf, i): + while buf[i:i+1].isspace(): i += 1 + j = i + while not buf[j:j+1].isspace(): j += 1 + return buf[i:j], j + +if data[:2] != b'P6': + print("FAIL: not a P6 PPM"); sys.exit(1) +i = 2 +w, i = token(data, i); h, i = token(data, i); mx, i = token(data, i) +w, h = int(w), int(h) +px = data[i+1:] +print(f" screendump {w}x{h}") + +def at(x, y): + o = (y*w + x)*3 + return px[o], px[o+1], px[o+2] + +def near(got, want, tol=8): + return all(abs(a-b) <= tol for a, b in zip(got, want)) + +fails = 0 +inside = at(150, 120) # middle of the 240x160 block at 100,80 +if near(inside, (0xC0, 0x40, 0x20)): + print(f" [ok] surface pixel at 150,120 is {inside} -- the client's colour") +else: + print(f" FAIL: 150,120 is {inside}, expected (192, 64, 32)"); fails += 1 + +desktop = at(20, 20) # compositor background 0x203040 +if near(desktop, (0x20, 0x30, 0x40)): + print(f" [ok] desktop pixel at 20,20 is {desktop} -- compositor background") +else: + print(f" FAIL: 20,20 is {desktop}, expected (32, 48, 64)"); fails += 1 + +border = at(100, 80) # top-left of the surface: white border +if near(border, (0xFF, 0xFF, 0xFF)): + print(f" [ok] border pixel at 100,80 is {border} -- surface landed exactly") +else: + print(f" FAIL: 100,80 is {border}, expected white border"); fails += 1 + +print("schcom end-to-end: PASSED" if not fails else f"schcom end-to-end: {fails} FAILED") +sys.exit(1 if fails else 0) +EOF