SearchA-ZS › Sfotty Pie A8

Sfotty Pie A8

2024 Open source · MIT Online

Sfotty Pie A8 is an Atari 8-bit emulator for the browser written from scratch in TypeScript as part of the Sfotty Pie 6502 toolset. It bundles open-replacement OS and BASIC ROMs (AltirraOS and Altirra BASIC) so it needs no original Atari firmware.

Visit the official site ↗

Runs on: Web browser

Sfotty Pie A8 Online Emulator

Play Sfotty Pie A8 using JavaScript directly in your browser.

Configurations

ConfigurationEmulatorMachineOSLegal
Atari 800XLSfotty Pie A8Atari 800XLgreyOpen ⛶
Atari 800Sfotty Pie A8Atari 800greyOpen ⛶

Machines emulated

Chips

Notes

Embedding

Sfotty Pie is a headless Atari 8-bit machine (@sfotty-pie/a8) on a cycle-exact 6502 core (@sfotty-pie/sfotty): the framebuffer is a byte array, audio is a level to sample, and input goes through device objects. Nothing draws itself; the host wires every socket. Both packages are TypeScript, so we bundle a small boot entry with esbuild (--format=esm) into sfotty-boot.js, load it as <script type="module">, and drive the machine from a loop we control so it can be paused and single-stepped, which the debugger needs.

Boot. Construct an Atari with the OS ROM (and, on the XL, built-in BASIC), then run one machine cycle at a time and blit the frame through the Atari palette:

import { Atari } from "@sfotty-pie/a8";
const machine = new Atari({ xl: true, os, basic });   // 800XL: BASIC is built in
const palette = paletteFor("ntsc");              // 0xAABBGGRR, canvas-ready
(function frame(){
  for (let c = 0; c < 29868; c++) machine.cycle();   // one NTSC frame of colour clocks
  for (let i = 0; i < n; i++) img32[i] = palette[machine.frame[base + i]];
  ctx.putImageData(img, 0, 0); requestAnimationFrame(frame);
})();

The machine is plain objects. Everything the debugger needs is a live property, with no wasm heap to reach into:

MemberKindWhat it does
machine.cycle()methodRun one whole machine cycle (ANTIC + POKEY + the CPU's bus access). A bus phase may throw to suspend; catch it, resolve, and call cycle() again to retry the same cycle.
machine.cpufieldThe Sfotty 6502: A X Y S PC and the flags (nFlag…cFlag) are get/set properties, with getP()/setP() for the status byte and reset(cold).
cpu.statefieldThe microstate; state === DECODE means the next cycle fetches an opcode, i.e. the CPU is at an instruction boundary. That is the single-step primitive: cycle until state === DECODE.
machine.mmu.read(a, PEEK)methodRead the banked CPU bus with no side effects (the PEEK flag) - what the hex and disassembly views use; write(a, v) pokes memory live.
machine.framefieldThe framebuffer, one Atari colour byte per pixel (376 × the scan-line count); map through paletteFor(tv) to RGBA.
machine.keyboardfieldThe POKEY key matrix: pressKey/releaseKey (6-bit scan codes) and pressMetaKey/releaseMetaKey (Shift/Control/Break lines).
machine.observeWrite(a, fn), interceptExecutemethodMemory-trap hooks - used here for watchpoints; returns a handle to unregister.

Because the CPU, bus and RAM are ordinary JavaScript, the debugger single-steps by cycling to the next DECODE, reads and writes registers straight off machine.cpu, and reads memory side-effect-free through mmu.read(a, PEEK) - with no changes to the emulator core.

Debugger integration

Wiring Sfotty Pie into the shared in-browser debugger needed two pieces of custom work, plus the techniques for reaching into the live machine.

1 · A boot shim, because the machine is headless. @sfotty-pie/a8 ships no display, audio or loop, by design. So the boot builds the machine, wires the framebuffer/keyboard/audio, and owns a loop we control so pause / step / breakpoints work:

// our loop, not the emulator's, one colour clock at a time
function cycleOnce(){ try { machine.cycle(); } catch (e) { machine.cycle(); } }  // resolve an internal trap, retry
function stepInsn(){ do { cycleOnce(); } while (cpu.state !== DECODE); }  // run to the next boundary

2 · Bundled as an ES module, not an IIFE. The core is a TypeScript package graph; esbuild in --format=esm keeps live module bindings. Because a module is deferred, the boot publishes window.EMU_BOOT asynchronously, so the debugger plug-in polls for it rather than reading it once.

Techniques for deeper access - all through the emulator's own surfaces, no fork of the core:

  • Side-effect-free reads. The hex and disassembly views read with mmu.read(addr, ReadOptions.PEEK), so auto-polling the view can never trip a GTIA/POKEY read side effect.
  • Direct state. Registers are plain get/set properties on the Sfotty CPU (A X Y S PC, the flags, getP/setP), read and written live.
  • Instruction step. One cycle() is a single colour clock; cycling until cpu.state === DECODE advances exactly one instruction.
  • Breakpoints. With any set, the loop steps instruction-by-instruction and halts when cpu.state === DECODE && PC ∈ breakpoints.
  • Watchpoints. machine.observeWrite(addr, …) registers a native write observer per watched address that trips the loop.

Everything the debugger shows, register read/write, memory hex/disassembly, follow-PC, single-step, breakpoints and watchpoints, is built from these. Sfotty Pie is a strong debugging target precisely because its whole state is ordinary, inspectable JavaScript.

Architecture

Sfotty Pie splits into a cycle-exact CPU and a headless machine that hangs the Atari chips off it:

  • Sfotty (@sfotty-pie/sfotty) - an NMOS 6502 verified cycle-by-cycle against the SingleStepTests/65x02 suite; all documented and undocumented opcodes. The bus is a {read, write} object it calls each cycle, and a bus method may throw to suspend the CPU mid-cycle.
  • AnticGtia - the combined ANTIC display-list processor and GTIA video chip, rendered into the framebuffer one colour clock at a time.
  • Pokey - the keyboard scanner, timers, POKEY audio and the serial (SIO) engine; Pia - the 6520 port chip (joysticks, PORTB banking).
  • Mmu - the banked memory map (RAM, OS/BASIC/cartridge ROMs, I/O), with a trap layer (interceptRead/observeWrite/interceptExecute) the machine uses for high-level SIO and the host uses for watchpoints.
  • Keyboard, Joystick, ConsolePanel - the input devices, modelled as the real key matrix and port lines.
  • ROMs - Avery Lee's open AltirraOS and Altirra BASIC, bundled; no original Atari firmware is used. On the 800 BASIC is an ordinary $A000 cartridge; on the 800XL it is built in and PORTB-banked.

machine.cycle() steps ANTIC scheduling, POKEY and the bus phase (ANTIC DMA or the CPU) in lockstep, rendering as it goes. Because every component is an ordinary object, the whole machine state is inspectable, which is what the debugger reads each refresh.