Initial commit: NES emulator with GTK4 desktop frontend
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:
2026-03-13 11:48:45 +03:00
commit bdf23de8db
143 changed files with 18501 additions and 0 deletions

116
docs/api_contract.md Normal file
View File

@@ -0,0 +1,116 @@
# API Contract
This document defines the supported external contract for `nesemu` `0.x`.
Use this file as the boundary of what external clients should rely on. For practical embedding examples, see `integration.md`. For internal structure, see `architecture.md`.
## Supported Surface
External users should prefer these entry points:
- Root crate re-exports from `src/lib.rs`
- `nesemu::runtime::*`
- `nesemu::prelude::*`
Optional adapter-facing API is available behind features:
- `adapter-api`
- `adapter-headless`
## Recommended Public Entry Points
The main public API is organized around these groups:
- ROM loading:
- `parse_header`
- `parse_rom`
- `InesHeader`
- `InesRom`
- `Mirroring`
- Cartridge mapping:
- `create_mapper`
- `Mapper`
- Low-level execution:
- `Cpu6502`
- `CpuBus`
- `CpuError`
- `NativeBus`
- High-level runtime:
- `NesRuntime`
- Host execution and lifecycle:
- `RuntimeHostLoop`
- `ClientRuntime`
- `HostConfig`
- `EmulationState`
- Host IO traits:
- `InputProvider`
- `VideoOutput`
- `AudioOutput`
- Timing and pacing:
- `FrameClock`
- `FramePacer`
- `PacingClock`
- `NoopClock`
- `VideoMode`
- Input helpers:
- `JoypadButton`
- `JoypadButtons`
- `JOYPAD_BUTTON_ORDER`
- `JOYPAD_BUTTONS_COUNT`
- `set_button_pressed`
- `button_pressed`
## Supported Client Flow
The expected integration flow is:
1. Load ROM bytes and parse them, or construct `NesRuntime` directly from ROM bytes.
2. Choose your integration level:
- use `Cpu6502` + `NativeBus` for low-level control
- use `NesRuntime` for a higher-level core wrapper
- use `RuntimeHostLoop` or `ClientRuntime` for host-facing frame execution
3. Provide input, video, and audio implementations through the public host traits.
4. Use save/load state through the runtime or bus APIs when snapshot behavior is needed.
## Stability Rules
The following are considered the primary supported surface for `0.x`:
- root re-exports
- `runtime`
- `prelude`
The following are available but less stable:
- `native_core::*` for advanced or experimental integrations
Lower-level modules may evolve faster than the root re-export surface.
## Compatibility Notes
- Types marked `#[non_exhaustive]` may gain fields or variants without a major version bump.
- Save-state compatibility is only guaranteed within the same crate version unless explicitly documented otherwise.
- Optional features may expose additional adapter-facing API, but they do not change the baseline contract of the main library.
## Extension Points
The intended extension points for hosts and frontends are:
- `InputProvider`
- `VideoOutput`
- `AudioOutput`
- `FrameClock`
- optional adapter bridge types when `adapter-api` is enabled:
- `InputAdapter`
- `VideoAdapter`
- `AudioAdapter`
- `ClockAdapter`
## Out Of Scope
This contract does not promise stability for:
- GTK frontend behavior in `nesemu-desktop`
- internal module layout under `native_core` and `runtime`
- concrete implementation details of mapper modules
- cross-version save-state compatibility unless explicitly documented

107
docs/architecture.md Normal file
View File

@@ -0,0 +1,107 @@
# Architecture
This document describes how the workspace is organized internally.
Use `../README.md` for project overview, `integration.md` for host integration, and `api_contract.md` for supported public surface.
## Workspace Layout
- `nesemu`: reusable emulation library and host-facing runtime wrappers
- `crates/nesemu-adapter-api`: backend-agnostic adapter traits
- `crates/nesemu-adapter-headless`: headless/null adapter implementations
- `crates/nesemu-desktop`: GTK4 desktop frontend that consumes the root crate
## High-Level Layers
The workspace is split into four layers:
1. `native_core`
Owns emulation correctness and hardware-facing behavior.
2. `runtime`
Wraps the core with host-oriented execution, pacing, lifecycle control, and save-state helpers.
3. adapter crates
Define integration edges without coupling the core to a concrete backend.
4. desktop frontend
Serves as a consumer and manual test harness, not as part of the library contract.
## Core Module Boundaries
- `src/native_core/cpu`: 6502 execution, addressing helpers, opcode dispatch
- `src/native_core/ppu`: rendering pipeline, VRAM/OAM/register behavior
- `src/native_core/apu`: timing, channel state, and audio-facing hardware state
- `src/native_core/bus`: component wiring and device-visible read/write semantics
- `src/native_core/mapper`: cartridge mapper abstraction and concrete implementations
- `src/native_core/ines`: iNES parsing and ROM metadata
- `src/native_core/state_io`: shared state decoding helpers
## Runtime Module Boundaries
- `src/runtime/core.rs`: `NesRuntime` orchestration around CPU + bus
- `src/runtime/state.rs`: runtime save/load state format
- `src/runtime/audio.rs`: interim PCM synthesis from core state
- `src/runtime/timing.rs`: frame pacing types and video timing
- `src/runtime/types.rs`: public joypad-related types and helpers
- `src/runtime/host/io.rs`: host IO traits and null implementations
- `src/runtime/host/executor.rs`: per-frame execution unit
- `src/runtime/host/clock.rs`: clock abstraction and pacing implementations
- `src/runtime/host/loop_runner.rs`: host loop wrapper for frame-based execution
- `src/runtime/host/session.rs`: app lifecycle wrapper for running, pausing, and stepping
## Data Flow
At a high level, the runtime stack looks like this:
1. ROM bytes are parsed into cartridge metadata and ROM contents.
2. A mapper is created from the ROM description.
3. `NativeBus` wires CPU, PPU, APU, mapper, and input-visible state together.
4. `Cpu6502` executes against the bus.
5. `NesRuntime` wraps the core to provide frame-level execution, rendering, and save-state helpers.
6. `RuntimeHostLoop` and `ClientRuntime` adapt the runtime to host application control flow.
## Public Surface Strategy
The root crate re-exports the integration-critical API so external users do not need to depend on the internal module layout.
The design intent is:
- external clients use root re-exports and `runtime`
- advanced clients may use `native_core`
- internal module paths are free to evolve faster than the root surface
## Host Responsibilities
The library intentionally leaves these concerns to the host application:
- windowing and presentation backend
- audio device/output backend
- platform input mapping
- ROM file I/O
- persistent state storage
## Input, Video, and Audio Contracts
- `JoypadButtons` are exposed in the public order `[Up, Down, Left, Right, A, B, Start, Select]`
- `InputProvider` polls the current button state from the host
- `VideoOutput` receives RGBA frames
- `AudioOutput` receives mixed mono samples
- port 2 is currently treated as disconnected in the exposed core API
## Save-State Design
- `NativeBus::save_state` and `NativeBus::load_state` persist low-level emulator state
- `NesRuntime` extends that state with runtime metadata such as frame number and active buttons
- save-state payloads are versioned for crate-internal use, not for long-term external compatibility
## Testing Layout
- CPU tests are grouped by behavior, interrupts, and invariants
- mapper tests are grouped by mapper family and property-style checks
- runtime tests cover frame execution, pacing, state roundtrips, and lifecycle control
- `tests/public_api.rs` exercises the supported public flow as a black-box consumer
## Constraints And Tradeoffs
- no platform backend is bundled beyond the GTK desktop example
- audio mixing in `runtime/audio.rs` is intentionally interim
- optional adapter crates are thin integration layers, not mandatory parts of the core runtime
- compatibility promises are defined in `docs/api_contract.md`, not by internal module visibility

177
docs/integration.md Normal file
View 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};
```