SearchA-ZJ › JS7800

JS7800

2019 Open source · GPL-2.0 Online

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.

Visit the project on GitHub ↗

Visit the official site ↗

Runs on: Web browser

JS7800 Online Emulator

Play JS7800 using JavaScript directly in your browser.

Configurations

ConfigurationEmulatorMachineOSLegal
Atari 7800JS7800Atari 7800greyOpen ⛶

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:

MemberKindWhat it does
ProSystem.ExecuteFrame(input)methodEmulate 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)methodReset the console; cb runs once the (optional) high-score cartridge has loaded.
Sally.ExecuteInstruction()methodRun exactly one 6502 instruction and return its cycle count, the basis of single-step.
Sally.GetSallyA/X/Y/S/P(), Set…methodsRead and write the CPU registers and status byte directly.
Sally.GetSallyPC()methodThe program counter, a Pair object: .getW() / .setW() read and write the 16-bit value.
Memory.ramfieldThe 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])methodsBus access with hardware side effects; Write also routes ROM-region pokes through cartridge banking.
Video.flipImage()methodBlit Maria's finished framebuffer through the region palette to the canvas.
ProSystem.ProSystemSave/Load()methodsSerialise / 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 mos6502 disassembler 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 a Pair, written through GetSallyPC().setW().
  • Side-effect-free reads. The hex and disassembly views read the raw Memory.ram array, so auto-polling the view never acknowledges a RIOT timer or an interrupt flag the way Memory.Read() would; pokes still go through Memory.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 through Memory.
  • 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 .a78 header, hashes the ROM and selects the bank-switching scheme; Region holds 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.