545 lines
21 KiB
Rust
545 lines
21 KiB
Rust
use crate::cartridge::Cartridge;
|
|
use crate::mapper::Mirroring;
|
|
|
|
|
|
// 4-byte OAM entry: Y, tile, attribute, X
|
|
#[derive(Clone, Copy, Default)]
|
|
struct Sprite {
|
|
y: u8,
|
|
tile: u8,
|
|
attr: u8,
|
|
x: u8,
|
|
}
|
|
pub struct Ppu {
|
|
ctrl: u8, // $2000
|
|
mask: u8, // $2001
|
|
status: u8, // $2002
|
|
oamaddr: u8, // $2003
|
|
oam: [u8; 256], // $2004
|
|
scanline: u32,
|
|
cycle: u32,
|
|
framebuffer: [u8; 256 * 240],
|
|
nmi_pending: bool,
|
|
frame_complete: bool,
|
|
odd_frame: bool,
|
|
t: u16,
|
|
x: u8,
|
|
pattern_lo_latch: u8,
|
|
pattern_hi_latch: u8,
|
|
bg_pattern_lo: u16,
|
|
bg_pattern_hi: u16,
|
|
bg_attr_lo: u16,
|
|
bg_attr_hi: u16,
|
|
tile_latch: u8,
|
|
attr_latch: u8,
|
|
write_latch: bool, // shared W toggle for $2005/$2006, reset by $2002 read
|
|
vram_addr: u16, // 14-bit
|
|
data_buffer: u8, // $2007 read-buffer
|
|
vram: [u8; 0x1000], // 4KB nametables (FourScreen support)
|
|
palette: [u8; 32],
|
|
sprite_scanline: [Sprite; 8], // 8 sprites selected for the current scanline
|
|
sprite_count: u8, // how many were selected (up to 9 for overflow)
|
|
sprite_shifter_lo: [u8; 8], // per-sprite pattern bitplanes
|
|
sprite_shifter_hi: [u8; 8],
|
|
sprite_zero_hit_possible: bool, // sprite 0 is in this scanline's set
|
|
sprite_zero_being_rendered: bool, // sprite 0 has a non-transparent pixel here
|
|
}
|
|
|
|
impl Ppu {
|
|
pub fn tick(&mut self, cart: &mut Cartridge, cycles: u64) {
|
|
for _ in 0..(cycles * 3) {
|
|
self.dot(cart);
|
|
}
|
|
}
|
|
|
|
fn dot(&mut self, cart: &mut Cartridge) {
|
|
// vblank flag + NMI
|
|
if self.cycle == 0 && self.scanline == 241 {
|
|
self.status |= 0x80;
|
|
if self.ctrl & 0x80 != 0 {
|
|
self.nmi_pending = true;
|
|
}
|
|
}
|
|
// clear vblank
|
|
if self.cycle == 0 && self.scanline == 261 {
|
|
self.status &= !0x80;
|
|
}
|
|
|
|
let rendering = self.mask & 0x18 != 0; // bg OR sprites
|
|
|
|
// Pre-render line start: clear sprite flags + shifters (reference: -1, cycle 1)
|
|
if self.scanline == 261 && self.cycle == 1 {
|
|
self.status &= !0x60; // sprite overflow + sprite zero hit
|
|
self.sprite_shifter_lo = [0; 8];
|
|
self.sprite_shifter_hi = [0; 8];
|
|
}
|
|
|
|
// Background + sprite pipeline: visible + pre-render scanlines only
|
|
if rendering && (self.scanline < 240 || self.scanline == 261) {
|
|
// Shifters shift ONLY during the fetch windows (reference timing)
|
|
if (self.cycle >= 2 && self.cycle < 258) || (self.cycle >= 321 && self.cycle < 338) {
|
|
self.update_shifters();
|
|
self.bg_fetch(cart);
|
|
}
|
|
|
|
// Sprite X countdown + shifter shifting (reference: cycle 1-257)
|
|
if self.cycle >= 1 && self.cycle < 258 {
|
|
self.sprite_shift();
|
|
}
|
|
|
|
// End of visible scanline: advance vertical position
|
|
if self.cycle == 256 && (self.scanline < 240 || self.scanline == 261) {
|
|
self.increment_vertical();
|
|
}
|
|
// ...and preload the first tile of the NEXT scanline, resetting X
|
|
if self.cycle == 257 && (self.scanline < 240 || self.scanline == 261) {
|
|
self.load_shifters();
|
|
self.copy_horizontal();
|
|
self.sprite_evaluate(); // pick sprites for the next scanline
|
|
}
|
|
// Idle nametable fetches at the end of the line
|
|
if (self.cycle == 338 || self.cycle == 340) && (self.scanline < 240 || self.scanline == 261) {
|
|
self.tile_latch = self.nametable_read(cart, 0x2000 | (self.vram_addr & 0x0FFF));
|
|
}
|
|
// Pre-render line: reload vertical scroll from t
|
|
if self.scanline == 261 && self.cycle >= 280 && self.cycle < 305 {
|
|
self.copy_vertical();
|
|
}
|
|
// Load the 8 selected sprites' patterns into shifters for the next scanline
|
|
if self.cycle == 340 && (self.scanline < 240 || self.scanline == 261) {
|
|
self.sprite_fetch_patterns(cart);
|
|
}
|
|
}
|
|
|
|
// Compose + store pixel (visible only, x = cycle - 1)
|
|
if rendering && self.scanline < 240 && self.cycle >= 1 && self.cycle <= 256 {
|
|
self.render_pixel(cart); // was render_bg_pixel
|
|
}
|
|
|
|
// advance timing
|
|
self.cycle += 1;
|
|
let line_len = if self.scanline == 261 && self.odd_frame && rendering { 340 } else { 341 };
|
|
if self.cycle >= line_len {
|
|
self.cycle = 0;
|
|
self.scanline += 1;
|
|
if self.scanline >= 262 {
|
|
self.scanline = 0;
|
|
self.frame_complete = true;
|
|
self.odd_frame = !self.odd_frame;
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn new() -> Self {
|
|
Self {
|
|
ctrl: 0, mask: 0, status: 0, oamaddr: 0,
|
|
oam: [0; 256],
|
|
scanline: 0, cycle: 0,
|
|
framebuffer: [0; 256 * 240],
|
|
nmi_pending: false, frame_complete: false, odd_frame: false,
|
|
t: 0, x: 0,
|
|
pattern_lo_latch: 0, pattern_hi_latch: 0,
|
|
bg_pattern_lo: 0, bg_pattern_hi: 0,
|
|
bg_attr_lo: 0, bg_attr_hi: 0,
|
|
tile_latch: 0, attr_latch: 0,
|
|
write_latch: false, vram_addr: 0, data_buffer: 0,
|
|
vram: [0; 0x1000], palette: [0; 32],
|
|
sprite_scanline: [Sprite::default(); 8],
|
|
sprite_count: 0,
|
|
sprite_shifter_lo: [0; 8],
|
|
sprite_shifter_hi: [0; 8],
|
|
sprite_zero_hit_possible: false,
|
|
sprite_zero_being_rendered: false,
|
|
}
|
|
}
|
|
|
|
pub fn read(&mut self, cart: &mut Cartridge, addr: u16, open_bus: u8) -> u8 {
|
|
match addr & 0x2007 {
|
|
0x2000 => open_bus, // write-only: returns bus contents
|
|
0x2001 => open_bus, // write-only
|
|
0x2002 => {
|
|
let s = (self.status & 0xE0) | (open_bus & 0x1F);
|
|
self.status &= !0x80; // reading clears the vblank flag
|
|
self.write_latch = false;
|
|
s
|
|
}
|
|
0x2003 => open_bus, // write-only
|
|
0x2004 => self.oam[self.oamaddr as usize],
|
|
0x2005 => open_bus, // write-only
|
|
0x2006 => open_bus, // write-only
|
|
0x2007 => {
|
|
let data = self.vram_read(cart, self.vram_addr);
|
|
let result = if self.vram_addr >= 0x3F00 { data } else { self.data_buffer };
|
|
self.data_buffer = data;
|
|
self.vram_addr = self.increment_addr();
|
|
result
|
|
}
|
|
_ => open_bus,
|
|
}
|
|
}
|
|
|
|
pub fn write(&mut self, cart: &mut Cartridge, addr: u16, val: u8) {
|
|
match addr & 0x2007 {
|
|
0x2000 => { self.ctrl = val; self.t = (self.t & !0x0C00) | (((val & 0x03) as u16) << 10); },
|
|
0x2001 => self.mask = val,
|
|
0x2002 => {} // write ignored
|
|
0x2003 => self.oamaddr = val,
|
|
0x2004 => {
|
|
self.oam[self.oamaddr as usize] = val;
|
|
self.oamaddr = self.oamaddr.wrapping_add(1);
|
|
}
|
|
0x2005 => {
|
|
if !self.write_latch {
|
|
self.t = (self.t & 0x7FE0) | ((val >> 3) as u16); // coarse X (bits 0-4)
|
|
self.x = val & 0x07; // fine X
|
|
} else {
|
|
self.t = (self.t & 0x0C1F) // keep fine Y + nametable Y + coarse X
|
|
| (((val & 0xF8) as u16) << 2) // coarse Y -> bits 5-9
|
|
| ((val & 0x07) as u16) << 12; // fine Y -> bits 12-14
|
|
}
|
|
self.write_latch = !self.write_latch;
|
|
}
|
|
0x2006 => {
|
|
if !self.write_latch {
|
|
self.t = (self.t & 0x00FF) | (((val & 0x3F) as u16) << 8); // high 6 bits -> bits 8-13
|
|
} else {
|
|
self.t = (self.t & 0x7F00) | (val as u16); // low 8 bits
|
|
self.vram_addr = self.t; // v = t
|
|
}
|
|
self.write_latch = !self.write_latch;
|
|
}
|
|
0x2007 => {
|
|
self.vram_write(cart, self.vram_addr, val);
|
|
self.vram_addr = self.increment_addr();
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
fn increment_addr(&self) -> u16 {
|
|
let step = if self.ctrl & 0x04 != 0 { 32 } else { 1 };
|
|
self.vram_addr.wrapping_add(step) & 0x3FFF
|
|
}
|
|
|
|
fn vram_read(&mut self, cart: &mut Cartridge, addr: u16) -> u8 {
|
|
match addr {
|
|
0x0000..=0x1FFF => cart.read_chr(addr), // pattern tables -> CHR
|
|
0x2000..=0x3EFF => self.nametable_read(cart, addr),
|
|
0x3F00..=0x3FFF => self.palette_read(addr),
|
|
_ => 0,
|
|
}
|
|
}
|
|
|
|
fn vram_write(&mut self, cart: &mut Cartridge, addr: u16, val: u8) {
|
|
match addr {
|
|
0x0000..=0x1FFF => cart.write_chr(addr, val), // CHR-RAM writes through
|
|
0x2000..=0x3EFF => {
|
|
let off = self.nametable_offset(cart, addr);
|
|
self.vram[off] = val;
|
|
}
|
|
0x3F00..=0x3FFF => {
|
|
let i = palette_index(addr);
|
|
self.palette[i] = val;
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
fn nametable_offset(&self, cart: &Cartridge, addr: u16) -> usize {
|
|
let a = addr & 0x2FFF; // fold the $3000-$3EFF mirror into $2000-$2FFF
|
|
let table = match cart.mirroring() {
|
|
Mirroring::Horizontal => (a >> 11) & 1,
|
|
Mirroring::Vertical => (a >> 10) & 1,
|
|
Mirroring::OneScreenLow => 0,
|
|
Mirroring::OneScreenHigh => 1,
|
|
Mirroring::FourScreen => (a >> 10) & 3,
|
|
};
|
|
table as usize * 0x400 + (a & 0x3FF) as usize
|
|
}
|
|
|
|
fn nametable_read(&self, cart: &Cartridge, addr: u16) -> u8 {
|
|
self.vram[self.nametable_offset(cart, addr)]
|
|
}
|
|
|
|
fn palette_read(&self, addr: u16) -> u8 {
|
|
self.palette[palette_index(addr)]
|
|
}
|
|
|
|
fn update_shifters(&mut self) {
|
|
self.bg_pattern_lo <<= 1;
|
|
self.bg_pattern_hi <<= 1;
|
|
self.bg_attr_lo <<= 1;
|
|
self.bg_attr_hi <<= 1;
|
|
}
|
|
|
|
fn load_shifters(&mut self) {
|
|
self.bg_pattern_lo = (self.bg_pattern_lo & 0xFF00) | self.pattern_lo_latch as u16;
|
|
self.bg_pattern_hi = (self.bg_pattern_hi & 0xFF00) | self.pattern_hi_latch as u16;
|
|
self.bg_attr_lo = (self.bg_attr_lo & 0xFF00) | if self.attr_latch & 0x01 != 0 { 0xFF } else { 0x00 };
|
|
self.bg_attr_hi = (self.bg_attr_hi & 0xFF00) | if self.attr_latch & 0x02 != 0 { 0xFF } else { 0x00 };
|
|
}
|
|
|
|
fn bg_fetch(&mut self, cart: &mut Cartridge) {
|
|
match (self.cycle - 1) % 8 {
|
|
0 => {
|
|
self.load_shifters();
|
|
self.tile_latch = self.nametable_read(cart, 0x2000 | (self.vram_addr & 0x0FFF));
|
|
}
|
|
2 => {
|
|
let addr = 0x23C0
|
|
| (self.vram_addr & 0x0C00)
|
|
| ((self.vram_addr >> 4) & 0x38)
|
|
| ((self.vram_addr >> 2) & 0x07);
|
|
let byte = self.nametable_read(cart, addr);
|
|
let shift = ((self.vram_addr & 0x40) >> 4) | (self.vram_addr & 0x02);
|
|
self.attr_latch = (byte >> shift) & 0x03;
|
|
}
|
|
4 => {
|
|
let base = ((self.ctrl & 0x10) as u16) << 8;
|
|
let fine_y = (self.vram_addr >> 12) & 0x07;
|
|
self.pattern_lo_latch = cart.read_chr(base | ((self.tile_latch as u16) << 4) | fine_y);
|
|
}
|
|
6 => {
|
|
let base = ((self.ctrl & 0x10) as u16) << 8;
|
|
let fine_y = (self.vram_addr >> 12) & 0x07;
|
|
self.pattern_hi_latch = cart.read_chr(base | ((self.tile_latch as u16) << 4) | fine_y + 8);
|
|
}
|
|
7 => self.increment_coarse_x(),
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
pub fn oam_dma(&mut self, ram: &[u8; 0x800], page: u8) {
|
|
for i in 0..256u16 {
|
|
let src = (((page as u16) << 8) + i) & 0x7FF; // CPU RAM mirrors every 2KB
|
|
self.oam[i as usize] = ram[src as usize];
|
|
}
|
|
}
|
|
|
|
// Pick the up-to-8 sprites that overlap the NEXT scanline (cycle 257).
|
|
fn sprite_evaluate(&mut self) {
|
|
self.sprite_count = 0;
|
|
self.sprite_zero_hit_possible = false;
|
|
self.sprite_scanline = [Sprite::default(); 8];
|
|
|
|
// The scanline these sprites will be drawn on (pre-render -> scanline 0)
|
|
let target = if self.scanline == 261 { 0 } else { self.scanline as i32 + 1 };
|
|
let height: i32 = if self.ctrl & 0x20 != 0 { 16 } else { 8 };
|
|
|
|
let mut entry: usize = 0;
|
|
while entry < 64 && self.sprite_count < 9 {
|
|
let y = self.oam[entry * 4] as i32;
|
|
let diff = target - y;
|
|
if diff >= 0 && diff < height {
|
|
if self.sprite_count < 8 {
|
|
if entry == 0 {
|
|
self.sprite_zero_hit_possible = true;
|
|
}
|
|
let i = self.sprite_count as usize;
|
|
self.sprite_scanline[i] = Sprite {
|
|
y: self.oam[entry * 4],
|
|
tile: self.oam[entry * 4 + 1],
|
|
attr: self.oam[entry * 4 + 2],
|
|
x: self.oam[entry * 4 + 3],
|
|
};
|
|
}
|
|
self.sprite_count += 1;
|
|
}
|
|
entry += 1;
|
|
}
|
|
// sprite overflow flag (status bit 5)
|
|
self.status = (self.status & !0x20) | if self.sprite_count > 8 { 0x20 } else { 0 };
|
|
}
|
|
|
|
// Decrement sprite X until the sprite's screen position, then shift its pattern.
|
|
fn sprite_shift(&mut self) {
|
|
if self.mask & 0x10 == 0 { return; } // sprite rendering disabled
|
|
for i in 0..(self.sprite_count.min(8) as usize) {
|
|
if self.sprite_scanline[i].x > 0 {
|
|
self.sprite_scanline[i].x -= 1;
|
|
} else {
|
|
self.sprite_shifter_lo[i] <<= 1;
|
|
self.sprite_shifter_hi[i] <<= 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fetch the pattern rows for the 8 selected sprites (cycle 340).
|
|
fn sprite_fetch_patterns(&mut self, cart: &mut Cartridge) {
|
|
let n = self.sprite_count.min(8) as usize;
|
|
for i in 0..n {
|
|
let s = self.sprite_scanline[i];
|
|
let row = (self.scanline as i32 + 1 - s.y as i32) & 0x0F; // row within sprite (0-15)
|
|
|
|
let addr_lo: u16 = if self.ctrl & 0x20 == 0 {
|
|
// 8x8 sprite: pattern table from ctrl bit 3
|
|
let base = ((self.ctrl & 0x08) as u16) << 12;
|
|
let tile = s.tile as u16;
|
|
if s.attr & 0x80 != 0 {
|
|
base | (tile << 4) | (7 - (row & 7)) as u16 // V-flipped
|
|
} else {
|
|
base | (tile << 4) | (row & 7) as u16
|
|
}
|
|
} else {
|
|
// 8x16 sprite: pattern table from tile id bit 0, two tiles stacked
|
|
let table = (s.tile & 0x01) as u16;
|
|
let tile_even = (s.tile & 0xFE) as u16;
|
|
let row_lo = (row & 7) as u16;
|
|
if s.attr & 0x80 != 0 {
|
|
// V-flipped: bottom tile first
|
|
if row < 8 {
|
|
(table << 12) | ((tile_even + 1) << 4) | (7 - row_lo)
|
|
} else {
|
|
(table << 12) | (tile_even << 4) | (7 - row_lo)
|
|
}
|
|
} else if row < 8 {
|
|
(table << 12) | (tile_even << 4) | row_lo
|
|
} else {
|
|
(table << 12) | ((tile_even + 1) << 4) | row_lo
|
|
}
|
|
};
|
|
|
|
let mut lo = cart.read_chr(addr_lo);
|
|
let mut hi = cart.read_chr(addr_lo + 8); // hi plane always +8
|
|
if s.attr & 0x40 != 0 { // H-flip
|
|
lo = lo.reverse_bits();
|
|
hi = hi.reverse_bits();
|
|
}
|
|
self.sprite_shifter_lo[i] = lo;
|
|
self.sprite_shifter_hi[i] = hi;
|
|
}
|
|
}
|
|
|
|
fn increment_coarse_x(&mut self) {
|
|
if self.vram_addr & 0x001F == 0x001F { // coarse X wraps 31 -> 0
|
|
self.vram_addr &= !0x001F;
|
|
self.vram_addr ^= 0x0400; // flip nametable X bit
|
|
} else {
|
|
self.vram_addr += 1;
|
|
}
|
|
}
|
|
|
|
fn increment_vertical(&mut self) {
|
|
if self.vram_addr & 0x7000 != 0x7000 {
|
|
self.vram_addr += 0x1000; // fine Y++
|
|
} else {
|
|
self.vram_addr &= !0x7000; // fine Y = 0
|
|
let mut y = (self.vram_addr >> 5) & 0x1F; // coarse Y
|
|
if y == 29 {
|
|
y = 0;
|
|
self.vram_addr ^= 0x0800; // flip nametable Y
|
|
} else if y == 31 {
|
|
y = 0;
|
|
} else {
|
|
y += 1;
|
|
}
|
|
self.vram_addr = (self.vram_addr & !0x03E0) | (y << 5);
|
|
}
|
|
}
|
|
|
|
fn copy_horizontal(&mut self) { // t's coarse X + nametable X -> v
|
|
self.vram_addr = (self.vram_addr & !0x041F) | (self.t & 0x041F);
|
|
}
|
|
|
|
fn copy_vertical(&mut self) { // t's fine Y + coarse Y + nametable Y -> v
|
|
self.vram_addr = (self.vram_addr & !0x7BE0) | (self.t & 0x7BE0);
|
|
}
|
|
|
|
fn render_pixel(&mut self, _cart: &mut Cartridge) {
|
|
// ---- background pixel (mask bit 3) ----
|
|
let mut bg_pixel = 0u8;
|
|
let mut bg_palette = 0u8;
|
|
if self.mask & 0x08 != 0 {
|
|
let bit_mux = 0x8000u16 >> self.x;
|
|
let lo = (self.bg_pattern_lo & bit_mux) != 0;
|
|
let hi = (self.bg_pattern_hi & bit_mux) != 0;
|
|
let al = (self.bg_attr_lo & bit_mux) != 0;
|
|
let ah = (self.bg_attr_hi & bit_mux) != 0;
|
|
bg_pixel = (hi as u8) << 1 | lo as u8;
|
|
bg_palette = (ah as u8) << 1 | al as u8;
|
|
}
|
|
|
|
// ---- foreground (sprite) pixel (mask bit 4) ----
|
|
let mut fg_pixel = 0u8;
|
|
let mut fg_palette = 0u8;
|
|
let mut fg_priority = false;
|
|
self.sprite_zero_being_rendered = false;
|
|
if self.mask & 0x10 != 0 {
|
|
for i in 0..(self.sprite_count.min(8) as usize) {
|
|
if self.sprite_scanline[i].x == 0 {
|
|
let lo = (self.sprite_shifter_lo[i] & 0x80) != 0;
|
|
let hi = (self.sprite_shifter_hi[i] & 0x80) != 0;
|
|
let pixel = (hi as u8) << 1 | lo as u8;
|
|
if pixel != 0 {
|
|
fg_pixel = pixel;
|
|
fg_palette = (self.sprite_scanline[i].attr & 0x03) + 0x04; // sprites use palettes 4-7
|
|
fg_priority = self.sprite_scanline[i].attr & 0x20 == 0; // bit5=0 -> in front
|
|
if i == 0 {
|
|
self.sprite_zero_being_rendered = true;
|
|
}
|
|
break; // first non-transparent sprite wins (priority order)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---- composite ----
|
|
let (pixel, palette) = match (bg_pixel, fg_pixel) {
|
|
(0, 0) => (0, 0),
|
|
(0, f) => (f, fg_palette),
|
|
(b, 0) => (b, bg_palette),
|
|
(b, f) if fg_priority => (f, fg_palette),
|
|
(b, _) => (b, bg_palette),
|
|
};
|
|
|
|
// ---- sprite zero hit (status bit 6) ----
|
|
if self.sprite_zero_hit_possible
|
|
&& self.sprite_zero_being_rendered
|
|
&& bg_pixel != 0
|
|
&& fg_pixel != 0
|
|
&& self.mask & 0x08 != 0
|
|
&& self.mask & 0x10 != 0
|
|
{
|
|
if self.cycle >= 9 && self.cycle < 258 { // skip leftmost 8px, both enabled
|
|
self.status |= 0x40;
|
|
}
|
|
}
|
|
|
|
// ---- write framebuffer ----
|
|
let color = if pixel == 0 {
|
|
self.palette[0]
|
|
} else {
|
|
self.palette[(palette * 4 + pixel) as usize]
|
|
};
|
|
let idx = (self.scanline as usize) * 256 + (self.cycle as usize - 1);
|
|
self.framebuffer[idx] = color;
|
|
}
|
|
|
|
pub fn frame_done(&self) -> bool { self.frame_complete }
|
|
pub fn take_nmi(&mut self) -> bool { let n = self.nmi_pending; self.nmi_pending = false; n }
|
|
pub fn framebuffer(&self) -> &[u8] { &self.framebuffer }
|
|
pub fn begin_frame(&mut self) { self.frame_complete = false; self.scanline = 0; self.cycle = 0; }
|
|
|
|
// read-only accessors for the headless state dump
|
|
pub fn ctrl(&self) -> u8 { self.ctrl }
|
|
pub fn mask(&self) -> u8 { self.mask }
|
|
pub fn status(&self) -> u8 { self.status }
|
|
pub fn scanline(&self) -> u32 { self.scanline }
|
|
pub fn cycle(&self) -> u32 { self.cycle }
|
|
pub fn vram_addr(&self) -> u16 { self.vram_addr }
|
|
pub fn t(&self) -> u16 { self.t }
|
|
pub fn palette(&self) -> &[u8; 32] { &self.palette }
|
|
pub fn vram(&self) -> &[u8; 0x1000] { &self.vram }
|
|
pub fn framebuffer_nonzero(&self) -> usize {
|
|
self.framebuffer.iter().filter(|&&b| b != 0).count()
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
fn palette_index(addr: u16) -> usize {
|
|
let mut i = (addr & 0x1F) as usize; // $3F20-$3FFF folds into $3F00-$3F1F
|
|
if i >= 0x10 && i % 4 == 0 { i -= 0x10; } // 10/14/18/1C mirror to 00/04/08/0C
|
|
i
|
|
} |