NES Emulator Banner

Emulating vintage game consoles presents unique architectural challenges, but implementing an NES emulator in Haskell—a purely functional language—pushes the boundaries of conventional emulator design. Developer Arthur Chaudron recently chronicled this experiment in building FuNes, exploring whether functional programming aids or obstructs hardware simulation while achieving playable frame rates.

The NES Architecture: Interconnected State Machines

The NES comprises tightly coupled components: a 6502-derived CPU handling logic, the Picture Processing Unit (PPU) rendering graphics, an Audio Processing Unit (APU), and cartridge/controller interfaces. These communicate via a shared memory bus, where reads/writes to specific addresses trigger component interactions. Crucially, operations synchronize through cycle-accurate timing; PPU OAM DMA transfers halt the CPU, while interrupts redirect execution flow.

Article illustration 2

Simplified NES component architecture (Credit: Arthur Chaudron)

Functional Modeling: State, Lenses, and Effects

Each processing unit was modeled as state-transforming computations:

data CPUState = MkC { _registerA :: Byte, _registerX :: Byte, ... }
type CPU a = State -> SideEffect -> (State, SideEffect, a)

Pure functions updated state, but verbose record updates proved cumbersome. Lenses solved this, enabling concise, imperative-style mutations:

inc :: CPU ()
inc = registerA += 1  -- Lens magic replaces nested lambdas

Cross-component effects (like interrupts) were handled via a dedicated SideEffect state passed through computations, avoiding global variables while preserving purity.

Performance: From Theory to Playable Speeds

Initial implementations suffered sluggish performance due to pervasive state copying. Three optimizations proved critical:

  1. Continuation-Passing Style (CPS): Restructured computations to minimize closure allocations
  2. Strict Evaluation: Enabled via -XStrict and bang patterns to avoid lazy overhead
  3. Concurrent Rendering: Offloaded PPU pixel buffering and audio mixing to dedicated threads using MVar synchronization

Frame rates jumped from unplayable to ~60fps after optimizations, though heavy titles still stress the Haskell runtime.

Validation: Leveraging the Emulator Community

Functional purity simplified unit testing—opcodes could be verified in isolation:

it "Push and Pull Register A" $ do
  withState [0x48, 0xA9, 0x10, 0x68, 0x00] initialState $ \st' ->
    _registerA st' `shouldBe` 1

Community test ROMs like nestest provided cycle-accurate execution traces for end-to-end validation, while AccuracyCoin flagged rendering glitches.

Article illustration 3

AccuracyCoin test suite output (Credit: Arthur Chaudron)

Functional Emulation: Viable but Imperative Underneath

Despite initial skepticism, Haskell proved capable for NES emulation. The functional foundation simplified testing and state modeling, though performance required CPS and strictness pragmas. Ironically, lens-driven state updates made code resemble imperative implementations. As Chaudron notes: "The lack of low-level memory management sometimes made me regret not using C or Rust"—yet the project demonstrates Haskell's flexibility for unconventional domains when optimized judiciously.


Source: Writing an NES Emulator in Haskell by Arthur Chaudron (CC BY 4.0)