Skip to content

Wayland on Kandelo - Fix desktop pointer-grab release routing, munmap page rounding, and browser reboot sizing - #948

Open
mho22 wants to merge 13 commits into
explore-dri-sdl2from
explore-dri-wayland
Open

mho22 wants to merge 13 commits into
explore-dri-sdl2from
explore-dri-wayland

Conversation

@mho22

@mho22 mho22 commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

Why

Kandelo could scan out pixels and read input, but had no display server. This PR ports the Wayland libraries to the wasm32 sysroot and builds wlcompositor, a PID-2 Wayland server that composites real clients onto the KMS surface.

Third layer of the six-PR DRI stack (#698#709#948#976#1318#1241). It stacks on explore-dri-sdl2.

What changed

The Wayland library stack

The compositor and its clients

  • wlcompositor, a PID-2 Wayland server: wl_compositor, wl_shm, xdg-shell, wl_seat, and CPU or GPU compositing onto /dev/dri/card0.
  • wpkdraw, libkwl and the wlterm terminal.
  • A three-client desktop with GPU compositing.

The fixes a running desktop exposed

Driving the desktop as a real user surfaced defects that marker-based gates passed straight over:

  • Implicit pointer grab. The compositor re-derived pointer focus on every motion, so a button release went to whatever surface sat under the cursor rather than the surface that saw the press. A drag released off the window left the client stuck mid-stroke. Focus now stays pinned to the pressed surface while any button is down; click-to-focus fires only on the first press. libkwl forwards every release to the app.
  • munmap page rounding. sys_munmap ran its fb0/DRI coverage sweeps against the raw length while MemoryManager::munmap rounds to the 64 K page, so a map(aligned) / unmap(raw bo size) pair freed pages the host kept mirroring. The length is now rounded up front, with checked_add closing the overflow edge.
  • Surface lifetime. The compositor tracked only mapped, z-ordered surfaces, so destroying the buffer of a never-mapped surface was a use-after-free. Every live surface is tracked now, and a failed eglSwapBuffers degrades to CPU compositing instead of freezing the canvas.
  • Browser reboot sizing. detachKernel() never cleared the per-CRTC display-size map, and the pane's ResizeObserver kept feeding the dead kernel, so a second boot in the same tab letterboxed at 1920×1080. The map is cleared on detach and the stale observer disconnects.

Host-side robustness that rides along: the imported-bo coherence copy moves out of the blocked-poll retry path, inherited GL state is sanitized before a presenter rebuild, the presenter rebuilds when a remounted pane brings a new canvas, the guest's UNPACK_ROW_LENGTH is restored, a binding's foreign textures are deleted on unbind, and the scanout shader is raised to highp for modes above 2048 px.

Docs

docs/plans/2026-07-14-build-hyprland-class-compositor-plan.md — the roadmap #976 stacks on.

Both hosts

The kernel and host/src/{kernel-worker,webgl/*}.ts changes are shared code and take effect on Node and the browser together. The browser-only surface is the kandelo-session reboot fix and the Playwright GPU-compositing gate; the GPU path exists only in the browser, and the Node smokes assert the inverse (WLC_RENDERER cpu). Coverage lands on the side it exercises: wldesktop-smoke (Node) gains toolbar-routing and two off-window grab-release gates, kandelo-wayland.spec (Chromium) asserts the GPU-path glReadPixels COMPOSITE_SAMPLE is present and non-black, and kandelo-session gets two unit tests for the detach/reboot display-size lifecycle. The kernel changes carry two unit tests.

Deliberately left as is

With reasons in the commit bodies: the guest sees EIO for all host bind-foreign-texture failures; select/pselect completions have no import-coherence sync, since there is no in-tree consumer; and the single-sample bilinear degrade is kept pending a follow-up consecutive-overrun counter.

Rebased and rebuilt

This branch sits on explore-dri-sdl2 at 42e3a4abc. The history was rebuilt from 15 commits to 11; two test commits added since bring it to 13.

Against the previous tip, the only files that differ outside those main itself moved are 17, and every change in them is a comment — including one in crates/kernel/src/syscalls.rs, where a test comment cited a plan label (the PR6 crash) that no longer resolves. No code changed.

The earlier description of this PR covered only its tip commit and was marked draft pending the series merge order. That order is now settled: #1318 sits between #976 and #1241.

Verification

Run at the pre-rebase stack tip ab7ab2b63, which contained the 11 commits here at the time:

  • cargo test -p kandelo --target aarch64-apple-darwin --lib1665 passed, 0 failed
  • bash scripts/check-abi-version.shexit 0
  • scripts/run-libc-tests.sh302 passed, 20 XFAIL, 0 build failures, and 1 unexpected failure: regression/raise-race. That test forks 100 children inside a signal handler; 5 waits return ECHILD, reproducibly. scripts/run-libc-tests.sh:163 skips it whenever CI=true, so it does not gate this PR, and whether it regressed or predates the stack is not yet established.
  • scripts/run-posix-tests.sh174 passed, 0 FAIL, 3 XFAIL, 2 SKIP

Both suites need WASM_POSIX_RESOLUTION_POLICY=source-only-v1 and WASM_POSIX_SOURCE_ONLY_BINARY_ROOT=<repo>/local-binaries/source-only-v1. binaries-abi-v44/index.toml does not exist until the toolkit tier merges, so the binaries/ provenance tier cannot be materialized locally. The full host vitest suite needs both provenance tiers resolvable at once, which only CI's prepared workspace provides — it is the gate for that suite and for the browser demos.

The stack has since been rebased onto main at 888e628d5. Two gates were re-run at the new stack tip ccc7c6214 (explore-dri-omarchy): bash scripts/check-abi-version.shexit 0, snapshot in sync and the 43 → 44 bump consistent; scripts/ci-run-test-suite.sh vitest exact-abi-source3049 passed, 0 failed, with two files failing to collect on paths #1321 moved to crates/runtime-core/. That collection failure is a main defect and #1337 fixes it.

@mho22 mho22 changed the title Harden the Wayland desktop (PR8 audit): implicit pointer grabs, munmap page rounding, compositor UAF + present fallback, reboot display sizing wayland: fix desktop pointer-grab release routing, munmap page rounding, and browser reboot sizing Jul 14, 2026
@mho22
mho22 changed the base branch from main to explore-dri-sdl2 July 14, 2026 20:07
@mho22 mho22 changed the title wayland: fix desktop pointer-grab release routing, munmap page rounding, and browser reboot sizing Wayland on Kandelo - Fix desktop pointer-grab release routing, munmap page rounding, and browser reboot sizing Jul 14, 2026
@github-actions

github-actions Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

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

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

Package Arch Status Sha
alsa-lib wasm32 built 76a1d382
libcurl wasm32 built 4f9b5a1f
libcxx wasm32 built ec27aa51
libcxx wasm64 built 8f4ee8d0
libdrm wasm32 built 1d5db816
libevdev wasm32 built 9333885b
libffi wasm32 built e3940282
libinput-lite wasm32 built 1c625bbd
libinput wasm32 built a60361d6
libpng wasm32 built 9689c61e
libudev wasm32 built 30ef5cef
libwayland wasm32 built 6f1dfcf1
libxkbcommon wasm32 built f2d812a7
libxml2 wasm32 built e75d97c9
libxml2 wasm64 built 32bd5ba0
mtdev wasm32 built 5abcd835
openssl wasm32 built 04ac59f8
openssl wasm64 built 049550f6
sdl2 wasm32 built 90b6724c
sqlite wasm32 built 68933c56
sqlite wasm64 built 88bbf041
zlib wasm32 built 073eb2db
zlib wasm64 built 0d12bab1
bc wasm32 built 324366af
bzip2 wasm32 built 1eed3d03
coreutils wasm32 built 72e54e1f
curl wasm32 built bdcbcc48
dash wasm32 built 46e890ed
diffutils wasm32 built 08846c06
dinit wasm32 built c6b49d0d
fbdoom wasm32 built f29b87e9
file wasm32 built e75acd51
findutils wasm32 built 2359e785
gawk wasm32 built a1f49aa3
git wasm32 built 0be6d225
grep wasm32 built ee2307ef
gzip wasm32 built d87a65c0
kandelo-sdk wasm32 built 327eb2ca
kernel wasm32 built a25b079e
less wasm32 built 2707cc56
lsof wasm32 built 85b55778
m4 wasm32 built 1cade1f2
make wasm32 built bd353850
mariadb wasm32 built d485f8ad
mariadb wasm64 built 79043662
msmtpd wasm32 built 8876faf5
nano wasm32 built 6d2d0e7f
ncurses wasm32 built dcd4f1be
netcat wasm32 built 38dfa0f9
nginx wasm32 built 8165bdc3
php wasm32 built ef94bc44
posix-utils-lite wasm32 built 7f8aa0f1
sed wasm32 built d39bedcf
spidermonkey wasm32 built e3c4dd41
tar wasm32 built df3a2b98
tcl wasm32 built 26cd0438
unzip wasm32 built 69dc6810
userspace wasm32 built 758ef3be
vim wasm32 built 8cab3cab
wget wasm32 built ac5f4481
xz wasm32 built f371fe0e
zip wasm32 built d0a493c6
zstd wasm32 built 97996306
bash wasm32 built 66c00206
mariadb-test wasm32 built 1651df80
mariadb-vfs wasm32 built 1f6769f5
mariadb-vfs wasm64 built 575a5172
nethack wasm32 built 99bf9067
node wasm32 built dbee0563
spidermonkey-node wasm32 built 659020ab
vim-browser-bundle wasm32 built e51bb29e
nethack-browser-bundle wasm32 built 9a7373e0
rootfs wasm32 built 470d7e0a
shell wasm32 built e5ef64d2
lamp wasm32 built bcf07ede
node-vfs wasm32 built 2ceb0b40
wordpress wasm32 built 1a3268f3

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

@mho22
mho22 force-pushed the explore-dri-wayland branch 2 times, most recently from 4440a9d to e9ca2d3 Compare July 16, 2026 11:34
@mho22
mho22 marked this pull request as ready for review July 17, 2026 08:40
@mho22
mho22 force-pushed the explore-dri-sdl2 branch from d2f79e1 to a97a334 Compare August 21, 2026 10:02
@mho22
mho22 force-pushed the explore-dri-wayland branch from e9ca2d3 to 1d4b00b Compare August 21, 2026 10:02
@mho22
mho22 force-pushed the explore-dri-sdl2 branch from a97a334 to 52ad9c6 Compare August 24, 2026 19:08
@mho22
mho22 force-pushed the explore-dri-wayland branch from 1d4b00b to a3ccc13 Compare August 24, 2026 19:08
@mho22
mho22 force-pushed the explore-dri-sdl2 branch from 52ad9c6 to d194992 Compare August 26, 2026 14:37
@mho22
mho22 force-pushed the explore-dri-wayland branch from a3ccc13 to af3e6c7 Compare August 26, 2026 14:37
@mho22
mho22 changed the base branch from explore-dri-sdl2 to main August 28, 2026 20:13
@mho22
mho22 force-pushed the explore-dri-wayland branch from af3e6c7 to 539230f Compare August 28, 2026 20:14
@mho22
mho22 changed the base branch from main to explore-dri-sdl2 August 28, 2026 20:46
@mho22
mho22 force-pushed the explore-dri-sdl2 branch 2 times, most recently from d194992 to 42e3a4a Compare August 28, 2026 20:47
mho22 added 8 commits August 30, 2026 11:05
libwayland dispatches every protocol message through a libffi call, so
the port needs closures that can be built at runtime. This shim covers
the calling conventions the Wayland stack actually uses rather than the
whole libffi surface, which keeps the generated call_indirect dispatch
small enough to audit.
wayland-scanner generates the client and server glue from protocol XML,
and it runs on the build host rather than the target — a wasm binary
cannot generate its own bindings. Vendoring the protocol XML alongside
it pins what every client in this stack is compiled against, so a
compositor and its clients cannot disagree about a protocol version.
Upstream libwayland builds unmodified once the kernel answers the parts
it depends on: a unix socket per connection, SCM_RIGHTS to pass buffer
and dmabuf descriptors between processes, and shared memory the two
sides map at different addresses.

The port is the point where a real Wayland client can connect to a real
Wayland server inside the kernel, so the protocol work above it is
upstream code rather than an imitation.
A Wayland compositor sends a keymap, not keysyms: the client compiles
it and does its own translation. libxkbcommon is what compiles it, so
without the port every client sees raw keycodes and no client can
produce text.
libinput is what a compositor uses to turn evdev records into pointer,
keyboard and touch events, and it will not open a device it cannot
identify. That pulls in libevdev to describe the device, mtdev to
normalise multitouch, and enough of libudev to enumerate and tag one.

The libudev piece is a shim over the kernel's device nodes rather than
a port: there is no udev daemon here, and libinput only needs the
enumeration and the input-id properties that tagging produces.
The compositor runs as an ordinary process against the same kernel its
clients use. It serves wl_shm, xdg_shell, wl_seat and wl_output — the
minimum a stock client needs to map a surface, receive input and learn
the output geometry it is drawing for.
libkwl is the client-side half a Wayland program needs before it can
draw anything: connect, bind globals, take a role, handle the configure
handshake, compile the keymap. wpkdraw is the software rasteriser above
it — rectangles, text and antialiased edges into a wl_shm buffer.

wlterm is the first real client built on both: a VT100 terminal that
runs a shell. It is what makes the compositor demonstrably useful
rather than a surface that renders a test pattern.
Three clients on one compositor is where the software blit stops being
enough. The compositor now hands each client's buffer to the host as a
foreign texture and composites on the GPU, and a WebGL2 presenter scans
the result out to the canvas.

The kernel and host fixes here are the ones three concurrent clients
exposed: page-rounding on munmap unbind, an implicit pointer grab so a
drag does not change surface mid-gesture, a use-after-free when a
client disconnects while its buffer is still referenced, and a present
path that degrades to the software blit instead of failing when a GL
context is lost.
mho22 added 5 commits August 30, 2026 11:05
Records what the compositor still lacks against a real tiling
compositor — layer shell, a bar, a launcher, theming — and the order
that work is expected to land in.
The local source-build set asserts its exact roster, so each new
library has to be placed: they are platform packages, since the sysroot
exposes them to any client rather than shipping them as a product.

`fetch-binaries` also has to skip `kind=source` packages under
`--fetch-only`: they have no published archive to fetch, so treating a
missing one as a failure made the fetch step report errors for packages
that were never meant to be fetched.
wlterm folded both waitpid outcomes into one line:

    int exit_code = WIFEXITED(status) ? WEXITSTATUS(status) : 0;
    printf("WLTERM_EXIT code=%d\n", exit_code);

WEXITSTATUS is undefined unless WIFEXITED, so a shell killed by a
signal reported `code=0` — the same text a clean exit prints. A crashed
shell was indistinguishable from a finished one in the marker stream
the smokes and the browser specs read.

Report the signal on its own marker, `WLTERM_EXIT signal=<n>`, and keep
`code=` for the WIFEXITED case. The smoke gains a second case: a dash
that sends itself SIGKILL must produce `signal=9` and no `code=` line.
943cb1f raised `ABI_VERSION` to 44 and left three hand-written test
fixtures at 43:

    (func (export "__abi_version") (result i32)
      i32.const 43)

Each one is compiled and run as a guest program, so the kernel refused
it before execution:

    pid=100: refusing unsafe program artifact before execution:
    ABI 43, expected 44

That failed three cases across `catch-ref-fresh-worker.test.ts` and
`gc-reference-state-fresh-worker.test.ts`. The tier rebuild never
cleared them because the suite builds these artifacts itself.

`gc-reference-state-fresh-worker` runs the checked byte fixture rather
than the `.wat`, since WABT cannot parse the typed-reference syntax. Its
bytes are regenerated with the Rust `wat` crate from the corrected
source and differ from the old ones in exactly one byte — offset 208,
`0x2b` to `0x2c`. `apps/browser-demos/test/fork-continuation.spec.ts`
imports the same constant, so both hosts move together.
`scripts/ci-vitest-evidence-classes.tsv` must exactly cover the live
Vitest file inventory, so `ci-run-test-suite.sh vitest exact-abi-source`
refuses to enumerate while the two drift. This branch raises
ABI_VERSION, which is exactly when that group runs.

Classify the test files this branch adds, and drop the rows for files it
no longer carries. A file is `source-only` only when its whole import
closure stays clear of the binary resolver; the runner enforces that
separately from the cover check. The smokes exec staged wasm, so they
are `prepared-product`; the remaining unit tests import only `host/src/`.
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