a800 is a small, readable Atari 400/800, XL/XE, and partial 5200 (NTSC or PAL)
emulator in C. Its flat
source layout follows A8E (6502.c, Antic.c, Gtia.c, Pokey.c, Pia.c,
and AtariIo.c), while the implementation is new code informed by Atari800's
hardware organization and the Atari register model.
This is an early simplified emulator, not a replacement for Atari800. It is useful as a compact core to read and extend.
- NMOS 6502 official instruction set, decimal mode, interrupts, indirect JMP page-wrap behavior, stable undocumented read/modify/write and load/store families, and undocumented NOP encodings
- 400/800, XL/XE, and 5200 hardware/OS address maps; XL OS/BASIC/self-test overlays and PORTB banking
- selectable NTSC (default: 262 lines, 1.790 MHz, 59.92 Hz) or PAL (312 lines, 1.773 MHz, 49.86 Hz) timing, WSYNC, VCOUNT, GTIA PAL identification, VBI NMI, practical display-list/playfield/character/PMG DMA stealing, and cross-scanline accounting for instruction-boundary timing overruns
- ANTIC display-list traversal, including monochrome, multicolor, and double-width character modes, CHACTL, practical HSCROL/VSCROL, and approximate bitmap-mode rendering
- scanline DLI scheduling with per-line ANTIC/GTIA register and PMG DMA capture
- GTIA color registers, modes 9/10/11 for ANTIC mode F, console/trigger input, Atari800-derived default NTSC/PAL palettes, and an indexed 8-bit framebuffer
- basic player/missile DMA or direct-GRAF rendering with positions, sizes, two-bit missile shifters, VDELAY, fifth-player and multicolor priority mixing, sticky collision latches, HITCLR, and latched joystick triggers
- PIA joystick and PORTB registers
- POKEY keyboard and SKCTL-controlled polynomial RANDOM register; AUDCTL-aware 15/64/1790 kHz and joined per-channel audio-divider and timer countdowns and IRQ edges; CPU-synchronized register playback and sample buffering; CPU-clocked pure-tone and reference-tap polynomial distortion/noise; cycle-averaged output sampling; latched high-pass; volume-only audio; linear four-bit channel-volume mixing with output headroom; and DC filtering
- read-only ATR parsing, sector access, and an XL OS SIOV acceleration for disk status and reads
- segmented XEX loading with ordered INITAD execution and RUNAD handoff after OS startup
- raw or sparse ROM-XEX loading for OSA, OSB, XL/XE, and the 5200 OS/hardware memory map
- SDL2 video, audio, keyboard, joystick-key, and console-key frontend
- optional WASMCART packaging of a XEX with framebuffer, audio, joystick, console-button, and keyboard adapters
Important current limits: ANTIC/GTIA and DMA steals are scanline approximations rather than cycle-exact; precise scrolling fetch windows, serial SIO command transport at the POKEY bit level, Atari cartridge images, portable cross-build save states, and unstable undocumented CPU instructions are not implemented. WASMCART is a host package for the existing XEX loader, not emulation of an Atari cartridge. POKEY serial timing, analog-accurate DAC mixing, and fractional-cycle resampling remain approximations. The SDL frontend queues one audio block per emulated frame and starts playback after a small initial buffer, keeping POKEY register writes synchronized without generating future audio. The SIOV acceleration lets the stock XL OS boot compatible ATR images without bit-level serial emulation. Display-list state, playfield data, colors, character-base, player/missile data, and collisions now advance one scanline at a time. Register changes within a scanline are still approximated at its ending boundary.
A focused comparison with Atari800, A8E, and the local hardware reference identified compatibility improvements while retaining the flat, small implementation. The ANTIC pass now honors display-list DMA enable, advances the display list live rather than predicting DLI lines at frame start, wraps playfield DMA within 4 KiB, promotes and clips horizontal-scroll fetches, handles vertical-scroll entry and exit rows, and applies GTIA modes 9, 10, and 11 to ANTIC character modes 2/3 as well as mode F.
The remaining priorities, in recommended order, are:
- Turn the representative XEX set used during development into a repeatable regression corpus. Extend validation beyond the current blank-frame check with selected-frame hashes or reference captures, plus audio sample-count, peak, and transition summaries. Cover NTSC and PAL where applicable before removing or substantially rewriting implemented behavior.
- Refine CPU/ANTIC timing beyond the current scanline DMA budget and instruction-overrun carry; page-crossing penalties, stable undocumented opcode families, playfield/character/PMG steals, and master-clock advancement during CPU pauses are implemented. Memory-refresh steals are omitted until their fixed within-scanline placement can be represented without distorting DLI timing.
- Refine POKEY serial timing and analog mixing beyond the current per-channel timer IRQs, CPU-synchronized writes, reference polynomial phases, joined-channel handling, high-pass latches, and linear volume response.
- Model same-scanline player/missile repositioning and shift-register retrigger behavior only when a compatibility test demonstrates a need; the current sample set does not justify a raster-event subsystem yet.
Lower-priority subsystems such as cartridges, portable save states, and bit-level SIO should remain separate additions rather than enlarging the core preemptively.
The following implemented features are candidates for simplification if a larger automated program corpus and focused hardware tests show no meaningful compatibility loss. They should not be removed based only on the current small sample set:
- The synchronized POKEY sample buffer is sized for 8192 samples even though the frontend normally drains it every frame. A 2048- or 4096-sample buffer would reduce each machine instance by 8-12 KiB if underrun and XEX-handoff tests confirm that the additional capacity is never needed.
- POKEY currently drains its contiguous sample buffer with
memmove. A small ring buffer would avoid that copy, but adds indices and wrap handling; keep the linear buffer unless profiling shows the frame-sized copy matters. audio_phase[]now stores integral CPU-cycle countdowns and could become an integer array. Likewise, the output-sample accumulator could use fixed-point integer arithmetic for deterministic NTSC/PAL sample counts without doubles.- Add focused producer/consumer tests for the number of samples generated by one NTSC and PAL frame before simplifying the POKEY buffer or sample clock.
- Per-channel POKEY timer countdowns could use calculated absolute deadlines if timer IRQ tests confirm identical STIMER, AUDCTL, and AUDF behavior.
- Cross-scanline CPU overrun debt could be folded into the scanline DMA budget if timing-sensitive display-list and interrupt tests do not distinguish it.
- GTIA modes 9, 10, and 11 on ANTIC character modes 2/3 could be excluded behind a build option if a broad software corpus never uses the combination.
- Stable undocumented 6502 opcodes are already optional through
A800_UNDOCUMENTED_OPCODES; a strictly minimal build can disable them, and they could eventually be removed if opcode tracing finds no users.
Live display-list processing, display-list DMA gating, 4 KiB ANTIC wrapping,
basic GTIA priority/collisions, and correct BASIC/XEX startup are not current
simplification candidates because they fixed demonstrated behavior. Any
simplification should be measured with a800-validate, targeted unit tests,
screen comparisons, and audio traces before and after the change.
Requirements: a C99 compiler, CMake 3.16+, and SDL2 development files.
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build
ctest --test-dir build --output-on-failureFor a headless core-only build, which needs no SDL:
cmake -S . -B build-core -DA800_BUILD_SDL=OFF
cmake --build build-core
ctest --test-dir build-core --output-on-failureStable undocumented NMOS 6502 opcodes are enabled by default. They can be
excluded for a strictly documented instruction set with
-DA800_UNDOCUMENTED_OPCODES=OFF.
BASIC, NTSC, PAL, and XEX screen-editor initialization are enabled by default.
Diagnostic save states are disabled by default. Minimal or diagnostic builds can
use -DA800_BASIC=OFF, -DA800_NTSC=OFF, or -DA800_PAL=OFF; these define
NO_BASIC, NO_NTSC, or NO_PAL for the core. At least one video standard
must remain enabled. A no-BASIC build removes -b, --basic, and
--basic-rom; a single-standard build removes both runtime video-selection
options because there is no alternative standard to select. Use
-DA800_SAVE_STATE=ON to compile State.c and enable F6/F7, --state, and
--load-state. Use -DA800_OPEN_EDITOR=OFF to omit the pre-XEX E: open for
programs known not to depend on the normal OS editor setup.
Provide a supported raw OS ROM dump or a sparse image made by rom2xex.py
(ROMs are not included):
ATARIOSA.ROM/ATARIOSB.ROM— 10 KiB 400/800 OSATARIXL.ROM— 16 KiB XL/XE OS5200.ROM— 2 KiB 5200 OSATARIBAS.ROM— optional 8 KiB BASIC ROM, loaded only with-b
./build/a800
./build/a800 --pal # select PAL; NTSC is the default
./build/a800 --ntsc # explicitly select NTSC
./build/a800 -b
./build/a800 game.atr
./build/a800 program.xex
./build/a800 -b --os path/to/ATARIXL.ROM --basic-rom path/to/ATARIBAS.ROM
./build/a800 --load-state saved.state # with A800_SAVE_STATE=ON
./build/a800 --os OSA.XEX
./build/a800 --os 5200-OS.XEXWhen BASIC support is compiled in, it remains disabled without -b. XEX
startup performs the XL
OS initialization needed by programs, and the BASIC ROM is not read even when
it exists. The no-BASIC boot pulses OPTION while the OS samples the console
keys, and the XEX loader supplies the normal XL OS character-base default
without depending on BASIC startup. XEX startup transfers control while the
CPU is still executing OS code, disables the BASIC bank at handoff, and
temporarily holds OPTION for executables that invoke the XL warm-start path.
During that launch window, a warm start with a RAM DOSINI hook transfers to
the hook directly; this is a compact substitute for Atari800's virtual
boot-device loader. BASIC
code is not executed and READY is not displayed while launching a XEX.
Atari BASIC is available only with the XL/XE machine profile. See
ROM2XEX.md for the sparse ROM format, safe omission flags, and
the current 5200 scope.
Controls: arrows are joystick directions, Left/Right Alt is fire, F2/F3/F4
are OPTION/SELECT/START, F5 resets, and F8 is BREAK. In a save-state build, F6
saves a diagnostic snapshot and F7 restores it. The default snapshot is
a800.state; --state FILE selects another F6/F7 slot. Normal letter, number,
punctuation, Return, Space, Backspace, Escape, Tab, Shift, and Control input is
mapped to the Atari keyboard matrix and POKEY registers.
| File | Responsibility |
|---|---|
6502.c |
CPU execution and interrupts |
Antic.c |
NTSC/PAL frame timing and display-list renderer |
Gtia.c |
colors and console/trigger registers |
Palette.h |
generated default NTSC/PAL RGB tables |
Pokey.c |
keyboard, timers, random source, and audio |
Pia.c |
joystick and XL memory-control ports |
AtariIo.c |
ROM, ATR/SIOV, and segmented XEX loading |
Atari.c |
machine wiring and memory map |
State.c |
optional same-build diagnostic snapshots |
A800.c |
SDL2 application |
wasmcart_main.c |
optional WASMCART framebuffer, audio, and input adapter |
rom2xex.py |
OSA/OSB/XL/5200 raw-ROM to sparse ROM-XEX converter |
The hardware-named files are intentionally retained even when small. Merging
Gtia.c into Antic.c, or folding Pia.c/Pokey.c into Atari.c, would mix
register-device behavior with the scanline scheduler and memory bus. The
current boundaries match the requested A8E-style layout and leave room for the
remaining behavior without requiring additional source files.
An optional Emscripten adapter packages a XEX and the XL OS ROM as a WASMCART
.wasc file, with the BASIC ROM included only when -b is given. It reuses the
normal in-memory XEX loader and keeps WASMCART-specific code out of the native
core build:
python3 xex2wasc.py game.xex game.wasc "Game name"
python3 xex2wasc.py --pal game.xex game-pal.wasc "Game name"
python3 xex2wasc.py -b game.xex game-with-basic.wasc "Game name"
python3 xex2wasc.py --no-open-editor game.xex game-small.wasc "Game name"
python3 xex2wasc.py --os OS.XEX game.xex game-romxex.wasc "Game name"See WASMCART.md for prerequisites, assets, and exported devices.
The build also produces a800-validate, a headless replacement for the small
one-off boot and framebuffer probes used during development. It reports CPU,
banking, ANTIC, GTIA, POKEY audio/control registers, and framebuffer state. It
can run for an emulated duration and save the final frame as a dependency-free
binary portable pixmap (PPM):
./build/a800-validate --basic --frames 180
./build/a800-validate --frames 300 program.xex
./build/a800-validate --seconds 5 --output frame.ppm program.xex
./build/a800-validate --seconds 8 --start-frame 200 game.xex
./build/a800-validate --frames 300 --output frame.ppm disk.atr
./build/a800-validate --pal --frames 180
./build/a800-validate --load-state saved.state --frames 120Use --os and --basic-rom to select ROM files. The utility returns failure
for invalid arguments or ROM/media/output errors. A completed capture returns
1 when every framebuffer pixel has the same color and 0 when the frame contains
more than one color, making the command directly usable in automated program
smoke tests. When the frame is blank (a single color), a file requested with
--output is not written. --xex-frame N overrides the default frame-85 XEX
handoff; --start-frame N presses START for one frame; and --trace-audio
reports POKEY register changes. The blank-frame result is a smoke test, not a
complete visual regression check; the regression-corpus priority above is
intended to cover nonblank-but-incorrect output.
Save-state support must be enabled with -DA800_SAVE_STATE=ON. Snapshots are
intended to make intermittent emulator bugs reproducible. They
capture the complete core, mounted disk image, and pending XEX data, but omit
SDL/WASMCART host state and discard already-buffered audio samples when loaded.
They include a format version and build-feature checks, but are deliberately
same-build diagnostic files rather than a portable, compatibility-stable save
format. Use F6/F7 in a800, or load a captured state headlessly with
a800-validate --load-state FILE.
The simplified emulator core was written independently. It does not copy A8E source code. Atari800 was used as an architectural and behavioral reference, especially for CPU, memory, ANTIC, GTIA, POKEY, and PIA behavior.