diff --git a/.gitignore b/.gitignore index 8b12f57..683ea4e 100644 --- a/.gitignore +++ b/.gitignore @@ -36,8 +36,6 @@ dist/ # Build output under projects/ -- these are programs that run on SchOS, and # their binaries were tracked by accident. -projects/schcom/schcom -projects/schcom/schcom-demo # A built ISO dropped next to the site for download.html. 62 MB; not source. docs/Website/releases/ diff --git a/Makefile b/Makefile index 9504c30..70dc3e3 100644 --- a/Makefile +++ b/Makefile @@ -779,7 +779,7 @@ ports: # # Staged into $(SYSROOT) so the next image build picks them up; run # `make projects && make` to get them onto a disk. -PROJECTS ?= schcom +PROJECTS ?= projects: @test -d $(MUSLPREFIX)/include || { \ @@ -1624,15 +1624,6 @@ $(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: esp $(BUILD)/schfs.img - sh tools/posix/schcom_test.sh - # The firmware and the disks, in one place. Every run target below boots the # same way, because there is only one way to boot now: OVMF, the staged ESP as # the first disk, the SchFS volume as the second. Keeping the argument list in @@ -1829,7 +1820,7 @@ qrtest: $(QRTEST) qrcheck: $(QRTEST) $(PYTHON) tools/python/qrcheck.py -.PHONY: disk all kernel ramroot cmd ports projects sysroot-install qemu qemu-console qemu-kvm schcom-test \ +.PHONY: disk all kernel ramroot cmd ports projects sysroot-install qemu qemu-console qemu-kvm \ schfs-test schfs-objtest schfs-image-test schfs-check kmem-test mkschfs \ schfs-fsck crashshot qemu-schfs iso qemu-iso clean distclean \ logo qrtest qrcheck crashqr usb-ids pci-ids hwdata ovmf esp uefi diff --git a/README.md b/README.md index 14fc3d0..7150da7 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,6 @@ features. It boots from a USB stick on three different real machines. | Image format | **SCFF** | The SchOS Compressed File Format the live root ships in | | Packages | **apk** | Alpine's package manager manages the root; SchOS itself is the package `schos-base` | | Desktop | **Xorg + Window Maker** | Alpine's, on the framebuffer | -| Compositor | **Schcom** | SchOS's own compositor — v0 runs; the installer's GUI first, a desktop (**SchDE**) later | | Crash screen | **PSOGM / GMMOS** | The Purple Screen of Guru Meditation, and the micro OS that runs under it | | API | **SchAPI** | The long-term native interface — *designed, not built* | diff --git a/docs/Website/index.html b/docs/Website/index.html index 9897695..4d5e6f9 100644 --- a/docs/Website/index.html +++ b/docs/Website/index.html @@ -405,7 +405,6 @@ Xorg on the framebuffer with Window Maker, xterm and a few X clients; NetSurf for the web. No acceleration yet — the GPU is a plan, and the plan is Linux's own amdgpu through LDLS. - Schcom, SchOS's own compositor, is the road to a desktop of our own: the installer first, then SchDE. diff --git a/docs/architecture/README.md b/docs/architecture/README.md index a861df7..025fa1f 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -16,7 +16,6 @@ its distribution. | Kernel | **SchIFT** | Multi-core x86-64 kernel with a modular VFS | | Filesystem | **SchFS** | Transactional, copy-on-write, the root filesystem | | API | **SchAPI** | The native programming interface — *designed, not built* | -| Compositor | **Schcom** | SchOS's own compositor — v0 runs ([../plan/roadmap.md](../plan/roadmap.md) §5) | | Packages | **apk** | Alpine's package manager manages the root; SchOS itself is the package `schos-base` | Twelve virtual terminals sit under all of it -- `/dev/tty1`..`tty12`, Ctrl+Alt+F1..F12 diff --git a/docs/architecture/desktop.md b/docs/architecture/desktop.md index 8be437c..92b80c8 100644 --- a/docs/architecture/desktop.md +++ b/docs/architecture/desktop.md @@ -5,8 +5,9 @@ driver and the `evdev` input driver, Window Maker as the window manager, and `xt `startx` runs `wmaker`. Nothing here is written by SchOS except the kernel interfaces it stands on and four small files in `tools/posix/mkaroot.sh`. -**Xorg is the desktop for now, not for ever.** SchOS's own compositor, Schcom, is the road to a desktop of its own — the installer's -GUI first, SchDE on top later ([../plan/roadmap.md](../plan/roadmap.md) §5). +**Xorg is the desktop for now, not for ever.** SchOS will not write its own compositor (the one it had, Schcom, was deleted). +What comes next is whichever of XLibre (the Xorg fork), Xorg or a Wayland compositor from Alpine suits the machine; the groundwork +Wayland needs — udev, `/sys/devices`, libinput, a DRM device — is in [../plan/roadmap.md](../plan/roadmap.md) §5. ## Why X and not Wayland diff --git a/docs/architecture/graphics.md b/docs/architecture/graphics.md index 0526912..6676a1f 100644 --- a/docs/architecture/graphics.md +++ b/docs/architecture/graphics.md @@ -17,7 +17,7 @@ first thing here. | **About** | Pixels and text | *Frames* | | **Operations** | `draw_pixel`, `fill_rect`, `putc`, `text_at` | `acquire`, `present`, `wait_vblank` | | **Buffering** | None. Direct writes to the scanout. | Double-buffered | -| **Users** | The console, PSOGM, GMMOS | Schcom (v0 and its successors); Xorg draws through `/dev/fb0` instead | +| **Users** | The console, PSOGM, GMMOS | None today — Xorg draws through `/dev/fb0` | | **Must work when** | **Nothing else in the system does** | Userspace is healthy | That last row is the reason they are not one interface. The console and the diff --git a/docs/guides/testing.md b/docs/guides/testing.md index 9e4651b..dbd9579 100644 --- a/docs/guides/testing.md +++ b/docs/guides/testing.md @@ -14,7 +14,7 @@ reporting that it started. |---|---|---| | `make schfs-check`, `make kmem-test`, `ldls-inspect`/`ldls-run` | Host-native unit suites and the Linux-module loader run on the build machine | The filesystem's logic, the slab allocator's bookkeeping, and a `.ko`'s layout, in about a second | | `tools/windows/bootcheck.ps1` | A real boot, driven over the serial console | That the system answers correctly | -| `make crashqr`, `make schcom-test` | A boot, then pixel assertions from the host | That the *screen* is right | +| `make crashqr` | A boot, then pixel assertions from the host | That the *screen* is right | The levels exist because each catches what the one below cannot. A unit test cannot tell you the kernel fails to boot; a boot test cannot tell you the screen @@ -97,17 +97,11 @@ can; the rest are run by hand or by a script: ```sh make crashshot && make crashqr # crash the kernel, decode the QR out of the framebuffer -make schcom-test # boot, run the compositor and a client, check the pixels ``` `tools/windows/splashshot.ps1` counts colours. **That exists because a log-only harness once passed on a blank screen.** -`schcom-test` asserts **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, because a compositor that cannot order -two surfaces cannot host a window manager. - Other capture tools, all in `tools/windows/`: `dispshot`, `fbshot`, `splashshot`, `textshot`, `ttyshot`. diff --git a/docs/plan/1.0.md b/docs/plan/1.0.md index ed2eb0d..ee7ab5c 100644 --- a/docs/plan/1.0.md +++ b/docs/plan/1.0.md @@ -60,8 +60,7 @@ boot on real hardware from a USB stick The test used to end with *reboot — the file is still there*. That clause is **dropped**, deliberately: 1.0 is a **live** system. The root lives in RAM and nothing written to it survives a reboot, and that is what the project is calling finished. -Persistence (an installer, or a persistent partition) is after 1.0, and its GUI is Schcom's job — see -[roadmap.md](roadmap.md) §5. +Persistence (an installer, or a persistent partition) is after 1.0 — see [roadmap.md](roadmap.md) §5. --- @@ -90,7 +89,6 @@ directory that has said "to be deleted" for months is the corner, not the cure. | | Where it stopped | |---|---| -| **`projects/schcom/`** | A compositor v0 (about 700 lines) with no client but its own demo: the desktop is Xorg. Kept on purpose — it is to draw the installer, and later SchDE ([roadmap.md](roadmap.md) §5) — but until that starts it is built and connected to nothing. | | **The in-kernel LDLS window** | `vm.c` maps the kernel image a second time in the top 2 GB at every boot, and `include/ldls/ldls.h` describes a `/dev/ldls` loader — the start of loading Linux modules *into the kernel*, which [ldls.md](../architecture/ldls.md) rule 2 says is not how it is done. Nothing calls either. **Delete it**, or decide. | | **`display_ops`** | `acquire` and `wait_vblank` are in the vtable, implemented by one driver at most, with no consumer: Xorg draws through `/dev/fb0`. Kept for the GPU work ([gpu.md](gpu.md)); that is the reason it stays. | | **Socket activation** | `schinitd` parses `[Socket name]` and remembers the path. Nothing holds the socket. This is the entire reason for copying launchd, and it is the half that was not built. | @@ -129,7 +127,7 @@ Neither cancelled nor in progress. **Do not open these.** | | | |---|---| -| `projects/schinst/` | the installer, the installation environment, `.sip` — drawn by Schcom | +| `projects/schinst/` | the installer, the installation environment, `.sip` | | `projects/schfs-linux/` | SchFS as a filesystem for Linux | | [gpu.md](gpu.md) | amdgpu and nouveau through LDLS — measured, with the work it needs written down | | [finch.md](finch.md) | Finch inside SchOS (the language itself is finished and lives in its own repository) | diff --git a/docs/plan/README.md b/docs/plan/README.md index d474a7e..81178ba 100644 --- a/docs/plan/README.md +++ b/docs/plan/README.md @@ -40,8 +40,8 @@ Plans leave this directory when they are done. These did, and what replaced them And one that was **decided against**: `wayland.md`. Wayland needs DRM/KMS, libinput, udev and libseat, and DRM/KMS alone is a kernel graphics subsystem. The desktop is X. The reasoning moved to [desktop.md](../architecture/desktop.md). -`SchWM` and `SchDE` were removed from the tree as READMEs with no code: Schcom is its own window manager, and SchDE returns as -ordinary clients of Schcom when there is a Schcom to build them on. Meanwhile the window manager is Window Maker. +`SchWM`, `SchDE` and the compositor `Schcom` are gone from the tree: SchOS uses other people's window manager and desktop — Window Maker +now; XLibre, Xorg or a Wayland compositor later — and puts its work into what they need underneath (udev, DRM). --- @@ -50,11 +50,11 @@ ordinary clients of Schcom when there is a Schcom to build them on. Meanwhile th Set by [1.0.md](1.0.md), which is the authority. **1.0 is reached** (a live system; persistence is not part of it). What is left before anything new starts: -1. **Empty the register**: decide the unused in-kernel LDLS window (delete it, or use it), build socket activation - or remove the key, and give `projects/schcom/` its first client. -2. **Then**, whatever [roadmap.md](roadmap.md) says is next. The two things the owner wants most are the installer - drawn by Schcom — SchOS's own compositor, and the road to SchDE ([roadmap.md](roadmap.md) §5) — and GPU - acceleration on the laptop ([gpu.md](gpu.md)). Finch inside SchOS ([finch.md](finch.md)) comes after. +1. **Empty the register**: decide the unused in-kernel LDLS window (delete it, or use it), and build socket activation or + remove the key. +2. **The owner's list** ([roadmap.md](roadmap.md)): the Wayland groundwork (udev first, §5), making LDLS good enough that the GPU + driver can run ([gpu.md](gpu.md)), a boot manager worth looking at (§5b), then the installer, then Finch inside SchOS + ([finch.md](finch.md)). ## What this plan does not address diff --git a/docs/plan/roadmap.md b/docs/plan/roadmap.md index 05bc8e3..deb7a14 100644 --- a/docs/plan/roadmap.md +++ b/docs/plan/roadmap.md @@ -54,22 +54,44 @@ what is present ([boot.md](../architecture/boot.md)). - **udev's job** — a uevent socket and `/sys/devices` — so X and others are told about devices instead of being given a list. -## 5. Schcom, the installer, and SchDE +## 5. The desktop, and the road to Wayland -**Schcom** (`projects/schcom/`) is SchOS's own compositor. v0 is 715 lines that composite clients over a wire format of its -own, and everything it was blocked on in the kernel now exists: Unix sockets, descriptor passing, `memfd`, evdev, framebuffer -`mmap`. The plan is in three steps, each useful on its own: +SchOS does not write its own compositor. (Schcom, a 715-line v0, was deleted: its wire format was a dead end, and a +compositor worth running is years of work that Alpine already packages.) The desktop is **Xorg with Window Maker** now +([desktop.md](../architecture/desktop.md)); **XLibre**, the Xorg fork, is a drop-in alternative if Alpine carries it, and a +Wayland compositor (labwc, sway, weston, a GNOME or KDE session) is the larger step. What Wayland — and a modern X — need +from the system, in the order they are useful: -1. **The installer's GUI is Schcom's first real client.** An installer wants a window, a list, a progress bar and a handful of - input events — small enough to be the program that forces Schcom's protocol to be real, and independent of Xorg, which is not in - the installation environment. It needs a wire format for clients that is designed (Wayland-shaped: surfaces, buffers shared by - descriptor, input events), not the v0 one. -2. **If that works, Schcom becomes useful in its own right**: window management as a module of the compositor (a compositor *is* - its window manager), multiple clients with focus and stacking, damage tracking. -3. **SchDE on top of it**: a panel, a launcher, a wallpaper and settings, as ordinary clients — only once there is a Schcom worth - putting them on. Until then the desktop is Xorg with Window Maker ([desktop.md](../architecture/desktop.md)). +1. **udev.** Today Xorg is *told* its input devices (`xorg.conf.d/10-schos.conf`) because nothing announces them. The fix is + Alpine's `eudev` (or `udev`) running on SchOS, which wants: a **uevent netlink socket** (`NETLINK_KOBJECT_UEVENT`, with the + `add`/`remove`/`change` messages and their environment), a real **`/sys/devices` tree** with `/sys/bus/*/devices/*` and + `/sys/class/*/*` as symlinks into it, `uevent` files that can be written to replay events (`udevadm trigger`), and devtmpfs + that udev can create nodes in. libinput then enumerates evdev devices through it. +2. **libinput and libseat** (seatd is in Alpine): evdev with the properties libinput reads (`ID_INPUT_*` from udev), and a way + for a non-root compositor to take the keyboard and the display from the kernel — the `VT_PROCESS` handshake exists. +3. **A DRM device.** Wayland compositors want `/dev/dri/card0` with KMS, and Mesa wants `renderD128`. SchOS has neither: it has + `/dev/fb0`. This is the GPU plan ([gpu.md](gpu.md)) — Linux's own `amdgpu` run by LDLS and `/dev/dri` served by that + process — and, for machines without a real GPU driver, a *simple-framebuffer* DRM shim (Linux's `simpledrm`, which is a + `.ko` LDLS could also host). +4. **dma-buf, `epoll`-able sync files and the rest of the Linux graphics ABI**, as the compositors turn out to need them. -Nothing here starts before [1.0.md](1.0.md)'s register is empty. +None of this is a kernel rewrite: it is the Linux interfaces that already exist half-built (`/sys`, netlink, evdev, `SCM_RIGHTS`, +`memfd`), finished until real programs stop asking for what is missing. The method is the one that has worked all along: `apk +add` the program, run it, and fix what it names. + +Nothing here starts before [1.0.md](1.0.md)'s register is empty, except where the owner says otherwise. + +## 5b. The boot manager + +The loader ([boot.md](../architecture/boot.md)) is a text menu: a splash, "Press F8", a list of options. UEFI gives much more: +a graphics-output framebuffer at the panel's native mode, a pointer protocol, file systems to read themes from. The plan is a +boot manager that is worth looking at — in the **Niveus / skeuomorphic** style the site and the wallpapers already use (the +Shift mark, real materials, depth) — and usable: a mouse and keyboard, a list of what can be booted (this ISO, a SchFS +partition, other operating systems' loaders), the options that live in the F8 menu now (resolution with its "keep this?" +confirmation, verbose boot, drivers off), and an honest failure screen. It is its own piece of work in `boot/uefi/` +(a small text and image renderer in the loader — the kernel's TrueType rasteriser is not available yet at that point) and it +replaces the boot-time display handling that grew by accretion (GOP mode choice, the "keep this resolution" probe, the font scaling +in the kernel) with one design. ### The installer and `.sip` @@ -96,7 +118,7 @@ Open questions, all wanting a decision before code: now, or not? - **Can the kernel mount a `.sip` read-only directly?** If it can, the installation environment's root can simply *be* the `.sip`. -The GUI for this is a Schcom client (above). +Its GUI is the boot manager's (5b) — the installation environment starts in it — or an X client. ## 6. SchFS beyond SchOS diff --git a/docs/plan/schapi.md b/docs/plan/schapi.md index 1ec4a0f..b2f5329 100644 --- a/docs/plan/schapi.md +++ b/docs/plan/schapi.md @@ -3,7 +3,9 @@ Status: **direction agreed, not yet implemented**, and frozen until 1.0 ships ([1.0.md](1.0.md)). Read it as a direction and not as a schedule: written when the plan was a compositor of SchOS's own, it has not been revisited since the desktop became Xorg and the -packages became Alpine's, and §7 (what happens now) in particular predates both. Nothing in this document +packages became Alpine's, and §7 (what happens now) in particular predates both. It also assumed SchOS would write its +own compositor (Schcom) for the API to be designed *from*; that compositor was deleted, and SchOS's desktop +is other people's, so read every mention of Schcom below as "the desktop". Nothing in this document changes what is being built this week; it changes what everything is being built *towards*. See §7 for what actually happens now. diff --git a/projects/README.md b/projects/README.md index 9faf146..2163149 100644 --- a/projects/README.md +++ b/projects/README.md @@ -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). diff --git a/projects/schcom/Makefile b/projects/schcom/Makefile deleted file mode 100644 index 90cbd2e..0000000 --- a/projects/schcom/Makefile +++ /dev/null @@ -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 diff --git a/projects/schcom/README.md b/projects/schcom/README.md deleted file mode 100644 index 86fc93c..0000000 --- a/projects/schcom/README.md +++ /dev/null @@ -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. diff --git a/projects/schcom/proto.h b/projects/schcom/proto.h deleted file mode 100644 index f7ebf15..0000000 --- a/projects/schcom/proto.h +++ /dev/null @@ -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 - -#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 deleted file mode 100644 index 92d09d6..0000000 Binary files a/projects/schcom/schcom and /dev/null differ diff --git a/projects/schcom/schcom-demo b/projects/schcom/schcom-demo deleted file mode 100644 index d6cdb49..0000000 Binary files a/projects/schcom/schcom-demo and /dev/null differ diff --git a/projects/schcom/schcom-demo.c b/projects/schcom/schcom-demo.c deleted file mode 100644 index dc4bdeb..0000000 --- a/projects/schcom/schcom-demo.c +++ /dev/null @@ -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 -#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 deleted file mode 100644 index 7a45438..0000000 --- a/projects/schcom/schcom.c +++ /dev/null @@ -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 -#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; -} diff --git a/tools/posix/schcom_test.sh b/tools/posix/schcom_test.sh deleted file mode 100644 index 13986f8..0000000 --- a/tools/posix/schcom_test.sh +++ /dev/null @@ -1,154 +0,0 @@ -#!/bin/sh -# SPDX-License-Identifier: GPL-2.0-or-later -# -# 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 -# Run from anywhere: resolve the repo root from this script's own location -# rather than trusting the caller's working directory. Everything below is -# relative to it, and running from tools/posix/ used to fail with "missing -# build/kernel/kernel -- run make first" on a tree that was perfectly built. -script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) -repo=$(CDPATH= cd -- "$script_dir/../.." && pwd) -cd "$repo" - -BUILD=${BUILD:-build} -mkdir -p "$BUILD" - -LOG=$BUILD/schcom-test.log -PPM=${1:-$BUILD/schcom.ppm} -rm -f "$LOG" "$PPM" - -# The firmware varstore is written by OVMF, so this run gets its own copy -# rather than sharing the pristine one every other run starts from. -cp -f build/OVMF_VARS.fd build/schcom-OVMF_VARS.fd - -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 24 &\n'; sleep 6 - printf 'schcom-demo --colour 0x00C04020 --at 100 80 --size 240 160 --wait 40 &\n'; sleep 10 - printf 'schcom-demo --colour 0x002060C0 --at 260 180 --size 240 160 --wait 30 &\n'; sleep 12 - printf '\n' - sleep 40 -) | timeout -k 5 120 qemu-system-x86_64 \ - -machine pc-i440fx-9.2 -cpu qemu64,+rdtscp \ - -drive if=pflash,format=raw,unit=0,readonly=on,file=build/OVMF_CODE.fd \ - -drive if=pflash,format=raw,unit=1,file=build/schcom-OVMF_VARS.fd \ - -drive file=fat:rw:build/esp,index=0,media=disk,format=raw \ - -drive file=build/schfs.img,format=raw,index=1,media=disk \ - -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 *both* clients have committed, then photograph. Breaking on the -# first one screenshots a desktop the second client has not reached yet, which -# looks exactly like a compositor that cannot handle two clients. -for i in $(seq 1 90); do - [ "$(grep -ac 'committed' "$LOG" 2>/dev/null)" -ge 2 ] && 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 first surface -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 - -# Second client, created later, so it is above the first where they overlap. -second = at(460, 320) # inside surface 2, clear of the pointer -if near(second, (0x20, 0x60, 0xC0)): - print(f" [ok] second surface at 460,320 is {second} -- two clients composited") -else: - print(f" FAIL: 460,320 is {second}, expected (32, 96, 192)"); fails += 1 - -# 260,180..340,240 is where the two overlap. The later surface must win: if -# stacking were ignored the first client's colour would show through here, and -# a compositor that cannot order two windows cannot host a window manager. -overlap = at(300, 220) -if near(overlap, (0x20, 0x60, 0xC0)): - print(f" [ok] overlap at 300,220 is {overlap} -- newer surface on top") -elif near(overlap, (0xC0, 0x40, 0x20)): - print(f" FAIL: overlap shows the older surface -- stacking order ignored"); fails += 1 -else: - print(f" FAIL: overlap at 300,220 is {overlap}, expected the newer surface"); 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