Palm Pilot
Cloudpilot, by Christian Speckner, is a modern emulator of the Motorola DragonBall Palm handhelds — the original PalmPilot, the Palm III, V and m-series — descended from the Palm OS Emulator (POSE) and cross-compiled to WebAssembly. Here it runs directly in the browser: it boots a Palm III to the PalmOS launcher, takes the pen on the touchscreen and Graffiti area, and plugs into the same live in-frame debugger the other machines have, driven through the shared Motorola 68000 decoder.
Runs on: Web browser, Windows, macOS, Linux
Palm Pilot Online Emulator
Play Palm Pilot using JavaScript directly in your browser.
Controls
Configurations
| Configuration | Emulator | Machine | OS | Legal | |
|---|---|---|---|---|---|
| Palm III — PalmOS launcher | Palm Pilot | Palm Pilot | PalmOS | grey | Open ⛶ |
| Palm m515 — PalmOS 4.1 (colour) | Palm Pilot | Palm Pilot | PalmOS | grey | Open ⛶ |
| Vexed — open source puzzle game | Palm Pilot | Palm Pilot | PalmOS | open | Open ⛶ |
Machines emulated
Operating systems
Chips
Notes
Embedding
Cloudpilot compiles a POSE-derived PalmOS emulator to WebAssembly. Its public C++ API (bound to JS through Emscripten's WebIDL binder) is small and we drive it ourselves from a <canvas> — no Web Worker, no PWA. We self-host the rebuilt cloudpilot_web.js/.wasm, a grey PalmOS ROM, and a tiny primed session so the machine wakes at the launcher:
// the Emscripten module is a global factory (MODULARIZE, EXPORT_NAME=createModule)
var Module = await createModule({ locateFile: p => SRC + p }); // finds cloudpilot_web.wasm
var cp = new Module.Cloudpilot();
var buf = cp.Malloc(rom.length); Module.HEAPU8.set(rom, Module.getPointer(buf));
cp.InitializeSession(buf, rom.length, "PalmIII"); // pick the DragonBall device
cp.ImportMemoryImage(memPtr, memLen); cp.LoadState(statePtr, stateLen); // resume at the launcher
requestAnimationFrame(function loop(){ cp.RunEmulation(cyclesPerFrame); blit(); requestAnimationFrame(loop); });
Everything is driven through the Cloudpilot object. The run loop, the screen and the input all go through a handful of calls:
| Call | What it does |
|---|---|
cp.RunEmulation(cycles) | Runs the 68000 for up to cycles cycles and returns. We call it once per rAF tick with GetCyclesPerSecond()/60; the debugger's step calls it with tiny budgets. |
cp.CopyFrame() / IsScreenDirty() | Hands back the framebuffer (bpp 1/2/4/24) with dirty-line bounds; we convert it to RGBA using Cloudpilot's grayscale palette and putImageData onto the canvas. |
cp.QueuePenMove(x,y) / QueuePenUp() | The digitizer. Pointer events on the canvas map to device coordinates (0-159 x, 0-219 y) — y ≥ 160 is the silkscreen Graffiti area, so tapping the screen and writing Graffiti use the same path. |
cp.QueueButtonDown/Up(id) | The hardware buttons (Date/Address/ToDo/Memo, scroll up/down, Power) — wired to the on-screen gamepad and the physical arrow keys. |
cp.QueueKeyboardEvent(c) | A character into PalmOS — the physical keyboard types straight into text fields. |
cp.GetMemoryPtr()/GetRomPtr() | Direct pointers to the emulated RAM and ROM inside the wasm heap — the debugger reads them with zero side effects. |
The boot shim publishes all of this, plus the debug hooks below, on window.EMU_BOOT.transport; palm-debug.js then calls EmuKit.defineMachine.
Debugger integration
This is a Tier-4 integration: the 68000 lives inside a compiled wasm module, so we rebuilt Cloudpilot from source with a small state-export file (CpDebug.cpp) and reused the shared 68000 decoder. Cloudpilot's public API already gives frame, input, run-loop control and direct RAM/ROM pointers; the only thing missing for a full debugger was the CPU register file and a true instruction step, so we added on-demand hooks that read the UAE core's global register struct — never inside the instruction loop, honouring the golden rule:
// CpDebug.cpp — reads the live UAE 68000 register file on demand (regs/regflags
// are the globals defined in EmCPU68K.cpp). 0-7 D, 8-15 A, 16 PC, 17 SR.
EMSCRIPTEN_KEEPALIVE unsigned int emudbg_reg(int i){
if (i < 16) return regs.regs[i]; // D0-D7 then A0-A7 (A7 = active SP)
if (i == 16) return m68k_getpc();
if (i == 17) return cpdbg_make_sr(); // composes SR from regs + regflags (X N Z V C)
return 0;
}
EMSCRIPTEN_KEEPALIVE void emudbg_set_reg(int i, unsigned int v); // poke a register / flag
EMSCRIPTEN_KEEPALIVE unsigned int emudbg_read8(unsigned int a); // one bus byte via EmMemGet8
EMSCRIPTEN_KEEPALIVE unsigned int emudbg_rom_base(); // bus address of the ROM (EmBankROM)
Run it on the main thread, one slice at a time. Cloudpilot's PWA runs the core in a Web Worker and talks to it over shared memory; a worker cannot answer the debugger's synchronous register reads. So we run the core on the page's own thread and drive its existing RunEmulation(cycles) from requestAnimationFrame. Pause simply stops calling it; resume restarts the loop.
Single-instruction step is RunEmulation(1) — the UAE core always retires at least one instruction for a one-cycle budget, so the PC advances exactly one instruction (verified: 10c7cba2 → 10c7cba6 → 10c7cbaa). Frame step runs one 60 Hz slice.
Breakpoints and watchpoints are really enforced, in the run loop. When either set is non-empty the loop switches from "run a whole slice" to stepping instruction-by-instruction: after each RunEmulation(1) it checks the PC against the breakpoint set, and re-reads each watched byte and compares it to its last value — pausing the moment a breakpoint PC is reached or a watched location changes. This is the same technique the pure-JS cores use, driven here from the wasm step hook; it is slower while armed but exact (verified: a breakpoint set on a recurring PC pauses precisely at it, and a watchpoint on a live RAM byte pauses on the write).
Memory is side-effect-free. The debugger exposes three chips — the 68000 bus, RAM at $00000000 and the PalmOS ROM at its device bus base (from emudbg_rom_base) — all disassembled with the shared m68000 decoder. RAM and ROM are read straight from the wasm heap buffers (GetMemoryPtr/GetRomPtr); the bus falls back to the guarded emudbg_read8 for other regions and skips the DragonBall register page (≥ $FFF00000) so nothing latches.
Architecture
Cloudpilot descends from the Palm OS Emulator (POSE): it emulates the Motorola DragonBall family (MC68328 / EZ / VZ / SZ) that powered the classic Palm PDAs, plus the Palm Tungsten's ARM. This integration uses the DragonBall side — a Palm III (68EZ328, PalmOS 3.3, 160×160 monochrome) by default, with a Palm m515 (68VZ328, PalmOS 4.1, colour) as an alternative.
emulator/uae/— the UAE Motorola 68000 interpreter (Bernd Schmidt), with the globalregsregister file andregflagsthe debug hooks read.emulator/hardware/EmCPU68K.cpp,EmRegsEZ/VZ/SZ.cpp— the CPU glue and the DragonBall integrated peripherals (LCD controller, timers, UART, SPI touchscreen, GPIO buttons).emulator/hardware/EmMemory.cpp,EmBankDRAM/ROM/Regs.cpp— the 32-bit address bus: DRAM at$00000000, the ROM card at its device base (e.g.0C00000), and the memory-mapped registers.web/Cloudpilot.cpp— the public API (session, run loop, frame, pen/button/key queues, memory/ROM pointers, save state) bound to JavaScript;web/CpDebug.cpp— our added register/step/bus hooks.
Boot media is a grey PalmOS system ROM (Palm/Access copyright, self-hosted here and cited in README.txt) plus a small primed session — a RAM image with first-boot setup already completed and a savestate — so the machine resumes at the launcher instead of the calibration wizard. Both are removed on request.