nescope

    A Nintendo Entertainment System emulator in pure MoonBit: 6502 CPU, PPU, frame-counter APU and the NROM/UxROM/CNROM/MMC1/MMC3 mappers, verified line by line against the nestest golden trace.

    nes
    6502
    emulator
    ppu
    retro
    Download zip
    Author
    Version
    0.1.0
    License
    Apache-2.0
    Last updated
    4 hours ago
    Downloads
    3

    #nescope

    A Nintendo Entertainment System emulator in pure MoonBit.

    The console runs: CPU, PPU, cartridge and the bus that joins them. The whole nestest ROM executes from a real cartridge image and every one of its 8991 instruction traces is compared against nestest.log, the reference trace that ships with the ROM.

    #Why it is structured this way

    A 6502's cost is a property of its addressing mode, not of its operation: LDA $1234 takes 4 cycles and LDA ($12),Y takes 5, and either grows by one when the effective address crosses a page. Putting the cycle count next to the operation is the obvious thing to do and it is wrong — the first version of this code did exactly that and produced a trace that drifted from the golden log by the third line.

    So the module is split three ways:

    addressing.mbt operand decoding; page-crossing penalties are computed here ops.mbt what an instruction does to registers and flags isa.mbt the opcode table; each arm fixes the addressing mode and the cost

    An operation is written once no matter how many modes reach it, and the cycle count is written once per opcode, next to the data-sheet entry it came from.

    #Quick start

    moon test

    Read a real cartridge's header:

    moon run cmd/main -- --inspect internal/fixtures/nestest.nes

    file internal/fixtures/nestest.nes mapper NROM mirroring horizontal program 1 x 16 KiB (4000 bytes) character 1 x 8 KiB (2000 bytes) battery false reset C004

    Boot it and run a frame:

    moon run cmd/main -- --run internal/fixtures/nestest.nes --frames 2 --write-png frame.png

    frames 2 cycles 59567 pc C5F4 a=00 x=00 y=00 sp=FA wrote frame.png (184638 bytes)

    nestest renders nothing but its backdrop, so that frame is a single flat grey. The point of the flag is that any ROM's frame becomes a file you can look at, which is the only way to check rendering without a window system.

    --trace still exists for flat binaries: it loads an image at $C000 and prints one line per instruction in the layout nestest.log uses. Its (?) column is the mnemonic, which this build does not disassemble yet. It is left visibly empty rather than filled with a guess so that a column that disagrees with a reference trace is known to be a state difference and not a formatting one.

    #What works today

    CapabilityStatus
    All 151 official 6502 opcodes with data-sheet cycle countsdone
    Unofficial opcodes: LAX, SAX, SLO, RLA, SRE, RRA, DCP, ISC, ANC, ALR, ARR, SBX, multi-byte NOPsdone
    Page-crossing penalties on abs,X / abs,Y / (zp),Y readsdone
    Branch penalties including the page-crossing extradone
    The JMP ($xxFF) page-boundary bugdone
    BRK / RTI / JSR / RTS stack behaviourdone
    BIT as a two-bit branch on one memory readdone
    Golden-log trace comparison against nestest.logall 8991 lines
    iNES header parsing, including the twelve-bit bank countsdone
    NROM cartridge, 16 KiB mirroring into both halves of the mapdone
    UxROM (mapper 2): switchable low window, last bank fixed at $C000done
    CNROM (mapper 3): switchable character bankdone
    MMC1 (mapper 1): serial shift register, four bank arrangements, run-time mirroringdone
    MMC3 (mapper 4): two switchable 8 KiB windows, 1 KiB character banking, scanline IRQdone
    PPU registers, read buffer, scroll latch, palette mirrorsdone
    PPU: per-pixel background and sprite renderingdone
    Sprite DMA through $4014 with the odd-cycle stalldone
    Controller shift registers in hardware orderdone
    Four-screen mirroring with its own 4 KiB of nametable RAMdone
    Sprite-zero hit: opaque sprite pixel over opaque background pixeldone
    Mappers: MMC5, VRC, and the discrete boards (AxROM, GxROM, MMC2)planned
    APU frame counter: 4-step and 5-step sequences, frame IRQdone
    APU channel status and length counters, as $4015 reports themdone
    APU: waveform synthesis and mixingplanned
    Sprite overflow flagapproximate
    PNG output of a finished frame, written out without a dependencydone

    #How it is tested

    Three kinds of test:

    • Synthetic. Each test builds a few bytes of program in flat RAM and pins one instruction: the cycle count, the flags set, the address written, or the program counter afterwards. A failure names the instruction.
    • Banking. mapper_wbtest.mbt builds synthetic cartridges whose program banks are filled with their own bank number, so a read says which bank answered. That is the only way to tell "bank three loaded" from "bank zero loaded and happened to contain the right bytes".
    • Differential. The real nestest ROM is executed and every line compared with nestest.log — program counter, accumulator, X, Y, status, stack pointer and cumulative cycle count. The log is decimal in its CYC: column and hex everywhere else, which cost a debugging round to notice.

    The fixtures in internal/fixtures are nestest.nes and nestest.log from christopherpow/nes-test-roms. The planned PPU and APU work will use Gekkio/mooneye-test-suite and pinobatch/240p-test-mini.

    #Source layout

    nescope.mbt errors, status flags, buttons, region timings cpu.mbt register file, bus trait, fetch/step, stack addressing.mbt the addressing modes and their page penalties ops.mbt operations: loads, ALU, shifts, compare, branch, jump isa.mbt the opcode table cart.mbt iNES parsing and the cartridge's memories mapper.mbt bank switching: NROM, UxROM, CNROM, MMC1 ppu.mbt registers, rendering, sprites, palette and mirroring png.mbt palette to RGB, and a dependency-free PNG writer apu.mbt the frame counter and channel status registers bus.mbt the console: address decoding, DMA, controllers cmd/main/ the CLI

    #License

    Apache-2.0.

    PaletteIndex

    type PaletteIndex = UInt

    A palette entry: six bits selecting one of the console's 64 colours, plus the emphasis bits that tint the whole screen.

    Bus

    pub(open) trait Bus {
    fn read(self : Self, address : UInt) -> UInt
    fn write(self : Self, address : UInt, value : UInt) -> Unit
    }

    A read/write window the CPU can reach.

    The real machine has no single flat memory; the CPU addresses cartridge ROM, work RAM, and memory-mapped registers that all answer through the bus. Keeping that indirection here is what lets the CPU be tested with a plain RAM array and then dropped into the console unchanged.

    NesError

    pub(all) suberror NesError {
    UnexpectedEnd(offset~ : Int64, needed~ : Int64, available~ : Int64)
    NotAnINes
    BadValue(field~ : String, value~ : Int64)
    Truncated(kind~ : String, declared~ : Int64, present~ : Int64)
    UnsupportedMapper(number~ : UInt)
    UnknownOpcode(opcode~ : UInt, at~ : UInt)
    LockoutChip
    }

    Every way a cartridge image or an emulator request can be rejected.

    Loading never returns a half-filled cartridge: a malformed image surfaces as one of these variants instead of an out-of-range read, and every variant names the field or mapper that failed.
    impl Show for NesError

    NesError::output

    fn NesError::output(self : NesError, logger : &Logger) -> Unit

    Exposes the message as a plain string for tests and CLI output.

    NesError::to_string

    fn NesError::to_string(self : NesError) -> String

    Exposes the message as a plain string for tests and CLI output.

    Apu

    pub(all) struct Apu {
    mode : FrameMode
    irq_inhibit : Bool
    frame_irq : Bool
    cycles : Int64
    step : Int
    length : FixedArray[UInt]
    halt : FixedArray[Bool]
    control : FixedArray[UInt]
    dmc_active : Bool
    enabled_channels : UInt
    }

    The APU.

    Apu::new

    fn Apu::new() -> Apu

    A freshly powered APU.

    Apu::read_status

    fn Apu::read_status(self : Apu) -> UInt

    Reads the status register $4015.

    Bit n is set while channel n still has length left, which is how a game knows a note has finished without tracking the length counter itself. Reading the register also clears the frame IRQ flag, which is the only way to acknowledge it.

    Apu::tick

    fn Apu::tick(self : Apu, cycles : Int64) -> Bool

    Advances the frame counter by a number of CPU cycles.

    Returns true on the cycle the four-step sequence raises its IRQ, so the console can take the interrupt at the right moment rather than polling.

    Apu::write_register

    fn Apu::write_register(self : Apu, address : UInt, value : UInt) -> Unit

    Writes a channel register.

    Only the bits that change the length counter and the frame counter are acted on. The rest — the timer periods, the envelopes — would matter to a mixer and are recorded so a later version can pick them up without changing the register decode.

    Apu::write_status

    fn Apu::write_status(self : Apu, value : UInt) -> Unit

    Writes the status register $4015, which only uses its low five bits: each one enables or disables a channel, and writing a zero clears that channel's length counter so it falls silent at once.

    Button

    pub(all) enum Button {
    A
    B
    Select
    Start
    Up
    Down
    Left
    Right
    }

    The eight buttons a standard NES controller reports.

    Button::shift_index

    fn Button::shift_index(self : Button) -> Int

    The position of a button in the controller's serial shift order.

    The order is fixed by the hardware's shift register and is the order every ROM's input code assumes: A first, then B, then the two selects, then the d-pad from up to right.

    Cart

    pub(all) struct Cart {
    mirroring : Mirroring
    has_battery : Bool
    prg : Bytes
    chr : FixedArray[Byte]
    chr_ram : Bool
    mapper : MapperKind
    prg_banks : Int64
    chr_banks : Int64
    prg_bank : Int64
    prg_bank_high : Int64
    chr_bank : Int64
    shift : UInt
    shift_count : UInt
    control : UInt
    mirroring_from_mapper : Bool
    bank_registers : FixedArray[Int64]
    bank_select : UInt
    prg_mode : Bool
    chr_mode : Bool
    irq_counter : UInt
    irq_latch : UInt
    irq_line : Bool
    irq_enabled : Bool
    }

    A parsed cartridge.

    Cart::chr_byte

    fn Cart::chr_byte(self : Cart, offset : Int64) -> UInt

    Reads a character-ROM byte at a cartridge-relative offset.

    A CHR-RAM cartridge hands this function an empty array; the PPU owns the RAM and answers pattern fetches itself before reaching here.

    Cart::parse

    fn Cart::parse(bytes : Bytes) -> Cart raise NesError

    Parses an iNES image.

    The 16-byte header is validated in two stages: the magic and the declared sizes must be consistent with the file, and then the mapper must be one this build can wire. A bad header is an error; an unknown mapper is not, because the cartridge's identity is still worth reporting.

    Cart::prg_byte

    fn Cart::prg_byte(self : Cart, offset : Int64) -> UInt

    Reads a program-ROM byte at a cartridge-relative offset.

    Returns zero past the end rather than raising: a ROM is allowed to be smaller than the address space the CPU sees, and the unpopulated region of a NROM board reads back as open bus, which games treat as harmless.

    Cart::write_chr

    fn Cart::write_chr(self : Cart, offset : Int64, value : UInt) -> Bool

    Writes a character-ROM byte, which only a CHR-RAM cartridge accepts.

    Returns true when the write landed, so the caller knows whether the PPU should have handled it instead.

    Console

    pub(all) struct Console {
    cpu : Cpu
    ppu : Ppu
    cart : Cart
    ram : FixedArray[Byte]
    controllers : (UInt, UInt)
    controller_shift : (Int, Int)
    dma_cycles : Int64
    apu : Apu
    irq_pending : Bool
    }

    The whole console: CPU, PPU, cartridge and the memories they share.
    impl Bus for Console

    Console::irq

    fn Console::irq(self : Console) -> Unit

    Takes a maskable interrupt through the vector at $FFFE.

    The difference from NMI is more than the vector. Panning the pushed status: the hardware clears the break bit for both, but a maskable interrupt is also suppressed while the interrupt-disable flag is set, and an NMI is not. That distinction is what makes SEI a usable critical section.

    Console::new

    fn Console::new(cart : Cart) -> Console

    A console with a freshly reset CPU and PPU and the given cartridge.

    Console::nmi

    fn Console::nmi(self : Console) -> Unit

    Services an NMI by pushing the return address and status and vectoring through $FFFA, which is what the hardware does when the PPU raises the line.

    Console::read

    fn Console::read(self : Console, address : UInt) -> UInt

    Reads a byte from the CPU's address space.

    Each region is handled explicitly rather than by subtraction from a base, because the regions do not share a stride: work RAM repeats every 2 KiB while the PPU registers repeat every 8 bytes, and a single formula would have to special-case one of them anyway.

    Console::reset

    fn Console::reset(self : Console) -> Unit

    Resets the console as the hardware does: the CPU reads its program counter from the vector at $FFFC and the stack pointer starts at $FD.

    Console::run_frame

    fn Console::run_frame(self : Console, limit : Int) -> Int raise NesError

    Runs until the PPU completes a frame, returning the number of CPU instructions executed.

    Bounded so that a ROM stuck in a loop cannot hang a test: the limit is a few frames' worth of instructions, and hitting it returns the count rather than spinning forever.

    Console::set_buttons

    fn Console::set_buttons(self : Console, port : Int, buttons : Array[Button]) -> Unit

    Sets the buttons held on a controller.

    Console::step

    fn Console::step(self : Console) -> Bool raise NesError

    Runs one instruction, advancing the PPU by the three dots per CPU cycle the hardware uses.

    Returns true when the instruction's final cycle raised the PPU's vblank and the control register has NMI enabled, so the caller can service the interrupt.

    Console::write

    fn Console::write(self : Console, address : UInt, value : UInt) -> Unit

    Writes a byte to the CPU's address space.

    Cpu

    pub(all) struct Cpu {
    a : UInt
    x : UInt
    y : UInt
    sp : UInt
    pc : UInt
    p : UInt
    cycles : Int64
    stalled : Int64
    last_access : MemoryAccess?
    }

    A 6502 register file plus the memory it addresses.

    Memory is the console's 64 KiB bus, not the CPU's own store: reads and writes go through Bus so that the PPU, APU and cartridge can intercept the addresses the hardware routes to them.

    Cpu::absolute

    fn Cpu::absolute(self : Cpu, bus : &Bus) -> UInt

    A 16-bit absolute address.

    Cpu::absolute_x

    fn Cpu::absolute_x(self : Cpu, bus : &Bus) -> UInt

    Absolute address plus X, without reporting a page crossing.

    Used by the addressing modes that always take five cycles: a store cannot profit from the crossed page being already open, so the penalty is part of its fixed cost.

    Cpu::absolute_x_page

    fn Cpu::absolute_x_page(self : Cpu, bus : &Bus) -> (UInt, Int64)

    Absolute address plus X, reporting one extra cycle when the addition carries out of the low byte.

    Cpu::absolute_y

    fn Cpu::absolute_y(self : Cpu, bus : &Bus) -> UInt

    Absolute address plus Y, without reporting a page crossing.

    Cpu::absolute_y_page

    fn Cpu::absolute_y_page(self : Cpu, bus : &Bus) -> (UInt, Int64)

    Absolute address plus Y, reporting one extra cycle when the addition carries out of the low byte.

    Cpu::adc

    fn Cpu::adc(self : Cpu, value : UInt) -> Unit

    ADC: add with carry.

    The 2A03 has no decimal adder, so the decimal flag only suppresses the binary result's meaning on real hardware; the arithmetic performed is always binary. The overflow flag is set from the sign bits of the operands rather than from the result's carry, because carries out of bit 6 and bit 7 disagree in the overflow case.

    Cpu::and_

    fn Cpu::and_(self : Cpu, value : UInt) -> Unit

    AND with the accumulator.

    Cpu::asl

    fn Cpu::asl(self : Cpu, value : UInt) -> (UInt, UInt)

    Arithmetic shift left: bit 7 moves into carry, bit 0 becomes zero.

    Cpu::bit

    fn Cpu::bit(self : Cpu, value : UInt) -> Unit

    BIT: tests bits of a memory value against the accumulator.

    The zero flag is set from the AND, but the negative and overflow flags are copied straight from bits 7 and 6 of the operand, which is why BIT can be used as a two-bit branch on a single memory read.

    Cpu::branch

    fn Cpu::branch(self : Cpu, bus : &Bus, taken : Bool) -> Int64

    A taken branch: adds the signed displacement and one cycle, plus another when the branch crosses into a new page.

    Cpu::brk

    fn Cpu::brk(self : Cpu, bus : &Bus) -> Int64

    BRK: pushes the return address plus two, the break flag and the status, then vectors through the IRQ vector with the interrupt mask set.

    Cpu::compare

    fn Cpu::compare(self : Cpu, register : UInt, value : UInt) -> Unit

    Compares a register with a value, setting carry when the register is at least the value.

    Cpu::dec

    fn Cpu::dec(self : Cpu, value : UInt) -> UInt

    Decrements a value without touching memory.

    Cpu::eor

    fn Cpu::eor(self : Cpu, value : UInt) -> Unit

    EOR with the accumulator.

    Cpu::flag

    fn Cpu::flag(self : Cpu, flag : Flag) -> Bool

    Reads a status flag.

    Cpu::immediate

    fn Cpu::immediate(self : Cpu, bus : &Bus) -> UInt

    The one-byte operand of an immediate instruction.

    Returns the operand value rather than an address, since immediate operands live in the instruction stream.

    Cpu::inc

    fn Cpu::inc(self : Cpu, value : UInt) -> UInt

    Increments a value without touching memory.

    Cpu::indexed_indirect

    fn Cpu::indexed_indirect(self : Cpu, bus : &Bus) -> UInt

    The (zp,X) mode: the operand is a zero-page address, X is added to it with wraparound, and the two bytes there form the effective address.

    The zero-page wraparound is not a bug to be fixed: LDA ($FF,X) reads its pointer from $FF and $00.

    Cpu::indirect_indexed

    fn Cpu::indirect_indexed(self : Cpu, bus : &Bus) -> UInt

    The (zp),Y mode without a page-crossing report.

    Cpu::indirect_indexed_page

    fn Cpu::indirect_indexed_page(self : Cpu, bus : &Bus) -> (UInt, Int64)

    The (zp),Y mode reporting one extra cycle when Y carries out of the low byte of the pointer.

    Cpu::jmp_indirect

    fn Cpu::jmp_indirect(self : Cpu, bus : &Bus) -> Int64

    JMP ($addr): the 6502's indirect jump does not carry into the high byte of the pointer, so a pointer at $xxFF reads its high byte from $xx00.

    Reproducing the bug matters because real ROMs jump through tables placed to avoid it, and because nestest checks it.

    Cpu::lda

    fn Cpu::lda(self : Cpu, value : UInt) -> Unit

    Loads the accumulator.

    Cpu::ldx

    fn Cpu::ldx(self : Cpu, value : UInt) -> Unit

    Loads X.

    Cpu::ldy

    fn Cpu::ldy(self : Cpu, value : UInt) -> Unit

    Loads Y.

    Cpu::lsr

    fn Cpu::lsr(self : Cpu, value : UInt) -> (UInt, UInt)

    Logical shift right: bit 0 moves into carry, bit 7 becomes zero.

    Cpu::new

    fn Cpu::new() -> Cpu

    A CPU with every register zeroed and the interrupt line masked, which is the state nestest is written to observe.

    Cpu::ora

    fn Cpu::ora(self : Cpu, value : UInt) -> Unit

    ORA with the accumulator.

    Cpu::rol

    fn Cpu::rol(self : Cpu, value : UInt) -> (UInt, UInt)

    Rotate left through carry.

    Cpu::ror

    fn Cpu::ror(self : Cpu, value : UInt) -> (UInt, UInt)

    Rotate right through carry.

    Cpu::sbc

    fn Cpu::sbc(self : Cpu, value : UInt) -> Unit

    SBC: subtract with borrow, which on the 6502 is ADC of the complement.

    Cpu::set_flag

    fn Cpu::set_flag(self : Cpu, flag : Flag, value : Bool) -> Unit

    Sets or clears a status flag.

    Cpu::set_zn

    fn Cpu::set_zn(self : Cpu, value : UInt) -> Unit

    Sets or clears the zero and negative flags from an 8-bit result.

    Every instruction that produces a value calls this once, which is why the two flags never drift: there is exactly one place that touches them.

    Cpu::step

    fn Cpu::step(self : Cpu, bus : &Bus) -> StepResult raise NesError

    Executes one instruction and returns what it did.

    bus is mutable through its trait object: the CPU is the only component that drives the address lines, so everything else observes memory through the effects of this call.

    Cpu::store

    fn Cpu::store(self : Cpu, bus : &Bus, address : UInt, value : UInt) -> Unit

    Stores the accumulator, recording the write for the trace column.

    Cpu::zeropage

    fn Cpu::zeropage(self : Cpu, bus : &Bus) -> UInt

    Zero-page address, always below $0100.

    Cpu::zeropage_x

    fn Cpu::zeropage_x(self : Cpu, bus : &Bus) -> UInt

    Zero-page address plus X, wrapping inside the page.

    Cpu::zeropage_y

    fn Cpu::zeropage_y(self : Cpu, bus : &Bus) -> UInt

    Zero-page address plus Y, wrapping inside the page.

    Ctrl

    pub(all) struct Ctrl {
    nametable : UInt
    increment32 : Bool
    sprite_pattern : UInt
    background_pattern : UInt
    tall_sprites : Bool
    nmi_enabled : Bool
    }

    The PPU's control register at $2000.

    Held as named fields rather than a raw byte so that a test can set the nametable select without also having to know which bits the other fields use.

    Flag

    pub(all) enum Flag {
    Carry
    Zero
    Interrupt
    Decimal
    Break
    Unused
    Overflow
    Negative
    }

    The 6502 status register, one constructor per bit.

    Kept as an enum rather than raw bit numbers so that the instruction implementations read the way the data sheet does; the bit position is only materialised when the register is pushed to or pulled from the stack.

    Flag::bit

    fn Flag::bit(self : Flag) -> Int

    The bit position of a flag inside the packed status register.

    Flag::mask

    fn Flag::mask(self : Flag) -> UInt

    The mask that isolates a flag inside the packed status register.

    FlatRam

    pub(all) struct FlatRam {
    bytes : FixedArray[Byte]
    }

    A flat 64 KiB of RAM, used by tests and by the CPU-only trace runner.

    It is deliberately not the console's bus: nestest executes with the cartridge mapped at $C000 and plain RAM everywhere else, which is exactly what this provides.
    impl Bus for FlatRam

    FlatRam::load

    fn FlatRam::load(self : FlatRam, base : UInt, image : Bytes) -> Unit

    Copies an image into RAM starting at base.

    Used to place a test ROM's code where the real console's mapper would have put it, without pulling in a cartridge.

    FlatRam::new

    fn FlatRam::new() -> FlatRam

    Allocates 64 KiB of zeroed RAM.

    FlatRam::read

    fn FlatRam::read(self : FlatRam, address : UInt) -> UInt

    Reaches the Bus methods through the trait only, so that a bare ram.read is not silently available.

    FlatRam::write

    fn FlatRam::write(self : FlatRam, address : UInt, value : UInt) -> Unit

    Reaches the Bus methods through the trait only, so that a bare ram.read is not silently available.

    FrameMode

    pub(all) enum FrameMode {
    FourStep
    FiveStep
    }

    Which of the frame counter's two sequences is running.

    FrameMode::name

    fn FrameMode::name(self : FrameMode) -> String

    The name of the sequence, used in reports and tests.

    FrameStep

    pub(all) enum FrameStep {
    Idle
    Quarter
    Half
    Both
    }

    The frame counter's step within its sequence.

    The sequence's position is exposed because the quarter- and half-frame clocks drive the length counters, which $4015 reports.

    MapperKind

    pub(all) enum MapperKind {
    Nrom
    UxRom
    Cnrom
    Mmc1
    Mmc3
    Unsupported(UInt)
    }

    The mappers this build implements.

    The set is chosen by how much of the licensed library each one unlocks per line of code rather than by number: NROM is the boards with no banking at all, UxROM and CNROM are single-register boards, and MMC1 is a shift register that most of the early library uses.

    MapperKind::name

    fn MapperKind::name(self : MapperKind) -> String

    A short name for the mapper, used in reports.

    Mask

    pub(all) struct Mask {
    show_background_left : Bool
    show_sprites_left : Bool
    show_background : Bool
    show_sprites : Bool
    emphasize_red : Bool
    emphasize_green : Bool
    emphasize_blue : Bool
    greyscale : Bool
    }

    The PPU's mask register at $2001.

    MemoryAccess

    pub(all) struct MemoryAccess {
    address : UInt
    value : UInt
    write : Bool
    }

    A memory location an instruction touched, with the value after the access.

    Mirroring

    pub(all) enum Mirroring {
    Horizontal
    Vertical
    SingleLow
    SingleHigh
    FourScreen
    }

    Which set of nametables the PPU sees.

    The console has 2 KiB of video RAM for nametables, but the PPU can address four 1 KiB tables; the cartridge decides whether the two physical ones appear twice, or are replaced by extra RAM on the cartridge.

    Mirroring::name

    fn Mirroring::name(self : Mirroring) -> String

    A short name for the mirroring, used in reports.

    Ppu

    pub(all) struct Ppu {
    ctrl : Ctrl
    mask : Mask
    status : Status
    v : UInt
    t : UInt
    fine_x : UInt
    write_latch : Bool
    nt_ram : FixedArray[Byte]
    palette : FixedArray[Byte]
    oam : FixedArray[Byte]
    read_buffer : UInt
    scanline : Int
    dot : Int
    frame : FixedArray[Byte]
    frame_complete : Bool
    mirroring : Mirroring
    chr : FixedArray[Byte]
    chr_ram : Bool
    scanlines : UInt
    chr_bank : Int64
    chr_pages : FixedArray[Int64]?
    oam_addr : UInt
    }

    A complete PPU.

    The two 16-bit address latches are the source of most of the PPU's awkwardness:

    • v is the address the PPU renders from; it walks across the nametable as dots are emitted.
    • t is the address a scroll write stages a value into.
    • fine_x is the sub-tile horizontal scroll, which cannot be represented in t and so is kept separately.

    The $2006/$2005 write protocol is what moves values into and out of them, and getting it wrong produces a picture that is subtly shifted rather than obviously broken.

    Ppu::new

    fn Ppu::new() -> Ppu

    A PPU with every register at its power-on value.

    ctrl starts at zero, which selects nametable 0 and 8x8 sprites; mask starts at zero, so nothing renders until a game enables it. That is what makes a game's first frame black rather than random.

    Ppu::read_register

    fn Ppu::read_register(self : Ppu, address : UInt) -> UInt

    Reads one of the PPU's eight registers.

    Only $2002 returns a value directly; the others either return the read buffer's contents or return nothing a program should rely on. Register reads also have side effects — reading the status clears the vblank flag and the write latch — which is why they cannot be modelled as pure getters.

    Ppu::rendering_enabled

    fn Ppu::rendering_enabled(self : Ppu) -> Bool

    True when either layer is on, which is when the hardware advances the rendering address.

    Ppu::set_palette_byte

    fn Ppu::set_palette_byte(self : Ppu, index : UInt, value : UInt) -> Unit

    Writes one byte of palette RAM through the same mirrors.

    Ppu::tick

    fn Ppu::tick(self : Ppu) -> Bool

    Advances the raster by one dot and renders the pixel it lands on.

    Returns true on the dot that starts vertical blank, so that the caller can raise an NMI without polling. The scanline counters follow the roster the hardware uses: an NTSC frame is 262 lines of 341 dots, and the visible picture ends at line 240.

    Ppu::write_register

    fn Ppu::write_register(self : Ppu, address : UInt, value : UInt) -> Unit

    Writes one of the PPU's eight registers.

    Region

    pub(all) enum Region {
    NTSC
    PAL
    Dendy
    }

    The console region, which decides the CPU and PPU clock rates.

    The rates differ enough that a PAL ROM run at NTSC speed drifts, so the rate is carried in the type rather than assumed.

    Region::cpu_hz

    fn Region::cpu_hz(self : Region) -> Int64

    CPU clock rate in hertz for the region.

    Region::dots_per_scanline

    fn Region::dots_per_scanline(self : Region) -> Int

    PPU dots per scanline for the region.

    Region::scanlines_per_frame

    fn Region::scanlines_per_frame(self : Region) -> Int

    PPU scanlines per frame for the region, including the vertical blanking lines the PPU does not render.

    Status

    pub(all) struct Status {
    vblank : Bool
    sprite_zero_hit : Bool
    overflow : Bool
    }

    The PPU's status register at $2002.

    StepResult

    pub(all) struct StepResult {
    cycles : Int64
    opcode : UInt
    at : UInt
    memory : MemoryAccess?
    }

    The result of one instruction: how many cycles it consumed and whether it touched memory in a way the caller should know about.

    SCREEN_HEIGHT

    let SCREEN_HEIGHT : Int

    The height of the visible picture in pixels.

    SCREEN_WIDTH

    let SCREEN_WIDTH : Int

    The width of the visible picture in pixels.

    encode_png

    fn encode_png(frame : FixedArray[Byte]) -> Bytes

    Encodes a frame of palette indices as a PNG file.

    frame is the PPU's 256x240 buffer: one palette index per pixel, row major. The result is a complete PNG file, which is what makes it possible to look at what the emulator drew without a window system.

    hex2

    fn hex2(value : UInt) -> String

    Formats a byte as $ plus two lowercase hex digits, the width the 6502 disassembly column uses.

    hex4

    fn hex4(value : UInt) -> String

    Formats a 16-bit value as $ plus four lowercase hex digits, the width of a 6502 address.

    hex8

    fn hex8(value : UInt) -> String

    Formats a 32-bit value as $ plus eight lowercase hex digits.

    nes_colour

    fn nes_colour(index : UInt) -> (UInt, UInt, UInt)

    The RGB triple the NES palette gives for a palette index.

    Indices are six bits wide, so the table always answers; the mask is there because a frame buffer that has been written by a buggy renderer could hold anything and reading past the end would panic rather than produce a wrong colour.

    peek

    fn peek(bus : &Bus, address : UInt) -> UInt

    Fetches a byte without touching memory, used by the disassembler and the trace runner to render the operand bytes of an instruction that has not executed yet.