Chapter 2: console, a Super Nintendo you can keep in a variable
Most projects that let a program play a game run an emulator as a separate
process and poke at it from outside: a Lua script inside it, a socket, a
screenshot to read pixels off. This one does the opposite. The emulator core
is a library, and Console is a value:
let mut snes = Console::boot(rom_bytes, Saves::default())?;
snes.set_button(SnesButton::Start, true);
snes.run_frame()?;
snes.frame // the picture the PPU just finished: size and RGBA pixels
snes.samples // stereo audio produced during that frame
snes.saves // cartridge SRAM, keyed by extension, held in memory
snes.wram() // all 128 KiB of work RAM; wram()[0x10] is $7E0010
snes.wram_mut() // the same, writable, between frames
let machine = snes.snapshot()?; // the whole machine, between frames
snes.restore(&machine)?; // and back; refused for another core or cartridge
snes.power_cycle(); // off and on again, battery save kept
Everything else in this repository is built on those few lines. The game state, the bot, Jev's questions and the on-screen controller all come from the same machine the picture comes from, in the same process, with no copying through a socket.
Aside: a frame, not a step. A real SNES runs its CPU, its picture unit and its sound chip on their own clocks, and the core emulates all three cycle by cycle.
run_framekeeps ticking until the core says a frame was rendered, so one call is one sixtieth of a second of game, never one CPU instruction. NTSC is actually 60.0988 frames a second, which is whytarget_fps()exists: a host paces on it, never on its monitor.
The core underneath, jgenesis' snes-core, is written to be driven by a
frontend: every tick it is handed somewhere to draw, somewhere to put sound,
somewhere to keep saves and something to ask about the controller. This
crate supplies all four as plain in-memory values (Frame, Samples,
Saves, the pad), which is what lets it run the same natively and, one day,
in a browser. Where the picture goes, where the sound plays and where a save
is kept are the app's decisions, made after the frame.
A snapshot is the machine, not something like it. It starts with a
header naming the core's save-state version and the CRC-32 of the cartridge
image, so a snapshot from another build of the core or another ROM is
refused rather than half-loaded. There are two encodings: snapshot (compact
varint integers, the format of every state kept on disk) and
snapshot_fixed (every integer at full width, so the same field sits at the
same offset every time, which is what lets chapter 6 store keyframes as
small deltas). The two do not read each other.
Try it.
apps/headless(chapter 10) boots your cartridge, runs it, then takes a snapshot, runs 300 frames, goes back, runs the same 300 again, and asserts the two futures are the same picture and the same work RAM:nix develop -c buck2 run //apps/headless:headless -- \ "roms/Legend of Zelda, The - A Link to the Past (USA).sfc" 1500 run/out.ppmLook for
snapshot … bytes, identical after 300 frames.
For the people who maintain it
In this folder
| Path | What |
|---|---|
| src/lib.rs | Console (boot, run_frame, target_fps, buttons, power_cycle, both snapshot formats, wram/wram_mut), the in-memory Frame, Samples and Saves, sound-device resampling (audio_output, audio_queued), and SnapshotError. |
| BUCK | The rust_library, over //third-party/rust:snes-core, snes-config and jgenesis-common. No test target: apps/headless is its test. |
wram() and wram_mut() exist only because this repository carries two
small patches on the vendored core, which upstream keeps private (chapter
19).
← Previous: Chapter 1, packages/ · Up: packages · Next: Chapter 3, alttp/ →