Skip to content

Repository files navigation

PrismNES

A cycle-accurate NES emulator in C11. The core has no external dependencies and emits a 256x240 framebuffer plus an audio stream; the frontend presents them through whatever graphics API the platform prefers.

The app

make app        # -> dist/PrismNES.app
open dist/PrismNES.app

A real macOS bundle with a native menu bar (Cocoa NSMenu, NSOpenPanel, NSSavePanel). The core stays platform neutral: src/macos_menu.m turns menu items into SDL user events, so another platform only has to supply the six functions in src/menu.h.

Menu Items
File Open ROM, Import/Export Save RAM, Save/Load State, Save/Load State As
Emulation Pause, Frame Advance, Reset, Power Cycle, Mute
Debug CPU State, Console Help, Game Genie code, Run 6502 code
View 1x / 2x / 3x / 4x

Drag and drop a .nes file onto the window to load it. Battery saves are written next to the ROM on exit and reloaded on open.

Debug console

The terminal that launched the app is a live console; it never blocks the emulator:

r ADDR [LEN]      read memory          r 0300 16
w ADDR B [B..]    write bytes          w 0300 AA BB
x HEXBYTES        inject and run code  x A9 42 8D 00 03 60
gg CODE           Game Genie code      gg SXIOPO
cpu               registers, cycle, PPU position
p / q             pause / quit

x is arbitrary code execution inside the emulated machine: the bytes are written to $0700, a return address is pushed, and the PC is pointed at them, so an RTS hands control back to the running game. The example above is LDA #$42 / STA $0300 / RTS and leaves $42 at $0300.

Save states are the whole machine with pointers rewritten as offsets; a round-trip is bit-identical, verified by tools/statetest.c across NROM, MMC1 and MMC3.

Build and run

make
./prism roms/AccuracyCoin.nes

Headless, for scripted verification (no graphics dependency at all):

./prism roms/AccuracyCoin.nes --headless 4000 --script "60:start" --text
Flag Meaning
--headless N run N frames, write a PNG, exit
--script "60:start,120:right" press buttons at given frame numbers
--hold N frames to hold each scripted press (default 6)
--text decode the nametable to stdout (greppable test results)
--blargg run a blargg-protocol test ROM and print its result string
--ram HEX:LEN dump CPU RAM after the run
--watch print every distinct screen the ROM draws
--bench report frames/sec and realtime multiple
--png PATH, --scale N, --no-vsync, --mute

Controls: arrows, Z = A, X = B, Enter = Start, RShift = Select, R = reset, Esc = quit. Battery-backed carts save to <rom>.sav on exit.

Mappers

# Name Notes
0 NROM
1 MMC1 / SxROM serial shifter, 512K PRG via CHR bit 4, CHR RAM
2 UxROM
3 CNROM
4 MMC3 / TxROM full banking, mirroring, PRG-RAM protect, A12 scanline IRQ
7 AxROM single-screen mirroring
11 Color Dreams
66 GxROM
94, 180 UxROM variants

Banking resolves into pointer tables (four 8K PRG windows, eight 1K CHR windows), so a PRG or CHR access stays one indexed load regardless of mapper. iNES 1.0 and 2.0 headers are both parsed; unsupported mappers are rejected with a clear message rather than silently misbehaving.

Accuracy

AccuracyCoin: 126 / 141 (up from 109 when the core first booted it).

blargg's suites, run via --blargg:

Suite Result
instr_test-v5/official_only 16/16 passed
instr_test-v5/all_instrs 16/16 passed (all 256 opcodes incl. unofficial)
ppu_vbl_nmi 10/10 passed
instr_timing 2/2 passed
instr_misc 4/4 passed
oam_read, oam_stress, cpu_dummy_reads passed
mmc3_test_2 4/5 passed (4-scanline_timing #3)
cpu_interrupts_v2 2/5 passed (cli_latency, nmi_and_brk)
apu_test fails 4-jitter #2

How the timing works

Every CPU bus access consumes exactly one cycle, and the PPU and APU are advanced from inside that access:

read cycle  : tick tick <bus read> tick
write cycle : tick tick tick <bus write>

Placing the read strobe two dots into the cycle and the write strobe at the end is what makes the $2002 / NMI race windows land correctly.

Notable hardware behaviour that is modelled, each one verified by a test ROM that fails without it:

  • Every dummy read and dummy write in every addressing mode.
  • /NMI as a level that the CPU edge-detects at the end of each cycle, so a $2002 read in the same cycle the flag is set suppresses the interrupt.
  • The IRQ level detector sampling at φ2, before the access lands — a $4015 read that acknowledges an IRQ still leaves that cycle's sample asserted.
  • Branch interrupt polling: before cycle 2, and again before cycle 4 only when the branch crosses a page. That asymmetry is the "branch interrupt delay".
  • Interrupt hijacking: an NMI asserted mid-sequence steals a BRK or IRQ.
  • DMC and OAM DMA as real RDY stalls whose halt and alignment cycles re-read whatever address the CPU left on the bus — which is why DMA is visible to $2007, $4015 and the controller ports.
  • A $4015-triggered DMC fetch waiting two APU cycles before pulling RDY low.
  • The SH* opcodes losing their & H term when a DMA steals the dummy read.
  • $4015 answering from inside the 2A03 without driving the external data bus.
  • The frame IRQ flag clearing on the get cycle after the $4015 read, so a double read can still observe it set.
  • The controller latch being clocked by the put half of a CPU cycle, so a one-cycle strobe landing on a get cycle never latches the buttons.
  • Sprite fetches comparing against scanline & 255, which is how leftover secondary OAM can put a sprite on scanline 0.
  • MMC3 A12 edge detection filtered by PPU dots, driven by pattern fetches and by $2006/$2007 moving the VRAM address.

What still fails

Fifteen AccuracyCoin tests, in three groups with known root causes:

Sub-dot strobe placement (4 tests). M2 falls 15/24 of the way through a CPU cycle - 1.875 PPU dots - and this core advances the PPU in whole dots, so a read strobe can only sit at dot 1 or dot 2. Measured directly: latching vblank at M2 rise while the sprite flags stay at dot 2 fixes Hybrid Addresses and advances $2002 Flag Timing and Stale Sprite Shift Regs, but costs the seven vblank/NMI timing tests, because the true offset is fractional. The fix is a master-clock core (CPU /12, PPU /4) rather than three ticks per cycle.

PPU external bus (5 tests). The PPU multiplexes A0-A7 with D0-D7 through an external octal latch strobed by ALE. A $2007 access during rendering collides with the fetch cadence, and when ALE and /RD assert together the latch feeds back on itself. Blocks ALE + Read, Internal Data Bus, BG Serial In and the $2004/$2007 stress tests. Needs PPU memory access restructured into a per-dot bus state machine.

DMC request stages (3 tests) and APU detail (3 tests). The abort answer key needs four distinguishable request stages; this core has two. Current output is 04 04 04 04 04 04 02 04 00 00 ... against an expected 04 04 04 04 04 04 03 04 01 01 ....

Older notes: Roughly two thirds need modelling of the PPU's external pins and internal buses, which this core does not have: ALE + Read, Hybrid Addresses, Internal Data Bus, BG Serial In, stale BG and sprite shift registers, the $2004/$2007 stress tests, and OAM corruption from $2003 writes during rendering.

The rest are narrower: three DMC DMA abort/bus-conflict cases (the abort duration answer key needs a finer DMC request state machine than the four stages implemented here), $2002 flag timing (needs vblank latched at M2 rise and the sprite flags at M2 fall — a sub-cycle split that the current fixed strobe placement cannot express without retuning every other PPU test), plus Address $2004 behavior, Frame Counter IRQ check C, DMC check L, and APU Register Activation check 4.

./check.sh reproduces the full breakdown; ./score.sh just prints the score. ./page.sh N lists menu page N; ./runtest.sh PAGE ROW runs a single test and prints every screen it draws.

Performance

~1140 fps headless, about 19x realtime, single threaded, with the fully cycle-accurate CPU, PPU and APU running.

The GPU backend is irrelevant to that number: presenting a frame is one 256x240 texture upload and one quad. The work is the emulation loop:

  • Unity build. The whole core compiles as one translation unit (src/prism.c includes the others), so bus_read, ppu_tick and apu_tick inline into the CPU's instruction loop. This beats cross-file LTO.
  • -O3 -march=native -fomit-frame-pointer, jump-table opcode dispatch, no function pointers in the hot path.
  • Mapper banking via pointer tables, so mapper support costs nothing per access.

Graphics backend

The windowed frontend uses SDL_Renderer, which resolves to the platform's native API: Metal on macOS, D3D11 on Windows, Vulkan/GL on Linux. The binary prints which one it selected at startup (renderer: metal). The core links no graphics library, so a hand-written Metal or Vulkan backend can be dropped in later purely for presentation features (CRT shaders, lower-latency present, HDR) without touching the emulator.

Layout

src/nes.h      public API and all state
src/cpu.c      6502 core, DMA engine
src/ppu.c      2C02, dot accurate
src/apu.c      2A03 audio, DMC DMA requests
src/mapper.c   cartridge mappers and bank tables
src/bus.c      address decoding, cartridge loading, controllers
src/prism.c    unity translation unit + public entry points
src/main.c     SDL frontend, headless runner, PNG writer
tools/trace.c  instruction tracer and PC histogram
check.sh score.sh page.sh runtest.sh   test harnesses

About

an nes emulator made for fun

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages