Files
nesemu/docs/integration.md
se.cherkasov bdf23de8db
Some checks failed
CI / rust (push) Has been cancelled
Initial commit: NES emulator with GTK4 desktop frontend
Full NES emulation: CPU, PPU, APU, 47 mappers, iNES/NES 2.0 parsing.
GTK4 desktop client with HeaderBar, pixel-perfect Cairo rendering,
drag-and-drop ROM loading, and keyboard shortcuts.
187 tests covering core emulation, mappers, and runtime.
2026-03-13 11:48:45 +03:00

4.7 KiB

Integration Guide

This guide shows how to embed nesemu into a host application or frontend.

For the stable API boundary, see api_contract.md. For internal structure, see architecture.md.

Choose An Integration Level

Use the lowest level that matches your needs:

  • Cpu6502 + NativeBus Use this if you need fine-grained stepping or low-level control.
  • NesRuntime Use this if you want frame-oriented execution, rendering helpers, and runtime state handling.
  • RuntimeHostLoop Use this if your host runs the emulator frame-by-frame and wants explicit input, video, audio, and pacing control.
  • ClientRuntime Use this if your app has running/paused/step states and needs lifecycle-oriented ticking.

Minimal ROM Load

use nesemu::{create_mapper, parse_rom, Cpu6502, NativeBus};

let rom_bytes = std::fs::read("game.nes")?;
let rom = parse_rom(&rom_bytes)?;
let mapper = create_mapper(rom)?;
let mut bus = NativeBus::new(mapper);
let mut cpu = Cpu6502::default();
cpu.reset(&mut bus);

Using NesRuntime

use nesemu::{FRAME_RGBA_BYTES, NesRuntime};

let rom_bytes = std::fs::read("game.nes")?;
let mut runtime = NesRuntime::from_rom_bytes(&rom_bytes)?;
runtime.run_until_frame_complete()?;

let mut frame = vec![0; FRAME_RGBA_BYTES];
runtime.render_frame_rgba(&mut frame)?;

Use NesRuntime when you want:

  • frame-based stepping instead of raw CPU control
  • framebuffer extraction
  • runtime-level save/load state helpers

Using RuntimeHostLoop

RuntimeHostLoop is the main integration point for hosts that want explicit control over frame execution.

use nesemu::{
    AudioOutput, HostConfig, InputProvider, JOYPAD_BUTTONS_COUNT, NesRuntime, RuntimeHostLoop,
    VideoOutput,
};

struct Input;
impl InputProvider for Input {
    fn poll_buttons(&mut self) -> [bool; JOYPAD_BUTTONS_COUNT] {
        [false; JOYPAD_BUTTONS_COUNT]
    }
}

struct Video;
impl VideoOutput for Video {
    fn present_rgba(&mut self, _frame: &[u8], _width: usize, _height: usize) {}
}

struct Audio;
impl AudioOutput for Audio {
    fn push_samples(&mut self, _samples: &[f32]) {}
}

let rom_bytes = std::fs::read("game.nes")?;
let runtime = NesRuntime::from_rom_bytes(&rom_bytes)?;
let mut host = RuntimeHostLoop::with_config(runtime, HostConfig::new(48_000, false));

let mut input = Input;
let mut video = Video;
let mut audio = Audio;

let stats = host.run_frame_unpaced(&mut input, &mut video, &mut audio)?;
let _ = stats;

Use run_frame for paced execution and run_frame_unpaced when the host controls timing externally.

Using ClientRuntime

ClientRuntime wraps the runtime with a simple running/paused/step lifecycle.

use nesemu::{
    AudioOutput, ClientRuntime, EmulationState, HostConfig, InputProvider, JOYPAD_BUTTONS_COUNT,
    NesRuntime, VideoOutput,
};

struct Input;
impl InputProvider for Input {
    fn poll_buttons(&mut self) -> [bool; JOYPAD_BUTTONS_COUNT] {
        [false; JOYPAD_BUTTONS_COUNT]
    }
}

struct Video;
impl VideoOutput for Video {
    fn present_rgba(&mut self, _frame: &[u8], _width: usize, _height: usize) {}
}

struct Audio;
impl AudioOutput for Audio {
    fn push_samples(&mut self, _samples: &[f32]) {}
}

let rom_bytes = std::fs::read("game.nes")?;
let runtime = NesRuntime::from_rom_bytes(&rom_bytes)?;
let mut client = ClientRuntime::with_config(runtime, HostConfig::new(48_000, true));

client.set_state(EmulationState::Running);

let mut input = Input;
let mut video = Video;
let mut audio = Audio;
let _ = client.tick(&mut input, &mut video, &mut audio)?;

client.pause();
client.step_frame(&mut input, &mut video, &mut audio)?;

Use this wrapper when your UI loop naturally switches between running, paused, and manual stepping.

Input Mapping

Public helpers are available to avoid hard-coded button indices:

  • JoypadButton
  • JOYPAD_BUTTON_ORDER
  • set_button_pressed
  • button_pressed

Public button order is:

[Up, Down, Left, Right, A, B, Start, Select]

Framebuffer And Audio

  • Video frames are exposed as RGBA8
  • Frame size is 256x240
  • Audio output is a stream of mixed mono f32 samples
  • The runtime mixer is usable for host integration, but it is intentionally interim

Save-State Use

Use runtime-level state when you need host-visible frame metadata and input state preserved alongside low-level emulation state.

Use bus-level state if you are integrating at the low-level core boundary.

Optional Adapter Crates

If you want backend-agnostic adapter traits and headless implementations:

[dependencies]
nesemu = { path = "../nesemu", features = ["adapter-api", "adapter-headless"] }

Then:

#[cfg(feature = "adapter-api")]
use nesemu::adapter_api::{AudioSink, InputSource, TimeSource, VideoSink};