JS7800
JS7800 is an Atari 7800 ProSystem emulator written entirely in JavaScript by raz0red, a hand port of Greg Stanton's ProSystem emulator. It runs in-browser with an HTML5 canvas and Web Audio, emulating the 7800's SALLY (6502) CPU, the MARIA display processor and the TIA/RIOT support chips, with optional POKEY and Expansion Module sound.
Runs on: Web browser
JS7800 Online Emulator
Play JS7800 using JavaScript directly in your browser.
Controls
Configurations
| Configuration | Emulator | Machine | OS | Legal | |
|---|---|---|---|---|---|
| Atari 7800 | JS7800 | Atari 7800 | grey | Open ⛶ |
Machines emulated
Chips
Notes
Embedding
JS7800 is a webpack ES-module project, not a single script. We bundle a small boot entry with esbuild into js7800-boot.js, load it as <script type="module">, and drive the ProSystem core ourselves rather than using JS7800's own UI, so the loop can be paused and single-stepped, which the debugger needs.
Boot. Wire the shared Video module to a <canvas> through the "init" event, load the cartridge, then reset the machine and run a loop on ProSystem.ExecuteFrame:
import * as ProSystem from "./core/prosystem/ProSystem.js";
import * as Cartridge from "./core/prosystem/Cartridge.js";
import * as Video from "./core/web/video.js";
Events.fireEvent("init", { canvas, mainContainer, innerContainer, controlsDiv });
Cartridge.Load(romBytes, romBytes.length); // parse the .a78 header + map banks
Database.Load(Cartridge.GetDigest()); // per-cart tweaks by MD5
Events.fireEvent("onCartridgeLoaded", Cartridge);
ProSystem.Reset(function(){ loop(); }); // reset is async (high-score cart)
function loop(){ ProSystem.ExecuteFrame(input); Video.flipImage(); requestAnimationFrame(loop); }
The machine is plain JavaScript. Each hardware block is a module of ordinary functions and arrays, no wasm heap to reach into:
| Member | Kind | What it does |
|---|---|---|
ProSystem.ExecuteFrame(input) | method | Emulate one video frame: steps the Sally CPU, Maria graphics, RIOT and TIA/POKEY for a full field. input is a 19-entry array of joystick and console-switch booleans. |
ProSystem.Reset(cb) | method | Reset the console; cb runs once the (optional) high-score cartridge has loaded. |
Sally.ExecuteInstruction() | method | Run exactly one 6502 instruction and return its cycle count, the basis of single-step. |
Sally.GetSallyA/X/Y/S/P(), Set… | methods | Read and write the CPU registers and status byte directly. |
Sally.GetSallyPC() | method | The program counter, a Pair object: .getW() / .setW() read and write the 16-bit value. |
Memory.ram | field | The 64 KB CPU-visible address space (RAM, register shadows and the mapped cartridge bank) - a plain array, readable with no side effects. |
Memory.Read/Write(addr[,v]) | methods | Bus access with hardware side effects; Write also routes ROM-region pokes through cartridge banking. |
Video.flipImage() | method | Blit Maria's finished framebuffer through the region palette to the canvas. |
ProSystem.ProSystemSave/Load() | methods | Serialise / restore the whole machine (a save state). |
Because it is all JavaScript, the host page inspects and controls the machine directly, which is exactly what the debugger does.
Debugger integration
Wiring JS7800 into the shared in-browser debugger needed a boot shim, plus a set of techniques for reaching into the live machine.
1 · A boot shim, because JS7800's top module owns the loop. Its js7800.js builds a logo, a controls bar and a high-score client, and runs an internal setTimeout loop that cannot be paused or single-stepped from outside. So we import only the core modules and drive them from a loop we control:
// our loop, not JS7800's, so pause / step / breakpoints work
function step(n){ while(n-->0) ProSystem.ExecuteFrame(input); Video.flipImage(); }
function stepInsn(n){ while(n-->0) Sally.ExecuteInstruction(); Video.flipImage(); }
2 · Bundled as an ES module. The core is a graph of circularly-importing ES modules with a webpack-only palette loader. Building the boot entry with esbuild (--bundle --format=esm --loader:.pal=dataurl) keeps live module bindings and inlines the palettes; the page loads the result with <script type="module">. Because a module is deferred, EMU_BOOT is published asynchronously, so the debug plug-in polls for it.
Techniques for deeper access. The plug-in reaches into the running machine through the core's own surfaces; no fork:
- The Sally CPU is a 6502. Atari's SALLY is a stock 6502 core, so it reuses the shared
mos6502disassembler and the standard status-flag layout (C Z I D B V N) with no new decoder. - Direct registers.
Sally.GetSallyA/X/Y/S/P()and their setters read and write the CPU live; the program counter is aPair, written throughGetSallyPC().setW(). - Side-effect-free reads. The hex and disassembly views read the raw
Memory.ramarray, so auto-polling the view never acknowledges a RIOT timer or an interrupt flag the wayMemory.Read()would; pokes still go throughMemory.Write()for correct banking. - Instruction step and breakpoints.
Sally.ExecuteInstruction()advances exactly one instruction. With breakpoints set, the loop runs instruction-by-instruction and halts the moment the PC reaches a watched address; with none set it runs whole frames at full speed with correct Maria video and RIOT timing.
Everything the debugger shows, registers read/write, memory hex/disassembly, follow-PC, single-step and execution breakpoints, is built from these, with no changes to the emulator core.
Architecture
JS7800 is a port of Greg Stanton's ProSystem, a readable interpreted Atari 7800 emulator. Each chip of the 7800 is its own module:
Sally- the 6502 CPU (Atari's "SALLY" variant), with the 64 KB memory map and cartridge banking throughMemory.Maria- the 7800's display processor: a DMA-driven graphics engine that reads display lists and rasters scan-line by scan-line into a blit surface.Tia- the 2600-era TIA, retained on the 7800 for two audio channels (and 2600 backwards compatibility).Riot- the 6532 RIOT: the joystick / console-switch ports and the interval timer.Pokey/Xm- optional POKEY sound in some carts, and the Expansion Module (POKEY + YM2151).Cartridge- parses the.a78header, hashes the ROM and selects the bank-switching scheme;Regionholds the NTSC/PAL palettes and timing.
Each ExecuteFrame() walks the scan lines, running the CPU up to each Maria DMA and WSYNC boundary and firing the vertical-blank NMI, then hands the finished framebuffer to Video. Because every component is an ordinary object, the whole machine state is inspectable, which is what the debugger reads each refresh.