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

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

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

2
.gitignore vendored
View file

@ -36,8 +36,6 @@ dist/
# Build output under projects/ -- these are programs that run on SchOS, and # Build output under projects/ -- these are programs that run on SchOS, and
# their binaries were tracked by accident. # 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. # A built ISO dropped next to the site for download.html. 62 MB; not source.
docs/Website/releases/ docs/Website/releases/

View file

@ -779,7 +779,7 @@ ports:
# #
# Staged into $(SYSROOT) so the next image build picks them up; run # Staged into $(SYSROOT) so the next image build picks them up; run
# `make projects && make` to get them onto a disk. # `make projects && make` to get them onto a disk.
PROJECTS ?= schcom PROJECTS ?=
projects: projects:
@test -d $(MUSLPREFIX)/include || { \ @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, # 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 # and under KVM every one of those port accesses is a VM exit, where under TCG
# it is cheap. # 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 # 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 # 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 # the first disk, the SchFS volume as the second. Keeping the argument list in
@ -1829,7 +1820,7 @@ qrtest: $(QRTEST)
qrcheck: $(QRTEST) qrcheck: $(QRTEST)
$(PYTHON) tools/python/qrcheck.py $(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-test schfs-objtest schfs-image-test schfs-check kmem-test mkschfs \
schfs-fsck crashshot qemu-schfs iso qemu-iso clean distclean \ schfs-fsck crashshot qemu-schfs iso qemu-iso clean distclean \
logo qrtest qrcheck crashqr usb-ids pci-ids hwdata ovmf esp uefi logo qrtest qrcheck crashqr usb-ids pci-ids hwdata ovmf esp uefi

View file

@ -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 | | 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` | | 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 | | 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 | | 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* | | API | **SchAPI** | The long-term native interface — *designed, not built* |

View file

@ -405,7 +405,6 @@
Xorg on the framebuffer with Window Maker, <code class="nv-code">xterm</code> and a few X clients; Xorg on the framebuffer with Window Maker, <code class="nv-code">xterm</code> and a few X clients;
NetSurf for the web. No acceleration yet — the GPU is a plan, and the plan is Linux's own NetSurf for the web. No acceleration yet — the GPU is a plan, and the plan is Linux's own
<code class="nv-code">amdgpu</code> through LDLS. <code class="nv-code">amdgpu</code> through LDLS.
Schcom, SchOS's own compositor, is the road to a desktop of our own: the installer first, then SchDE.
</div> </div>
</article> </article>

View file

@ -16,7 +16,6 @@ its distribution.
| Kernel | **SchIFT** | Multi-core x86-64 kernel with a modular VFS | | Kernel | **SchIFT** | Multi-core x86-64 kernel with a modular VFS |
| Filesystem | **SchFS** | Transactional, copy-on-write, the root filesystem | | Filesystem | **SchFS** | Transactional, copy-on-write, the root filesystem |
| API | **SchAPI** | The native programming interface — *designed, not built* | | 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` | | 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 Twelve virtual terminals sit under all of it -- `/dev/tty1`..`tty12`, Ctrl+Alt+F1..F12

View file

@ -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 `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`. 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 **Xorg is the desktop for now, not for ever.** SchOS will not write its own compositor (the one it had, Schcom, was deleted).
GUI first, SchDE on top later ([../plan/roadmap.md](../plan/roadmap.md) §5). 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 ## Why X and not Wayland

View file

@ -17,7 +17,7 @@ first thing here.
| **About** | Pixels and text | *Frames* | | **About** | Pixels and text | *Frames* |
| **Operations** | `draw_pixel`, `fill_rect`, `putc`, `text_at` | `acquire`, `present`, `wait_vblank` | | **Operations** | `draw_pixel`, `fill_rect`, `putc`, `text_at` | `acquire`, `present`, `wait_vblank` |
| **Buffering** | None. Direct writes to the scanout. | Double-buffered | | **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 | | **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 That last row is the reason they are not one interface. The console and the

View file

@ -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 | | `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 | | `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 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 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 ```sh
make crashshot && make crashqr # crash the kernel, decode the QR out of the framebuffer 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 `tools/windows/splashshot.ps1` counts colours. **That exists because a log-only
harness once passed on a blank screen.** 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`, Other capture tools, all in `tools/windows/`: `dispshot`, `fbshot`,
`splashshot`, `textshot`, `ttyshot`. `splashshot`, `textshot`, `ttyshot`.

View file

@ -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** 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. 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 Persistence (an installer, or a persistent partition) is after 1.0 — see [roadmap.md](roadmap.md) §5.
[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 | | | 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. | | **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. | | **`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. | | **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 | | `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 | | [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) | | [finch.md](finch.md) | Finch inside SchOS (the language itself is finished and lives in its own repository) |

View file

@ -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 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). 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 `SchWM`, `SchDE` and the compositor `Schcom` are gone from the tree: SchOS uses other people's window manager and desktop — Window Maker
ordinary clients of Schcom when there is a Schcom to build them on. Meanwhile the window manager is 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). 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: 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 1. **Empty the register**: decide the unused in-kernel LDLS window (delete it, or use it), and build socket activation or
or remove the key, and give `projects/schcom/` its first client. remove the key.
2. **Then**, whatever [roadmap.md](roadmap.md) says is next. The two things the owner wants most are the installer 2. **The owner's list** ([roadmap.md](roadmap.md)): the Wayland groundwork (udev first, §5), making LDLS good enough that the GPU
drawn by Schcom — SchOS's own compositor, and the road to SchDE ([roadmap.md](roadmap.md) §5) — and GPU driver can run ([gpu.md](gpu.md)), a boot manager worth looking at (§5b), then the installer, then Finch inside SchOS
acceleration on the laptop ([gpu.md](gpu.md)). Finch inside SchOS ([finch.md](finch.md)) comes after. ([finch.md](finch.md)).
## What this plan does not address ## What this plan does not address

View file

@ -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 - **udev's job** — a uevent socket and `/sys/devices` — so X and others are told about devices instead of being
given a list. 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 SchOS does not write its own compositor. (Schcom, a 715-line v0, was deleted: its wire format was a dead end, and a
own, and everything it was blocked on in the kernel now exists: Unix sockets, descriptor passing, `memfd`, evdev, framebuffer compositor worth running is years of work that Alpine already packages.) The desktop is **Xorg with Window Maker** now
`mmap`. The plan is in three steps, each useful on its own: ([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 1. **udev.** Today Xorg is *told* its input devices (`xorg.conf.d/10-schos.conf`) because nothing announces them. The fix is
input events — small enough to be the program that forces Schcom's protocol to be real, and independent of Xorg, which is not in Alpine's `eudev` (or `udev`) running on SchOS, which wants: a **uevent netlink socket** (`NETLINK_KOBJECT_UEVENT`, with the
the installation environment. It needs a wire format for clients that is designed (Wayland-shaped: surfaces, buffers shared by `add`/`remove`/`change` messages and their environment), a real **`/sys/devices` tree** with `/sys/bus/*/devices/*` and
descriptor, input events), not the v0 one. `/sys/class/*/*` as symlinks into it, `uevent` files that can be written to replay events (`udevadm trigger`), and devtmpfs
2. **If that works, Schcom becomes useful in its own right**: window management as a module of the compositor (a compositor *is* that udev can create nodes in. libinput then enumerates evdev devices through it.
its window manager), multiple clients with focus and stacking, damage tracking. 2. **libinput and libseat** (seatd is in Alpine): evdev with the properties libinput reads (`ID_INPUT_*` from udev), and a way
3. **SchDE on top of it**: a panel, a launcher, a wallpaper and settings, as ordinary clients — only once there is a Schcom worth for a non-root compositor to take the keyboard and the display from the kernel — the `VT_PROCESS` handshake exists.
putting them on. Until then the desktop is Xorg with Window Maker ([desktop.md](../architecture/desktop.md)). 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` ### The installer and `.sip`
@ -96,7 +118,7 @@ Open questions, all wanting a decision before code:
now, or not? 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`. - **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 ## 6. SchFS beyond SchOS

View file

@ -3,7 +3,9 @@
Status: **direction agreed, not yet implemented**, and frozen until 1.0 ships 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 ([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 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 changes what is being built this week; it changes what everything is being
built *towards*. See §7 for what actually happens now. built *towards*. See §7 for what actually happens now.

View file

@ -7,8 +7,7 @@ Everything above this directory — `kernel/`, `boot/`, `lib/`, `cmd/`,
is one program that either works or does not. Everything in here is an 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. ordinary program that talks to it through syscalls, exactly as `toybox` does.
The split exists because the alternative gets bad quickly. A compositor, The split exists because the alternative gets bad quickly. A desktop environment is larger than the kernel will be for a
a window manager and a desktop are each larger than the kernel will be for a
long time. Grown inside the OS tree they would blur into it: a compositor 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 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 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 | | 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. | | `schinst/` | The installer, the installation environment, and `.sip` tooling. |
| `schfs-linux/` | SchFS as a filesystem for Linux. FUSE first, a kernel module later. | | `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 `schcom/`, `schwm/` and `schde/` used to be here and were **deleted**: SchOS does not write its own compositor, window
is Schcom's own business now (a compositor is its window manager), and **SchDE**, a panel, launcher and the rest, manager or desktop. The desktop is Xorg with Window Maker ([`docs/architecture/desktop.md`](../docs/architecture/desktop.md)),
comes back as ordinary clients of Schcom once there is something for them to run on. Until then the desktop is 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).
Xorg with Window Maker ([`docs/architecture/desktop.md`](../docs/architecture/desktop.md)).
**`schfs-linux/` is written to stand on its own.** It does not mention SchOS **`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 anywhere, and that is deliberate rather than an oversight: it is intended to be
@ -51,11 +48,5 @@ by anything above it.
## Status ## 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 `schinst/` and `schfs-linux/` are scoped and unstarted, and are **frozen** -- see
[`docs/plan/1.0.md`](../docs/plan/1.0.md). [`docs/plan/1.0.md`](../docs/plan/1.0.md).

View file

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

View file

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

View file

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

Binary file not shown.

Binary file not shown.

View file

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

View file

@ -1,715 +0,0 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
* Schcom -- the SchOS compositor.
*
* Owns the display and the input devices. Clients draw into shared buffers and
* hand them over; Schcom decides what is on top of what, composites, and
* presents. It does not decide where windows go: that is SchWM's job, and the
* moment this file starts making placement decisions the split has failed.
*
* v0, deliberately small but not a toy:
*
* - real framebuffer output through /dev/fb0 and the fbdev ioctls
* - real clients over a Unix socket, with a versioned handshake
* - real shared-memory buffers, one per surface
* - a back buffer, so a half-composited frame never reaches the screen
* - input read from /dev/input/eventN and routed to the top surface
*
* What it does not do yet: damage tracking beyond "repaint everything",
* multiple outputs, subsurfaces, or any notion of focus beyond "topmost".
* Those are additions, not rewrites -- which is the point of committing to the
* surface/commit model now rather than growing into it.
*/
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/shm.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/un.h>
#include <linux/fb.h>
#include <linux/input.h>
#include "proto.h"
#define MAX_CLIENTS 16
#define MAX_SURFACES 32
struct surface {
int used;
uint32_t id;
int client; /* index into clients[] */
int shmid;
void *pixels;
uint32_t w, h, stride, size;
int32_t x, y;
};
struct client {
int fd; /* -1 when the slot is free */
int hello_done;
};
static struct client clients[MAX_CLIENTS];
static struct surface surfaces[MAX_SURFACES];
static uint32_t next_surface_id = 1;
/* The screen. */
static uint8_t *fb;
static uint32_t fb_w, fb_h, fb_stride, fb_bpp;
static size_t fb_size;
static uint8_t *back; /* composited here, then copied out in one go */
/* Pointer position: set outright by EV_ABS from a tablet, nudged by EV_REL
* from a plain mouse. */
static int32_t ptr_x, ptr_y;
static void
die(const char *what)
{
fprintf(stderr, "schcom: %s: %s\n", what, strerror(errno));
exit(1);
}
/* ---------------------------------------------------------------- output */
static int
open_framebuffer(void)
{
int fd = open("/dev/fb0", O_RDWR);
if (fd < 0)
die("open /dev/fb0");
struct fb_var_screeninfo var;
struct fb_fix_screeninfo fix;
if (ioctl(fd, FBIOGET_VSCREENINFO, &var) < 0)
die("FBIOGET_VSCREENINFO");
if (ioctl(fd, FBIOGET_FSCREENINFO, &fix) < 0)
die("FBIOGET_FSCREENINFO");
fb_w = var.xres;
fb_h = var.yres;
fb_bpp = var.bits_per_pixel;
/* The stride comes from the driver, never from xres*bpp: the two differ on
* real hardware and assuming otherwise shears the image. */
fb_stride = fix.line_length;
fb_size = (size_t)fb_stride * fb_h;
if (fb_bpp != 32) {
fprintf(stderr, "schcom: need 32bpp, got %u\n", fb_bpp);
exit(1);
}
fb = mmap(NULL, fb_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (fb == MAP_FAILED)
die("mmap /dev/fb0");
back = malloc(fb_size);
if (!back)
die("malloc back buffer");
printf("schcom: %ux%u, %u bpp, stride %u\n", fb_w, fb_h, fb_bpp, fb_stride);
fflush(stdout);
return fd;
}
/* Damage: the rectangle that has to be redrawn and copied out.
*
* Without this the compositor repainted the whole screen for every event --
* at 1280x800 that is a million pixels of desktop fill plus a 4 MB copy to the
* framebuffer, per mouse movement. Under emulation the pointer lagged behind
* the mouse by whole seconds, and it was not the emulation's fault: moving a
* 10x10 cursor does not require touching four megabytes.
*
* One rectangle rather than a list. Two distant small changes then repaint the
* span between them, which is wasteful in theory; in practice the changes are
* a pointer and the window under it, and a list costs more bookkeeping than it
* saves.
*/
static int dmg_x0, dmg_y0, dmg_x1, dmg_y1; /* x1/y1 exclusive */
static int dirty;
static void
damage(int x, int y, int w, int h)
{
int x0 = x, y0 = y, x1 = x + w, y1 = y + h;
if (x0 < 0) x0 = 0;
if (y0 < 0) y0 = 0;
if (x1 > (int)fb_w) x1 = (int)fb_w;
if (y1 > (int)fb_h) y1 = (int)fb_h;
if (x0 >= x1 || y0 >= y1)
return;
if (!dirty) {
dmg_x0 = x0; dmg_y0 = y0; dmg_x1 = x1; dmg_y1 = y1;
dirty = 1;
return;
}
if (x0 < dmg_x0) dmg_x0 = x0;
if (y0 < dmg_y0) dmg_y0 = y0;
if (x1 > dmg_x1) dmg_x1 = x1;
if (y1 > dmg_y1) dmg_y1 = y1;
}
static void
damage_all(void)
{
damage(0, 0, (int)fb_w, (int)fb_h);
}
#define PTR_SIZE 10
static void
composite(void)
{
if (!dirty)
return;
const int rx0 = dmg_x0, ry0 = dmg_y0, rx1 = dmg_x1, ry1 = dmg_y1;
/* Desktop. A flat colour rather than black, so that "nothing was drawn"
* and "the compositor is not running" cannot look the same. */
for (int y = ry0; y < ry1; y++) {
uint32_t *row = (uint32_t *)(back + (size_t)y * fb_stride);
for (int x = rx0; x < rx1; x++)
row[x] = 0x00203040;
}
/* Surfaces, bottom to top. Creation order is stacking order in v0; SchWM
* will reorder later through a protocol request rather than by reaching in
* here. */
for (int i = 0; i < MAX_SURFACES; i++) {
struct surface *s = &surfaces[i];
if (!s->used || !s->pixels)
continue;
/* Only the part of this surface inside the damage rectangle. */
for (uint32_t sy = 0; sy < s->h; sy++) {
int dy = s->y + (int)sy;
if (dy < ry0 || dy >= ry1)
continue;
uint32_t *dst = (uint32_t *)(back + (size_t)dy * fb_stride);
uint32_t *src = (uint32_t *)((uint8_t *)s->pixels + (size_t)sy * s->stride);
for (uint32_t sx = 0; sx < s->w; sx++) {
int dx = s->x + (int)sx;
if (dx < rx0 || dx >= rx1)
continue;
dst[dx] = src[sx];
}
}
}
/* A crude pointer, drawn last so it is never under a window. */
for (int py = 0; py < PTR_SIZE; py++) {
int dy = ptr_y + py;
if (dy < ry0 || dy >= ry1)
continue;
uint32_t *dst = (uint32_t *)(back + (size_t)dy * fb_stride);
for (int px = 0; px < PTR_SIZE - py; px++) {
int dx = ptr_x + px;
if (dx >= rx0 && dx < rx1)
dst[dx] = 0x00FFFFFF;
}
}
/* Copy out only what changed, row by row. Compositing straight into the
* framebuffer would show every intermediate state -- the flicker would be
* the whole desktop being repainted under the user's eyes -- but copying
* the whole thing to move a cursor is what made the pointer lag. */
size_t bytes = (size_t)(rx1 - rx0) * 4;
for (int y = ry0; y < ry1; y++) {
size_t off = (size_t)y * fb_stride + (size_t)rx0 * 4;
memcpy(fb + off, back + off, bytes);
}
dirty = 0;
}
/* ---------------------------------------------------------------- clients */
static int
send_all(int fd, const void *buf, size_t n)
{
const uint8_t *p = buf;
while (n) {
ssize_t w = write(fd, p, n);
if (w < 0) {
if (errno == EINTR)
continue;
return -1;
}
p += w;
n -= (size_t)w;
}
return 0;
}
static int
read_all(int fd, void *buf, size_t n)
{
uint8_t *p = buf;
while (n) {
ssize_t r = read(fd, p, n);
if (r == 0)
return -1; /* client went away */
if (r < 0) {
if (errno == EINTR)
continue;
return -1;
}
p += r;
n -= (size_t)r;
}
return 0;
}
static void
send_error(int fd, uint32_t serial, uint32_t code)
{
struct schcom_error e;
memset(&e, 0, sizeof e);
e.hdr.op = SCHCOM_OP_ERROR;
e.hdr.length = sizeof e;
e.hdr.serial = serial;
e.code = code;
send_all(fd, &e, sizeof e);
}
static void
drop_client(int ci)
{
if (clients[ci].fd < 0)
return;
for (int i = 0; i < MAX_SURFACES; i++) {
struct surface *s = &surfaces[i];
if (s->used && s->client == ci) {
damage(s->x, s->y, (int)s->w, (int)s->h);
if (s->pixels)
shmdt(s->pixels);
/* IPC_RMID once, here: the segment goes when the last attach does,
* so a client that died mid-frame does not leak it. */
shmctl(s->shmid, IPC_RMID, NULL);
memset(s, 0, sizeof *s);
}
}
close(clients[ci].fd);
clients[ci].fd = -1;
clients[ci].hello_done = 0;
printf("schcom: client %d gone\n", ci);
fflush(stdout);
}
static void
handle_create_surface(int ci, const struct schcom_create_surface *req)
{
int fd = clients[ci].fd;
int slot = -1;
for (int i = 0; i < MAX_SURFACES; i++)
if (!surfaces[i].used) { slot = i; break; }
if (slot < 0) {
send_error(fd, req->hdr.serial, ENOSPC);
return;
}
uint32_t w = req->width, h = req->height;
if (w == 0 || h == 0 || w > fb_w || h > fb_h) {
send_error(fd, req->hdr.serial, EINVAL);
return;
}
uint32_t stride = w * 4;
uint32_t size = stride * h;
/* The compositor owns the segment, not the client: it has to survive the
* client crashing mid-frame, and it must not be something a client can
* quietly resize underneath us. */
int shmid = shmget(IPC_PRIVATE, size, IPC_CREAT | 0600);
if (shmid < 0) {
send_error(fd, req->hdr.serial, errno);
return;
}
void *pix = shmat(shmid, NULL, 0);
if (pix == (void *)-1) {
shmctl(shmid, IPC_RMID, NULL);
send_error(fd, req->hdr.serial, errno);
return;
}
memset(pix, 0, size);
struct surface *s = &surfaces[slot];
s->used = 1;
s->id = next_surface_id++;
s->client = ci;
s->shmid = shmid;
s->pixels = pix;
s->w = w;
s->h = h;
s->stride = stride;
s->size = size;
s->x = req->x;
s->y = req->y;
struct schcom_surface rep;
memset(&rep, 0, sizeof rep);
rep.hdr.op = SCHCOM_OP_SURFACE;
rep.hdr.length = sizeof rep;
rep.hdr.serial = req->hdr.serial;
rep.hdr.surface = s->id;
rep.handle_kind = SCHCOM_HANDLE_SHMID;
rep.handle = (uint32_t)shmid;
rep.width = w;
rep.height = h;
rep.stride = stride;
rep.size = size;
if (send_all(fd, &rep, sizeof rep) < 0) {
drop_client(ci);
return;
}
damage(s->x, s->y, (int)w, (int)h);
printf("schcom: surface %u %ux%u at %d,%d for client %d (shm %d)\n",
s->id, w, h, s->x, s->y, ci, shmid);
fflush(stdout);
}
static struct surface *
find_surface(uint32_t id)
{
for (int i = 0; i < MAX_SURFACES; i++)
if (surfaces[i].used && surfaces[i].id == id)
return &surfaces[i];
return NULL;
}
/* Returns 1 if the screen needs repainting. */
static int
handle_message(int ci)
{
int fd = clients[ci].fd;
struct schcom_header hdr;
if (read_all(fd, &hdr, sizeof hdr) < 0) {
drop_client(ci);
return 0;
}
if (hdr.length < sizeof hdr || hdr.length > 4096) {
send_error(fd, hdr.serial, EPROTO);
drop_client(ci);
return 0;
}
uint8_t body[4096];
memcpy(body, &hdr, sizeof hdr);
size_t rest = hdr.length - sizeof hdr;
if (rest && read_all(fd, body + sizeof hdr, rest) < 0) {
drop_client(ci);
return 0;
}
if (!clients[ci].hello_done && hdr.op != SCHCOM_OP_HELLO) {
/* Nothing before the handshake. A client that gets this wrong is a
* client built against a protocol we do not speak. */
send_error(fd, hdr.serial, EPROTO);
drop_client(ci);
return 0;
}
switch (hdr.op) {
case SCHCOM_OP_HELLO: {
const struct schcom_hello *h = (const void *)body;
if (hdr.length < sizeof *h || h->version != SCHCOM_VERSION) {
send_error(fd, hdr.serial, EPROTONOSUPPORT);
drop_client(ci);
return 0;
}
struct schcom_hello ack;
memset(&ack, 0, sizeof ack);
ack.hdr.op = SCHCOM_OP_HELLO_ACK;
ack.hdr.length = sizeof ack;
ack.hdr.serial = hdr.serial;
ack.version = SCHCOM_VERSION;
if (send_all(fd, &ack, sizeof ack) < 0) {
drop_client(ci);
return 0;
}
clients[ci].hello_done = 1;
printf("schcom: client %d speaks v%u\n", ci, SCHCOM_VERSION);
fflush(stdout);
return 0;
}
case SCHCOM_OP_CREATE_SURFACE:
if (hdr.length < sizeof(struct schcom_create_surface)) {
send_error(fd, hdr.serial, EINVAL);
return 0;
}
handle_create_surface(ci, (const void *)body);
return 1;
case SCHCOM_OP_COMMIT: {
struct surface *s = find_surface(hdr.surface);
if (!s || s->client != ci) {
send_error(fd, hdr.serial, ENOENT);
return 0;
}
/* The commit is the whole point: the pixels were already there, but
* they were not allowed to be shown until now. The damage rectangle in
* the message is advisory -- repaint the whole surface, which is never
* wrong, only sometimes more than asked. */
damage(s->x, s->y, (int)s->w, (int)s->h);
struct schcom_header fr;
memset(&fr, 0, sizeof fr);
fr.op = SCHCOM_OP_FRAME;
fr.length = sizeof fr;
fr.surface = s->id;
if (send_all(fd, &fr, sizeof fr) < 0) {
drop_client(ci);
return 1;
}
return 1;
}
case SCHCOM_OP_DESTROY: {
struct surface *s = find_surface(hdr.surface);
if (s && s->client == ci) {
damage(s->x, s->y, (int)s->w, (int)s->h);
if (s->pixels)
shmdt(s->pixels);
shmctl(s->shmid, IPC_RMID, NULL);
memset(s, 0, sizeof *s);
return 1;
}
return 0;
}
default:
/* Unknown but well-formed: skipped, not fatal. That is what the length
* field is for, and it is how a v0 compositor survives a v1 client. */
return 0;
}
}
/* ----------------------------------------------------------------- input */
/* 1 if the screen needs repainting, 0 if the event changed nothing, -1 to
* quit, -2 when the queue is empty. The last is separate from 0 on purpose:
* every burst of movement ends in an EV_SYN that changes nothing, and a
* drain loop that stopped there would leave the rest of the burst queued. */
static int
handle_input(int fd)
{
struct input_event ev;
if (read(fd, &ev, sizeof ev) != (ssize_t)sizeof ev)
return -2;
int repaint = 0;
/* An absolute pointer says where it is; a relative one says how far it
* moved. Taking the absolute value as authoritative is the whole point of
* usb-tablet: two pointers that accumulate deltas independently drift
* apart, which inside a VM shows up as the guest cursor sitting somewhere
* the host's is not. */
if (ev.type == EV_ABS) {
int old_x = ptr_x, old_y = ptr_y;
if (ev.code == ABS_X) { ptr_x = ev.value; repaint = 1; }
if (ev.code == ABS_Y) { ptr_y = ev.value; repaint = 1; }
if (ptr_x < 0) ptr_x = 0;
if (ptr_y < 0) ptr_y = 0;
if (ptr_x >= (int32_t)fb_w) ptr_x = (int32_t)fb_w - 1;
if (ptr_y >= (int32_t)fb_h) ptr_y = (int32_t)fb_h - 1;
if (repaint) {
damage(old_x, old_y, PTR_SIZE, PTR_SIZE);
damage(ptr_x, ptr_y, PTR_SIZE, PTR_SIZE);
}
}
if (ev.type == EV_REL) {
int old_x = ptr_x, old_y = ptr_y;
if (ev.code == REL_X) { ptr_x += ev.value; repaint = 1; }
if (ev.code == REL_Y) { ptr_y += ev.value; repaint = 1; }
if (ptr_x < 0) ptr_x = 0;
if (ptr_y < 0) ptr_y = 0;
if (ptr_x >= (int32_t)fb_w) ptr_x = (int32_t)fb_w - 1;
if (ptr_y >= (int32_t)fb_h) ptr_y = (int32_t)fb_h - 1;
if (repaint) {
/* Where it was and where it is. Everything between is untouched. */
damage(old_x, old_y, PTR_SIZE, PTR_SIZE);
damage(ptr_x, ptr_y, PTR_SIZE, PTR_SIZE);
}
}
if (ev.type == EV_KEY && ev.value == 1 && ev.code == KEY_ESC)
return -1;
/* Route to the topmost surface. "Topmost" is the whole focus model in v0;
* anything cleverer is policy and belongs to SchWM. */
struct surface *top = NULL;
for (int i = 0; i < MAX_SURFACES; i++)
if (surfaces[i].used)
top = &surfaces[i];
if (top && (ev.type == EV_KEY || ev.type == EV_REL ||
ev.type == EV_ABS)) {
struct schcom_input msg;
memset(&msg, 0, sizeof msg);
msg.hdr.op = SCHCOM_OP_INPUT;
msg.hdr.length = sizeof msg;
msg.hdr.surface = top->id;
msg.type = ev.type;
msg.code = ev.code;
msg.value = ev.value;
msg.x = ptr_x;
msg.y = ptr_y;
if (clients[top->client].fd >= 0)
send_all(clients[top->client].fd, &msg, sizeof msg);
}
return repaint;
}
/* ------------------------------------------------------------------ main */
int
main(int argc, char **argv)
{
int frames_then_exit = 0; /* --frames N: for automated testing */
for (int i = 1; i < argc; i++)
if (!strcmp(argv[i], "--frames") && i + 1 < argc)
frames_then_exit = atoi(argv[++i]);
for (int i = 0; i < MAX_CLIENTS; i++)
clients[i].fd = -1;
open_framebuffer();
ptr_x = (int32_t)fb_w / 2;
ptr_y = (int32_t)fb_h / 2;
unlink(SCHCOM_SOCKET_PATH);
int lfd = socket(AF_UNIX, SOCK_STREAM, 0);
if (lfd < 0)
die("socket");
struct sockaddr_un addr;
memset(&addr, 0, sizeof addr);
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, SCHCOM_SOCKET_PATH, sizeof addr.sun_path - 1);
if (bind(lfd, (struct sockaddr *)&addr, sizeof addr) < 0)
die("bind " SCHCOM_SOCKET_PATH);
if (listen(lfd, 8) < 0)
die("listen");
int kbd = open("/dev/input/event0", O_RDONLY | O_NONBLOCK);
int mouse = open("/dev/input/event1", O_RDONLY | O_NONBLOCK);
printf("schcom: listening on %s\n", SCHCOM_SOCKET_PATH);
fflush(stdout);
damage_all();
composite();
int frames = 1;
for (;;) {
struct pollfd pfd[MAX_CLIENTS + 3];
int n = 0;
pfd[n].fd = lfd; pfd[n].events = POLLIN; n++;
int kbd_slot = -1, mouse_slot = -1;
if (kbd >= 0) { kbd_slot = n; pfd[n].fd = kbd; pfd[n].events = POLLIN; n++; }
if (mouse >= 0) { mouse_slot = n; pfd[n].fd = mouse; pfd[n].events = POLLIN; n++; }
int first_client = n;
for (int i = 0; i < MAX_CLIENTS; i++)
if (clients[i].fd >= 0) {
pfd[n].fd = clients[i].fd;
pfd[n].events = POLLIN;
n++;
}
if (poll(pfd, n, 100) < 0) {
if (errno == EINTR)
continue;
die("poll");
}
int repaint = 0;
if (pfd[0].revents & POLLIN) {
int cfd = accept(lfd, NULL, NULL);
if (cfd >= 0) {
int slot = -1;
for (int i = 0; i < MAX_CLIENTS; i++)
if (clients[i].fd < 0) { slot = i; break; }
if (slot < 0) {
close(cfd);
} else {
clients[slot].fd = cfd;
clients[slot].hello_done = 0;
printf("schcom: client %d connected\n", slot);
fflush(stdout);
}
}
}
/* Drain both devices before painting anything. A single mouse
* movement produces a burst of EV_REL followed by EV_SYN, and
* repainting per event meant the compositor rendered every
* intermediate position of a cursor nobody saw at those positions --
* which is most of what "the mouse lags" was. */
int quit = 0;
if (kbd_slot >= 0 && (pfd[kbd_slot].revents & POLLIN)) {
for (int n = 0; n < 256; n++) {
int r = handle_input(kbd);
if (r == -2) break;
if (r < 0) { quit = 1; break; }
if (r > 0) repaint = 1;
}
}
if (!quit && mouse_slot >= 0 && (pfd[mouse_slot].revents & POLLIN)) {
for (int n = 0; n < 256; n++) {
int r = handle_input(mouse);
if (r == -2) break;
if (r < 0) { quit = 1; break; }
if (r > 0) repaint = 1;
}
}
if (quit) break;
/* Walk the client slots in the same order they were added to pfd. */
int k = first_client;
for (int i = 0; i < MAX_CLIENTS; i++) {
if (clients[i].fd < 0)
continue;
if (pfd[k].revents & (POLLIN | POLLHUP))
repaint |= handle_message(i);
k++;
}
if (repaint) {
composite();
frames++;
if (frames_then_exit && frames >= frames_then_exit) {
printf("schcom: %d frames, exiting as asked\n", frames);
fflush(stdout);
break;
}
}
}
unlink(SCHCOM_SOCKET_PATH);
return 0;
}

View file

@ -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