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:
parent
6e78bb0d5e
commit
ed81f9a07a
21 changed files with 62 additions and 1356 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -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/
|
||||||
|
|
|
||||||
13
Makefile
13
Makefile
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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* |
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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`.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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) |
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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).
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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.
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
Loading…
Reference in a new issue