This app runs RHDL IR simulator backends in the browser via WebAssembly and renders live VCD waveforms with p5.js.
Architecture details are included at the bottom of this page.
- Load RHDL-generated IR JSON
- Select backend (
interpreter,jit,compiler) - Preconfigured runner presets (Generic, CPU, MOS 6502, Apple II, Game Boy)
- One-click Apple II runner setup (IR + ROM load)
- Step, run, pause, reset simulation
- Clocked stepping (forced clock edge) or unclocked ticking
- Tunable run pacing:
Cycles/FrameandUI Every(cycles) to batch simulation and reduce UI/VCD overhead - Live VCD streaming from Rust tracer to the browser
- Watch list + current value table
- Value breakpoints (
signal == value) - Export full VCD file for GTKWave
- Redux-backed UX state store for tooling/automation
- LitElement UI components with co-located styles
- Panel components split into modules under
web/app/components/and bundled by Bun - Built-in terminal command dispatcher for runner/backend switching, stepping, watches, breakpoints, and memory helpers
- Tabbed workspace:
1. I/O: Apple II display +HIRES,COLOR,SOUNDtoggles, keyboard input queue, debug registers2. VCD + Signals: waveform canvas, watch table, event log3. Memory: RAM browser + direct byte writes + memory dump loading4. Components: source-backed component details (RHDL+Verilog) when source bundles are present
- Memory dump utilities:
Save Dumpexports current Apple II RAM to.binand stores it as "last saved"Download Snapshotexports current Apple II RAM as a portable.rhdlsnapfile (includesstartPcwhen available)Load Dumpaccepts both raw binary dumps and.rhdlsnapsnapshot filesLoad Last Savedrestores the most recently saved dump from browser storageReset (Vector)resets the Apple II runner from the Memory tab, with optional manual reset-vector override ($B82A/0xB82A)
The web simulator uses a two-stage build:
- Ruby-side artifact generation (WASM + IR/fixture metadata)
- Bun bundle (JS + runtime assets) into
web/dist/
bundle exec rake web:buildGenerates core web runtime artifacts under web/assets/pkg/*.wasm.
If artifacts are missing, run:
bundle exec rake web:generateir_compiler is built as AOT for web:
web:buildrunsir_compiler'saot_codegenoverassets/fixtures/apple2/ir/apple2.jsonby default.- Then it builds
ir_compiler.wasmwith--features aot.
Bundle simulator output into web/dist with Bun:
cd web
bun run buildor via rake:
bundle exec rake web:bundleFor production bundle:
cd web
bun run build:prodor:
bundle exec rake web:bundle:prodweb:bundle copies required third-party runtime files (vim-wasm, ghostty-web) into web/dist/assets/pkg so editor and terminal WASM integrations are packaged together.
Serve the bundled web/dist directory:
bundle exec rake web:startweb:start serves with cross-origin isolation headers required for SharedArrayBuffer:
Cross-Origin-Opener-Policy: same-originCross-Origin-Embedder-Policy: require-corpCross-Origin-Resource-Policy: same-origin
If you use a custom server, you must set equivalent headers.
Without those headers, the mirb worker will not be able to use SharedArrayBuffer.
Manual example (must be configured to emit COOP/COEP):
cd web/dist
python3 -m http.server 8080Open http://localhost:8080.
Desktop builds package the same web/dist/ output into web/desktop/src/simulator via
web/desktop/scripts/prebuild.ts.
# From repository root
bundle exec rake desktop:install # Install Electrobun dependencies
bundle exec rake web:bundle # Ensure web/dist is fresh (or run `bun run build` in web/)
bundle exec rake desktop:dev # Build and launch desktop app in dev mode
bundle exec rake desktop:build # Build dev package output
bundle exec rake desktop:release # Build stable package output
bundle exec rake desktop:clean # Remove packaged desktop artifactsdesktop:dev/desktop:build/desktop:release rely on the prebuild hook to sync the web/dist bundle.
Runner presets are generated into:
web/app/components/runner/config/generated_presets.ts
Current generated runner order:
generic(manual IR runner, frompresets.ts)cpumos6502apple2gameboyriscvriscv_linux
Current defaults:
- Default runner preset:
apple2 - Default backend state:
compiler(Compiler (AOT)) - Trace capture starts disabled on load for all presets (
traceEnabledOnLoad: false) - To auto-enable trace at runner load, set
runner.traceEnabledOnLoad: true(or legacyrunner.defaults.traceEnabled: true) in the runner config JSON beforebundle exec rake web:generate
Preset generation source list is controlled by:
lib/rhdl/cli/tasks/web_generate_task.rb(RUNNER_CONFIG_PATHS)
Current generated list includes:
examples/8bit/config.jsonexamples/mos6502/config.jsonexamples/apple2/config.jsonexamples/gameboy/config.jsonexamples/riscv/config.jsonexamples/riscv/config_linux.json
RISC-V web preset status:
- Included by default in
RUNNER_CONFIG_PATHSand exported as:riscv(xv6)riscv_linux(Linux)
riscvmaps to UART I/O (mode: uart) with:./assets/fixtures/riscv/software/bin/xv6_kernel.bin./assets/fixtures/riscv/software/bin/xv6_fs.img./assets/pkg/ir_compiler_riscv.wasm- custom xv6 kernel/disk binaries can be swapped in through runner controls, but the defaults above are the shipped preloads
riscv_linuxmaps to UART I/O (mode: uart) and loads Linux assets when present:./assets/fixtures/riscv/software/bin/linux_kernel.bin./assets/fixtures/riscv/software/bin/linux_initramfs.cpio./assets/fixtures/riscv/software/bin/linux_virt.dtb./assets/fixtures/riscv/software/bin/linux_bootstrap.bin
- Note: regenerate assets from
examples/riscv/config.json/examples/riscv/config_linux.jsonwhenever preset defaults change so generated presets stay aligned. - If RISC-V preset assets are not generated, preset selection still appears but default boot will skip/fail the required loads.
- A workflow is included at
.github/workflows/pages.yml. - It builds all web artifacts via:
bundle exec rake web:generatebundle exec rake web:bundle
- It publishes a static artifact containing:
web/dist/index.htmlweb/dist/coi-serviceworker.jsweb/dist/(bundled JS, WASM, and static assets)
- GitHub Pages does not let this repo configure COOP/COEP response headers directly.
index.htmlregisterscoi-serviceworker.jsto inject COOP/COEP/CORP on same-origin responses as a fallback.- Enable Pages in repository settings:
Settings -> Pages -> Source: GitHub Actions
- Deploy URL will be exposed in the workflow run after the
deployjob completes.
Run all web unit tests (no build step required):
bun test $(find web/test -type f -name '*.test.ts' | sort)Run only browser integration smoke tests (Playwright):
cd web
bun install
bunx playwright install chromium
bun run test:integrationUseful integration suites:
web/test/integration/app_load.test.tsweb/test/integration/app_flows.test.tsweb/test/integration/mos6502_compiler_backend.test.tsweb/test/integration/cpu8bit_default_bin_autoload.test.tsweb/test/integration/cpu8bit_programs.test.tsweb/test/integration/memory_dump_asset_tree.test.tsweb/test/integration/memory_follow_pc_highlight.test.ts
Run only state store tests:
bun test web/test/state/store.test.ts- The selected dropdown sample is loaded automatically on startup (default:
assets/fixtures/apple2/ir/apple2.json). - Backend selection is in the left control panel.
compiler(AOT wasm) is the default backend state at startup.compilerin the web UI isir_compilerAOT (precompiled wasm), not runtimerustccompilation in-browser.- Terminal command help is available in-app via
helpand includes runner/backend/theme/sim/watch/breakpoint/memory actions. - Apple II / CPU runner assets are included under
assets/fixtures/:assets/fixtures/apple2/ir/apple2.json(fromexamples/apple2/hdl/apple2)assets/fixtures/apple2/ir/apple2_sources.json(RHDL+Verilogsources for Apple II components)assets/fixtures/apple2/ir/apple2_schematic.json(precomputed schematic connectivity for Apple II)assets/fixtures/cpu/ir/cpu_sources.json(RHDL+Verilogsources for CPU components)assets/fixtures/cpu/ir/cpu_schematic.json(precomputed schematic connectivity for CPU)
assets/fixtures/apple2/memory/appleiigo.rom(12KB system ROM)assets/fixtures/apple2/memory/karateka_mem.bin+assets/fixtures/apple2/memory/karateka_mem_meta.txtfor quick dump load- Build/update wasm backends only:
bundle exec rake web:buildbundle exec rake web:bundle
- Regenerate web artifacts (IR + source + schematic):
bundle exec rake web:generate
- Memory tab supports:
- arbitrary dump file load at offset (via Apple II RAM interface)
- one-click Karateka dump load (patches reset vector and resets to dump PC)
- Clock options are labeled by execution mode:
forced: direct process clock stepping (tick_forced)driven: toggle external clock (for example top-levelclk) and propagate through combinational logic
- The UI consumes incremental VCD chunks from
trace_take_live_vcd()each frame. - Redux state bridge (if
redux.min.jsloads):- store:
window.__RHDL_REDUX_STORE__ - current in-memory app state:
window.__RHDL_UX_STATE__ - manual sync helper:
window.__RHDL_REDUX_SYNC__('manual')
- store:
This section describes the current architecture of the RHDL web simulator and where responsibilities live.
web/app/core/bootstrap.tsis the composition root.web/app/main.tsloads UI components and starts bootstrap.- Bootstrap creates:
- DOM refs (
bindings/dom_bindings.ts) - mutable runtime context (
runtime/context.ts) - mutable UX state (
state/initial_state.ts) - Redux store + bridge (
state/store.ts,state/store_bridge.ts) - controller registry (
controllers/registry_controller.ts) - startup orchestration (
controllers/startup_controller.ts)
- DOM refs (
state/: reducers, actions, store plumbing, redux sync helpers.runtime/: backend definitions, wasm simulator wrapper, VCD parser.controllers/: app behavior and orchestration. High-level intent lives here.controllers/terminal/: command routing and command handlers.controllers/registry_lazy/: lazy construction for heavy controller/managers.managers/: reusable behavior units (watch manager, dashboard layout manager).bindings/: DOM event wiring that maps UI events to controller-domain operations.components/: LitElement panels and rendering helpers.lib/: shared pure utilities (numeric parsing, IR metadata, dashboard/state helpers).
createControllerRegistry returns grouped domains consumed by startup/bindings:
shellrunnercomponentsapple2simwatch
Each domain exposes cohesive capabilities instead of one flat API bag. Lazy getters in
controllers/registry_lazy/* defer heavy object construction until needed.
startApp receives explicit grouped dependencies:
env: host environment hooksstore: state dispatchers/sync helpersutil: pure utility functionskeys: storage keys/constantsbindings: binding constructors + UI binding registryapp: registry domains
This keeps startup deterministic and testable without browser globals.
index.htmlcontains shell markup and panel containers.- Lit components (
components/*.ts) own panel-specific rendering and styles. - Bindings attach listeners and call domain methods.
- Redux sync snapshots app state for toolability and test instrumentation.
The component schematic tab (5. Schematic) renders interactive RTL schematics
with hierarchical drill-down into sub-components.
Layout is handled by ELK.js (Eclipse Layout Kernel), a port-aware hierarchical layout engine loaded from CDN. An adapter converts the internal render list into an ELK graph, runs the layout, and maps computed positions back onto the schematic primitives (symbols, pins, nets, wires).
Rendering uses a WebGL 2.0 instanced pipeline as the primary backend, with a
Canvas 2D fallback for environments where WebGL is unavailable (e.g. headless
browsers). Both renderers consume the same flat RenderList of typed primitives
and resolve colors from a shared theme palette. The WebGL path uses SDF
rounded-rect fragment shaders for crisp edges at any zoom level.
Interaction is built on a spatial R-tree index over rendered elements.
Single-click selects a component or highlights a signal; double-click drills down
into a child component's internal schematic. Left-button drag pans the viewport,
mouse wheel/trackpad scroll zooms around the cursor, and Zoom + / Zoom - /
Reset View buttons provide explicit viewport controls.
Live activity updates wire and net colors each frame based on current signal values from the running simulation — non-zero signals highlight in green, toggled signals flash amber. This animation is gated by trace state: when trace is off, schematic activity is static; when trace is on, live activity animates.
Each element type has a distinct color: cyan for components, purple for IO ports, amber for ops/assigns, copper for memory, green for nets, and neutral gray for pins. A color legend is drawn in screen space in the bottom-right corner.
- Unit tests:
- controller units (
test/controllers) - binding units (
test/bindings) - manager/runtime/lib/state modules
- controller units (
- Browser integration tests (
test/integration):- app load smoke
- core user flows (runner load, memory dump actions, run/pause, terminal commands)
assets/pkg: wasm artifacts.assets/fixtures: generated IR/source/schematic fixtures and sample binary assets.bundle exec rake web:build: wasm build pipeline entrypoint.bundle exec rake web:bundle: Bun bundle entrypoint forweb/dist/.bundle exec rake web:generate: web asset generation entrypoint (builds wasm first when missing).