SearchA-ZP › Palm Pilot

Palm Pilot

1997 Open source · GPL-3.0 Online

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.

Visit the official site ↗

Runs on: Web browser, Windows, macOS, Linux

Palm Pilot Online Emulator

Play Palm Pilot using JavaScript directly in your browser.

Configurations

ConfigurationEmulatorMachineOSLegal
Palm III — PalmOS launcherPalm PilotPalm PilotPalmOSgreyOpen ⛶
Palm m515 — PalmOS 4.1 (colour)Palm PilotPalm PilotPalmOSgreyOpen ⛶
Vexed — open source puzzle gamePalm PilotPalm PilotPalmOSopenOpen ⛶

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:

CallWhat 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 global regs register file and regflags the 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.