diff --git a/README.md b/README.md index ff9d894..93d96a5 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # SNES -Work in progress experimental SNES emulator written in Rust. +Work in progress SNES emulator written in Rust. ![Emulator](screenshots/screenshot.png) diff --git a/snes-core/src/ppu/interface.rs b/snes-core/src/ppu/interface.rs index 60e6dbe..ecd5848 100644 --- a/snes-core/src/ppu/interface.rs +++ b/snes-core/src/ppu/interface.rs @@ -1,4 +1,5 @@ -use super::registers::{PPURegisters, MAX_TV_HEIGHT, MAX_TV_WIDTH}; +use super::registers::{PPURegisters, Background, MAX_TV_HEIGHT, MAX_TV_WIDTH}; +use crate::utils::color::rgb555_to_rgb888; const FRAMEBUFFER_SIZE: usize = MAX_TV_HEIGHT * MAX_TV_WIDTH * 4; @@ -12,13 +13,24 @@ pub struct PPU { impl PPU { pub fn new() -> Self { Self { - framebuffer: vec![0; FRAMEBUFFER_SIZE], + framebuffer: Self::initialize_tv_framebuffer(), registers: PPURegisters::new(), was_vblank_nmi_set: false, is_irq_set: false, } } + fn initialize_tv_framebuffer() -> Vec { + let mut fb = vec![0x00; FRAMEBUFFER_SIZE]; + // 0x00 also makes the alpha channel transparent, we need to make it opaque + let mut i = 3; // start at the first alpha channel + while i < FRAMEBUFFER_SIZE { + fb[i] = 0xFF; + i += 4; + } + fb + } + pub fn tick(&mut self, cpu_cycles: usize) { for _ in 0..(cpu_cycles * 2) { self.dot_cycle(); @@ -26,6 +38,9 @@ impl PPU { } pub fn dot_cycle(&mut self) { + if !self.registers.is_vblanking() && !self.registers.is_hblanking() { + self.put_pixel(self.compute_pixel()) + } self.increment_hv_count(); } @@ -51,6 +66,122 @@ impl PPU { pub fn framebuffer(&self) -> &[u8] { &self.framebuffer } + + fn compute_pixel(&self) -> (u8, u8, u8) { + // Objectives: + // 1. first try to render one of the backgrounds. Background 0 for now. + // 2. render the rest of the backgrounds + // 3. render sprites + // 4. figure out the priorities of each background + self.compute_background_pixel(Background::Bg1) + // (0xFF, 0x00, 0xFF) + } + + // TODO: wirte tests for this function + fn compute_background_pixel(&self, background: Background) -> (u8, u8, u8) { + // 0. detect video mode? + // 1. get base tileset vram address + // 2. get base charset vram address + // 3. know the height and width of background + // 4. calculate which is the current tile: (x / tile width) + ((y / tile height) * background width) + // 4.1: TODO: consider scroll values of the background for this calculation (x += x scroll, y += y scroll) + // 5. get the tile information from vram + // 6. get character index for the tile + // 7. calculate the vram address of the character + // 7.1: TODO: consider that the character vram address also depends on the background's BPP mode + // TODO: consider that each tile can be mirrored either vertically or horizontally. Keep this in mind when fetching the character information from vram + // 8. look up color palette + // ---- + // possible optimizations: + // - Fetch all of the necessary data before starting to render the scanline + let tileset_vram_base_address = self.registers.get_bg_tile_base_address(background) as usize; + let charset_vram_base_address = self.registers.get_bg_char_base_address(background) as usize; + let (bg_size_width, _) = self.registers.get_bg_size(background).to_usize(); + let (tile_size_width, tile_size_height) = self.registers.get_bg_tile_size(background).to_usize(); + let vram = self.registers.vram(); + let x = (self.registers.h_count as usize) - 22; // H count ranges from 0 to 339. Pixels become visible at 22. + let y = (self.registers.v_count as usize) - 1; // V count ranges from 0 to 261 (depending on the region and video mode). Pixels become visible at 1. + + let current_tile = (x / tile_size_width) + ((y / tile_size_height) * bg_size_width); + let tile_byte = vram[tileset_vram_base_address + current_tile]; + let char_index = tile_byte & 0b11_11111111; + let current_char_column = x.rem_euclid(tile_size_width); + let current_char_row = y.rem_euclid(tile_size_height); + + let effective_vram_address = + charset_vram_base_address + + ((char_index as usize) * tile_size_width) + + current_char_row; + + let vram_word = vram[effective_vram_address]; + let lsb_bitplane= vram_word as u8; + let msb_bitplane= (vram_word >> 8) as u8; + + let pixels = Self::mix_pixel_bitplanes(lsb_bitplane, msb_bitplane); + let effective_pixel_index = pixels[current_char_column]; + + // TODO: this cgram lookup is experimental, it will eventually be replaced by actual cgram lookup + let base_cgram_address: u8 = 0x00; + let rgb555_pixel = self.registers.read_cgram(base_cgram_address.wrapping_add(effective_pixel_index)); + rgb555_to_rgb888(( + (rgb555_pixel & 0b11111) as u8, + ((rgb555_pixel >> 5) & 0b11111) as u8, + ((rgb555_pixel >> 10) & 0b11111) as u8, + )) + } + + fn mix_pixel_bitplanes(lsb_bitplane: u8, msb_bitplane: u8) -> [u8; 8] { + [ + ( + (lsb_bitplane >> 7) | + ((msb_bitplane >> 7) << 1) + ), + ( + ((lsb_bitplane >> 6) & 1) | + (((msb_bitplane >> 6) & 1) << 1) + ), + ( + ((lsb_bitplane >> 5) & 1) | + (((msb_bitplane >> 5) & 1) << 1) + ), + ( + ((lsb_bitplane >> 4) & 1) | + (((msb_bitplane >> 4) & 1) << 1) + ), + ( + ((lsb_bitplane >> 3) & 1) | + (((msb_bitplane >> 3) & 1) << 1) + ), + ( + ((lsb_bitplane >> 2) & 1) | + (((msb_bitplane >> 2) & 1) << 1) + ), + ( + ((lsb_bitplane >> 1) & 1) | + (((msb_bitplane >> 1) & 1) << 1) + ), + ( + (lsb_bitplane & 1) | + ((msb_bitplane & 1) << 1) + ), + ] + } + + fn put_pixel(&mut self, pixel: (u8, u8, u8)) { + let fb_index = self.get_pixel_index(); + self.framebuffer[fb_index] = pixel.0; + self.framebuffer[fb_index + 1] = pixel.1; + self.framebuffer[fb_index + 2] = pixel.2; + } + + fn get_pixel_index(&self) -> usize { + let h_count = self.registers.h_count as usize; + let v_count = self.registers.v_count as usize; + ( + (h_count - 22) + + ((v_count - 1) * MAX_TV_WIDTH) + ) * 4 + } } impl Default for PPU { @@ -81,4 +212,62 @@ mod ppu_general_test { assert_eq!(ppu.registers.h_count, 0); assert_eq!(ppu.registers.v_count, 0); } -} \ No newline at end of file + + #[test] + fn test_get_current_pixel_index() { + let mut ppu = PPU::new(); + ppu.registers.v_count = 1; + ppu.registers.h_count = 22; + assert_eq!(ppu.get_pixel_index(), 0); + ppu.registers.v_count = 1; + ppu.registers.h_count = 23; + assert_eq!(ppu.get_pixel_index(), 4); + ppu.registers.v_count = 1; + ppu.registers.h_count = 24; + assert_eq!(ppu.get_pixel_index(), 8); + ppu.registers.v_count = 2; + ppu.registers.h_count = 22; + assert_eq!(ppu.get_pixel_index(), 2048); + ppu.registers.v_count = 2; + ppu.registers.h_count = 23; + assert_eq!(ppu.get_pixel_index(), 2052); + ppu.registers.v_count = 2; + ppu.registers.h_count = 24; + assert_eq!(ppu.get_pixel_index(), 2056); + } + + #[test] + fn test_put_pixel() { + let mut ppu = PPU::new(); + ppu.registers.v_count = 2; + ppu.registers.h_count = 22; + ppu.put_pixel((11, 22, 33)); + assert_eq!(ppu.framebuffer[2048], 11); + assert_eq!(ppu.framebuffer[2049], 22); + assert_eq!(ppu.framebuffer[2050], 33); + } + + #[test] + fn test_mix_pixel_bitplanes() { + assert_eq!( + PPU::mix_pixel_bitplanes(0b00000000, 0b00000000), + [0b00, 0b00, 0b00, 0b00, 0b00, 0b00, 0b00, 0b00], + ); + assert_eq!( + PPU::mix_pixel_bitplanes(0b11111111, 0b11111111), + [0b11, 0b11, 0b11, 0b11, 0b11, 0b11, 0b11, 0b11], + ); + assert_eq!( + PPU::mix_pixel_bitplanes(0b11111111, 0b00000000), + [0b01, 0b01, 0b01, 0b01, 0b01, 0b01, 0b01, 0b01], + ); + assert_eq!( + PPU::mix_pixel_bitplanes(0b00000000, 0b11111111), + [0b10, 0b10, 0b10, 0b10, 0b10, 0b10, 0b10, 0b10], + ); + assert_eq!( + PPU::mix_pixel_bitplanes(0b11110000, 0b00001111), + [0b01, 0b01, 0b01, 0b01, 0b10, 0b10, 0b10, 0b10], + ); + } +} diff --git a/snes-core/src/ppu/registers.rs b/snes-core/src/ppu/registers.rs index 17d3c0b..efb04d3 100644 --- a/snes-core/src/ppu/registers.rs +++ b/snes-core/src/ppu/registers.rs @@ -45,7 +45,7 @@ pub const M7Y: u16 = 0x2120; // Rotation/Scaling Center Coordinate Y ( // PPU CGRAM pub const CGADD: u16 = 0x2121; // Palette CGRAM Address -pub const CGDATA: u16 = 0x2122; // Palette CGRAM Address +pub const CGDATA: u16 = 0x2122; // Palette CGRAM data write // PPU Window pub const W12SEL: u16 = 0x2123; // Window BG1/BG2 Mask Settings (W) @@ -147,12 +147,21 @@ pub enum Background { } +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum CGRamDataReadFlipflop{ + FirstAccess, // Lower 8 bits + SecondAccess, // Upper 8 bits +} + + pub struct PPURegisters { data: [u8; 64], vram: [u16; 0x8000], + cgram: [u16; 256], pub vblank_nmi: bool, pub h_count: u16, pub v_count: u16, + cgram_data_read_flipflop: CGRamDataReadFlipflop } impl PPURegisters { @@ -160,9 +169,11 @@ impl PPURegisters { Self { data: [0x00; 64], vram: [0; 0x8000], + cgram: [0; 256], vblank_nmi: false, h_count: 0, v_count: 0, + cgram_data_read_flipflop: CGRamDataReadFlipflop::FirstAccess, } } @@ -174,6 +185,10 @@ impl PPURegisters { &self.vram } + pub fn cgram(&self) -> &[u16] { + &self.cgram + } + fn _read(&self, address: u16) -> u8 { match address { 0x2100..=0x213F => self.data[(address as usize) - 0x2100], @@ -196,6 +211,10 @@ impl PPURegisters { match address { VMDATAH => self.handle_vram_addr_auto_increment(Some(result), None), VMDATAL => self.handle_vram_addr_auto_increment(None, Some(result)), + RDCGRAM => { + let value = self.get_rdcgram(); + self._write(RDCGRAM, value); + }, _ => {}, }; self._read(address) @@ -212,6 +231,11 @@ impl PPURegisters { self.handle_write_vram(None, Some(value)); }, RDVRAML | RDVRAMH => {}, + CGADD => { + self._write(address, value); + self.cgram_data_read_flipflop = CGRamDataReadFlipflop::FirstAccess; + }, + CGDATA => self.write_cgram(value), _ => self._write(address, value), }; } @@ -375,10 +399,72 @@ impl PPURegisters { } pub fn is_vblanking(&self) -> bool { - if self.v_count >= 1 && self.v_count <= 224 { - return false - } - true + !(self.v_count >= 1 && self.v_count <= 224) + } + + pub fn is_hblanking(&self) -> bool { + !(self.h_count >= 22 && self.h_count <= 277) + } + + pub fn get_current_res(&self) -> (u16, u16) { + let w = if self.is_true_high_res_mode_enabled() {512} else {256}; + let h = if self.is_interlace_mode_enabled() {448} else {224}; + (w, h) + } + + pub fn is_interlace_mode_enabled(&self) -> bool { + self._read(SETINI) & 0x01 == 0b1 + } + + pub fn is_true_high_res_mode_enabled(&self) -> bool { + let current_bg_mode = self._read(BGMODE); + current_bg_mode == 5 || current_bg_mode == 6 + } + + fn get_cgram_index(&self) -> u8 { + self._read(CGADD) + } + + fn handle_cgram_flipflop(&mut self) { + match self.cgram_data_read_flipflop { + CGRamDataReadFlipflop::FirstAccess => { + self.cgram_data_read_flipflop = CGRamDataReadFlipflop::SecondAccess; + }, + CGRamDataReadFlipflop::SecondAccess => { + let current_index = self._read(CGADD); + self._write(CGADD, current_index.wrapping_add(1)); + self.cgram_data_read_flipflop = CGRamDataReadFlipflop::FirstAccess; + }, + }; + } + + fn get_rdcgram(&mut self) -> u8 { + let cgram_index = self.get_cgram_index() as usize; + let value = match self.cgram_data_read_flipflop { + CGRamDataReadFlipflop::FirstAccess => self.cgram[cgram_index] as u8, + CGRamDataReadFlipflop::SecondAccess => (self.cgram[cgram_index] >> 8) as u8, + }; + self.handle_cgram_flipflop(); + value + } + + pub fn read_cgram(&self, address: u8) -> u16 { + self.cgram[address as usize] + } + + fn write_cgram(&mut self, data: u8) { + let cgram_index = self.get_cgram_index() as usize; + match self.cgram_data_read_flipflop { + CGRamDataReadFlipflop::FirstAccess => { + let current_value = self.cgram[cgram_index]; + self.cgram[cgram_index] = (current_value & 0xFF00) | (data as u16); + }, + CGRamDataReadFlipflop::SecondAccess => { + let current_value = self.cgram[cgram_index]; + self.cgram[cgram_index] = (current_value & 0x00FF) | ((data as u16) << 8); + }, + }; + self.handle_cgram_flipflop(); } } @@ -590,4 +676,160 @@ mod ppu_registers_test { registers.v_count = 50; assert!(!registers.is_vblanking()); } + + #[test] + fn test_is_hblanking() { + let mut registers = PPURegisters::new(); + registers.h_count = 0; + assert!(registers.is_hblanking()); + registers.h_count = 20; + assert!(registers.is_hblanking()); + registers.h_count = 21; + assert!(registers.is_hblanking()); + registers.h_count = 278; + assert!(registers.is_hblanking()); + registers.h_count = 300; + assert!(registers.is_hblanking()); + registers.h_count = 22; + assert!(!registers.is_hblanking()); + registers.h_count = 100; + assert!(!registers.is_hblanking()); + registers.h_count = 277; + assert!(!registers.is_hblanking()); + } + + #[test] + fn test_is_interlace_mode_enabled() { + let mut registers = PPURegisters::new(); + registers._write(SETINI, 0x01); + assert!(registers.is_interlace_mode_enabled()); + registers._write(SETINI, 0x00); + assert!(!registers.is_interlace_mode_enabled()); + } + + #[test] + fn test_is_true_high_res_mode_enabled() { + let mut registers = PPURegisters::new(); + registers._write(BGMODE, 1); + assert!(!registers.is_true_high_res_mode_enabled()); + registers._write(BGMODE, 2); + assert!(!registers.is_true_high_res_mode_enabled()); + registers._write(BGMODE, 3); + assert!(!registers.is_true_high_res_mode_enabled()); + registers._write(BGMODE, 4); + assert!(!registers.is_true_high_res_mode_enabled()); + registers._write(BGMODE, 5); + assert!(registers.is_true_high_res_mode_enabled()); + registers._write(BGMODE, 6); + assert!(registers.is_true_high_res_mode_enabled()); + registers._write(BGMODE, 7); + assert!(!registers.is_true_high_res_mode_enabled()); + } + + #[test] + fn test_get_current_res() { + let mut registers = PPURegisters::new(); + registers._write(BGMODE, 1); + registers._write(SETINI, 0x00); + assert_eq!(registers.get_current_res(), (256, 224)); + registers._write(BGMODE, 1); + registers._write(SETINI, 0x01); + assert_eq!(registers.get_current_res(), (256, 448)); + registers._write(BGMODE, 5); + registers._write(SETINI, 0x00); + assert_eq!(registers.get_current_res(), (512, 224)); + registers._write(BGMODE, 6); + registers._write(SETINI, 0x01); + assert_eq!(registers.get_current_res(), (512, 448)); + } + + #[test] + fn test_get_cgram_index() { + let mut registers = PPURegisters::new(); + registers._write(CGADD, 0x00); + assert_eq!(registers.get_cgram_index(), 0x00); + registers._write(CGADD, 0x10); + assert_eq!(registers.get_cgram_index(), 0x10); + registers._write(CGADD, 0xFF); + assert_eq!(registers.get_cgram_index(), 0xFF); + registers._write(CGADD, 0xAB); + assert_eq!(registers.get_cgram_index(), 0xAB); + } + + #[test] + fn test_handle_cgram_flipflop() { + let mut registers = PPURegisters::new(); + registers.cgram_data_read_flipflop = CGRamDataReadFlipflop::FirstAccess; + registers.handle_cgram_flipflop(); + assert_eq!( + registers.cgram_data_read_flipflop, + CGRamDataReadFlipflop::SecondAccess, + ); + + registers.cgram_data_read_flipflop = CGRamDataReadFlipflop::SecondAccess; + registers.handle_cgram_flipflop(); + assert_eq!( + registers.cgram_data_read_flipflop, + CGRamDataReadFlipflop::FirstAccess, + ); + + // When CGADD is written to, the flipflop is reset to first access + registers.cgram_data_read_flipflop = CGRamDataReadFlipflop::FirstAccess; + registers.write(CGADD, 0x00); + assert_eq!( + registers.cgram_data_read_flipflop, + CGRamDataReadFlipflop::FirstAccess, + ); + + registers.cgram_data_read_flipflop = CGRamDataReadFlipflop::SecondAccess; + registers.write(CGADD, 0x00); + assert_eq!( + registers.cgram_data_read_flipflop, + CGRamDataReadFlipflop::FirstAccess, + ); + } + + #[test] + fn test_rdcgram_registers() { + let mut registers = PPURegisters::new(); + registers.cgram_data_read_flipflop = CGRamDataReadFlipflop::FirstAccess; + registers.cgram[0x10] = 0x1234; + registers._write(CGADD, 0x10); + let first_access_value = registers.read(RDCGRAM); + assert_eq!(first_access_value, 0x34); + assert_eq!( + registers.cgram_data_read_flipflop, + CGRamDataReadFlipflop::SecondAccess, + ); + + let second_access_value = registers.read(RDCGRAM); + assert_eq!(registers._read(CGADD), 0x11); + assert_eq!(second_access_value, 0x12); + assert_eq!( + registers.cgram_data_read_flipflop, + CGRamDataReadFlipflop::FirstAccess, + ); + } + + #[test] + fn test_cgdata_registers() { + let mut registers = PPURegisters::new(); + registers.cgram_data_read_flipflop = CGRamDataReadFlipflop::FirstAccess; + registers.cgram[0x10] = 0x0000; + registers._write(CGADD, 0x10); + registers.write(CGDATA, 0x34); + assert_eq!(registers.cgram[0x10], 0x0034); + assert_eq!( + registers.cgram_data_read_flipflop, + CGRamDataReadFlipflop::SecondAccess, + ); + + registers.write(CGDATA, 0x12); + assert_eq!(registers._read(CGADD), 0x11); + assert_eq!(registers.cgram[0x10], 0x1234); + assert_eq!( + registers.cgram_data_read_flipflop, + CGRamDataReadFlipflop::FirstAccess, + ); + } } diff --git a/snes-core/src/utils/color.rs b/snes-core/src/utils/color.rs new file mode 100644 index 0000000..f8daf1e --- /dev/null +++ b/snes-core/src/utils/color.rs @@ -0,0 +1,29 @@ +pub fn rgb555_to_rgb888(rgb555: (u8, u8, u8)) -> (u8, u8, u8) { + let red = rgb555.0 & 0b11111; + let green = rgb555.1 & 0b11111; + let blue = rgb555.2 & 0b11111; + ( + (red << 3) | (red >> 2), + (green << 3) | (green >> 2), + (blue << 3) | (blue >> 2), + ) +} + + +#[cfg(test)] +mod color_tests { + use super::*; + + #[test] + fn test_rgb555_to_rgb888() { + assert_eq!(rgb555_to_rgb888((0x00, 0x00, 0x00)), (0x00, 0x00, 0x00)); + assert_eq!( + rgb555_to_rgb888((0b0001_1111, 0b0001_1111, 0b0001_1111)), + (0xFF, 0xFF, 0xFF) + ); + assert_eq!( + rgb555_to_rgb888((0b10101, 0b01010, 0b11011)), + (0xAD, 0x52, 0xDE) + ); + } +} \ No newline at end of file diff --git a/snes-core/src/utils/mod.rs b/snes-core/src/utils/mod.rs index 6112635..fa36a3d 100644 --- a/snes-core/src/utils/mod.rs +++ b/snes-core/src/utils/mod.rs @@ -1,3 +1,4 @@ pub mod alu; pub mod addressing; pub mod num_trait; +pub mod color; diff --git a/snes-frontend/Cargo.toml b/snes-frontend/Cargo.toml index f330aec..87d9c75 100644 --- a/snes-frontend/Cargo.toml +++ b/snes-frontend/Cargo.toml @@ -9,11 +9,11 @@ edition = "2021" snes-core = { path = "../snes-core" } # Frontend stuff -eframe = "0.25.0" +eframe = "0.26.0" env_logger = "0.10.1" rfd = "0.12.1" regex = "1.10.2" -wgpu = "0.18.0" +wgpu = "0.19.0" [features] wgpu = ["eframe/wgpu"] diff --git a/snes-frontend/src/emu_state/debug_options.rs b/snes-frontend/src/emu_state/debug_options.rs index a456352..cbb5d13 100644 --- a/snes-frontend/src/emu_state/debug_options.rs +++ b/snes-frontend/src/emu_state/debug_options.rs @@ -102,6 +102,7 @@ pub struct PPUDebugControlOptions { pub is_enabled: bool, pub show_registers: bool, pub show_vram: bool, + pub show_cgram: bool, pub vram_inputs: VramInputs, pub vram_inputs_result: VramInputs, pub backgrounds: [BgDebug; 4], @@ -113,6 +114,7 @@ impl PPUDebugControlOptions { is_enabled: true, show_registers: true, show_vram: true, + show_cgram: true, vram_inputs: VramInputs::new(), vram_inputs_result: VramInputs::new(), backgrounds: [ diff --git a/snes-frontend/src/emu_ui/debug/ppu.rs b/snes-frontend/src/emu_ui/debug/ppu.rs index 9cc0704..b9c342e 100644 --- a/snes-frontend/src/emu_ui/debug/ppu.rs +++ b/snes-frontend/src/emu_ui/debug/ppu.rs @@ -1,5 +1,6 @@ -use eframe::egui; +use eframe::egui::{self, Color32, Rect, Ui, Vec2}; use snes_core::ppu::registers::PPURegisters; +use snes_core::utils::color::rgb555_to_rgb888; use crate::emu_state::debug_options::PPUDebugControlOptions; use crate::emu_ui::debug::common::sanitize_input; @@ -45,6 +46,7 @@ pub fn build_ppu_debug_controls(ctx: &egui::Context, ppu_debug_options: &mut PPU build_ppu_registers_window(ctx, ppu_debug_options, ppu_registers); build_vram_window(ctx, ppu_debug_options, ppu_registers); + build_cgram_window(ctx, ppu_debug_options, ppu_registers); } fn build_ppu_registers_window(ctx: &egui::Context, ppu_debug_options: &mut PPUDebugControlOptions, ppu_registers: &PPURegisters) { @@ -195,3 +197,66 @@ fn build_vram_window(ctx: &egui::Context, ppu_debug_options: &mut PPUDebugContro }); }); } + +fn build_cgram_window(ctx: &egui::Context, ppu_debug_options: &mut PPUDebugControlOptions, ppu_registers: &PPURegisters) { + if !ppu_debug_options.show_cgram { + return; + } + egui::Window::new("CGRAM Viewer") + .open(&mut ppu_debug_options.show_cgram) + .default_width(610.0) + .max_width(610.0) + .show(ctx, |ui| { + egui::ScrollArea::both().show(ui, |ui| { + let address_start = 0x00; + let address_end = 0xFF; + let mut header = String::from(" | "); + for page in 0x00..=0x0F { + header = format!("{} {:02X} ", header, page); + } + ui.monospace(header); + let mut divider = String::from("-----|-"); + for _ in 0x00..=0x0F { + divider = format!("{}-----", divider); + } + ui.monospace(divider); + let vector = (address_start..=address_end).collect::>(); + let chunks = vector.chunks(0x10); + for row in chunks { + let mut address_row = format!("{:04X} | ", row[0]); + for address in row { + address_row = format!("{}{:04X} ", address_row, ppu_registers.cgram()[((*address) & 0x7FFF) as usize]); + } + ui.monospace(address_row); + } + }); + + ui.separator(); + + ui.horizontal(|ui| { + ui.label("0x00: "); + paint_cgram_color_address(ui, 0x00, ppu_registers); + ui.label("0x01: "); + paint_cgram_color_address(ui, 0x01, ppu_registers); + ui.label("0x02: "); + paint_cgram_color_address(ui, 0x02, ppu_registers); + }); + }); +} + +fn paint_cgram_color_address(ui: &mut Ui, address: u8, ppu_registers: &PPURegisters) { + let rgb555_value = ppu_registers.read_cgram(address); + let rgb888_value = rgb555_to_rgb888(( + (rgb555_value & 0b11111) as u8, + ((rgb555_value >> 5) & 0b11111) as u8, + ((rgb555_value >> 10) & 0b11111) as u8, + )); + paint_square_color(ui, rgb888_value); +} + +fn paint_square_color(ui: &mut Ui, color: (u8, u8, u8)) { + let (_, painter) = ui.allocate_painter(Vec2::splat(15.0), egui::Sense::hover()); + let square_color = Color32::from_rgb(color.0, color.1, color.2); + let rect = Rect::EVERYTHING; + painter.rect_filled(rect, 0.0, square_color); +}