Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
24dcfce
docs: add web shell demo design spec
cyyeh Apr 27, 2026
343796a
docs: add web shell demo implementation plan
cyyeh Apr 27, 2026
db0d49e
feat(emulator/block): add disk_slice for in-memory disk backing
cyyeh Apr 27, 2026
b554b86
refactor(emulator/block): hoist RAM_BASE; gate NSECTORS for both path…
cyyeh Apr 27, 2026
af06d5a
feat(wasm): add disk_buffer + extend runStart with disk_len
cyyeh Apr 27, 2026
dc55536
build: install kernel-fs.elf + shell-fs.img into web/ for shell demo
cyyeh Apr 27, 2026
dd0b4e8
ci(pages): wildcard-copy zig-out/web into Pages staging
cyyeh Apr 27, 2026
24605ae
feat(web/runner): parallel disk fetch; pass disk_len to runStart
cyyeh Apr 27, 2026
61243a8
feat(web/ansi): scroll on newline at last row
cyyeh Apr 27, 2026
3ddb18f
docs(web/ansi): tighten _lineFeed comment — only \n calls it
cyyeh Apr 27, 2026
df7a901
feat(web/demo): 80×24 terminal + per-program key map + shell support
cyyeh Apr 27, 2026
4d49444
feat(web/index): add shell.elf as default + shell instructions card
cyyeh Apr 27, 2026
e2c9a7c
feat(web/css): grow output panel to fit 80×24 grid + disable wrap
cyyeh Apr 27, 2026
800e09e
docs: web shell demo — README updates
cyyeh Apr 27, 2026
9657e5b
docs(web): sync stale runStart signature + diskBufferPtr in exports list
cyyeh Apr 27, 2026
1ead6bf
fix(wasm): bump RAM_SIZE from 16 MB to 128 MB to match CLI default
cyyeh Apr 27, 2026
e4f997e
fix(wasm): wire UART → PLIC so pushInput drives the kernel's RX IRQ
cyyeh Apr 27, 2026
7b1c244
feat(web): visual backspace + blinking block cursor
cyyeh Apr 27, 2026
4244860
feat(web/demo): "waiting..." placeholder + hidden cursor during boot
cyyeh Apr 27, 2026
a3473ae
fix(web/ansi): treat LF as LF+CR (ONLCR-equivalent)
cyyeh Apr 27, 2026
ed5d1b0
perf+ui(web): 10× chunk size + tighter terminal line-height
cyyeh Apr 27, 2026
8036dae
feat(userland/ls): print entries space-separated on one line
cyyeh Apr 27, 2026
41defd9
docs(web/index): document editor controls + Ctrl-vs-Cmd note
cyyeh Apr 27, 2026
d2dc2f5
feat(userland/edit): visible save status + clear screen on exit
cyyeh Apr 27, 2026
62a2fa3
docs(references): add shell-execution walkthrough; link from demo
cyyeh Apr 27, 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
9 changes: 6 additions & 3 deletions .github/workflows/pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,12 @@ jobs:
mkdir -p _site/web
cp index.html deck-stage.js .nojekyll _site/
cp -r web/. _site/web/
cp zig-out/web/ccc.wasm _site/web/ccc.wasm
cp zig-out/web/hello.elf _site/web/hello.elf
cp zig-out/web/snake.elf _site/web/snake.elf
# Copy every artifact zig build wasm installs into zig-out/web/
# (ccc.wasm + hello.elf + snake.elf + kernel-fs.elf + shell-fs.img,
# plus anything future tasks add). Wildcard avoids the per-file
# allowlist drift that bit us when shell-fs.img + kernel-fs.elf
# were added — see plan 2026-04-27-web-shell-demo Task 3.
cp -r zig-out/web/. _site/web/
ls -lh _site _site/web

- name: Configure Pages
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ zig-out/
web/ccc.wasm
web/hello.elf
web/snake.elf
web/kernel-fs.elf
web/shell-fs.img
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ graphics.

**Live demo:** [https://cyyeh.github.io/ccc/web/](https://cyyeh.github.io/ccc/web/)
— `ccc` cross-compiled to `wasm32-freestanding`, running RV32 binaries in
your browser. Pick `snake.elf` (default — WASD to play) or `hello.elf` (auto-runs + shows the instruction trace). Same Zig core as the CLI; the browser hosts
the emulator in a Web Worker that drives execution in chunks.
your browser. Pick `shell.elf` (default — full Phase 3 shell with
`ls`/`cat`/`echo`/`edit`/`^C`/`exit` against an in-wasm `shell-fs.img`),
`snake.elf` (WASD to play), or `hello.elf` (auto-runs + shows the
instruction trace). Same Zig core as the CLI; the browser hosts the
emulator in a Web Worker that drives execution in chunks.

## Goal

Expand Down
19 changes: 14 additions & 5 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1239,13 +1239,22 @@ pub fn build(b: *std.Build) void {
const wasm_step = b.step("wasm", "Cross-compile ccc to wasm32-freestanding");
wasm_step.dependOn(&install_wasm.step);

// Install hello.elf and snake.elf alongside the wasm so the demo
// can fetch them at runtime. Keeps the wasm tiny (~50 KB instead of
// ~1.5 MB) and lets new programs be dropped in without recompiling.
const install_web_hello = b.addInstallFile(hello_elf.getEmittedBin(), "web/hello.elf");
const install_web_snake = b.addInstallFile(snake_elf.getEmittedBin(), "web/snake.elf");
// Install hello.elf, snake.elf, kernel-fs.elf, and shell-fs.img
// alongside the wasm so the demo can fetch them at runtime. Keeps
// the wasm tiny (~50 KB instead of bundling the binaries) and lets
// programs be added by dropping a file next to index.html.
//
// shell-fs.img is the 4 MB FS image baked by the shell-fs-img build
// step; the wasm demo loads it into its disk_buffer when the visitor
// selects shell.elf.
const install_web_hello = b.addInstallFile(hello_elf.getEmittedBin(), "web/hello.elf");
const install_web_snake = b.addInstallFile(snake_elf.getEmittedBin(), "web/snake.elf");
const install_web_kernel_fs = b.addInstallFile(kernel_fs_elf.getEmittedBin(), "web/kernel-fs.elf");
const install_web_shell_fs_img = b.addInstallFile(shell_fs_img, "web/shell-fs.img");
wasm_step.dependOn(&install_web_hello.step);
wasm_step.dependOn(&install_web_snake.step);
wasm_step.dependOn(&install_web_kernel_fs.step);
wasm_step.dependOn(&install_web_shell_fs_img.step);
}

/// Build a user binary by linking start.S + usys.S + ulib.zig + uprintf.zig +
Expand Down
46 changes: 39 additions & 7 deletions demo/web_main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@
//! keeping ccc.wasm at ~50 KB (just the emulator core).
//!
//! Exports:
//! elfBufferPtr() [*]u8 — base of the 2 MB ELF receive buffer
//! elfBufferCap() u32 — capacity of the ELF buffer (2 MB)
//! runStart(elf_len, trace) i32 — initialise state, 0 on success
//! elfBufferPtr() [*]u8 — base of the 2 MB ELF receive buffer
//! elfBufferCap() u32 — capacity of the ELF buffer (2 MB)
//! diskBufferPtr() [*]u8 — base of the 4 MB disk receive buffer
//! diskBufferCap() u32 — capacity of the disk buffer (4 MB)
//! runStart(elf_len, trace, disk_len) i32 — initialise state, 0 on success
//! runStep(maxInstructions) i32 — -1 still running, ≥0 exit code
//! consumeOutput() u32 — bytes available since last drain
//! outputPtr() [*]u8 — base of output buffer (drain offset)
Expand Down Expand Up @@ -46,6 +48,22 @@ export fn elfBufferCap() u32 {
return ELF_BUFFER_CAP;
}

// 4 MB disk receive buffer. JS fetches the program's disk image
// (currently only shell-fs.img for the shell demo), copies its bytes
// here via diskBufferPtr/diskBufferCap, then calls runStart with a
// non-zero disk_len. shell-fs.img is exactly 4 MB by mkfs convention.
// Snake/hello pass disk_len=0 and the buffer is unused.
const DISK_BUFFER_CAP: u32 = 4 * 1024 * 1024;
var disk_buffer: [DISK_BUFFER_CAP]u8 = undefined;

export fn diskBufferPtr() [*]u8 {
return &disk_buffer;
}

export fn diskBufferCap() u32 {
return DISK_BUFFER_CAP;
}

// 16 KB is comfortable headroom for a "hello world" run.
const OUTPUT_BUF_SIZE: usize = 16 * 1024;
var output_buf: [OUTPUT_BUF_SIZE]u8 = undefined;
Expand All @@ -68,8 +86,12 @@ fn jsClock() i128 {
return mtime_ns;
}

// 16 MiB of guest RAM is plenty for hello.elf.
const RAM_SIZE: usize = 16 * 1024 * 1024;
// 128 MiB of guest RAM matches the CLI default (`--memory 128` in
// src/emulator/main.zig). The kernel's trampoline page lives at
// RAM_BASE + 128 MB - 4 KB (= 0x87FFF000), so anything smaller than
// 128 MB triggers an access fault during kmain's page-table setup,
// even though hello.elf alone would happily fit in 16 MB.
const RAM_SIZE: usize = 128 * 1024 * 1024;

// Module-level emulator state that survives across runStep calls.
// The arena, devices, memory, and cpu all live here so no heap pointer
Expand Down Expand Up @@ -134,9 +156,10 @@ export fn pushInput(byte: u32) void {
/// elf_buffer[0..elf_len] by JS (via elfBufferPtr/elfBufferCap + fetch).
/// trace: non-zero enables per-instruction trace output.
/// Returns 0 on success, negative on error:
/// -1 mem init failed, -2 ELF parse/load failed, -5 bad elf_len.
export fn runStart(elf_len: u32, trace: i32) i32 {
/// -1 mem init failed, -2 ELF parse/load failed, -5 bad elf_len, -6 bad disk_len.
export fn runStart(elf_len: u32, trace: i32, disk_len: u32) i32 {
if (elf_len == 0 or elf_len > ELF_BUFFER_CAP) return -5;
if (disk_len > DISK_BUFFER_CAP) return -6;

// Tear down any in-progress run before reinitialising.
if (state != null) {
Expand All @@ -158,6 +181,15 @@ export fn runStart(elf_len: u32, trace: i32) i32 {
state_storage.plic = plic_dev.Plic.init();
state_storage.block = block_dev.Block.init();

// Wire UART → PLIC so pushRx() raises src 10 (UART RX IRQ) when the
// FIFO transitions from empty → non-empty. Mirrors src/emulator/main.zig.
// Without this, browser keystrokes land in the FIFO but the kernel never
// takes the interrupt and never echoes/processes them.
state_storage.uart.plic = &state_storage.plic;
if (disk_len > 0) {
state_storage.block.disk_slice = disk_buffer[0..disk_len];
}

const io: std.Io = std.Io.failing;

state_storage.mem = mem_mod.Memory.init(
Expand Down
Loading
Loading