Search › A-Z › P › Playdate (Lua)
Playdate (Lua)
This plays Playdate Lua games in your browser. A JavaScript Lua VM (fengari) runs the game's Lua, and the Playdate SDK API is reimplemented in JavaScript — 1-bit graphics, the mechanical crank, sound and a Lua-level debugger. It is a VM-level high-level emulation: it does not emulate the Cortex-M7 silicon, so there is no chip page here. It ships an original CC0 demo plus public-domain games.
Runs on: Web browser
Playdate (Lua) Online Emulator
Play Playdate (Lua) using JavaScript directly in your browser.
Controls
Configurations
| Configuration | Emulator | Machine | OS | Legal | |
|---|---|---|---|---|---|
| Crankit | Playdate (Lua) | Panic Playdate | open | Open ⛶ | |
| Tennis | Playdate (Lua) | Panic Playdate | open | Open ⛶ | |
| Snake | Playdate (Lua) | Panic Playdate | open | Open ⛶ | |
| Hello Playdate | Playdate (Lua) | Panic Playdate | open | Open ⛶ |
Machines emulated
Notes
Embedding
The emulator is an authored, clean-room stack: a vendored Lua VM plus our own JavaScript implementation of the documented Playdate SDK. No proprietary Panic firmware or SDK code is used.
- Lua VM.
fengari— a pure-JavaScript port of Lua, vendored asvendor/fengari-web.js(MIT). Being pure JS with no wasm heap, it runs fromfile://under the site's strict CSP. It implements Lua 5.3; the Playdate device runs a Lua 5.4-based build, so a smallpdc-style preprocessor rewrites the two constructs games rely on that 5.3 lacks: the<const>variable attribute (stripped) and the+= -= *= /= %=compound-assignment operators (rewritten to plain Lua). - The SDK, in JavaScript.
playdate-runtime.jsowns a 400x240 one-bit framebuffer, the drawing primitives, an 8x8 bitmap font, a small polyphonic synth, and the input/crank state, and exposes them to Lua as a table of native functions.playdate-prelude.jsis our own Lua that builds theplaydate(aliaspd) surface on top of those primitives — graphics, a sprite system, timers, an in-memory datastore, the system menu, and a from-scratchclass()object model.importis a no-op because every CoreLibs piece is preloaded here. - The loop. Each game update runs inside a fresh Lua coroutine; the host resumes it once per
playdate.update(), mixes one video frame of audio toEmuAudio, and blits the framebuffer. Running each frame in a coroutine is what lets the debugger pause and single-step Lua (below).
| Member | Kind | What it does |
|---|---|---|
pd.loadGame(src,name) | method | Preprocess + load + run a game's Lua source, defining playdate.update and building the source/line tables. |
pd.resumeFrame() | method | Advance the current update's coroutine to completion, a breakpoint, or an error; returns 'done' | 'paused' | 'error'. |
pd.stepLine() | method | Advance exactly one Lua SOURCE line (single-step), via a line hook that yields the coroutine. |
pd.curLine / callDepth() / localsAt(0) | state | The line the VM is on, the call depth, and the current frame's live locals — read by the debugger. |
pd.fb | field | The 400x240 1-bit framebuffer (1 = black ink), presented to the canvas at 2x. |
Debugger integration
playdate-lua-debug.js reads window.EMU_BOOT and hands the shared debugger a Lua-level machine: the Lua VM plays the role of the "CPU". Every feature is a host-side check around the coroutine, with no change to Lua semantics.
- Single-step Lua. Step runs
pd.stepLine(), which sets aLUA_MASKLINEhook that callslua_yieldafter one source line, so each Step advances the game by exactly one line of Lua. - Breakpoints on Lua lines. The disassembly view shows the game's Lua source through the new
luadecoder (debugger/src/cpus/lua.js), one line per row. Clicking a line toggles a breakpoint; the line hook compares the current line against that set and pauses on a match. Byte offsets map to line numbers through a source line-offset table. - The Lua stack & locals. While paused,
registers()walks the paused coroutine withlua_getstack/lua_getinfo/lua_getlocalto show the current LINE, call DEPTH, and the current frame's numeric locals (L0..). The d-pad / A / B / dock state and the CRANK angle and FRAME count are shown live too. - Memory views. Two chips: the Lua source image (source-line disassembly + hex) and the 400x240 1-bit framebuffer (bits / hex / tiles), packed 1bpp so the tiles view renders the screen contents.
- Not applicable. A Lua VM has no flat addressable RAM to watch, so hardware-style write watchpoints are intentionally empty; line breakpoints are the equivalent. There is no machine register file — the "registers" are Lua state.
Architecture
The Playdate is a small yellow handheld by Panic: a 400x240 one-bit (black/white) reflective screen with no backlight, a d-pad and A/B buttons, a mono/stereo speaker, and its signature feature — a mechanical crank that folds out of the side and reports its absolute angle. Games are written in Lua (or C) against the playdate library and run at a 30–50 fps refresh rate.
- Display. One bit per pixel. This emulator keeps a 400x240 byte-per-pixel buffer, draws into it with rectangle / line / circle / text primitives, and presents it to the canvas at 2x with nearest-neighbour scaling and a Playdate-like light panel tint.
- Crank. A rotary control reporting an absolute position in degrees and a per-frame change. Here it is driven by the mouse wheel, an on-screen dial you can drag in a circle, and a dock/undock toggle — surfaced to Lua as
playdate.getCrankChange/getCrankPosition/isCrankDocked. - Sound. Games synthesise notes with
playdate.sound.synth; this emulator implements a small polyphonic synth (sine / square / saw / triangle / noise with a decay envelope) and mixes it to the shared audio sink. - Coverage. The commonly-used graphics / sprite / timer / input / sound / datastore / menu surface is implemented; asset-loading (image / font / sample files), physics and the fuller sprite/collision engine are stubbed or simplified. See README.txt for the exact implemented-vs-stubbed list.
Sound
Sound uses Pattern S (an authored core): the SDK's synth is our own small polyphonic engine in playdate-runtime.js. Each active voice is a sine / square / sawtooth / triangle / noise oscillator with a linear decay envelope; playdate.sound.synth:playMIDINote(note, vol, len) (note number or name like "C4") pushes a voice. Every video frame the mixer produces exactly Math.round(EmuAudio.sampleRate / 60) interleaved-stereo Int16 sample pairs at the context rate (so no resampling) and calls EmuAudio.push().
Mute contract. window.EMU_BOOT.transport.isMuted() / setMute() delegate straight to EmuAudio; it starts muted (browsers block audio before a gesture) and the on-page Sound button unmutes from a real click.
Sound. Sound is fully supported and plays whatever the software makes. It starts muted, because browsers block audio before a gesture, so click the Sound button to hear it.