SearchA-ZP › px68k

px68k

2006 Open source · GPL-2.0 Online & downloadable

px68k is a portable Sharp X68000 emulator derived from Keropi (WinX68k), targeting PSP, Android and other platforms and available as a libretro core for RetroArch. It emulates the 68000 CPU, YM2151 sound and Human68k disk images.

Visit the official site ↗

Runs on: Windows, Linux, macOS, Android, PSP, RetroArch

px68k Online Emulator

Play px68k using JavaScript directly in your browser.

Configurations

ConfigurationEmulatorMachineOSLegal
X68000 (Human68k)px68kSharp X68000greyOpen ⛶

Machines emulated

Chips

Notes

Embedding

px68k is a Sharp X68000 emulator written in C. For the web it is rebuilt from source to WebAssembly with Emscripten (SDL2 backend). You self-host three files — px68k.js (the Emscripten loader/glue), px68k.wasm (the compiled machine), and px68k.data (a preload bundle carrying the X68000 IPL ROM, the CGROM font and a Human68k boot floppy) — hand the module a <canvas>, and call main():

var Module = {
  canvas: document.getElementById("canvas"),   // SDL2 renders the X68000 screen here
  arguments: [],
  noInitialRun: true                            // we call main() ourselves after setup
};
PX68K(Module).then(function(m){
  m.callMain([]);                                // boots IPL -> Human68k -> A>
});

The main loop is ours. The native px68k runs an infinite while(1); under Emscripten that would hang the browser, so the one source patch to x11/winx68k.cpp hands the loop body to emscripten_set_main_loop. Each animation frame it runs one X68000 frame (unless a debug-pause flag is set) and pumps SDL keyboard events. The exported control surface:

MemberKindWhat it does
Module._emudbg_pause(1|0)exportSet / clear the paused flag the main loop checks once per frame. Our transport's pause / resume.
Module._emudbg_step_frame()exportRun one whole X68000 video frame (CPU + CRTC + MFP + video). Frame-step, and our boot fast-forward.
Module._emudbg_step_insn()exportRun exactly one 68000 instruction (C68k_Exec(&C68K, 1)). Single-step.
Module._emudbg_reg(i) / _emudbg_set_reg(i,v)exportRead / write one register: 0-7 = D0-D7, 8-15 = A0-A7, 16 = PC, 17 = SR.
Module._emudbg_read(addr)exportSide-effect-free byte of the 24-bit 68000 space (RAM + IPL ROM; 0 in I/O windows).
Module._emudbg_key(sym, down)exportFeed an SDL keycode to px68k's own key handler — drives the on-screen keyboard.
Module._emudbg_reset()exportSoft-reset the machine (re-runs the IPL).

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. px68k keeps its 68000 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; with the debugger closed the core's hot path is untouched.

Where the state lives. px68k's 68000 is the C68K interpreter (m68000/c68k.c), whose whole register file is a single global C struct C68K:

typedef struct c68k_t {
  UINT32 D[8];   // data registers  D0-D7
  UINT32 A[8];   // address regs    A0-A7
  UINT32 flag_C, flag_V, flag_Z, flag_N, flag_X, flag_I, flag_S;
  UINT32 PC;      // program counter
  ...
} c68k_struc;
extern c68k_struc C68K;              // the live CPU — a plain global

What emudbg.c reads. emudbg_reg(i) returns C68K.D[i], C68K.A[i-8], C68K.PC, or the assembled SR via the core's own C68k_Get_Reg(&C68K, C68K_SR); emudbg_set_reg writes them back. emudbg_read(addr) reads the 24-bit bus side-effect-free: main RAM straight from the MEM buffer, the IPL ROM from IPL, and 0 for the I/O windows so auto-polling a memory view can never touch a device. One subtlety the notes must record: px68k stores RAM and ROM byte-swapped (MEM[addr ^ 1]) because the 68000 is big-endian on a little-endian host, so every debug read XORs the address with 1 to hand back bytes in true big-endian order — exactly what the shared m68000 disassembler expects.

What the debugger needsWhere it comes from in the wasm core
D0-D7 A0-A7 PC (writable)C68K.D[], C68K.A[], C68K.PC via emudbg_reg/_set_reg.
SR + T S X N Z V C flagsC68k_Get_Reg(&C68K, C68K_SR); flags are decoded from the SR word in the plug-in.
16 MB 68000 bus / RAM / IPL ROMemudbg_readMEM[a^1] (RAM), IPL[(a-0xFC0000)^1] (ROM), 0 elsewhere.
Disassemblythe shared m68000 decoder pointed at emudbg_read.

Pause / step — and their honesty. Pause and resume flip a single emudbg_paused flag that the patched main loop tests once per frame (never per instruction). Because C68K is an interpreter with a real C68k_Exec(&C68K, cycles) entry point, single-instruction step is exact: emudbg_step_insn() calls C68k_Exec(&C68K, 1), which advances one instruction — the register and memory views change by one 68000 op, unlike the JIT-in-wasm cores whose step is a whole time-slice. emudbg_step_frame() runs one whole video frame for coarse stepping. Breakpoints and watchpoints are surfaced in the UI but not enforced — a native PC check would have to sit in the interpreter's hot dispatch loop, which the golden rule forbids — so they are honestly labelled best-effort rather than pretended exact.

Reaching another compiled core. The recipe is general: find the struct/globals holding the CPU registers and the guest-RAM pointer, add a handful of EMSCRIPTEN_KEEPALIVE functions that copy them out (and a paused flag + a step that calls the core's smallest advance), export them, and read them from JS each refresh. A core that keeps its registers in a plain struct — as C68K does — is fully debuggable from JS with a source change measured in dozens of lines.

Architecture

px68k is a full-system Sharp X68000: the 68000 CPU, the custom CRTC/video (text + graphics planes, sprites/BG), the MFP, DMAC, FDC floppy controller, SASI/SCSI, the RTC, and Yamaha YM2151 (OPM) + ADPCM sound. It boots real X68000 software from floppy images entirely client-side.

  • m68000/c68k.c — the C68K Motorola 68000 interpreter; C68K is the register file, C68k_Exec runs it.
  • x68k/*.c — the machine: crtc/gvram/tvram video, mfp, dmac, fdc/fdd + disk_xdf/disk_dim/disk_d88 floppy, mem_wrap the 24-bit bus.
  • x11/*.c — the SDL front end: windraw (blits the composited 16-bpp screen to the SDL2 window surface), keyboard, winx68k.cpp the main loop (patched for Emscripten).
  • fmgen/*.cpp — the OPM/OPNA/PSG FM sound cores.
  • px68k.data — preloaded into the wasm filesystem: iplrom.dat (X68000 IPL, 128 KB), cgrom.dat (CG/font ROM, 768 KB), MasterDisk.xdf (a Human68k v3.02 master floppy) and its sram.dat.

The default machine is a Human68k command prompt, so the shared 68000 disassembler drives the disasm view correctly across RAM and the IPL ROM.

What was patched to build this (the reproducible bits). Source: github.com/hissorii/px68k. Two source changes only: (1) a new emudbg.c with the sampling hooks above; (2) a small patch to x11/winx68k.cpp — hand the loop to emscripten_set_main_loop, point the ROM directory at the preloaded /home/web_user/keropi, mount MasterDisk.xdf in drive 0, force Config.FrameRate=1 (the browser rAF can dip below realtime and px68k's auto-frameskip would otherwise never composite the screen), and two extern "C" wrappers so the C hooks can call the C++ frame/reset. Build (Emscripten 6.0.3):

# compile every C/C++ unit with the SDL2 port + BSD-type shims
emcc -O2 -fno-strict-aliasing -sUSE_SDL=2 \
     -D_GNU_SOURCE -include sys/types.h -DNO_MERCURY \
     -I./x11 -I./x68k -I./fmgen -I./win32api -c <each .c/.cpp>
# link, exporting the emudbg_* hooks + the malloc/runtime helpers JS needs
em++ *.o -o px68k.js -sUSE_SDL=2 -sALLOW_MEMORY_GROWTH=1 \
     -sMODULARIZE=1 -sEXPORT_NAME=PX68K -sINVOKE_RUN=0 -sFORCE_FILESYSTEM=1 \
     -sEXPORTED_FUNCTIONS=_main,_emudbg_reg,_emudbg_set_reg,_emudbg_pc,\
_emudbg_read,_emudbg_read_block,_emudbg_pause,_emudbg_step_frame,\
_emudbg_step_insn,_emudbg_key,_emudbg_reset,_malloc,_free \
     -sEXPORTED_RUNTIME_METHODS=ccall,cwrap,getValue,setValue,HEAPU8,HEAPU32,callMain \
     --preload-file rom@/home/web_user/keropi

Sound

px68k's Yamaha YM2151 (OPM) FM synthesiser plus the X68000 ADPCM channel are emulated by the fmgen cores and mixed to a single stereo stream. Because this is an Emscripten SDL2 build, that stream already flows through SDL2's own WebAudio pipeline: SDL lazily creates an AudioContext and installs a ScriptProcessorNode whose onaudioprocess pulls a fresh block of OPM/ADPCM samples out of the wasm heap on demand (via Module.SDL2.audio.currentOutputBuffer). We therefore use the native pattern: rather than re-route samples through a second sink (which would risk double audio or phase corruption) we drive the core's own context.

The handle. The context is Module.SDL2.audioContext and the source node is Module.SDL2.audio.scriptProcessorNode, which SDL wires straight to audioContext.destination. Browsers block audio until a real user gesture, and SDL auto-resumes the context on any page gesture, so to guarantee the emulator starts silent and only sounds when the viewer asks, we splice a GainNode between the script-processor and the destination and hold it at zero while muted:

// splice: scriptProcessorNode -> gain(0) -> destination, context suspended
var g = ctx.createGain();
g.gain.value = 0;
g.connect(ctx.destination);
node.disconnect();
node.connect(g);
ctx.suspend();

A ~300 ms guard re-asserts silence and (re)splices the gain the moment SDL first opens audio, so a stray gesture can never leak sound before the button is pressed.

The contract. window.EMU_BOOT.transport exposes isMuted() and setMute(m). It starts muted. setMute(false) — called from the real click on the Sound button — sets the gain to 1 and resume()s the context; setMute(true) sets the gain back to 0 and suspend()s it, which also stops the script-processor callbacks so nothing reaches the destination.

The machine boots to the idle A> Human68k prompt and stays there; we do not force any startup noise. Sound is fully supported through the pipeline above, so anything the running software plays through the YM2151 (OPM) or ADPCM channels is heard the moment the machine produces it, once the visitor clicks the Sound button. Headless Chrome runs the WebAudio graph on a software renderer, so verification asserts the SDL2 output buffer carries a real oscillating (non-DC) waveform once unmuted rather than "hearing" it; the audible result is best confirmed in a normal GPU browser.