cge_nes 0.1.2

Cycle-accurate NES (Nintendo Entertainment System) emulator library: CPU, PPU, cartridge, input, and iNES ROM loading.
Documentation
//! Registers implementation and helper methods.
//!
//! Default initialization and helper accessors for the PPU's `Registers` struct,
//! including control/mask/status defaults and internal scroll/address helpers.
use crate::ppu::registers::ppu_control::PpuControlFlags;
use crate::ppu::registers::ppu_mask::PpuMaskFlags;
use crate::ppu::registers::ppu_status::PpuStatus;
use crate::ppu::registers::vram_addr::{
    combine_coarse_fine_into_coord, separate_coarse_fine_from_coord,
};
use crate::ppu::registers::Registers;
use crate::ppu::Register;
use ringbuffer::RingBuffer;

impl Default for Registers {
    fn default() -> Self {
        let mut def = Self {
            ppu_ctrl: Default::default(),
            ppu_mask: Default::default(),
            ppu_status: Default::default(),
            oam_addr: 0,
            oam_data: 0,
            data: 0,
            v: 0,
            t: 0,
            x: 0,
            show_bg_buffer: Default::default(),
            show_sprites_buffer: Default::default(),
            write_second: false,
        };

        for _ in 0..def.show_bg_buffer.capacity() {
            def.show_bg_buffer.push(false);
        }

        for _ in 0..def.show_sprites_buffer.capacity() {
            def.show_sprites_buffer.push(false);
        }

        def
    }
}

impl Registers {
    /// Resets all PPU registers to their default values.
    pub fn reset(&mut self) {
        *self = Default::default();
    }

    /// Returns true if the next write to `$2005` or `$2006` will be the
    /// second byte (i.e. the byte that commits the latched address to v).
    /// The PPU uses this to pre-load the VRAM read buffer on the second
    /// write to `$2006` (PPUADDR), matching the behaviour most games and
    /// test ROMs expect.
    pub fn next_write_is_second_byte(&self) -> bool {
        self.write_second
    }

    /// Writes a value to the specified PPU register.
    ///
    /// Handles special cases for each register type including:
    /// * Address/scroll register toggles
    /// * OAM address auto-increment
    /// * Read-only registers
    ///
    /// # Parameters
    /// * `value` - The byte value to write
    /// * `selected_reg` - Which PPU register to write to
    pub fn write_register(&mut self, value: u8, selected_reg: Register) {
        match selected_reg {
            Register::PpuControl => {
                self.ppu_ctrl = PpuControlFlags::from_bits_truncate(value);

                let mut t_components = self.t_addr_components();
                t_components.nametable_index = value & 0b11;
                self.t = t_components.build_vram_address();
            }
            Register::PpuMask => {
                self.ppu_mask = PpuMaskFlags::from_bits_truncate(value);
            }
            Register::PpuStatus => {}
            Register::OamAddr => self.oam_addr = value,
            Register::OamData => {
                self.oam_data = value;
                self.oam_addr = self.oam_addr.wrapping_add(1);
            }
            Register::PpuScroll => {
                let (coarse, fine) = separate_coarse_fine_from_coord(value);

                let mut t_addr_components = self.t_addr_components();

                if !self.write_second {
                    t_addr_components.coarse_x = coarse;
                    self.x = fine;
                } else {
                    t_addr_components.coarse_y = coarse;
                    t_addr_components.fine_y = fine;
                }
                self.t = t_addr_components.build_vram_address();

                self.write_second = !self.write_second;
            }
            Register::PpuAddr => {
                if !self.write_second {
                    let high_byte = ((value & 0x3F) as u16) << 8;
                    let low_byte = self.t & 0xFF;
                    self.t = high_byte | low_byte;
                } else {
                    let high_byte = self.t & 0xFF00;
                    let low_byte = value as u16;
                    self.t = high_byte | low_byte;
                    self.v = self.t;
                }
                self.write_second = !self.write_second;
            }
            Register::Data => {
                self.data = value;
                self.update_addr_after_data_access();
            }
        }
    }

    /// Reads the current value from the specified PPU register.
    ///
    /// Special behaviors:
    /// * Status register clears VBlank flag and address latches
    /// * Data register triggers VRAM address increment
    /// * Some registers return 0 or open bus values
    ///
    /// # Parameters
    /// * `selected_reg` - Which PPU register to read from
    ///
    /// # Returns
    /// The current value of the selected register
    pub fn read_register(&mut self, selected_reg: Register) -> u8 {
        // Don't emulate the latch behaviour
        match selected_reg {
            Register::PpuControl => 0,
            Register::PpuMask => 0,
            Register::PpuStatus => {
                let value = self.ppu_status.bits();
                self.reset_addr_latch();
                self.ppu_status.remove(PpuStatus::VBLANK_STARTED);
                value
            }
            Register::OamAddr => 0,
            Register::OamData => self.oam_data,
            Register::PpuScroll => 0,
            Register::PpuAddr => 0,
            Register::Data => {
                self.update_addr_after_data_access();
                0 // bogus data, ppu needs to access vram
            }
        }
    }

    /// Returns the current scroll position as (x, y) coordinates.
    pub fn scroll(&self) -> (u8, u8) {
        let vram_addr_components = self.t_addr_components();

        let x = combine_coarse_fine_into_coord(vram_addr_components.coarse_x, self.x);
        let y = combine_coarse_fine_into_coord(
            vram_addr_components.coarse_y,
            vram_addr_components.fine_y,
        );

        (x, y)
    }

    pub fn oam_addr(&self) -> u8 {
        self.oam_addr
    }

    #[allow(dead_code)]
    pub fn set_oam_addr(&mut self, addr: u8) {
        self.oam_addr = addr;
    }

    pub fn vram_addr(&self) -> u16 {
        self.v
    }

    pub fn set_data(&mut self, data: u8) {
        self.data = data;
    }

    /// Resets the address and scroll write latches.
    ///
    /// Called after reading the status register or resetting the PPU.
    fn reset_addr_latch(&mut self) {
        self.write_second = false;
    }

    /// Updates the VRAM address after a read/write to the data register.
    ///
    /// Increments by either 1 or 32 based on the VRAM increment mode,
    /// handling address wraparound.
    fn update_addr_after_data_access(&mut self) {
        let increment = if self.ppu_ctrl.contains(PpuControlFlags::INC_MODE) {
            32
        } else {
            1
        };

        // To do: increment during rendering
        self.v = self.v.wrapping_add(increment);
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn increment_vram_addresses_read_mode0() {
        let mut expected_addr = 0u16;

        let mut regs = Registers::default();

        assert_eq!(regs.v, expected_addr);

        for _ in 0..u16::MAX {
            regs.read_register(Register::Data);
            expected_addr = expected_addr.wrapping_add(1);
            assert_eq!(regs.v, expected_addr);
        }
    }

    #[test]
    fn increment_vram_addresses_read_mode1() {
        let mut expected_addr = 0u16;

        let mut regs = Registers::default();
        regs.ppu_ctrl.insert(PpuControlFlags::INC_MODE);

        assert_eq!(regs.v, expected_addr);

        for _ in 0..u16::MAX {
            regs.read_register(Register::Data);
            expected_addr = expected_addr.wrapping_add(32);
            assert_eq!(regs.v, expected_addr);
        }
    }

    #[test]
    fn increment_vram_addresses_write_mode0() {
        let mut expected_addr = 0u16;

        let mut regs = Registers::default();

        assert_eq!(regs.v, expected_addr);

        for _ in 0..u16::MAX {
            regs.write_register(0, Register::Data);
            expected_addr = expected_addr.wrapping_add(1);
            assert_eq!(regs.v, expected_addr);
        }
    }

    #[test]
    fn increment_vram_addresses_write_mode1() {
        let mut expected_addr = 0u16;

        let mut regs = Registers::default();
        regs.ppu_ctrl.insert(PpuControlFlags::INC_MODE);

        assert_eq!(regs.v, expected_addr);

        for _ in 0..u16::MAX {
            regs.write_register(0, Register::Data);
            expected_addr = expected_addr.wrapping_add(32);
            assert_eq!(regs.v, expected_addr);
        }
    }

    fn ppu_addr_high(regs: &Registers) -> u8 {
        (regs.t >> 8) as u8
    }

    fn ppu_addr_low(regs: &Registers) -> u8 {
        regs.t as u8
    }

    #[test]
    fn shared_latch_between_ppuaddr_and_ppuscroll() {
        let mut regs = Registers::default();

        // Start with first write state (write_second = false)
        // Write to PPUADDR once (high byte), this should toggle the latch to second-write
        regs.write_register(0xAB, Register::PpuAddr);
        assert_eq!(ppu_addr_high(&regs), 0xAB & 0x3F);

        // Now write to PPUSCROLL. Because we're on the second write, this must write Y
        regs.write_register(0xCD, Register::PpuScroll);
        assert_eq!(regs.scroll().1, 0xCD);

        // Latch toggled back to first-write, so next PPUSCROLL goes to X
        regs.write_register(0xEF, Register::PpuScroll);
        assert_eq!(regs.scroll().0, 0xEF);

        // Latch toggled to second-write again, so next PPUADDR write should target LOW byte
        regs.write_register(0x12, Register::PpuAddr);
        assert_eq!(ppu_addr_low(&regs), 0x12);

        // Reading PPUSTATUS should reset latch to first-write
        let _ = regs.read_register(Register::PpuStatus);

        // After reset, next PPUSCROLL write must target X again
        regs.write_register(0x34, Register::PpuScroll);
        assert_eq!(regs.scroll().0, 0x34);

        // Reading PPUSTATUS should reset latch to first-write
        let _ = regs.read_register(Register::PpuStatus);

        // After reset, next PPUSCROLL write must target X again
        regs.write_register(0x36, Register::PpuScroll);
        assert_eq!(regs.scroll().0, 0x36);
    }

    #[test]
    fn reset_addr_latch() {
        let mut regs = Registers::default();

        regs.write_register(0xCA, Register::PpuAddr);
        regs.write_register(0xFE, Register::PpuAddr);

        assert_eq!(regs.t, 0xCAFE & 0x3FFF);

        regs.write_register(0xFE, Register::PpuAddr);
        regs.write_register(0xCA, Register::PpuAddr);

        assert_eq!(regs.t, 0xFECA & 0x3FFF);

        regs.write_register(0xFE, Register::PpuAddr);
        regs.reset_addr_latch();
        regs.write_register(0xCA, Register::PpuAddr);

        assert_eq!(regs.t, 0xCACA & 0x3FFF);
    }

    #[test]
    fn reset_addr_latch_scroll() {
        let mut regs = Registers::default();

        regs.write_register(0xCA, Register::PpuScroll);
        regs.write_register(0xFE, Register::PpuScroll);

        assert_eq!(regs.scroll().0, 0xCA);
        assert_eq!(regs.scroll().1, 0xFE);

        regs.write_register(0xFE, Register::PpuScroll);
        regs.write_register(0xCA, Register::PpuScroll);

        assert_eq!(regs.scroll().0, 0xFE);
        assert_eq!(regs.scroll().1, 0xCA);

        regs.write_register(0xFE, Register::PpuScroll);
        regs.reset_addr_latch();
        regs.write_register(0xCA, Register::PpuScroll);

        assert_eq!(regs.scroll().0, 0xCA);
        assert_eq!(regs.scroll().1, 0xCA);
    }

    #[test]
    fn increment_oam_address_write() {
        let mut regs = Registers::default();

        let mut expected_addr = 0u8;

        assert_eq!(regs.oam_addr, expected_addr);

        for _ in 0..u8::MAX {
            regs.write_register(0, Register::OamData);
            expected_addr = expected_addr.wrapping_add(1);
            assert_eq!(regs.oam_addr, expected_addr);
        }
    }

    #[test]
    fn dont_increment_oam_address_read() {
        let mut regs = Registers::default();
        let mut expected_addr = 0u8;
        assert_eq!(regs.oam_addr, expected_addr);

        regs.read_register(Register::OamData);
        assert_eq!(regs.oam_addr, expected_addr);

        expected_addr = 0xCA;
        regs.write_register(expected_addr, Register::OamAddr);
        assert_eq!(regs.oam_addr, expected_addr);

        regs.read_register(Register::OamData);
        assert_eq!(regs.oam_addr, expected_addr);
    }
}