use crate::ppu::registers::Registers;
const VRAM_COARSE_X_MASK: u16 = 0b0001_1111;
const VRAM_COARSE_Y_MASK: u16 = 0b0011_1110_0000;
const VRAM_NAMETABLE_INDEX_X_MASK: u16 = 0b0000_0100_0000_0000;
const VRAM_NAMETABLE_INDEX_Y_MASK: u16 = 0b0000_1000_0000_0000;
const VRAM_FINE_Y_MASK: u16 = 0b0111_0000_0000_0000;
const VRAM_NAMETABLE_INDEX_MASK: u16 = VRAM_NAMETABLE_INDEX_X_MASK | VRAM_NAMETABLE_INDEX_Y_MASK;
const VRAM_VERTICAL_BITS_MASK: u16 =
VRAM_FINE_Y_MASK | VRAM_NAMETABLE_INDEX_Y_MASK | VRAM_COARSE_Y_MASK;
const VRAM_HORIZONTAL_BITS_MASK: u16 = VRAM_COARSE_X_MASK | VRAM_NAMETABLE_INDEX_X_MASK;
const VRAM_COARSE_X_ATTRIBUTE_MASK: u16 = 0b0000_0000_0111;
const VRAM_COARSE_Y_ATTRIBUTE_MASK: u16 = 0b0000_0011_1000;
const VRAM_ATTRIBUTE_OFFSET: u16 = 0b0000_0011_1100_0000;
const VRAM_ATTRIBUTE_NAMETABLE_MASK: u16 = 0b0000_1100_0000_0000;
const VRAM_COARSE_X_SHIFT: u16 = 0;
const VRAM_COARSE_Y_SHIFT: u16 = 5;
const VRAM_NAMETABLE_INDEX_SHIFT: u16 = 10;
const VRAM_FINE_Y_SHIFT: u16 = 12;
const VRAM_ATTRIBUTE_COARSE_X_SHIFT: u16 = 0;
const VRAM_ATTRIBUTE_COARSE_Y_SHIFT: u16 = 3;
const VRAM_ATTRIBUTE_NAMETABLE_INDEX_SHIFT: u16 = 10;
const VRAM_NAMETABLE_BASE_ADDR: u16 = 0x2000;
#[derive(Default, Debug)]
pub struct VramAddressComponents {
pub fine_y: u8,
pub nametable_index: u8,
pub coarse_y: u8,
pub coarse_x: u8,
}
impl VramAddressComponents {
pub fn build_vram_address(&self) -> u16 {
let fine_y = ((self.fine_y as u16) << VRAM_FINE_Y_SHIFT) & VRAM_FINE_Y_MASK;
let nametable_index = ((self.nametable_index as u16) << VRAM_NAMETABLE_INDEX_SHIFT)
& VRAM_NAMETABLE_INDEX_MASK;
let coarse_y = ((self.coarse_y as u16) << VRAM_COARSE_Y_SHIFT) & VRAM_COARSE_Y_MASK;
let coarse_x = ((self.coarse_x as u16) << VRAM_COARSE_X_SHIFT) & VRAM_COARSE_X_MASK;
fine_y | nametable_index | coarse_y | coarse_x
}
pub fn build_nametable_address(&self) -> u16 {
let addr = self.build_vram_address();
(addr & !VRAM_FINE_Y_MASK) + VRAM_NAMETABLE_BASE_ADDR
}
pub fn build_attribute_address(&self) -> u16 {
let coarse_x = (self.coarse_x as u16) >> 2; let coarse_x = (coarse_x << VRAM_ATTRIBUTE_COARSE_X_SHIFT) & VRAM_COARSE_X_ATTRIBUTE_MASK;
let coarse_y = (self.coarse_y as u16) >> 2; let coarse_y = (coarse_y << VRAM_ATTRIBUTE_COARSE_Y_SHIFT) & VRAM_COARSE_Y_ATTRIBUTE_MASK;
let nametable_index = self.nametable_index as u16;
let nametable_index = (nametable_index << VRAM_ATTRIBUTE_NAMETABLE_INDEX_SHIFT)
& VRAM_ATTRIBUTE_NAMETABLE_MASK;
(nametable_index | VRAM_ATTRIBUTE_OFFSET | coarse_x | coarse_y) + VRAM_NAMETABLE_BASE_ADDR
}
pub fn from_vram_address(addr: u16) -> Self {
let fine_y = ((addr & VRAM_FINE_Y_MASK) >> VRAM_FINE_Y_SHIFT) as u8;
let nametable_index =
((addr & VRAM_NAMETABLE_INDEX_MASK) >> VRAM_NAMETABLE_INDEX_SHIFT) as u8;
let coarse_y = ((addr & VRAM_COARSE_Y_MASK) >> VRAM_COARSE_Y_SHIFT) as u8;
let coarse_x = (addr & VRAM_COARSE_X_MASK) as u8;
Self {
fine_y,
nametable_index,
coarse_y,
coarse_x,
}
}
}
fn coarse_component(coord: u8) -> u8 {
coord >> 3
}
fn fine_component(coord: u8) -> u8 {
coord & 0b0000_0111
}
pub fn separate_coarse_fine_from_coord(coord: u8) -> (u8, u8) {
(coarse_component(coord), fine_component(coord))
}
pub fn combine_coarse_fine_into_coord(coarse: u8, fine: u8) -> u8 {
let coarse = coarse & 0b0001_1111;
let fine = fine & 0b0000_0111;
coarse << 3 | fine
}
impl Registers {
pub fn v_addr_components(&self) -> VramAddressComponents {
VramAddressComponents::from_vram_address(self.v)
}
pub fn t_addr_components(&self) -> VramAddressComponents {
VramAddressComponents::from_vram_address(self.t)
}
pub fn vram_addr_update_vertical_bits(&mut self) {
if self.rendering_enabled() {
let vertical_from_t = self.t & VRAM_VERTICAL_BITS_MASK;
let horizontal_from_v = self.v & !VRAM_VERTICAL_BITS_MASK;
self.v = horizontal_from_v | vertical_from_t;
}
}
pub fn vram_addr_update_horizontal_bits(&mut self) {
if self.rendering_enabled() {
let horizontal_from_t = self.t & VRAM_HORIZONTAL_BITS_MASK;
let vertical_from_v = self.v & !VRAM_HORIZONTAL_BITS_MASK;
self.v = vertical_from_v | horizontal_from_t;
}
}
pub fn vram_addr_increment_horizontal(&mut self) {
if self.rendering_enabled() {
let old_addr = self.v;
let mut addr_components = VramAddressComponents::from_vram_address(old_addr);
addr_components.coarse_x += 1;
if addr_components.coarse_x == 0b0010_0000 {
addr_components.coarse_x = 0;
addr_components.nametable_index ^= 0b0001; }
self.v = addr_components.build_vram_address();
}
}
pub fn vram_addr_increment_vertical(&mut self) {
if self.rendering_enabled() {
let old_addr = self.v;
let mut addr_components = VramAddressComponents::from_vram_address(old_addr);
if addr_components.fine_y < 7 {
addr_components.fine_y += 1;
} else {
addr_components.fine_y = 0;
match addr_components.coarse_y {
29 => {
addr_components.coarse_y = 0;
addr_components.nametable_index ^= 0b0010; }
31 => {
addr_components.coarse_y = 0;
}
_ => {
addr_components.coarse_y = addr_components.coarse_y.wrapping_add(1);
}
}
}
self.v = addr_components.build_vram_address();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ppu::registers::ppu_mask::PpuMaskFlags;
use crate::ppu::registers::Register;
#[test]
fn vram_address_components_from_tile_addr() {
let addr = 0b0000_0000_0000_0000;
let components = VramAddressComponents::from_vram_address(addr);
assert_eq!(components.fine_y, 0);
assert_eq!(components.nametable_index, 0);
assert_eq!(components.coarse_y, 0);
assert_eq!(components.coarse_x, 0);
let addr = 0b0001_1111_0011_1100;
let components = VramAddressComponents::from_vram_address(addr);
assert_eq!(components.fine_y, 0b0000_0001);
assert_eq!(components.nametable_index, 0b0000_0011);
assert_eq!(components.coarse_y, 0b0001_1001);
assert_eq!(components.coarse_x, 0b0001_1100);
}
#[test]
fn vram_address_components_build_vram_address() {
let components = VramAddressComponents {
fine_y: 0b0000_0001,
nametable_index: 0b0000_0011,
coarse_y: 0b0001_1001,
coarse_x: 0b0001_1100,
};
let addr = components.build_vram_address();
assert_eq!(addr, 0b0001_1111_0011_1100);
}
#[test]
fn vram_address_components_build_nametable_address() {
let components = VramAddressComponents {
fine_y: 0b0000_0001,
nametable_index: 0b0000_0011,
coarse_y: 0b0001_1001,
coarse_x: 0b0001_1100,
};
let addr = components.build_nametable_address();
assert_eq!(addr, 0b0010_1111_0011_1100);
}
#[test]
fn vram_address_components_build_attribute_address() {
let components = VramAddressComponents {
fine_y: 0,
nametable_index: 0b0000_0010,
coarse_y: 0b0001_1001,
coarse_x: 0b0001_1100,
};
let addr = components.build_attribute_address();
assert_eq!(addr, 0b010_1011_1111_0111);
}
fn enable_rendering(regs: &mut Registers) {
regs.ppu_mask.insert(PpuMaskFlags::SHOW_BG);
for _ in 0..4 {
regs.update_render_toggle_buffers();
}
}
fn set_v_components(
regs: &mut Registers,
coarse_x: u8,
coarse_y: u8,
fine_y: u8,
nt_index: u8,
) {
regs.v = VramAddressComponents {
fine_y: fine_y & 0b111,
nametable_index: nt_index & 0b11,
coarse_y: coarse_y & 0b1_1111,
coarse_x: coarse_x & 0b1_1111,
}
.build_vram_address();
}
fn set_t_components(
regs: &mut Registers,
coarse_x: u8,
coarse_y: u8,
fine_y: u8,
nt_index: u8,
) {
regs.t = VramAddressComponents {
fine_y: fine_y & 0b111,
nametable_index: nt_index & 0b11,
coarse_y: coarse_y & 0b1_1111,
coarse_x: coarse_x & 0b1_1111,
}
.build_vram_address();
}
#[test]
fn horizontal_bits_copy_from_t_when_rendering_enabled() {
let mut regs = Registers::default();
enable_rendering(&mut regs);
set_v_components(&mut regs, 3, 7, 2, 0b10);
set_t_components(&mut regs, 20, 7, 2, 0b11);
let v_before = regs.v;
regs.vram_addr_update_horizontal_bits();
let v_after = regs.v;
let horiz_after = v_after & super::VRAM_HORIZONTAL_BITS_MASK;
let horiz_from_t = regs.t & super::VRAM_HORIZONTAL_BITS_MASK;
assert_eq!(
horiz_after, horiz_from_t,
"horizontal bits should copy from t at dot 257 when rendering"
);
let vert_after = v_after & super::VRAM_VERTICAL_BITS_MASK;
let vert_from_v_before = v_before & super::VRAM_VERTICAL_BITS_MASK;
assert_eq!(
vert_after, vert_from_v_before,
"vertical bits should be preserved when copying horizontal bits"
);
}
#[test]
fn vertical_bits_copy_from_t_when_rendering_enabled() {
let mut regs = Registers::default();
enable_rendering(&mut regs);
set_v_components(&mut regs, 12, 10, 1, 0b00);
set_t_components(&mut regs, 12, 25, 6, 0b10);
let v_before = regs.v;
regs.vram_addr_update_vertical_bits();
let v_after = regs.v;
let vert_after = v_after & super::VRAM_VERTICAL_BITS_MASK;
let vert_from_t = regs.t & super::VRAM_VERTICAL_BITS_MASK;
assert_eq!(vert_after, vert_from_t);
let horiz_after = v_after & super::VRAM_HORIZONTAL_BITS_MASK;
let horiz_from_v_before = v_before & super::VRAM_HORIZONTAL_BITS_MASK;
assert_eq!(horiz_after, horiz_from_v_before);
}
#[test]
fn horizontal_increment_wraps_and_toggles_nametable_x() {
let mut regs = Registers::default();
enable_rendering(&mut regs);
set_v_components(&mut regs, 31, 5, 0, 0b00);
let before = VramAddressComponents::from_vram_address(regs.v);
assert_eq!(before.coarse_x, 31);
assert_eq!(before.nametable_index & 0b01, 0);
regs.vram_addr_increment_horizontal();
let after = VramAddressComponents::from_vram_address(regs.v);
assert_eq!(
after.coarse_x, 0,
"coarse_x should wrap to 0 when incrementing from 31"
);
assert_eq!(
after.nametable_index & 0b01,
1,
"nametable X bit should toggle on coarse_x wrap"
);
}
#[test]
fn vertical_increment_matches_loopy_rules() {
let mut regs = Registers::default();
enable_rendering(&mut regs);
set_v_components(&mut regs, 0, 0, 3, 0);
regs.vram_addr_increment_vertical();
let c = VramAddressComponents::from_vram_address(regs.v);
assert_eq!(c.fine_y, 4);
assert_eq!(c.coarse_y, 0);
set_v_components(&mut regs, 0, 10, 7, 0);
regs.vram_addr_increment_vertical();
let c = VramAddressComponents::from_vram_address(regs.v);
assert_eq!(c.fine_y, 0);
assert_eq!(c.coarse_y, 11);
set_v_components(&mut regs, 0, 29, 7, 0b00);
regs.vram_addr_increment_vertical();
let c = VramAddressComponents::from_vram_address(regs.v);
assert_eq!(c.fine_y, 0);
assert_eq!(c.coarse_y, 0);
assert_eq!(c.nametable_index & 0b10, 0b10);
set_v_components(&mut regs, 0, 31, 7, 0b10);
regs.vram_addr_increment_vertical();
let c = VramAddressComponents::from_vram_address(regs.v);
assert_eq!(c.fine_y, 0);
assert_eq!(c.coarse_y, 0);
assert_eq!(c.nametable_index & 0b10, 0b10);
}
#[test]
fn mid_scanline_scroll_changes_take_effect_on_horizontal_copy() {
let mut regs = Registers::default();
enable_rendering(&mut regs);
set_v_components(&mut regs, 5, 12, 0, 0b00);
regs.write_register(147, Register::PpuScroll);
let t_before_copy = regs.t_addr_components();
assert_eq!(t_before_copy.coarse_x, 18);
assert_eq!(regs.x & 0b111, 3);
let v_before_copy = regs.v_addr_components();
assert_eq!(v_before_copy.coarse_x, 5);
regs.vram_addr_update_horizontal_bits();
let v_after_copy = regs.v_addr_components();
assert_eq!(
v_after_copy.coarse_x, 18,
"coarse_x in v should reflect the new scroll after the copy"
);
assert_eq!(
v_after_copy.nametable_index & 0b01,
t_before_copy.nametable_index & 0b01
);
}
}