![icon](https://raw.githubusercontent.com/anddani/nelm/main/assets/elm_logo_8bit.png)

# Nelm

A NES emulator written in Elm.

---

Nelm is designed to be headless so the UI could be adapted to any given frontend.
The abstraction level of a step is on instruction level instead of cycle. The reason is due to simplicity of the implementation as well as
for performance (due to object allocations in Elm).

## How to use

Install the package and import the `NES` module:

```sh
elm install anddani/nelm
```

```elm
import NES exposing (Button(..), ControllerNumber(..), NES)
```

Create a `NES` from the bytes of an iNES ROM (for example picked with
`File.Select` and read with `File.toBytes`):

```elm
case NES.fromRom bytes of
    Ok nes ->
        ( { model | status = Loaded nes }, Cmd.none )

    Err _ ->
        ( { model | status = Error "Failed to parse iNES bytes" }, Cmd.none )
```

Once per animation frame, step until the PPU's frame counter advances:

```elm
stepUntilNextFrame : NES -> NES
stepUntilNextFrame initialNes =
    let
        startFrame =
            NES.frameCount initialNes

        go nes =
            if NES.frameCount nes > startFrame then
                nes

            else
                go (Tuple.first (NES.step nes))
    in
    go initialNes
```

For performance, rendering is best done in JavaScript. Send the framebuffer
out through a port after each frame:

```elm
port renderFramebuffer : Array (Array Int) -> Cmd msg

-- in update, after stepping:
( { model | status = Loaded nextNes }
, renderFramebuffer (NES.framebuffer nextNes)
)
```

On the JavaScript side, copy
[`examples/basic/index.js`](https://github.com/anddani/nelm/blob/main/examples/basic/index.js)
into your project. It subscribes to the `renderFramebuffer` port, maps each
palette index through the NES master palette, and draws the 256×240 frame to
a `<canvas>`.

Controller input is signalled from your keyboard handlers:

```elm
-- press the A button on controller 1
NES.signalControllerButton Controller1 A True nes

-- release it
NES.signalControllerButton Controller1 A False nes
```

A complete runnable app (ROM picker, frame loop capped to NTSC timing,
keyboard controls, pause and frame stepping) is in
[`examples/basic`](https://github.com/anddani/nelm/tree/main/examples/basic).

## Interface

The main interface with the library is:

```elm
{- Creates a new NES from an iNES (.nes) rom bytestream.
-}
fromRom : Bytes -> Result Cartridge.Error NES

{- Advances one instruction of the nes.

Returns a new NES with an advanced Program Counter,
and the number of cycles that were advanced by the
instruction.
-}
step : NES -> ( NES, Int )

{- Used to register a key press.

type ControllerNumber = Controller1 | Controller2

type Button
    = A
    | B
    | Select
    | Start
    | Up
    | Down
    | Left
    | Right

type alias Pressed = Bool

-}
signalControllerButton : ControllerNumber -> Controller.Button -> Pressed -> NES -> NES

{- Get the framebuffer from the PPU.

Used to render a frame on the screen.
-}
framebuffer : NES -> Array (Array Int)

{- Get the frame count for the PPU.

To know when to render the frame, step
instructions until the frame count advances
by one.
-}
frameCount : NES -> Int
```

It is up to the user of this library to step until next frame and choose its own framerate. The reason for doing this
is to allow a user to implement different speeds, save state, and rewinding.

## Limitations

**Cartridges**

- Only **NROM (mapper 0)** is supported. `fromRom` returns an `UnsupportedMapper` error for
  anything else (MMC in progress). CHR-ROM only (no CHR-RAM boards).
- ROMs with a 512-byte trainer are not handled and will fail parsing.
- iNES 1.0 headers only. Mirroring is horizontal/vertical only.

**Audio**

- There is **no APU**: no audio output (WIP).

**Timing and PPU accuracy**

- Stepping is **per instruction, not per cycle**. For performance reasons (not to overwhelm the heap).
- The sprite overflow flag (`$2002` bit 5) is never set.
- NTSC timing only (no PAL); the odd-frame dot skip is not modeled.

## AI disclaimer

**None** of the code in the `src/` directory has been generated using AI. This project built as a learning experience to understand how the NES, and 6502 in particular, works.
I explicitly instructed the coding agent to never suggest any code changes in `src/` and never suggest fixes in code review.
The only way I allowed it to communicate was in written communication.

A coding agent was used to ask clarifying questions, ask for sources (from [Nesdev Wiki](https://www.nesdev.org/wiki/Nesdev_Wiki)),
ask for code review and generating tests in the `tests/` directory, example in `examples/` and a performance benchmark script in `bench/`.
