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
Open
Wayland on Kandelo - Fix desktop pointer-grab release routing, munmap page rounding, and browser reboot sizing#948mho22 wants to merge 13 commits into
mho22 wants to merge 13 commits into
Conversation
test-gate-prepare failed because fetch-binaries could not resolve espeak-ng (wasm32): its build.toml lists gitignored vendored sources (pcaudiolib-src/src/audio*.c) as cache-key inputs, so the resolver cannot even hash them on a clean checkout, and no archive is published for it in any binaries-abi-v* index. The base branch masked this because its last staging run skipped test-gate-prepare entirely. espeak-ng's port is still WIP (placeholder all-zero source sha, vendored pcaudiolib-src not committed) and unpublishable, so treat it like the other temporarily-disabled packages: add it to disabled_pkgs (matrix exclusion) and WASM_POSIX_FETCH_SKIP_PKGS (fetch gate) in both staging-build.yml and prepare-merge.yml, keeping the two lists mirrored. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Vendored libffi replacement providing a call_indirect trampoline for wl_closure_invoke's ffi_call across arities 0..22, so libwayland can dispatch decoded requests without a full libffi wasm32 port. Ships the recipe/build state, ffi.h, the shim source, and a native unit test that exercises every arity's argument marshalling. Opens the Wayland compositor stack; see docs/plans/2026-07-08-dri-wayland-compositor-plan.md (§4, the libffi de-risk). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…(PR2) Adds pkgs.wayland-scanner (the split -bin derivation — generator only, darwin+linux clean, unlike the Linux-only full library) to the dev shell, and a wayland-protocols package vendoring wayland.xml + xdg-shell.xml pinned to the same wayland version. The scanner turns protocol XML into C marshalling glue that consumers compile to wasm32. A generate-and-verify test drives the scanner over the vendored XML. Part of the Wayland compositor stack; see docs/plans/2026-07-08-dri-wayland-compositor-plan.md (§3). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…pport (PR3)
Ports libwayland (client + server) to wasm32 linked against the PR1
libffi shim, and adds the three kernel primitives it needs:
- SO_PEERCRED getsockopt: returns struct ucred{pid,uid,gid} for a
connected AF_UNIX socket. wl_client_create refuses a client on error.
Single-user model reports the caller's own credentials (exact for the
canonical socketpair / same-process accept). +4 unit tests.
- epoll_pwait finite-timeout fix: persist a per-channel deadline so
wakeup-driven retries (retrySyscall) reuse it instead of resetting the
timeout on every readiness poke. A permanently-drained fd set now
parks the full timeout and returns 0 rather than retrying forever.
Host-side only, so dual-host parity holds.
- Drop the generic sendmsg/recvmsg arg-descriptors: arg 2 is flags, not
a length, so the generic pointer-copy path would splat ~flags bytes of
scratch over the caller's msghdr on the EAGAIN retry. Both syscalls are
hand-marshalled by dedicated host handlers.
Proven end-to-end by programs/wl_smoke.c — one process hosting a
libwayland server AND client over a kernel AF_UNIX socketpair: real
wl_marshal -> recvmsg -> demarshal -> wl_closure_invoke through the ffi
shim (create_surface + damage(7,11,100,200), all four i32 args checked),
plus wl_event_loop epoll park (~60ms) and prompt wake on a readable fd.
libwayland-smoke.test.ts gates it.
ABI: removes SYSCALL_ARG_DESCRIPTORS entries 137/138 from the snapshot
without an ABI_VERSION bump — the change is runtime-compatible (the host
already hand-marshals these). The vs-origin/main reconciliation bump is
deferred to stack integration.
Verified: 1084 kernel unit tests pass; libc-test 0 unexpected fail;
POSIX 0 fail; wl_smoke green against the rebuilt kernel.
Part of the Wayland compositor stack; see
docs/plans/2026-07-08-dri-wayland-compositor-plan.md (§5).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Port libxkbcommon 1.7.0 to wasm32 using the libwayland pattern: bypass meson, compile the core TUs against a hand-curated config.h, with bison generating the xkbcomp parser. Gated by host/test/libxkbcommon-smoke.test.ts driving programs/xkb_smoke.c (keymap compile + keycode/modifier -> keysym/UTF-8 translation) under the kernel. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…input 1.25.0 (PR5)
Ports the real libinput input stack for the Wayland compositor, replacing
the libinput-lite no-op stub as the compositor's input library. Lands as
one commit spanning the bottom-up sub-stack (PR5a+PR5b+PR5c), since real
libinput builds on all three lower layers. See
docs/plans/2026-07-08-dri-wayland-compositor-plan.md §3 (PR5a/5b/5c result
notes) and §5.
PR5a — libevdev 1.13.3 (packages/registry/libevdev/, libevdev.a). Wraps the
/dev/input/event* fd: owns the fd, does EVIOCG* capability probing, turns
the raw input_event stream into a resyncable per-slot state machine. Meson
bypassed; two core TUs compiled against a hand-curated config.h + a
python-generated event-name table from the bundled full linux UAPI headers.
Gate: host/test/libevdev-smoke.test.ts.
PR5b — the link + classification layers, plus the kernel st_rdev fix:
- mtdev (libmtdev.a) — link-only stub of mtdev's public ABI. libinput
references five mtdev_* symbols but calls them only for legacy protocol-A
multitouch (evdev_need_mtdev()); the kernel's single-touch virtual
devices never enter that path, so each entry point aborts if ever called.
- libudev (libudev.a) — PATH-mode shim reimplementing systemd v255 input_id
classification (test_pointers + test_key + EV_SW/scrollwheel fallbacks)
over EVIOCGBIT/EVIOCGPROP, synthesizing the ID_INPUT* tags libinput's
evdev core requires to accept a device. Other 13 udev fns are thin no-ops.
- ABI v16→v17: WasmStat grows 88→96 with a trailing st_rdev, and virtual
evdev nodes now stat as Linux char major 13, minor 64+N (event0 = 13:64).
Load-bearing for libinput's path backend, which drops the devnode and
hands udev_device_new_from_devnum only the st_rdev; the shim recovers the
node by scanning /dev/input/event* for the matching devnum.
Gates: host/test/{mtdev-smoke,libudev-input-id-smoke}.test.ts.
PR5c — real libinput 1.25.0 core (packages/registry/libinput/, libinput.a).
The 35 path-backend TUs (src_libfilter + src_libinput core + util +
quirks), meson bypassed, compiled against a hand-curated config.h +
sed-substituted version headers, linking libevdev + the libudev/mtdev
shims. src/udev-seat.c is dropped (the udev enumerate/monitor backend; a
symbol audit confirmed no in-set TU references it) — the compositor drives
the path backend only. The bundled full linux UAPI wins over the sysroot's
minimal <linux/input.h> via -Iinclude/linux + a <linux/types.h> shim.
Gate: host/test/libinput-smoke.test.ts drives the full chain —
libinput_path_add_device(event0) runs stat → st_rdev recovery →
udev_device_new_from_devnum → input_id → evdev_device_new → libevdev probe
→ device accepted, then a host-injected EV_KEY is read off the evdev ring
by libinput's epoll loop (sys_epoll_pwait → sys_poll) and decoded into a
LIBINPUT_EVENT_KEYBOARD_KEY.
Dual-consumer decision: SDL2 keeps depending on libinput-lite, not the real
port. SDL2 2.30 references zero libinput symbols (verified over the source
+ configure) — it uses libinput only as an optional-detection stub — so the
real 35-TU library would only bloat its dep graph for no gain. The real
libinput is a distinct consumer (this smoke + the PR6/PR7 compositor); its
archive is linked from the resolver cache prefix by full path, never
through $SYSROOT/lib/libinput.a (which stays the lite stub), so the two
never collide.
Verification: kernel unit 1087/0, libc-test 302/0 FAIL, POSIX 174/0 FAIL,
ABI check exit 0 (v17 consistent). Vitest: the four new smokes pass; the
remaining failures are ABI-16 package binaries (php/spidermonkey/wordpress)
awaiting the v17 release-tag matrix — not logic regressions.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ell/seat/output) (PR6) Adds a standalone libwayland *server* (wlcompositor) plus a raw libwayland-client test client, wiring wl_shm / xdg_shell / wl_seat / wl_output over real libinput (PR5) + libxkbcommon (PR4). Getting injected input to flow end-to-end surfaced a latent kernel+host bug: - Root-caused the epoll_event wasm32 struct-layout mismatch. The kernel and host marshalled epoll_event as packed 12 bytes / data@4, but on wasm32 the struct is unpacked → 16 bytes: events@0, pad@4, data@8 (__packed__ applies only on x86_64). At a single ready fd the offset-4 read/write accidentally cancel, so socket epoll worked all the way to CLIENT_READY; at i>=1 the 12-vs-16 stride desyncs and the 2nd event's data comes back 0. libinput registers 3 fds (timer + 2 evdev), so injected input was the first workload with 2 ready at once — libinput_dispatch read a NULL source and call_indirect'd on garbage. Fixed across all 4 marshalling sites: kernel_epoll_ctl, kernel_epoll_pwait (crates/kernel/src/wasm_api.rs) and handleEpollCtl, handleEpollPwait (host/src/kernel-worker.ts). kernel-worker.ts is the authoritative host path, shared by Node + browser — dual-host parity. - Nested-epoll readiness: poll_check → poll_check_depth with POLL_CHECK_MAX_DEPTH and a FileType::Epoll arm, so an epoll fd nested in another epoll reports POLLIN when its inner interest is ready. - SCM_RIGHTS prime-bo sidecar (InFlightFd.prime_bo in pipe.rs + extract_scm_rights/install_scm_rights_fds in wasm_api.rs) so a cross-process wl_shm buffer composites correctly. Regression coverage: test_epoll_pwait_multiple_events_data (each ready fd returns its own data, none 0) + test_epoll_nested_readiness in crates/kernel/src/syscalls.rs, and host/test/wlcompositor-smoke.test.ts which exercises the host epoll path end-to-end (GOT_KEY + GOT_BTN + CLIENT_OK). epoll marshalling is not ABI surface, nested-epoll is behavioral, SCM_RIGHTS is kernel-internal — ABI_VERSION stays 17. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rument frame-address fix (PR7)
PR7 Phase 4 lands the userspace terminal for the Wayland stack and fixes a
latent fork-instrumentation bug that its larger memory footprint exposed.
New userspace:
- examples/libs/wpkdraw — software 2D draw lib (clear/pixel/rect + an
stb_truetype font engine over bundled Inconsolata) for client-side
framebuffer rendering.
- examples/libs/libkwl — minimal Wayland client toolkit (wl_shm/xdg_shell/seat)
wrapping wpkdraw surfaces.
- programs/wlterm — libkwl VT100 terminal (vt100.c parser) running a forkpty'd
dash, composited by wlcompositor on /dev/dri/card0 via KMS.
- programs/wpkdraw_smoke, programs/kwldemo — smoke drivers.
- host/test/{wpkdraw,libkwl,wlterm}-smoke.test.ts — Node smoke gates.
- apps/browser-demos/test/kandelo-wayland.spec.ts — end-to-end browser gate
(compositor presents a non-blank frame; typed input routes to the grid).
Kernel: PTY master read() now drains buffered slave output BEFORE reporting
EOF when the slave has closed — a shell writing its final bytes and exiting
in one go must not lose them (regression test
test_pty_master_read_drains_output_after_slave_close).
fork-instrument fix (crates/fork-instrument):
- Frames are stored at `_wpk_fork_buf + current_pos` where `current_pos` is a
buffer-relative offset. The postamble (write) and preamble (read) helpers
used `current_pos` directly as an absolute address, dropping the
`+ _wpk_fork_buf` add, so every fork save-frame was written to absolute low
memory (addr 16..). Small programs survived by luck; wlterm's larger memory
collided with live data, corrupting a call_indirect funcidx and trapping with
"table index out of bounds" during the child rewind replay.
- Convert `current_pos` -> absolute pointer at both access sites; keep cursor
advance in offset space. Frame layout / header / wpk_fork_* exports unchanged
— not an ABI change (snapshot unchanged, no ABI_VERSION bump). Updated 3 unit
tests that pinned the exact instruction sequences.
- docs/fork-instrumentation.md documents the `frame @ buf + current_pos`
invariant and the historical absolute-addressing bug.
Verification: cargo fork-instrument + kernel (1090) pass; wayland smokes pass;
libc-test 0 unexpected FAIL; POSIX 0 FAIL; check-abi-version.sh exit 0 no bump;
browser kandelo-wayland.spec.ts passes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… WPK_BIND_FOREIGN_TEXTURE, WebGL2 scanout presenter, wpkdraw AA, kernel + host fixes (PR8)
Boots a full Wayland desktop in the browser (`/?demo=wayland`) and under
node: wlcompositor composites wlclock (animated analog clock), wlpaint
(pointer painting), and wlterm (VT100 terminal over a forkpty'd dash)
onto /dev/dri/card0, with typing, titlebar window drags, and
drag-painting routed end-to-end through libinput + wl_seat. In the
browser the compositor now COMPOSITES ON THE GPU: client wl_shm buffers
are imported as WebGL textures through the newly-dispatched
DRM_IOCTL_WPK_BIND_FOREIGN_TEXTURE and rendered as GLES quads straight
to the display canvas (first slice of the wayland plan's §7 F′ GPU
tier). Headless hosts (node smokes) keep the CPU blit + the new WebGL2
scanout presenter path. wpkdraw gains anti-aliased line/disc primitives
and wlclock uses them — hands, ticks and hub render smooth at 1:1.
GPU compositing (plan §8.10; kernel + host + glue + compositor):
- kernel: DRM_IOCTL_WPK_BIND_FOREIGN_TEXTURE dispatched for CPU-tier
bos — resolves the caller's fd-local GEM handle, validates the GL
session on the SAME fd, and forwards to the new
host gl_bind_foreign_texture import; host failure surfaces EIO so
callers degrade. Unit test drm_bind_foreign_texture_uploads_and_
writes_texture_id (happy path + ENOENT/EINVAL/EIO). Additive ioctl +
import: no ABI-surface change (snapshot unchanged).
- host (shared by BOTH hosts, kernel.ts + webgl/foreign-texture.ts):
uploads the bo's canonical SAB pixels into a per-binding WebGLTexture
(stable id from 0x40000000, mirrored into the cmdbuf texture table so
glBindTexture resolves it; UNPACK_ROW_LENGTH honors the bo stride;
creator's live mmap flushed to the SAB first via syncCreatorToSab;
texSubImage2D reuse on steady state; save/restore of the shared
context's texture/unpack state since this runs outside the muxer).
Texture lifetime is tied to the bo: gbm_bo_destroy drops it across
all bindings (dropForeignTexturesForBo).
- libEGL glue: wpkEglImportDmabufHandle / wpkEglBindBoTexture /
wpkEglCloseBoHandle (stand-in for EGL_EXT_image_dma_buf_import), and
eglCreateWindowSurface now honors EGL_WIDTH/EGL_HEIGHT attribs (the
host resizes the canvas drawing buffer — a compositor creates its
surface before its first ADDFB, when the fb-based resize has nothing
to size against).
- wlcompositor: probes GL at boot by compiling its quad shader (sync
queries fail cleanly on headless hosts → CPU path; WLC_NO_GPU=1
forces it; one-shot WLC_RENDERER gpu|cpu marker). GPU frames render
wallpaper texture + z-ordered window quads + focus border in ONE
cmdbuf flush (the canvas transitions atomically between complete
frames — the flicker gate stays meaningful), with per-commit dirty
tracking so only changed buffers re-upload. PAGE_FLIPs continue as
the frame clock (frame callbacks, flip counters). COMPOSITE_SAMPLE
comes from a 1×1 glReadPixels on the GL path. A runtime GL failure
tears down EGL and falls back to the CPU blit permanently.
- canvas ownership hand-off (kernel-worker.ts + kernel.ts):
markKmsCanvasGlOwned now fires only after getContext succeeds,
disposes the pump's webgl2-scanout presenter (same underlying
context) and reports stats slot 7 = 3 (Modeset chip "webgl2-gl");
new markKmsCanvasGlReleased (fired from context destroy / session
teardown via the binding's claimedKmsCrtc) restores the pre-claim
presenter mode so a degrading compositor never freezes the canvas.
The presenter's context is created with preserveDrawingBuffer so the
inheriting compositor's cross-task readbacks survive the present.
- spec gate 1d asserts WLC_RENDERER gpu in the browser.
wpkdraw anti-aliasing (the "jagged clock hands" fix):
- wpk_line_aa (capsule distance field, round caps, per-pixel coverage
scales the colour's alpha) + wpk_disc_aa; __builtin_sqrtf lowers to
wasm f32.sqrt so consumers stay libm-free. wlclock hands/ticks/hub
now draw with them (one AA capsule per hand instead of stamped
squares). wpkdraw build.toml revision 2; smoke program + vitest
assert full-alpha cores and partially-covered AA fringes.
Client-GPU evaluation (plan §7.1, new): do NOT port the wpkdraw
clients to GL; finish the GPU tier for already-GL apps instead —
PR10 WPK_CREATE_GPU_BO as WebGLTexture-backed bos rendered into FBOs
on ONE muxer-shared context (bind-foreign-texture then degenerates to
a texture-id lookup, true zero-copy), PR11 zwp_linux_dmabuf_v1 (thin
now that the compositor texture path exists), PR12 SDL2 wayland
backend, PR13 wlcube.
The desktop is otherwise presented through the session-6 WebGL2 scanout
path (GPU swizzle + scaling) rather than the 2D putImageData blit.
New programs and demo wiring:
- programs/wlclock.c, programs/wlpaint.c — libkwl floating-window
clients (poll-paced animation; wl_pointer painting).
- libkwl: CSD titlebar buttons, pointer enter/leave/motion plumbing,
close-box exit (kwl.h/kwl.c).
- wlcompositor: placement rules for the three clients, focus border,
MOVE_GRAB/MOVE_END + WLPAINT_STROKE/COMPOSITE_SAMPLE markers, loud
stderr on gbm_bo_map/drmModeAddFB failure, software cursor removed
(the browser host pointer is mapped absolutely — a sprite would sit
exactly under it; hover no longer schedules no-op repaints).
Input-batch repaint coalescing + peg suppression: the browser bridge
emulates each absolute pointer move as an EV_REL peg-to-(0,0) +
jump pair, and per-event synchronous repaints rendered every drag
update with the grabbed window teleported to the top-left corner.
Repaints now defer to the end of the libinput drain loop, and a peg
frame (REL ≤ −2048 on both axes — impossible from real hardware)
updates the cursor without delivering motion to grabs or clients.
- Modeset pane: the canvas renders with object-fit: contain instead
of stretching — a 16:9 desktop distorted into a wider pane smeared
every 1-px glyph non-uniformly ("everything looks pixelated").
Pointer mapping and the spec's desktopPoint helper map through the
fitted (letterboxed) content box, using the kernel-reported scanout
dims (stats slots 2/3) rather than canvas.width — Chrome reflects
the committed OffscreenCanvas bitmap size back into the placeholder
attributes, which under the display-sized presenter is NOT the fb.
The status chip appends the active renderer (stats slot 7).
- wlterm: 960×540 window sized for the three-client desktop layout.
- live-setup.ts: stages the four binaries, attaches BrowserInputSource
(keyboard → event0; pointer owned by the Modeset pane → event1), and
opts the Modeset canvas into the vblank pump's WebGL2 scanout
presenter (setKmsDisplayMode "webgl2-scanout") — the compositor
renders CPU-side, so the program-owned WebGL2 bridge never fires
for it.
- kernel-host.ts (kandelo-session): setKmsDisplayMode lets boot flows
pick the canvas paint mode before the pane mounts; attachKmsDisplay
no longer hardcodes the webgl2 default, and for webgl2-scanout
canvases a ResizeObserver (devicePixelContentBoxSize) streams the
pane's device-pixel size to the presenter via the new dual-host
kms_set_display_size message.
WebGL2 scanout presenter (kernel-worker.ts, mode "webgl2-scanout",
new; the legacy "2d" blit remains):
- The vblank pump owns a WebGL2 context on the CRTC canvas and draws
the scanout bo as a texture: fragment-shader XRGB8888→RGB swizzle
(replaces the per-pixel CPU loop), GL-viewport letterbox (same
contain math as the pane pointer mapping), and GPU scaling at the
embedder-reported display resolution with trilinear-over-mipmap
minification — a 1920×1080 desktop shown smaller no longer shimmers.
- Presents are change-driven, not unconditional 60 Hz: kernel commit
count (PAGE_FLIP) + fb id + target size, with a ~15 Hz strided-
checksum content probe as backstop for scanout mutations that never
flip. The probe is load-bearing for the flicker gate's detection
power: flip-synced presents alone sample a latch-pinned bo BEFORE
its repaint and would hide the PAGE_FLIP-latch regression (negative
control re-verified under WebGL: latch removed → 2/120 dropouts,
median 19.2 KB; fixed kernel → 0/120).
- Software-GL hosts (headless Chromium/SwiftShader, where mipmap +
trilinear cost ~100 ms/frame and would starve the kernel worker)
auto-degrade to bilinear when a steady-state present exceeds the
16 ms frame budget; hardware GL never trips it. WebGL2 acquisition
failure (node, no polyfill) is cached and the CRTC degrades to
stats-only.
- Stats slot 7 reports the active presenter (1=2d, 2=webgl2-scanout);
protocol + host wiring (kms_set_display_size, mode union) landed on
BOTH hosts (browser-/node-kernel-protocol.ts, both worker entries,
both kernel hosts).
Dynamic connector mode — the desktop fills the pane (no letterbox):
- host_kms_mode_info derives the PREFERRED mode from the embedder-
reported display size (buildVirtualConnectorMode: round(1080×aspect)
× 1080, width clamped [1440, 3840], even-aligned; 1920×1080 fallback
for Node/headless — node smokes and GL demos are unaffected).
- live-setup.ts feeds the pane's device-pixel size to the kernel
BEFORE spawning wlcompositor (waits ≤1.5 s for the pane's
ResizeObserver; measures the canvas directly as a fallback).
- wlcompositor placement rules are edge-anchored (negative x = offset
from the right edge: wlclock width-680, wlpaint width-840); resolved
coordinates are byte-identical to the old fixed layout at 1920, and
wider modes spread the demo across the full width. The mode is fixed
at boot; post-boot resizes letterbox rather than re-mode.
- kandelo-wayland.spec.ts parses the live mode from the Modeset chip
(readDesktopDims) and derives all window geometry (clock titlebar,
wlterm region, paint strokes) from the anchors.
- Unit tests: buildVirtualConnectorMode aspect/clamp/even-align/
fallback (dri-kms-registry.test.ts).
Kernel fixes (shared by BOTH hosts):
- PAGE_FLIP now latches the host scanout fb (host.kms_set_fb) at ioctl
time. Previously only SETCRTC latched, so the host's 60 Hz 2D blit
scanned out the first bo forever — the double-buffered compositor's
BACK buffer every other frame — and the desktop flickered randomly
with windows missing mid-composite. Unit test
kms_page_flip_latches_host_scanout_fb; browser flicker gate verified
to fail on the pre-fix kernel.
- munmap length now rounds up to the 64 KB wasm page (Linux semantics).
The compositor maps/unmaps its scanout bo every frame; the literal-
length free stranded a tail remnant per cycle (~8.3 MB address space
per flip), hitting ENOMEM and a silent desktop freeze at ~124 frames.
Two unit tests (memory.rs).
- TFD_TIMER_ABSTIME timerfd targets now compare against the timerfd's
own clock_id — libinput arms CLOCK_MONOTONIC absolute timers, and
comparing them against CLOCK_REALTIME made every monotonic timer
look already-expired on hosts where the two epochs differ.
Host runtime fixes (kernel-worker.ts — shared by BOTH hosts):
- Finite poll/select/pselect timeouts never expired while the system
was busy: every EAGAIN retry re-entered the handler and recomputed
the deadline. New blockingWaitDeadlines map persists the first-block
deadline across retries (same pattern as epollWaitDeadlines);
cleared on completion/abandon/process cleanup. wlclock ran at
~0.5 fps and froze at idle without this.
- Blocking-retry keys are pid-qualified (retryKey pid:channelOffset)
and per-pid import sync (syncImportsForPid) fixed for multi-process
KMS demos.
- The 2D KMS blit swizzles DRM XRGB8888 → RGBA (R and B were swapped:
wlpaint's blue painted orange). Under the webgl2-scanout presenter
the same swizzle happens in the fragment shader.
Tests:
- host/test/wldesktop-smoke.test.ts (node): two clients composite,
per-client input routing, titlebar move grabs.
- host/test/wldesktop-liveness-smoke.test.ts (node): PAGE_FLIP commits
keep advancing at idle and across 4 drag-paint strokes (caught the
poll-deadline and munmap bugs at 100% pre-fix).
- kandelo-wayland.spec.ts (browser): gate 1c asserts the webgl2
renderer is active (Modeset chip, stats slot 7), gate 3 samples the
desktop's top-left corner per drag step (teleport guard), + gate 4
drag-paint liveness via the Modeset flips chip, + gate 5 flicker
stability (120 rapid canvas screenshots; no frame < 90% of median
PNG size — fails on the pre-latch kernel under BOTH the 2d blit and
the WebGL presenter, passes post-fix), syslogStream joins .ksys-msg
spans so chunk-split markers match.
- host/test/dri-kms-stats-sab.test.ts: webgl2-scanout coverage — fake
GL context (upload/texSubImage2D reuse, letterbox viewport math,
slot 7, present-on-change skip, content-probe re-present),
no-WebGL2 graceful degradation with cached getContext failure,
the 2d-path XRGB→RGBA swizzle at pixel level, the GL claim/release
canvas handoff (slot 7 = 3, pump stand-down, presenter rebuild on
release), and the software-GL bilinear degrade.
- host/test/webgl-foreign-texture.test.ts: BIND_FOREIGN_TEXTURE TS
upload path — id allocation from 0x40000000, texImage2D→
texSubImage2D reuse, stride-aware upload with GL state
save/restore, EINVAL/EIO, dropForeignTexturesForBo cleanup.
- host/test/dri-registry.test.ts: GbmBoRegistry coherence helpers —
dims() without pid membership, syncCreatorToSab flush direction,
hasStaleableImports gate, syncImportsForPid delivery.
- wlcompositor/libkwl smokes extended for the new client behaviors,
incl. shifted-key modifiers delivery (GOT_TEXT "A") and the
WLC_RENDERER cpu probe fallback on Node.
Docs: browser-support.md (modeset + wayland demo rows, Wayland desktop
demo section incl. GPU compositing + the WebGL2 scanout presenter,
boot-pattern legend), architecture.md (PAGE_FLIP latch semantics,
BIND_FOREIGN_TEXTURE + wpkEgl* extension, GPU-first compositing,
blocking-wait deadline persistence, 1920×1080 preferred mode,
three-client demo + webgl2-scanout presenter), posix-status.md (munmap
rounding, card0 + renderD128 entries), wayland compositor plan §7
(F′ partial landing + §7.1 client-GPU evaluation) and §8 post-PR7 gaps
(items 1-10, incl. the WebGL flip, its negative-control
re-verification, the dynamic connector mode, and GPU compositing).
Both hosts verified: node via the five wl smokes + full vitest
(3 pre-existing baseline failures only: wasm-binary-parse,
fork-dlopen-replay-e2e, spidermonkey-node-compat); browser via
Playwright kandelo-wayland (incl. new gate 1d WLC_RENDERER gpu) +
kandelo-modeset + kandelo-sdl2, plus a visual screenshot check of the
GPU-composited desktop and the AA clock. Cargo lib 1096 passed /
0 failed. libc-test and Open POSIX suites at baseline (0 unexpected
FAIL). check-abi-version.sh exit 0 — the new ioctl + kernel-wasm
import are additive; no ABI surface change, no further ABI_VERSION
bump beyond the branch's existing v17.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…unding unbind, implicit pointer grab, compositor UAF + present fallback, presenter/foreign-texture fixes, reboot display sizing, new gates Second devil's-advocate audit of the PR8 three-client Wayland desktop (2d02785): fix real bugs in the new code, remove stale code, give every new behavior a dedicated test, trim WHAT-comments (keep WHY). Kernel (crates/kernel/src/syscalls.rs): - sys_munmap rounds len up to the 64K page BEFORE the fb0/DRI coverage sweeps (they used raw len while MemoryManager::munmap rounds — a map(aligned)/unmap(raw bo size) pair freed pages the host kept mirroring); checked_add on the rounded len fixes the overflow edge. Test: munmap_dri_raw_len_unbinds_page_aligned_binding. - timerfd lazy-recompute deduped into timerfd_refresh(), used by both the sys_read and poll arms. Test: test_timerfd_expiry_surfaces_through_epoll (libinput's epoll shape). Host (kernel-worker.ts + webgl/, shared by BOTH hosts): - imported-bo coherence hook moved AFTER the EAGAIN branch, gated (POLL||PPOLL) && retVal > 0 — it listed 4 unreachable syscalls and burned MB-scale copies on every blocked-poll retry. - buildKmsGlPresenter sanitizes inherited GL state at entry (the release-path rebuild runs on the dead compositor's dirty context). - attachKmsCanvas rebuilds the presenter when a remounted pane brings a new canvas for the same CRTC (was painting the orphaned canvas). - foreign-texture.ts queries + restores UNPACK_ROW_LENGTH (not shadow- tracked; the guest-set value was clobbered to 0). - registry.ts unbind() deletes the binding's foreign textures deterministically (the shared context outlives the binding). - KMS scanout shader mediump -> highp (fp16 texel addressing breaks above 2048 px; modes go to 3840). - removed a stale selEntry.channel identity guard in the select-retry cancel branch (retryKey is pid-qualified). wlcompositor + libkwl + wlpaint (C): - implicit pointer grab: focus stays pinned to the pressed surface while any button is down; click-to-focus only on the first press; releases always delivered; focus follows the cursor again after the last release. - libkwl consumes only titlebar PRESSES; every RELEASE is forwarded to the app (content y < 0) — fixes stuck drags ending over the titlebar. - all live surfaces tracked (not just mapped/z-ordered): never-mapped surface attach -> wl_buffer.destroy -> commit was a use-after-free; create_surface posts no_memory when full instead of partial tracking. - failed eglSwapBuffers degrades to CPU compositing like a failed texture bind (was ignored -> frozen canvas). - keymap fd leak: fd stored in the wl_keyboard resource user data and closed on resource destroy. - wlpaint: #include <linux/input-event-codes.h> replaces a wrong #ifndef BTN_LEFT fallback; new WLPAINT_COLOR/WLPAINT_CLEAR/ WLPAINT_STROKE_END markers for the smoke gates. - wpk_disc_aa delegates to wpk_line_aa (byte-equivalent); kwldemo/ wlclock/wlpaint link recipes folded into one loop; wpkdraw build.toml inert revision reverted. kandelo-session (web-libs, browser reboot bug): - detachKernel() clears kmsDisplaySizes and the attachKmsDisplay ResizeObserver disconnects once its kernel is detached — a second wayland boot in the same page read the dead session's size, never pushed a mode to the new kernel, and letterboxed at 1920x1080 while the stale observer kept feeding the shared map. 2 unit tests. New gates: - wldesktop-smoke: swatch click -> WLPAINT_COLOR i=1, clear click -> WLPAINT_CLEAR, and two implicit-grab gates (release over wallpaper / over wlpaint's own titlebar -> WLPAINT_STROKE_END). - kandelo-wayland.spec gate 1e: GPU-path COMPOSITE_SAMPLE (glReadPixels readback) present + non-black — previously asserted by no test. - wldesktop-liveness-smoke stats SAB 7*4 -> 8*4 (slot 7 = renderer). Docs/comments: GPU-compositing-is-steady-state corrections in live-setup.ts + kernel-host.ts; Modeset.tsx pointer-scaler bullet (stats slots 2/3, not canvas.width); libkwl-smoke sample point is the CSD titlebar; architecture.md node-gate list includes both wldesktop smokes; wlcompositor-smoke socket path /tmp/wayland-0; WLC_NO_GPU described as a manual debug escape hatch. Deliberately not fixed: guest sees EIO for bind-foreign-texture host failures (host distinctions TS-tested); select/pselect import-coherence gap (pre-existing, no in-tree consumer); single-sample bilinear degrade kept (recommend a 2-consecutive-overrun counter). Tests: cargo 1098/0; ABI check exit 0 (no snapshot drift); vitest full solo 897 pass / 3 fail = pre-existing baseline (wasm-binary-parse, fork-dlopen-replay-e2e, spidermonkey cowsay); 6 wl node smokes green solo; Playwright wayland+modeset+sdl2 3/3; libc-tests exit 0 (XFAIL only, baseline FLAKE-PASS pthread_cond-smasher, TIME raise-race); posix-tests exit 0, 0 FAIL. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Resolves the kernel-worker.ts conflict between two independent fixes for the same finite-timeout-starvation bug: - base (d2f79e1 "vblank select starvation"): threads a deadlineOverride parameter through handleSelect/handlePselect6 to persist the absolute deadline across re-dispatches, plus a separate vblankWakeCarry gate so the pump only broad-wakes on an actual KMS flip. - ours (wayland series): a blockingWaitDeadlines map keyed by the pid-qualified retryKey, wired through pid teardown, covering select, pselect6, AND poll. Kept ours for the deadline-persistence half (it subsumes deadlineOverride — covers all three paths, persists via the map so no arg threading is needed, and carries the pid-qualified keying audit fix) and removed the now-dead deadlineOverride parameter + its call-site args. Kept the base's vblankWakeCarry flip-gating verbatim (orthogonal optimization) and merged the pump comment to reflect that both the 2d and webgl2-scanout presenter paths run. Host TS build/typecheck clean; dri-kms-stats-sab, wldesktop and wldesktop-liveness smokes pass solo. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase B-1 matrix build status —
|
| 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.
Tiered plan extending the dri-wayland compositor plan past its §7 post-v1 milestones. PR10–PR13 (GPU tier, zwp_linux_dmabuf_v1, SDL2 wayland backend, wlcube) stay the Tier 0 prerequisite; GTK/glib moves to Tier 2. Groundwork for the follow-up Hyprland PR, which stacks on this Wayland branch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
mho22
force-pushed
the
explore-dri-wayland
branch
from
July 16, 2026 08:44
ffb750a to
4440a9d
Compare
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>
mho22
force-pushed
the
explore-dri-wayland
branch
from
July 16, 2026 11:34
4440a9d to
e9ca2d3
Compare
mho22
marked this pull request as ready for review
July 17, 2026 08:40
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Driving the PR8 Wayland desktop (
/?demo=wayland) as a real user surfaced defects that every marker-based gate passed straight over. A pointer drag whose release landed off the window left the client stuck mid-stroke. Unmapping a DRI buffer with an unrounded length leaked address-space pages the host kept mirroring. A never-mapped surface whose buffer was destroyed then committed hit freed memory. And a second Wayland boot in the same browser tab letterboxed at 1920×1080 instead of filling the pane. None of these were caught because the existing gates assert on printf markers, not on grab semantics, page accounting, or reboot state.Root cause
sys_munmapran its fb0/DRI coverage sweeps against the raw length whileMemoryManager::munmaprounds to the 64K page, so amap(aligned)/unmap(raw bo size)pair freed pages that stayed host-mirrored.eglSwapBufferswas ignored and froze the canvas.detachKernel()never cleared the per-CRTC display-size map and the pane'sResizeObserverkept feeding the dead kernel, so the next boot skipped its sizing handshake.Fix
checked_addclosing the overflow edge.wl_keyboarddestroy.detachKernel()and disconnect the staleResizeObserveronce its kernel is gone, so a re-boot re-measures the pane and pushes a real mode.UNPACK_ROW_LENGTH, delete a binding's foreign textures on unbind, and raise the scanout shader tohighpfor modes above 2048 px.Both hosts
The kernel and
host/src/{kernel-worker,webgl/*}.tschanges are shared code and take effect on Node and the browser together. The browser-only surface is thekandelo-sessionreboot fix and the new Playwright gate 1e (the GPU compositing path exists only in the browser; the Node smokes assert the inverse,WLC_RENDERER cpu). New regression coverage is added 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-pathglReadPixelsCOMPOSITE_SAMPLEis present and non-black, andkandelo-sessiongets two unit tests for the detach/reboot display-size lifecycle. Kernel changes carry two new unit tests.Validation
wasm-binary-parse,fork-dlopen-replay-e2e, spidermonkey cowsay).pthread_cond-smasher, TIMEraise-race).Deliberately left as-is (with reasons in the commit body): the guest sees
EIOfor all host bind-foreign-texture failures, select/pselect completions have no import-coherence sync (no in-tree consumer), and the single-sample bilinear degrade is kept pending a follow-up consecutive-overrun counter.Scope
No
ABI_VERSION, package recipe, package artifact, VFS image, or binary-index changes. One planning doc is included —docs/plans/2026-07-14-build-hyprland-class-compositor-plan.md, the roadmap the follow-up Hyprland PR will stack on this branch; the transient handoff docs are left untracked.🤖 Generated with Claude Code