Skip to content

Omarchy on Kandelo — the desktop shell: layer-shell bar, launcher, themes - #1241

Open
mho22 wants to merge 40 commits into
explore-dri-hyprlandfrom
explore-dri-omarchy
Open

Omarchy on Kandelo — the desktop shell: layer-shell bar, launcher, themes#1241
mho22 wants to merge 40 commits into
explore-dri-hyprlandfrom
explore-dri-omarchy

Conversation

@mho22

@mho22 mho22 commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Stacked on #976 (the tiling WM). This starts at the O1 milestone of docs/plans/2026-07-14-build-hyprland-class-compositor-plan.md §3 (PR15–PR18) and continues past it: the desktop shell that turns the tiling compositor into an Omarchy-shaped desktop, then the identity work that makes it look and feel like Omarchy rather than a compositor test.

Commits

  1. wlr-layer-shell, kbar + klauncher, theme enginezwlr_layer_shell_v1 (anchors, margins, exclusive zones, keyboard-interactivity) with the arrangement engine that reserves strips and retiles beneath them; the file-based theme system (/usr/share/kandelo/themes/<name>/theme.conf, kwlctl dispatch theme, theme>> broadcast); kbar and klauncher; kwl_layer_create() in libkwl; a SIGPIPE fix (a control client exiting without reading its reply killed the desktop).
  2. /?demo=omarchy — the O1 integration gate: bar + launcher + themes, every SUPER bind mirrored on CTRL (browsers reserve SUPER).
  3. Image wallpapers + six themeswallpaper = <file> (KWLP raw pixels, bilinear-scaled by the compositor; raw because nothing in C decodes PNG/JPEG — the page renders per-theme aurora wallpapers at staging time). Tokyo Night, Catppuccin Mocha, Gruvbox, Nord, Everforest, Rosé Pine. Fixes a wlterm bug this exposed: a resize smaller than one terminal cell left the window buffer-less and invisible.
  4. Omarchy menu + real applications — the launcher registry offers Vim, NetHack, Nano and Bash running unmodified in wlterm (lazy-fetched from the shell image's archives); klauncher --menu is the Omarchy menu (Apps/Theme, ESC back to root) on SUPER/CTRL+ALT+Space; the compositor gains the ALT modifier and the keymap gains Alt_L + arrow keys (arrows never reached clients before); wlterm advertises TERM=vt100 to its child.
  5. knotify toasts + bar uptimenotify = <path> spawns a themed overlay toast on every theme switch; kbar shows the kernel's monotonic uptime (no /proc/meminfo → no memory module; real stats or none).

Both hosts

  • Node: wlcompositor-{layer-shell,theme,keybind,resize,…} smokes — all 10 wlcompositor/wlterm suites pass serially, including new coverage for the wallpaper triple (image / gradient fallback / broken file), the grid-preserving-resize recommit regression, CTRL-ALT exact-mods binds, the menu flow, and the notify hook.
  • Browser: kandelo-omarchy.spec.ts grew from 7 to 10 gates (vim launch through the launcher incl. an ABI-mismatch guard, the menu flow, the toast lifecycle); omarchy + hyprland + wayland gates pass.

Verification: cargo -p kandelo --lib 0 fail; run-libc-tests.sh / run-posix-tests.sh 0 FAIL; check-abi-version.sh in sync — everything here is additive userspace, no ABI_VERSION bump.

Known issues carried in the tree (not this PR's regressions): the published vim archive is still abi-v15 and needs a matrix re-publish (a locally rebuilt vim.zip is what the demo uses meanwhile); the exclusive-zone release path (bar exits → tiling area grows back) remains untested — a prototype dispatch closelayer exposed a host-layer deadlock when a layer client exits in reaction to a compositor event, which needs its own investigation.

Docs: architecture.md (desktop shell, themes, menu, knotify), browser-support.md (Omarchy demo section), presets.

@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Phase B-1 matrix build status — pr-1241-staging

ABI v17. 0 built, 110 failed, 110 total.

Package Arch Status Sha
alsa-lib wasm32 failed
atk wasm32 failed
atkmm wasm32 failed
basu wasm32 failed
cairo wasm32 failed
cairomm wasm32 failed
expat wasm32 failed
fcft wasm32 failed
fmt wasm32 failed
fontconfig wasm32 failed
freetype wasm32 failed
fribidi wasm32 failed
gdk-pixbuf wasm32 failed
glib wasm32 failed
glibmm wasm32 failed
gtk-layer-shell wasm32 failed
gtk3 wasm32 failed
gtkmm3 wasm32 failed
harfbuzz wasm32 failed
jsoncpp wasm32 failed
libcurl wasm32 failed
libcxx wasm32 failed
libcxx wasm64 failed
libdrm wasm32 failed
libepoxy wasm32 failed
libevdev wasm32 failed
libffi wasm32 failed
libinput-lite wasm32 failed
libinput wasm32 failed
libpng wasm32 failed
libsigcxx wasm32 failed
libudev wasm32 failed
libwayland wasm32 failed
libxkbcommon wasm32 failed
libxkbregistry wasm32 failed
libxml2 wasm32 failed
libxml2 wasm64 failed
mtdev wasm32 failed
openssl wasm32 failed
openssl wasm64 failed
pango wasm32 failed
pangomm wasm32 failed
pcre2 wasm32 failed
pixman wasm32 failed
sdl2 wasm32 failed
spdlog wasm32 failed
sqlite wasm32 failed
sqlite wasm64 failed
tllist wasm32 failed
utf8proc wasm32 failed
zlib wasm32 failed
zlib wasm64 failed
bc wasm32 failed
bzip2 wasm32 failed
coreutils wasm32 failed
curl wasm32 failed
dash wasm32 failed
dbus wasm32 failed
diffutils wasm32 failed
dinit wasm32 failed
fbdoom wasm32 failed
file wasm32 failed
findutils wasm32 failed
foot wasm32 failed
gawk wasm32 failed
git wasm32 failed
grep wasm32 failed
gzip wasm32 failed
kandelo-sdk wasm32 failed
kernel wasm32 failed
less wasm32 failed
lsof wasm32 failed
m4 wasm32 failed
make wasm32 failed
mako wasm32 failed
mariadb wasm32 failed
mariadb wasm64 failed
msmtpd wasm32 failed
nano wasm32 failed
ncurses wasm32 failed
netcat wasm32 failed
nginx wasm32 failed
php wasm32 failed
posix-utils-lite wasm32 failed
sed wasm32 failed
spidermonkey wasm32 failed
tar wasm32 failed
tcl wasm32 failed
unzip wasm32 failed
userspace wasm32 failed
vim wasm32 failed
waybar wasm32 failed
wget wasm32 failed
xz wasm32 failed
zip wasm32 failed
zstd wasm32 failed
bash wasm32 failed
mariadb-test wasm32 failed
mariadb-vfs wasm32 failed
mariadb-vfs wasm64 failed
nethack wasm32 failed
node wasm32 failed
spidermonkey-node wasm32 failed
vim-browser-bundle wasm32 failed
nethack-browser-bundle wasm32 failed
rootfs wasm32 failed
shell wasm32 failed
lamp wasm32 failed
node-vfs wasm32 failed
wordpress wasm32 failed

Auto-generated; replaced on each push. Raw data in the publish-status workflow artifact.

mho22 and others added 29 commits August 20, 2026 13:32
…me engine

The Tier 1 / O1 desktop shell from the Hyprland-class compositor plan
(docs/plans/2026-07-14-build-hyprland-class-compositor-plan.md §3).

Compositor: zwlr_layer_shell_v1 (anchors, margins, exclusive zones,
keyboard-interactivity). layers_arrange() computes the window work area
from the anchored strips, background→overlay, then retiles. A surface
joins the layer list at role creation, not at map, so a client dying
mid-handshake cannot strand a strip. Compositing and hit-testing put
background/bottom under windows and top/overlay over them on the GPU
and CPU paths; layer surfaces show on every workspace.

Theme engine: theme_scan/theme_load/theme_apply/theme_switch over
/usr/share/kandelo/themes/<name>/theme.conf. `theme =` config key,
`kwlctl theme` query, `dispatch theme <name|next|prev>`, a `theme`
keybind dispatcher, and a `theme>>name` broadcast. Gaps and the focus
border color are theme variables; the wallpaper repaints and re-uploads
through a retained staging bo on switch.

Fixes: SIGPIPE from a control client that exits without reading its
reply no longer kills the compositor; a failed exec reports
KWLCTL_EXEC_FAILED on stdout.

libkwl: kwl_layer_create() + struct kwl_layer_opts; the connect path is
factored into kwl_open()/kwl_open_buffers() shared with
kwl_window_create().

New clients: kbar (top bar, exclusive zone, workspace pills + focused
window + clock off the kwlctl socket) and klauncher (overlay launcher,
keyboard-exclusive, filters /usr/share/kandelo/apps, Enter dispatches
exec).

Tests: layer-shell and theme Node smokes; the resize smoke now waits
for both windows' resize markers instead of racing the second one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The O1 integration gate: the tiling compositor boots with kbar on a
layer-shell strip, klauncher on SUPER/CTRL+Space, and three themes
(Tokyo Night, Catppuccin Mocha, Gruvbox) cycling on CTRL+SHIFT+Space.

omarchy-desktop.ts carries the data half: the compositor config, the
launcher app registry, and the theme palettes. Staging and spawn reuse
the hyprland block in live-setup.ts; only the extra files and the two
extra binaries differ. Every SUPER bind is mirrored on CTRL because the
browser reserves SUPER for the OS.

Both hosts: the compositor, bar, launcher, and theme engine are gated
by Node smokes (previous commit); the browser path is gated by the new
kandelo-omarchy.spec.ts Playwright suite (config + theme loaded, bar
anchored, windows tile under the bar, canvas composited, launcher takes
the keyboard + filters + launches, theme cycles, bar tracks the
workspace).

Docs: architecture.md gains the desktop-shell subsection;
browser-support.md gains the demo-table row and the Omarchy demo
section.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A theme's theme.conf gains `wallpaper = <file>`: KWLP raw pixels
("KWLP", u32le width/height, XRGB) that the compositor bilinear-scales
into g.wallpaper, falling back to the gradient when the key is absent
or the file fails to load. Raw pixels because nothing in the
compositor decodes PNG/JPEG — whoever stages the theme renders the
image.

The demo now ships six themes — Tokyo Night, Catppuccin Mocha,
Gruvbox, Nord, Everforest, Rosé Pine — each with an aurora wallpaper
the page renders at staging time (renderWallpaperKwlp: radial glows in
the palette colours on an OffscreenCanvas, 960x540, ~2 MB staged per
theme).

Fix exposed by per-theme gaps: wlterm only redrew + committed on a
KWL_RESIZE that changed its cell grid, but kwl_apply_resize has
already destroyed the buffer the compositor held — a tile shift
smaller than one cell (a theme's gap change) left the window invisible
until the shell printed again. wlterm now always recommits and gates
only the PTY winsize/SIGWINCH on a grid change. The other libkwl
clients already redrew unconditionally.

Both hosts: theme smoke gains the wallpaper triple (image, gradient
fallback, broken file) and the small-gap recommit regression (three
1px gap steps guarantee a grid-preserving resize); the Playwright gate
asserts WALLPAPER image and the widened theme-cycle set; hyprland +
omarchy browser gates green, all 10 wlcompositor/wlterm Node suites
green serially.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The launcher registry now offers the shell image's real software next
to the demo clients: Vim, NetHack and Nano run unmodified inside a
wlterm (`exec = /usr/local/bin/wlterm /usr/bin/vim`), plus a Bash
terminal. The binaries lazy-fetch from the image's archives on first
launch — the normal runtime path, no demo staging.

wlterm now sets TERM=vt100 in its forkpty child: the inherited TERM
describes the launcher's terminal, and curses apps must see the type
this terminal implements.

`klauncher --menu` opens the Omarchy menu: a root level (Apps, Theme)
that descends into the app list or the installed-theme list (read from
`kwlctl theme`, switched with `dispatch theme`). ESC in a submenu goes
back to the root; ESC at the root dismisses. Bound to
SUPER/CTRL+ALT+Space in the demo config.

Compositor: the keybind engine gains the ALT (Mod1) modifier, and the
self-contained xkb keymap gains Alt_L and the four arrow keys — the
keymap had no arrows, so Up/Down never reached the launcher (or vim)
in the browser.

Both hosts: keybind smoke gains a CTRL ALT exact-mods case; theme
smoke gains the menu flow (root → themes → dispatch, ESC back to
root); the Playwright gate gains the vim launch through the launcher
(lazy fetch + fifth tile + an ABI-mismatch guard) and the menu flow.
All 10 wlcompositor/wlterm Node suites and the omarchy/hyprland/
wayland browser gates pass.

The vim launch initially died on exec: the cached vim archive was
built against ABI 15 (stale June link output reused by an incremental
build). Rebuilt locally against the current sysroot; the published
archive needs a re-publish for abi-v17.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
knotify — the notify-send slot: one toast per process. A corner-
anchored overlay layer surface (top-right, under the bar's strip)
shows `knotify <title> <body…>` for a moment and exits; the surface
teardown is the dismissal, so there is no daemon and no unmap
protocol. It adopts the compositor's live theme over kwlctl.

The compositor gains a `notify = <path>` config key: a theme switch
spawns the configured notifier with the new name — the hook Omarchy
fills with notify-send in its scripts. Absent key, no spawn.

libkwl: kwl_layer_opts gains margins
(zwlr_layer_surface_v1.set_margin), which corner toasts need.

kbar: the right side gains the kernel's monotonic uptime next to the
clock, in the theme's muted colour. The kernel exposes no
/proc/meminfo, so there is no memory module — real stats or none.

Both hosts: theme smoke asserts the notify hook fires on a switch
(Node); the Playwright gate asserts the toast maps and dismisses
after the theme cycle. All 10 wlcompositor/wlterm Node suites and the
omarchy/hyprland/wayland browser gates pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The browser input bridge translated keys purely by physical position
(KeyboardEvent.code), and the kernel-side xkb keymap is a fixed US
layout — so a non-US physical layout delivered the letter printed at
the US position of the pressed key. On AZERTY the key labeled W sits
at the QWERTY-Z position: Ctrl+W arrived at the compositor as Ctrl+Z
and the killactive bind never fired, and every typed letter in wlterm
or the launcher came out QWERTY-scrambled.

charToKey maps a single typed ASCII letter to its US-layout keycode
and wins over the positional path; everything else (digits,
punctuation, dead keys, modifier-altered characters) keeps
KeyboardEvent.code, because their key value depends on Shift and the
layout's unshifted symbols. The Framebuffer pane's own translation
stays positional on purpose — game movement keys are positional.

Browser-only layer: Node has no keyboard capture (tests inject evdev
codes directly), so there is no Node-side counterpart to change.

Unit tests cover the AZERTY Ctrl+W pair and the non-letter fallback;
verified live against /?demo=omarchy with synthesized code=KeyZ/key=w
events (window closes), and the omarchy/hyprland/wayland/sdl2 browser
gates pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Tier 2 groundwork for unmodified foot (plan §4 PR19, server side).
wlcompositor now advertises wp_presentation v1: clock_id is
CLOCK_MONOTONIC — the clock kernel_vblank stamps page-flip events
with — and each flip resolves the pending feedbacks with presented
(flip timestamp, refresh interval from the mode's vrefresh, flip
sequence, VSYNC flag) for visible surfaces and discarded for hidden
ones. The SetCrtc first frame has no flip event, so it stamps itself
with clock_gettime. Feedback resources queued on a dying surface are
discarded before the free so their destructors don't dangle.

The embedded xkb keymap gains F1-F12 and the nav cluster
(Home/End/PgUp/PgDn/Insert/Delete) — foot and full-screen terminal
apps send them; the map previously stopped at arrows.

wlclient-test binds the global at registry time (clock_id arrives on
the bind roundtrip), requests feedback on its mapping commit, and now
compiles the received keymap with libxkbcommon and probes F1 + Delete
by keycode. The smoke gate asserts PRESENTATION_CLOCK id=1, a
PRESENTED line, no discard, and KEYMAP_SYMS f1=1 delete=1. All 14
wayland Node suites green serially.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
First rungs of the Tier 2 font stack (plan §4 PR19): fcft composites
glyphs through pixman and segments graphemes through utf8proc;
freetype/fontconfig land next on top of them.

pixman is the last autotools release (0.43+ is meson-only, and the
flake carries no meson) cross-built like libpng with every SIMD
backend disabled — wasm32 has none, the generic C paths are complete.
Library-only build: the test/ and demo/ trees want a runnable host.
utf8proc is one freestanding TU compiled directly, no upstream
Makefile involved.

build-programs.sh resolves both and symlinks archive + headers into
the sysroot for the smoke drivers (pixman's headers flat because
pixman.h angle-includes pixman-version.h and build_program adds no
-I). pixman_smoke asserts an a8r8g8b8 fill and a solid OP_OVER
composite pixel-exactly; utf8proc_smoke asserts NFC composition,
tolower, and grapheme breaks. Both green under the kernel.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… stack

Completes the PR19 library ladder (plan §4): fontconfig resolves a
family, freetype opens the TTF, fcft rasterizes into pixman — the path
foot takes for every cell.

freetype 2.13.3 is a plain autotools cross-build on the zlib dep; the
optional deps (png, harfbuzz, brotli, bzip2) stay off. fontconfig
2.15.0 builds against libxml2 instead of expat (already ported) and
needs three kinds of host-probe correction: glibc/BSD-only functions
(random_r/initstate_r, getprogname, getexecname) forced off, and
fstatfs off because fcstat's f_type branch is gated on __linux__,
which this toolchain does not define. Its fcobjshash.h is generated at
build time — the tarball ships only the .gperf input, so gperf joins
flake.nix and [[host_tools]]. Runtime paths bake to /usr/share/fonts,
/etc/fonts, and /tmp/fontconfig (the only world-writable dir).

tllist 1.1.0 is a header-only stage. fcft 3.1.9 is meson-only
upstream, so it gets the libxkbcommon-style bypass: the three upstream
header generators run (version, unicode precompose, emoji data), then
two TUs compile directly — no harfbuzz (shaping off, and with it
utf8proc's only fcft hooks) and no SVG backend.

fontstack_smoke stages fonts.conf + the in-tree Inconsolata TTF via
FONTCONFIG_FILE, resolves "monospace" through an alias, and asserts a
rasterized 'A' has nonzero ink. Green under the kernel alongside the
pixman/utf8proc smokes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
foot's SIGCHLD reaper blocks the signal everywhere except inside
epoll_pwait's atomic sigmask window — and never saw it: the wait hung
until its timeout with the handler unrun, so a dead shell was never
reaped (isolated by a C probe running exactly that pattern).

Two composed gaps. sys_epoll_pwait swapped the mask around each
attempt and restored it before parking on EAGAIN, so while parked the
process's mask claimed the signal was blocked and sendSignalToProcess
skipped the wake; it now uses the same sigsuspend-saved-mask pattern
as sys_ppoll/sys_pselect6 (swap stays active across retries,
kernel_dequeue_signal restores after delivering the signal that ended
the wait). And the worker runtime converts epoll_pwait into
timeout=0 poll attempts, which skip sys_poll's pending-signal check
entirely — handleEpollPwait now swaps the mask through two new
additive kernel exports (kernel_swap_poll_sigmask /
kernel_restore_poll_sigmask, snapshot regenerated, no ABI bump) and
ends the wait with EINTR when the per-attempt dequeue delivered a
signal and no fd events were ready.

Covered by a kernel unit test (pending-but-masked SIGCHLD +
unblocking sigmask → EAGAIN park keeps the swap, retry EINTRs);
1114 cargo tests and the full host vitest suite green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three small holes real Wayland apps walk into. The overlay's
input-event-codes.h stopped at BTN_EXTRA — foot's mouse-button mapping
uses BTN_FORWARD/BACK/TASK. The wasm bits/fenv.h defined no exception
flag names, so foot's box-drawing guard (fetestexcept(FE_INVALID|...))
failed to compile; they exist now as zeros — no flag can ever be
raised on wasm's non-trapping FP, and fetestexcept(0) is always 0.
And the rootfs gains /usr/share/X11/xkb: upstream xkbcommon's
xkb_context_new(NO_FLAGS) returns NULL outright when its default
include root is missing, which every stock client hits before it can
compile the compositor's keymap string. Empty is fine — nothing reads
xkb data from disk.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
data-device stub, seat v5, wl_surface.enter

foot refuses to start without wl_subcompositor and
wl_data_device_manager, requires wl_seat v5, and sizes its fonts only
after wl_surface.enter — none of which the compositor spoke. All four
land, plus a real bug foot exposed.

wl_subcompositor: thin parent-glued overlays. A subsurface never
tiles, focuses, or hit-tests; it maps with its buffer, rides its
parent's position (recomputed at composite time — tiling moves the
parent under it), and draws right above the parent in both the CPU
and GL paths. Destroy handles all three orders (subsurface first,
wl_surface first, parent first). Mapped-subsurface pixel output has
no direct gate yet — foot only maps them in its URL/search overlays,
which the browser gate will drive.

wl_data_device_manager v3: inert — accepts sources and selections,
transfers nothing. Real clipboard data paths are the O2 tier's work.

wl_seat v5: name event, keyboard repeat_info(25, 400), and a pointer
frame event closing every enter/leave/motion/button burst.

wl_surface.enter: bound wl_output resources are tracked per client
and the matching enter is sent when a surface maps. The output also
advertises scale(1) + done, and reports physical size 0x0 (unknown):
it previously sent pixels as millimetres, and foot derived DPI 25.4
from the fake 86-inch panel and reloaded its fonts at a garbage size.

Gated by foot-smoke plus the 14 wayland Node suites and the full
host vitest run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Tier 2 PR19 gate (plan §4): stock foot runs under wlcompositor on
the ported font stack, with two declared patches at the platform's
compatibility boundaries.

Meson-bypass build like fcft: the two upstream generators run
(version.h, builtin terminfo via python — no tic), wayland-scanner
generates client glue for the eleven protocols foot compiles against
(eight XMLs newly vendored at the wayland-protocols 1.45 pin, and the
package's inputs list now names every XML so edits invalidate the
staged source), then 44 TUs compile directly. Fork instrumentation is
mandatory — foot forks its shell.

Patch 0001 (shm): a memfd MAP_SHARED mapping only writes back on
msync/munmap under this kernel, so a memfd wl_shm pool never carries
live pixels to the compositor. Pools become renderD128 dumb-bos
passed by prime-fd — the same contract every in-tree client uses —
one bo per pool, one buffer per pool, offset 0, SHM scrolling off.
Patch 0002 (fonts): concurrent FcFontMatch calls race pattern
refcounts under this kernel's thread model and return garbled sizes;
the four font variants load serially.

host/test/foot-smoke.test.ts is the gate: foot connects via stock
wl_display_connect (XDG_RUNTIME_DIR=/tmp), resolves "monospace"
through a staged fonts.conf + the in-tree Inconsolata, composites
onto card0 through the gbm pools (COMPOSITE_SAMPLE), tiles under
dwindle, takes host-injected keys spelling "exit", and both foot and
the compositor exit 0. Green in 2.3s; clean-cache resolver build
verified.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…auncher entry, run.sh wiring

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… static closure trampolines

The Tier 3 PR20 gate (plan §4): real ffi_call (i64, floats, by-value
structs) plus ffi_closure, replacing the Wayland-scoped shim. wasm32
cannot JIT trampolines, so both combinatorial halves are generated at
build time by gen-dispatch.sh (bash + awk): the ffi_call signature
dispatch (every arity <= 8 with at most two non-i32 args, all-i32 to
the Wayland ceiling of 22, times five return classes — 4435 keys) and
a static closure trampoline pool (810 classes / 3240 slots baked into
the function table).

ffi_core.c encodes the clang wasm32 lowering verified by probe:
singleton structs travel as their scalar; every other by-value struct
is a pointer to a caller-owned copy (the callee may clobber it, so
ffi_call passes scratch copies); struct returns are a hidden leading
sret pointer; i8/i16 travel caller-extended. Pointers classify by
word size so the same core is correct on the native test leg. ffi_cif
gained no fields: libwayland embeds it by value, so cached consumer
archives stay ABI-compatible and the signature key is recomputed per
call. Un-emitted signatures abort loudly printing their key.

The dispatch is split into one leaf per (return class, arity), routed
through a function-pointer table on the key's low byte. Fused into
one function it is a ~275 KB sparse uint64 switch that kills the
whole process with "Fatal process out of memory: Zone" when V8's
optimizing tier compiles it (the foot smoke hit this); the clang
driver runs wasm-opt -O2 post-link and binaryen re-inlines
single-caller functions of any size, so only indirect-call routing
holds the split. Core + dispatch and the closure pool are separate
archive members: ffi_call-only consumers (libwayland) don't pull the
pool.

Gates: host/test/libffi-full-unit.test.ts — the 263-case matrix
native and wasm-under-kernel (arities x types x call/closure, byval
clobber isolation, ffi_arg widening, pool reuse) plus a function-size
bound that fails if the dispatch ever fuses again;
host/test/libffi-shim-unit.test.ts keeps the PR1 Wayland arity gate
green through the rewrite. Full checklist green: cargo, vitest (two
known stale-ABI failures), libc-test, Open POSIX, ABI snapshot
(userspace-only — no bump).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…yped callback thunks

The Tier 2 PR21 gate (plan §4): glib as four static archives
(libglib, libgmodule, libgobject, libgio) for wasm32, meson bypass
against a hand-curated config.h + glibconfig.h (libxkbcommon
pattern). gobject rides the PR20 libffi port for its generic closure
marshaller. gio drops the gdbus object graph until PR22 lands a
session dbus daemon: a giomodule patch guards the dbus/portal
built-in type registrations behind GIO_DBUS_BUILTIN_MODULES, and the
four portal entry points referenced behind glib_should_use_portal()
become G_IO_ERROR_NOT_SUPPORTED stubs. gregex stays out (needs
pcre2).

The port's key discovery: wasm call_indirect checks exact signatures,
and glib leans on arity-changing callback casts that every native ABI
tolerates — each one traps at runtime with "null function or function
signature mismatch". wasm-callback-signatures.patch routes them
through typed thunks: free_full's GDestroyNotify-as-GFunc, compare
funcs stored as GCompareDataFunc, the G_DEFINE_TYPE / G_DEFINE_INTERFACE
/ G_DEFINE_DYNAMIC_TYPE macros (patched to generate matching
*_intern_* wrappers, so every macro user — all of gio, GTK later —
is fixed at compile time), G_IMPLEMENT_INTERFACE via a new exported
g_wasm_iface_init_thunk, pspec instance_init via type qdata, and
gcancellable's connect-data trampoline. The pattern is documented in
docs/porting-guide.md for the next GObject port.

config.h turns off what the kernel cannot back or must not take:
HAVE_POSIX_SPAWN (gspawn must fork/exec so fork instrumentation
applies), HAVE_EVENTFD (GWakeup on pipe2), HAVE_FUTEX (GMutex on
musl pthreads), inotify, xattr, splice. USE_SYSTEM_PRINTF and
ENABLE_NLS are load-bearing: without them g_*printf and glibintl
route to un-compiled fallbacks that -Wl,--allow-undefined silently
converts into trapping null table entries.

Gate: programs/glib_smoke_test.c + host/test/glib-smoke.test.ts —
GMainLoop idle/timeout/unix-fd dispatch, GObject type + property +
signal through the libffi generic marshaller, g_spawn_sync and
g_spawn_async with child watch (the binary re-execs itself at
/bin/glib_smoke_test). Full checklist green: cargo, vitest (two
known stale-ABI failures), libc-test, Open POSIX, ABI snapshot
(userspace-only — no bump).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…AL auth over SO_PEERCRED

The Tier 2 PR22 gate (plan §4): a session dbus stack for wasm32 —
expat 2.8.3 (the daemon's config-parser dependency), dbus 1.14.10
(dbus-daemon, dbus-send, dbus-monitor; the last autotools series —
1.16 is meson/cmake only), and glib rev2 adding the 27 upstream
gdbus_sources TUs to gio. GApplication, the dbus-backed built-in
modules, and the portal backends stay out; GIO_DBUS_BUILTIN_MODULES
stays undefined until a port needs one.

The port's key discovery: the SDK links with -Wl,--allow-undefined,
so every AC_CHECK_FUNCS link test "succeeds", including for functions
the sysroot lacks. Absent functions must be forced off with
ac_cv_func_<name>=no, or guarded includes break the build (dbus's
getpeerucred pulls Solaris ucred.h) or trap at runtime as null table
entries. The hazard is documented in docs/porting-guide.md.

Auth rides a new third glib patch, wasm-credentials.patch: a
__wasm32__ branch in gio/gcredentialsprivate.h mirroring the Linux
ucred block. The kernel backs SO_PEERCRED and sendmsg drops
non-SCM_RIGHTS cmsgs, so GCredentials follows the Linux contract;
without the branch GCredentials is unsupported, gdbus EXTERNAL auth
claims uid -1, and dbus-daemon rejects the handshake.

Gate: programs/glib_gdbus_smoke.c + host/test/dbus-smoke.test.ts —
dbus-daemon --version, a dash-orchestrated session bus answering a
dbus-send GetId round trip, and a two-process gdbus ping +
notify-send-shaped Notify round trip with dbus-monitor eavesdropping
the bus. Full checklist green: cargo, vitest (two known stale-ABI
failures), libc-test, Open POSIX, ABI snapshot (userspace-only — no
bump).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…the kernel

The Tier 2 PR23 render stack (plan §4), four ports on the PR19 font
stack and the PR21 glib: harfbuzz 10.1.0 (upstream is meson-only, but
the dist tarball ships a single-TU amalgam built for embedding — one
wasm32posix-c++ invocation with the freetype and glib backends, first
C++ library port, links libc++), fribidi 1.0.16 (plain autoconf),
cairo 1.16.0 (last autotools release; image surfaces, ft/fc fonts,
png — no GL, no X, no vector backends), and pango 1.42.4 (last
autotools release; pango/pangoft2/pangocairo, deps probed through
PKG_CONFIG_PATH over the resolved prefixes).

glib grows two consumer-facing shims pango's configure expects: a
gthread-2.0.pc (threading lives in libglib since 2.32; upstream still
ships the .pc) and glib-mkenums installed into bin/ with a
glib_mkenums pc variable (pango generates its enum types with it).
cairo's png probe ignores png_CFLAGS/png_LIBS and asks pkg-config, so
the build pins png_REQUIRES=libpng16 with PKG_CONFIG_PATH at the
resolved libpng.

Gate: programs/pango_cairo_smoke.c + host/test/pango-cairo-smoke.test.ts
— fontconfig resolves "monospace" from a staged fonts.conf,
pango itemizes and shapes "kandelo" through harfbuzz, pangocairo
renders into an ARGB32 image surface, and the test asserts layout
extents, inked pixels, and the exact FNV-1a pixel hash (byte-stable
under the pinned stack; verified across two runs). Full checklist
green: cargo, vitest (two known stale-ABI failures), libc-test, Open
POSIX, ABI snapshot (userspace-only — no bump).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two link defects the GTK3 port surfaced.

lld's default shadow stack is 64 KiB and sits directly above the data
segment, so a frame larger than what is left of it writes into libc's
BSS instead of trapping. gdk-pixbuf's load_from_stream keeps a 64 KiB
buffer on the stack; the overrun showed up as mallocng ctx.active
corruption far from the call. Every link line now asks for the 8 MiB
Linux main-thread default: sdk/src/lib/flags.ts for SDK links, and the
in-tree link flag lists that do not go through the SDK
(scripts/build-programs.sh, the six test-suite runners,
examples/dlopen, lsof).

wasm32posix-cc re-ordered its link inputs, emitting sources, then
objects, then archives, whatever order the caller wrote. Link-input
order is semantic for wasm-ld: an object placed before an archive gets
a signature-mismatch stub on an unprototyped call, while the reverse
order binds the mismatched definition and emits invalid wasm. parseArgs
now also records every bucketed argument in its original position
(orderedArgs) and cc.ts emits that.

Gate: sdk/test/cc.test.ts asserts the stack-size flag reaches the link
line and that `foo.o -lz bar.o` keeps its order.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The original commit made kernel_sendmsg gather and kernel_recvmsg
scatter every iovec, and added the signalfd4/signalfd sigset arg
descriptors. This base supersedes both: the canonical message wire
flattens the iovec array before the kernel sees it
(validate_canonical_message_iov_len admits only 0 or the flattened
count), and host_abi.rs already carries the signalfd descriptors.

The regression gate is kept: programs/sendmsg-iovec-gather.c +
host/test/sendmsg-iovec-gather.test.ts drive the sd-bus
empty-leading-iovec auth shape, a three-iovec gather, a two-iovec
scatter, and an empty leading receive iovec that must not read as EOF.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A blocking syscall the kernel parks returns EAGAIN, not EINTR, and the
host re-parks the process. A Handler signal dequeued on that path had
its delivery info written into a channel nobody reads: the signal was
consumed, its sa_mask stayed applied, and every later instance of it was
blocked for good. Waybar sits in poll() and read(), so the theme
switch's SIGUSR2 died in the park and the bar never reloaded.

The host now leaves the signal queued while it re-parks, and instead
ends the park when one is deliverable: it dequeues the signal and
completes the syscall with EINTR.

Whether the guest sees that EINTR follows the handler's flags. Only
poll and select consult them in the kernel (SignalState::should_restart,
which returns EINTR rather than parking); every other syscall parks
without reading them, so the park itself carries no intent. The host
reads sa_flags out of CH_SIG_FLAGS and sets the new channel::SIG_RESTART
byte for an SA_RESTART handler; the glue re-issues the syscall after the
handler returns and returns the EINTR otherwise. Restarting
unconditionally hangs the standard shutdown loop, where the handler sets
a flag and the EINTR is what lets the loop check it.

channel_syscall.c splits into __do_syscall_once plus a loop, and
__deliver_pending_signal reports whether to re-issue. SIG_RESTART sits
at SIG_BASE + 44 inside the 48-byte delivery area the host already
clears, so no existing offset moves.

Gate: programs/signal-no-restart.c +
host/test/signal-no-restart.test.ts — a handler with sa_flags = 0
interrupts a read() that nothing will ever satisfy. Replacing the flag
read with a constant true makes the program hang. programs/signal-to-
threaded.c covers the restart side, which is what signal() installs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

# Conflicts:
#	crates/shared/src/lib.rs
#	docs/architecture.md
#	host/src/generated/abi.ts
#	host/src/kernel-worker.ts
#	libc/glue/channel_syscall.c
#	tools/xtask/src/dump_abi.rs
…tted

Two defects in the kernel worker's blocking-syscall bookkeeping, both
found bringing the omarchy desktop up.

A poll or ppoll that outlives its first pass is completed later from a
host timer, without running the syscall again, and that completion went
through completeChannel's argDescs path. pollfd[] is an inout buffer, so
that path copies the kernel's scratch region back over the caller's
array — and the scratch is shared by every process, holding whatever
syscall ran last by the time the timer fires. dbus-daemon's 30-second
auth timeout came back carrying another process's stat buffer; the
daemon then polled a descriptor it had never opened and spun on "invalid
request, socket fd 12 not open" with every bus client unserved. The new
completePollTimeout writes the POSIX result of a timeout directly:
return 0 and clear every revents.

Three loops walked pendingPollRetries / pendingSelectRetries live. A
poll that is still not ready re-registers inside retrySyscall, and a Map
iterator visits an entry re-inserted after its key was deleted, so the
walk never terminates — the kernel worker spins and every process on the
machine stops. Waybar's signal thread sits in poll, so the SIGCHLD from
wordexp's /bin/sh froze the whole desktop. All three now iterate a
snapshot.

Gate: programs/poll-timeout-preserves-pollfds.c — a child runs stat() in
a loop to keep foreign data in the scratch while the parent's poll times
out. programs/p_10_fork_with_polling_thread.c, wired into the
fork-instrument coverage matrix as P-10, reproduces the SIGCHLD spin.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Clang lays out a callee's frame from a stack pointer it assumes is
16-byte aligned. musl's pthread_create aligns the new thread's stack to
sizeof(uintptr_t) — 4 here — and then subtracts struct start_args, so
the value it hands to clone is routinely 4 mod 8. On such a thread the
caller of a variadic function stores a 64-bit argument four bytes away
from where va_arg reads it.

Waybar exposed it: its GDBus client runs on a thread, and GIO builds the
EXTERNAL auth identity with g_strdup_printf("%lli", (gint64) uid). It
printed the high half, so every process claimed uid 0 and dbus-daemon
rejected the handshake. The worker now rounds the pointer down, which
stays inside the thread's own stack region.

Gate: programs/thread-vararg-alignment.c +
host/test/thread-vararg-alignment.test.ts — printf("%lld", 1000) from
the main thread and from a pthread.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Tier 2 PR24 base (plan §4): five new ports over the PR23 render
stack — pcre2 10.44, gdk-pixbuf 2.36.12, atk 2.36.0, libepoxy 1.5.4,
and gtk3 3.24.34 with its wayland backend.

glib rev3 grows what a toolkit needs: gregex.c against pcre2 (glibmm
registers the GRegex error domain at init), gmodule-deprecated.c, the
GApplication / GAction / GMenu / GNotification TUs in gio, and
glib-genmarshal plus gmodule-no-export-2.0.pc and gio-unix-2.0.pc for
consumers that probe those names. cairo rev3 turns on cairo-gobject
and the pdf/ps/svg surfaces, which GTK3 hard-requires. cairo and pango
each take a wasm-callback-arity patch, routing arity-changing casts
(cairo_spline_add_point_func_t, GFunc) through typed wrappers — a
native ABI tolerates the extra argument, wasm's call_indirect traps on
it. libwayland now also builds and ships libwayland-egl.a, so a package
build that links an executable no longer depends on
scripts/build-programs.sh having run.

The wasm32 sysroot has no dynamic loader, so gdk-pixbuf's loaders and
GTK3's immodules are built in. gdk's wl_shm pools come from libgbm
dumb BOs (wayland-shm-gbm-pool.patch) so the prime-fd import path the
compositor already speaks carries them. glib-compile-resources and
glib-compile-schemas are compiled C host tools, so they come from the
nix host glib in flake.nix, alongside xorg util-macros for libepoxy's
autoreconf.

Gate: programs/gtk3_smoke.c + host/test/gtk3-smoke.test.ts — an
unmodified GTK3 window with a label, through gdk-wayland, pango
shaping and a cairo wl_shm render on wlcompositor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Twelve ports finishing PR24's client side: fmt 11.2.0, jsoncpp 1.9.6,
spdlog 1.15.3, libsigc++ 2.10.3, the gtkmm binding chain (glibmm
2.62.0, cairomm 1.12.2, pangomm 2.42.0, atkmm 2.28.0, gtkmm3 3.24.2),
gtk-layer-shell 0.9.2, libxkbregistry 1.7.0, and Waybar 0.14.0 itself.

Waybar is built unmodified. It links one added TU,
src/glib-static-init.c: glib and gobject each initialise from a
plain constructor, and upstream the dynamic loader guarantees glib's
runs first. A fully static link runs them in link order, so
gobject_init reaches g_quark_from_static_string before glib_init has
allocated the quark table and g_quark_init's assertion aborts the
process. A priority-101 constructor calling the idempotent glib_init
sorts it.

The musl overlay's linux/input-event-codes.h gains BTN_STYLUS,
BTN_STYLUS2 and BTN_STYLUS3, which gdk's tablet code needs to compile.

Gate: host/test/waybar-smoke.test.ts — the bar maps a layer-shell
surface on wlcompositor and reloads its stylesheet on SIGUSR2.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The PR24 notification stack: basu 0.2.1 (sd-bus lifted out of systemd)
and mako 1.10.0, upstream's Wayland notification daemon, owning
org.freedesktop.Notifications on the dbus session bus and drawing on
wlr-layer-shell.

Three patches carry mako: its pool buffer allocates through a gbm
dumb BO and hands the compositor the prime-fd (the same import path
every other client uses); parse_boolean is renamed off a musl symbol
clash; and its wl_registry listeners get typed no-op entries, since a
missing entry is a null call_indirect on wasm.

programs/notify-send.c is the sender the desktop calls — one
org.freedesktop.Notifications.Notify over the session bus, printing
the assigned id. glib_gdbus_smoke grows a --notify mode that does the
same against whichever daemon owns the name.

Gate: host/test/mako-smoke.test.ts — dbus-daemon, mako on the
compositor, a Notify from the sender, and makoctl dismissing it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… IPC

What an unmodified GTK3 bar asks wlcompositor for.

Three new globals. zxdg_output_manager_v1 v3 answers the logical
geometry of the single fullscreen output — position (0,0), size = the
mode, name "virtual-0" — as a one-shot burst, since it never changes.
wp_viewporter carries a per-surface crop and scale: the committed size
follows the protocol's destination-then-source-then-buffer rule, the
CPU blit nearest-samples the source rect across the destination box,
and the GL path passes the rect as a uv uniform.
wp_fractional_scale_manager_v1 reports a fixed 120/120ths.
wl_output moves to v4 so it can send name and description, which mako
binds unconditionally.

wl_shm pools accept a buffer at any offset — GTK packs several into
one pool — so the import covers every row up to the buffer's end and
the base pointer walks to its own first row. A buffer that starts
partway into a pool has no GL texture of its own; the GL path now
skips it instead of tearing down the EGL session, which is what
libwayland-cursor's packed theme used to do to the whole desktop.

The control socket gains a second face: $XDG_RUNTIME_DIR/hypr/
wlcompositor/.socket.sock and .socket2.sock, where Waybar's hyprland
modules look. Requests answer in hyprctl -j shapes (clients,
workspaces, activeworkspace, monitors), the j/ prefix is accepted and
ignored, socket2 streams events from its first byte, and the event
names carry their v2 variants. Titles reach the bar: xdg_toplevel
set_title is recorded and JSON-escaped, since it is client-controlled
text.

A layer surface announces its LAYER marker when it maps, not when it
is configured — a strip reserves nothing until then, and announcing
early put it ahead of the windows re-tiling under it. A window that
maps while the launcher holds the keyboard no longer steals it.

Gate: host/test/wlcompositor-protocols-smoke.test.ts for the three
globals; the kwlctl smoke covers both Hyprland sockets.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The browser demo's shell is no longer ours. kbar and knotify are
replaced by upstream Waybar on wlr-layer-shell and upstream mako on
the dbus session bus, and the demo stages what they read: a Waybar
config.jsonc and style.css translated from Omarchy's, a mako config, a
session bus config with EXTERNAL auth, and the six themes' real
Omarchy background images rendered to KWLP at staging time (the
aurora spec stays as the fallback when a fetch or decode fails).

The theme switch is Omarchy's shape. The compositor's `notify =` hook
is a bash script that reads the new theme.conf, writes the bar's
stylesheet from the same template the boot-time file comes from,
signals every waybar it finds in /proc with SIGUSR2, then execs
notify-send. Waybar reloads in place and mako draws the toast.

dbus-daemon and mako start once both the display and bus sockets
exist, and Waybar once the display, bus and Hyprland sockets do:
each connects exactly once, with no retry.

run.sh learns dbus, mako and waybar targets and adds them to the
browser dependency set.

Gate: apps/browser-demos/test/kandelo-omarchy.spec.ts — the bar as a
layer surface with its IPC listener attached, windows tiling under the
height it actually reserved, the desktop still on the GPU presenter
after boot, a theme switch reaching the bar and the notification
daemon, and a workspace switch arriving as Waybar's own IPC log line.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The session handoffs written while the work in this branch was done,
kept in docs/plans alongside the numbered series already there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
mho22 and others added 11 commits August 20, 2026 14:15
kernel_sendmsg and kernel_recvmsg returned EINVAL when msg_iovlen
exceeded IOV_MAX. Linux returns EMSGSIZE. Its __copy_msghdr path in
net/socket.c serves both calls and rejects the array before it reads any
iovec.

The four readv/writev entry points keep EINVAL. POSIX specifies that
errno for an iovcnt above IOV_MAX, so the two families differ on
purpose. Each kernel site now carries that fact in a comment.

IOV_MAX and UIO_MAXIOV are both 1024 in the sysroot, so the existing
bound was already correct and only the errno changes. The change is
semantic: it moves no snapshot offsets and needs no ABI_VERSION bump.

Gate: programs/sendmsg-iovec-gather.c +
host/test/sendmsg-iovec-gather.test.ts gain two cases. An array of
exactly IOV_MAX sends and scatters its payload across the full array.
An array one longer fails with EMSGSIZE on both calls. Each guard is
pinned on its own: restoring EINVAL fails the test with "send over
IOV_MAX errno=22", widening both guards to >= 1024 fails it with "send
at IOV_MAX errno=90", and widening only the recvmsg guard fails it with
"recv at IOV_MAX".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
buildKmsGlPresenter runs on the WebGL2 context the dead compositor was
driving — getContext returns the canvas's existing context — and its
reset list stopped at draw state. Pixel-store state survives too. A
leftover UNPACK_ROW_LENGTH, UNPACK_SKIP_ROWS, UNPACK_SKIP_PIXELS or a
still-bound PIXEL_UNPACK_BUFFER makes every scanout upload
GL_INVALID_OPERATION. WebGL raises no JS exception for it, so
presentKmsGlScanout reports success, slot 7 stays 2 and the commit count
advances while the canvas is black. A leftover UNPACK_FLIP_Y_WEBGL or
UNPACK_PREMULTIPLY_ALPHA_WEBGL uploads cleanly and corrupts the image
instead: the scanout is XRGB8888 with X = 0, so premultiply zeroes the
colour.

All six are guest-reachable and none is restored on its own.
OP_PIXEL_STOREI (host/src/webgl/bridge.ts) forwards any pname while
host/src/webgl/shadow.ts shadows only unpackAlignment and packAlignment,
and OP_BIND_BUFFER accepts any target.

kernel-worker.ts is shared by both hosts, so the fix lands on Node and
the browser at once. Node degrades to stats-only without WebGL2, which
leaves the browser as the only real consumer of this path.

Gate: host/test/dri-kms-stats-sab.test.ts. The fake GL records
pixelStorei and bindBuffer, and the claim/release test asserts all six
unpack defaults plus the buffer unbind on both rebuilds. Each reset is
pinned on its own — deleting UNPACK_ROW_LENGTH fails the test with
"pixelStorei(UNPACK_ROW_LENGTH, 0): expected [] to have a length of 2",
and deleting the bindBuffer or the premultiply reset fails it the same
way under their own labels.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…lease

host/test/dri-kms-stats-sab.test.ts pins the pixel-store resets against a
fake GL: it records the calls buildKmsGlPresenter makes. It cannot see
whether the uploads those calls protect are accepted, and a rejected
texImage2D raises no JS exception — which is the whole failure mode. Only
a real context answers it.

The fixture drives the shipped path, not a copy of it: it builds a
CentralizedKernelWorker in a worker, attaches a transferred OffscreenCanvas
with mode webgl2-scanout, stubs kms.currentFb / kms.scanoutBytes the way
the vitest suite does, and ticks the vblank pump. Between the two ticks it
sets every unpack pname and binds a PIXEL_UNPACK_BUFFER through
markKmsCanvasGlOwned, then releases — the EGL-teardown sequence.

The spec samples the composited page with page.screenshot({clip}). A
readPixels inside the worker cannot serve here: the drawing buffer and the
composited page disagree, which is the defect. locator.screenshot()
returns a stale rasterization.

The scanout is two-tone, and that is load-bearing. A rejected upload
leaves both halves black; a leftover UNPACK_FLIP_Y_WEBGL swaps them; a
leftover UNPACK_PREMULTIPLY_ALPHA_WEBGL zeroes the colour because the
scanout is XRGB8888 with X = 0. A uniform image would report the last two
as passing. Deleting the whole reset block fails the spec with "rebuilt
top half", and so does deleting the flip reset alone.

The fixture lives under apps/browser-demos/test/fixtures/ and is served by
the dev server only — vite.config.ts builds an explicit input list, so it
never enters the static build.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The spec added in a845e83 was in the tree but in no workflow. The COI
job is the only browser job that runs on a bare checkout, and the gate
fits it: no binaries, no kernel wasm, ~2 s.

Its own step rather than the existing three-browser line — the gate needs
WebGL2 on an OffscreenCanvas transferred into a worker, which only the
`chromium` channel provides.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…dows

The demo opened a clock and two terminals for the user. It now opens
nothing. Every client is one the user asks for, through the binds the
compositor already loads from its own config (CTRL+K a clock, CTRL+Return
a terminal) or through the launcher.

The third terminal was also the keep-alive: `spawn` resolves on process
exit, and awaiting it held the boot task open. The compositor is the
process whose lifetime is the desktop's, so the omarchy branch awaits it
instead. `spawnBg` returns its promise for that; the calls that stay
fire-and-forget say so with `void`, as the rest of the file does. The
hyprland demo keeps its three clients — its gates read the tiling itself.

The spec opens the same three clients through the binds before the gates
that need them, so gate 3 onward is unchanged. It also asserts the desktop
is bare first, which the tile counts alone cannot: deleting that assertion
and restoring the wlclock spawn fails it with "the desktop opened a clock
on its own".

Those assertions count `KBD_FOCUS app_id=` markers rather than tiles.
Waybar leaves a second toplevel behind with an empty app_id once its bar
is up, and it takes a tile slot — so a tile count answers "how many
windows exist", not "which windows the demo opened".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Waybar's bar came up with a second window beside it: an empty app_id, no
title, 10x16, that took the keyboard and a tile of its own. It is not a
window at all. It is Waybar's cursor surface — GTK hands it to
wl_pointer.set_cursor, which the compositor accepts and ignores because
the host pointer already draws the sprite.

surface_commit mapped it anyway. The map path ran for every surface that
was neither a subsurface nor a layer surface, so a buffer alone was
enough to become a window. The xdg_toplevel role is what makes a surface
a window; a surface without one now commits its buffer and stops there.
Every in-tree client already takes its role before its first commit —
libkwl at kwl.c:609 and :663, wlclient-test, wldmabuf-test — so nothing
that maps today stops mapping.

The three gates in the omarchy spec that count tiles were each satisfied
by the previous client: the phantom took slot 0, so "TILE n=4 i=3" after
the launcher was already true at gate 3 time, and the same shift carried
into vim and foot. They now prove what they say — making the launcher's
kwlctl exec a no-op fails gate 5 on TILE n=4 i=3, where before it passed.

The new assertion at gate 3 is what keeps the phantom from coming back:
the three windows opened through the binds are the only tiles. Running
the spec against the pre-fix binary fails it with "a surface with no
window role took a tile".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Waybar-stack recipes declared libcxx@21, the version string the
package carried on the old base. This base pins libcxx to the exact
LLVM version from flake.nix (21.1.7) and the resolver refuses a
projection whose dependency pins drift from the registry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
wl_cursor_theme_load backs its theme pool with memfd_create +
mmap(MAP_SHARED). The host's shared-mmap preflight rejected any
regular file without a persistent host capability with ENOTSUP, so
every GTK wayland client died in gdk's cursor-theme assert
(_gdk_wayland_display_get_scaled_cursor_theme) before first draw.

MAP_SHARED on non-regular files already falls back to fd-pread
population without a writeback tracker entry. A kernel-backed regular
file (memfd, synthetic /etc) now takes the same fallback: the pages
fill at map time and writes stay local. Nothing reads a cursor pool
back — the compositor accepts and ignores wl_pointer.set_cursor. Live
coherence still needs a kernel-owned mapping bridge.

The projection regen rides along (host/src is a build input of the
package images).

Gate: host/test/gtk3-smoke.test.ts (was aborting before draw) and the
updated contract case in host/test/file-shared-memory.test.ts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The test forced main-thread mode with a raw NodePlatformIO, where the
kernel's authoritative spawn-target stat reaches the real host
filesystem and /bin/glib_smoke_test cannot exist. Worker mode bakes
execPrograms into the test rootfs, the same staging the dbus and
dash-orchestrated smokes already use.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The bundle bakes in the generated ABI surface. The epoll mask-swap
commit added two kernel exports without regenerating it, so the
bundled resolver rejected every freshly built kernel.wasm ("Binary
exists but was rejected by artifact policy") and the libc/posix suite
runners died at kernel resolution. scripts/test-resolve-binary-bundle.sh
is the CI gate for this staleness.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant