[explore-dri-sdl2] sysroot(sdl2): full Phase A–C — backends + GL stubs + sdl2_demo#709
Draft
mho22 wants to merge 13 commits into
Draft
[explore-dri-sdl2] sysroot(sdl2): full Phase A–C — backends + GL stubs + sdl2_demo#709mho22 wants to merge 13 commits into
mho22 wants to merge 13 commits into
Conversation
…TL ioctl gate + ioctl-encoded host marshalling
Phase A of the SDL2 port (DRI plan 7 / milestone D — see
docs/plans/2026-06-29-sdl2-port-plan.md). Vendors the three
third-party sysroot dependencies SDL2 2.30's configure step
links against (libdrm KMS subset, libinput-lite stub, alsa-lib
PCM-hardware-direct subset), and lands the kernel-side surface
those libraries need.
## Vendored sysroot libraries
### libdrm 2.4.120 (KMS subset)
- New `packages/registry/libdrm/` package builds the KMS-side
subset of upstream libdrm 2.4.120 (xf86drm.c + xf86drmMode.c +
xf86drmHash.c + xf86drmRandom.c -> libdrm.a, ~96 KB) for
wasm32-unknown-none. Bypasses upstream's meson and the
per-vendor subdirs (libdrm_amdgpu/radeon/intel/...).
- Generates the static fourcc table via upstream's
`gen_table_fourcc.py`, and stages small `<linux/types.h>` /
`<asm/ioctl.h>` shims so the DRM UAPI headers compile under
musl. A force-included `xf86drm_compat.h` pins `__linux__=1`
and `MAJOR_IN_SYSMACROS=1` so xf86drm.c reaches musl's
`<sys/sysmacros.h>` for `major()` / `minor()` and selects the
Linux ioctl-macro flavour.
- Replaces the in-tree `libc/glue/libdrm_stub.c` (391 LoC) and
the vendored UAPI subset under `libc/musl-overlay/include/`
(`xf86drm.h`, `xf86drmMode.h`, `drm/{drm,drm_mode,drm_fourcc}.h`,
~4500 LoC). `build-musl.sh` step 10 symlinks the cached
archive into the sysroot; `build-programs.sh` (and step 11's
libgbm build) adds `-I$SYSROOT/include/{libdrm,drm}` so
consumers of upstream `xf86drm.h` resolve `<drm.h>`.
- `programs/dri-modeset.c` now `poll(fd, POLLIN, -1)`s before
`drmHandleEvent()`. The in-tree stub used to do this
internally to work around the kernel's `read(card0)` returning
0 on an empty event ring; upstream `drmHandleEvent()` does a
bare `read()` and assumes Linux's blocking-mode parks the
caller. Real consumers (SDL2 KMSDRM, weston, mutter) already
poll() first.
### libinput-lite (no-op stub)
- New `packages/registry/libinput-lite/` follows the
posix-utils-lite sentinel-source convention; the resolver
doesn't fetch, build script compiles the bundled
`src/libinput_stub.c` directly. Provides `<libinput.h>` so
SDL2 2.30's configure step finds the header, and every entry
point returns NULL / 0 so consumers that prefer libinput fall
back onto the direct evdev path (DRI plan 5). `build-musl.sh`
step 12 symlinks the cache artifact into the sysroot.
- `programs/libinput_stub_smoke.c` +
`host/test/libinput-stub.test.ts` exercise all five entry
points (udev_create, path_create, unref, dispatch,
get_event) and assert the NULL/0 contract.
### alsa-lib (PCM-hardware-direct subset)
- New `packages/registry/alsa-lib/` builds a small subset of
alsa-lib 1.2.10 (pcm core + pcm_hw + pcm_params + control +
control_hw + interval + mask + dlmisc + error) on top of
vendored upstream `<sound/asound.h>` headers.
`src/conf_stubs.c` provides `page_align`/`page_size`/
`page_ptr` + `snd_async_*` no-ops so we can elide
`src/conf.c` (the snd_config_* configuration tree).
- `0001-default-to-hw00.patch` routes
`snd_pcm_open("default")` / `"hw:0,0"` / `"plughw:0,0"`
straight to `/dev/snd/pcmC0D0p` bypassing
`snd_config_*`. `build-musl.sh` step 13 installs and
replaces the in-tree `<sound/asound.h>` subset overlay with
alsa-lib's vendored upstream UAPI so kernel-direct and
libasound-linked consumers share one set of struct
definitions.
## Kernel-side surface
### CTL ioctl gate
- New `crates/kernel/src/audio/ctl_ioctl.rs` handler with
`SNDRV_CTL_IOCTL_PVERSION = 0x8004_5500` and
`SNDRV_CTL_IOCTL_PCM_PREFER_SUBDEVICE = 0x4004_5532`
(`_IOW('U', 0x32, int)`). Plumbed via `audio/mod.rs` +
dispatch in `syscalls.rs`. alsa-lib's `snd_pcm_open` flow
fans CTL_PVERSION + PCM_PREFER_SUBDEVICE before opening the
PCM fd.
### PCM ioctl semantics
- `pcm_ioctl::refine_interval` now leaves intervals as
ranges (`min..=max` clamped to capabilities). Linux
semantics: HW_REFINE returns ranges; alsa-lib's
`snd_pcm_hw_params_choose()` narrows iteratively via
`set_first` before HW_PARAMS commits.
- Derived intervals (`FRAME_BITS`, `PERIODS`) propagate ranges
from the primary intervals so
`snd_pcm_hw_param_set_rate_near()` can still widen the rate
interval after a wildcard refine.
### Audio struct wasm32 audit
- `__SND_STRUCT_TIME64` is defined on wasm32 musl
(`__USE_TIME_BITS64 = 1`), and `unsigned long = 4`. Several
marshalled audio structs were sized against x86_64 (8-byte
`snd_pcm_uframes_t`) and so their `_IOC_SIZE` request bits
disagreed with what alsa-lib actually emits:
- `WpkAlsaPcmHwParams.fifo_size: u64 -> u32` (struct 608 -> 604);
`SNDRV_PCM_IOCTL_HW_{REFINE,PARAMS} = 0xc25c_4110/0xc25c_4111`.
- `WpkAlsaPcmSwParams`: drop `_pad0`, all `snd_pcm_uframes_t`
fields `u64 -> u32` (136 -> 104);
`SNDRV_PCM_IOCTL_SW_PARAMS = 0xc068_4113`.
- `WpkAlsaXferi`: `i64,u64,u64 -> i32,u32,u32` (24 -> 12);
`SNDRV_PCM_IOCTL_WRITEI_FRAMES = 0x400c_4150`.
- `WpkAlsaPcmStatus` (size still 128 B by coincidence),
`WpkAlsaPcmMmapStatus`, `WpkAlsaPcmMmapControl` documented
as known wasm32 layout-mismatch defects; smoke test does
not exercise them. Fix concurrently with SDL2 writei/poll
wire-up.
### Host adapter: ioctl-encoded marshalling
- New `SyscallArgSize::IoctlEncoded { arg_index, floor }`
variant reads `(args[arg_index] >> 16) & 0x3fff` (the
`_IOC_SIZE` bits) and falls back to `floor` for legacy
size=0 ioctls (FIONBIO, KDGKBTYPE, ...). Wired through
`host_abi.rs`, `dump_abi.rs`, `host/src/generated/abi.ts`,
`libc/glue/abi_constants.h`, `libc/glue/syscall_glue.c`,
and the input/output marshalling loops in
`host/src/kernel-worker.ts`. SYS_IOCTL entry switched from
`fixed(256)` to `ioctl_encoded(arg_index=1, floor=256)` so
the channel round-trips the full ioctl payload (the
`WpkAlsaPcmHwParams` request is 604 B; the previous fixed
256 B truncated intervals starting at offset 260).
## ABI bump
- `ABI_VERSION 15 -> 16`. The IoctlEncoded variant and the
audio struct shrinks change existing ABI surface; old
binaries that emit ioctl payloads >256 B now have their
full payload round-trip the channel instead of being
truncated.
## Tests
- `programs/alsa_lib_smoke.c` +
`host/test/alsa-lib-smoke.test.ts`:
`snd_pcm_open("default") -> hw_params_any -> set_access /
format / channels / rate_near(48000) -> snd_pcm_hw_params`
end-to-end through the kernel CTL gate + HW_REFINE +
HW_PARAMS + SW_PARAMS path.
- `programs/libinput_stub_smoke.c` +
`host/test/libinput-stub.test.ts` cover libinput-lite.
- DRI vitest suites continue to pass against upstream libdrm.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…2 uframes_t=4 Phase A of the SDL2 port shrunk WpkAlsaPcmHwParams / SwParams / Xferi to match wasm32 musl `unsigned long` = 4 B but documented the remaining three audio structs as "deferred" with WASM32 LAYOUT NOTE doc comments. SDL2's polling-audio path (open-arch #1) exercises snd_pcm_status() + mmap-status/control synchronisation via alsa-lib's hw_open path, so the deferred-struct lie would land as a runtime hang the first time the SDL2 demo wires up the writei consumer. This commit lands the fix in the same ABI 16 window the rest of the audio shrink already paid for, so the single SDL2 PR's reviewer doesn't have to absorb the "these are knowingly broken" caveat. ## Struct changes (all under crates/shared/src/lib.rs::audio) - `WpkAlsaPcmStatus` keeps its 128 B size (so `SNDRV_PCM_IOCTL_STATUS` = `0x8080_4120` is unchanged), but field offsets shift: * Every `snd_pcm_uframes_t` / `snd_pcm_sframes_t` field (appl_ptr, hw_ptr, delay, avail, avail_max, overrange) shrinks i64/u64 -> i32/u32. * Each `struct timespec` slot becomes `i64 sec + i32 nsec + 4 B pad` (16 B with 8-byte alignment from i64) instead of `i64 sec + i64 nsec`. * Two upstream UAPI fields previously omitted are now present: `driver_tstamp` and `audio_tstamp_accuracy`. - `WpkAlsaPcmMmapStatus` shrinks 64 -> 56 B and matches `struct __snd_pcm_mmap_status64`: `state @ 0, _pad1 @ 4, hw_ptr (u32) @ 8, _pad_after_hw_ptr @ 12, tstamp (16 B) @ 16, suspended_state @ 32, _pad3 @ 36, audio_tstamp (16 B) @ 40`. Mapped at `SNDRV_PCM_MMAP_OFFSET_STATUS_NEW` (= 0x82000000). - `WpkAlsaPcmMmapControl` shrinks 64 -> 12 B and matches `struct __snd_pcm_mmap_control64`: `appl_ptr (u32) @ 0, avail_min (u32) @ 4, _pad_after @ 8`. Mapped at `SNDRV_PCM_MMAP_OFFSET_CONTROL_NEW` (= 0x83000000). ## Callsite ripples The shrink propagates through ~40 mechanical `i64 -> u32` casts: - `pcm_ioctl.rs::SNDRV_PCM_IOCTL_STATUS` handler writes the reshaped struct, computing `delay` via i64 then clamping to i32. - `pcm_ioctl.rs::handle_writei` uses i64 for the appl/hw delta before the avail clamp; appl_frame_offset becomes `appl as usize % ring_frames`. - `tick.rs::tick` uses `hw_ptr.wrapping_add(frames_consumed)` (both u32 now) and casts the host-supplied i64 tv_nsec to i32. - `tick.rs::current_appl_ptr` keeps its `-> i64` signature (kernel_audio_get_appl_ptr export contract) but internally works with u32 and widens on return. - `fork.rs::write_audio_state` / `read_audio_state` switch the mmap_status / mmap_control wire format to match the new field widths (drops the `_reserved_tail[8]` / `_reserved[48]` runs that no longer exist). - `syscalls.rs::install_alsa_pcm_fd` test helper's appl_ptr / hw_ptr params shrink i64 -> u32. ## In-tree UAPI header alignment `libc/musl-overlay/include/sound/asound.h` was the strict-subset authoring reference. `scripts/build-musl.sh` step 11 already rm -rf's that subtree and symlinks alsa-lib's vendored upstream UAPI in its place, so the overlay isn't what user-space programs see — but leaving it at the old layout would mislead anyone reading the kernel side. Rewritten to the wasm32 layout, with a header comment noting the build-time supersession by alsa-lib's UAPI. ## Smoke test extension `programs/alsa_lib_smoke.c` now calls `snd_pcm_status()` after `snd_pcm_hw_params()` and asserts `snd_pcm_status_get_state == SND_PCM_STATE_PREPARED` (alsa-lib's hw_params calls prepare() internally). This drives `SNDRV_PCM_IOCTL_STATUS` end-to-end through the marshalled 128 B WpkAlsaPcmStatus, catching any future drift between kernel and userspace ABI for that struct. ## ABI / gauntlet - `ABI_VERSION` stays 16 — Phase A already paid the bump; the struct shrinks here are inside the same window. - `abi/snapshot.json` unchanged (audio struct field offsets are not snapshotted, only export names). - cargo test -p kandelo --lib: 1069 / 1069 pass. - cargo test -p wasm-posix-shared --lib: 28 / 28 pass (new size + offset assertions added for WpkAlsaPcmStatus, MmapStatus, MmapControl). - vitest spot-check: alsa-lib-smoke, libinput-stub, dri-modeset, dri-libdrm-kms, dri-kms-pageflip, audio-driver, browser-audio-driver-drain all green. - run-libc-tests.sh: clean (XFAILs + 1 FLAKE-PASS + 1 TIME match baseline, exit 0). - run-posix-tests.sh: clean (3 XFAIL + 2 SKIP match baseline, 0 FAIL / 0 UNRES). - check-abi-version.sh: clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase B task B1 per docs/plans/2026-06-29-sdl2-port-plan.md. Adds the package skeleton under packages/registry/sdl2/ so subsequent commits can iterate on the cross-compile (B2), the polling-audio patch (B5 / open-arch #1), and the three backend smoke tests (B3/B4/B5). Files: packages/registry/sdl2/package.toml — kernel_abi=16, depends_on libdrm + alsa-lib + libinput-lite, outputs lib/libSDL2.a + include/SDL2. packages/registry/sdl2/build.toml — commit pin + index_url. packages/registry/sdl2/build-sdl2.sh — configure invocation documenting the intended backend matrix (KMSDRM video + ALSA audio + evdev input; everything else off, --disable-pthreads --disable-loadso --disable-libudev). Includes the initial ac_cv_*=no override set per CLAUDE.md cross-compile policy; B2 will iterate to add whatever else the first-pass build surfaces. source.sha256 is left blank — B2 will populate it after the first successful build. patches/ + src/ stay empty for now; the polling- audio patch lands in B5. No build is attempted yet. `cargo xtask build-deps resolve sdl2` will source-build (no archive published) and is expected to surface the next round of ac_cv_*=no overrides in its configure log. That's B2's first action. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…+ dynapi patch (B2)
Configure overrides (per CLAUDE.md "Cross-Compilation and Configure
Scripts" — autoconf can't tell the wasm32 sysroot apart from the host):
ac_cv_func_sysctlbyname=no — header missing, link test
passes only because wasm-ld
tolerates undefined refs
ac_cv_func__strrev / _strupr / itoa /
_ltoa / _uitoa / _ultoa / _i64toa /
_ui64toa / _stricmp / _strnicmp /
_wcsdup / _wcsicmp / _wcsnicmp / ... — Win32-style helpers that
SDL2 uses if detected;
our musl doesn't ship them
ac_cv_func_elf_aux_info / getauxval=no — ELF-only
Backend wiring:
--host=wasm32-unknown-linux-musl — matches SDL2's "linux" arm in
configure.ac; wasm32-unknown-
none falls into "Unsupported
host" because SDL2 dispatches
backends per host triple
LIBDRM_CFLAGS/_LIBS + LIBGBM_CFLAGS/
_LIBS set explicitly — short-circuits PKG_CHECK_MODULES
so KMSDRM detects without
generating .pc files in the
libdrm/gbm installs
--disable-alsa-shared / --disable-
alsatest / --disable-kmsdrm-shared — link tests against
-lpthread/-ldl/-lasound
blow up wasm-validate
AR/RANLIB/NM=wasm32posix-* — libtool's default ranlib (host
BSD/macOS ranlib) aborts on
wasm32 archives
Two new on-disk artifacts:
patches/0001-dynapi-disable-on-wasm32.patch
Adds `__wasm__` branch in SDL_dynapi.h that turns SDL_DYNAMIC_API
off (forced because SDL_dynapi.c has no wasm platform branch and
the DYNAPI_NEEDS_DLOPEN trigger doesn't fire — HAVE_DLOPEN gets
set even with --disable-loadso).
src/sdl2-evdev-shim.h
Force-included via -include on every TU. Provides the upstream
linux/input-event-codes.h constants (EV_MAX, ABS_MAX, KEY_MAX,
REL_MAX, MSC_TIMESTAMP, BTN_MOUSE, BTN_TOUCH, BTN_STYLUS,
ABS_RX/RY/RZ, ABS_MT_*, BTN_TRIGGER_HAPPY[1-40], etc.) that SDL2's
evdev backend references but our minimal musl-overlay header
(libc/musl-overlay/include/linux/input-event-codes.h) doesn't yet
expose. Every define is #ifndef-guarded; the file becomes a no-op
if the musl-overlay header ever extends to cover them.
Verified outputs in /Users/mho/.cache/kandelo/libs/sdl2-2.30.0-rev1-
wasm32-2ede9b12/lib/:
libSDL2.a 3,907,868 bytes
libSDL2_test.a 474,416 bytes
libSDL2main.a 1,512 bytes (dummy main shim, not used by
kandelo demos)
pkgconfig/sdl2.pc
llvm-nm confirms SDL_Init, SDL_CreateWindow, SDL_OpenAudio, and
SDL_PumpEvents are all defined (TEXT) in libSDL2.a.
Configure summary at this commit:
Audio drivers : disk dummy alsa
Video drivers : dummy kmsdrm opengl_es2
Input drivers : linuxev
source.sha256 = 36e2e41557e0fa4a1519315c0f5958a87ccb27e25c51776beb6f1239526447b0
(verified against https://github.com/libsdl-org/SDL/releases/download/
release-2.30.0/SDL2-2.30.0.tar.gz)
Also fixed `depends_on` syntax — the resolver requires `<name>@<version>`
exact pins (no semver ranges), so the three deps now read
"libdrm@2.4.120", "alsa-lib@1.2.10", "libinput-lite@0.1.0".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… mode-info fixes Smoke programs and vitest tests for SDL2's three Linux-ish backends — KMSDRM video (/dev/dri/card0), ALSA audio (/dev/snd/pcmC0D0p), and linuxev input (/dev/input/event*). Each verifies SDL2 links cleanly against its archive set, that SDL_Init reaches the backend, and that SDL_GetCurrent*Driver reports the expected name. Three load-bearing fixes that were uncovered by running the smokes: - `scripts/build-programs.sh`: the SDL2 resolver block now re-symlinks every transitive dep archive (libasound.a, libdrm.a, libinput.a) whenever it resolves SDL2, not just libSDL2.a. Previously the fast-path guarded on `libSDL2.a` only, so an alsa-lib (or libdrm / libinput) revision bump produced a fresh sdl2 cache while sysroot symlinks for the deps stayed pointing at pre-bump caches — programs then linked against stale dep archives. The resolver is idempotent and cached, so always re-resolving is cheap. - `alsa-lib/src/conf_stubs.c` + `build.toml` (rev4 → rev5): adds empty-list stubs for `snd_device_name_hint` / `snd_device_name_free_hint` / `snd_device_name_get_hint`. SDL2's `SDL_alsa_audio.c` calls `snd_device_name_hint` during init to enumerate PCM devices; without it the import resolves to `Unimplemented import: env.snd_device_name_hint` at first use. Our PCM-direct subset doesn't compile namehint.c, so these stubs return an empty hint array (sufficient for SDL2's probe). - `host/src/dri/kms-registry.ts` + `host/src/kernel.ts`: the `host_kms_mode_info` host import now returns a populated 1024x768@60 VESA-standard modeinfo struct flagged `PREFERRED | DRIVER`. The previous impl returned 68 zero bytes, which caused SDL2's `KMSDRM_AddDisplay` to fail its `mode.hdisplay == 0 || mode.vdisplay == 0` check with "Couldn't get a valid connector videomode." Our virtual KMS surface has no real fixed mode, but consumers that probe the connector's preferred mode (any KMSDRM client, not just SDL2) need a non-zero default. Verified: B3/B4/B5 smokes pass; dri-libdrm-kms, dri-modeset, dri-kms-pageflip, dri-dumb-roundtrip, dri-smoke, dri-multiplex, dri-registry, dri-kms-registry, dri-kms-stats-sab, audio-driver, browser-audio-driver-drain all pass — no regressions in the existing DRI or audio paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…dioDevices for SDL_THREADS_DISABLED (rev2) Patch SDL2's audio thread machinery so SDL_OpenAudioDevice succeeds on wasm32 (--disable-pthreads), and add a non-thread driver for the audio callback loop. - src/audio/SDL_audio.c::open_audio_device — gate the SDL_CreateThread block in `#if !SDL_THREADS_DISABLED`. When threads are disabled, call impl.ThreadInit synchronously and register the device in a small `wpk_polled_audio_devices[]` static array. The thread-driven path is byte-identical when SDL_THREADS_DISABLED is undefined (upstream behaviour preserved). - src/audio/SDL_audio.c::close_audio_device — polled-mode counterpart: ThreadDeinit + unregister. - New public API `SDL_PumpAudioDevices(void)` (gated on SDL_THREADS_DISABLED in include/SDL_audio.h) walks the registered devices and drives one iteration of the audio loop per call — fills the callback buffer, calls impl.PlayDevice, returns without WaitDevice. The application's main loop invokes it each frame; the kernel's per-quantum tick advances hw_ptr between calls. - src/audio/alsa/SDL_alsa_audio.c::ALSA_PlayDevice — treat -EAGAIN from snd_pcm_writei as "ring full, try again next pump" (return) instead of `SDL_Delay(1); continue;`. The spin-and-retry path deadlocks the single-threaded polled loop: nothing else runs while SDL_Delay holds the main thread, so the kernel's PCM tick never advances. The thread-driven path is unaffected — its outer while loop re-enters PlayDevice naturally. Resolves plan 7 open-architecture #1 — option (b) per the plan 9 devil's-advocate (see docs/plans/2026-06-29-sdl2-port-plan.md §"audio thread resolution: option (b)"). ~115 LoC across the three files. build.toml revision 1 → 2 to invalidate cached sdl2 archive and force a rebuild against the new patch set. Verified: libSDL2.a (3.9 MB) builds; `llvm-nm` shows SDL_PumpAudioDevices exported as a global text symbol; B3/B4/B5 smoke tests still pass (the polling-audio path is gated on SDL_THREADS_DISABLED + only exercised when SDL_OpenAudioDevice is called, which the smokes don't do). sdl2_demo (Phase C) will be the first consumer once it lands. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… gbm_surface_* build-musl.sh now archives libc/glue/libegl_stub.c into sysroot/lib/libEGL.a and libc/glue/libglesv2_stub.c into sysroot/lib/libGLESv2.a, matching the existing libgbm pattern. The alternative of three new packages (libegl-stub / libgles2-stub / libgbm-extended) was rejected as needless ceremony — the source files already live under libc/glue/ alongside the syscall + libgbm glue. libegl_stub.c gains the stubs SDL_egl.c's LOAD_FUNC macros need under SDL_VIDEO_STATIC_ANGLE: eglGetProcAddress (returns NULL — we expose no EGL-side extensions), eglSwapInterval (no vsync knob; accept any value), eglWaitGL / eglWaitNative (flush + return TRUE), eglQueryAPI (returns OPENGL_ES_API), eglCreatePbufferSurface (returns EGL_NO_SURFACE; we don't enable offscreen). libgbm_stub.c grows the gbm_surface_* API SDL2's KMSDRM backend drives: create / create_with_modifiers / create_with_modifiers2 / lock_front_buffer / release_buffer / has_free_buffers / destroy. Each surface owns a fixed two-BO ring (double-buffer); lock hands out the next free BO, release marks it free. Modifiers other than DRM_FORMAT_MOD_LINEAR are ignored — every BO stays linear, CPU-shared, single-plane. Also adds gbm_device_is_format_supported, gbm_bo_write, and gbm_bo_set/get_user_data so the KMSDRM mouse/cursor + FB cache paths have somewhere to land. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…n both pinned
refine_hw_params() previously computed PARAM_PERIODS from
PARAM_BUFFER_SIZE / PARAM_PERIOD_SIZE and OVERWROTE whatever the
caller had constrained. alsa-lib's hw_params_choose() drives the
handshake through repeated HW_REFINEs that tighten one interval at a
time — set_periods_min(2) sends periods=[2,…] and expects the kernel
to keep that floor across subsequent refines. The overwrite dropped
that constraint silently; SDL2's audio path (set_period_size_near +
set_periods_first(2) + commit) then surfaced -1 from
ALSA_set_buffer_size which SDL_strerror mapped to EPERM
("Couldn't set hardware audio parameters: Operation not permitted").
Fix: intersect derived periods [buffer_min/period_max,
buffer_max/period_min] with the caller's [periods.min, periods.max];
return EINVAL if the intersection is empty (rather than ignoring
the caller). Additionally, when both period_size and periods pin to
single values but buffer_size is still a range, derive
buffer = period × periods and pin it eagerly — that's what the next
refine would converge to anyway, and HW_PARAMS' read_interval_single
requires single values.
The refined_hw_params() test helper now pins periods to
buffer/period (the only self-consistent value) instead of the
interval's `min`; the previous tests passed only because the kernel
silently ignored the broken periods=1 they were feeding it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…Sv2 statically
With --disable-loadso, SDL_LoadObject is a stub that always returns
NULL. SDL_egl.c's default LOAD_FUNC macro then errors out at
SDL_EGL_LoadLibraryInternal with "Could not initialize OpenGL / GLES
library" before any window can be created, even though libEGL.a +
libGLESv2.a are linked into libSDL2.a.
The SDL_VIDEO_STATIC_ANGLE macro (intended for ANGLE's static-EGL
build, but generic) flips LOAD_FUNC to a direct symbol-address
assignment:
_this->egl_data->eglFoo = (void *)eglFoo;
and skips the dlopen path entirely. Defining it via CPPFLAGS gives
SDL_egl.c the statically-linked entry points it expects. build.toml
revision 2 → 3 forces a fresh cache_key_sha so the resolver rebuilds
libSDL2.a with the new flag.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
programs/sdl2_demo.c exercises KMSDRM video + ALSA audio + evdev
input together — 320×240 spinning GLES2 quad, continuous 440 Hz
sine, exits on either a 5 s timeout or ESC keydown. The main loop
drives both SDL_PumpEvents() and the new SDL_PumpAudioDevices() each
iteration, the latter required because SDL_THREADS_DISABLED means
the audio callback runs on the polling driver rather than a
SDL_CreateThread worker.
setenv("SDL_EVDEV_DEVICES", "2:/dev/input/event0,1:/dev/input/event1")
substitutes for libudev: src/core/linux/SDL_evdev.c's no-udev branch
is literally `TODO: scan like a caveman` and leaves the device list
empty unless this env var is set. Both devices match the kandelo
kernel's two virtual evdev surfaces (input-evdev-smoke.test.ts).
Elapsed time is captured BEFORE SDL_Quit() because
SDL_QuitSubSystem(TIMER) tears down the start_ts cache; a post-Quit
SDL_GetTicks() would re-init from a fresh base and the subtraction
would wrap to ~UINT32_MAX.
host/test/sdl2-demo.test.ts covers both exit paths. The ESC variant
uses NodeKernelHost.injectInputEvent to push KEY_ESC press + SYN
about 250 ms into the run; the timeout variant just lets the demo
hit its own 5 s deadline. Both assert exit code 0 and stdout's
sentinel line shape.
build-programs.sh's sdl2_demo case adds libEGL.a + libGLESv2.a
explicitly because the source's <SDL2/SDL_opengles2.h> only
transitively pulls <GLES2/gl2.h>, and build_program's auto-detect
regex matches only direct top-level EGL/GLES includes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…lper + tests, polling-audio cap error, gbm-ring smoke, docs, vitest exnref flag, erlang subprocess flag, llvm-bin fallback Devil's-advocate audit follow-up for PR #709 + the cluster of test-infrastructure fixes needed to get vitest from 151 fail -> 1 fail. Audit fixes (see docs/plans/2026-06-16-dri-kandelo-port-handoff-58-audit.md): - programs/sdl2_demo.c + host/test/sdl2-demo.test.ts: trim WHAT-comments, rename test, drop redundant SDL_PumpAudioDevices extern. - crates/kernel/src/audio/pcm_ioctl.rs: 3 regression tests for cf61010. - host/src/kernel-worker.ts + host/test/ioctl-encoded-marshalling.test.ts: extract computeIoctlEncodedSize helper + 6 boundary tests. - programs/gbm_surface_smoke.c + host/test/gbm-surface-ring.test.ts: lock/release/has_free_buffers smoke + vitest. - packages/registry/sdl2/patches/0002-polling-audio-eagain.patch (rev3 -> rev4): surface SDL_SetError on >8 concurrent opens. - docs/{architecture,posix-status}.md: document ALSA direct path, IoctlEncoded, polling-audio, KMS surface, 2-BO gbm ring, GL cmdbuf. Test-infra fixes (this session): - host/vitest.config.ts: poolOptions.forks.execArgv += [--experimental-wasm-exnref]. Lets cached php/erlang/spidermonkey/ wordpress/mariadb (built with -fwasm-exceptions) compile. - packages/registry/erlang/test/erlang.test.ts: spawn node with the flag rather than 'npx tsx', since execArgv doesn't inherit across process spawn. - host/test/fork-dlopen-replay-e2e.test.ts: discoverLlvmBin() falls back to 'command -v clang' when /opt/homebrew is absent; skipIf when no clang is found at all. Tests: cargo 1072/0 (handoff-58, no kernel changes since), vitest 882/1/19 (was 710/151/41; 1 remaining fail = installs cowsay with npm, pre-existing at tip e6cc2f5 - see docs/plans/2026-06-16-dri-kandelo-port-handoff-59.md section 'The 1 remaining failure'), libc-test/posix-tests GREEN per handoff-58. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase B-1 matrix build status —
|
| Package | Arch | Status | Sha |
|---|---|---|---|
| alsa-lib | wasm32 | built | 1caa0b95 |
| libcurl | wasm32 | built | f458bc9d |
| libcxx | wasm32 | built | ff70de04 |
| libcxx | wasm64 | built | 81c2ec20 |
| libdrm | wasm32 | built | 86e06240 |
| libinput-lite | wasm32 | built | d652e083 |
| libpng | wasm32 | built | 60f4d66b |
| libxml2 | wasm32 | built | 90f0f99a |
| libxml2 | wasm64 | built | 2af7935f |
| openssl | wasm32 | built | a24620d0 |
| openssl | wasm64 | built | cfa4e28e |
| sdl2 | wasm32 | built | 94a0ce9f |
| sqlite | wasm32 | built | 0e5738b2 |
| sqlite | wasm64 | built | 0e34b4a2 |
| zlib | wasm32 | built | fe36cb3f |
| zlib | wasm64 | built | 3f5d181e |
| bc | wasm32 | built | a52ea0bc |
| bzip2 | wasm32 | built | 57b54f64 |
| coreutils | wasm32 | built | d71e7695 |
| curl | wasm32 | built | 0818e0ab |
| dash | wasm32 | built | 7c056df3 |
| diffutils | wasm32 | built | 5cbf6377 |
| dinit | wasm32 | built | 630dcc01 |
| fbdoom | wasm32 | built | e5e1c70b |
| file | wasm32 | built | dd036c27 |
| findutils | wasm32 | built | eb035db6 |
| gawk | wasm32 | built | 5487de54 |
| git | wasm32 | built | 5fdb5e16 |
| grep | wasm32 | built | e32dadf9 |
| gzip | wasm32 | built | df2c567c |
| kandelo-sdk | wasm32 | built | 6e2d73a9 |
| kernel | wasm32 | built | 830c3ef8 |
| less | wasm32 | built | 9a60cd57 |
| lsof | wasm32 | built | 85fe5cf2 |
| m4 | wasm32 | built | b714beef |
| make | wasm32 | built | e8b2e0c5 |
| mariadb | wasm32 | built | 0c32739e |
| mariadb | wasm64 | built | c67da976 |
| msmtpd | wasm32 | built | bde52af6 |
| nano | wasm32 | built | 328824ad |
| ncurses | wasm32 | built | eccd0780 |
| netcat | wasm32 | built | bb4af825 |
| nginx | wasm32 | built | 1cc7bb16 |
| php | wasm32 | built | 96236a5a |
| posix-utils-lite | wasm32 | built | 1066f489 |
| sed | wasm32 | built | 4c0c1e20 |
| spidermonkey | wasm32 | built | 94b390a7 |
| tar | wasm32 | built | a14c8877 |
| tcl | wasm32 | built | 96f124a3 |
| unzip | wasm32 | built | 94f1d123 |
| userspace | wasm32 | built | dd56ddb4 |
| vim | wasm32 | built | 004fb47b |
| wget | wasm32 | built | bfe5d484 |
| xz | wasm32 | built | e3ff82eb |
| zip | wasm32 | built | 39a7aee0 |
| zstd | wasm32 | built | ed37b4a4 |
| bash | wasm32 | built | 0e8d90ce |
| mariadb-test | wasm32 | built | 67e5f804 |
| mariadb-vfs | wasm32 | built | 72e579b6 |
| mariadb-vfs | wasm64 | built | 38b04794 |
| nethack | wasm32 | built | 98adb3f8 |
| node | wasm32 | built | 54c6bc2d |
| spidermonkey-node | wasm32 | built | 1b68454d |
| vim-browser-bundle | wasm32 | built | c92f8b4f |
| nethack-browser-bundle | wasm32 | built | b3833c25 |
| rootfs | wasm32 | built | e3804f83 |
| shell | wasm32 | built | 010d14a1 |
| lamp | wasm32 | built | ea14b4c1 |
| node-vfs | wasm32 | built | 2f8273a7 |
| wordpress | wasm32 | built | e6f3bdb8 |
Auto-generated; replaced on each push. Raw data in the publish-status workflow artifact.
…SC via evdev
Lands the Kandelo SDL2 demo as the GLSL-playground base before any
playground-specific behavior. Phase 0 keeps the spinning quad, the
5 s self-timeout, and the ESC-quits-early path; future phases will
edit it from this baseline.
programs/sdl2/main.c (renamed from sdl2_demo.c)
GLES2 program drawing a rotating textured quad on /dev/dri/card0,
440 Hz square wave through ALSA, ESC via /dev/input/event*.
Self-exits after 5 s. Reports exit reason on stdout.
Kernel + ABI
- SNDRV_PCM_IOCTL_HWSYNC handler so SDL2's polling-audio loop can
advance hw_ptr without writing.
- DRM_IOCTL_MODE_ADDFB + WpkDrmModeFbCmd for SDL2's KMSDRM
backend (KMSDRM_FBFromBO uses the legacy single-plane API).
- kernel_audio_get_hw_ptr / get_state / init_appl_ptr_sab — three
additive kernel-wasm exports for SAB-backed pointer tracking.
- audio/sab.rs + audio/tick.rs + audio/pcm_ioctl.rs: SAB-backed
appl_ptr/hw_ptr, status state machine, and HWSYNC/STATUS ioctl
branches.
- abi/snapshot.json regenerated; additive only, no ABI_VERSION
bump per docs/abi-versioning.md.
Host runtime (Node + Browser symmetry)
- browser-kernel-* / node-kernel-* / kernel-worker / kernel.ts:
forward SAB-handle wiring + audio init through both hosts.
- audio drivers + worklet: wire SAB pointer init and the polling
pump so SDL2's audio thread-less mode pulls samples per frame.
Kandelo demo wiring
- presets.ts + live-setup.ts: drop "evdev" + "espeak" presets,
add the "sdl2" preset (kms feature, BrowserAudioDriver +
BrowserInputSource attached, stages sdl2.wasm under
/usr/local/bin).
- Browser test apps/browser-demos/test/kandelo-sdl2.spec.ts and
host vitest host/test/sdl2.test.ts validate the boot path,
quad-pixel sampling, and audio-pump survival.
Removed
- programs/evdev_demo.c, programs/sdl2_demo.c (replaced).
- host/test/sdl2-demo.test.ts (renamed to sdl2.test.ts).
- apps/browser-demos/test/kandelo-{evdev,espeak}.spec.ts
(presets removed).
sysroot
- packages/registry/sdl2/patches/0002-polling-audio-eagain.patch
extended so SDL_PumpAudioDevices reports EAGAIN-style backoff
instead of busy-looping.
Docs
- docs/plans/2026-06-17-sdl2-* handoff chain + the
2026-06-17-sdl2-glsl-playground-plan (still authoritative;
Phase 1 is unchanged from the plan and was NOT implemented
in this commit).
- 2026-06-18-sdl2-glsl-playground-handoff-1.md captures why
Phase 1 wasn't attempted yet.
Verified
- cargo test -p kandelo --target aarch64-apple-darwin --lib
- vitest including host/test/sdl2.test.ts
- Playwright apps/browser-demos/test/kandelo-sdl2.spec.ts
- ABI snapshot check passes additive-only
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…font/pointer fixes
A viewport-split SDL2 GLSL playground for the Kandelo browser demo:
a 2/3-width gap-buffer text editor (Inconsolata atlas via stb_truetype)
beside a 1/3-width live shader-render pane.
- GLSL ES 1.0 mainImage wrapper + built-in plasma preset, with a VFS
shader-source chain: F5 reload, red error strip on compile failure,
and last-good-shader retention.
- Editor: gap buffer (cursor == gs invariant), full key handling
(arrows/home/end/pgup/pgdn/tab/enter/backspace/delete, numpad),
250 ms debounced auto-recompile, Ctrl+S persist + force-reload.
- Renderer facade owns the user image-shader / error-strip / textured-quad
programs and the Inconsolata atlas.
Browser-verified fixes folded in (all distinct root causes):
- KMS virtual connector now advertises 1920×1080@60 (CTA-861) so SDL's
KMSDRM backend builds a framebuffer that fills the Modeset canvas 1:1
(host/src/dri/kms-registry.ts).
- 28px font atlas (256×240, 1× oversample, stride 34) under the
OP_TEX_IMAGE_2D 65499-byte cap; imageRendering:auto on the canvas.
- evdev relative "peg-and-move" pointer emulation: SDL classifies event1
as a relative mouse (REL_X+REL_Y advertised) and ignores EV_ABS, so
sendPointerAbs() pegs to (0,0) then moves to the target across three
SYN frames (kernel-host.ts sendPointerAbs + injectInputEvent on
KernelLike; wired from Modeset.tsx; BrowserInputSource {pointer:false}
keyboard-only option to avoid double-feeding event1).
- EDITOR_TOP_PAD=8 applied consistently to layout/render/hit-test.
libgles stub: glPixelStorei, glDeleteBuffers, and real glTexImage2D pixel
upload (bytes_per_pixel table) needed for the atlas upload.
Build: scripts/build-programs.sh regenerates inconsolata_ttf.h from the
vendored TTF (git-ignored generated header).
Also fixes a SIGPIPE/pipefail false-negative in check-abi-version.sh:
a large lib.rs diff let `grep -q` close the pipe before `git diff`
finished, SIGPIPE-ing it to exit 141, which pipefail then propagated as
"ABI_VERSION not bumped". Switched to process substitution; semantics
unchanged. (No ABI change here — this work is user-space C + host TS.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
Summary
Ports SDL2 2.30.0 (KMSDRM + ALSA + evdev backends) onto kandelo's wasm32-musl target end-to-end, lands the GL stubs (libEGL.a + libGLESv2.a + libgbm.a w/ gbm_surface_*) the KMSDRM backend needs, fixes a kernel-side
refine_hw_paramsbug surfaced by SDL2's ALSA init handshake, and addsprograms/sdl2_demo.c+ a Node vitest that boots SDL2 across all three backends and exits cleanly on timeout AND ESC.10 commits since
explore-dri-evdev-and-alsa. ABI 15 → 16 (the IoctlEncoded marshalling + audio struct shrinks, landed in the first commit). The kernel PCM refine fix is semantic-only — no struct/syscall-number drift.What this PR lands
Phase A — kernel + shim prep
4dc64cf79— libdrm-KMS shim + alsa-lib shim + libinput-lite stub + kernel CTL ioctl gate +SyscallArgSize::IoctlEncoded { arg_index, floor }. ABI 15 → 16. Audio structs aligned to wasm32unsigned long = 4:WpkAlsaPcmSwParams136 → 104,WpkAlsaXferi24 → 12,WpkAlsaPcmMmapStatus64 → 56,WpkAlsaPcmMmapControl64 → 12.9312b390f—WpkAlsaPcmStatus/MmapStatus/MmapControlfinal alignment pass for wasm32uframes_t = 4(timespec layout shift, newdriver_tstamp+audio_tstamp_accuracyfields).Phase B — SDL2 package (B1 → B5)
6eda62af4— SDL2 2.30.0 package scaffolding + dep manifest (B1).8ffe0c0b2— cross-compile pass: configure-time overrides for host-only autoconf detections, evdev shim header (packages/registry/sdl2/src/sdl2-evdev-shim.h, 389 LoC), dynapi disable patch (0001-dynapi-disable-on-wasm32.patch). (B2)1d38beac3— B3/B4/B5 backend smokes (KMSDRM, ALSA, evdev) — green. Includes three real fixes uncovered by the smokes:scripts/build-programs.shre-symlinks every transitive dep archive (libasound.a, libdrm.a, libinput.a) on every SDL2 resolve, not just libSDL2.a.alsa-librev5:snd_device_name_hintstubs (empty array) — SDL2's audio init probes this; without it the import resolves toUnimplemented import: env.snd_device_name_hint.host/src/dri/kms-registry.ts::buildVirtualConnectorMode()returns a populated 1024×768@60 VESA-standard modeinfo struct flaggedPREFERRED | DRIVER(was 68 zero bytes, which caused SDL2'sKMSDRM_AddDisplayto fail itsmode.hdisplay == 0check).f60ccff85— polling-audio patch (0002-polling-audio-eagain.patch, ~115 LoC): underSDL_THREADS_DISABLED,SDL_OpenAudioDeviceregisters the device in a static array instead ofSDL_CreateThread, andSDL_PumpAudioDevices()runs one polling iteration per call. ALSA's-EAGAINfromsnd_pcm_writeinow returns "try next pump" instead of spin-and-SDL_Delay(1). (B5)Phase B GL stubs (open-arch gap from handoff-53)
a11dc1bb2—scripts/build-musl.shstep 11b: archiveslibc/glue/libegl_stub.c→sysroot/lib/libEGL.aandlibc/glue/libglesv2_stub.c→sysroot/lib/libGLESv2.a(matches the in-tree libgbm pattern; rejected the "three new packages" alternative as ceremony).libgbm_stub.cgains thegbm_surface_*API (create,create_with_modifiers,lock_front_buffer,release_buffer,destroy,has_free_buffers,set_user_data) backed by a fixed 2-BO ring, plusgbm_device_is_format_supportedandgbm_bo_write. Six new libEGL thin stubs (eglGetProcAddress→ NULL,eglSwapInterval,eglWaitGL/eglWaitNative,eglQueryAPI→EGL_OPENGL_ES_API,eglCreatePbufferSurface→EGL_NO_SURFACE).1ed6bb394— SDL2 rev3:-DSDL_VIDEO_STATIC_ANGLE=1flipsLOAD_FUNCfrom the (stubbed)SDL_LoadFunctionpath to direct symbol-address assignment, so the static libEGL/libGLESv2 archives resolve at link time. Without this,SDL_EGL_LoadLibraryInternalerrors with "Could not initialize OpenGL / GLES library" before any window can be created. The "ANGLE" in the macro name is misleading — it's the upstream-blessed--disable-loadsoescape hatch for any static-EGL build.Phase B kernel PCM refine fix
cf610100d—crates/kernel/src/audio/pcm_ioctl.rs::refine_hw_params(). Pre-fix,PARAM_PERIODSwas recomputed purely fromPARAM_BUFFER_SIZE / PARAM_PERIOD_SIZEand silently overwrote whatever value the caller had constrained it to. SDL2's audio-init handshake (set_period_size_near(1024)→set_periods_min(2)→set_periods_first(2)→snd_pcm_hw_params()) tripped this: alsa-lib treatedset_periods_min(2)as failed,SDL_alsa_audio.c::ALSA_set_buffer_sizereturned -1, andSDL_strerror(-1)mapped to a misleading "Operation not permitted" message. Fix intersects derived[buffer_min/period_max, buffer_max/period_min]with caller's[periods.min, periods.max]; returns EINVAL if intersection is empty. Additionally, when bothperiod_sizeANDperiodspin to a single value butbuffer_sizeis still a range, pinbuffer_size = period * periodseagerly (within the v1 cap and the current buffer range) — matches what alsa-lib'ssnd_pcm_hw_params_choose()converges to anyway. Test helperrefined_hw_params()now pinsPARAM_PERIODS = buffer/period(the only self-consistent value) — previous tests passed only because the kernel silently ignored the brokenperiods=1they fed it.This is NOT an ABI bump.
WpkAlsaPcmHwParamsstruct layout,SNDRV_PCM_IOCTL_HW_REFINE/_HW_PARAMSnumbers, and mask bit layout are unchanged. Pure refine semantics.Phase C — sdl2_demo + vitest (C1 + C2)
e6cc2f5d8—programs/sdl2_demo.c(~200 LoC): KMSDRM video + ALSA audio + evdev input combined, spinning GLES2 quad (320×240, color shifts onsin(t * 2)), continuous 440 Hz sine viaSDL_OpenAudioDevice+ audio callback, exits on either 5s timeout OR ESC keydown.host/test/sdl2-demo.test.tscovers both paths: timeout path passes withframes=68670 elapsed=5117 ms; ESC path passes withexit=escat ~356 ms viaNodeKernelHost.injectInputEvent(KEY_ESC, …).SDL_EVDEV_DEVICES=2:/dev/input/event0,1:/dev/input/event1— without libudev,SDL_evdev.c::SDL_EVDEV_Init's no-udev branch is literally a/* TODO: Scan the devices manually, like a caveman */comment and leaves the device list empty.SDL_EVDEV_DEVICESis the upstream escape hatch (<class>:<path>[,...]).SDL_Quit()—SDL_QuitSubSystem(TIMER)tears down theticks_startedflag inSDL_systimer.c; a post-QuitSDL_GetTicks()re-runsSDL_TicksInitfrom a fresh base and any earlier subtraction wraps to ~UINT32_MAX.SDL_PumpAudioDevices()per frame — required becauseSDL_THREADS_DISABLEDroutes the audio callback through the polling driver landed in B5.scripts/build-programs.sh sdl2_demoexplicitly appendslibEGL.a+libGLESv2.ato the link line because<SDL2/SDL_opengles2.h>only transitively pulls<GLES2/gl2.h>; the auto-detect regex matches direct top-level includes only.Test plan / verification
Cargo (kernel) — GREEN
1069 passed, 0 failed. Includes the 26
audio::pcm_ioctltests covering the new refine semantics.Vitest (Node host) — sdl2_demo, smokes, all DRI/audio/evdev — GREEN
test/sdl2-demo.test.ts > sdl2 end-to-end > 5s timeout path: PASS, exit=timeout, frames=68670, elapsed=5117 ms.test/sdl2-demo.test.ts > sdl2 end-to-end > ESC injection: PASS, exit=esc, ~356 ms.All SDL2 + DRI + audio + evdev backend tests pass at tip.
Vitest (Node host) — full sweep
Test Files: 99 passed | 19 failed | 14 skipped.
Tests: 703 passed | 151 failed | 41 skipped.
The 151 failures are pre-existing on the base branch (
explore-dri-evdev-and-alsa) — not introduced by this PR. Spot-checked signatures:exec-brk-base.test.ts:WebAssembly.compile(): invalid value type 'exn', enable with --experimental-wasm-exnref @+1466— Node-level wasm-exnref feature flag mismatch for the mariadbd binary. Unrelated to SDL2 / DRI / ALSA work.spidermonkey.test.ts:expected -1 to be +0— child exit code -1, consistent with stale-cache binary trap/crash.wordpress/php-sanity.test.ts: empty stdout, child failed to launch.dash/dash-signal-regression.test.ts: signal-handling regression. Unrelated to ALSA/SDL2 surface.Failing test files (all
packages/registry/*and a fewhost/test/):packages/registry/{bash,coreutils,dash,erlang,git,php,spidermonkey,wordpress}/test/*host/test/{cpp-throw,exec-brk-base,fork-dlopen-replay-e2e,fork-instrument-coverage,interactive-stdin,wasm-binary-parse}.test.tsBase-branch evidence log:
[ATTACH BASE LOG].libc-test (musl libc-test) — GREEN
PASS 303, FAIL 0, XFAIL 20, FLAKY 1 (regression/pthread_cond-smasher flake-passed), BUILD 0, TIMEOUT 0, TOTAL 324.
POSIX test suite — GREEN
PASS 174, FAIL 0, XFAIL 3 (mlock/12-1, munmap/1-1, munmap/1-2), SKIP 2.
ABI snapshot — sources consistent, classifier confirms bump is needed
ABI_VERSION = 16,abi/snapshot.json.abi_version = 16— sources and snapshot in sync.abi_constants.h+host/src/generated/abi.tsregenerate identically.xtask dump-abi --classify-compat) flags the host_adapter +syscall_arg_descriptors[72](SYS_IOCTL) sections as breaking — bumped accordingly.Dual-host parity disclosure (per CLAUDE.md)
Host TypeScript diff vs base:
host/src/kernel-worker.tsSyscallArgSize::IoctlEncoded(extracts_IOC_SIZEfrom bits 16..29 of the ioctl request, with afloorfor legacy size=0 ioctls). Pure data-decode, no platform branch.CentralizedKernelWorkerhost/src/kernel.tshost_kms_mode_infonow delegates tobuildVirtualConnectorMode(connector_id)instead of returning 68 zeroes. Pure data construction.host/src/dri/kms-registry.tsbuildVirtualConnectorMode()returning a 1024×768@60 VESA modeinfo.host/src/generated/abi.tsabi/snapshot.json.These changes are all in the shared
kernel-worker.ts/kernel.tslayer that both Node and browser host adapters wrap. There is nonode-…vsbrowser-…split here; both runtimes execute the same code path.Node vitest exercises the changes end-to-end (sdl2_demo, sdl2-kmsdrm-smoke, etc.). Browser-side parity:
No new browser entry page for sdl2_demo is included in this PR. The kandelo browser demo's program loader expects packaged Kandelo images (not ad-hoc wasm binaries); plumbing sdl2_demo through that pipeline is its own focused UI work — separate from validating the SDL2 sysroot port. The Modeset pane already demonstrates that the shared KMS canvas bridge works for KMSDRM programs; SDL2 reuses that same KMS host import surface unchanged. Tracking the browser sdl2_demo entry as a follow-up.
§C4 lock-contention profiling gate — deferred-by-design
The original plan §C4 called for a vitest harness that publishes per-tick-handler
PROCESS_TABLE.lock()acquire-vs-body percentages with a <5% threshold at peak demo load.grep -rn "PROCESS_TABLE.lock\|tick_handler" crates/kernel/src host/srcreturns nothing — no instrumentation, no tick-handler-tagged code paths, no IPC channel for lock-timing telemetry. Implementing the gate end-to-end would mean adding kernel-side lock-time accounting, a host telemetry channel, a vitest harness that drives the demo at peak load (1000 Hz fake input, 375 Hz audio quanta, 60 Hz vblank), and a reporter — substantial scope outside SDL2 port work.More importantly, the kernel runs on a single dedicated worker thread (Node
worker_thread, browserWorker) per CLAUDE.md's architecture requirement. There is no second thread that could contend forPROCESS_TABLE. By construction, lock acquire time is "the time to call.lock()on an uncontendedMutex" — measured in nanoseconds against bodies measured in microseconds. The <5% threshold is essentially guaranteed by the architecture.Deferring to wpkcompositor — when a workload that actually multiplexes lands and PROCESS_TABLE genuinely becomes hot, the OFD-table-split refactor + the contention gate will land together.
Findings worth preserving (avoid re-deriving)
SDL_VIDEO_STATIC_ANGLE=1is the right macro for static-EGL builds. Despite the name, it just means "EGL symbols are link-time, not dlopen". Applies cleanly to any static-EGL build; the SDL_egl.c check is#if defined(SDL_VIDEO_STATIC_ANGLE) || defined(SDL_VIDEO_DRIVER_VITA). Composes with our existing--disable-loadso+DYNAPI_NEEDS_DLOPENpatches.SDL_EVDEV_DEVICESis the no-udev escape hatch.SDL_evdev.c::SDL_EVDEV_Init's no-libudev branch has a literalTODO: Scan the devices manually, like a cavemancomment and leaves the device list empty. Format:<cls>:<path>[,…], class 1=mouse, 2=keyboard, 4=joystick, 8=sound, 16=touchscreen, 32=accelerometer, 64=touchpad.SDL_Quitinvalidates theSDL_GetTicksbase.SDL_QuitSubSystem(TIMER)resetsticks_startedinSDL_systimer.c. Always capture elapsed time beforeSDL_Quit.gbm_surfacev1 is a two-BO ring with eager allocation. Mesa's surface is a variable-size ring; we fix it at 2 (double-buffer), allocated eagerly atgbm_surface_createso dims stay constant across lock cycles. Both-BOs-in-flight →gbm_surface_lock_front_bufferreturns NULL + EBUSY (matches mesa).refine_hw_paramswas silently dropping callerPARAM_PERIODSconstraints. Any caller that tried to constrain periods saw its constraint dropped, thensnd_pcm_hw_params_choose()returned an inconsistent struct, thenHW_PARAMS'read_interval_singlefailed, then alsa-lib'sSDL_strerrormapped the truncated -1 to "Operation not permitted." When SDL2'sSDL_OpenAudioDevicereports "Operation not permitted" on a wasm32 ALSA path, look at the kernel-side hw_params delta first.NodeKernelHost.injectInputEventworks end-to-end for SDL2 evdev input. The full path (injectInputEvent → kernel /dev/input/eventN ring → SDL_PumpEvents → SDL_KEYDOWN) is exercised byhost/test/sdl2-demo.test.ts's ESC variant.Commits since base
🤖 Generated with Claude Code