mirror of
https://github.com/FranLMSP/snes.git
synced 2026-01-01 07:21:35 -05:00
Initial PPU rendering (#9)
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
# SNES
|
||||
Work in progress experimental SNES emulator written in Rust.
|
||||
Work in progress SNES emulator written in Rust.
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -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<u8> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
#[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],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
29
snes-core/src/utils/color.rs
Normal file
29
snes-core/src/utils/color.rs
Normal file
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod alu;
|
||||
pub mod addressing;
|
||||
pub mod num_trait;
|
||||
pub mod color;
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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::<Vec<u16>>();
|
||||
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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user