Skip to content

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

Draft
mho22 wants to merge 19 commits into
explore-dri-waylandfrom
explore-dri-hyprland
Draft

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

Conversation

@mho22

@mho22 mho22 commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator

Why

Kandelo's Wayland desktop (/?demo=wayland, added earlier in the dri/wayland series) 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, and no keyboard-driven window control — the things that make a Wayland compositor usable as an actual desktop rather than a demo canvas. To show that Kandelo's Direct Rendering Infrastructure (DRI) / Kernel Mode Setting (KMS) + Wayland stack can host a full desktop environment, it needs the window-management layer that 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.

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

Stacked PR. This branch continues the dri/wayland series on top of explore-dri-wayland (#948). It carries one fork-buffer fix from that branch's tip (e9ca2d3ce) as pending drift; the reviewable diff here is the 14 commits PR10 → step 11–13 → PR14a–f → tip fix. Draft until the series merge order is settled.

What changed

Compositor — the tiling window manager (programs/wlcompositor/wlcompositor.c)

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

GPU + protocol plumbing this builds on

  • Host GPU-rendering path (PR10): shared-context multiplex, GPU-bo Framebuffer Object (FBO) targeting, and C glue, so compositing runs on the browser GPU.
  • zwp_linux_dmabuf_v1 buffer import in the compositor, reusing the existing shm_buffer import path (step 11); adds linux-dmabuf-v1.xml + xdg-decoration-unstable-v1.xml to wayland-protocols.

Demo + clients

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

Fixes landed on the tip commit

  • 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 (kernel process_table.rs), with guard tests. No ABI change.
  • Gate 7 killactive focus timing: the /?demo=hyprland 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 (socket connect). The compositor now emits an authoritative KBD_FOCUS app_id=… marker (consistent with its existing TILE / WORKSPACE markers and the activewindow>> event), and the gate waits for focus to reach the fresh terminal before killactive. No compositor semantics change.
  • The PR14-necessity audit drops dead code and the raw-GL gallery demos (wlcube, SDL2-on-Wayland-GL), keeping the libwayland-egl shim as latent GL-DRI infrastructure.

Notes

  • No ABI_VERSION bump. The work is user-space C (wlcompositor, clients) + host TS + additive kernel changes; abi/snapshot.json is unchanged and in sync. The only crates/shared edit is a doc-comment.
  • Asyncify-free, per the repo fork-instrumentation policy.

Both hosts

Per CLAUDE.md dual-host parity: 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/kernel code takes effect on both. New 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.

Verification

Re-run at the branch tip this pass:

  • cd host && npx vitest run915 passed; only the 4 known, unrelated pre-existing failures (spidermonkey npm-runner, fork-dlopen dev-shell clang, sdl2-kmsdrm, node.wasm ABI policy). All wlcompositor smoke suites green.
  • apps/browser-demos: npx playwright test test/kandelo-hyprland.spec.tsboth gates green; Gate 7 deterministic across --repeat-each=3.
  • ABI: no ABI_VERSION bump and abi/snapshot.json unchanged across the whole branch (the only crates/shared edit is a doc-comment), so the change is additive-compatible.

The kernel unit suite and the libc / POSIX conformance suites were run as each earlier commit landed; they were not re-run in this pass because the tip fix is compositor-C + test-only, with no kernel/libc surface. Worth a full-suite pass before this leaves draft.

🤖 Generated with Claude Code

mho22 and others added 13 commits July 15, 2026 12:59
…x, GPU-bo FBO targeting, C glue

Completes Tier 0 / PR10: the host half of GPU-tier buffer objects, on top
of the kernel foundation (bo tier + WPK_CREATE_GPU_BO). A GPU-tier bo is a
host WebGLTexture (+FBO) on the DRM-master compositor's scanout WebGL2
context — no SAB, unmappable on the CPU, sampled zero-copy by the
compositor and rendered into by its producer (true dmabuf semantics).

Kernel (folded from the prior foundation commit + this pass):
- dri/bo.rs: BoTier {CpuShared, GpuTexture}; alloc_gpu() keeps the
  LINEAR-equivalent stride/size so gbm_bo_get_stride stays sane.
- syscalls.rs: DRM_IOCTL_WPK_CREATE_GPU_BO (registry-alloc →
  host.gbm_gpu_bo_create → per-fd handle, full rollback on host failure /
  EMFILE); MAP_DUMB and the sys_mmap path both reject GPU-tier bos as
  unmappable (defense in depth against a forged offset). GLIO_CREATE_SURFACE
  translates reserved[0]'s per-fd target bo handle to a global bo_id in
  place before forwarding to the host.
- process.rs / wasm_api.rs: gbm_gpu_bo_create HostIO trait method + import.

Host (shared import object — both hosts):
- webgl/registry.ts: GpuBo bookkeeping (createGpuBo/gpuBo/destroyGpuBo) on
  the shared context; texture id band 0x6000_0000; renderTargetFbo on the
  binding. dropForeignTexturesForBo leaves gpuBos untouched (bo-owned).
- kernel.ts: real host_gbm_gpu_bo_create (−ENOSYS → CPU-tier fallback when
  no shared context, e.g. headless Node); shared-context routing for
  non-master GL clients; EGL surface → bo FBO render targeting; zero-copy
  GPU path in host_gl_bind_foreign_texture; producer-only swap fence in
  host_gl_present; render-target release on destroy_surface.
- webgl/bridge.ts: OP_BIND_FRAMEBUFFER name 0 → renderTargetFbo redirect.

C glue:
- libgbm_stub.c: WPK_DRM_IOCTL_CREATE_GPU_BO; gbm_bo_create requests the
  GPU tier only for render-only bos (RENDERING set, none of
  SCANOUT/CURSOR/WRITE/LINEAR) and falls back to CREATE_DUMB on any error;
  gbm_bo_map rejects GPU bos.
- libegl_stub.c: wpkEglSetWindowSurfaceTarget(handle) stamps reserved[0].

The producer path (client routing / FBO targeting / swap fence) is
browser-only and inert on Node (guarded on a live shared context), so
vitest stays at baseline; it is unvalidated end-to-end until the GL
clients in PR12/PR13.

Tests: +8 kernel unit tests (bo tier; CREATE_GPU_BO handle/stride writeback
+ host call, zero-dim reject, host-failure rollback, MAP_DUMB + sys_mmap
GPU-tier reject, GLIO_CREATE_SURFACE target-handle translation) and
webgl-gpu-bo.test.ts (7 tests: alloc one tex+FBO, id band, idempotent
re-create, zero-copy bind, destroy, drop-leaves-gpu-bos, null on alloc
fail). cargo lib 1107/0; vitest 904/3 baseline; libc-test + posix-test
clean; ABI check exit 0 (host imports are not snapshot-tracked → no
ABI_VERSION bump).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ffer import reusing the shm_buffer path, LINEAR/v3, dmabuf smoke gate

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

Record the v3 LINEAR-only dmabuf buffer-import path (reusing the shm_buffer
composite/destroy path) in the compositor architecture section — the buffer
surface SDL2/wlcube bind in steps 12-13.

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

SDL2's upstream Wayland+OpenGLES2 backend now renders through wlcompositor
zero-copy. The sdl2gl-test client drives the full step-10/11/12 GPU chain via
a third-party toolkit: SDL's wl_egl_window is backed by a GPU-tier bo (the
libwayland-egl shim), libEGL targets the bo's FBO, and eglSwapBuffers
attach+commits it as a zwp_linux_dmabuf_v1 wl_buffer the compositor samples as
a foreign texture.

Key host fix: the GPU-bo render-target redirect was applied only at
GLIO_CREATE_SURFACE and gated on b.gl, but SDL2 creates the wl_egl_window
surface during SDL_CreateWindow — BEFORE the GL context (SDL_GL_CreateContext)
— so b.gl was still null, the redirect never fired, and the client rendered
into its canvas default framebuffer while the compositor sampled a
never-written (black) bo. Capture the target bo at surface creation
(pendingRenderTargetBoId) and apply the redirect from a shared
GlContextRegistry.applyRenderTarget() called at BOTH create_surface and
create_context — whichever runs last wins.

- libc/glue/libwayland-egl.c + wayland-egl-include/: libwayland-egl shim
  (wl_egl_window -> GPU bo + zwp_linux_dmabuf_v1 wl_buffer; _wpk_wlegl_* hooks).
- libc/glue/libegl_stub.c: eglSwapBuffers drives the wl_egl_window present.
- packages/registry/{libwayland,libxkbcommon,sdl2}: enable the SDL2 Wayland
  backend; revision bumps.
- programs/wlcompositor/sdl2gl-test.c: spinning-triangle GLES2 client.
- apps/browser-demos: sdl2gl demo boot + kandelo-sdl2gl.spec.ts browser gate.
- docs/architecture.md: SDL GL path + surface-before-context redirect.

Verified: cargo (1107), libc-test (0 unexpected FAIL), POSIX (0 FAIL), ABI in
sync, and the kandelo-sdl2gl browser smoke green (COMPOSITE_SAMPLE non-black =
the teal clear crossed the process boundary). Host-only logic change, no
kernel/ABI changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add wlcube: a raw libwayland-egl GLES2 client (no toolkit) that renders a
spinning, back-face-culled cube through wlcompositor, driving the standard
wl_egl_window → libEGL bo-FBO targeting → dmabuf present chain a mesa client
uses. Complements step-12's SDL2 proof by exercising the same GPU-bo/dmabuf
GL path directly against the libwayland-egl entry points.

Occlusion is resolved with back-face culling alone (the GPU-tier bo's FBO is
color-only): a convex opaque solid's front faces tile its silhouette without
overlap, so no depth test is needed. Every face is wound CCW when viewed from
outside; WebGL2's cull GL_BACK / front GL_CCW defaults remove the away-facing
faces. A depth attachment for arbitrary 3D is tracked as follow-up.

Adds the missing glUniformMatrix4fv entry point to the GLES2 stub (additive,
no ABI bump; OP_UNIFORM_MATRIX4FV and its host decoder already existed and are
unit-tested in webgl-bridge.test.ts). Wires the wlcube browser demo (live-setup
boot branch, preset, webgl2-scanout) and gates it with kandelo-wlcube.spec.ts.

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

Add a pure, unit-testable compute_tiling(area, n) -> geoms[] to the PID-2
wlcompositor: recursive binary split along the longer side (Hyprland's
dwindle default), near-half to window i, remainder to the last. retile()
recomputes over mapped toplevels in map order and pushes dictated w,h through
the xdg_toplevel_send_configure / xdg_surface_send_configure path, emitting a
TILE marker per window.

Layout is FLOATING by default (zero-init), so /?demo=wayland is byte-identical;
WLC_LAYOUT=dwindle selects the tiler. place_surface() runs only in FLOATING
mode; retile() is a no-op otherwise. Hooked into map (surface_commit), unmap
(surface_resource_destroy), and env selection in main().

Gate: host/test/wlcompositor-tiling-smoke.test.ts spawns 3 clients in dwindle
and asserts the emitted geometry equals the partition recomputed from the same
rule, plus in-bounds and pairwise non-overlap. Floating + dmabuf + desktop
smokes stay green. No ABI surface added.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ch/move)

Add N_WORKSPACES=9 1-based workspaces on the single output. Each surface
carries a workspace id (assigned to the active workspace at first map);
surface_visible() = mapped AND on the active workspace now gates compositing
(both GPU and CPU paths), input hit-testing (surface_at), and tiling (retile).

switch_workspace() shows a workspace and restores focus to its topmost window
(z-order doubles as per-workspace focus memory since focusing raises);
move_focus_to_workspace() sends the focused window away and re-tiles the
remainder. A bootstrap keybind table in try_keybind() drives them —
SUPER+1..9 switch, SUPER+SHIFT+1..9 move — intercepting the key before the
focused client. PR14d replaces this table with the config-file engine.

The self-contained xkb keymap gains a Super_L key (evdev 125 -> Mod4) so
XKB_MOD_NAME_LOGO reflects SUPER; additive to clients. Focus handoff on
destroy is now workspace-scoped (topmost_on_ws).

Gate: wlcompositor-tiling-smoke gains a workspaces test — 3 clients in dwindle,
SUPER+SHIFT+2 injected via evdev moves the focused window to ws 2, and ws 1
re-tiles to the exact 2-way dwindle partition. Floating/desktop/dmabuf smokes
stay green. No ABI surface added.

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

Add an AF_UNIX control socket at /tmp/kwlctl-0, polled in the compositor's
wl_event_loop alongside the wayland + libinput fds. Verbs:
  - clients / workspaces / activewindow  -> JSON (Hyprland -j-shaped subset)
  - dispatch workspace N | movetoworkspace N | close | exec <path ...>
  - keyword layout <dwindle|floating>    -> runtime config set
  - --listen                             -> `event>>data` stream (socket2 format)
Workspace switches and focus changes emit workspace>> / activewindow>> events
to every listener. New CLI client programs/wlcompositor/kwlctl.c speaks it.

dispatch exec uses the NON-forking posix_spawnp (SYS_SPAWN) — fork() from
inside an event-loop callback would wedge the server, and the direct spawn
syscall needs no fork instrumentation. Accepted control fds are set CLOEXEC so
they don't leak into spawned children (an inherited copy keeps the socket
half-open and the kwlctl client never sees EOF).

Parity: kwlctl is a client<->compositor UNIX socket entirely inside the kernel
— zero host/src changes, so the dual-host rule is not triggered.

Gate: host/test/wlcompositor-kwlctl-smoke.test.ts — clients JSON equals the
dwindle partition; --listen streams the event from dispatch workspace 2;
dispatch exec spawns a fourth client (resolved via execPrograms). Prior wayland
smokes + centralized-spawn stay green. No ABI surface added.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Generalize the PR14b bootstrap into a config-driven bind table. Each
`bind = MODS, KEY, DISPATCHER[, ARGS]` line (hyprland.conf-shaped subset) is
parsed into { mods, base-level keysym, action, arg/param }. try_keybind now
matches the pressed key's SHIFT-independent base keysym + an exact MOD_* mask
against the table and runs the dispatcher (exec, workspace, movetoworkspace,
killactive, cyclenext/cycleprev), swallowing the combo before the client.

Config source: WLC_CONFIG or /etc/kandelo/wlcompositor.conf; absent = generic
SUPER-based defaults (install_default_binds), NOT demo-specific fallbacks.
BINDS_LOADED marks the count + source. The keymap already carries Super_L
(PR14b); XKB_KEY base-level lookup makes `SUPER SHIFT, 1` match the same key
as `SUPER, 1`. focus_cycle moves focus without reordering z-order so a tiled
layout keeps its geometry.

Entirely in the compositor keyboard path — no host/src change.

Gate: host/test/wlcompositor-keybind-smoke.test.ts — (1) default binds drive
SUPER+3/SUPER+1 workspace switches + SUPER+J focus-cycle (via kwlctl); (2) a
WLC_CONFIG file's custom `SUPER, 5, workspace, 7` overrides the default and
fires workspace 7. Prior wayland smokes stay green. No ABI surface added.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Vendor xdg-decoration-unstable-v1.xml and generate server+client code in the
wlcompositor build. The compositor advertises zxdg_decoration_manager_v1 and
always negotiates SERVER_SIDE (a tiled window has no titlebar; a floating one
gets only the focus ring), so clients drop CSD. wlclient-test gains an optional
decoration request behind WLC_DECOR=1 that prints the negotiated mode.

Docs (CLAUDE.md mandate for PR14a-e): architecture.md gains a "Tiling window
manager" subsection (layout engine, workspaces, kwlctl IPC, keybinds,
decoration); browser-support.md notes WLC_LAYOUT=dwindle tiling mode.
posix-status.md already documents posix_spawn/posix_spawnp; PR14 adds no new
syscalls or ioctls.

Gate: host/test/wlcompositor-decoration-smoke.test.ts — a WLC_DECOR client is
configured SERVER_SIDE, never client_side. All prior wayland smokes stay green.
Entirely in-kernel — no host/src change. No ABI surface added.

Deferred (need browser verification per CLAUDE.md item 6, not reliable
headless): the /?demo=omarchy browser demo + its Playwright gate, the VFS-image
demo config, and libkwl CSD title-bar suppression.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… tests+comments

- Remove speculative MOD_CTRL (no bind, no test) and the untested
  `keyword layout` kwlctl verb (effect unobservable, no PR14 consumer).
- wlclient-test: observe xdg_toplevel.close so killactive is testable
  (prints CLIENT_CLOSED and exits 0).
- keybind-smoke: cover SUPER+K cycleprev (focus restore) and SUPER+W
  killactive (CLIENT_CLOSED + ws1 re-tiles to n=1).
- kwlctl-smoke: assert the `workspaces` JSON query.
- wlcompositor.c: WHY-only trim of compute_tiling / retile comments.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Make wlcompositor's dwindle layout usable by real clients: the compositor
composites at the surface's NATIVE buffer size (blit_surface does not scale
to the tile), so tiling requires the CLIENT to resize into the size the
compositor dictates on retile.

- libkwl: honor xdg configure(w,h) via the xdg_surface.configure ack
  barrier — rebuild both wl_shm buffers at the new size and push a
  KWL_RESIZE event. Bind zxdg_decoration_manager_v1 and honor the
  negotiated mode: SERVER_SIDE drops the CSD titlebar (tb_h=0, all pointer
  events are content). The initial configure(0,0) is ignored, so a floating
  client never resizes.
- wlcompositor: negotiate decoration mode by layout (dwindle→SERVER_SIDE,
  floating→CLIENT_SIDE) instead of always SERVER_SIDE. Add a CTRL modifier
  to the keybind engine (parse_mods + active_mod_mask; the xkb keymap
  already tracks Control_L) — a browser reserves SUPER (Cmd/Win), so CTRL
  is the modifier the in-browser demo can actually use. Fold a config bind's
  keysym to lowercase at parse time: xkb_keysym_from_name("W") resolves to
  the uppercase keysym while binds match the lowercase base-level keysym, so
  without the fold `bind = CTRL, W` (any letter/named key) silently never
  fired — only digit binds worked.
- wlclock: derive dial geometry from the live surface size; redraw on
  KWL_RESIZE (prints WLCLOCK_RESIZE).
- wlterm/vt100: vt100_resize reflows the grid (marking all cells dirty); on
  KWL_RESIZE recompute cols/rows, TIOCSWINSZ + SIGWINCH the child, re-render
  (prints WLTERM_RESIZE).
- build-programs.sh: link the xdg-decoration client protocol into the
  libkwl clients and wlterm.
- Node smokes: new wlcompositor-resize-smoke (dwindle + wlclock resizes to
  the predicted tile); decoration smoke asserts floating→client_side too;
  keybind smoke gains a CTRL + letter (case-folded) case.

Additive ABI — no bump (no new syscalls/structs). Node smoke sweep green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The first Hyprland-class tiling desktop in the browser, on top of the
client-side resize + layout-driven SSD compositor work.

- live-setup.ts: hyprlandDemo staging block — boot wlcompositor with
  WLC_LAYOUT=dwindle + a staged /etc/kandelo/wlcompositor.conf (read via
  WLC_CONFIG), plus one wlclock and two wlterm terminals. The clients tile
  into gapped, borderless (SSD) frames and resize into their tiles. Mirrors
  the wayland block's input-source + display-sizing; browser-only page
  wiring (Node uses the host/test smokes).
- The staged config binds Return / W / 1..9 on both SUPER (real Hyprland)
  and CTRL. A browser reserves SUPER (Cmd/Win — Cmd+W closes the tab,
  Cmd+1..9 switch tabs), so CTRL is the modifier that actually reaches the
  demo: Ctrl+Return spawns a terminal, Ctrl+W kills, Ctrl+1..9 switch
  workspaces.
- presets.ts: "Hyprland tiling WM" preset (id hyprland) for the sidebar +
  descriptor title/summary.
- kandelo-hyprland.spec.ts: Playwright gate — dwindle layout, config
  loaded, CLIENT_CONNECTED count=3, TILE n=3, WLCLOCK_RESIZE/WLTERM_RESIZE,
  composited canvas, and CTRL keybinds (Ctrl+Return spawns a 4th client;
  Ctrl+2 switches workspace).
- docs: architecture.md (layout-driven SSD + client-side resize + CTRL
  modifier), browser-support.md (Hyprland tiling demo section + table row,
  "use Ctrl in the browser").

Verified: full serial wayland smoke sweep (16/16), ABI check (exit 0),
Playwright gate passed, and visual confirmation of the tiled desktop.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Automattic Automattic deleted a comment from github-actions Bot Jul 18, 2026
@github-actions

Copy link
Copy Markdown

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.

…; drop GL demos

Bundles three related pieces of the DRI/Wayland work on this branch.

1. Fix compositor freeze on posix_spawn (scanout-bo over-decref)
   `kernel_spawn_process` is non-forking: `spawn_child` value-clones the
   parent ofd_table (incl. the card0 Card OFD referencing the screen-sized
   scanout bo) and called `bump_inherited_resource_refcounts`, which increfs
   inherited pipes/host-handles/PTYs/sockets but NOT DRI bos. When the spawned
   child exec'd and closed its O_CLOEXEC card0, `dri_release_ofd_state`
   decref'd the scanout bo twice (handle + fb) — unbalanced — driving it to 0
   and evicting it from the BoRegistry while the compositor still gbm_bo_maps
   it every frame, flooding EINVAL and freezing the desktop.
   New spawn-only `bump_inherited_dri_bos` mirrors fork's deserialize increfs
   (Card handle+fb, RenderNode handle, PrimeBo), called from `spawn_child`
   after the resource bump. DRI incref stays spawn-only so `fork_process`
   (deserialize + bump) does not double-incref. Internal refcount logic only;
   no ABI change. Tests in process.rs guard both the spawn incref and that
   fork still increfs exactly once. Repro gated by scratch-hyprland-crash-repro.

2. Hyprland per-app launch keybinds + wlpaint
   Config binds Return->wlterm, K->wlclock, P->wlpaint (each on SUPER and
   CTRL) opened Hyprland-style via the exec dispatcher; wlpaint is staged and
   summoned on demand (not auto-spawned). Keybind/resize smoke tests updated;
   docs/browser-support.md + architecture.md describe the bind engine.

3. Remove the SDL2-on-Wayland (GL) and Wayland-cube (raw EGL) gallery demos
   Drops the two `sdl2gl`/`wlcube` gallery cards, their live-setup boot
   machinery, the sdl2gl-test.c/wlcube.c clients, their build blocks, and the
   two Playwright specs. The libwayland-egl shim, its vendored headers, and
   the libEGL/libGLESv2 wl_egl_window support are kept as latent GL-DRI
   infrastructure (the retained SDL2 GLSL playground's configure needs the
   wayland-egl headers/pc). architecture.md loses the demo-specific narrative;
   a dangling wlcube reference in libglesv2_stub.c is removed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mho22
mho22 force-pushed the explore-dri-hyprland branch from 457d76e to a01ef50 Compare July 18, 2026 16:26
mho22 and others added 5 commits July 18, 2026 21:29
…y materialization

test-gate-prepare's Materialize binaries step runs fetch-only;
wayland-protocols is ManifestKind::Source with no published binary
archive, so the resolver can only satisfy it from a warm cache and
fails deterministically on a cold runner (build_deps.rs cmd_resolve).
It has no runtime binary to materialize — the protocol XML is compiled
into wlcompositor/wlterm during the later Build test programs step,
which also warms the resolver cache before the fetch-only
prepare-browser step. Skip it here, matching the other non-materialized
packages already in WASM_POSIX_FETCH_SKIP_PKGS.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The wayland desktop demo's keep-alive foreground client used
host.runShellCommand("/usr/local/bin/wlterm"), which types the command
into the pts/0 shell and makes it fork()+exec the client. That shell-fork
intermittently fails to start wlterm under CI's Linux headless-chromium
worker scheduling, so the third client never connects and the compositor
never prints CLIENT_CONNECTED count=3 — the kandelo-wayland browser gate
then times out. This is the same fragile pattern already fixed for the
hyprland demo; launch wlterm through the reliable non-forking spawn path
(like the clock + paint clients) and await its exit promise for the same
keep-alive semantics.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
bash's `$(...)` command-substitution fork pushes a frame stack past the
old 16 KB fork-save buffer, spilling into the adjacent syscall channel
and trapping the child on rewind with a cryptic `call_indirect`
signature mismatch (surfaced by the browser merge-gate shell demo).

- crates/shared: FORK_SAVE_BUFFER_SIZE 16 KB → 48 KB. The buffer sits at
  the top of a dedicated 64 KB fork-save page, so it grows in-page
  without moving any other region. Instrumented binaries take the buffer
  address at runtime and never bake the size in, so this needs no
  ABI_VERSION bump (stays 17).
- xtask/dump_abi: classify_process_memory_layout treats an in-page
  increase of fork_save_buffer_size as additive (shrink / over-page /
  any other field change stays breaking), so the snapshot change passes
  the ABI gate without a version bump. +6 unit tests.
- host/worker-main: assertForkBufferWithinBounds reads current_pos after
  unwind at both fork sites and throws — naming the peak — turning
  silent channel corruption into a diagnostic. Shared file, both hosts.
  +dedicated vitest (wasm32 + wasm64).
- Consumers that hand-roll onFork and derived forkBufAddr from a
  hardcoded 16384 (nginx.test.ts, nginx-wrapper.ts, mariadb run-tests.ts)
  now import FORK_SAVE_BUFFER_SIZE. With the 48 KB kernel a stale literal
  pointed the child's fork replay at the wrong address and hung the
  worker — nginx's HTTP test timed out at 60s. Host source already
  imported the constant; only these three lagged.
- docs/fork-instrumentation: buffer capacity, overflow guard, sizing
  history.

Also folds in the fetch-binaries kind=source CI fix (squashed):
fetch-binaries.sh skips `kind = "source"` packages by kind, not just by
build.toml absence — wayland-protocols carries a build.toml for its
inputs cache-key yet must not resolve under --fetch-only (test-gate-
prepare). +dedicated regression test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…o-select survives the Wayland probe

Step 12 built SDL2 with `--enable-video-wayland --disable-wayland-shared`,
so libSDL2.a's video bootstrap array now lists Wayland_bootstrap before
KMSDRM_bootstrap and direct-references wl_display_connect (no dlopen).
SDL_Init(SDL_INIT_VIDEO) therefore probes Wayland first and calls
wl_display_connect(NULL) — but the sdl2_*_smoke link lines only linked
libSDL2 + libgbm + libdrm, so wl_display_connect resolved to the host's
throw-on-call stub and the program aborted:

    Kernel worker failed: Unimplemented import: env.wl_display_connect

This surfaced only now that the shell-demo merge-gate test passes and CI
reaches test-suite (vitest) → sdl2-kmsdrm-smoke.test.ts (was green on the
base branch, which built SDL2 without the Wayland backend).

Link the real libwayland-client.a + libffi.a into all three sdl2_*_smoke
programs (they all pull in libSDL2.a's Wayland backend via DYNAPI). Now
wl_display_connect(NULL) runs for real, returns NULL in this env (no
XDG_RUNTIME_DIR / compositor), and SDL falls through to KMSDRM — the same
auto-select path real hardware takes. Verified: wl_display_connect is no
longer an undefined import and `vitest run test/sdl2-kmsdrm-smoke.test.ts`
passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The `/?demo=doom` demo boots fbDOOM against /doom1.wad, fetched at runtime
from DOOM_WAD_URL. That URL — distro.ibiblio.org/slitaz/.../doom1.wad —
was removed upstream (returns 404 as of ~Jul 2026), so the demo fails to
boot:

    err  Failed to boot fbDOOM
    err  fetch failed for /doom1.wad: 404 Not Found

This broke the browser merge-gate test "Kandelo fbDOOM demo renders to the
framebuffer". It is not a rendering regression and not specific to this
branch — it affects the base branch equally; it was simply masked earlier
because the (now-fixed) shell-demo failure aborted the run before fbDOOM
executed. The base branch's last green run predates the upstream removal.

Repoint DOOM_WAD_URL to a pinned Wayback Machine snapshot of the same file
(timestamp 20260714010718, `id_` = original raw bytes, no Wayback UI
injection). The captured bytes are byte-identical to the original — they
hash to the unchanged DOOM_WAD_SHA256 (4,196,020 B, DOOM shareware v1.9,
freely distributable) — so the runtime sha256 gate still passes. Pinning
the timestamp keeps the source deterministic. CI rebuilds shell.vfs.zst,
which bakes the URL into the demo config.

Co-Authored-By: Claude Opus 4.8 (1M context) <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