Skip to content

Add a browser (WebAssembly) build of the emulator#177

Merged
LinuxJedi merged 1 commit into
mainfrom
browser-wasm-frontend
Jul 13, 2026
Merged

Add a browser (WebAssembly) build of the emulator#177
LinuxJedi merged 1 commit into
mainfrom
browser-wasm-frontend

Conversation

@LinuxJedi

Copy link
Copy Markdown
Owner

What this is

Copperline compiled to WebAssembly, running the same cycle-driven core in a browser at real speed. A hosted page goes to copperline.dev under /try (site-repo commit, published separately): it boots the bundled AROS ROM, takes your own Kickstart image and disk files, and runs entirely client-side.

Full write-up of the architecture, local build steps, the embedding API, and the benchmarking workflow: docs/guide/browser.md.

How it is structured

  • Feature split, defaults unchanged. New default features frontend (winit/pixels/cpal/gilrs/rfd/arboard) and wasm-boards (the wasmtime plugin host, which cannot be compiled to wasm32). --no-default-features is the portable headless core; a new CI job keeps it compiling, plus the web crate for wasm32-unknown-unknown.
  • Core portability: timebase.rs (std::time natively, web-time in browsers), an empty envcfg snapshot on wasm (std::env panics there), the pure presentation helpers moved to video/present_common.rs (re-exported into the window module family, so the desktop path is untouched), and byte-based ROM/floppy ingestion for hosts with no filesystem.
  • crates/copperline-web: a standalone cdylib (not a workspace member; the root lockfile never changes when building it) exporting a small WebEmu API. No wgpu -- canvas putImageData + AudioWorklet -- so the wasm is ~1.4 MiB (~0.6 MiB gzipped) and single-threaded (no COOP/COEP headers needed; GitHub Pages can serve it).
  • Publishing: wasm-demo.yml mirrors docs-site.yml -- on v* tags it rebuilds the bundle and pushes it, the page glue (www/try.js, www/audio-worklet.js), and the AROS ROMs to the website repository with the existing SITE_DEPLOY_KEY. The glue lives in this repo so it can never drift from the WebEmu API.
  • copperline-bench (bench-bin feature): headless benchmark that also builds for wasm32-wasip1 and runs under Node's WASI (same V8 as Chrome) via tools/wasi-bench.mjs.

Desktop regression checks

  • cargo fmt --check, cargo clippy --all-targets --all-features --locked -- -D warnings: clean.
  • cargo build --release --locked + full cargo test --release --locked: green.
  • --benchmark-until 5 before/after the refactor: 440.3/s vs 443.1/s, p50 1.73 ms vs 1.68 ms (noise).
  • myst build --html --ci --strict --check-links: clean with the new docs page.
  • The moved presentation helpers are re-exports; window.rs call sites are unchanged. BoardDevice's serde variant set is unchanged in default builds, so save states are unaffected.

Measured wasm performance (Node/V8, Apple Silicon, --render)

Workload Native wasm
AROS boot, default A500 8.3x realtime, mean 1.9 ms/frame 6.4x, mean 2.4 ms, p99 2.7 ms
KICK 1.3 + State of the Art (90 s) 4.0x, mean 4.1 ms 2.7x, mean 5.9 ms, p99 7.9 ms, 0/5590 frames over the 20 ms PAL budget

Render checksums are identical between native and wasm runs.

Verified in Chrome against the hosted page layout: AROS boots, 0.99x realtime pacing with a stable ~120 ms audio queue, ADF insert boots Amiga Test Kit, the keyboard map reports the expected raw codes, mouse position/buttons register in ATK's controller-port test, and a Kickstart 1.3 swap cold-boots correctly.

Not in this PR

The website page shell (try/index.html) lives in the site repository. Joystick mapping, gamepads, save states in the browser, and a Firefox/Safari pass are follow-ups.

Split the crate by cargo features so the deterministic core compiles
without the desktop stack, and add a thin wasm frontend that runs the
same core in a browser at real speed, published to copperline.dev/try.

Feature split (defaults unchanged; native builds are byte-for-byte
behaviourally identical, verified by the full test suite and matching
--benchmark-until numbers):

- `frontend`: the winit/pixels window, launcher/UI, cpal output,
  gilrs gamepads, rfd dialogs, and the console's arboard paste.
- `wasm-boards`: the wasmtime plugin-board host. Cranelift cannot be
  compiled *to* wasm32, so browser builds turn it off; the pure-data
  manifest structs move to `wasm_manifest.rs` so config/zorro still
  compile without it.
- `bench-bin`: a headless benchmark binary (`copperline-bench`) that
  also builds for wasm32-wasip1 and runs under Node's WASI
  (tools/wasi-bench.mjs), measuring wasm performance on the same V8
  engine Chrome uses. With --render it includes the presentation
  pipeline; render checksums match native exactly.

Portability work in the core:

- `timebase.rs` re-exports std::time natively and web-time on
  wasm32-unknown-unknown (std Instant/SystemTime abort there); the
  hot-path probe sampling in bus.rs and the emulator stats go through
  it. envcfg snapshots an empty environment on that target instead of
  panicking in std::env::vars_os().
- The pure presentation helpers (post-process, recentring, TV mask,
  aperture predicates) move from window/present.rs to
  `video/present_common.rs` with re-exports, so frontend-less builds
  (cpu.rs debug screenshots, the web crate) can present frames.
- Byte-based ingestion: `FloppyImage::from_bytes` and
  `FloppyController::insert_disk_image_bytes` mirror the path-based
  loaders for hosts with no filesystem.

The browser frontend (`crates/copperline-web`, standalone cdylib, no
wgpu, ~1.4 MiB wasm) exports a small WebEmu API: rAF-driven pacing with
the audio queue as master clock, canvas presentation via ImageData,
AudioWorklet audio, the desktop key table transliterated to W3C codes,
and Pointer Lock mouse. The page glue (www/try.js, www/audio-worklet.js)
lives here so it cannot drift from the API; the wasm-demo.yml workflow
publishes it with the bundle and the AROS ROMs to the website repository
on release tags, mirroring docs-site.yml. A new CI job guards the
portability invariant (core check without default features, web crate
check for wasm32-unknown-unknown).

Documented in docs/guide/browser.md: architecture, local build steps,
the embedding API, and the wasip1 benchmarking workflow.
@LinuxJedi LinuxJedi merged commit 52c5137 into main Jul 13, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant