Initial commit: NES emulator with GTK4 desktop frontend
Some checks failed
CI / rust (push) Has been cancelled
Some checks failed
CI / rust (push) Has been cancelled
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.
This commit is contained in:
177
docs/integration.md
Normal file
177
docs/integration.md
Normal file
@@ -0,0 +1,177 @@
|
||||
# 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
|
||||
|
||||
```rust
|
||||
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`
|
||||
|
||||
```rust
|
||||
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.
|
||||
|
||||
```rust
|
||||
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.
|
||||
|
||||
```rust
|
||||
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:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
nesemu = { path = "../nesemu", features = ["adapter-api", "adapter-headless"] }
|
||||
```
|
||||
|
||||
Then:
|
||||
|
||||
```rust
|
||||
#[cfg(feature = "adapter-api")]
|
||||
use nesemu::adapter_api::{AudioSink, InputSource, TimeSource, VideoSink};
|
||||
```
|
||||
Reference in New Issue
Block a user