Evdev on Kandelo - /dev/input keyboard and pointer, plus an espeak-ng speech demo - #698
Evdev on Kandelo - /dev/input keyboard and pointer, plus an espeak-ng speech demo#698mho22 wants to merge 26 commits into
Conversation
Phase B-1 matrix build status —
|
| Package | Arch | Status | Sha |
|---|---|---|---|
| libcurl | wasm32 | built | 05aae726 |
| libcxx | wasm32 | built | 7924fbe0 |
| libcxx | wasm64 | built | cfe94678 |
| libpng | wasm32 | built | e11caae0 |
| libxml2 | wasm32 | built | 6db06345 |
| libxml2 | wasm64 | built | bfabb4ff |
| openssl | wasm32 | built | 2708b58d |
| openssl | wasm64 | built | a1be8942 |
| sqlite | wasm32 | built | 0affae3a |
| sqlite | wasm64 | built | 2638b6ad |
| zlib | wasm32 | built | a74da2ed |
| zlib | wasm64 | built | 76101599 |
| bc | wasm32 | built | 46973bfb |
| bzip2 | wasm32 | built | ffa5f4c4 |
| coreutils | wasm32 | built | 4d8239d1 |
| curl | wasm32 | built | dca90723 |
| dash | wasm32 | built | ad59e65d |
| diffutils | wasm32 | built | d67cb386 |
| dinit | wasm32 | built | 6780ca3d |
| fbdoom | wasm32 | built | a4dcc46f |
| file | wasm32 | built | d2312c00 |
| findutils | wasm32 | built | 3767b4e1 |
| gawk | wasm32 | built | faedad8e |
| git | wasm32 | built | f50d2974 |
| grep | wasm32 | built | b9b6b1e6 |
| gzip | wasm32 | built | 539fee04 |
| kandelo-sdk | wasm32 | built | bdddb4fe |
| kernel | wasm32 | built | 51062af5 |
| less | wasm32 | built | 5180eac9 |
| lsof | wasm32 | built | 96fa5be8 |
| m4 | wasm32 | built | 0aa7ce01 |
| make | wasm32 | built | 5cdd9bd8 |
| mariadb | wasm32 | built | 4c185e36 |
| mariadb | wasm64 | built | ec3ea5f8 |
| msmtpd | wasm32 | built | aa3de664 |
| nano | wasm32 | built | 4a52b067 |
| ncurses | wasm32 | built | ff1ca5a3 |
| netcat | wasm32 | built | 20b49fdf |
| nginx | wasm32 | built | 5c15a366 |
| php | wasm32 | built | f1bfba4b |
| posix-utils-lite | wasm32 | built | df2ce6e9 |
| sed | wasm32 | built | 80e62002 |
| spidermonkey | wasm32 | built | 0b3b3066 |
| tar | wasm32 | built | 0532dd66 |
| tcl | wasm32 | built | eba6fe94 |
| unzip | wasm32 | built | b4f32850 |
| userspace | wasm32 | built | 1d4c6a6f |
| vim | wasm32 | built | 9895fd65 |
| wget | wasm32 | built | 6c82e2c4 |
| xz | wasm32 | built | 7bf9b496 |
| zip | wasm32 | built | 261d8148 |
| zstd | wasm32 | built | 0c16fdb0 |
| bash | wasm32 | built | 15fc009b |
| mariadb-test | wasm32 | built | 884a67b1 |
| mariadb-vfs | wasm32 | built | dd1e248f |
| mariadb-vfs | wasm64 | built | f3286762 |
| nethack | wasm32 | built | 73993e86 |
| node | wasm32 | built | 2e1b58a3 |
| spidermonkey-node | wasm32 | built | 22cfbf7c |
| vim-browser-bundle | wasm32 | built | 0deeb2d2 |
| nethack-browser-bundle | wasm32 | built | 8561a152 |
| rootfs | wasm32 | built | 74c566e1 |
| shell | wasm32 | failed | — |
| lamp | wasm32 | failed | — |
| node-vfs | wasm32 | failed | — |
| wordpress | wasm32 | failed | — |
Auto-generated; replaced on each push. Raw data in the publish-status workflow artifact.
brandonpayton
left a comment
There was a problem hiding this comment.
Codex review
This feedback is from Codex (OpenAI), reviewing at Brandon's request.
Summary
I do not recommend adopting this kernel ALSA surface as Kandelo's sound API for the stated SDL goal. The browser AudioWorklet, shared-ring transport, dual-host AudioDriver abstraction, and end-to-end eSpeak demonstration are useful engineering work. However, the demo proves a Kandelo-specific pcaudiolib backend and the host playback path; it does not demonstrate compatibility with SDL's ALSA backend, alsa-lib, or ordinary ALSA applications.
ALSA is a large Linux kernel/userspace contract, not simply a PCM node plus several ioctl numbers. Implementing /dev/snd/* usefully requires the control plane, device/card enumeration, hardware and software parameter negotiation, state and XRUN behavior, poll semantics, read/write and mmap paths, and the ABI details expected by alsa-lib. Carrying a small custom subset under ALSA names risks freezing an interface that looks compatible but is not.
For Kandelo's immediate objective, I recommend a smaller implementation-neutral PCM core with a correct OSS-compatible /dev/dsp frontend.
Completeness concerns
-
This does not establish SDL ALSA compatibility. SDL calls a broad
libasoundAPI; it does not normally issue these raw kernel ioctls itself. This PR does not port or testalsa-lib, and eSpeak uses a new Kandelo-specific pcaudiolib backend instead. The resulting demo is valuable, but it should not be treated as evidence that SDL or other ALSA software will run unmodified. -
The control device is intentionally a stub. The PR description says
/dev/snd/controlC0opens but all ioctls fail. A node that passes an existence probe but cannot enumerate or describe a card is not a complete ALSA control implementation and can cause consumers to fail later in less obvious ways. -
The advertised PCM ABI is only a narrow subset. Capture is absent, the full control/device discovery contract is absent, and compatibility with real
alsa-libread/write and mmap behavior has not been demonstrated. Unsupported functionality should not be advertised under standard ALSA device names unless the supported conformance boundary is explicit and tested. -
The proof is application-specific. The custom eSpeak backend is coupled to exactly the implemented subset, so it cannot reveal missing behavior that a general ALSA client would exercise. At minimum, a claim of ALSA compatibility would require an unmodified
alsa-libplus representative clients and negative/state-transition tests. -
Required verification is incomplete. The PR says the musl libc-test and Open POSIX Test Suite were not run. Repository policy requires both before completion, even for an additive change. The full Vitest suite also needs to run, not only selected audio and package-system files.
-
The size cost is not isolated. This PR is merged with large DRI/input work, so its source diff and final kernel artifact do not show the incremental cost of ALSA. Before choosing this API, measure the audio-only delta in raw and compressed kernel Wasm, host/worklet JavaScript, steady-state ring memory, and userspace artifacts such as libraries and test/demo binaries.
Direction Brandon is exploring
Brandon is exploring deeper /dev/dsp support on a clean branch, with the goal of satisfying SDL without implementing the Linux ALSA kernel ABI.
There are strong reasons for that direction:
- Upstream SDL2 and SDL3 already contain an OSS
dspbackend. SDL2 primarily relies on blockingwrite()for pacing; SDL3 additionally usesSNDCTL_DSP_GETOSPACE. - Kandelo already has
/dev/dsp, its SDK header, and an existing non-SDL consumer, so this is an incremental compatibility improvement rather than a second sound ABI. - FreeBSD's native PCM framework exposes an OSS-compatible
/dev/dspAPI rather than kernel ALSA. FreeBSD supplies ALSA compatibility in userspace over OSS, which is also a viable later path for Kandelo if Linux-specific applications become important. - POSIX does not standardize audio. A small Unix device extension does not make Kandelo less POSIX, while ALSA does not make it more POSIX.
- SDL performs application-side conversion and resampling, so the kernel does not need ALSA's full negotiation and plugin model merely to provide useful playback.
The /dev/dsp work should be source/API compatible rather than claiming host binary compatibility. Kandelo should own and ABI-snapshot its wasm32 ioctl values and fixed-width structures such as audio_buf_info.
For SDL-quality playback, the current /dev/dsp implementation still needs real backpressure: blocking writes, O_NONBLOCK/EAGAIN, truthful POLLOUT with wakeups, GETOSPACE, actual drain/reset behavior, audio-clock-driven consumption, and deterministic open-file-description ownership. It must stop dropping the oldest queued audio when full. Playback-only is a reasonable first boundary, but capture opens should then fail rather than advertise a recording device that returns EOF.
Suggested disposition
I would treat this PR as a valuable audio-transport exploration and reuse the sound parts selectively:
- retain the
AudioDriverseparation and dual-host wiring; - retain the AudioWorklet-driven browser consumption concept;
- retain deterministic Node pacing and host instrumentation where useful;
- move those pieces behind a generic PCM stream rather than ALSA state/mmap structures;
- do not merge
/dev/sndand the partial ALSA UAPI as Kandelo's public sound contract solely for SDL.
If broad compatibility with unmodified Linux ALSA applications later becomes an explicit product requirement, evaluate a userspace alsa-lib OSS/native PCM adapter first. A kernel ALSA implementation should only be undertaken with a defined compatibility target and conformance suite.
— Codex (OpenAI), at Brandon's request
Initial port of the DRI v2 work (PRs #58/#61–#66 against mho22/wasm-posix-kernel) onto current upstream/main. This commit covers: - **shared ABI**: append `pub mod gl` (cmdbuf opcodes + GLES2 sync-query tags + marshalled ioctl arg structs) and `pub mod dri` (DRM ioctl numbers, fourcc constants, KMS struct definitions) to `crates/shared/src/lib.rs`, plus the matching unit tests. No `ABI_VERSION` bump — additive-only. - **kernel `dri/` module**: bo registry + global master tracking (`crates/kernel/src/dri/{mod,bo,master}.rs`), 17 unit tests pass. - **HostIO trait extensions**: gbm_bo_*, gl_*, kms_*, proc_read_bytes, proc_write_bytes all added with no-op / -ENOSYS default impls so existing host adapters compile without changes. - **libc stubs**: full `libdrm`, `libgbm`, `libegl`, `libglesv2` stubs; `gl_abi.h` shared header. - **musl-overlay headers**: drm, GLES2, EGL, KHR, gbm + sys/ioccom.h. - **example programs**: cube, cube_pyramid, dri-smoke, dri_paint, dumb_roundtrip, kms-pageflip-smoke, libdrm-kms-smoke, modeset. - **build script**: scripts/build-gles-stubs.sh. - **design docs**: webgl-gles2 + dri-v2 plans. - **host TS surface** (`host/src/dri/`, `host/src/webgl/`) and the matching `host/test/{dri,webgl}-*.test.ts` files copied for the next pass to integrate against upstream's evolved `kernel.ts`/`kernel-worker.ts`. Next commits: wire DRI ioctls into syscalls.rs ioctl dispatch + devfs.rs + ofd.rs + fork.rs + wasm_api.rs, then integrate the host TS surface, then build the Kandelo React UI pane. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nstants First slice of plan 5 / evdev. Adds pub mod input as a sibling of pub mod dri: WpkInputEvent (24-byte repr(C) with explicit _pad at offset 12 so ev_type lands at offset 16 where C's struct timeval puts it), WpkInputId, WpkInputAbsinfo, EV_/KEY_/BTN_/REL_/ABS_/SYN_ codes, BUS_VIRTUAL, EVIOCGVERSION/EVIOCGID/EVIOCGRAB plus the variable-length nr bases for EVIOCGNAME / EVIOCGBIT / EVIOCGABS. KEY_* covers 0..248 — the full Linux input-event-codes.h surface that browsers can emit through KeyboardEvent.code, so Phase B2's translation table is just a key-by-key lookup. input_tests verifies struct sizes (24/8/24), field offsets (the _pad is load-bearing; without it ev_type sits at offset 12 and the C reader silently misreads every record), and re-derives each EVIOC* number from (dir, magic, nr, size) so a copy-paste typo cannot survive. Purely additive — no ABI_VERSION bump; snapshot regen lands in A7. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Second slice of plan 5 / evdev. Extends VirtualDevice with the
InputEvent { device: u8 } struct variant (kbd / ptr; host_handle
-10 / -11), wires match_virtual_device to recognise event0 +
event1 (event2+ deliberately returns None), and adds the per-OFD
InputFdState sidecar: device + 24 KiB event ring + grabbed flag
+ dropped flag + ring_high_water diagnostic. Sidecar is parallel
to dri_state, not nested — disjoint state machines.
devfs lists event0 + event1 alongside the existing mice entry.
sys_read on an evdev fd is a placeholder Ok(0) for now — A5
lands the ring drain + SYN_DROPPED resync semantics. Fork
deserialise leaves input_state None with a TODO(A4/A5)
breadcrumb; no observable consumer exists yet.
Two pre-existing assertions adjusted:
* match_virtual_device_recognizes_mice no longer claims
/dev/input/event0 returns None.
* test_virtual_device_roundtrip's "first sentinel past the
range" moves from -10 to -12 since -10 / -11 are now kbd / ptr.
Purely additive — no ABI_VERSION bump.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ndling Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…OPPED resync Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
sys_close snapshots OFD.input_state on last-ref and drops it via new input_release_ofd_state helper (mirrors dri_release_ofd_state; v1 body is a no-op drop, signature reserved for plan-9 grab-released hook). Fork/exec serialise the per-OFD ring + grab + dropped flags + high water mark across write_input_state / read_input_state (modelled on write_dri_state / read_dri_state). Reader bounds-checks against INPUT_RING_MAX_BYTES and rejects non-record-aligned lengths as EINVAL. Tests: - close_releases_grab_so_next_open_can_grab — OFD slot is freed, a fresh open on the same node comes up clean and re-grabbable. - fork_then_close_in_child_keeps_grab_on_parent — child inherits grab + queued events; closing the child's fd doesn't touch parent. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A4 added `kernel_input_event` (host → kernel event producer) and `kernel_set_input_canvas_dims` (host → kernel pointer canvas geometry). Both are pure additions to the kernel-wasm export set — no existing entry changes — so per CLAUDE.md ABI policy this is additive-compatible and `ABI_VERSION` stays at 14. Note: `scripts/check-abi-version.sh` continues to flag `kernel_reserve_host_region` + `kernel_reserve_host_region_at` as "removed" and reports `host_adapter` + `process_memory_layout` as reshaped vs `upstream/main`. That is the same pre-existing upstream snapshot drift from PR #629 ("Make pthread control slots dynamic") already documented in 00d123b — not introduced by this branch and not addressed here. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase A artifacts removed: - ring_high_water field on InputFdState: debug-only counter no producer reads; carried 4 bytes per OFD across the fork wire. - input::wait::wake_event_reader stub + was_empty/woken_ofds bookkeeping in push_event: empty no-op. Phase B will add real wake routing where it is actually wired. - input_release_ofd_state + the sys_close snapshot block: 4-param helper whose body was `let _ = state;`. Slot Drop already releases the box on dec_ref — the take/helper pattern only exists for dri_state because that path calls into the host. - VirtualDevice::InputEvent host_handle catchall returning -10: collapsed to `device => -10 - device as i64`. Trim the matching test (push_event_tracks_high_water) and the sys_read comment that referenced wake_event_reader by name. ABI snapshot byte-identical; cargo test 983/983. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds host/src/input/input-source.ts with the InputEvent record (device 0|1, ev_type, code, value) and the InputSource interface (start(dispatch), stop()) that subsequent commits implement per host: BrowserInputSource captures DOM events (keyboard/pointer/wheel) and translates to evdev codes; NodeInputSource is a null-source for headless test runs. Convention encoded in the doc-comment: the source emits the type-specific record then a SYN_REPORT to close the logical frame, mirroring Linux evdev. Host wires dispatch to kernel.exports.kernel_input_event at boot, after kernel.exports.kernel_set_input_canvas_dims. Vitest is import-and-instantiate sanity only — a StubSource records two dispatches, stop() clears the dispatch handle, post-stop emits are dropped. No kernel or ABI surface change; pure-additive host module with no consumers until B4. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…_* translation
Adds host/src/input/browser-input-source.ts (DOM keyboard/pointer/wheel
capture, evdev translation, SYN_REPORT framing) and
host/src/input/key-code-table.ts (~130-entry KeyboardEvent.code →
KEY_* lookup mirroring shared::input KEY_* values — Linux UAPI
verbatim, same numeric space SDL2's evdev backend consumes on real
Linux).
Coordinate convention: pointer-lock active → REL_X/REL_Y deltas
(movementX/Y); inactive → ABS_X/ABS_Y absolute (offsetX/Y). A bare
SYN_REPORT on pointerlockchange gives libinput / SDL2 a re-sync point
so a stale axis value doesn't carry across the lock-mode transition.
Wheel normalisation handles browser deltaMode quanta: PIXEL (Chromium
±100/±120 per notch, Safari ±1–10), LINE (Firefox ±3 per notch). A
small-but-nonzero delta clamps to ±1 tick so continuous-trackpad
scrolls still emit at least one record (otherwise Math.trunc(0.3 /
120) = 0 swallows the entire scroll).
Vitest covers every translation path with 17 specs: keydown
down/repeat/unknown, keyup, pointermove ABS vs REL with mocked
pointerLockElement, single-axis movement skip, three pointer buttons,
wheel PIXEL/LINE/HWHEEL/small-delta/zero-delta, lock-change SYN, and
stop() listener removal. Uses a FakeTarget EventTarget stub and
vi.stubGlobal('document', …) — no jsdom/happy-dom dependency.
The unused canvas constructor parameter is intentionally stashed for
B4, which calls kernel_set_input_canvas_dims off it.
No kernel or ABI surface change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds host/src/input/node-input-source.ts implementing InputSource as a pair of no-ops. There's no DOM in Node, and the integration tests drive evdev events directly via kernel.exports.kernel_input_event(…) instead of synthesising KeyboardEvent / PointerEvent. The Node host still registers an InputSource at boot so the init path is symmetric with the browser-side one (CLAUDE.md §"Two hosts" — dual-host parity is load-bearing). Vitest: start() registers but emits no records; start()/stop() are safe to call repeatedly. No kernel or ABI surface change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the host-side plumbing for evdev: both BrowserKernel and
NodeKernelHost gain injectInputEvent / setInputCanvasDims raw entry
points plus an attachInputSource(source, dims) helper that mirrors
the boot pattern across the two hosts. CLAUDE.md §"Two hosts —
DUAL-HOST PARITY IS LOAD-BEARING": every layer gets a parallel diff
in the same commit (protocol message type, host class method, worker
entry switch case, shared kernel-worker.ts wrapper, shared kernel.ts
export wrapper).
Three layers, symmetric on both sides:
1. host/src/kernel.ts (WasmPosixKernel) — calls the new
kernel_input_event / kernel_set_input_canvas_dims exports from
A4 + A7. Silent-drop pattern if the kernel module isn't yet
instantiated, same as injectMouseEvent.
2. host/src/kernel-worker.ts (CentralizedKernelWorker) — wraps the
calls and schedules a blocked-reader wake on injectInputEvent
so processes parked in sys_read on /dev/input/event{0,1} get
re-poked. Picks "extend pendingPipeReaders" for the wake
mechanism (handoff-28's B4-open choice) by reusing
scheduleWakeBlockedRetries — same path mice uses.
3. host/src/{browser,node}-kernel-protocol.ts +
host/src/{browser,node}-kernel-worker-entry.ts +
host/src/{browser,node}-kernel-host.ts — main↔worker message
plumbing and public host-class API. attachInputSource(source,
dims) sets canvas dims then starts the source with a dispatch
callback that funnels each emitted record through
injectInputEvent. Both hosts share the same method shape so
callers see identical surface; only constructor patterns for
the InputSource differ (BrowserInputSource on the browser,
NodeInputSource null-source on Node).
Vitest:
- input-attach-source.test.ts: attachInputSource posts dims-msg
once, calls source.start once, routes dispatched records through
injectInputEvent → input_event_inject. setInputCanvasDims and
injectInputEvent also tested standalone. Bypasses init() (which
spawns a real worker_thread) by monkey-patching sendToWorker;
the constructor only stores options, so a bare new
NodeKernelHost() is safe.
- Browser-side end-to-end exercised at Phase C via Playwright per
plan §C; pure-logic translation already covered by
browser-input-source.test.ts (B2, 17 specs).
Symmetry sweep verified: every new symbol
(input_event_inject / set_input_canvas_dims / injectInputEvent /
setInputCanvasDims / attachInputSource) shows parallel hits in
browser-* and node-* trees at every layer.
No Rust changes; no ABI surface change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
B5 closes Phase B with an end-to-end gate for the evdev path. `input-evdev-smoke.c` opens /dev/input/event0 + event1, does EVIOCGNAME / EVIOCGABS, and drains the per-OFD rings. Each phase gates on a stdin byte so the host injects events AFTER the OFD exists — `kernel_input_event` fans out at push time and a pre-open injection would land nowhere. The vitest drives `NodeKernelHost.injectInputEvent` (B4) directly and asserts: EVIOCGNAME returns "wpk virtual keyboard"; EVIOCGABS(ABS_X).maximum reports canvas_w-1; KEY_A↓+SYN_REPORT round-trip with monotonic-non-decreasing CLOCK_MONOTONIC stamps; REL_X=+5 + SYN_REPORT; and overflow drains to 1025 records (1 synthesised SYN_DROPPED at index 0, then 1024 surviving ring records) with the last record still an EV_KEY/KEY_A — i.e. the incoming records were the ones discarded, Linux semantics. `it.skipIf(!fixtureBinary)` keeps the suite green when the wasm fixture isn't built locally, matching the dri-kms-pageflip pattern. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… browser spec Closes the evdev plan's Phase C (PR #3 scope) plus a build-script regression that blocked the demo from compiling. - `libc/musl-overlay/include/linux/input.h` + `libc/musl-overlay/include/linux/input-event-codes.h` — vendored minimal subset (C1). `programs/evdev_demo.c` is the lone consumer in tree; its `_Static_assert(sizeof(struct input_event) == 24)` is the compile-time guard against wasm32 layout drift. - `programs/evdev_demo.c` (~100 LoC) — opens /dev/input/event{0,1}, prints EVIOCGNAME for both, then polls forever and logs every key and pointer event. Free-running (no stdin-barrier harness) so it works as the interactive Kandelo pane (C2). - `apps/browser-demos/pages/kandelo/presets.ts` + `kernel-host/live-setup.ts` — wire an `evdev` preset that stages the binary into /usr/local/bin, attaches a BrowserInputSource to window so DOM key/pointer events reach `kernel_input_event`, and runs the demo through bash so its stdout lands in the Shell pane. Boot path mirrors the existing `modeset` preset. - `apps/browser-demos/test/kandelo-evdev.spec.ts` — Playwright spec that drives KeyA + pointer moves and asserts the on-canvas log contains `key down: code=30` and `ptr (abs|rel) code=N value=N`. Proves the B4 dual-host parity claim end-to-end in a real browser. - `scripts/build-musl.sh` steps 10-11 — restores libdrm.a + libgbm.a compile-and-archive steps that were lost when commit b25ef59 ported the DRI sources forward but missed the build wiring from commits a091a5b and 2f7fead. Without these, programs/dri-modeset (and friends) fail to link with `no such file or directory: libgbm.a`. Manual browser verification (CLAUDE.md item 6 / C4) passes: keystrokes and pointer movement appear in the on-canvas log under `./run.sh browser` → `?demo=evdev`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First pass dropped the unused `canvas` ctor arg on BrowserInputSource and trimmed top-of-file what-narration in input.h / evdev_demo.c / live-setup.ts / input/mod.rs. Second pass strips the param-table doc on `kernel_input_event` + `kernel_set_input_canvas_dims`, collapses the per-case EVIOCG* headers and ioctl-decode preamble in `handle_input_ioctl`, tightens the SYN_DROPPED / blocking-read narration in the evdev read drain and poll gate, collapses the dispatch.rs file-doc, drops the three-phase prose and ioctl-encoding paragraph from `input-evdev-smoke.c`, and trims test-internal narration that restates test names across `syscalls.rs`, `ofd.rs`, `input/mod.rs`, and `devfs.rs`. Kept the load-bearing WHYs: ENOTTY-not-EINVAL for SDL2 probe walking, SYN_DROPPED resync after overflow, blocking-read-returns-0 retry contract, concurrency-justification for tests that mutate canvas-dim globals. Net across both passes: -184 LoC (12 files). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wire a Kandelo ALSA subsystem (devfs nodes, PCM ioctl dispatch, SAB ring, mmap pages, tick, poll, fork serialisation) and a host audio stack (`AudioDriver`, Browser + Node drivers, AudioWorklet, drain-on-stop, instrumented wrapper) on both Node and browser hosts. Port espeak-ng with a vendored pcaudiolib `audio_kandelo` backend, bake the binary and English data dir into `shell.vfs.zst`, and expose a kandelo "ALSA – espeak-ng" demo preset with a Playwright spec. ABI delta is additive (three new exports, two new virtual devices) and does not bump `ABI_VERSION`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Five call sites the 647-commit rebase left dangling. `with_processes` came from a DRI commit main already carries through #678, so replaying it was a no-op — but the evdev fan-out and the audio period tick both call it. Restored on its own. `OpenFileDesc`'s SCM_RIGHTS construction site is main's; it never carried the three sidecar fields. The two test helpers passed a pid to `create_process`, which main now allocates itself, so they insert the process at their chosen pid directly. `/dev/snd/pcmC0D0c` reported ENOENT instead of ENODEV. It is deliberately absent from the synthetic tree, and main's path resolver now rejects a missing node before `sys_open` reaches the disabled-device check. The resolver's ENOENT is translated back. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ontract Codex's review of #698 recommended against adopting `/dev/snd` as Kandelo's sound API: ALSA is a large kernel/userspace contract, this was a narrow subset of it, and carrying it under standard ALSA names freezes an interface that looks compatible but is not. The reviewer's alternative — a correct OSS-compatible `/dev/dsp` — is already on main. `crates/kernel/src/audio/oss.rs` was byte-identical to main's `crates/kernel/src/audio.rs`; this branch only renamed it. Main's version already answers every item on the review's SDL-quality list: blocking `write`, `set_nonblock`/EAGAIN, `poll_writable`, `output_space` (GETOSPACE), `output_delay`, `output_pointer`, `sync`, `reset_stream`, `claim_transport`/`clock_update` for audio-clock-driven consumption, and `open_stream`/`preflight_close` for deterministic OFD ownership. So the file moves back and the ALSA siblings go. Removed: `audio/{pcm_ioctl,mmap,sab,tick,wait}.rs`, the `/dev/snd` devfs directory with `controlC0` + `pcmC0D0p`, the `AlsaPcm` / `AlsaControl` virtual devices, the `pcmC0D0c` disabled-node path, the `audio` / `audio_ctl` OFD sidecars and their fork/exec serialisation, the `kernel_audio_{init_sab,period_tick,get_appl_ptr}` exports, the `shared::audio` ALSA ABI, and `sound/asound.h`. Host side loses the ALSA-shaped driver — `audio/{audio-driver, browser-audio-driver,node-audio-driver,instrumented-audio-driver}.ts` and `wpk-audio-worklet.js` — plus `attachAudioDriver` and the three ring/tick/appl-ptr protocol messages on both hosts. Main's PCM stack (`audio/{pcm-driver,browser-pcm-driver,node-pcm-driver,pcm-transport}.ts` + `pcm-audio-worklet.js`) already serves `/dev/dsp` and stays. The espeak demo now rides that path instead of attaching its own driver. Kernel: 1635 tests pass. Shared: 53 pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… table `IOCTL_REQUESTS` is keyed by exact request number, which the evdev surface does not fit. `EVIOCGNAME(len)` and `EVIOCGBIT(ev, len)` encode a caller-chosen buffer length in the request itself, so every length is a distinct number; `EVIOCGABS(axis)` keeps one structure across 64 axes. None of the three can be enumerated. `IOCTL_REQUEST_FAMILIES` names the `nr` ranges instead. A family fixes either the exact structure size (`EVIOCGABS`, 24 bytes) or a bound on the caller's length (256 bytes, wide enough for the `EV_KEY` bitmap), and a request whose encoded size falls outside stays unresolved rather than staging a wrong buffer. `request_contract` consults the sorted table first and the families second. `host/src/ioctl-contract.ts` mirrors that order for both hosts, so the kernel and the two host runtimes agree on the staged byte count. The table itself is generated into `host/src/generated/abi.ts` by `dump_abi`, alongside a snapshot section. `EVIOCGVERSION`, `EVIOCGID` and `EVIOCGRAB` have fixed numbers and join the sorted table directly. ABI: additive only. A request absent from the table resolved to "unknown" before, so adding one cannot change how an older program marshals a call it already made; `classify_compat_change` treats added `ioctl_request_contracts` entries and the new `ioctl_request_families` section as compatible. Changing or removing an entry stays breaking. `ABI_VERSION` stays 43. Shared: 60 pass. xtask: 787 pass. Host: 7 new cases in `host/test/ioctl-contract.test.ts`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`injectInputEvent` and `setInputCanvasDims` called into the kernel instance directly. Both are host ingress: the browser pushes a key or pointer record whenever the DOM fires, with no regard for whether the kernel is mid-export. Calling straight through re-enters the kernel and trips `KernelReentrantEntryError`, or corrupts state that an in-flight export owns. `injectMouseEvent` already solved this — it runs under `#runOrDeferKernelEntry`, which executes immediately when the gate is open and queues the call when it is not. The two evdev entry points now do the same, and the record wakes blocked readers through the same `scheduleWakeBlockedRetries(entry)` the mouse path uses. `WasmPosixKernel` carried its own copy of both methods. Nothing called them: `CentralizedKernelWorker` reaches the exports itself, the same shape `injectMouseEvent` has on main. Deleted rather than gated. The new test proves both calls defer out of a detached host callback and then run in order. `kernel_input_event` and `kernel_set_input_canvas_dims` join the test instance's export signatures so the fixture builds a genuine Wasm function for each. Host: 6 pass in `kernel-clone-exit-entry.test.ts`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`432d4c4d1` brought forward `programs/cube.c`, `programs/dri_paint.c` and `host/test/webgl-foreign-texture.test.ts` from the pre-rebase branch. Main carries the DRI/WebGL surface these were written against, so all three arrived as additions with no consumer. `scripts/build-programs.sh` globs `programs/*.c`, so both sources were compiled on every build and neither binary is named by any demo, test or image. Only two plan documents mention them, as history. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`scripts/browser-binary-package-roots.mjs` requires every `@binaries`
import in the browser app to name a registry package, and the audit at
`tests/package-system/browser-binary-dependencies.test.ts` enforces it.
`live-setup.ts` reaches `evdev_demo.wasm` through `import.meta.glob`,
and nothing owned it.
`packages/registry/evdev-demo/` follows `packages/registry/modeset/`,
the precedent for a `programs/*.c` binary that drives a Kandelo demo
pane: the source stays in `programs/`, and the package builds it. The
output name is `evdev_demo`, which keeps the flat mirror path
`programs/wasm32/evdev_demo.wasm` that `live-setup.ts` already names,
so no consumer changed. The package name needs the hyphen — the
package-name regex in `browser-binary-package-roots.mjs` rejects the
underscore.
`build-programs.sh` now skips the source, exactly as it skips
`modeset.c`: once a package owns a mirror path, the resolver publishes
a generation-backed symlink there and the compiler must not write to it.
`docs/posix-status.md` claimed there was no `evdev`. It now carries a
`/dev/input/event{0,1}` row written from `handle_input_ioctl` and the
1024-record per-OFD ring.
No package test: `packages/registry/modeset/` has none either, and the
generic audits under `tests/package-system/` already cover it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The image builder read the voice data from `packages/registry/espeak-ng/espeak-ng-install/share/espeak-ng-data`, a build-tree path that exists on no fresh checkout and no CI runner. `docs/package-management.md` names that exact pattern as forbidden: repo-side builders query the resolver, they do not scan build caches. The data is now a closure member, following `packages/registry/cpython`. `build-espeak-ng.sh` packs the tree into `espeak-ng-data.zip` — stored-only, sorted, fixed timestamp and mode, so the archive bytes follow the voice data alone and the cache key stays stable — and publishes it with `install_local_runtime_file`. Verified byte-identical across two builds from the same tree: 251 entries, 919 KB. Adding a runtime file makes espeak-ng multi-member, so the resolver moves its whole closure under the package directory: `espeak-ng/espeak-ng.wasm` and `espeak-ng/espeak-ng-data.zip`. `build.toml` revision goes 2 -> 3, which invalidates every cached archive and makes CI rebuild it. The branch had also added `install_local_binary shell` to `images/vfs/scripts/build-shell-vfs-image.sh`, overwriting the resolver-owned `shell` mirror with a foreign image and failing the ownership audit. That script and its `.ts` sibling are both on main's retirement list and `./run.sh browser` never runs them — the shell image comes from `packages/registry/shell/build-shell.sh`. Both files go back to main and the test asserting the added line goes with them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Neither demo could run. Both staged their binary after boot through `kernel.fs`, and `BrowserKernel` has no `fs` member — the kernel worker owns the VFS exclusively and the main thread holds no VFS SharedArrayBuffer. `live-setup.ts` says so itself: post-boot main-thread staging was removed, and a binary has to be part of the image bytes. `tsc` surfaced it; no test did. Both now stage while the image is composed, in the same block that already writes `profile.init.programUrl`. `stageEspeakRuntime` fetches both espeak-ng closure members, writes `/usr/bin/espeak-ng`, and unpacks the zip into `/usr/share/espeak-ng-data` — the tree must be unpacked because libespeak-ng's `PATH_ESPEAK_DATA` is fixed to `/usr/share` by `CMAKE_INSTALL_PREFIX=/usr` at build time. `stageEvdevDemo` writes `/usr/local/bin/evdev_demo`. The post-boot blocks now only run the command; the evdev one still attaches `BrowserInputSource` first, because the binary polls as soon as it starts. A second copy of the `ensureServiceWorkerReady` import made the whole file unparseable — Babel raised `Identifier 'ensureServiceWorkerReady' has already been declared`, which failed five tests and masked the package-ownership defects behind them. The added block goes; its other specifier had no use anywhere in the file. `optionalBinaryUrl` told the reader to run `./run.sh build programs`. That is now wrong for package-owned binaries, which the script deliberately skips, so the message names the resolver too. The espeak spec dropped its `__alsaFramesConsumed` assertion with the counter itself. It now clicks for a trusted gesture and waits for `data-audio-state="running"`, the same signal the doom demo's spec uses; pcaudiolib aborts when it cannot open `/dev/dsp`, so reaching the prompt with a running sink proves the OSS backend negotiated the device. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Derived from the two package changes ahead of it: the new `evdev-demo` recipe, and espeak-ng becoming multi-member, which moves its closure under the package directory as `espeak-ng/espeak-ng.wasm` and `espeak-ng/espeak-ng-data.zip`. Editing anything a package lists in `build.toml` `inputs` invalidates this file, and every `resolveBinary()` then throws `Program package source projection is not current`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
d1b1156 to
e439b86
Compare
Why
Kandelo had no input devices: nothing existed under
/dev/input, so evdev consumers — SDL2 (#709), Wayland (#948), Hyprland (#976) up the stack — had no way to read a keyboard or a pointer. This PR is the base layer of the five-PR DRI stack (#698 → #709 → #948 → #976 → #1241). It adds a kernel evdev subsystem fed by host input sources on both hosts, and proves the audio path end-to-end by porting espeak-ng against main's existing OSS/dev/dspdevice.Earlier revisions of this PR added a kernel ALSA subsystem instead. The review recommended against carrying a narrow subset of the ALSA contract under standard ALSA names, and main's
/dev/dspalready covers the PCM needs — the ALSA surface is gone from this branch (see Notes).What changed
crates/kernel/src/input/):/dev/input/event0(keyboard) and/dev/input/event1(pointer) devfs nodes with per-OFDInputFdState;EVIOCG*ioctl dispatch, with the length-encodedEVIOCioctls resolved through a family table; akernel_input_eventexport that fans events out to every open fd, with ring overflow handled bySYN_DROPPEDresync;sys_readdrains the ring andsys_pollgatesPOLLIN; state is released on close and serialised across fork/execstruct input_eventplusEV_*/KEY_*/BTN_*/REL_*/ABS_*/EVIOC*constants;linux/input.handlinux/input-event-codes.hvendored into the musl overlay;abi/snapshot.json,host/src/generated/abi.ts, andlibc/glue/abi_constants.hregenerated — additive-only, noABI_VERSIONbumpInputSourceinterface;BrowserInputSourcecaptures DOM keyboard/pointer events and translates them through a key-code table;NodeInputSourceis the headless null source so vitest exercises the same path; ingress routes through the kernel entry gatepackages/registry/espeak-ng/): cross-build recipe with a cmake toolchain file, linked against upstream pcaudiolib 1.3 built with only its OSS backend — that backend opens/dev/dsp, so no pcaudiolib source is patched and no kernel audio code is added; the English-only voice data ships as a resolver-published runtime file; the image builder stages/usr/bin/espeak-ng,/usr/share/espeak-ng-data, and/usr/local/bin/evdev_demoat image-compose timekandelo-evdev.spec.ts/kandelo-espeak.spec.ts;programs/evdev_demo.candprograms/input-evdev-smoke.c; anevdev-demopackage owningevdev_demo.wasmNotes
/dev/snd/{pcmC0D0p,controlC0}, the mmap ring, the threekernel_audio_*exports) is removed in cfa1d51. main'saudio.rsalready answers the OSS quality checklist (blocking write, EAGAIN, GETOSPACE, audio-clock-driven consumption), and this branch's copy was byte-identical to it. The net kernel audio delta vs main is zero.host/src/audio/is also unchanged vs main — the PCM drivers the demo plays through (BrowserPcmDriver,NodePcmDriver) already landed there.Both hosts
Browser:
BrowserInputSource, wired throughbrowser-kernel-host.ts,browser-kernel-protocol.ts,browser-kernel-worker-entry.ts. Node:NodeInputSource, wired through the node counterparts in the same commits. Shared:kernel-worker.tscarries the input fan-in for both. Vitest covers the Node path end-to-end (input-evdev.test.ts: key + pointer + ring overflow) and the browser translation layer (browser-input-source.test.ts); the Playwright specs cover the browser path.Verification
The stack was verified at its tip
272be070a(explore-dri-omarchy), which contains all 26 commits here:cargo test -p kandelo --target aarch64-apple-darwin --lib→ 1665 pass, 0 failscripts/run-libc-tests.sh→ 0 unexpected failuresscripts/run-posix-tests.sh→ FAIL: 0bash scripts/check-abi-version.sh→ consistent; this layer is additive at ABI 43, the wayland layer up-stack bumps to 44shell.vfs.zstbuilt for the stack's ABI, which is not yet published. The Playwright specs are in the tree but have not run against this rebase.