-
Notifications
You must be signed in to change notification settings - Fork 0
Scripting Engine
rusty2600-script adds Lua scripting to Rusty2600, off by default. It's a std-only, unsafe-permitted crate — the exception to the chip crates' no_std + #![forbid(unsafe_code)] house style, since mlua's C FFI requires it (the same reasoning that makes rusty2600-cheevos vendor rcheevos with unsafe). It never touches core or frontend types directly: a host implements a ScriptBus trait over whatever real emulator state it owns, and the scripting crate stays completely host-agnostic.
The plan called for mlua as the native backend plus a pure-Rust piccolo-backed WebAssembly fallback (mlua's vendored C Lua build cannot target wasm32-unknown-unknown at all). Only mlua shipped initially — piccolo was, at the time, a materially less mature project (fewer standard-library facilities, less battle-tested), and landing a second, non-byte-parity scripting backend alongside a brand-new API surface in the same release risked shipping either half-tested.
This was revisited for real in v2.9.0, not just re-deferred on the same reasoning: piccolo's only published crates.io release at the time implemented almost none of Lua's string/table standard library (ruling out the kind of script this crate's own emu API invites), and its architecture (a gc-arena-based "stackless" VM driven by repeated step() calls inside an arena scope) is a genuine mismatch for mlua's current plain-owned-VM embedding shape — adopting it would mean a real engine rewrite, not a second backend file. The decision: defer again, on a narrower, confirmed-external-blocker basis rather than a scope-discipline judgment call. In-browser Lua scripting remains unsupported on wasm32; the native mlua path is unaffected either way.
| Function | Gated? | Backs onto |
|---|---|---|
emu.peek(addr) |
No | read-only bus access |
emu.poke(addr, val) |
Yes (WritesLocked) |
a real bus write |
emu.cpu() |
No | a CPU register snapshot |
emu.onFrame(fn) |
No | registers a callback invoked once per real emulated frame |
emu.setJoystick(port, direction, pressed) |
Yes | joystick input override |
emu.setConsoleSwitch(name, value) |
Yes | console-switch override |
emu.drawText(x, y, text) / drawRect(...) / drawPixel(...)
|
No | an on-screen overlay compositor |
emu.pause() |
No | pauses the host |
emu.saveState() / emu.loadState(bytes)
|
No | the real, versioned SaveState format |
A locked write surfaces as a Lua runtime error to the script — verified by tests that the underlying write never actually reaches the bus, not just that an error gets thrown.
Every script-driven write is checked against a lock that folds in every real source of "an unreplicated local write would break something": RetroAchievements hardcore mode, and — once connected — an active rollback netplay session (see Netplay-and-Rollback). The lock deliberately does not carry stub fields for subsystems that don't have a real lock concept yet; a field gets added in the same change that gives its subsystem something real to gate.
A real ScriptBus implementation, a live onFrame hook, and a Tools → Load Script... menu entry exist in rusty2600-frontend. The implementation owns a private System clone synced from the real emulator state at the start of each tick rather than a live pointer into it — a deliberate, documented indirection (not a corner cut) needed because the script engine's Lua VM is fixed to a 'static bound at construction, while the real emulator state lives behind a short-lived mutex guard re-acquired every render pass. Since the entire 2600 game-state is small (128 bytes of RIOT RAM plus a handful of registers), cloning it every tick is cheap and the resulting peek/poke/save-state operations are exact, real operations, not an approximation.
Script-drawn overlay primitives (drawText/drawRect/drawPixel) composite onto the display via an egui foreground layer, piggybacking on the frontend's always-on egui pass rather than adding a new render pipeline of its own — see Frontend-Architecture.
Lua's default print writes to the real process stdout, invisible in a GUI app. The scripting engine overrides print to push into a capped, persistent log ring buffer instead, and any onFrame runtime error gets pushed into the same log distinctly marked as an error. The debugger's Lua Console panel renders this log, oldest-first, errors highlighted — output-only, not an interactive REPL (executing arbitrary ad-hoc Lua from the debugger would need its own WritesLocked integration, deliberately out of scope for this panel).
Architecture-Overview · Frontend-Architecture · Netplay-and-Rollback · Deferred-Features
Rusty2600 is a cycle-accurate Atari 2600 (VCS) emulator written in pure Rust. Licensed under MIT OR Apache-2.0. | GitHub Repository | Web Demo