NP2kai
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.
Runs on: Windows, macOS, Linux, Android, RetroArch, Web browser
NP2kai Online Emulator
Play NP2kai using JavaScript directly in your browser.
Controls
Configurations
| Configuration | Emulator | Machine | OS | Legal | |
|---|---|---|---|---|---|
| FreeDOS(98) | NP2kai | NEC PC-9801 | DOS (MS-DOS / DR-DOS) | grey | Open ⛶ |
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.
| Handle | Kind | What it does |
|---|---|---|
Module._emudbg_pause(1|0) | hook | Set / clear the paused flag the frame loop checks once per frame. Our transport pause / resume. |
Module._emudbg_step() | hook | Queue exactly one pccore_exec() frame while paused. Our step / stepInsn (a frame burst). |
Module._emudbg_reg(i) / _emudbg_read(a) | hook | Sample one register / one guest RAM byte on demand. |
Module._emudbg_key(ref,down) | hook | Inject a PC-98 key by matrix code, drives the on-screen keyboard. |
Module._emudbg_reset() | hook | pccore_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 needs | Where it lives in the i386c core |
|---|---|
| EAX ECX EDX EBX ESP EBP ESI EDI | CPU_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 GS | CPU_REGS_SREG(i). Read-only: writing a selector alone would not recompute the cached segment base. |
| EFLAGS + condition flags | REAL_EFLAGREG, which recombines CPU_FLAG with the lazily-evaluated overflow (CPU_OV). Read-only for that reason. |
| Physical RAM | mem[] (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 theemudbg.cexport 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 toA:\>.
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).