use crate::console::Nes;
const CHR_SIZE: usize = 8192;
const NAMETABLE_SIZE: usize = 1024;
const NUM_NAMETABLES: usize = 4;
const PALETTE_SIZE: usize = 32;
const TILE_SIZE: usize = 8;
const CHR_BYTES_PER_TILE: usize = TILE_SIZE * 2;
const PATTERN_TABLE_TILES_PER_ROW: usize = 16;
const PATTERN_TABLE_WIDTH: usize = TILE_SIZE * PATTERN_TABLE_TILES_PER_ROW;
const PATTERN_TABLE_OUTPUT_WIDTH: usize = PATTERN_TABLE_WIDTH * 2;
const PATTERN_TABLE_OUTPUT_HEIGHT: usize = PATTERN_TABLE_WIDTH;
const NAMETABLE_TILE_COLS: usize = 32;
const NAMETABLE_TILE_ROWS: usize = 30;
const NAMETABLE_WIDTH: usize = TILE_SIZE * NAMETABLE_TILE_COLS;
const NAMETABLE_HEIGHT: usize = TILE_SIZE * NAMETABLE_TILE_ROWS;
const NAMETABLE_OUTPUT_WIDTH: usize = NAMETABLE_WIDTH * 2;
const NAMETABLE_OUTPUT_HEIGHT: usize = NAMETABLE_HEIGHT * 2;
const ATTRIBUTE_TABLE_OFFSET: usize = NAMETABLE_TILE_COLS * NAMETABLE_TILE_ROWS;
const ATTRIBUTE_TABLE_COLS: usize = NAMETABLE_TILE_COLS / 4;
pub struct PpuViewerSnapshot {
pub chr: [u8; CHR_SIZE],
pub nametables: [[u8; NAMETABLE_SIZE]; NUM_NAMETABLES],
pub palette: [u8; PALETTE_SIZE],
pub bg_pattern_table: u16,
pub scroll: (u16, u16),
}
impl PpuViewerSnapshot {
pub fn from_nes(nes: &Nes) -> Self {
let binding = nes.ppu();
let ppu = binding.borrow();
Self {
chr: ppu.chr_snapshot_for_debugger(),
nametables: ppu.nametable_snapshot_for_debugger(),
palette: ppu.palette_for_debugger(),
bg_pattern_table: ppu.bg_pattern_table_for_debugger(),
scroll: ppu.scroll_for_debugger(),
}
}
}
pub fn render_pattern_tables_rgba(chr: &[u8; CHR_SIZE], palette: &[u8; PALETTE_SIZE]) -> Vec<u8> {
let mut pixels = vec![0u8; PATTERN_TABLE_OUTPUT_WIDTH * PATTERN_TABLE_OUTPUT_HEIGHT * 4];
for table in 0u16..2 {
let table_x_offset = table as usize * PATTERN_TABLE_WIDTH;
for ty in 0..PATTERN_TABLE_TILES_PER_ROW {
for tx in 0..PATTERN_TABLE_TILES_PER_ROW {
let tile_index = ty * PATTERN_TABLE_TILES_PER_ROW + tx;
let tile_addr = table * 0x1000 + (tile_index as u16) * CHR_BYTES_PER_TILE as u16;
render_tile_into(
chr,
palette,
tile_addr,
0,
&mut pixels,
(table_x_offset + tx * TILE_SIZE, ty * TILE_SIZE),
PATTERN_TABLE_OUTPUT_WIDTH,
);
}
}
}
pixels
}
pub fn render_nametables_rgba(
chr: &[u8; CHR_SIZE],
nametables: &[[u8; NAMETABLE_SIZE]; NUM_NAMETABLES],
palette: &[u8; PALETTE_SIZE],
bg_pattern_table: u16,
) -> Vec<u8> {
let mut pixels = vec![0u8; NAMETABLE_OUTPUT_WIDTH * NAMETABLE_OUTPUT_HEIGHT * 4];
for (nt, nt_data) in nametables.iter().enumerate().take(NUM_NAMETABLES) {
let nt_x = (nt % 2) * NAMETABLE_WIDTH;
let nt_y = (nt / 2) * NAMETABLE_HEIGHT;
for ty in 0..NAMETABLE_TILE_ROWS {
for tx in 0..NAMETABLE_TILE_COLS {
let tile_index = nt_data[ty * NAMETABLE_TILE_COLS + tx] as u16;
let tile_addr = bg_pattern_table + tile_index * CHR_BYTES_PER_TILE as u16;
let attr_byte =
nt_data[ATTRIBUTE_TABLE_OFFSET + (ty / 4) * ATTRIBUTE_TABLE_COLS + (tx / 4)];
let pal_shift = ((ty / 2) % 2) * 4 + ((tx / 2) % 2) * 2;
let palette_num = ((attr_byte >> pal_shift) & 0x03) as usize;
render_tile_into(
chr,
palette,
tile_addr,
palette_num,
&mut pixels,
(nt_x + tx * TILE_SIZE, nt_y + ty * TILE_SIZE),
NAMETABLE_OUTPUT_WIDTH,
);
}
}
}
pixels
}
fn render_tile_into(
chr: &[u8; CHR_SIZE],
palette: &[u8; PALETTE_SIZE],
tile_addr: u16,
palette_num: usize,
pixels: &mut [u8],
position: (usize, usize),
stride: usize,
) {
let (px, py) = position;
for row in 0..TILE_SIZE {
let base = tile_addr as usize + row;
let lo = chr[base];
let hi = chr[base + TILE_SIZE];
for col in 0..TILE_SIZE {
let lo_bit = (lo >> (7 - col)) & 1;
let hi_bit = (hi >> (7 - col)) & 1;
let color_idx = (hi_bit << 1) | lo_bit;
let palette_ram_idx = if color_idx == 0 {
0
} else {
palette_num * 4 + color_idx as usize
};
let nes_color = palette[palette_ram_idx] & 0x3F;
let (r, g, b) = Nes::lookup_system_palette(nes_color);
let offset = ((py + row) * stride + (px + col)) * 4;
pixels[offset] = r;
pixels[offset + 1] = g;
pixels[offset + 2] = b;
pixels[offset + 3] = 255;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cartridge::{Cartridge, NametableLayout};
use crate::console::{Config, Nes};
#[test]
fn test_render_pattern_tables_rgba_has_correct_dimensions() {
let chr = [0u8; 8192];
let palette = [0u8; 32];
let pixels = render_pattern_tables_rgba(&chr, &palette);
assert_eq!(pixels.len(), 256 * 128 * 4);
}
#[test]
fn test_render_nametables_rgba_has_correct_dimensions() {
let chr = [0u8; 8192];
let nametables = [[0u8; 1024]; 4];
let palette = [0u8; 32];
let pixels = render_nametables_rgba(&chr, &nametables, &palette, 0x0000);
assert_eq!(pixels.len(), 512 * 480 * 4);
}
#[test]
fn test_render_pattern_tables_rgba_pixel_color_from_chr_data() {
let mut chr = [0u8; 8192];
for row in 0..8 {
chr[row] = 0xFF; chr[row + 8] = 0x00; }
let mut palette = [0u8; 32];
palette[0] = 0x0F; palette[1] = 0x20;
let pixels = render_pattern_tables_rgba(&chr, &palette);
let (r, g, b) = Nes::lookup_system_palette(0x20);
assert_eq!(pixels[0], r, "R mismatch at pixel (0,0)");
assert_eq!(pixels[1], g, "G mismatch at pixel (0,0)");
assert_eq!(pixels[2], b, "B mismatch at pixel (0,0)");
assert_eq!(pixels[3], 255, "Alpha should be 255");
}
#[test]
fn test_render_pattern_table_1_starts_at_pixel_x_128() {
let mut chr = [0u8; 8192];
for row in 0..8 {
chr[0x1000 + row] = 0xFF;
}
let mut palette = [0u8; 32];
palette[1] = 0x11;
let pixels = render_pattern_tables_rgba(&chr, &palette);
let offset = 128 * 4;
let (r, g, b) = Nes::lookup_system_palette(0x11);
assert_eq!(pixels[offset], r);
assert_eq!(pixels[offset + 1], g);
assert_eq!(pixels[offset + 2], b);
}
#[test]
fn test_render_nametables_rgba_renders_tile_from_index() {
let mut chr = [0u8; 8192];
for row in 0..8 {
chr[16 + row] = 0xFF; chr[16 + row + 8] = 0xFF; }
let mut nametables = [[0u8; 1024]; 4];
nametables[0][0] = 1;
let mut palette = [0u8; 32];
palette[0] = 0x0F;
palette[3] = 0x20;
let pixels = render_nametables_rgba(&chr, &nametables, &palette, 0x0000);
let (r, g, b) = Nes::lookup_system_palette(0x20);
assert_eq!(pixels[0], r);
assert_eq!(pixels[1], g);
assert_eq!(pixels[2], b);
assert_eq!(pixels[3], 255);
}
#[test]
fn test_render_nametables_rgba_nt1_starts_at_x256() {
let mut chr = [0u8; 8192];
for row in 0..8 {
chr[2 * 16 + row] = 0xFF; }
let mut nametables = [[0u8; 1024]; 4];
nametables[1][0] = 2;
let mut palette = [0u8; 32];
palette[1] = 0x15;
let pixels = render_nametables_rgba(&chr, &nametables, &palette, 0x0000);
let offset = 256 * 4;
let (r, g, b) = Nes::lookup_system_palette(0x15);
assert_eq!(pixels[offset], r);
assert_eq!(pixels[offset + 1], g);
assert_eq!(pixels[offset + 2], b);
}
#[test]
fn test_render_nametables_rgba_nt2_starts_at_y240() {
let mut chr = [0u8; 8192];
for row in 0..8 {
chr[3 * 16 + row] = 0x00; chr[3 * 16 + row + 8] = 0xFF; }
let mut nametables = [[0u8; 1024]; 4];
nametables[2][0] = 3;
let mut palette = [0u8; 32];
palette[2] = 0x1A;
let pixels = render_nametables_rgba(&chr, &nametables, &palette, 0x0000);
let offset = (240 * 512) * 4;
let (r, g, b) = Nes::lookup_system_palette(0x1A);
assert_eq!(pixels[offset], r);
assert_eq!(pixels[offset + 1], g);
assert_eq!(pixels[offset + 2], b);
}
#[test]
fn test_render_nametables_rgba_uses_bg_pattern_table_offset() {
let mut chr = [0u8; 8192];
for row in 0..8 {
chr[0x1000 + row] = 0xFF; }
let nametables = [[0u8; 1024]; 4];
let mut palette = [0u8; 32];
palette[1] = 0x16;
let pixels = render_nametables_rgba(&chr, &nametables, &palette, 0x1000);
let (r, g, b) = Nes::lookup_system_palette(0x16);
assert_eq!(pixels[0], r);
assert_eq!(pixels[1], g);
assert_eq!(pixels[2], b);
}
#[test]
fn test_ppu_viewer_snapshot_from_nes_has_correct_field_sizes() {
let mut nes = Nes::new(crate::app_context::AppContext::new_with_config(
Config::default(),
));
let prg_rom = vec![0u8; 32 * 1024];
let chr_rom = vec![0u8; 8 * 1024];
let cart = Cartridge::from_parts(prg_rom, chr_rom, NametableLayout::Horizontal);
nes.insert_cartridge(cart);
let snap = PpuViewerSnapshot::from_nes(&nes);
assert_eq!(snap.chr.len(), 8192);
assert_eq!(snap.nametables.len(), 4);
assert_eq!(snap.nametables[0].len(), 1024);
assert_eq!(snap.palette.len(), 32);
assert!(snap.bg_pattern_table == 0x0000 || snap.bg_pattern_table == 0x1000);
let (sx, sy) = snap.scroll;
assert!(sx < 512, "scroll_x {sx} out of range");
assert!(sy < 480, "scroll_y {sy} out of range");
}
#[test]
fn test_scroll_for_debugger_default_is_origin() {
let nes = Nes::new(crate::app_context::AppContext::new_with_config(
Config::default(),
));
let (sx, sy) = nes.ppu().borrow().scroll_for_debugger();
assert_eq!(sx, 0);
assert_eq!(sy, 0);
}
#[test]
fn test_scroll_for_debugger_reflects_x_scroll_write() {
let nes = Nes::new(crate::app_context::AppContext::new_with_config(
Config::default(),
));
nes.ppu().borrow_mut().write_scroll(0x80, false);
nes.ppu().borrow_mut().write_scroll(0x00, false);
let (sx, sy) = nes.ppu().borrow().scroll_for_debugger();
assert_eq!(sx, 128, "scroll_x should be 128");
assert_eq!(sy, 0, "scroll_y should be 0");
}
#[test]
fn test_scroll_for_debugger_reflects_y_scroll_write() {
let nes = Nes::new(crate::app_context::AppContext::new_with_config(
Config::default(),
));
nes.ppu().borrow_mut().write_scroll(0x00, false);
nes.ppu().borrow_mut().write_scroll(0x78, false);
let (sx, sy) = nes.ppu().borrow().scroll_for_debugger();
assert_eq!(sx, 0, "scroll_x should be 0");
assert_eq!(sy, 120, "scroll_y should be 120");
}
}