6502.ts
6502.ts is a full-featured Atari 2600 emulator written in TypeScript for both Node.js and the browser, including sound and CRT phosphor simulation. Its 'Stellerator' web front-end lets users import, manage and play VCS ROMs directly in the browser.
Runs on: Web browser
6502.ts Online Emulator
Play 6502.ts using JavaScript directly in your browser.
Controls
Configurations
| Configuration | Emulator | Machine | OS | Legal | |
|---|---|---|---|---|---|
| Flapping | 6502.ts | Atari 2600 | open | Open ⛶ | |
| Flapping | 6502.ts | Atari 2600 Jr | open | Open ⛶ | |
| Starfield demo | 6502.ts | Atari 2600 | open | Open ⛶ |
Machines emulated
Chips
Notes
Embedding
6502.ts ships a browser front-end (Stellerator) whose convenience wrapper runs the machine inside a Web Worker, great for play, but the CPU is then only reachable as periodic snapshots. For a live debugger we vendor a tiny bundle of the machine core and drive the Board on the main thread ourselves.
Boot a cartridge. Build a Config, turn the ROM bytes into a cartridge, construct a Board, and wire its video output to a <canvas> through a VideoEndpoint (it hands you a ready-to-blit ImageData each frame):
const { Board, Config, CartridgeFactory, VideoEndpoint } = window.STELLERATOR;
const cfg = Config.create({ tvMode: 0 /* ntsc */, enableAudio: false });
const cart = await new CartridgeFactory().createCartridge(romBytes);
const board = new Board(cfg, cart);
const video = new VideoEndpoint(board.getVideoOutput());
video.newFrame.addHandler(m => { ctx.putImageData(m.get(), 0, 0); m.release(); });
(function frame(){ board.tick(262 * 228); requestAnimationFrame(frame); })();
The machine is plain objects. Everything the host page needs hangs off the board:
| Member | Kind | What it does |
|---|---|---|
board.tick(n) | method | Advance n TIA colour clocks; the 6507 steps once every third. One NTSC frame is 262×228 clocks. |
board.getCpu().state | field | Live 6507 registers: a x y s, p (the 16-bit program counter) and flags (the 8-bit P status register). |
board.getCpu().executionState | field | boot / fetch / execute; watching for the fetch boundary is how we single-step whole instructions. |
board.getBus().peek(a) | method | Side-effect-free read of the 6507 bus (masked to 13 bits: TIA, PIA RAM, cartridge). |
board.getBus().poke(a, v) | method | Write the bus, poke memory live for cheats or a debugger. |
board.getJoystick0() | method | Player-1 joystick: getUp/Down/Left/Right/Fire(), each a switch with toggle(down). |
board.getControlPanel() | method | The console switches: getResetButton(), getSelectSwitch(), difficulty and colour switches. |
board.getVideoOutput() | method | The TIA as a video source (160 px wide, variable height); feed it to a VideoEndpoint. |
Debugger integration
The plug-in (6502-ts-debug.js) reads the live board from window.EMU_BOOT and registers the 6507 with the shared debugger. Because the ROM is fetched asynchronously the board appears late, so the plug-in polls for EMU_BOOT before wiring up.
Mind the register names. On the 6502.ts CPU state.p is the program counter and state.flags is the P status byte, the opposite of the usual 6502 mnemonic. We map PC ← state.p and P ← state.flags and reuse the shared mos6502 disassembler:
pc: () => cpu.state.p & 0xffff,
registers: () => [
{ name: 'PC', value: cpu.state.p, width: 16, set: v => cpu.state.p = v & 0xffff },
{ name: 'P', value: cpu.state.flags, width: 8, set: v => cpu.state.flags = v & 0xff },
/* A, X, Y, SP and the N V B D I Z C flag bits */
]
Owning the clock. The boot script runs the frame loop itself, so it can pause, single-step and breakpoint. As tick(1) is one colour clock and the CPU only advances every third, single-stepping watches executionState: leave the current fetch, then run to the next one - exactly one instruction. Execution breakpoints are a PC set tested at each fetch; watchpoints wrap bus.write and pause when a watched address is stored. Memory windows read through peek() so auto-refresh never disturbs TIA/PIA state.
Architecture
6502.ts is a cycle-accurate Atari 2600 written in TypeScript. The Board ties the chips together and is clocked at the TIA colour-clock rate:
StateMachineCpu- a micro-stepped MOS 6507 (a 6502 with only 13 address pins, so its 8 KB space mirrors across the bus). ItsstateandexecutionStateare plain fields.Tia- the Television Interface Adaptor: it generates the picture line by line into a 160×N RGBA surface and the two audio channels. It is the video source behindgetVideoOutput().Pia(RIOT), 128 bytes of RAM, the interval timer and the I/O ports the joysticks and console switches read.Cartridge*- one class per bank-switching scheme;CartridgeFactorysniffs the ROM and picks the right one (the bundled games are plain 4K).Bus- decodes each 13-bit address to TIA ($00–$7F), PIA ($80select) or cartridge (A12), with a side-effect-freepeek/pokepair for debugging.
The whole machine is ordinary objects with no wasm heap, so the debugger single-steps the CPU, reads registers straight off state, and implements breakpoints and watchpoints as host-side checks around tick() - with no changes to the emulator core.