Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
ec102cb
dri+wpk: bring forward DRI/WebGL surface against kandelo:main
mho22 Jun 8, 2026
bdbc3fb
kernel(input): shared ABI — struct input_event + EV_*/KEY_*/EVIOC* co…
mho22 Jun 11, 2026
b6a2205
kernel(input): add /dev/input/event{0,1} + InputFdState on OFD
mho22 Jun 11, 2026
72991c2
kernel(input): EVIOCG* ioctl dispatch + populate_evbit
mho22 Jun 11, 2026
8b2d4fa
kernel(input): kernel_input_event export + fan-out + ring overflow ha…
mho22 Jun 11, 2026
4a0a571
kernel(input): sys_read drains ring + sys_poll POLLIN gating + SYN_DR…
mho22 Jun 11, 2026
0028b6d
kernel(input): release input_state on close + serialise across fork/exec
mho22 Jun 11, 2026
128ba46
abi: regenerate snapshot for additive evdev kernel exports
mho22 Jun 11, 2026
83fbccf
cleanup(input): drop ceremony — ring_high_water, wake stub, close helper
mho22 Jun 11, 2026
d5c381d
host(input): InputSource interface
mho22 Jun 11, 2026
fa1d55f
host(input): BrowserInputSource — DOM capture + KEY_*/BTN_*/REL_*/ABS…
mho22 Jun 11, 2026
52c074d
host(input): NodeInputSource — null-source for headless tests
mho22 Jun 11, 2026
62ec243
host(input): wire kernel_input_event + dual-host boot path
mho22 Jun 11, 2026
4bc6e9c
host(input): vitest — end-to-end key + pointer + ring overflow
mho22 Jun 11, 2026
1ed2069
dri(input): Phase C — sysroot headers + evdev_demo + kandelo preset +…
mho22 Jun 12, 2026
e75277c
cleanup(input): devil's advocate — narration + verbose docs cut
mho22 Jun 12, 2026
910c90a
Add ALSA audio backend and espeak-ng browser demo
mho22 Jun 15, 2026
131a96e
fix(rebase): reconnect the evdev + audio work to main's kernel APIs
mho22 Aug 17, 2026
b30f818
refactor(audio): drop the kernel ALSA surface — /dev/dsp is the PCM c…
mho22 Aug 17, 2026
387096f
feat(input): resolve the length-encoded EVIOC ioctls through a family…
mho22 Aug 19, 2026
5f96d0b
fix(input): route evdev ingress through the kernel entry gate
mho22 Aug 19, 2026
ec498cb
cleanup(dri): drop the three files 432d4c4d1 added and nothing uses
mho22 Aug 19, 2026
6f14d72
feat(input): give evdev_demo.wasm a registry owner
mho22 Aug 19, 2026
f60729b
feat(audio): publish the espeak-ng voice data as a runtime file
mho22 Aug 19, 2026
d60b5fa
fix(browser): bake the espeak and evdev binaries into the image
mho22 Aug 19, 2026
f05894c
chore(packages): regenerate the program package projection
mho22 Aug 19, 2026
f498fc4
fix(packages): classify espeak-ng and evdev-demo for the local source…
mho22 Aug 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Real, unmodified software compiled to WebAssembly:
| Vim | 9.1 | Full editor with ncurses terminal UI |
| NetHack | 3.6.7 | Classic roguelike with curses UI |
| fbDOOM | (maximevince) | id Software's DOOM via the kernel's `/dev/fb0` Linux fbdev surface |
| espeak-ng | 1.52 | Speech synthesis; plays through upstream pcaudiolib's OSS backend on `/dev/dsp` |
| Perl | 5.40 | Interpreter with core modules |
| Ruby | 3.3 | Interpreter with core stdlib |
| SpiderMonkey | 140 ESR | JavaScript engine backing the Node.js-compatible runtime with Intl, SharedArrayBuffer, worker_threads, and npm package installs. |
Expand Down Expand Up @@ -321,6 +322,7 @@ bash packages/registry/nano/build-nano.sh # GNU nano 8.3
bash packages/registry/curl/build-curl.sh # curl
bash packages/registry/netcat/build-netcat.sh # GNU Netcat 0.7.1
bash packages/registry/make/build-make.sh # GNU make
bash packages/registry/espeak-ng/build-espeak-ng.sh # espeak-ng 1.52
```

See [docs/porting-guide.md](docs/porting-guide.md) for how to port your own software.
Expand Down
57 changes: 57 additions & 0 deletions abi/snapshot.json
Original file line number Diff line number Diff line change
Expand Up @@ -1224,6 +1224,12 @@
}
},
"ioctl_request_contracts": {
"1074021776": {
"argKind": "scalar-i32",
"direction": "none",
"wasm32Size": 0,
"wasm64Size": 0
},
"1074024452": {
"argKind": "pointer",
"direction": "in",
Expand Down Expand Up @@ -1326,6 +1332,12 @@
"wasm32Size": 0,
"wasm64Size": 0
},
"2147763457": {
"argKind": "pointer",
"direction": "out",
"wasm32Size": 4,
"wasm64Size": 4
},
"2147766274": {
"argKind": "pointer",
"direction": "out",
Expand Down Expand Up @@ -1380,6 +1392,12 @@
"wasm32Size": 4,
"wasm64Size": 4
},
"2148025602": {
"argKind": "pointer",
"direction": "out",
"wasm32Size": 8,
"wasm64Size": 8
},
"2148028435": {
"argKind": "pointer",
"direction": "out",
Expand Down Expand Up @@ -1759,6 +1777,35 @@
"wasm64Size": null
}
},
"ioctl_request_families": [
{
"dir": 2,
"direction": "out",
"fixedSize": null,
"magic": 69,
"maxCallerSize": 256,
"nrFirst": 6,
"nrLast": 6
},
{
"dir": 2,
"direction": "out",
"fixedSize": null,
"magic": 69,
"maxCallerSize": 256,
"nrFirst": 32,
"nrLast": 63
},
{
"dir": 2,
"direction": "out",
"fixedSize": 24,
"magic": 69,
"maxCallerSize": null,
"nrFirst": 64,
"nrLast": 127
}
],
"kernel_exports": [
{
"kind": "func",
Expand Down Expand Up @@ -2405,6 +2452,11 @@
"name": "kernel_inject_mouse_event",
"signature": "(i32,i32,i32) -> ()"
},
{
"kind": "func",
"name": "kernel_input_event",
"signature": "(i32,i32,i32,i32) -> ()"
},
{
"kind": "func",
"name": "kernel_ioctl",
Expand Down Expand Up @@ -2970,6 +3022,11 @@
"name": "kernel_set_fork_fd_action",
"signature": "(i32,i32,i32) -> (i32)"
},
{
"kind": "func",
"name": "kernel_set_input_canvas_dims",
"signature": "(i32,i32) -> ()"
},
{
"kind": "func",
"name": "kernel_set_max_addr",
Expand Down
166 changes: 165 additions & 1 deletion apps/browser-demos/pages/kandelo/kernel-host/live-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import {
bindImageOwnedRuntimeUrls,
type ImageOwnedRuntimeLazyAssets,
} from "../../../lib/init/image-owned-runtime-urls";
import { BrowserInputSource } from "../../../../../host/src/input/browser-input-source";
import { HttpBridgeHost } from "../../../lib/http-bridge";
import { rewriteShellLazyFileUrls } from "../../../lib/init/shell-lazy-files";
import { resolveShellLazyArchiveUrl } from "../../../lib/init/lazy-archives";
import {
WORDPRESS_CONFIG_INIT_SCRIPT,
Expand All @@ -24,6 +27,10 @@ import {
WORDPRESS_MARIADB_SOCKET_PATH,
} from "../../../lib/init/wordpress-mariadb-readiness";
import { MemoryFileSystem } from "../../../../../host/src/vfs/memory-fs";
import {
extractZipEntry,
parseZipCentralDirectory,
} from "../../../../../host/src/vfs/zip";
import { loadHomebrewBottleMirrorClosedAssets } from "../../../../../host/src/homebrew-bottle-mirror-browser";
import { HOMEBREW_BOTTLE_MIRROR_PLAN_VFS_PATH } from "../../../../../host/src/homebrew-bottle-mirror-plan";
import {
Expand Down Expand Up @@ -195,6 +202,26 @@ const OPTIONAL_BINARY_URLS = {
import: "default",
},
),
...import.meta.glob("../../../../../local-binaries/programs/wasm32/evdev_demo.wasm", {
query: "?url", import: "default",
}),
...import.meta.glob("../../../../../binaries/programs/wasm32/evdev_demo.wasm", {
query: "?url", import: "default",
}),
// espeak-ng publishes a wasm output plus a runtime file, so the resolver
// mirrors its whole closure under the package directory.
...import.meta.glob("../../../../../local-binaries/programs/wasm32/espeak-ng/espeak-ng.wasm", {
query: "?url", import: "default",
}),
...import.meta.glob("../../../../../binaries/programs/wasm32/espeak-ng/espeak-ng.wasm", {
query: "?url", import: "default",
}),
...import.meta.glob("../../../../../local-binaries/programs/wasm32/espeak-ng/espeak-ng-data.zip", {
query: "?url", import: "default",
}),
...import.meta.glob("../../../../../binaries/programs/wasm32/espeak-ng/espeak-ng-data.zip", {
query: "?url", import: "default",
}),
} as Record<string, () => Promise<string>>;

async function optionalBinaryUrl(
Expand All @@ -205,7 +232,11 @@ async function optionalBinaryUrl(
const loader = OPTIONAL_BINARY_URLS[relPath];
if (loader) return loader();
}
throw new Error(`${label} is not built. Run: ./run.sh build programs`);
throw new Error(
`${label} is not built. Run: ./run.sh build programs, ` +
`or for package-owned binaries: ` +
`cargo xtask build-deps resolve <package>`,
);
}

type GalleryPackageRequirement = {
Expand Down Expand Up @@ -393,6 +424,8 @@ const LIVE_DEMO_IDS = [
"wordpress-mariadb",
"doom",
"modeset",
"evdev",
"espeak",
] as const;

type LiveDemoId = (typeof LIVE_DEMO_IDS)[number];
Expand Down Expand Up @@ -482,6 +515,12 @@ const LIVE_DEMO_SPECS: Record<LiveDemoId, LiveDemoSpec> = {
image: "shell",
features: ["kms"],
},
evdev: {
image: "shell",
},
espeak: {
image: "shell",
},
};

const DEFAULT_DEMO_FOR_VFS_IMAGE: Record<LiveVfsImage, LiveDemoId> = {
Expand Down Expand Up @@ -540,6 +579,22 @@ interface LiveProfile {
};
};
framebufferTest: boolean;
/**
* Stage `evdev_demo` into `/usr/local/bin`, attach a `BrowserInputSource`
* to the window so keyboard/pointer events flow into the kernel's
* `/dev/input/event{0,1}`, and run the binary from bash so its event
* log streams to the user's Shell pane. The C1 sysroot vendoring proof.
*/
evdevDemo: boolean;
/**
* Spawn `espeak-ng "..."` from the booted shell. espeak-ng links
* upstream pcaudiolib built with only its OSS backend, so
* `create_audio_device_object` falls through to `/dev/dsp` and a
* single binary invocation produces audible synthesised speech
* without any host-side pipeline. The binary + data dir are baked
* into the image via `stageEspeakRuntime`.
*/
espeakDemo: boolean;
}

interface WebReadinessState {
Expand Down Expand Up @@ -999,6 +1054,8 @@ function customVfsProfile(
shell: "default",
maxVfsByteLength: CUSTOM_VFS_PROFILE_MAX_BYTES,
framebufferTest: fb === "test",
evdevDemo: false,
espeakDemo: false,
};
}

Expand All @@ -1018,6 +1075,8 @@ function profileFor(id: string, fb?: FbDemo): LiveProfile {
fallbackPresentation: software.presentation,
init: software.init,
framebufferTest: false,
evdevDemo: false,
espeakDemo: false,
};
}

Expand Down Expand Up @@ -1065,6 +1124,8 @@ function profileFor(id: string, fb?: FbDemo): LiveProfile {
},
},
framebufferTest: fb === "test",
evdevDemo: normalized === "evdev",
espeakDemo: normalized === "espeak",
};
}

Expand Down Expand Up @@ -1520,6 +1581,18 @@ async function bootProfile(
ensureDirRecursive(buildFs, dirname(profile.init.argv[0]));
writeVfsBinary(buildFs, profile.init.argv[0], new Uint8Array(bytes), 0o755);
}
// Both demos run their binary from a path, so the bytes have to be in the
// image before the worker takes exclusive ownership of the VFS.
if (profile.espeakDemo) {
tick("staging espeak-ng...");
await stageEspeakRuntime(buildFs);
assertCurrent();
}
if (profile.evdevDemo) {
tick("staging evdev_demo...");
await stageEvdevDemo(buildFs);
assertCurrent();
}
ensureDemoHomes(buildFs);
}
// Bake the shell + gallery-software binaries into the image before the
Expand Down Expand Up @@ -1850,6 +1923,49 @@ async function bootProfile(
tick,
assertCurrent,
);
} else if (profile.espeakDemo) {
// The binary and its voice data are already in the image; see
// stageEspeakRuntime. Playback rides the /dev/dsp path every other
// sound demo uses.
void (async () => {
try {
tick("running espeak-ng...");
await host.runShellCommand(
`/usr/bin/espeak-ng "Welcome to Kandelo, the WebAssembly POSIX kernel"`,
);
tick("espeak-ng exited");
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
tick(`espeak-ng failed: ${msg}`);
}
})();
} else if (profile.evdevDemo) {
// autoCommand can't run this: the InputSource must be attached before
// the binary starts polling /dev/input/event{0,1}. The binary itself is
// already in the image; see stageEvdevDemo.
const kernelForEvdev = kernel;
void (async () => {
try {
tick("attaching input source...");
kernelForEvdev.attachInputSource(new BrowserInputSource(window), {
width: window.innerWidth,
height: window.innerHeight,
});
tick("running evdev_demo...");
// evdev_demo runs forever; runShellCommand resolves when the
// bash prompt reappears (it never will) or rejects after its
// internal 5-minute timeout. Both are expected — log neutrally.
await host.runShellCommand("/usr/local/bin/evdev_demo");
tick("evdev_demo exited");
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
if (/timed out waiting for PTY prompt/.test(msg)) {
tick("evdev_demo running (long-tail; no further status updates)");
} else {
tick(`evdev_demo failed: ${msg}`);
}
}
})();
} else if (presentation?.autoCommand) {
tick("starting configured command from the default shell...");
void host.runShellCommand(presentation.autoCommand).catch((err) => {
Expand Down Expand Up @@ -1924,6 +2040,54 @@ function stageShellUtilities(
}
}

/**
* Bake espeak-ng and its voice data into the image.
*
* Both come from the espeak-ng package closure, so the demo consumes the same
* bytes the resolver published. libespeak-ng's PATH_ESPEAK_DATA is fixed to
* /usr/share at build time, so the data tree has to land unpacked there.
*/
async function stageEspeakRuntime(fs: MemoryFileSystem): Promise<void> {
const binaryUrl = await optionalBinaryUrl([
"../../../../../local-binaries/programs/wasm32/espeak-ng/espeak-ng.wasm",
"../../../../../binaries/programs/wasm32/espeak-ng/espeak-ng.wasm",
], "espeak-ng.wasm");
const binary = await fetch(binaryUrl)
.then(failOn("espeak-ng.wasm"))
.then((r) => r.arrayBuffer());
ensureDirRecursive(fs, "/usr/bin");
writeVfsBinary(fs, "/usr/bin/espeak-ng", new Uint8Array(binary), 0o755);

const dataUrl = await optionalBinaryUrl([
"../../../../../local-binaries/programs/wasm32/espeak-ng/espeak-ng-data.zip",
"../../../../../binaries/programs/wasm32/espeak-ng/espeak-ng-data.zip",
], "espeak-ng-data.zip");
const data = await fetch(dataUrl)
.then(failOn("espeak-ng-data.zip"))
.then((r) => r.arrayBuffer());
const zipBytes = new Uint8Array(data);
const root = "/usr/share/espeak-ng-data";
ensureDirRecursive(fs, root);
for (const entry of parseZipCentralDirectory(zipBytes)) {
if (entry.isDirectory) continue;
const target = `${root}/${entry.fileName}`;
ensureDirRecursive(fs, target.slice(0, target.lastIndexOf("/")));
writeVfsBinary(fs, target, extractZipEntry(zipBytes, entry), 0o644);
}
}

async function stageEvdevDemo(fs: MemoryFileSystem): Promise<void> {
const url = await optionalBinaryUrl([
"../../../../../local-binaries/programs/wasm32/evdev_demo.wasm",
"../../../../../binaries/programs/wasm32/evdev_demo.wasm",
], "evdev_demo.wasm");
const bytes = await fetch(url)
.then(failOn("evdev_demo.wasm"))
.then((r) => r.arrayBuffer());
ensureDirRecursive(fs, "/usr/local/bin");
writeVfsBinary(fs, "/usr/local/bin/evdev_demo", new Uint8Array(bytes), 0o755);
}

function ensureDemoHomes(fs: MemoryFileSystem): void {
ensureDirRecursive(fs, "/home");
ensureOwnedDir(fs, DEMO_HOME, 0o755, DEMO_UID, DEMO_GID);
Expand Down
22 changes: 22 additions & 0 deletions apps/browser-demos/pages/kandelo/presets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,4 +128,26 @@ export const PRESET_LIBRARY: Preset[] = [
bootCommand: ["/usr/local/bin/modeset"],
estimatedUrlBytes: 612,
},
{
id: "evdev",
title: "Evdev input log",
summary: "Keystrokes + pointer motion captured from the DOM and replayed through /dev/input/event{0,1}.",
base: SHELL_BASE,
packages: ["bash@local", "coreutils@local"],
accent: "#7e57c2",
glyph: "E",
bootCommand: ["bash", "-l", "-i"],
estimatedUrlBytes: 612,
},
{
id: "espeak",
title: "OSS - Espeak-NG",
summary: "The kernel speaks: espeak-ng synthesises text directly through libpcaudio's OSS backend.",
base: SHELL_BASE,
packages: ["bash@local", "coreutils@local"],
accent: "#f48fb1",
glyph: "T",
bootCommand: ["bash", "-l", "-i"],
estimatedUrlBytes: 612,
},
];
Loading