Add Direct Rendering Infrastructure (DRI/KMS) and Kandelo Modeset pane#678
Conversation
Initial port of the DRI v2 work (PRs #58/#61–#66 against mho22/wasm-posix-kernel) onto current upstream/main. This commit covers: - **shared ABI**: append `pub mod gl` (cmdbuf opcodes + GLES2 sync-query tags + marshalled ioctl arg structs) and `pub mod dri` (DRM ioctl numbers, fourcc constants, KMS struct definitions) to `crates/shared/src/lib.rs`, plus the matching unit tests. No `ABI_VERSION` bump — additive-only. - **kernel `dri/` module**: bo registry + global master tracking (`crates/kernel/src/dri/{mod,bo,master}.rs`), 17 unit tests pass. - **HostIO trait extensions**: gbm_bo_*, gl_*, kms_*, proc_read_bytes, proc_write_bytes all added with no-op / -ENOSYS default impls so existing host adapters compile without changes. - **libc stubs**: full `libdrm`, `libgbm`, `libegl`, `libglesv2` stubs; `gl_abi.h` shared header. - **musl-overlay headers**: drm, GLES2, EGL, KHR, gbm + sys/ioccom.h. - **example programs**: cube, cube_pyramid, dri-smoke, dri_paint, dumb_roundtrip, kms-pageflip-smoke, libdrm-kms-smoke, modeset. - **build script**: scripts/build-gles-stubs.sh. - **design docs**: webgl-gles2 + dri-v2 plans. - **host TS surface** (`host/src/dri/`, `host/src/webgl/`) and the matching `host/test/{dri,webgl}-*.test.ts` files copied for the next pass to integrate against upstream's evolved `kernel.ts`/`kernel-worker.ts`. Next commits: wire DRI ioctls into syscalls.rs ioctl dispatch + devfs.rs + ofd.rs + fork.rs + wasm_api.rs, then integrate the host TS surface, then build the Kandelo React UI pane. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds two VirtualDevice variants for /dev/dri/{card0,renderD128} with
negative host_handle sentinels (-8/-9). sys_open and sys_openat route
both through the existing CharDevice branch — no single-owner claim
since DRI is multi-process by design.
Minimal first-pass DRI ioctl handler implements only the two ioctls
libdrm's drmOpen() probe issues:
- DRM_IOCTL_VERSION returns 1.0.0 with zero-length name/date/desc
strings (user-supplied pointers echoed back so libdrm's
buffer-length round-trip stays clean).
- DRM_IOCTL_GET_CAP: DUMB_BUFFER → 1, PRIME → IMPORT|EXPORT,
unknown caps → 0 (matches Linux's value=0,errno=0 behavior, not
EINVAL).
Anything else returns ENOSYS so libdrm reports "feature unsupported"
rather than silently succeeding with a bogus result. CREATE_DUMB,
SET_MASTER, PAGE_FLIP, etc. land in the next pass alongside
DriFdState/KmsFdState and Process::dri_handles.
devfs lists /dev/dri as a directory under /dev, and getdents64 on
/dev/dri returns card0 and renderD128 as DT_CHR.
Adds three optional kernel-wasm exports for the host:
kernel_vblank (advances the global vblank sequence used by
WAIT_VBLANK responders), kernel_kms_commit_count(crtc), and
kernel_kms_last_frame_us(crtc). All three are additive — they're not
in HOST_ADAPTER_REQUIRED_KERNEL_EXPORTS so the host adapter manifest
shape is unchanged and no ABI_VERSION bump is needed.
890 kernel unit tests pass (881 prior + 9 new DRI tests covering
match_virtual_device, multi-process open, ioctl VERSION, ioctl
GET_CAP for known and unknown caps, ioctl ENOSYS on unimplemented,
read-returns-0, devfs listing).
Adds the per-fd DRI sidecar that subsequent commits hang real ioctl
behavior off. Three OFD flavors share one Option<Box<DriOfdState>>:
- PrimeBo: created by DRM_IOCTL_PRIME_HANDLE_TO_FD; binds the new
fd to a global BoId via a per-fd cookie that PRIME_FD_TO_HANDLE
validates before bumping the bo's refcount.
- RenderNode: open("/dev/dri/renderD128"). Carries a per-fd
GEM-handle namespace plus a future GLES2 cmdbuf binding
(CmdbufBinding + GlState — kept default-None here; the actual
GLIO_* dispatch lands later).
- Card { dri, kms }: open("/dev/dri/card0"). Same handle namespace
as a render node plus KmsFdState (holds_master flag, per-fd
fb_id -> KmsFb map, pending PAGE_FLIP queue, DRM event ring).
OpenFileDesc gains dri_state: Option<Box<DriOfdState>> — boxed so
non-DRI OFDs pay just one pointer slot. Accessor helpers (dri /
dri_mut / kms / kms_mut / prime_bo / take_prime_bo) thin the
ioctl-path boilerplate and prevent double-release of prime-bo
cookies.
OfdTable picks up iter_mut() so the upcoming DRI cleanup paths
(close-final-release-master, exec-clears-handles) can walk every
live OFD once.
fork.rs picks up dri_state: None on the two OpenFileDesc constructors
in the fork/exec deserialization path. Fork-time DRI state inheritance
(clone handles, drop master on parent-exit, etc.) is a separate
commit alongside Process state.
896 kernel unit tests pass (890 prior + 6 new covering default
sidecar absence, variant-routing through accessors, mutable handle
registration, take_prime_bo idempotence and non-prime safety, and
iter_mut visiting every live slot).
sys_open/sys_openat now install the DRI sidecar on /dev/dri/* opens:
RenderNode for renderD128, Card { dri, kms } for card0. Non-DRI
virtual devices skip the helper, so the call is unconditional from
the existing CharDevice branch.
handle_dri_ioctl gains the four ioctls libgbm and libdrm use to
allocate, mmap, and free CPU-shared bos via the per-fd DriFdState:
- DRM_IOCTL_MODE_CREATE_DUMB allocates a bo in the global registry,
asks the host to back it with a SAB (HostIO::gbm_bo_create),
registers a fresh per-fd handle, and writes (handle, pitch, size)
back to the caller. Three rollback edges are covered: registry
alloc OK + host fail → registry decref + ENOMEM; host OK + handle
overflow → registry decref + host gbm_bo_destroy + EMFILE; host OK
+ dri_state_mut fail → same rollback.
- DRM_IOCTL_MODE_MAP_DUMB encodes the BoId into a stable page-aligned
mmap offset (BoId << 12). The mmap path will decode it back later;
no Linux-style vma_offset_manager.
- DRM_IOCTL_MODE_DESTROY_DUMB / DRM_IOCTL_GEM_CLOSE share a release
path: drop the handle from the fd namespace, decref the bo, free
host backing on the last refcount drop. Second close returns
ENOENT.
Validates v1 limits: bpp must be 32 (ARGB8888-only); width/height
must be non-zero; flags must be 0 (no GBM_BO_USE_*).
sys_ioctl picks up &mut dyn HostIO so the dumb-buffer path can call
into gbm_bo_create / gbm_bo_destroy. The signature change ripples
to kernel_ioctl (wasm_api) and every test caller. Tests that didn't
have a MockHostIO in scope get one inline.
MockHostIO overrides gbm_bo_create → 0 and gbm_bo_destroy → no-op so
unit tests can exercise the happy-path bookkeeping without a real
host adapter. The default HostIO trait still returns -ENOSYS, which
is what production hosts need to override.
PRIME (HANDLE_TO_FD / FD_TO_HANDLE) and the WPK GPU-bo extensions
(CREATE_GPU_BO / BIND_FOREIGN_TEXTURE) land in the next commit
alongside the second-fd-creation plumbing for prime-bo OFDs.
905 kernel unit tests pass (896 prior + 9 new covering open-installs
state for render-node and card variants, CREATE_DUMB happy path +
EINVAL on bpp/dim/flags violations, MAP_DUMB offset encoding +
ENOENT on unknown handle, DESTROY_DUMB + GEM_CLOSE release path +
double-close ENOENT, and per-fd handle namespace isolation across
two opens of the same node).
…ease Adds the two ioctls libdrm uses to share bos across DRI fds (e.g. between a renderD128 client and a card0 KMS scanout fd): - DRM_IOCTL_PRIME_HANDLE_TO_FD looks up the bo by handle in the fd's namespace, materialises a per-bo prime cookie (idempotent — re-export reuses the existing cookie, Linux-shape), bumps the bo's refcount, and allocates a fresh OFD carrying PrimeBo sidecar state. The new fd's OFD uses host_handle = -200 — outside the VirtualDevice sentinel range (-1..=-9) — so it won't be misrouted by the DRI ioctl dispatcher. O_CLOEXEC on the request flags propagates to FD_CLOEXEC on the new fd. - DRM_IOCTL_PRIME_FD_TO_HANDLE looks up the prime-fd's OFD, clones its PrimeBoState, verifies the cookie still matches the bo's current cookie (a stale cookie — bo destroyed + new bo took its id — fails with EACCES, matching Linux), bumps the bo refcount, and registers a fresh handle in the destination fd's namespace. Both paths cover their rollback edges: registry incref on success followed by per-fd handle-alloc failure decrefs the bo back to its pre-ioctl refcount; OFD-create-success followed by fd_table.alloc failure releases both the OFD slot and the bo. sys_close picks up DRI cleanup so closing the last fd that references a bo decrefs it to zero and asks the host to destroy its SAB backing. The mechanic: - Before dec_ref, peek the OFD's ref_count. If ref_count == 1 (this close will free the OFD), `take()` the dri_state so close-on-exec and process exit can't double-release. Otherwise leave the sidecar in place — dup-shared OFDs must keep their DRI state until every fd closes. - After dec_ref returns true, dri_release_ofd_state walks the per-fd handle map (RenderNode / Card variants) or unwraps the PrimeBoState. Each bo is decref'd; bo_destroy fires on the last drop. dri::bo gains pub(crate) `next_id_for_test()` so syscall-layer tests can identify the most recently allocated bo (ioctls return a per-fd handle, not the global BoId). Also marks `reset_registry` as pub(crate) for the same reason. 910 kernel unit tests pass (905 prior + 5 new covering PRIME export's new-fd allocation with PrimeBo state + O_CLOEXEC propagation, PRIME import roundtrip aliasing the same bo across fds, PRIME import rejecting a non-prime fd with EINVAL, close releasing the last bo reference and destroying host backing, and multi-fd refcount tracking through the prime export+close sequence).
Splits the DRI ioctl dispatch by node: renderD128 stays on
handle_dri_ioctl, card0 routes to handle_dri_card_ioctl which
fall through to handle_dri_ioctl for the shared probe + dumb-buffer
+ prime surface.
Adds the full minimum KMS surface modeset clients need:
- SET_MASTER / DROP_MASTER. Single global master enforced by
crate::dri::master; re-set by the same (pid, ofd) is idempotent,
anyone else gets EBUSY. The KmsFdState `holds_master` flag tracks
ownership for the modeset gates below.
- MODE_GETRESOURCES. Reports one virtual {crtc=1, connector=1,
encoder=1} plus 1..16384 dimension bounds. The caller-supplied
count/ptr arrays are populated via HostIO::proc_write_bytes;
a write failure surfaces as EFAULT.
- MODE_GETCRTC / MODE_GETENCODER / MODE_GETCONNECTOR. Return sane
defaults for the one slot we expose; mismatched id → ENOENT. The
connector reports VIRTUAL + CONNECTED + the host's preferred
mode (HostIO::kms_mode_info(1)).
- MODE_ADDFB2. Looks up the bo, validates pixel_format
(ARGB8888/XRGB8888/RGB565), enforces stride == bo stride for
CPU-shared bos (GPU-tier bos skip the check — stride is 0 in
the registry), registers a per-fd fb_id, bumps the bo refcount,
asks the host to bind via HostIO::kms_addfb. Host-fail rollback
releases both the fb slot and the bo.
- MODE_RMFB. Drops the fb slot, decrefs the bo, frees host backing
on the last drop (gbm_bo_destroy) and asks the host to drop the
fb via HostIO::kms_rmfb. Second RMFB on the same id → ENOENT.
- MODE_SETCRTC. Master-gated (EACCES otherwise). crtc_id==1 only;
fb_id must be either 0 (unset) or a previously registered fb_id
on this fd. Delegates to HostIO::kms_set_fb.
- MODE_PAGE_FLIP. Same gates. EBUSY if a flip on the same crtc is
already pending. Queues the flip on `kms.pending_flips` and bumps
the global commit counter via dri::record_kms_commit so the host
UI (kernel_kms_commit_count) sees progress immediately. The
clock read is best-effort — a CLOCK_MONOTONIC failure leaves the
flip queued and just skips the counter bump.
- WAIT_VBLANK. Returns a best-effort reply with the current
monotonic time; sequence=0. The full vblank handshake (queued
waiters drained by the kernel_vblank tick) lands when the host
pump is wired in a later commit.
sys_close DRI release path picks up KMS state:
dri_release_ofd_state now drops every fb (each held an extra
bo refcount and an extra host kms_rmfb call), releases master via
crate::dri::master::release_if_held if the closing OFD held it,
and walks the GEM handle namespace like a render node. The
ofd_idx is now passed in so the master release can match (pid,
ofd_idx) — a release that doesn't match the current holder is a
no-op.
Adds shared helpers `kms_state` / `kms_state_mut` next to the
existing `dri_state` / `dri_state_mut`. The dispatch in sys_ioctl
routes card0 to handle_dri_card_ioctl and renderD128 to
handle_dri_ioctl directly.
Existing `dri_ioctl_unknown_returns_enosys` rewritten to use
0xdead_beef instead of MODE_GETRESOURCES (now implemented), so
the negative-path assertion still holds.
918 kernel unit tests pass (910 prior + 8 new covering SET/DROP
master roundtrip + holds_master flag tracking, second-pid SET
returning EBUSY, close releasing master, GETRESOURCES count
output, GETCRTC id==1 OK and id!=1 ENOENT, the full
ADDFB2+SETCRTC+PAGE_FLIP happy path + commit counter bump +
second-flip EBUSY, SETCRTC without master returning EACCES, and
RMFB decrementing both the per-fd fb slot and the bo refcount
plus double-RMFB ENOENT).
…dState
Picks up the per-fd DRI state that landed in earlier commits and
connects it to the two paths user space actually needs to share a bo
across processes: mmap of a /dev/dri/{renderD128,card0} fd, and fork
of a process that already holds DRI handles, fbs, or prime imports.
mmap → gbm_bo_bind:
- sys_mmap now matches a third per-OFD branch alongside fb0: if the
OFD has DriOfdState::RenderNode or DriOfdState::Card and the caller
provides the offset from DRM_IOCTL_MODE_MAP_DUMB (BoId << 12), the
kernel decodes the BoId, validates the bo is live and CPU-shared
in dri::bo's registry, page-aligns the requested length, allocates
wasm pages, then calls HostIO::gbm_bo_bind(pid, bo_id, addr, len)
so the host can point that region at the bo's SAB slice. On
gbm_bo_bind failure the wasm pages are returned via munmap so the
caller sees ENOMEM with no half-bound state.
- The active mappings are recorded on Process::dri_bindings so
sys_munmap can find them. munmap drops every binding fully covered
by [addr, addr+len) and asks the host to gbm_bo_unbind each one
before the wasm pages return to the anonymous pool, mirroring the
fb_binding teardown for /dev/fb0.
- A GPU-tier bo (from gbm_bo_create_gpu — backed by a WebGLTexture,
not a SAB) is not CPU-mappable and rejects mmap with EINVAL.
- The mmap path uses libgbm-shape geometry: the requested length must
match the bo's size rounded up to a wasm page (64 KiB), matching
what libgbm actually asks the kernel for. Wrong length → EINVAL,
no host call.
Fork (and exec) inheritance of DriOfdState:
- FORK_VERSION bumps 8 → 9. Every per-OFD record now carries a one-
byte variant tag (None / RenderNode / Card / PrimeBo) plus the
payload bytes — handle map and next_handle for renderD128, plus
the kms fb map / next_fb_id / pending_flips for card0, or the
(bo_id, cookie) pair for a prime-fd OFD.
- On deserialize, every BoId restored on a handle, an fb, or a
prime-bo gets an extra with_registry(|r| r.incref(_)) so the new
process owns its own refcount on every bo it inherits. The
parent's eventual close-path decref balances against its original
alloc/ioctl-bumped refcount; the child's against the incref made
here.
- Fork clears KmsFdState::holds_master in the child — the global
master is a singleton and the child must SET_MASTER itself if it
wants the lease. Exec preserves holds_master: the process
identity is unchanged across the image swap, so an inherited
card0 OFD legitimately keeps its KMS lease.
- The mmap-side host bindings on Process::dri_bindings are NOT
inherited (the child gets an empty Vec). The child's wasm memory
is a fresh region the host has not been told about; the child
must re-mmap to re-establish bindings, mirroring fb_binding.
MockHostIO grows two tracking vecs (gbm_bo_bind_calls /
gbm_bo_unbind_calls) and a configurable gbm_bo_bind_rc so the new
mmap tests can assert the host was actually told about the binding
and exercise the rollback edge when gbm_bo_bind returns an errno.
929 kernel unit tests pass (918 prior + 11 new covering: mmap on
renderD128 binding the host + recording the per-process binding,
mmap on card0 doing the same via the Card variant, mmap with wrong
length / unknown bo / host gbm_bo_bind failure all surfacing errnos
with no half-state, munmap unbinding the host and clearing the
binding; fork preserving renderD128 handles and incref'ing every
bo, fork preserving card0 fbs and incref'ing those bos, fork
preserving a PrimeBo state's (bo_id, cookie) and incref'ing the
shared bo, fork clearing holds_master in the child, and exec
preserving holds_master in the post-image OFD).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bridges the kernel-side HostIO trait methods (gbm_bo_*, kms_*, gl_*,
proc_read/write_bytes) added in commits 1-6 to a fresh batch of wasm
imports, and supplies the matching JS-side host functions on the
WasmPosixKernel env import object.
Kernel side (crates/kernel/src/wasm_api.rs):
- New `host_*` extern declarations: host_gbm_bo_{create,destroy,
create_gpu,bind,unbind}, host_gl_{bind,unbind,create_context,
destroy_context,create_surface,destroy_surface,make_current,
submit,present,query,bind_foreign_texture}, host_kms_{set_master,
drop_master,mode_info,addfb,rmfb,set_fb}, host_proc_{read,write}_bytes.
- WasmHostIO impls forward each HostIO trait method to the corresponding
extern. The trait's default no-op / -ENOSYS impls remain in place for
test mocks; only the production wasm path now actually calls into the
host.
- All additive — no existing extern, impl, or signature changed.
Host TS side (host/src/kernel.ts):
- Imports the existing host/src/dri/{registry,kms-registry} and
host/src/webgl/{registry,bridge,query,submit-queue,muxer,
submit-drain} surfaces (copied wholesale in b25ef59 but not yet
wired into kernel.ts).
- WasmPosixKernel instance fields bos / kms / gl / foreignTextures /
gl_submit_queue / gl_muxers track per-pid GBM bos, KMS state, GLES
bindings, foreign-texture handles, and the master-prioritized GL
submit lanes.
- KernelCallbacks gains `getProcessMemory?: (pid) => WebAssembly.Memory`,
threaded into host_gl_submit / host_gl_query / host_proc_* so the GL
bridge can decode cmdbuf bytes out of the calling process's wasm
Memory SAB.
- buildImportObject grows host_gbm_bo_*, host_gl_*, host_kms_*,
host_proc_read_bytes, host_proc_write_bytes — wired straight through
to the registries above.
- New private writeKernelBytes mirror of readKernelBytes for the query
/ mode-info / proc-read return paths.
The embedder (node/browser kernel-host) still needs to supply
`getProcessMemory` and `kmsAttachCanvas`/`kmsAttachStats` plumbing —
that's commit 9. Until then GL bindings stay null-canvas, host_gl_submit
silently no-ops, and the existing FB / channel paths are untouched.
Kernel tests: 929 passing (unchanged from prior tip).
ABI snapshot: not regenerated here; the kernel_vblank /
kernel_kms_commit_count / kernel_kms_last_frame_us export additions
from commit 1 will be rolled into the closing snapshot commit per
the session-3 handoff plan.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the 60 Hz vblank pump and OffscreenCanvas blit path that turn the
kernel-side KMS ioctls landed in commits 1-7 into actual on-screen
pixels.
CentralizedKernelWorker gains:
- `getProcessMemory` callback wired into the kernel's `KernelCallbacks`
so `host_gl_submit` / `host_gl_query` / `host_proc_{read,write}_bytes`
can reach the per-pid wasm `Memory` registered in `this.processes`.
- `attachKmsCanvas(crtc_id, canvas, statsSab?)` to register an
`OffscreenCanvas` (and optional stats SAB) as the scanout target for
a CRTC. Starts the pump on first attach.
- `attachKmsStats(crtc_id, statsSab)` for the GL-rendered case that
wants page-flip telemetry without a 2D blit canvas.
- `startVblankPump()` / `tickVblank()` running on a 16.67 ms interval:
* Calls `kernel_vblank()` to drain pending page-flips.
* For each registered canvas: pulls `kms.currentFb` + `kms.scanoutBytes`,
blits opaque RGBA8888 into a per-CRTC cached `Uint8ClampedArray`,
`putImageData` to the canvas, and writes (frame count, ts, width,
height, blit µs) into stats slots 0..4 atomically.
* For every stats SAB (canvas-bound or not), writes
`kernel_kms_commit_count` and `kernel_kms_last_frame_us` into
slots 5/6 so demos can show real PAGE_FLIP rate without coupling
to the blit cadence.
- `get bos()` / `get gl()` / `get kms()` accessors so demos and presenter
code can reach the registries that now live on `WasmPosixKernel`.
The pump auto-stops nothing once attached — the timer uses `.unref()` so
Node process exit isn't blocked, and the OffscreenCanvas/stats maps
empty out naturally on `terminate()` since the entire worker tears down.
Scratch buffer is explicitly `Uint8ClampedArray<ArrayBuffer>` (not the
default `ArrayBufferLike`) so `new ImageData(scratch, w, h)` accepts it
under TS's stricter typed-array generic.
Tests:
- Cargo: 929 passing (unchanged).
- Host TS: build clean; no new tsc errors. The browser-host vitest /
Playwright suites that exercise OffscreenCanvas paths land in
commit 10/11.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires the KMS presenter plumbing landed in commit 8 through to both
host adapters at parity (CLAUDE.md §"Two hosts" — neither host follows
the other; both go in the same commit). Without this commit a caller
can construct a `CentralizedKernelWorker` inside the kernel worker but
cannot reach `attachKmsCanvas`/`attachKmsStats` from the main thread,
so OffscreenCanvas + stats SAB never make it into the pump.
Protocol (host/src/{browser,node}-kernel-protocol.ts):
- New `KmsAttachCanvasMessage { type: "kms_attach_canvas", crtcId,
canvas: OffscreenCanvas, stats?: SharedArrayBuffer }`.
- New `KmsAttachStatsMessage { type: "kms_attach_stats", crtcId,
stats: SharedArrayBuffer }` for the GL-rendered case that wants
page-flip telemetry but no 2D blit canvas.
- Both unions extended.
Main-thread adapter (browser-kernel-host.ts / node-kernel-host.ts):
- `kmsAttachCanvas(crtcId, canvas, stats?)` — on browser, transfers
the canvas (browser refuses to share it); on Node passes it raw
(Node lacks a native `OffscreenCanvas`; the worker no-ops if no
polyfill is wired, but the wire shape stays parallel).
- `kmsAttachStats(crtcId, stats)` — plain forwarding both ways.
Worker entry (browser-kernel-worker-entry.ts / node-kernel-worker-entry.ts):
- New `case "kms_attach_canvas"` / `case "kms_attach_stats"` in the
top-level main→worker message switch. Forwards to the worker
instance's existing `attachKmsCanvas`/`attachKmsStats` methods,
which start the vblank pump on first attach.
These are singleton kernel-worker messages (the OffscreenCanvas /
stats SAB are owned by the worker, not per-process), so they do NOT
need parallel wiring inside `handleSpawn`/`handleFork`/`handleExec` —
no risk of the PR #410 dual-host-asymmetry regression here.
Symmetry check (CLAUDE.md): `grep kms_attach_canvas host/src/` shows
parallel structure on both trees; the matching consumer demo lives
in commit 10.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the in-Kandelo UI surface for the DRI/KMS stack: a `<Modeset>` pane that hands an OffscreenCanvas to the kernel-worker and shows the stats SAB the vblank pump writes into. Mirrors `Framebuffer` in shape (canvas + per-bound-process status), so whoever runs `modeset` (or any other CRTC-driving program) from the shell drives the pixels — the pane itself never spawns the renderer. `web-libs/kandelo-session/src/kernel-host.ts`: - `KernelLike` gains optional `kmsAttachCanvas` / `kmsAttachStats` methods so a `LiveKernelHost` wrapping a `BrowserKernel` (or the parallel `NodeKernelHost`) can reach the host-host's new commit-9 API surface. - New `KmsDisplayHandle` and `attachKmsDisplay(canvas, crtcId?)` on `KernelHost`. Default `crtcId = 1` matches the single CRTC the kernel currently advertises via `MODE_GETRESOURCES`. - `LiveKernelHost.attachKmsDisplay` lazy-allocates a 7-slot stats SAB (64 bytes, aligned for `Atomics.*`), transfers the canvas via `transferControlToOffscreen`, and forwards to `kmsAttachCanvas`. Returns null when the wrapped kernel lacks the method (older ABI) or the canvas can't be transferred (Node without polyfill). - Stats slot layout documented on `KmsDisplayHandle`: 0-4 from the blit pump, 5-6 from the kernel-side `kernel_kms_commit_count` / `kernel_kms_last_frame_us` exports. `apps/browser-demos/pages/kandelo/panes/Modeset.tsx`: - New pane following Framebuffer's pattern. Attaches the canvas on status === "running" mount, polls the stats SAB at 4 Hz for the status bar (frame count, scanout WxH, blit µs, PAGE_FLIP commits, last flip µs). The CRTC id is a prop (defaults to 1) so a future multi-CRTC layout can mount multiple instances. - Surfaces an error if the kernel ABI doesn't expose the new `attachKmsCanvas`, rather than silently appearing blank. - The legacy `apps/browser-demos/pages/modeset/` standalone page mentioned in the v2 handoff is already absent from the branch tip (nothing to drop here — the v2 doc was working against an older tree state). Layout integration (adding a "modeset" `PrimarySurface` to `MachineView` + `SurfaceAvailability` plumbing in `LiveKernelHost`) is deferred to a follow-up commit so the new pane can be reviewed independently of the demo-presentation logic. Build: host TS clean; apps/browser-demos tsc shows 8 fewer errors than tip (the new interface fills a previously-`any` hole). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add a "kms" PrimarySurface so the Modeset pane is reachable from the Kandelo UI without conflating it with /dev/fb0. LiveKernelHost flips the kms availability bit based on `kmsAttachCanvas` presence; Display routes the demo surface slot to <Modeset/> when the active demo declares kms. The new "modeset" preset boots the shell image, declares the kms runtime feature, and host-side stages /usr/local/bin/modeset from binaries/programs/wasm32/modeset.wasm — mirroring the fbtest staging path so the binary doesn't have to live inside the shell VFS. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The DRI port's build script came forward from mho22 with a uniform
build_program call that no longer linked libdrm.a / libgbm.a into
DRI programs. Without those archives the linker silently emitted
drmSetMaster / drmModeGetResources / gbm_create_device / ... as
`env.*` undefined imports, and any program that touched libdrm
crashed at instantiation with `Unimplemented import: env.drmSetMaster`.
Restore the per-program extra-libs path that lived in the original
DRI build script:
- Split LINK_FLAGS into LINK_PRE_LIBS (syscall glue + crt1) and
LINK_POST_LIBS (libc.a + -Wl,...), so extra archives can be
spliced BEFORE libc.a — required for the wrappers' internal
references (mmap, ioctl, calloc, ...) to resolve in a single
wasm-ld pass.
- build_program now accepts extra archives after the standard
two args, and grep-detects `#include <EGL/...>` / `<GLES{2,3}/...>`
to auto-append libEGL.a + libGLESv2.a (no-op for non-GL programs).
- The per-program case block routes modeset / dri_paint /
dumb_roundtrip to libgbm.a + libdrm.a, and libdrm-kms-smoke to
libdrm.a alone.
Effect on the vitest gauntlet: 36 failing tests → 7 (the latter set
is unrelated to linkage — DRI runtime / fixture mismatches that
predate this branch's bring-forward and need investigation per-test).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Full GLES2 port of github.com/PavelDoGreat/WebGL-Fluid-Simulation: 9 shader passes (curl, vorticity, divergence, pressure decay + 20× Jacobi, gradient subtract, advect velocity/dye), bloom (prefilter + 7-level Gaussian pyramid + final), sunrays (mask + 16-step radial sweep + separable blur), shading + gamma. Pavel "Quality High" config: SIM_RES=128 → 228×128, DYE_RES=1024 → 1820×1024, BLOOM/ SUNRAYS resolutions per getResolution(N). DEN_DISSIPATION bumped to 2.0 to reduce color saturation buildup on long drags. dt is now a per-frame wall-clock delta from CLOCK_MONOTONIC, capped at 1/15 s so a stalled frame can't blow up the sim. Replaces the hardcoded 1/60 — at the actual loop cadence the old constant caused ~30× sim-step-per-real-frame over-dissipation. A program-level 60 Hz throttle sleeps until the next 1/60 s tick after kms_pageflip_wait returns. The kernel-side vblank pump currently delivers PAGE_FLIP_EVENTs at ~2 kHz instead of 60; until that's fixed (Q4), the program needs to throttle itself or burn the GPU running bloom + sunrays + display 33× per real frame. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Canvas style is now width:100%, height:auto, maxHeight:100% — the 1920×1080 drawing buffer keeps its intrinsic aspect ratio so the mouse-coord mapping (rect.width vs canvas.width) stays correct on both axes; the canvas fills the pane width and caps at the pane body height. The bottom stats grid (scanout / blit / pump / commits / last flip / crtc) is gone; the header chip carries the load: "1920×1080 · N flips · Nµs". KmsStats slimmed to just the three fields still rendered. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Earlier branch commits 9575d78 (kernel_vblank + kernel_kms_commit_count + kernel_kms_last_frame_us via the dri probe surface) and the KMS card0 ioctl pass added three new optional kernel exports without regenerating `abi/snapshot.json`. This commit captures them. Per CLAUDE.md ABI policy: additive kernel-wasm exports do not require an `ABI_VERSION` bump as long as existing entries are unchanged. `ABI_VERSION` stays at 14. Note: `scripts/check-abi-version.sh` also flags `kernel_reserve_host_region` + `kernel_reserve_host_region_at` as "removed" and reports `host_adapter` + `process_memory_layout` as reshaped vs `upstream/main`. Those are pre-existing upstream snapshot drift from PR #629 ("Make pthread control slots dynamic"), not changes introduced on this branch — the upstream snapshot was never regenerated when those source-level changes landed. No action taken here; documenting for the next person. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the GLES2 cmdbuf state machine that libEGL / libGLESv2 stubs
drive on /dev/dri/renderD128:
- `GLIO_INIT` — version-checks the op-table; installs GlState.
Mismatch fails ENOSYS at first contact so a
future op-table bump can't silently decode wrong.
- `GLIO_TERMINATE` — tears the binding down + calls `host.gl_unbind`.
- `GLIO_CREATE_CONTEXT` / `GLIO_DESTROY_CONTEXT` — one context per fd
(v1 limit). Duplicate `CREATE_CONTEXT` fails
EBUSY until destroyed.
- `GLIO_CREATE_SURFACE` / `GLIO_DESTROY_SURFACE` — DEFAULT + PBUFFER
only.
- `GLIO_MAKE_CURRENT` — both context and surface must be present.
- `GLIO_SUBMIT` — validates `[offset, offset+length) <= cmdbuf.len`,
bumps `submit_seq`, forwards to `host.gl_submit`.
- `GLIO_PRESENT` — initialised-only gate, forwards to
`host.gl_present`.
- `GLIO_QUERY` — bounds-checks `out_buf_len <= MAX_QUERY_OUT_LEN`,
round-trips an opaque in/out buffer through
`host.gl_query` via the proc_read_bytes /
proc_write_bytes bridge.
`sys_mmap` on a renderD128 fd recognises `offset == 0` (the cmdbuf
slot — bo mmaps always encode `bo_id >= 1` into the offset via the
`bo_id << 12` MAP_DUMB convention) and:
- Requires `len == gl::CMDBUF_LEN`.
- Allocates an anonymous wasm region.
- Records the `CmdbufBinding { addr, len, submit_seq: 0 }` on the
fd's `GlState`.
- Calls `host.gl_bind(pid, addr, len)` so the host muxer can mirror
the encoder region into its own memory view.
The bo-mmap path is loosened to accept either the raw bo size returned
by `DRM_IOCTL_MODE_CREATE_DUMB` or the wasm-page-aligned size some
callers (libgbm's stub on the eager path) round to. `mmap_anonymous`
maps the same number of pages either way; only the binding length we
record differs.
Also fixes the channel SYS_MMAP dispatch path in `wasm_api.rs` to
call `syscalls::sys_mmap` directly instead of going through
`kernel_mmap`. The wrapper collapses every `Errno` to `MAP_FAILED`
(`usize::MAX`); the channel dispatcher then sees `-1` and reports
`-EPERM`. Going direct preserves `-ENOMEM`, `-EINVAL`, `-EBADF`, etc.
This was already wrong for non-DRI callers — the GLES2 cmdbuf path
just made it visible.
Tests cover: GLIO_INIT version skew + matching version + double-init
EBUSY; CREATE_CONTEXT double-attach EBUSY + destroy-then-recreate;
SUBMIT out-of-range EINVAL + valid range bumps submit_seq; cmdbuf
mmap length validation + binding recorded; second mmap-at-offset-0
falls through to the bo path and EINVALs.
934 kernel unit tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`drmModePageFlip` previously pushed onto `KmsFdState::pending_flips` and returned, leaving the host's 60 Hz vblank pump as the sole driver of `DRM_EVENT_FLIP_COMPLETE` delivery. That worked for stats counters but blocked anything that issued the PR-standard `drmModePageFlip(EVENT) → drmHandleEvent` round-trip — vitest fixtures and a freshly-spawned client both stalled until the next pump tick, and a back-to-back second flip on the same crtc failed EBUSY because the pump hadn't drained the first. Solution (v1, marked as such): inside `handle_dri_card_ioctl` after the PAGE_FLIP accepts, push into `pending_flips`, then immediately pop and serialise as a 32-byte DRM event record into the per-fd `event_ring`. `crate::dri::vblank_tick()` supplies the sequence; `host_clock_gettime` supplies tv_sec/tv_usec (a clock-read failure falls back to zeros, no flip is lost). `sys_read` on a DriCard0 fd now drains the event_ring byte-at-a-time into the caller's buffer (libdrm sets a 32-byte buffer for one event; honour shorter buffers by leaving the remainder queued). Empty ring + O_NONBLOCK → EAGAIN; empty + blocking → 0 (drmHandleEvent treats that as "no events this round" rather than a hard error, preserving the read-loop shape). This unblocks programs/dri-modeset.c (the kms-pageflip vitest fixture) and unsticks back-to-back flips on the same crtc — both now succeed instead of hitting EBUSY at the second push. Test updates: the kms-pageflip test asserts `pending_flips` is empty and `event_ring` holds exactly 32 bytes of a well-formed DRM_EVENT_FLIP_COMPLETE record; second flip succeeds and extends event_ring to 64 bytes. EBUSY case is now the in-flight-on-same-crtc case (preserved upstream of the push). This is still Q4 (host-side vblank gating) territory long-term — real refresh-rate pacing belongs in `kernel_vblank()` draining a pending queue, not in PAGE_FLIP. Marked v1 in the inline comment. 934 kernel unit tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires the host-side half of the modeset surface so a libdrm/libgbm/EGL
program (e.g. programs/modeset.c — Pavel's fluid sim) can drive the
Modeset React pane end-to-end. Mirrored across Node + Browser hosts
per CLAUDE.md dual-host parity rule.
KMS canvas ownership mode
-------------------------
`attachKmsCanvas` now takes `opts.mode`:
- `"auto"` (default) — no context is grabbed up front. If the
DRM-master pid later calls `eglCreateContext`, the GL bridge's
auto-attach path (see below) claims the canvas for WebGL2. Slots
5/6 (kernel-side commit count, last frame µs) still tick from
PAGE_FLIP regardless.
- `"2d"` — legacy CPU-blit path. Pump eagerly acquires a 2D context
and copies the kernel's scanout BO into the canvas each frame.
- `"webgl2"` — declared GL-owned up front. Pump stays hands-off.
The 2D-blit branch in `tickVblank` now only fires for CRTCs whose
`kmsContextMode === "2d"`. Touching an OffscreenCanvas with
`getContext("2d")` claims it for life — calling that on a canvas the
embedder later hands to WebGL2 used to break Modeset silently. Now
it doesn't.
Slots 2/3 (current scanout width/height) move out of the 2D blit
branch and into a sourced-from-kernel-FB block that runs for every
stats SAB. The Modeset pane uses (width > 0 && height > 0) as its
"scanout active" predicate; tying that to the blit branch broke the
auto/webgl2 modes.
GL auto-attach
--------------
`host_gl_create_context` now grows a fall-through: when `b.canvas`
is null but the pid holds DRM master on a CRTC the embedder has
registered, fetch the canvas through `callbacks.getKmsCanvas`,
resize its drawing buffer to match the kernel-side FB
(otherwise eglMakeCurrent draws into the default 300×150 corner),
`gl.attachCanvas`, and call `markKmsCanvasGlOwned` so the pump stays
off. Without this, modeset.c — which never calls a TS API — would
silently no-op every shader compile/link/draw against `null`.
`KmsRegistry.masterCrtcForPid` is the lookup the auto-attach path
needs.
bo prime SAB→Memory sync
------------------------
When `sys_mmap` on a /dev/dri/* fd returns, the kernel side has
already called `host.gbm_bo_bind` to record metadata, but the actual
SAB→wasm Memory copy is deferred until the anonymous-mmap zero-fill
is in place. `kernel-worker.ts` finalises that copy here. Without
it, a child that imported a PRIME fd from a forked parent saw zeros
instead of the parent's writes.
`bos.setProcessMemoryResolver` (driven by the new
`KernelCallbacks.getProcessMemory`) is what `primeBindFromSab` calls
to find the right `Memory` object — one per pid.
Kandelo-session
---------------
`attachKmsDisplay` grows `opts.mode` (default `"webgl2"` for the
Modeset pane) and a `sendMouseEvent` member on the returned handle.
React 18 StrictMode double-invokes effects and
`transferControlToOffscreen` can only run once per canvas, so the
handle is memoised on a per-canvas WeakMap.
Tests
-----
host/test/dri-kms-stats-sab.test.ts passes `{ mode: "2d" }` so it
keeps exercising the CPU-blit branch (default "auto" wouldn't blit
and slots 0/1/4 would be 0).
All 32 DRI vitests pass on Node host.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`programs/modeset.c` became Pavel's WebGL fluid sim in commit 52a1022. The vitest at `host/test/dri-modeset.test.ts` still wants a short-lived libdrm/libgbm CLI that runs the PR-standard SetMaster → ADDFB2 → SETCRTC → PageFlip loop and exits with a "modeset OK" summary, so add a separate `programs/dri-modeset.c` trimmed-down fixture (155 lines, behaviour matches the pre-fluid modeset.c described in §C2 of docs/plans/2026-06-08-dri-kms-plan.md). `scripts/build-programs.sh` adds dri-modeset.c to the libdrm/libgbm link group alongside modeset.c, dri_paint.c, dumb_roundtrip.c. `host/test/dri-modeset.test.ts` is repointed at the new `programs/dri-modeset.wasm`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a Playwright spec that boots the Kandelo browser demo with
`?demo=modeset`, waits for the Modeset pane to be visible, and
asserts:
1. The header chip ticks to "N flips" (with N ≥ 1), proving
PAGE_FLIP ioctls actually reach the kernel through the host
bridge.
2. A canvas screenshot is materially larger than a uniform-color
baseline (≥ 5 KiB; blank 800×600 PNGs are < 2 KiB), proving
WebGL2 acquired the scanout canvas and Pavel's fluid sim is
producing pixels. Without this clause a regression that
silently fails to compile the splat shader against a null
`b.gl` would still tick the flip counter.
Required infra changes:
- `playwright.config.ts` switches the chromium project to
`channel: "chromium"` (new headless mode). The default
chromium-headless-shell silently returns null for
`getContext("webgl2")` on a transferred OffscreenCanvas inside
a Web Worker — the entire path Modeset relies on.
- `host/test/dri-smoke.test.ts` bumps the timeout to 20 s. The DRI
smoke run can spend 5–15 s in the WordPress-style sysroot warmup
on CI runners and was tripping the default 10 s ceiling
intermittently.
programs/modeset.c cleanups (devil's-advocate pass):
- `DT_FALLBACK` macro was only used to initialise `g_dt`. Inlined
to `static float g_dt = 1.0f / 60.0f;` and drop the macro.
- `splat_radius_sq` renamed to `splat_radius`. The variable holds
`SPLAT_RADIUS_BASE * aspect` — Pavel's `correctRadius` output,
not a squared distance. The shader uniform is already named
`radius` and `dot(p, p) / radius` does the squaring at sample
time. The `_sq` suffix was a name from an earlier iteration that
never matched the maths. Function parameters renamed to match.
- The two long block comments documenting the user-space 60 Hz
throttle (one before the loop, one inside it) are condensed.
The "why" — kernel-side vblank gating is Q4 and the pump retires
PAGE_FLIP at ~2 kHz, so we throttle ourselves — is preserved in
one short sentence each.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ims + auto-mode test Pre-push devil's-advocate pass on `explore-direct-rendering-infrastructure` before publishing to `Automattic/kandelo`. Every added line on the branch re-walked against its parent; everything that wasn't load-bearing for the modeset.c demo target is removed. GPU-tier infrastructure (forward-ported in b25ef59, never wired in): - Kernel: `BoRegistry::alloc_gpu`, `BoTier` enum + `tier` field on `GbmBo`, `gbm_bo_create_gpu` + `gl_bind_foreign_texture` HostIO trait methods (default impls + WasmHostIO impls), the matching `host_gbm_bo_create_gpu` and `host_gl_bind_foreign_texture` import declarations. The two vacuous `if tier == CpuShared` / `if tier != CpuShared` checks in `syscalls.rs` (MODE_ADDFB2 stride validation, sys_mmap renderD128 path) collapse to the unconditional check. - Host: `WasmPosixKernel.foreignTextures`, the `host_gbm_bo_create_gpu` + `host_gl_bind_foreign_texture` handlers, `GbmBoRegistry.createGpu`, `GbmBoGpuCreateInput`, the `"gpu"`/`"cpu_shared"` tier discriminator, `format` + `texture` fields on `GbmBoEntry`, `ForeignTextureRegistry`. - Tests: GPU-tier `createGpu` tests in `dri-registry.test.ts`, full `webgl-foreign-texture.test.ts` (67 LOC) deleted. No production syscall path constructs a GPU-tier bo today, and no handler ever calls `host.gbm_bo_create_gpu` or `host.gl_bind_foreign_texture`. The infra was speculative scaffolding for a future plan-3 §B6/§B7 milestone; reintroducing it when that milestone actually ships is mechanical. Pulling it out now keeps the branch's "every character has to be needed" invariant honest. Orphan example programs: - `programs/cube.c` (364 LOC, brought forward in b25ef59 as a planned fork+pipe spinning-cube demo, never wired into any test or browser-demo). The only references were in design-plan docs. - `programs/dri_paint.c` (161 LOC, brought forward as a planned PRIME-export visualisation, never wired into any test). The build script case in `scripts/build-programs.sh` drops it from the libgbm/libdrm link group. `programs/cube_pyramid.c` stays — `dri-cube-pyramid.test.ts` exercises it. `programs/dri-smoke.c`, `programs/dumb_roundtrip.c`, `programs/kms-pageflip-smoke.c`, `programs/libdrm-kms-smoke.c`, `programs/modeset.c`, `programs/dri-modeset.c` all have tests and stay. Oversized doc comments in `syscalls.rs`: - `handle_dri_ioctl` had a 27-line `///` block enumerating every ioctl it handles (VERSION, GET_CAP, MODE_CREATE_DUMB, MODE_MAP_DUMB, MODE_DESTROY_DUMB, GEM_CLOSE). Trimmed to a 4-line summary: the enumeration is what the `match request` body literally shows. - `handle_dri_card_ioctl` had a 41-line `///` block enumerating each KMS ioctl with multi-line per-entry semantics. Trimmed to a 3-line summary; fall-through to `handle_dri_ioctl` is the load-bearing architectural fact. - Three obvious one-liners dropped: `// Roll back: drop the OFD and decref the bo.`, `// Bump refcount for the new local handle.`, `// Then the GEM handle namespace, like a render node.` — each restated the code below it. UI / test comment trims: - `apps/browser-demos/pages/kandelo/panes/Modeset.tsx` — the 15-line block-comment header (component summary + stats-slot layout enumeration) trimmed to a 3-line pointer that the layout lives in `tickVblank`. Reference-doc text moved to where the reader actually needs it — at the `kernel-worker.ts` source. - `apps/browser-demos/test/kandelo-modeset.spec.ts` — the 8-line "PNG IDAT chunk explanation" before the screenshot size assertion trimmed to one line. The assertion's intent (`>5KiB threshold` proves a real render, not just a blank canvas) survives; the PNG format walk-through reads like documentation aimed at the wrong audience. Test coverage gap closed: - `host/test/dri-kms-stats-sab.test.ts` now asserts that slots 2/3 (scanout width / height) are populated for a CRTC whose canvas was attached in the default `auto` mode — i.e. without the legacy `mode: "2d"` CPU-blit branch firing. This is the handoff-18 §4 regression risk: when slots 2/3 moved out of the 2D blit branch and into the unconditional stats block, only the `mode: "2d"` path had test coverage for them. Q4 follow-up: - `docs/plans/2026-06-10-dri-q4-vblank-gating-plan.md` documents the v1 simplification still in `5e0c15f1d`: PAGE_FLIP events retire immediately into the per-fd `event_ring` from `handle_dri_card_ioctl`, so `drmHandleEvent` returns at ioctl rate (~2 kHz) instead of monitor refresh. modeset.c masks this with a program-level 60 Hz throttle. Plan describes the architectural fix (drain pending_flips from `kernel_vblank()` instead, gated on a `process_table::with_processes`-style accessor) without blocking the push. Test verification (all 5 suites per CLAUDE.md): - cargo `-p kandelo --target aarch64-apple-darwin --lib`: 932 passed (lost 2 from the removed `alloc_gpu_sets_tier_and_zero_stride` + `alloc_marks_cpu_shared_tier` tests). - DRI vitest (`test/dri-*.test.ts`): 30 passed across 10 files — modeset, cube-pyramid, kms-pageflip, libdrm-kms, registry, kms registry, kms-stats-sab (+1 new), multiplex, smoke, dumb-roundtrip. Full vitest run shows 142 unrelated exnref failures (spidermonkey/php/coreutils/bash/dash/wordpress/ fork-instrument-coverage) which match the v18 carry-forward count. - libc-test (`scripts/run-libc-tests.sh`): 0 unexpected failures on re-run. First run flaked `regression/pthread_cond_wait-cancel_ignored` (timing-sensitive; unrelated to anything in this diff); second run clean. - POSIX (`scripts/run-posix-tests.sh`): 0 FAIL. XFAIL × 3 (mlock/12-1, munmap/1-1, munmap/1-2 — Wasm linear-memory limitations) + SKIP × 2 (sched_get_priority_*). - ABI snapshot (`scripts/check-abi-version.sh`): snapshot is in sync with sources. The breaking-diff vs origin/main is pre-existing upstream drift from PR #629 (pthread control slots dynamic) and PR #630 (wasm32 for Safari) which never regenerated the snapshot; documented in 00d123b and unchanged here. No `ABI_VERSION` bump needed — kernel exports are unchanged by this commit (the GPU-tier imports being removed are kernel `host_*` imports, not exports; ABI snapshot tracks exports only). Dual-host parity grep over `kms_attach_canvas`, `attachKmsCanvas`, `getKmsCanvas`, `markKmsCanvasGlOwned`, `primeBindFromSab`, `getProcessMemory` across `host/src/` and `apps/browser-demos/` remains clean: both `node-kernel-host` and `browser-kernel-host` forward `kms_attach_canvas` / `kms_attach_stats` messages; both worker entries dispatch them; the `attachKmsCanvas` / `attachKmsStats` implementations live in shared `kernel-worker.ts`. The `BrowserKernel.getProcessMemory(pid)` exposure is by-design (browser framebuffer renderer reads pixel SAB through it; Node's framebuffer demos render in-worker and don't need the bridge). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…-xmuz6 # Conflicts: # apps/browser-demos/pages/kandelo/panes/Display.tsx # apps/browser-demos/pages/kandelo/views/MachineView.tsx # crates/kernel/src/syscalls.rs # host/src/kernel-worker.ts
Phase B-1 matrix build status —
|
| Package | Arch | Status | Sha |
|---|---|---|---|
| shell | wasm32 | built | 827bd272 |
| lamp | wasm32 | built | 2ff25636 |
| node-vfs | wasm32 | built | 74852497 |
| wordpress | wasm32 | built | 063a7de3 |
Auto-generated; replaced on each push. Raw data in the publish-status workflow artifact.
Track the DRI ABI surface, shell VFS modeset package, browser demo wiring, and documentation for the direct rendering path. Release DRI/GL image state through the host exec setup path and return GL submit errors through the kernel/host bridge instead of throwing or silently dropping malformed guest command buffers.
|
Updated this PR with the DRI modeset platform plumbing and addressed the review feedback around exec and GL submit error handling. What changed:
Validation run from
I intentionally left out local-only state from the commit: dirty submodule worktrees under |
|
Follow-up on the browser failure and
Validation run locally via I also attempted the full Chromium |
|
prepare-merge: test-gate passed against the synthetic PR merge and |
|
Thank you, @mho22! |
Follow-up on : #678 ## Summary - `programs/modeset.c` was auto-firing a demo splat pair every 90 frames (~1.5 s) whenever no mouse button was held — three rotating positions, courtesy of `(buttons == 0 && (frame % 90) == 0)` plus `p = (frame / 90) % 3`. The repeated re-seed is visually intrusive: the splat exists to prove the sim is alive on the first frame, not to keep redrawing. - Trigger is now `frame == 0` only. The position-cycling `p` and the periodic disjunct are gone. - Splat colors moved from HDR red-orange + cyan (`2.60, 0.55, 0.18` / `0.18, 1.20, 2.80`) to cleaner red + blue (`2.80, 0.05, 0.05` / `0.05, 0.05, 2.80`). The off-channels stay at `0.05` rather than `0` so the bloom/sunrays passes have something non-zero to work with — well below visual perception of a tint. User-space C only — no kernel, host, or ABI surface touched, so dual-host parity isn't in play here. ## Test plan - [ ] `./run.sh browser`, open the Modeset pane, confirm one red+blue splat pair at startup and no further auto re-seeds while idle. - [ ] Drag the canvas with mouse held — splat-on-drag still works (untouched code path). - [ ] CI build of `programs/modeset.c` is green. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Why
The kernel had no graphics surface. User-space programs that want a framebuffer — fluid simulations, GUI demos, future GL-backed tools — had no path to a display. This PR ports a minimal but real DRI/KMS stack:
/dev/dri/card0+renderD128, dumb buffers, PRIME handles, page-flip with vblank events, and a GLES2 session backed by the host's WebGL2 context. A Kandelo browser pane (Modeset) renders the result into anOffscreenCanvason the worker, with a ported WebGL fluid simulation as the first client.Summary
/dev/dri/card0+/dev/dri/renderD128open surface withDriOfdState/DriFdState/KmsFdStatescaffolding, fork/exec inheritance, and close-releaseHANDLE_TO_FD/FD_TO_HANDLE, KMS master grab, modeset, page-flip, vblankDRM_IOCTL_MODE_PAGE_FLIPsynchronously into the card0 read queue sodrmHandleEventreturns at ioctl rate; program-level 60 Hz pacing handles cadence (architectural follow-up sketched indocs/plans/2026-06-10-dri-q4-vblank-gating-plan.md)host_gbm_*,host_kms_*,host_gl_*,host_proc_*and route them through to the host TypeScript via the sharedkernel-worker(Node + Browser parity)attachKmsCanvas/attachKmsStatshost APIs with auto-mode (KMS canvas) and explicitmode: "2d" | "auto", plus GL auto-attach and BO PRIME-sync on both Node and Browser hostsBrowserKernel.getProcessMemory(pid)so the browser framebuffer renderer can read pixel SABprograms/modeset.cwith the ported Pavel WebGL fluid simulation, 60 Hz frame pacing, and adri-modesetCLI fixturelibdrm/libgbminto DRI programs and auto-linkEGL/GLES2where usedModesetReact pane (full-width canvas + slim header chip), wire it intoMachineViewvia a newmodesetpreset, and exposeKandeloHost.attachKmsDisplayabi/snapshot.jsonfor the additive kernel exports (noABI_VERSIONbump — additions only)Follow-up Updates
Added after the original PR description:
packages/registry/modeset/package metadata/build script and wire the modeset package into the shell VFS image path, so the demo is built and consumed through the normal package/sysroot/VFS route instead of as an app-only fixture-EINVALfor malformed guest command buffers,-EIOfor unexpected host/device failure) instead of escaping as JS exceptions or being silently droppedmainand preserve the new lazy-download/session cleanup while keeping the DRI branch's KMS surface availability cleanupshell.vfs.zstrestore base directory/alias state, ship NetHack's writableVAR_PLAYGROUNDscore files, keep sticky/tmp, and avoid using the separately tracked browsergrep -qdefect (Browser shell grep -q returns success for non-matching input #708) as a test oracleVerification
cargo test -p kandelo --target aarch64-apple-darwin --lib— 932 ✓ (0 failures)cd host && npx vitest run test/dri-*.test.ts— 30 ✓ across 10 files (KMS stats SAB, registry, page-flip, prime, modeset spec, browser PAGE_FLIP regression)scripts/run-libc-tests.sh— 0 unexpected failures (one timing-sensitivepthread_cond_wait-cancel_ignoredflake on first run, clean on re-run)scripts/run-posix-tests.sh— 0 FAIL (3 XFAIL:mlock/12-1,munmap/1-1,munmap/1-2— Wasm linear-memory limits; 2 SKIP)bash scripts/check-abi-version.sh— snapshot in sync with sources; additive-compatible./run.sh browser— confirmed goneAdditional validation for the follow-up changes and conflict resolution, run through
scripts/dev-shell.sh:npm --prefix host test -- webgl-bridge.test.ts webgl-submit-drain.test.tscargo test -p kandelo --target "$host_target" --lib glio_submitcargo test -p kandelo --target "$host_target" --lib execve_releases_dri_bo_and_gl_mappingsbash scripts/check-abi-version.shnpm --prefix host run typechecknpm --prefix host test -- dri-multiplex.test.ts webgl-main-forward.test.tsnpm --prefix host test -- exec.test.tsnpm --prefix host test -- lazy-vfs.test.ts ../web-libs/kandelo-session/test/kandelo-session.test.tsnpm --prefix apps/browser-demos run buildnpm --prefix apps/browser-demos test -- --project=chromium sw-bridge-fetch.spec.tsgit diff --check && git diff --cached --checkbash scripts/dev-shell.sh bash packages/registry/shell/build-shell.shbash scripts/dev-shell.sh npm --prefix apps/browser-demos test -- --project=chromium -g "Kandelo shell demo runs bash, vim, and NetHack"bash scripts/dev-shell.sh npm --prefix apps/browser-demos run buildbash scripts/dev-shell.sh git diff --checkNotes
host_gbm_bo_create_gpuandhost_gl_bind_foreign_texturefrom earlier GPU-tier scaffolding were removed in the final cleanup commit. Import removals don't shift kernel exports, so the snapshot stays in sync without anABI_VERSIONbump.process_table::with_processesaccessor sokernel_vblankcan drainpending_flipsfor every open card0 fd; sketched indocs/plans/2026-06-10-dri-q4-vblank-gating-plan.md.kms_attach_canvas/attachKmsCanvas/getKmsCanvas/markKmsCanvasGlOwned/primeBindFromSab/getProcessMemorypaths are mirrored on Node and Browser hosts.BrowserKernel.getProcessMemory(pid)is browser-only by design (the canvas renderer reads pixel SAB through it; Node framebuffer demos render in-worker).