Skip to content

Hyprland on Kandelo - a dwindle tiling desktop with workspaces, keybinds, and hyprctl-style control - #976

Open
mho22 wants to merge 6 commits into
explore-dri-waylandfrom
explore-dri-hyprland
Open

Hyprland on Kandelo - a dwindle tiling desktop with workspaces, keybinds, and hyprctl-style control#976
mho22 wants to merge 6 commits into
explore-dri-waylandfrom
explore-dri-hyprland

Conversation

@mho22

@mho22 mho22 commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator

Why

explore-dri-wayland (#948) composites floating windows onto a real KMS scanout surface, but it stops at "windows on a screen." It has no window manager: no tiling, no workspaces, no keyboard-driven window control — the things that make a Wayland compositor usable as a desktop rather than a demo canvas. To show that Kandelo's DRI/KMS + Wayland stack can host a full desktop environment, it needs the window-management layer compositors like Hyprland provide.

This branch builds a Hyprland-class tiling window manager into wlcompositor and ships a /?demo=hyprland browser demo: a clock and two terminals tiled into gapped, decorated slots and driven entirely from the keyboard.

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

Terms used below: dwindle = Hyprland's default recursive binary-split tiling layout; hyprctl = Hyprland's control CLI/IPC; SSD = server-side decorations, where the compositor draws the title bar, border and focus ring instead of the client; bo = graphics buffer object; killactive = the "close the focused window" action.

What changed

GPU rendering and buffer import

  • Render client buffers on the GPU: shared-context multiplex, GPU-bo Framebuffer Object targeting, and the C glue, so compositing runs on the browser GPU.
  • zwp_linux_dmabuf_v1 buffer import, reusing the existing shm_buffer import path. Adds linux-dmabuf-v1.xml and xdg-decoration-unstable-v1.xml to wayland-protocols.
  • Upstream SDL2 and a raw EGL client on Wayland — the same SDL2 from SDL2 on Kandelo - GLSL shader playground demo #709, now running as a Wayland client rather than owning the KMS surface.

The tiling window manager (programs/wlcompositor/wlcompositor.c)

  • dwindle tiling layout behind a WLC_LAYOUT flag: recursive binary splits into gapped slots.
  • Workspaces, N per output, SUPER+digit to switch and SUPER+SHIFT+digit to move the focused window.
  • kwlctl, the hyprctl analog (programs/wlcompositor/kwlctl.c): a Hyprland socket2-format activewindow>> / workspace>> event stream, plus a control socket for querying and commanding the compositor.
  • A config-file keybind engine parsing Hyprland-style bind = MOD, key, action, param lines from /etc/kandelo/wlcompositor.conf.
  • Server-side xdg-decoration: the compositor draws window borders and a focus ring.
  • Client-side tiling resize and layout-driven SSD: clients honor the compositor's dictated tile size through xdg configure rather than drawing fixed-size islands.

Demo and clients

  • /?demo=hyprland: a dwindle-tiled clock and two terminals, with app-launch keybinds (CTRL+Returnwlterm, CTRL+Kwlclock, CTRL+Pwlpaint) and CTRL+W → killactive.
  • wlterm, wlclock and wlpaint resize to their dictated tile and tear down cleanly on close. wlterm SIGHUPs its shell so a window close does not wedge on waitpid.

The fixes a running desktop exposed

  • Spawn DRI-bo refcount freeze: posix_spawn over-decremented the inherited scanout bos' channel refcount, tombstoning the compositor's own buffer and freezing every subsequent composite. Fixed by scoping the inherited-bo refcount bump to spawn (crates/kernel/src/process_table.rs), with guard tests.
  • killactive focus timing: the end-to-end gate closed the wrong window because it raced the new terminal's map. Keyboard focus only moves on a surface's first commit, which lands after CLIENT_CONNECTED. The compositor now emits an authoritative KBD_FOCUS app_id=… marker, consistent with its existing TILE and WORKSPACE markers, and the gate waits for focus to reach the fresh terminal before killactive. No compositor semantics change.

Notes

Both hosts

The GPU-compositing path exists only in the browser — the headless Node GL stub has no readback — so the compositor's GPU renderer and the /?demo=hyprland gate are browser-only, while the Node smokes assert the inverse (WLC_RENDERER cpu). The shared compositor and kernel code takes effect on both. Coverage lands on the side it exercises: host/test/wlcompositor-*-smoke (Node) for tiling, keybinds, kwlctl, decoration and resize; apps/browser-demos/test/kandelo-hyprland.spec.ts (Chromium) for the tiled desktop, the launch/workspace/killactive keybinds, and the SCM_RIGHTS launch-storm regression.

Rebased and rebuilt

This branch sits on explore-dri-wayland at 539230f18. The history was rebuilt from 18 commits to 6.

Against the previous tip, 18 files differ outside those main itself moved. Seventeen are comment-only — plan labels (PR6, PR10, PR14aPR14f, step 1113) replaced by the fact each stood for. The eighteenth is a deletion: apps/browser-demos/test/scratch-hyprland-crash-repro.spec.ts, 164 lines, whose own header read SCRATCH observational repro (uncommitted). No code changed.

The earlier description of this PR was marked draft pending the series merge order. That order is now settled: #1318 sits between this branch and #1241.

Verification

Run at the stack tip ab7ab2b63, which contains all 6 commits here:

  • 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.

@Automattic Automattic deleted a comment from github-actions Bot Jul 18, 2026
@github-actions

Copy link
Copy Markdown
Contributor

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

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

Package Arch Status Sha
alsa-lib wasm32 built 543cb376
libcurl wasm32 built b4d20311
libcxx wasm32 built d46e071d
libcxx wasm64 built 55409df4
libdrm wasm32 built c5a0b0dc
libevdev wasm32 built 50d21500
libffi wasm32 built d042a8c3
libinput-lite wasm32 built e3060a10
libinput wasm32 built 71518e16
libpng wasm32 built 29bd2fa8
libudev wasm32 built 08d0d1fe
libwayland wasm32 built 664823ce
libxkbcommon wasm32 built d4833e99
libxml2 wasm32 built 1b0e159e
libxml2 wasm64 built 8a166364
mtdev wasm32 built 0adba22d
openssl wasm32 built 4744fc1a
openssl wasm64 built 867ed8f7
sdl2 wasm32 built 8329d055
sqlite wasm32 built c7fbdcfc
sqlite wasm64 built 8a761d10
zlib wasm32 built 3e5696ca
zlib wasm64 built c0691082
bc wasm32 built 64150f08
bzip2 wasm32 built cf2d3bc2
coreutils wasm32 built c63c2cf7
curl wasm32 built ae07bfd5
dash wasm32 built 9c9fdf54
diffutils wasm32 built 70f96266
dinit wasm32 built 1071644d
fbdoom wasm32 built 685bf5b1
file wasm32 built 6120a6b2
findutils wasm32 built 315afbdb
gawk wasm32 built 3a47b550
git wasm32 built c3e3a422
grep wasm32 built fea66afb
gzip wasm32 built ca83da7c
kandelo-sdk wasm32 built 4b815009
kernel wasm32 built e14cd2e1
less wasm32 built 732f2606
lsof wasm32 built c4141dcb
m4 wasm32 built bf234e77
make wasm32 built 82a016b2
mariadb wasm32 built 36587f54
mariadb wasm64 built 7ccde5d1
msmtpd wasm32 built 6ae1debf
nano wasm32 built ae92d650
ncurses wasm32 built 9662fe5a
netcat wasm32 built 09531db9
nginx wasm32 built 997ba1c5
php wasm32 built 6aac20de
posix-utils-lite wasm32 built 4c90f623
sed wasm32 built 8eba8c39
spidermonkey wasm32 built ab8b4de8
tar wasm32 built 8eb86d45
tcl wasm32 built c445e96a
unzip wasm32 built d24b3134
userspace wasm32 built a420e097
vim wasm32 built f5a07ef3
wget wasm32 built a1e2342e
xz wasm32 built fc99016f
zip wasm32 built 39cd06e4
zstd wasm32 built d3274e03
bash wasm32 built 27abbe01
mariadb-test wasm32 built 3f563124
mariadb-vfs wasm32 built a3e3e958
mariadb-vfs wasm64 built 607d4651
nethack wasm32 built cc88c382
node wasm32 built 995190fe
spidermonkey-node wasm32 built 6860ec20
vim-browser-bundle wasm32 built e3161b28
nethack-browser-bundle wasm32 built d7b61a73
rootfs wasm32 built 4cbb9ee7
shell wasm32 built b18f57be
lamp wasm32 built e286a798
node-vfs wasm32 built 4fd5df7b
wordpress wasm32 built f16deacd

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

mho22 added 6 commits August 30, 2026 11:06
Compositing three clients by copying their pixels through the CPU is
what limits the desktop. A client can now render into a GPU buffer
object and the compositor binds it as a foreign texture, so a frame
reaches the screen without a readback.

Several GL clients share one host context, and each non-master client
is routed to its own FBO — a producer draws into the target the
compositor later samples, which is what makes the handoff zero-copy.
The protocol a stock client uses to hand the compositor a GPU buffer
instead of shared memory. Import reuses the existing shm_buffer path
once the descriptor is resolved, so a dmabuf surface composites through
the same code as everything else; LINEAR modifiers and version 3 are
what current clients ask for.
Two clients that were written against real Wayland, not against this
compositor. SDL2's own Wayland+GLES2 backend runs end-to-end, and
wlcube drives libwayland-egl directly.

They matter because they fail differently from in-tree clients: an
in-tree client can be written around a gap in the compositor, while
upstream code either finds what the protocol promises or breaks.
The compositor gains what makes a tiling desktop rather than a surface
host: a dwindle layout that splits the output as windows arrive, N
workspaces per output with SUPER+digit to switch and move, a
config-file keybind engine, and server-side decorations so window
frames are drawn by the compositor rather than by each client.

`kwlctl` is the control channel — a newline-delimited command and event
socket at /tmp/kwlctl-0, the analog of hyprctl, which is how a bar or a
script reads desktop state and drives it. Resize is client-side: the
layout tells a client its new box and the client redraws, so a drag
does not block on the compositor.

`/?demo=hyprland` boots the result in the browser.
Three problems that only appear once real clients start and stop under
a window manager. A spawned client could freeze because its DRI buffer
object kept a reference that was never dropped. Launch keybinds bound
to the wrong target. A foreground wlterm was started through the
forking spawn path, which is wrong for a client that should own the
terminal it was launched from.
`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/`.
@mho22
mho22 force-pushed the explore-dri-wayland branch from 539230f to b5d3c7a Compare August 30, 2026 09:10
@mho22
mho22 force-pushed the explore-dri-hyprland branch from 916f226 to ab82ed9 Compare August 30, 2026 09:10
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