Skip to content

[explore-dri-sdl2] sysroot(sdl2): full Phase A–C — backends + GL stubs + sdl2_demo#709

Draft
mho22 wants to merge 13 commits into
explore-dri-evdev-and-alsafrom
explore-dri-sdl2
Draft

[explore-dri-sdl2] sysroot(sdl2): full Phase A–C — backends + GL stubs + sdl2_demo#709
mho22 wants to merge 13 commits into
explore-dri-evdev-and-alsafrom
explore-dri-sdl2

Conversation

@mho22

@mho22 mho22 commented Jun 16, 2026

Copy link
Copy Markdown
Collaborator

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_params bug surfaced by SDL2's ALSA init handshake, and adds programs/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 wasm32 unsigned long = 4: WpkAlsaPcmSwParams 136 → 104, WpkAlsaXferi 24 → 12, WpkAlsaPcmMmapStatus 64 → 56, WpkAlsaPcmMmapControl 64 → 12.
  • 9312b390fWpkAlsaPcmStatus/MmapStatus/MmapControl final alignment pass for wasm32 uframes_t = 4 (timespec layout shift, new driver_tstamp + audio_tstamp_accuracy fields).

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:
    • Stale-sysroot-symlink: scripts/build-programs.sh re-symlinks every transitive dep archive (libasound.a, libdrm.a, libinput.a) on every SDL2 resolve, not just libSDL2.a.
    • alsa-lib rev5: snd_device_name_hint stubs (empty array) — SDL2's audio init probes this; without it the import resolves to Unimplemented import: env.snd_device_name_hint.
    • Virtual connector mode-info: host/src/dri/kms-registry.ts::buildVirtualConnectorMode() returns a populated 1024×768@60 VESA-standard modeinfo struct flagged PREFERRED | DRIVER (was 68 zero bytes, which caused SDL2's KMSDRM_AddDisplay to fail its mode.hdisplay == 0 check).
  • f60ccff85 — polling-audio patch (0002-polling-audio-eagain.patch, ~115 LoC): under SDL_THREADS_DISABLED, SDL_OpenAudioDevice registers the device in a static array instead of SDL_CreateThread, and SDL_PumpAudioDevices() runs one polling iteration per call. ALSA's -EAGAIN from snd_pcm_writei now returns "try next pump" instead of spin-and-SDL_Delay(1). (B5)

Phase B GL stubs (open-arch gap from handoff-53)

  • a11dc1bb2scripts/build-musl.sh step 11b: archives libc/glue/libegl_stub.csysroot/lib/libEGL.a and libc/glue/libglesv2_stub.csysroot/lib/libGLESv2.a (matches the in-tree libgbm pattern; rejected the "three new packages" alternative as ceremony). libgbm_stub.c gains the gbm_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, plus gbm_device_is_format_supported and gbm_bo_write. Six new libEGL thin stubs (eglGetProcAddress → NULL, eglSwapInterval, eglWaitGL/eglWaitNative, eglQueryAPIEGL_OPENGL_ES_API, eglCreatePbufferSurfaceEGL_NO_SURFACE).
  • 1ed6bb394 — SDL2 rev3: -DSDL_VIDEO_STATIC_ANGLE=1 flips LOAD_FUNC from the (stubbed) SDL_LoadFunction path to direct symbol-address assignment, so the static libEGL/libGLESv2 archives resolve at link time. Without this, SDL_EGL_LoadLibraryInternal errors 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-loadso escape hatch for any static-EGL build.

Phase B kernel PCM refine fix

  • cf610100dcrates/kernel/src/audio/pcm_ioctl.rs::refine_hw_params(). Pre-fix, PARAM_PERIODS was recomputed purely from PARAM_BUFFER_SIZE / PARAM_PERIOD_SIZE and 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 treated set_periods_min(2) as failed, SDL_alsa_audio.c::ALSA_set_buffer_size returned -1, and SDL_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 both period_size AND periods pin to a single value but buffer_size is still a range, pin buffer_size = period * periods eagerly (within the v1 cap and the current buffer range) — matches what alsa-lib's snd_pcm_hw_params_choose() converges to anyway. Test helper refined_hw_params() now pins PARAM_PERIODS = buffer/period (the only self-consistent value) — previous tests passed only because the kernel silently ignored the broken periods=1 they fed it.

This is NOT an ABI bump. WpkAlsaPcmHwParams struct layout, SNDRV_PCM_IOCTL_HW_REFINE/_HW_PARAMS numbers, and mask bit layout are unchanged. Pure refine semantics.

Phase C — sdl2_demo + vitest (C1 + C2)

  • e6cc2f5d8programs/sdl2_demo.c (~200 LoC): KMSDRM video + ALSA audio + evdev input combined, spinning GLES2 quad (320×240, color shifts on sin(t * 2)), continuous 440 Hz sine via SDL_OpenAudioDevice + audio callback, exits on either 5s timeout OR ESC keydown. host/test/sdl2-demo.test.ts covers both paths: timeout path passes with frames=68670 elapsed=5117 ms; ESC path passes with exit=esc at ~356 ms via NodeKernelHost.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_DEVICES is the upstream escape hatch (<class>:<path>[,...]).
    • Elapsed time captured before SDL_Quit()SDL_QuitSubSystem(TIMER) tears down the ticks_started flag in SDL_systimer.c; a post-Quit SDL_GetTicks() re-runs SDL_TicksInit from a fresh base and any earlier subtraction wraps to ~UINT32_MAX.
    • SDL_PumpAudioDevices() per frame — required because SDL_THREADS_DISABLED routes the audio callback through the polling driver landed in B5.
    • scripts/build-programs.sh sdl2_demo explicitly appends libEGL.a + libGLESv2.a to 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

cargo test -p kandelo --target aarch64-apple-darwin --lib

1069 passed, 0 failed. Includes the 26 audio::pcm_ioctl tests covering the new refine semantics.

Vitest (Node host) — sdl2_demo, smokes, all DRI/audio/evdev — GREEN

cd host && npx vitest run test/sdl2-demo.test.ts

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.

cd host && npx vitest run \
  test/sdl2-kmsdrm-smoke.test.ts \
  test/sdl2-alsa-smoke.test.ts \
  test/sdl2-evdev-smoke.test.ts \
  test/alsa-lib-smoke.test.ts \
  test/input-evdev.test.ts \
  test/dri-libdrm-kms.test.ts \
  test/dri-smoke.test.ts \
  test/dri-modeset.test.ts \
  test/dri-kms-pageflip.test.ts \
  test/dri-cube-pyramid.test.ts \
  test/dri-dumb-roundtrip.test.ts \
  test/audio-driver.test.ts

All SDL2 + DRI + audio + evdev backend tests pass at tip.

Vitest (Node host) — full sweep

cd host && npx vitest run

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 few host/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.ts

Base-branch evidence log: [ATTACH BASE LOG].

libc-test (musl libc-test) — GREEN

scripts/run-libc-tests.sh

PASS 303, FAIL 0, XFAIL 20, FLAKY 1 (regression/pthread_cond-smasher flake-passed), BUILD 0, TIMEOUT 0, TOTAL 324.

POSIX test suite — GREEN

scripts/run-posix-tests.sh

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

bash scripts/check-abi-version.sh
  • ABI_VERSION = 16, abi/snapshot.json.abi_version = 16 — sources and snapshot in sync.
  • Snapshot regen idempotent; abi_constants.h + host/src/generated/abi.ts regenerate identically.
  • Classifier (xtask dump-abi --classify-compat) flags the host_adapter + syscall_arg_descriptors[72] (SYS_IOCTL) sections as breaking — bumped accordingly.

Known pre-existing script bug: check-abi-version.sh uses git diff origin/main -- crates/shared/src/lib.rs | grep -qE '^\+pub const ABI_VERSION' to detect the bump. With set -o pipefail, grep -q exits on first match → git diff gets SIGPIPE → pipeline returns 141 → script falsely concludes "ABI_VERSION was not bumped" when the diff exceeds the 64 KB pipe buffer (this branch's lib.rs diff is ~68 KB, our bump's prose is the trigger). Fix is a one-line change to capture the diff into a variable, or redirect grep output to /dev/null instead of using -q. Tracked separately — not in scope for this PR.

Dual-host parity disclosure (per CLAUDE.md)

Host TypeScript diff vs base:

File Change Host runtime
host/src/kernel-worker.ts +20/-2 — handles SyscallArgSize::IoctlEncoded (extracts _IOC_SIZE from bits 16..29 of the ioctl request, with a floor for legacy size=0 ioctls). Pure data-decode, no platform branch. shared via CentralizedKernelWorker
host/src/kernel.ts +9/-3 — host_kms_mode_info now delegates to buildVirtualConnectorMode(connector_id) instead of returning 68 zeroes. Pure data construction. shared
host/src/dri/kms-registry.ts +35 — exports buildVirtualConnectorMode() returning a 1024×768@60 VESA modeinfo. shared
host/src/generated/abi.ts +7/-3 — auto-regenerated from abi/snapshot.json. shared

These changes are all in the shared kernel-worker.ts / kernel.ts layer that both Node and browser host adapters wrap. There is no node-… vs browser-… 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:

  • The IoctlEncoded handling is data-decode-only — no Web/Node API surface used. Functionally identical in both runtimes.
  • The virtual-connector mode-info change is data construction — same.
  • The mariadbd-LAMP demo (which exercises the IoctlEncoded host adapter via real ioctl traffic) has been the canary for the browser path since PR fix(musl-overlay,host): a_crash trap + handle worker exit message #410; this PR's IoctlEncoded change was already deployed there and passes the existing browser smoke (apps/browser-demos pipeline).

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/src returns 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, browser Worker) per CLAUDE.md's architecture requirement. There is no second thread that could contend for PROCESS_TABLE. By construction, lock acquire time is "the time to call .lock() on an uncontended Mutex" — 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=1 is 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_DLOPEN patches.
  • SDL_EVDEV_DEVICES is the no-udev escape hatch. SDL_evdev.c::SDL_EVDEV_Init's no-libudev branch has a literal TODO: Scan the devices manually, like a caveman comment 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_Quit invalidates the SDL_GetTicks base. SDL_QuitSubSystem(TIMER) resets ticks_started in SDL_systimer.c. Always capture elapsed time before SDL_Quit.
  • gbm_surface v1 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 at gbm_surface_create so dims stay constant across lock cycles. Both-BOs-in-flight → gbm_surface_lock_front_buffer returns NULL + EBUSY (matches mesa).
  • refine_hw_params was silently dropping caller PARAM_PERIODS constraints. Any caller that tried to constrain periods saw its constraint dropped, then snd_pcm_hw_params_choose() returned an inconsistent struct, then HW_PARAMS' read_interval_single failed, then alsa-lib's SDL_strerror mapped the truncated -1 to "Operation not permitted." When SDL2's SDL_OpenAudioDevice reports "Operation not permitted" on a wasm32 ALSA path, look at the kernel-side hw_params delta first.
  • NodeKernelHost.injectInputEvent works end-to-end for SDL2 evdev input. The full path (injectInputEvent → kernel /dev/input/eventN ring → SDL_PumpEvents → SDL_KEYDOWN) is exercised by host/test/sdl2-demo.test.ts's ESC variant.

Commits since base

4dc64cf79  sysroot(sdl2-shims): libdrm-KMS + alsa-lib + libinput-lite + kernel CTL ioctl gate + ioctl-encoded host marshalling
9312b390f  sysroot(alsa): align WpkAlsaPcmStatus/MmapStatus/MmapControl to wasm32 uframes_t=4
6eda62af4  sysroot(sdl2): scaffold SDL2 2.30.0 package + dep manifest (B1)
8ffe0c0b2  sysroot(sdl2): cross-compile pass — configure overrides + evdev shim + dynapi patch (B2)
1d38beac3  sysroot(sdl2): B3/B4/B5 backend smoke tests + dep-symlink + connector mode-info fixes
f60ccff85  sysroot(sdl2): polling-audio patch — SDL_OpenAudioDevice + SDL_PumpAudioDevices for SDL_THREADS_DISABLED (rev2)
a11dc1bb2  sysroot(gl-stubs): archive libEGL.a + libGLESv2.a; extend libgbm with gbm_surface_*
cf610100d  kernel(pcm): respect caller-supplied periods + derive buffer_size when both pinned
1ed6bb394  sysroot(sdl2): -DSDL_VIDEO_STATIC_ANGLE=1 + rev3 — wire libEGL/libGLESv2 statically
e6cc2f5d8  examples(sdl2): sdl2_demo + vitest end-to-end (Phase C1 + C2)

🤖 Generated with Claude Code

mho22 and others added 11 commits June 15, 2026 21:43
…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>
@github-actions

github-actions Bot commented Jun 16, 2026

Copy link
Copy Markdown

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

ABI v16. 70 built, 0 failed, 70 total.

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.

mho22 and others added 2 commits June 18, 2026 09:55
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant