SDL2 on Kandelo - GLSL shader playground demo - #709
Conversation
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.
…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>
…tion, test pacing Three root causes kept PR #709 red; all fixes verified green in CI (run 28977279667). CI fetch gate (staging-build.yml, prepare-merge.yml, run.sh) The WIP espeak-ng package pins a placeholder all-zero source sha and lists gitignored vendored sources (pcaudiolib-src/) as cache-key inputs, so the resolver cannot hash it on a clean checkout and no archive exists in any binaries-abi-v* index. Disable it in the matrix + fetch-gate skip lists and in run.sh's BROWSER_FETCH_SKIP_PKGS (the single choke point every prepare-browser caller shares). Remove the entries once the port lands a real source pin + published binary. Host runtime (host/src/kernel-worker.ts) — the real platform bug The vblank pump broad-woke every blocked syscall 60x/s in every kernel worker, and each wake re-dispatched select/pselect6 from the entry's original args — re-reading the guest time-struct (which the kernel never decrements) and re-arming the FULL timeout. A select-based sleep woken every 16 ms never expires: mariadbd bootstrap parked in exactly such a sleep after "InnoDB started" and test/exec-brk-base.test.ts timed out 3/3. Fixes, both in the Node/browser-shared worker: - POSIX correctness: handleSelect/handlePselect6 thread a deadlineOverride through every re-dispatch path so the timeout stays measured from the original call; expired re-dispatches complete with 0. - Wake scoping: tickVblank gates the broad wake on real page-flip activity (kernel_kms_commit_count changed since the previous tick, one-tick carry for the submit/drain race). No DRM client → no wakes; a flipping client keeps its 60 fps cadence. The dri-kms-stats-sab tests' throwing sentinel moves from kernel_kms_commit_count (now read every tick by the gate) to kernel_kms_last_frame_us, which only the stats branch reads — the guarded invariant (never touch slots 5/6 of a small SAB) is unchanged. Tests (host/test/sdl2.test.ts, apps/browser-demos/test/kandelo-sdl2.spec.ts) Both paced assertions by wall clock instead of app-observable events, so slow CI runners failed them while fast machines passed: - vitest: wait for the "sdl2: editor recompile" breadcrumb after typing instead of sleeping 300 ms — on a slow runner all queued keys drained in one SDL_PollEvent burst and F2 correctly cancelled the still-pending debounce. - playwright: wait for the app's first presented frame (the Modeset pane unmounts its "waiting for PAGE_FLIP" placeholder on the first flip) before sampling animation, and sample until PNG byte-size variance appears (30 s deadline) instead of a fixed 6x500 ms schedule — under CI's software GL, init outlasted the old sampling window so every screenshot captured the static placeholder. Validated: exec-brk-base 3/3 on a fresh datadir (was 3/3 25 s timeouts; bisected to the Phase 0 vblank-wake commit and confirmed by kernel/host cross-swaps against main), sdl2 vitest + playwright pass locally, host typecheck clean, full host vitest green except pre-existing local-only wasm64 fixture gaps, and the full staging CI run green on this tree. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tion, test pacing Three root causes kept PR #709 red; all fixes verified green in CI (run 28977279667). CI fetch gate (staging-build.yml, prepare-merge.yml, run.sh) The WIP espeak-ng package pins a placeholder all-zero source sha and lists gitignored vendored sources (pcaudiolib-src/) as cache-key inputs, so the resolver cannot hash it on a clean checkout and no archive exists in any binaries-abi-v* index. Disable it in the matrix + fetch-gate skip lists and in run.sh's BROWSER_FETCH_SKIP_PKGS (the single choke point every prepare-browser caller shares). Remove the entries once the port lands a real source pin + published binary. Host runtime (host/src/kernel-worker.ts) — the real platform bug The vblank pump broad-woke every blocked syscall 60x/s in every kernel worker, and each wake re-dispatched select/pselect6 from the entry's original args — re-reading the guest time-struct (which the kernel never decrements) and re-arming the FULL timeout. A select-based sleep woken every 16 ms never expires: mariadbd bootstrap parked in exactly such a sleep after "InnoDB started" and test/exec-brk-base.test.ts timed out 3/3. Fixes, both in the Node/browser-shared worker: - POSIX correctness: handleSelect/handlePselect6 thread a deadlineOverride through every re-dispatch path so the timeout stays measured from the original call; expired re-dispatches complete with 0. - Wake scoping: tickVblank gates the broad wake on real page-flip activity (kernel_kms_commit_count changed since the previous tick, one-tick carry for the submit/drain race). No DRM client → no wakes; a flipping client keeps its 60 fps cadence. The dri-kms-stats-sab tests' throwing sentinel moves from kernel_kms_commit_count (now read every tick by the gate) to kernel_kms_last_frame_us, which only the stats branch reads — the guarded invariant (never touch slots 5/6 of a small SAB) is unchanged. Tests (host/test/sdl2.test.ts, apps/browser-demos/test/kandelo-sdl2.spec.ts, apps/browser-demos/test/kandelo-merge-gate.spec.ts) All paced assertions by wall clock instead of app-observable events, so slow CI runners failed them while fast machines passed: - vitest: wait for the "sdl2: editor recompile" breadcrumb after typing instead of sleeping 300 ms — on a slow runner all queued keys drained in one SDL_PollEvent burst and F2 correctly cancelled the still-pending debounce. - playwright: wait for the app's first presented frame (the Modeset pane unmounts its "waiting for PAGE_FLIP" placeholder on the first flip) before sampling animation, and sample until PNG byte-size variance appears (30 s deadline) instead of a fixed 6x500 ms schedule — under CI's software GL, init outlasted the old sampling window so every screenshot captured the static placeholder. - merge-gate shell demo: wait for bash's first kandelo$ prompt before typing, like the spec's other terminal tests already do. The test typed as soon as the page said "Ready", so on a slow runner the command landed before the shell was reading and was discarded by the boot chain's startup typeahead flush (tcflush) — the CI trace shows the echoed command followed by two bare prompts and an unchanged PS1. Input typed before the first prompt is legitimately flushable on any POSIX terminal, so the prompt is the only correct readiness signal. Validated: exec-brk-base 3/3 on a fresh datadir (was 3/3 25 s timeouts; bisected to the Phase 0 vblank-wake commit and confirmed by kernel/host cross-swaps against main), sdl2 vitest + playwright pass locally, host typecheck clean, full host vitest green except pre-existing local-only wasm64 fixture gaps, and the full staging CI run green on this tree. Merge-gate shell demo test passes 3/3 locally against fetched prebuilt assets after the prompt-wait fix. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
d1b1156 to
e439b86
Compare
d2f79e1 to
a97a334
Compare
e439b86 to
f498fc4
Compare
a97a334 to
52ad9c6
Compare
f498fc4 to
71654e9
Compare
52ad9c6 to
d194992
Compare
The stub was a hand-maintained subset: every ioctl an SDL2 or Wayland client might reach for had to be added to it by hand, and its headers were a vendored copy of the kernel's UAPI that could drift from what the kernel actually answers. libdrm now builds from upstream sources as a package, KMS subset only. That inverts the test: instead of a stub written to satisfy our clients, unmodified upstream code drives the kernel's `DRM_IOCTL_MODE_*` surface, and anything missing fails loudly at the real call site. `build-musl.sh` resolves the package into the sysroot beside the remaining GBM/EGL/GLES shims rather than compiling a stub, so `wasm32posix-pkg-config` answers the same either way and consumers such as `modeset` need no change beyond a rebuild.
`gbm_surface_lock_front_buffer` and `gbm_surface_release_buffer` are how a KMSDRM client hands a rendered frame to the display and gets a free one back. The shim had no surface concept at all, so SDL2's KMSDRM backend had nothing to page-flip between. The ring holds the buffers a surface owns and tracks which one the display currently holds, which is what lets a client render into one while the other is on screen. `gbm_surface_smoke` drives lock/release across a full cycle so a regression shows up as a specific failed transition rather than a black window.
SDL2 was built with the dummy video driver, so a program could link it
but never present a frame. Enabling KMSDRM points it at
`/dev/dri/card0` through libdrm and libgbm, and enabling the evdev
input backend points it at `/dev/input/event{0,1}`.
SDL scans devices by their advertised capability bits, so the vendored
code table grew the entries its probe expects to find. The two smokes
pin each half separately: one asserts a mode is set and a frame reaches
the display, the other that a synthesised keystroke arrives as an
SDL_KEYDOWN — so a break tells you which backend regressed.
The playground is the first program that uses all three backends at once: a gap-buffer editor on the left, the user's fragment shader running on `/dev/dri/card0` on the right, and audio through `/dev/dsp`. Typing recompiles after 250 ms of idle; a failed compile keeps the last good program and shows the offending line, so a typo never blanks the screen. Sound is generated the same way the image is. A second shader renders PCM, and its output feeds the audio device — the chip synth is the fallback for when GL cannot render (the headless harness reads back zero frames). Text comes from a font atlas baked once at startup with stb_truetype, which is why the TTF is vendored and its generated C header is ignored rather than committed: it is reproduced from the TTF on every build.
The playground exercises everything the evdev input-log demo showed —
keystrokes and pointer motion through `/dev/input/event{0,1}` — inside
a product surface rather than a proof of life, so both earlier presets
give way to it. espeak-ng stays a registry package with its published
voice data; only its preset and spec go, and `evdev_demo` goes with the
demo it existed to back.
The two input paths are split on purpose. `BrowserInputSource` gains
options that turn its pointer feed off, because the Modeset pane owns
the pointer: it injects framebuffer-absolute coordinates through
`KmsDisplayHandle.sendPointerAbs`. That call has to emulate an absolute
device over relative deltas — SDL sees `REL_X`/`REL_Y` advertised on
event1, classifies it as a relative mouse and ignores `EV_ABS` — so
each update slams the cursor into a corner with an oversized delta and
then walks it to the target.
The local source-build set names every package it builds and asserts the exact roster, so two new packages have to be placed: `libdrm` is platform — a library the sysroot exposes to any client — and `sdl2-demo` is user-software. Dropping `evdev-demo` leaves the same assertion, which is why the counts move in one place.
|
Folded into #948. The stack was rebased onto
Review the work in #948. The pre-rebase tip |
71654e9 to
6c28ee0
Compare
d194992 to
42e3a4a
Compare
|
Retracting the comment above. This PR was closed in error and is reopened. The stack was rebased onto The branch is force-pushed to The pre-rebase tip is kept locally as |
Why
Kandelo gained a DRI/KMS graphics surface in #678, but nothing in user space drove it through a real graphics toolkit. This PR ports SDL2 2.30 to the wasm32 sysroot — KMSDRM video on
/dev/dri/card0,/dev/dspaudio, evdev input — and ships a Shadertoy-style GLSL shader playground demo on top of it. The playground exercises the whole stack end to end in the browser: a GLES2 fragment-shader render pane, PCM synthesis through a "sound shader", and keyboard-driven editing.Second layer of the six-PR DRI stack (#698 → #709 → #948 → #976 → #1318 → #1241). It stacks on
explore-dri-evdev-and-alsa.What changed
libdrm and libgbm
wlcompositor— talks to the real library rather than a local approximation.gbm_surface_*).SDL2 port
libEGL/libGLESv2wired statically (-DSDL_VIDEO_STATIC_ANGLE=1).SDL_OpenAudioDevice+SDL_PumpAudioDevices) so audio works under the single-threadedSDL_THREADS_DISABLEDmodel, playing through main's OSS/dev/dsp.configureoverrides for the cross build, plus the dynapi patch.GLSL shader playground (
programs/sdl2/)/dev/dri/card0surface: a 2/3-width gap-buffer code editor beside a 1/3-width live GLES2 render pane, auto-recompiling 250 ms after the last keystroke, with an error strip on failure, last-good-shader retention, F5 reload and Ctrl+S persist.vec2 mainSound(float)rendered to an FBO, read back in ≤64 KB row-bands and decoded RGBA8 → S16 as the playback source. A 4-channel chip-synth and a 1024-point radix-2 FFT feed aniAudiospectrum texture back to the image shader./usr/share/shaders, with bundled image and sound presets.host/src/input/browser-input-source.tsgains awheeloption, so{ pointer: false, wheel: true }lets the editor wheel-scroll while the Modeset pane keeps absolute-pointer ownership.Demo and tests
sdl2kandelo preset replaces the evdev and espeak presets from Evdev on Kandelo - /dev/input keyboard and pointer, plus an espeak-ng speech demo #698.sdl2_demoexample plus an end-to-end vitest; KMSDRM / evdev backend smokes; a native editor unit test (programs/sdl2/test/editor_test.c, driven byhost/test/sdl2-editor-unit.test.ts) covering selection, undo/redo, auto-replace and movement including the column-memory case.packages: libdrm and sdl2-demo classified for the local source build.Notes
ABI_VERSIONbump. This layer is user-space C plus host TypeScript;abi/snapshot.jsonregenerates additive-compatible. Toolkit on Kandelo — the C/C++ library stack behind foot, Waybar and mako #1318 up-stack makes the 43 → 44 bump.glReadPixelsreadback — and the chip-synth fallback covers the Node path, so the demo runs and tests green there.move_verticalset the desired column beforemove_to_offset, whose horizontal primitives reset it, so vertical column memory never worked. The assignment now runs after the move.Rebased and rebuilt
This branch sits on
explore-dri-evdev-and-alsaat6c28ee0ec, itself onmainat888e628d5. The history stayed at 6 commits.Against the previous tip, the only files that differ outside those
mainitself moved are 16, and every change in them is a comment or a documentation line: plan labels (B1–B5,Phase C, and the playground'sPhase 3–Phase 7) were replaced by the fact each label stood for. No code changed.The earlier description of this PR listed ALSA as the audio backend. That was accurate for a revision of #698 that no longer exists — the stack converged on
/dev/dsp, and the rebuilt history never adds an ALSA surface.Verification
Run at the pre-rebase stack tip
ab7ab2b63, which contained all 6 commits here:cargo test -p kandelo --target aarch64-apple-darwin --lib→ 1665 passed, 0 failedbash scripts/check-abi-version.sh→ exit 0scripts/run-libc-tests.sh→ 302 passed, 20 XFAIL, 0 build failures, and 1 unexpected failure:regression/raise-race. That test forks 100 children inside a signal handler; 5 waits returnECHILD, reproducibly.scripts/run-libc-tests.sh:163skips it wheneverCI=true, so it does not gate this PR, and whether it regressed or predates the stack is not yet established.scripts/run-posix-tests.sh→ 174 passed, 0 FAIL, 3 XFAIL, 2 SKIPBoth suites need
WASM_POSIX_RESOLUTION_POLICY=source-only-v1andWASM_POSIX_SOURCE_ONLY_BINARY_ROOT=<repo>/local-binaries/source-only-v1.binaries-abi-v44/index.tomldoes not exist until the toolkit tier merges, so thebinaries/provenance tier cannot be materialized locally. The full host vitest suite needs both provenance tiers resolvable at once, which only CI's prepared workspace provides — it is the gate for that suite and for the browser demos.The stack has since been rebased onto
mainat888e628d5. Two gates were re-run at the new stack tipccc7c6214(explore-dri-omarchy):bash scripts/check-abi-version.sh→ exit 0, snapshot in sync and the 43 → 44 bump consistent;scripts/ci-run-test-suite.sh vitest exact-abi-source→ 3049 passed, 0 failed, with two files failing to collect on paths #1321 moved tocrates/runtime-core/. That collection failure is amaindefect and #1337 fixes it.