SearchA-ZN › NEC PC-8801

NEC PC-8801

1981 Online · runs in your browser Open source · QUASI88

The NEC PC-8801 (1981) was NEC's high-end Z80 home and business computer, a mainstay of the Japanese software and gaming scene through the 1980s. This build runs the real machine in the browser through QUASI88 (Showzoh Fukunaga's PC-8801 emulator), rebuilt from C to WebAssembly, booting the NEC system ROMs to the N-BASIC "Ok" prompt. It plugs into the shared in-frame debugger, so you can single-step the Z80, read and write the AF/BC/DE/HL/IX/IY/SP/PC registers (and the shadow set), inspect the banked memory live, and set breakpoints and watchpoints, with an on-screen keyboard for input.

NEC PC-8801 Online Emulator

Play NEC PC-8801 using JavaScript directly in your browser.

Configurations

ConfigurationEmulatorMachineOSLegal
PC-8801 (N-BASIC)NEC PC-8801NEC PC-8801greyOpen ⛶

Visit the official site ↗

Runs on: any modern browser — nothing to install.

Machines emulated

Chips

Notes

Embedding

QUASI88 is a NEC PC-8801 emulator written in C. For the web it is rebuilt from source to WebAssembly with Emscripten (SDL2 backend). You self-host three files — quasi88.js (the Emscripten loader/glue), quasi88.wasm (the compiled machine), and quasi88.data (a preload bundle carrying the PC-8801 system ROMs) — hand the module a <canvas id="canvas">, and call main() with the boot arguments:

var Module = {
  canvas: document.getElementById("canvas"),   // SDL2 renders the PC-8801 screen here
  arguments: ["-romdir", "/rom", "-n", "-romboot", "-nowait"],
  noInitialRun: true
};
QUASI88(Module).then(function(m){
  m.callMain(Module.arguments);            // power on -> N-BASIC ROM boot
});

The main loop is ours. Native QUASI88 runs a blocking while(1); under Emscripten that would hang the browser. The single patch to SDL2/main.c replaces the blocking quasi88() with emudbg_boot(), which runs QUASI88's one-time init (quasi88_start() — allocate memory, load ROMs, power on) and then emscripten_exit_with_live_runtime(), unwinding main() without tearing the runtime down. The frame loop is then driven from JS through the exported control surface:

MemberKindWhat it does
Module._emudbg_step_frame()exportRun one whole PC-8801 video frame (main Z80 + sub Z80 + I/O), then pump SDL input and composite the screen. The rAF loop calls this each frame; pausing just stops calling it.
Module._emudbg_step_insn()exportRun exactly one main-CPU Z80 instruction (z80_emu(&z80main_cpu, 1)). Single-step.
Module._emudbg_reg(i) / _emudbg_set_reg(i,v)exportRead / write one register: 0-7 = AF BC DE HL IX IY SP PC, 8-11 = shadow AF' BC' DE' HL', 12-15 = I R IFF IM.
Module._emudbg_read(addr)exportSide-effect-free byte of the main CPU's banked 64 KB space (main_mem_read).
Module._emudbg_key(key88, down)exportFeed a PC-8801 KEY88 code to quasi88_key() — drives the on-screen keyboard.
Module._emudbg_reset()exportSoft-reset the machine.

Debugger integration

This is the point of the Tier-4 build: how a COMPILED WebAssembly core gets the same debugger as the pure-JS emulators. QUASI88 keeps its Z80 state inside the wasm heap, unreachable from JS. Rather than serialise it out per frame, we added one small file, emudbg.c, whose functions — when CALLED by the debugger's ~10 Hz refresh loop or the Step button — copy the state out on demand. There is no per-instruction or per-cycle hook in the emulator's run path.

Where the state lives. QUASI88's main Z80 is a plain global C struct z80arch z80main_cpu (pc88main.c), whose whole register file is directly readable:

typedef struct {
  pair AF, BC, DE, HL;      // main register pairs (a pair is a word/byte union)
  pair IX, IY, PC, SP;
  pair AF1, BC1, DE1, HL1;  // shadow set
  byte I, R;  Uchar IFF, IM, HALT;
  ...
} z80arch;
extern z80arch z80main_cpu;   // the live main CPU — a plain global

What emudbg.c reads. emudbg_reg(i) returns z80main_cpu.AF.W, .BC.W, … by index; emudbg_set_reg writes them back. emudbg_read(addr) reads the CPU-visible banked bus through QUASI88's own main_mem_read(), which is side-effect-free (it returns banked ROM/RAM; the PC-8801 keeps I/O in a separate space), exactly what the shared z80 disassembler wants. Separate chips expose the raw 64 KB RAM (main_ram) and the N-BASIC / N88-BASIC ROMs (main_rom_n / main_rom).

What the debugger needsWhere it comes from in the wasm core
AF BC DE HL IX IY SP PC (writable)z80main_cpu.*.W via emudbg_reg / _set_reg.
shadow AF' BC' DE' HL', I R IFF IMthe same struct; S Z H P/V N C flags are decoded from F (low byte of AF) in the plug-in.
64 KB Z80 bus / RAM / ROMsemudbg_readmain_mem_read; emudbg_ram/_romn/_rom88.
Disassemblythe shared z80 decoder pointed at emudbg_read.

Pause / step / breakpoints. Pause and resume just stop and start the JS requestAnimationFrame loop that calls emudbg_step_frame. Single-instruction step calls z80_emu(&z80main_cpu, 1), so the register and memory views change by one Z80 op. Execution breakpoints and memory-write watchpoints are enforced in the JS run loop: while none are armed the loop runs a whole frame per rAF (no per-instruction cost — the golden rule); once either is armed the loop single-steps and checks the PC against the breakpoint set and the watched bytes against their previous values after each instruction, pausing on a hit. The per-instruction cost is therefore paid only while you are actively debugging.

Architecture

QUASI88 is a full-system NEC PC-8801: two Zilog Z80s (the main system CPU and the sub/FDD CPU), the CRTC + DMA text/graphics video, the 8255 PIO linking the two CPUs, the μPD765 floppy controller, the μPD1990 calendar clock, and YM2203 (OPN) FM sound. The default machine boots the N-BASIC ROM.

  • z80.c — the Z80 interpreter; z80main_cpu / z80sub_cpu are the two register files, z80_emu() runs one.
  • pc88main.c / pc88sub.c — the two CPU subsystems: banked memory (main_mem_read), the I/O map, and interrupt update.
  • crtcdmac.c, intr.c, screen*.c — CRTC/DMA video, the VSYNC/VRTC/RTC interrupt timing, and the framebuffer composition.
  • SDL2/*.c — the SDL2 front end: graph.c blits the composed screen, event.c maps browser keys to KEY88 codes, main.c the entry point (patched for Emscripten).
  • quasi88.data — preloaded into the wasm filesystem at /rom: n88.rom + n88_0..3.rom (N88-BASIC), n80.rom (N-BASIC), disk.rom (sub-CPU), kanji1/2.rom and font.rom.

What was changed to build this (the reproducible bits). Source: github.com/umjammer/quasi88-sdl2 (SDL2 port of Showzoh Fukunaga's QUASI88). Two source changes only: (1) a new emudbg.c with the sampling hooks + the JS-driven frame runner (emudbg_step_frame batches z80_emu(cpu,1) until one VSYNC, then pumps input and composites the screen); (2) a one-line patch to SDL2/main.c to call emudbg_boot() under __EMSCRIPTEN__. Sound is compiled out (QUASI88's snddrv.h provides no-op macros when USE_SOUND is undefined), so no proprietary sound tree is needed. Build (Emscripten 6.0.3):

# compile every core + SDL2 unit with the SDL2 port, no USE_SOUND/USE_MONITOR
emcc -O2 -fno-strict-aliasing -sUSE_SDL=2 -I. -IFUNIX -ISDL2 \
     -DQUASI88_SDL2 -DLSB_FIRST -DSUPPORT_8BPP -DSUPPORT_16BPP -DSUPPORT_32BPP \
     -DSUPPORT_DOUBLE -DSUPPORT_UTF8 -DJOY_NOTHING \
     -DROM_DIR='"/rom/"' -c <each core + SDL2 .c + emudbg.c>
# link, exporting the emudbg_* hooks + the runtime helpers JS needs
emcc *.o -o quasi88.js -sUSE_SDL=2 -sALLOW_MEMORY_GROWTH=1 \
     -sMODULARIZE=1 -sEXPORT_NAME=QUASI88 -sINVOKE_RUN=0 -sFORCE_FILESYSTEM=1 \
     -sEXPORTED_FUNCTIONS=_main,_emudbg_reg,_emudbg_set_reg,_emudbg_pc,\
_emudbg_read,_emudbg_ram,_emudbg_romn,_emudbg_rom88,_emudbg_step_insn,\
_emudbg_step_frame,_emudbg_key,_emudbg_reset,_emudbg_boot,_malloc,_free \
     -sEXPORTED_RUNTIME_METHODS=ccall,cwrap,getValue,setValue,HEAPU8,HEAPU32,callMain \
     --preload-file rom@/rom