Search › A-Z › S › Sfotty Pie A8
Sfotty Pie A8
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.
Runs on: Web browser
Sfotty Pie A8 Online Emulator
Play Sfotty Pie A8 using JavaScript directly in your browser.
Controls
Configurations
| Configuration | Emulator | Machine | OS | Legal | |
|---|---|---|---|---|---|
| Atari 800XL | Sfotty Pie A8 | Atari 800XL | grey | Open ⛶ | |
| Atari 800 | Sfotty Pie A8 | Atari 800 | grey | Open ⛶ |
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:
| Member | Kind | What it does |
|---|---|---|
machine.cycle() | method | Run 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.cpu | field | The 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.state | field | The 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) | method | Read 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.frame | field | The framebuffer, one Atari colour byte per pixel (376 × the scan-line count); map through paletteFor(tv) to RGBA. |
machine.keyboard | field | The POKEY key matrix: pressKey/releaseKey (6-bit scan codes) and pressMetaKey/releaseMetaKey (Shift/Control/Break lines). |
machine.observeWrite(a, fn), interceptExecute | method | Memory-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
SfottyCPU (A X Y S PC, the flags,getP/setP), read and written live. - Instruction step. One
cycle()is a single colour clock; cycling untilcpu.state === DECODEadvances 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.