A floating-window 6502 microcomputer simulator with two interchangeable
CPU cores, a memory-mapped VIC video chip, a real 6522 VIA peripheral
ticking on its own crystal, and a small library of demo programs. Built
on top of foxpro-go
(FoxPro-for-DOS-style TUI framework) and
6502-netsim-go
(transistor-level Visual6502 port). Each component plugs into a shared
bus at hardware-realistic chip-select boundaries; each gets its own
draggable window.
The point is to make a 6502 system you can see: every memory access, every register, every framebuffer cell, every timer underflow, in real time. Long-term goal: a teaching tool where demos written here transfer to real silicon unmodified.
TUI Logic Analyzer — bus + control-line trace in a real terminal, captured at 20 Hz with single-step granularity.
Wasm Logic Analyzer — same widget rendered to a browser canvas via the pixel-overlay path; ~8× the sample density per cell.
Wasm Emulation — the full simulator running in a browser tab: CPU window, memory viewer, Monitor REPL, and the VIC graphics demo all driven by the same backend that runs in the terminal.
Remote CPU, in the terminal — the TUI started with
-cpu=remote. RAM, ROM, VIA, and the framebuffer live here; the
CPU itself is the Visual 6502 transistor sim running in a browser
tab, talking back over a WebSocket. The CPU window flips from
"waiting" to live the instant the browser page connects.
Visual 6502 view — the browser build (the same wasm_emu.gif
machine above) with the Visual 6502 window open. It's a live,
transistor-level rendering of the actual 6502 die — every one of
the chip's ~3,500 transistors, drawn from the original
Visual6502 project's
polygon data — lit up node-by-node as your code executes. Click
the chip to flip on labelled annotations for the pin pads and
internal register blocks.
make tidy
make rungo.mod pins
foxpro-go (TUI framework)
and 6502-netsim-go
(transistor-level CPU) to tagged versions, so a clean clone fetches
everything from the module proxy with no sibling-checkout setup.
If you want to iterate on either dependency locally, add a temporary
replace directive in go.mod pointing at your sibling checkout —
remove it before pushing.
Defaults are tuned for "open it, see something happening": the TUI boots on the interpretive CPU at Max speed with batch auto-tuned to fit the per-tick budget, the marquee demo is loaded, and the clock is running. Esc or Ctrl+Q to quit.
Same code, same demos, same dual-CPU backend, same VIA — running in
the browser via WebAssembly through
foxpro-go/wasm. The
simulator's tcell.Screen is swapped for a tcell.SimulationScreen
(pure-Go cell buffer) and the JS side renders cells to a canvas.
Graphics-mode pixels are layered onto the cell grid via a sentinel-
rune trick — windows, drop shadows, and z-order all work over the
bitmap.
make wasm # build web/sim.wasm + copy wasm_exec.js
make wasm-serve # python3 -m http.server on port 8765 (override with PORT=)Then open http://localhost:8765/.
The wasm build defaults to:
- interp CPU (netsim is slow under wasm; swap via the CPU menu if you want to watch transistors crawl)
- auto-start running so visitors see motion immediately
- BouncingBalls graphics demo as the boot program
- Esc / Ctrl+Q disabled (would terminate the wasm runtime and brick the page); close the tab instead
Bundle size: ~5.3 MB raw, ~1.4 MB gzipped. Standard static-host MIME
config (application/wasm) is enough — no cross-origin headers
required.
Beyond the on-screen widgets, the simulator exposes a debugger
protocol — JSON-RPC 2.0 over NDJSON/TCP for remote drivers, or as
direct Go calls in-process. Both share one Go interface
(bridge.Target), so the same Monitor REPL drives every edge:
| Edge | Command | How it talks to the sim |
|---|---|---|
| Built-in Monitor (terminal) | cmd/6502-sim (Window → Toggle Monitor) |
in-process via bridge.HubDirect |
| Built-in Monitor (browser) | cmd/6502-wasm (Monitor visible by default) |
in-process via bridge.HubDirect |
| Headless bridge server | cmd/6502-sim-serve |
listens on :6502, NDJSON/TCP |
| Shared-bridge TUI | cmd/6502-sim --serve |
TUI runs locally + accepts remote bridge clients |
| Remote controller TUI | cmd/6502-control |
dials a bridge server, healing reconnect |
| Future | MCP server, VS Code extension, etc. | implement bridge.Target |
The Monitor itself lives in internal/monitor. Its command set:
| Group | Verbs |
|---|---|
| CPU & run | r regs · g [addr] run · s [n] step · . stop · reset · stack [r] |
| Memory | m [addr] [n] hex · d [addr] [n] disasm · : <addr> <b>… poke · f <s> <e> <b> fill · t <s> <d> <n> transfer · h <s> <e> <b>… hunt |
| Breakpoints | bp <addr> · bc [id|all] · bl |
| Interrupts | irq · nmi |
| Hardware | hw / info · via <sub> (list/dump/set) |
| Monitor | cls · help [cmd] · help window · reconnect · q |
Addresses are hex ($E000 / 0xE000 / E000) or symbolic:
pc, sp, reset, irq, nmi. The symbolic forms read live —
d pc disassembles wherever you currently are; m irq dumps the
IRQ handler the CPU would jump to right now.
The remote controller auto-reconnects with backoff if the sim
restarts. CPU state preservation across reconnect depends on which
loader: cmd/6502-sim --serve keeps the live Hub across reconnects;
cmd/6502-sim-serve's per-session Hub is fresh on each reconnect.
The protocol contract lives in docs/bridge-v2.md.
| Flag | Default | Notes |
|---|---|---|
-cpu |
interp |
CPU backend: interp or netsim (transistor) |
-run |
true |
Start the clock running immediately |
-speed |
max |
Initial clock target: 1, 10, 20, 100, 1k, max |
-batch |
0 |
Max half-cycles per UI tick (0 = auto-tune at startup) |
-cpuprofile |
(off) | Write CPU pprof to file |
-memprofile |
(off) | Write heap pprof at exit |
The wasm build doesn't take flags; it boots with the same defaults. User-facing controls live in the menus and keyboard shortcuts.
Hardware-realistic address decoding — components claim their ranges exactly the way a 74HC138 chip-select decoder would on a real board. A two-stage decoder (A13–A15 → 8 KB regions; A8–A11 within the I/O region → 256 B sub-regions) gives every peripheral its own CS line with no chip-select collisions.
| Range | Component | Size |
|---|---|---|
$0000–$1FFF |
RAM | 8 KB |
$A000–$A3FF |
VIC color plane | 1 KB CS (520 B used) |
$A400–$A7FF |
VIC char plane | 1 KB CS (520 B used) |
$A800–$ABFF |
VIC controller | 1 KB CS (16 B used) |
$B000–$B0FF |
6522 VIA #1 | 256 B CS (regs mirror ×16) |
$B100–$BFFF |
peripheral slots (15 ×) | 256 B CS each |
$C000–$DFFF |
VIC graphics plane | 8 KB (160 × 100 @ 4bpp) |
$E000–$FFFF |
ROM (reset vector at $FFFC) |
8 KB |
VIC controller registers (offsets within $A800):
| Off | Reg | Behavior |
|---|---|---|
+0 |
Cmd | Write triggers an op (Clear, Shift*, Rot*, Invert, Rect*, Gfx*) |
+1 |
Pause | 1 = UI shows snapshot; 0 = UI shows live memory |
+2 |
Frame | Any write captures a new snapshot (use while paused) |
+3 |
RectX | Rect parameters consumed by CmdRect* and CmdGfx* |
+4 |
RectY | opcodes — clamped to display bounds |
+5 |
RectW | |
+6 |
RectH | |
+7 |
GfxColor | Current draw color for CmdGfx* (palette idx 0–15) |
+8 |
Mode | 0 = char (default), 1 = graphics |
VIA #1 — Phase 1 implements Timer 1 in free-running and one-shot modes plus IFR/IER semantics; ports, T2, SR, and PCR are stubbed and read/write a backing byte without side effects yet. The chip is clocked from its own 1 MHz oscillator (independent of the CPU), so demos that pace off T1 keep ticking even while the CPU is single- stepping or paused — same as a real 65C22S board with a separate timer crystal. Pacing pattern (canonical W65C22):
; Set up T1 free-run with latch = $C350 (~50 ms @ 1 MHz)
LDA #$50 : STA $B006 ; T1L-L
LDA #$C3 : STA $B005 ; T1C-H — copies latch→counter, starts T1
LDA #$40 : STA $B00B ; ACR bit 6 = T1 free-run
; Poll for underflow
WAIT: LDA $B00D ; IFR
AND #$40 ; T1 flag
BEQ WAIT
LDA $B004 ; T1C-L read clears IFR T1| Backend | Speed | What it is |
|---|---|---|
interp |
several MHz | Conventional 151-opcode interpretive 6502 (default) |
netsim |
~26 kHz | Transistor-level Visual6502 port — every cycle simulates ~3500 transistors |
remote |
wire-bound | The CPU lives in another process — a browser tab, an FPGA on the LAN, a Pi across the room — dialed in over a WebSocket. The TUI keeps the bus, RAM, ROM, and VIA local; every cycle round-trips for memory access |
The Backend interface (cpu/backend.go) lets you swap at runtime
via the CPU menu. All three expose the same address/data bus
state plus IRQ/NMI for the simulator's introspection windows.
Start the TUI with -cpu=remote and it boots into "waiting" mode
with no CPU. The terminal binds an HTTP listener (-remote-addr :7777 by default) that serves both the /cpu WebSocket endpoint
and a self-hosted browser page at /:
./bin/6502-sim -cpu=remote
# then open http://localhost:7777/ in your browserThe page boots a foxpro-go shell containing the transistor-level
netsim core wired to a Visual6502-style
live die rendering. As soon as the page connects, the TUI's CPU
window stops saying "waiting" and the demo starts running — every
half-cycle round-trips between the terminal (which owns RAM, ROM,
VIA timers, and the framebuffer) and the browser (which owns the
CPU). The chip lights up node-by-node as instructions execute, with
the TUI showing the same activity on the bus side.
It's not fast — roughly 400 Hz on a localhost loop, slower over a LAN — but that's the point. You can read it. Close the browser tab and the TUI auto-pauses; open it again and it auto-resumes from reset.
Same protocol works for any client that speaks the wire
(cpu/remote/proto.go): there's a Go-only reference at
cmd/6502-cpu-fake/ for smoke testing, and the door is open for an
FPGA-hosted real 6502 driving a TUI over TCP.
Every component gets its own floating, draggable window. Click in the title bar to drag, click the corner to resize.
- CPU — A/X/Y/S/PC, P flags, half-cycle counter, live address bus, data bus, R/W direction, IRQ/NMI line states.
- RAM / ROM (Memory views) — hex view + ASCII column with
editable base address (click the
$XXXX:button, type 4 hex digits). Trace tinting: yellow = write that changed the byte, brown = write that left it unchanged, green = read.vcycles Hex / Disasm / Labels — Labels shows declared symbols within the current region (or a per-byte fallback view for regions without symbols). The disasm column substitutes operand addresses with symbol names where known and appends per-instruction comments. - VIC / Video — 40 × 13 framebuffer with 16-color palette, plus a 160 × 100 graphics plane (when in graphics mode). Right column has buttons for every controller command. Below the framebuffer, a scrollable hex strip shows the VIC's controller region.
- VIA 1 — live snapshot of the chip's state: ports + DDRs at the top, then Timer 1 (counter / latch / mode / armed flag), then ACR decoded + IFR + IER bit dots (● set, . clear). The chip's base address + crystal speed live in the title bar. The counter ticks down even when the CPU is paused or stepping, because the VIA's crystal runs independently.
- Monitor — the shared REPL described above. Toggleable from the Window menu in the terminal build; visible on startup in the browser build. Common pane across all three apps.
- Logic Analyzer (scope) — hidden by default; toggleable. 256 cycles of bus-trace history with auto-tuned sampling stride.
Run/Stop/Step + speed controls live on the menu bar's right-side tray (clickable). The Clock window was removed when the bridge landed — every action is in the menu, keyboard hotkeys, or the Monitor's command line.
Selectable from the Demo menu, in three sections:
| Demo | What it does |
|---|---|
| Marquee | Scrolling "HELLO 6502 SIM"; paces via VIA T1 (default boot demo for TUI) |
| Bouncer | Single * bouncing across row 6 |
| Scroller | Diagonal gradient scrolling up the display |
| Snow (LFSR) | 8-bit Galois LFSR fills + clears the framebuffer |
| Scroller (framed) | Same as Scroller but Pause + Frame for clean snapshots |
| Blitter (RAM→VIC) | Copies byte patterns out of RAM into the VIC planes |
| Quadrants | 4 independent rect rotations using CmdRect* |
| Bouncing Balls | Four colored balls in graphics mode, paced via VIA T1 (wasm only — TUI has no graphics plane) |
All demos are built via the in-tree asm package — a small fluent
6502 assembler that emits bytes plus per-instruction comments and
named memory symbols, surfaced by the Memory window's Labels and
Disasm views.
| Key | Action |
|---|---|
Z |
Reset machine (does NOT stop the clock — like a hardware reset button) |
F2 |
Toggle command window |
R |
Run |
. |
Stop |
S |
Step one instruction (until PC changes) |
T |
Step one half-cycle ("tick") |
Esc |
Quit (terminal) · close menu (browser, where Quit is disabled) |
In the Memory window:
| Key | Action |
|---|---|
g |
Edit base address |
v |
Cycle view: Hex / Disasm / Labels |
i |
Toggle disassembly info panel |
In the VIC window's hex strip:
| Key / mouse | Action |
|---|---|
| Mouse wheel | Scroll memBase by 1 row |
[ / ] |
Scroll by 1 row (16 bytes) |
{ / } |
Scroll by 1 page (112 bytes) |
Click ▲ / ▼ |
±1 row |
| Click track | Page up/down |
Drag ◆ |
Jump to position |
The execution model has two goroutines: the foxpro UI thread (handles input, draws windows) and the Hub Pump (drives the CPU
- peripherals in slices, synchronously). They serialise through a single mutex — concurrent reads from the UI side take a query lock; the Pump holds it during each slice. Same model native + wasm. In wasm, the Pump yields to the JS event loop every ~10 ms in Max mode so the browser tab can render.
The Pump slices each "tick" into 200-half-cycle chunks and pairs
each RunUntil with a bus.Tick(virtualDt) for peripherals — so
polling-based demos (those that LDA/AND/BEQ a peripheral flag in a
tight wait loop) observe timer underflows multiple times per app
frame instead of just once. The driver auto-tunes per-tick batch
size at startup to fit the host's tick budget.
Components self-describe their register layouts via the optional
bus.Labeller interface (Symbols() []asm.Symbol), so the Memory
window's Labels view annotates the VIC and VIA register regions
automatically — no hand-maintained mapping. Time-driven peripherals
implement bus.Ticker and get fanned out automatically.
The bridge protocol is a separate layer above the Hub. Clients
(remote cmd/6502-control, in-process Monitor in cmd/6502-sim /
cmd/6502-wasm, future MCP / VS Code) implement / consume the
bridge.Target interface; the transport (TCP NDJSON or direct Go
calls via bridge.HubDirect) is the only thing that differs.
Read docs/architecture.md for layering + component contracts,
docs/bridge-v2.md for the protocol surface, and docs/roadmap.md
for remaining work.
cmd/6502-sim/ terminal entry — main wiring, flags, profiling
cmd/6502-sim-serve/ headless bridge server (no UI, listens on :6502)
cmd/6502-control/ remote controller TUI — bridge client over NDJSON/TCP
cmd/6502-wasm/ browser entry — wasm-tagged, uses foxpro-go's wasm bridge
asm/ fluent 6502 assembler used by demos
backplane/ machine bus + interrupt aggregation + reset capability
bridge/ protocol layer — Hub + Pump + Target interface + HubDirect
clock/ Driver, Speeds, halfStep accumulator
cpu/ Backend interface + PCSetter capability
cpu/netsim/ netsim adapter
cpu/interp/ interpretive 151-opcode 6502 (with IRQ/NMI service paths)
components/ ram, rom, display, via
disasm/ 151-opcode disassembler with cycle counts and effects
instrument/ Instrument facade — wraps backplane + driver
internal/bridgeclient/ Go wire client for the bridge protocol
internal/monitor/ shared Monitor REPL — drives any bridge.Target
internal/demos/ shared demo programs (text + graphics)
ui/ cpuwin, ramwin, displaywin, viawin, scopewin, clockwin
web/ static frontend served by the wasm build (built artifacts)
docs/ architecture, bridge protocol, roadmap, systems
Working on both terminal and browser. The transistor-level core hits ~26 kHz on a recent Mac; the interpretive core is several MHz. Both pass the same demos.
The simulator is set up so each peripheral lives on its own chip-select region with realistic mirroring (the 6522's 16 registers mirror through a 256-byte CS block, exactly as on a real board with only RS0–RS3 hooked up). Demos written here should run on real silicon without modification.
The transistor-level CPU backend (netsim) and the live die view
(ui/visualcpuwin) are built on data from
Visual6502 by Greg James,
Brian Silverman, and Barry Silverman — the segment-definition polygon
table, node IDs, and layer color palette all come from that project.
The Visual6502 work is licensed under
CC BY-NC-SA 3.0;
see NOTICES for the redistribution details.
MIT — see LICENSE.




