Add Direct Rendering Infrastructure (DRI/KMS) and Kandelo Modeset pane#678
Draft
mho22 wants to merge 23 commits into
Draft
Add Direct Rendering Infrastructure (DRI/KMS) and Kandelo Modeset pane#678mho22 wants to merge 23 commits into
mho22 wants to merge 23 commits into
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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)Verification
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 goneNotes
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).