From daaed268f48e9fbfce037246e45a7a1360dd2d69 Mon Sep 17 00:00:00 2001 From: Hikari Date: Sat, 7 Dec 2024 14:35:28 +0100 Subject: [PATCH] [RUW-13] Implement VRAM mock and nametables (#8) * Added VRAM Nametables * Fix formatting issues * [RUW-11] Added Palette RAM * [RUW-11] Added Palette RAM * Fixed too long eq() function --- emulator/src/lib.rs | 2 +- emulator/src/logging/nes_logging.rs | 7 +- emulator/src/mirroring.rs | 25 ++ emulator/src/ppu/mod.rs | 2 + emulator/src/ppu/palette_ram/mod.rs | 1 + emulator/src/ppu/palette_ram/palette_ram.rs | 267 ++++++++++++++++++++ emulator/src/{ => ppu}/vram/mod.rs | 0 emulator/src/ppu/vram/vram.rs | 171 +++++++++++++ emulator/src/vram/vram.rs | 36 --- emulator/tests/int_ppu.rs | 32 ++- 10 files changed, 501 insertions(+), 42 deletions(-) create mode 100644 emulator/src/mirroring.rs create mode 100644 emulator/src/ppu/palette_ram/mod.rs create mode 100644 emulator/src/ppu/palette_ram/palette_ram.rs rename emulator/src/{ => ppu}/vram/mod.rs (100%) create mode 100644 emulator/src/ppu/vram/vram.rs delete mode 100644 emulator/src/vram/vram.rs diff --git a/emulator/src/lib.rs b/emulator/src/lib.rs index 0b65967..96d6b1c 100644 --- a/emulator/src/lib.rs +++ b/emulator/src/lib.rs @@ -9,7 +9,7 @@ mod file_loader; mod i_nes; pub mod logging; pub mod memory; +mod mirroring; mod nes_2; pub mod ppu; mod prg_rom; -pub mod vram; diff --git a/emulator/src/logging/nes_logging.rs b/emulator/src/logging/nes_logging.rs index ac4b58f..209d0a2 100644 --- a/emulator/src/logging/nes_logging.rs +++ b/emulator/src/logging/nes_logging.rs @@ -30,5 +30,10 @@ pub fn init_logging() { ) .unwrap(); - log4rs::init_config(config).unwrap(); + match log4rs::init_config(config) { + Ok(_) => (), + Err(e) => { + panic!("Error initializing log4rs: {}", e); + } + } } diff --git a/emulator/src/mirroring.rs b/emulator/src/mirroring.rs new file mode 100644 index 0000000..fe9080d --- /dev/null +++ b/emulator/src/mirroring.rs @@ -0,0 +1,25 @@ +use std::fmt::Debug; + +pub enum Mirroring { + Horizontal, + Vertical, +} + +impl PartialEq for Mirroring { + fn eq(&self, other: &Self) -> bool { + matches!( + (self, other), + (Mirroring::Horizontal, Mirroring::Horizontal) + | (Mirroring::Vertical, Mirroring::Vertical) + ) + } +} + +impl Debug for Mirroring { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Mirroring::Horizontal => write!(f, "Mirroring::Horizontal"), + Mirroring::Vertical => write!(f, "Mirroring::Vertical"), + } + } +} diff --git a/emulator/src/ppu/mod.rs b/emulator/src/ppu/mod.rs index 0aec22c..2f208c6 100644 --- a/emulator/src/ppu/mod.rs +++ b/emulator/src/ppu/mod.rs @@ -1,2 +1,4 @@ +pub mod palette_ram; pub mod ppu; mod registers; +pub mod vram; diff --git a/emulator/src/ppu/palette_ram/mod.rs b/emulator/src/ppu/palette_ram/mod.rs new file mode 100644 index 0000000..d7c6a89 --- /dev/null +++ b/emulator/src/ppu/palette_ram/mod.rs @@ -0,0 +1 @@ +pub mod palette_ram; diff --git a/emulator/src/ppu/palette_ram/palette_ram.rs b/emulator/src/ppu/palette_ram/palette_ram.rs new file mode 100644 index 0000000..052ec6c --- /dev/null +++ b/emulator/src/ppu/palette_ram/palette_ram.rs @@ -0,0 +1,267 @@ +use crate::addressing::Addressable; +use log::{debug, info}; +use std::fmt::Debug; + +pub static SYSTEM_PALETTE: [(u8, u8, u8); 64] = [ + (0x80, 0x80, 0x80), + (0x00, 0x3D, 0xA6), + (0x00, 0x12, 0xB0), + (0x44, 0x00, 0x96), + (0xA1, 0x00, 0x5E), + (0xC7, 0x00, 0x28), + (0xBA, 0x06, 0x00), + (0x8C, 0x17, 0x00), + (0x5C, 0x2F, 0x00), + (0x10, 0x45, 0x00), + (0x05, 0x4A, 0x00), + (0x00, 0x47, 0x2E), + (0x00, 0x41, 0x66), + (0x00, 0x00, 0x00), + (0x05, 0x05, 0x05), + (0x05, 0x05, 0x05), + (0xC7, 0xC7, 0xC7), + (0x00, 0x77, 0xFF), + (0x21, 0x55, 0xFF), + (0x82, 0x37, 0xFA), + (0xEB, 0x2F, 0xB5), + (0xFF, 0x29, 0x50), + (0xFF, 0x22, 0x00), + (0xD6, 0x32, 0x00), + (0xC4, 0x62, 0x00), + (0x35, 0x80, 0x00), + (0x05, 0x8F, 0x00), + (0x00, 0x8A, 0x55), + (0x00, 0x99, 0xCC), + (0x21, 0x21, 0x21), + (0x09, 0x09, 0x09), + (0x09, 0x09, 0x09), + (0xFF, 0xFF, 0xFF), + (0x0F, 0xD7, 0xFF), + (0x69, 0xA2, 0xFF), + (0xD4, 0x80, 0xFF), + (0xFF, 0x45, 0xF3), + (0xFF, 0x61, 0x8B), + (0xFF, 0x88, 0x33), + (0xFF, 0x9C, 0x12), + (0xFA, 0xBC, 0x20), + (0x9F, 0xE3, 0x0E), + (0x2B, 0xF0, 0x35), + (0x0C, 0xF0, 0xA4), + (0x05, 0xFB, 0xFF), + (0x5E, 0x5E, 0x5E), + (0x0D, 0x0D, 0x0D), + (0x0D, 0x0D, 0x0D), + (0xFF, 0xFF, 0xFF), + (0xA6, 0xFC, 0xFF), + (0xB3, 0xEC, 0xFF), + (0xDA, 0xAB, 0xEB), + (0xFF, 0xA8, 0xF9), + (0xFF, 0xAB, 0xB3), + (0xFF, 0xD2, 0xB0), + (0xFF, 0xEF, 0xA6), + (0xFF, 0xF7, 0x9C), + (0xD7, 0xE8, 0x95), + (0xA6, 0xED, 0xAF), + (0xA2, 0xF2, 0xDA), + (0x99, 0xFF, 0xFC), + (0xDD, 0xDD, 0xDD), + (0x11, 0x11, 0x11), + (0x11, 0x11, 0x11), +]; + +enum PaletteType { + Background, + Sprite, +} + +impl Debug for PaletteType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + PaletteType::Background => write!(f, "PaletteType::Background"), + PaletteType::Sprite => write!(f, "PaletteType::Sprite"), + } + } +} + +struct Palette { + index: u8, + background_entries: [u8; 4], + sprite_entries: [u8; 4], +} + +impl Palette { + pub fn new(index: u8) -> Self { + Palette { + index, + background_entries: [0; 4], + sprite_entries: [0; 4], + } + } + + pub fn set_palette(&mut self, palette_type: PaletteType, palette_index: u8, value: u8) { + debug!( + "[Palette #{}] Setting palette entry for type {:?} at index {} to value: {:#4X}", + self.index, palette_type, palette_index, value + ); + match palette_type { + PaletteType::Background => self.background_entries[palette_index as usize] = value, + PaletteType::Sprite => self.sprite_entries[palette_index as usize] = value, + } + } + + pub fn get_palette(&self, palette_type: PaletteType, palette_index: u8) -> u8 { + debug!( + "[Palette #{}] Getting palette entry for type {:?} at index {}", + self.index, palette_type, palette_index + ); + match palette_type { + PaletteType::Background => self.background_entries[palette_index as usize], + PaletteType::Sprite => self.sprite_entries[palette_index as usize], + } + } +} + +pub struct PaletteRAM { + palettes: [Palette; 4], +} + +impl PaletteRAM { + pub fn new() -> Self { + info!("PaletteRAM is initializing"); + PaletteRAM { + palettes: [ + Palette::new(0), + Palette::new(1), + Palette::new(2), + Palette::new(3), + ], + } + } + + fn read_from_palette(&self, address: u16) -> u8 { + let palette_type = match address { + 0x3F00..=0x3F0F => PaletteType::Background, + 0x3F10..=0x3F1F => PaletteType::Sprite, + _ => unreachable!(), + }; + + let index_in_palette = ((address & 0x0F) % 4) as u8; + let index = ((address & 0x0F) >> 4) as usize; + + self.palettes[index].get_palette(palette_type, index_in_palette) + } + + fn write_to_palette(&mut self, address: u16, data: u8) { + let palette_type = match address { + 0x3F00..=0x3F0F => PaletteType::Background, + 0x3F10..=0x3F1F => PaletteType::Sprite, + _ => unreachable!(), + }; + + let index_in_palette = ((address & 0x0F) % 4) as u8; + let index = ((address & 0x0F) >> 4) as usize; + + self.palettes[index].set_palette(palette_type, index_in_palette, data); + } + + fn mirror_address(&self, address: u16) -> u16 { + // Reduces the address to the range 0x3F00 - 0x3F1F + debug!( + "Mirroring address: {:#6X} down to {:#6X}", + address, + 0x3F00 + (address & 0x1F) + ); + 0x3F00 + (address & 0x1F) + } +} + +// Source for PPU Palette Reference can be found here: https://www.nesdev.org/wiki/PPU_palettes + +impl Addressable for PaletteRAM { + fn read(&mut self, address: u16) -> u8 { + debug!("Reading from palette address: {:#6X}", address); + match address { + 0x3F00..=0x3F1F => self.read_from_palette(address), + 0x3F20..=0x3FFF => self.read_from_palette(self.mirror_address(address)), + _ => panic!("Invalid palette address: {:#6X}", address), + } + } + + fn write(&mut self, address: u16, data: u8) { + debug!("Reading from palette address: {:#6X}", address); + match address { + 0x3F00..=0x3F1F => self.write_to_palette(address, data), + 0x3F20..=0x3FFF => self.write_to_palette(self.mirror_address(address), data), + _ => panic!("Invalid palette address: {:#6X}", address), + } + } +} + +impl Debug for PaletteRAM { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "PaletteRAM") + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn palette_initializes_correctly() { + let palette = Palette::new(0); + assert_eq!(palette.background_entries, [0; 4]); + assert_eq!(palette.sprite_entries, [0; 4]); + } + + #[test] + fn set_and_get_background_palette_entry() { + let mut palette = Palette::new(0); + palette.set_palette(PaletteType::Background, 2, 0x3F); + assert_eq!(palette.get_palette(PaletteType::Background, 2), 0x3F); + } + + #[test] + fn set_and_get_sprite_palette_entry() { + let mut palette = Palette::new(0); + palette.set_palette(PaletteType::Sprite, 1, 0x1F); + assert_eq!(palette.get_palette(PaletteType::Sprite, 1), 0x1F); + } + + #[test] + fn palette_ram_initializes_correctly() { + let palette_ram = PaletteRAM::new(); + for palette in &palette_ram.palettes { + assert_eq!(palette.background_entries, [0; 4]); + assert_eq!(palette.sprite_entries, [0; 4]); + } + } + + #[test] + fn read_write_palette_ram_within_bounds() { + let mut palette_ram = PaletteRAM::new(); + palette_ram.write(0x3F00, 0x12); + assert_eq!(palette_ram.read(0x3F00), 0x12); + } + + #[test] + fn read_write_palette_ram_mirrored_address() { + let mut palette_ram = PaletteRAM::new(); + palette_ram.write(0x3F20, 0x34); + assert_eq!(palette_ram.read(0x3F00), 0x34); + } + + #[test] + #[should_panic(expected = "Invalid palette address: 0x4000")] + fn read_palette_ram_out_of_bounds() { + let mut palette_ram = PaletteRAM::new(); + palette_ram.read(0x4000); + } + + #[test] + #[should_panic(expected = "Invalid palette address: 0x4000")] + fn write_palette_ram_out_of_bounds() { + let mut palette_ram = PaletteRAM::new(); + palette_ram.write(0x4000, 0x56); + } +} diff --git a/emulator/src/vram/mod.rs b/emulator/src/ppu/vram/mod.rs similarity index 100% rename from emulator/src/vram/mod.rs rename to emulator/src/ppu/vram/mod.rs diff --git a/emulator/src/ppu/vram/vram.rs b/emulator/src/ppu/vram/vram.rs new file mode 100644 index 0000000..52ab031 --- /dev/null +++ b/emulator/src/ppu/vram/vram.rs @@ -0,0 +1,171 @@ +use crate::addressing::Addressable; +use crate::mirroring::Mirroring; +use log::{debug, info}; +use std::cmp::PartialEq; +use std::fmt::Debug; + +pub struct VRAM { + nametable_1: [u8; 0x400], + nametable_2: [u8; 0x400], + mirroring: Mirroring, +} + +impl VRAM { + pub fn new() -> VRAM { + info!("VRAM is initializing"); + VRAM { + nametable_1: [0; 0x400], + nametable_2: [0; 0x400], + mirroring: Mirroring::Horizontal, + } + } + + fn read_from_nametable_1(&self, addr: u16) -> u8 { + debug!("Nametable 1 read at relative address {:#06X}", addr); + self.nametable_1[addr as usize] + } + + fn read_from_nametable_2(&self, addr: u16) -> u8 { + debug!("Nametable 2 read at relative address {:#06X}", addr); + self.nametable_2[addr as usize] + } + + fn read_from_nametable(&self, addr: u16) -> u8 { + debug!( + "Attempt to read from VRAM at address {:#06X}", + addr + 0x2000 + ); + if self.mirroring == Mirroring::Horizontal { + match addr { + 0x0000..=0x03FF => self.read_from_nametable_1(addr), + 0x0400..=0x07FF => self.read_from_nametable_1(addr - 0x400), + 0x0800..=0x0BFF => self.read_from_nametable_2(addr - 0x800), + 0x0C00..=0x0FFF => self.read_from_nametable_2(addr - 0xC00), + _ => panic!("Invalid VRAM address: {:#06X}", addr), + } + } else { + match addr { + 0x0000..=0x03FF => self.read_from_nametable_1(addr), + 0x0400..=0x07FF => self.read_from_nametable_2(addr - 0x400), + 0x0800..=0x0BFF => self.read_from_nametable_1(addr - 0x800), + 0x0C00..=0x0FFF => self.read_from_nametable_2(addr - 0xC00), + _ => panic!("Invalid VRAM address: {:#06X}", addr), + } + } + } + + fn write_to_nametable_1(&mut self, addr: u16, value: u8) { + debug!( + "Nametable 1 write at relative address {:#06X} with data {:#04X}", + addr, value + ); + self.nametable_1[addr as usize] = value; + } + + fn write_to_nametable_2(&mut self, addr: u16, value: u8) { + debug!( + "Nametable 2 write at relative address {:#06X} with data {:#04X}", + addr, value + ); + self.nametable_2[addr as usize] = value; + } + + fn write_to_nametable(&mut self, addr: u16, value: u8) { + debug!( + "Attempt to write to VRAM at address {:#06X} with data {:#04X}", + addr + 0x2000, + value + ); + if self.mirroring == Mirroring::Horizontal { + match addr { + 0x0000..=0x03FF => self.write_to_nametable_1(addr, value), + 0x0400..=0x07FF => self.write_to_nametable_1(addr - 0x400, value), + 0x0800..=0x0BFF => self.write_to_nametable_2(addr - 0x800, value), + 0x0C00..=0x0FFF => self.write_to_nametable_2(addr - 0xC00, value), + _ => panic!("Invalid VRAM address: {:#06X}", addr), + } + } else { + match addr { + 0x0000..=0x03FF => self.write_to_nametable_1(addr, value), + 0x0400..=0x07FF => self.write_to_nametable_2(addr - 0x400, value), + 0x0800..=0x0BFF => self.write_to_nametable_1(addr - 0x800, value), + 0x0C00..=0x0FFF => self.write_to_nametable_2(addr - 0xC00, value), + _ => panic!("Invalid VRAM address: {:#06X}", addr), + } + } + } + + pub fn set_mirroring(&mut self, mirroring: Mirroring) { + self.mirroring = mirroring; + } +} + +impl Addressable for VRAM { + fn read(&mut self, addr: u16) -> u8 { + self.read_from_nametable(addr - 0x2000) + } + + fn write(&mut self, addr: u16, data: u8) { + self.write_to_nametable(addr - 0x2000, data); + } +} + +impl Debug for VRAM { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("VRAM").finish() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mirroring::Mirroring; + + #[test] + fn vram_initializes_correctly() { + let vram = VRAM::new(); + assert_eq!(vram.nametable_1, [0; 0x400]); + assert_eq!(vram.nametable_2, [0; 0x400]); + assert_eq!(vram.mirroring, Mirroring::Horizontal); + } + + #[test] + fn read_from_nametable_1_within_bounds() { + let vram = VRAM::new(); + assert_eq!(vram.read_from_nametable_1(0x0000), 0); + assert_eq!(vram.read_from_nametable_1(0x03FF), 0); + } + + #[test] + #[should_panic(expected = "Invalid VRAM address: 0x1000")] + fn read_from_nametable_out_of_bounds() { + let vram = VRAM::new(); + vram.read_from_nametable(0x1000); + } + + #[test] + fn write_to_nametable_1_within_bounds() { + let mut vram = VRAM::new(); + vram.write_to_nametable_1(0x0000, 42); + assert_eq!(vram.read_from_nametable_1(0x0000), 42); + } + + #[test] + fn read_write_nametable_with_horizontal_mirroring() { + let mut vram = VRAM::new(); + vram.write_to_nametable(0x0000, 42); + assert_eq!(vram.read_from_nametable(0x0000), 42); + vram.write_to_nametable(0x0400, 84); + assert_eq!(vram.read_from_nametable(0x0400), 84); + } + + #[test] + fn read_write_nametable_with_vertical_mirroring() { + let mut vram = VRAM::new(); + vram.set_mirroring(Mirroring::Vertical); + vram.write_to_nametable(0x0000, 42); + assert_eq!(vram.read_from_nametable(0x0000), 42); + vram.write_to_nametable(0x0400, 84); + assert_eq!(vram.read_from_nametable(0x0400), 84); + } +} diff --git a/emulator/src/vram/vram.rs b/emulator/src/vram/vram.rs deleted file mode 100644 index 9142772..0000000 --- a/emulator/src/vram/vram.rs +++ /dev/null @@ -1,36 +0,0 @@ -use crate::addressing::Addressable; -use log::{debug, info}; -use std::fmt::Debug; - -pub struct VRAM { - pub(crate) data: [u8; 0x2000], -} - -impl VRAM { - pub fn new() -> VRAM { - info!("VRAM is initializing"); - VRAM { data: [0; 0x2000] } - } -} - -impl Addressable for VRAM { - fn read(&mut self, addr: u16) -> u8 { - debug!( - "VRAM read: addr: {:#06X}, data: {:#04X}", - addr, - self.data[addr as usize - 0x2000] - ); - self.data[addr as usize - 0x2000] - } - - fn write(&mut self, addr: u16, data: u8) { - self.data[addr as usize - 0x2000] = data; - debug!("VRAM write: addr: {:#06X}, data: {:#04X}", addr, data); - } -} - -impl Debug for VRAM { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("VRAM").finish() - } -} diff --git a/emulator/tests/int_ppu.rs b/emulator/tests/int_ppu.rs index 0479a51..d184a29 100644 --- a/emulator/tests/int_ppu.rs +++ b/emulator/tests/int_ppu.rs @@ -4,8 +4,8 @@ mod tests { #[test] fn test_ppu_vram_write() { - emulator::logging::nes_logging::init_logging(); - let vram = emulator::vram::vram::VRAM::new(); + // emulator::logging::nes_logging::init_logging(); + let vram = emulator::ppu::vram::vram::VRAM::new(); let mut ppu_bus = emulator::bus::Bus::new(); ppu_bus.register( vram, @@ -22,7 +22,31 @@ mod tests { let vram_data = ppu.read(*&0x2007); assert_eq!(vram_data, 0x00); - let vram_data = ppu.read(*&0x2007); - assert_eq!(vram_data, 0x66); + let vram_data_valid = ppu.read(*&0x2007); + assert_eq!(vram_data_valid, 0x66); + } + + #[test] + fn test_ppu_palette_ram_write() { + // emulator::logging::nes_logging::init_logging(); + let palette_ram = emulator::ppu::palette_ram::palette_ram::PaletteRAM::new(); + let mut ppu_bus = emulator::bus::Bus::new(); + ppu_bus.register( + palette_ram, + emulator::addressing::AddressRange::new(0x3F00, 0x3FFF), + ); + + let mut ppu = emulator::ppu::ppu::PPU::new(ppu_bus); + ppu.write(*&0x2006, 0x3F); + ppu.write(*&0x2006, 0x2C); + ppu.write(*&0x2007, 0b00101001); + + ppu.write(*&0x2006, 0x3F); + ppu.write(*&0x2006, 0x2C); + + let color_index = ppu.read(*&0x2007); + assert_eq!(color_index, 0x00); + let color_index_valid = ppu.read(*&0x2007); + assert_eq!(color_index_valid, 0b00101001); } }