cge_nes 0.1.2

Cycle-accurate NES (Nintendo Entertainment System) emulator library: CPU, PPU, cartridge, input, and iNES ROM loading.
Documentation
//! High-level PPU register read/write handling.
//!
//! Methods on `Ppu` for reading and writing memory-mapped PPU registers by
//! address, performing register-specific side-effects (OAM access, DMA, etc.).
use crate::ppu::{Ppu, PpuCartMemorySpace, Register};

#[cfg(feature = "show_name_table_change")]
use crate::ppu::ScanlineEvent;

impl Ppu {
    /// Writes a value to a PPU register, using the register's address.
    ///
    /// # Parameters
    /// * `value` - The byte value to write to the register
    /// * `addr` - The address of the PPU register (0x2000-0x2007)
    /// * `memory_space` - The PPU memory space implementation for cart/VRAM access
    pub fn write_ppu_register_by_addr(
        &mut self,
        value: u8,
        addr: u16,
        memory_space: &mut impl PpuCartMemorySpace,
    ) {
        let selected_reg = addr.into();
        self.write_ppu_register(value, selected_reg, memory_space);
    }

    /// Writes a value to the specified PPU register.
    ///
    /// # Parameters
    /// * `value` - The byte value to write to the register
    /// * `selected_reg` - The specific PPU register to write to
    /// * `memory_space` - The PPU memory space implementation for cart/VRAM access
    pub fn write_ppu_register(
        &mut self,
        value: u8,
        selected_reg: Register,
        memory_space: &mut impl PpuCartMemorySpace,
    ) {
        // Handle OAM data writes directly to OAM memory
        match selected_reg {
            Register::OamData => {
                let oam_addr = self.regs.oam_addr();
                self.oam.write_data(oam_addr, value);
            }
            _ => (),
        }

        // Compute the VRAM address for memory-mapped operations
        let vram_addr = self.regs.vram_addr() & 0x3FFF;
        let was_second_byte =
            matches!(selected_reg, Register::PpuAddr) && self.regs.next_write_is_second_byte();
        // Write the value to the register (updates internal state)
        self.regs.write_register(value, selected_reg);

        // When the second byte of $2006 (PPUADDR) is written, the full
        // 14-bit VRAM address is committed to v. At that point, pre-load
        // the read buffer with the byte at the new address so the very
        // first PPUDATA read returns a meaningful value
        if was_second_byte {
            let committed = self.regs.vram_addr() & 0x3FFF;
            let effective = if (0x3F00..=0x3FFF).contains(&committed) {
                committed - 0x1000
            } else {
                committed
            };
            self.read_buffer = memory_space.read(effective);
        }

        match selected_reg {
            Register::Data => {
                // Handle VRAM and palette writes based on address range
                match vram_addr {
                    0x0000..=0x2FFF => memory_space.write(value, vram_addr), // Pattern tables & nametables
                    0x3000..=0x3EFF => {
                        let vram_addr = vram_addr - 0x1000; // Mirror 0x2000..=0x2FFF
                        memory_space.write(value, vram_addr);
                    }
                    0x3F00..=0x3FFF => {
                        let palette_ram_addr = (vram_addr & 0x1F) as usize; // Palette mirroring
                        self.palette.write(palette_ram_addr, value);
                    }
                    _ => unreachable!(),
                }

                // Notify the cartridge of the address transition caused by the
                // auto-increment so mappers with A12-clocked counters (e.g. MMC3)
                // can detect rising edges on bit 12 of the post-increment value.
                notify_a12_change(&self.regs, memory_space, vram_addr);
            }
            Register::PpuMask => {
                // For debugging: record background rendering state if feature enabled
                #[cfg(feature = "show_name_table_change")]
                if self.regs.show_bg_flag() {
                    self.last_event = ScanlineEvent::ShowBg;
                } else {
                    self.last_event = ScanlineEvent::DontShowBg;
                }
            }
            _ => {}
        }
    }

    /// Reads a value from a PPU register, using the register's address.
    ///
    /// # Parameters
    /// * `addr` - The address of the PPU register (0x2000-0x2007)
    /// * `memory_space` - The PPU memory space implementation for cart/VRAM access
    ///
    /// # Returns
    /// The byte value read from the register
    pub fn read_ppu_register_by_addr(
        &mut self,
        addr: u16,
        memory_space: &mut impl PpuCartMemorySpace,
    ) -> u8 {
        let selected_reg = addr.into();
        self.read_ppu_register(selected_reg, memory_space)
    }

    /// Reads a value from the specified PPU register.
    ///
    /// # Parameters
    /// * `selected_reg` - The specific PPU register to read from
    /// * `memory_space` - The PPU memory space implementation for cart/VRAM access
    ///
    /// # Returns
    /// The byte value read from the register
    pub fn read_ppu_register(
        &mut self,
        selected_reg: Register,
        memory_space: &mut impl PpuCartMemorySpace,
    ) -> u8 {
        // Handle OAM data reads directly from OAM memory
        match selected_reg {
            Register::OamData => {
                let oam_addr = self.regs.oam_addr();
                let _oam_data = self.oam.data(oam_addr);
                // Note: Historically, reading $2004 didn't update internal data register the same way as $2007
                // but we follow existing logic here.
                // We might need a setter if we want to keep this.
            }
            _ => (),
        }

        // Compute the VRAM address for memory-mapped operations
        let mut vram_addr = self.regs.vram_addr() & 0x3FFF;
        // Read the value from the register (may be overwritten below)
        let mut value = self.regs.read_register(selected_reg);

        match selected_reg {
            Register::Data => {
                match vram_addr {
                    0x0000..=0x3EFF => {
                        // Mirror addresses >= 0x3000 to 0x2000..=0x2EFF
                        if vram_addr >= 0x3000 {
                            vram_addr = vram_addr - 0x1000;
                        }
                        // Return buffered value, then update buffer from VRAM
                        value = self.read_buffer;
                        self.read_buffer = memory_space.read(vram_addr);
                    }
                    0x3F00..=0x3FFF => {
                        // Palette RAM is not buffered; read directly
                        let palette_ram_addr = (vram_addr & 0x1F) as usize;
                        value = self.palette.read(palette_ram_addr);

                        // Update buffer with mirrored nametable data
                        vram_addr -= 0x1000;
                        self.read_buffer = memory_space.read(vram_addr);
                    }
                    _ => unreachable!(),
                }
                // Store the last read value in the register
                self.regs.set_data(value);

                // Notify the cartridge of the address transition caused by the
                // auto-increment so mappers with A12-clocked counters (e.g. MMC3)
                // can detect rising edges on bit 12 of the post-increment value.
                notify_a12_change(&self.regs, memory_space, vram_addr);
            }
            _ => {}
        };

        value
    }
}

/// If `selected_reg` is `Register::Data`, checks whether bit 12 of the PPU's
/// VRAM address changed after the increment and notifies the cartridge of any
/// rising transition.
fn notify_a12_change(
    regs: &crate::ppu::registers::Registers,
    memory_space: &mut impl PpuCartMemorySpace,
    pre_increment_addr: u16,
) {
    let new_addr = regs.vram_addr() & 0x3FFF;
    let old_a12 = (pre_increment_addr & 0x1000) != 0;
    let new_a12 = (new_addr & 0x1000) != 0;
    if !old_a12 && new_a12 {
        memory_space.notify_addr_change(pre_increment_addr, new_addr);
    }
}