SearchA-ZN › NP2kai

NP2kai

2016 Open source · BSD-3-Clause Online & downloadable

NP2kai is a modernized fork of Neko Project II that adds cross-platform builds and a libretro core for RetroArch, including an Emscripten/WebAssembly version. It emulates the PC-9801 and PC-9821 hardware with expanded sound and peripheral support.

Visit the official site ↗

Runs on: Windows, macOS, Linux, Android, RetroArch, Web browser

NP2kai Online Emulator

Play NP2kai using JavaScript directly in your browser.

Configurations

ConfigurationEmulatorMachineOSLegal
FreeDOS(98)NP2kaiNEC PC-9801DOS (MS-DOS / DR-DOS)greyOpen ⛶

Machines emulated

Chips

Notes

Embedding

np2kai (AZO234's actively-maintained fork of Neko Project II) emulates a whole NEC PC-98 - an Intel i386-class CPU, the µPD765 floppy controller, the text/graphics GDCs and the YM2203 sound chip - with the machine compiled to WebAssembly by Emscripten (its SDL2 target). We self-host everything (no CDN): the rebuilt np2kai.js + np2kai.wasm, and a GPL FreeDOS(98) boot floppy. NP2kai carries its own clean-room ITF/BIOS and an embedded font, so no NEC ROM is needed to reach a DOS prompt.

The build is a plain (non-modularised) Emscripten program, so you configure it by defining the global Module before the script loads, hand it the canvas, the boot disk as a command-line argument, and a preRun hook that drops the disk into the in-memory filesystem:

var Module = {
  canvas:    screenCanvas,
  arguments: ["/fd98.hdm"],                          // mounted to FDD1, booted by the ITF
  locateFile: function(p){ return p === "emnp21kai_sdl2.wasm" ? SRC+"np2kai.wasm" : SRC+p; },
  preRun: [function(){
    Module.addRunDependency("disk");
    fetch(DISK).then(r => r.arrayBuffer()).then(function(b){
      Module.FS.writeFile("/fd98.hdm", new Uint8Array(b));
      Module.removeRunDependency("disk");
    });
  }]
};

Once onRuntimeInitialized fires we publish a small transport plus the debug hooks on window.EMU_BOOT. SDL2 keeps a live reference to the canvas element, so the debugger can freely re-home it between the play view and its Screen window and the PC-98 keeps drawing wherever it lands.

HandleKindWhat it does
Module._emudbg_pause(1|0)hookSet / clear the paused flag the frame loop checks once per frame. Our transport pause / resume.
Module._emudbg_step()hookQueue exactly one pccore_exec() frame while paused. Our step / stepInsn (a frame burst).
Module._emudbg_reg(i) / _emudbg_read(a)hookSample one register / one guest RAM byte on demand.
Module._emudbg_key(ref,down)hookInject a PC-98 key by matrix code, drives the on-screen keyboard.
Module._emudbg_reset()hookpccore_reset() - reboots the machine off the mounted disk.

Debugger integration

How a WebAssembly core with PRIVATE state still gets the full debugger. v86 hands JavaScript typed-array views onto its CPU, so JS can read it directly. NP2kai does not: its i386c core keeps the architectural state in a C global (CPU_STATSAVE, i.e. i386core.s) that lives inside the wasm sandbox with no JS view. The dividing line the Tier-4 rollout drew is exactly this, a core that hides state needs a patched build that copies its state out. So we rebuilt NP2kai from source with one new file, emudbg.c, holding a handful of EMSCRIPTEN_KEEPALIVE functions that SAMPLE the state when the debugger asks (~10×/s + once per step). There is no per-instruction or per-cycle hook - the emulator's hot path is untouched, so the cost is paid only while the debug window is open.

Exactly what was changed (against AZO234/NP2kai, tag 0.86):

// NEW FILE: emudbg.c  - reads i386c/ia32/cpu.h macros over CPU_STATSAVE
uint32_t emudbg_reg(int i)      // CPU_REGS_DWORD(i)  EAX ECX EDX EBX ESP EBP ESI EDI
void     emudbg_set_reg(i,v)   // write-back (poke the core's reg)
uint32_t emudbg_sreg(int i)     // CPU_REGS_SREG(i)   ES CS SS DS FS GS
uint32_t emudbg_eip() / emudbg_eflags()   // CPU_EIP / REAL_EFLAGREG (recombines lazy CPU_OV)
uint32_t emudbg_pc()          // CS_BASE + IP  - real-mode linear PC for disasm
uint32_t emudbg_read(a) / emudbg_read_block(a,out,len)  // mem[] backing store, side-effect-free
void     emudbg_pause(on) / emudbg_step() / emudbg_reset()
void     emudbg_key(ref,down)  // keystat_keydown/keyup, PC-98 key matrix

// EDIT 1: sdl/np2.c np2exec(), honour the paused flag ONCE PER FRAME:
if (np2dbg_is_paused()) { if(np2dbg_take_step()){ joymng_sync(); pccore_exec(TRUE); } emscripten_sleep(16); continue; }
// EDIT 2: CMakeLists.txt, link the exports + memory growth (see build note).
What the debugger needsWhere it lives in the i386c core
EAX ECX EDX EBX ESP EBP ESI EDICPU_STATSAVE.cpu_regs.reg[i].d - sampled by emudbg_reg; writable via emudbg_set_reg.
EIP (drives disasm)CPU_EIP; the disasm PC is the real-mode linear CS_BASE + IP from emudbg_pc.
Segment selectors CS DS SS ES FS GSCPU_REGS_SREG(i). Read-only: writing a selector alone would not recompute the cached segment base.
EFLAGS + condition flagsREAL_EFLAGREG, which recombines CPU_FLAG with the lazily-evaluated overflow (CPU_OV). Read-only for that reason.
Physical RAMmem[] (i386c/cpumem.c, the 2 MB backing store) read directly by emudbg_read - side-effect-free, never an I/O port.

Play / pause / step, the honest limit. Pause and resume set a flag that NP2kai's own frame loop (np2exec) tests once per frame, so the machine idles cheaply while stopped. Step queues exactly one pccore_exec() - one emulated FRAME (many instructions). That is the smallest advance we can make without adding a per-instruction check to the hot loop, which the performance rule forbids. It is enough to watch the registers and RAM change live, but it is a frame burst, not one instruction, and we say so. Breakpoints / watchpoints are therefore NOT enforced: catching a PC or a RAM write needs a per-instruction hook that would slow the emulator even with the debugger closed, so we do not fake them.

How to rebuild this emulator (reproducible from scratch):

# toolchain: Homebrew emscripten 6.0.3 (emcc/em++/emcmake on PATH), cmake, make
git clone https://github.com/AZO234/NP2kai         # tag 0.86
# add emudbg.c (above); patch np2exec() (above); in CMakeLists.txt provide a
# PNG::PNG interface target using -sUSE_LIBPNG=1, drop ssl/crypto from the em
# base libs, add emudbg.c to the emnp21kai_sdl2 target + these link flags.
cd NP2kai && mkdir build && cd build
NP2KAI_VERSION=0.86 NP2KAI_HASH=$(git rev-parse --short HEAD) emcmake cmake .. -D__EMSCRIPTEN__=ON -DUSE_NETWORK=OFF -DUSE_TICKCOUNT=OFF -DCMAKE_BUILD_TYPE=Release
make emnp21kai_sdl2 -j8       # default target: IA-32 core, SDL2
# link flags added: -sALLOW_MEMORY_GROWTH=1 -sINITIAL_MEMORY=134217728
#   -sEMULATE_FUNCTION_POINTER_CASTS=1 -sASYNCIFY=1 -sFORCE_FILESYSTEM=1
#   -sEXPORTED_FUNCTIONS=_main,_emudbg_*,_malloc,_free
#   -sEXPORTED_RUNTIME_METHODS=ccall,cwrap,getValue,setValue,HEAPU8,HEAPU32,FS,addRunDependency,removeRunDependency
# out: emnp21kai_sdl2.{js,wasm} -> vendored as np2kai.{js,wasm}

Two build quirks worth noting: NP2kai's function-pointer I/O tables trip the strict wasm indirect-call type check, fixed with EMULATE_FUNCTION_POINTER_CASTS (which then needs an explicit ASYNCIFY=1 because it disables the auto-detection that emscripten_sleep relies on); and the default 64 MB heap OOMs during the PC-98 memory sizing, fixed with memory growth.

Architecture

NP2kai is a full-system NEC PC-98 emulator. The Intel i386-class CPU (the i386c/ia32 core) is compiled to WebAssembly; the surrounding machine - the two GDCs (text + graphics), the µPD765 floppy controller, the µPD8255 keyboard/mouse ports, the RTC and the YM2203 (OPN) sound chip - is C talking to the CPU over the bus, all client-side.

  • np2kai.js / np2kai.wasm - the whole PC-98 (CPU + devices) as the Emscripten SDL2 build, rebuilt with the emudbg.c export hooks.
  • Clean-room ITF / BIOS and an embedded font are built into NP2kai itself (BSD-3), so booting needs no NEC ROM.
  • freedos98_2hd.hdm - a 2HD (1232 KB) FreeDOS(98) boot floppy (GPL, freely redistributable) that boots straight to A:\>.

The default machine boots FreeDOS(98) in real mode, so the shared 16-bit x86 disassembler drives the disasm view correctly. Code that switches to 32-bit protected mode disassembles approximately (the decoder is a 16-bit model).