Some checks failed
CI / rust (push) Has been cancelled
Full NES emulation: CPU, PPU, APU, 47 mappers, iNES/NES 2.0 parsing. GTK4 desktop client with HeaderBar, pixel-perfect Cairo rendering, drag-and-drop ROM loading, and keyboard shortcuts. 187 tests covering core emulation, mappers, and runtime.
388 lines
13 KiB
Rust
388 lines
13 KiB
Rust
use super::state::{apply_color_emphasis, nes_rgb, palette_index};
|
|
use super::types::{OAM_SIZE, PALETTE_SIZE, Ppu, ScrollEvent, VISIBLE_SCANLINES, VRAM_SIZE};
|
|
use crate::native_core::ines::Mirroring;
|
|
use crate::native_core::mapper::Mapper;
|
|
use crate::native_core::state_io as sio;
|
|
|
|
const PPU_STATE_CTX: &str = "ppu state";
|
|
|
|
impl Ppu {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
vram: [0; VRAM_SIZE],
|
|
palette_ram: [0; PALETTE_SIZE],
|
|
oam: [0; OAM_SIZE],
|
|
ctrl: 0,
|
|
mask: 0,
|
|
status: 0,
|
|
oam_addr: 0,
|
|
write_latch: false,
|
|
vram_addr: 0,
|
|
temp_addr: 0,
|
|
fine_x: 0,
|
|
scroll_x: 0,
|
|
scroll_y: 0,
|
|
read_buffer: 0,
|
|
io_latch: 0,
|
|
scroll_events: vec![ScrollEvent {
|
|
scanline: 0,
|
|
x_start: 0,
|
|
scroll_x: 0,
|
|
scroll_y: 0,
|
|
base_nt: 0,
|
|
}],
|
|
frame_rgba: vec![0; 256 * 240 * 4],
|
|
bg_shift_pattern_lo: 0,
|
|
bg_shift_pattern_hi: 0,
|
|
bg_shift_attr_lo: 0,
|
|
bg_shift_attr_hi: 0,
|
|
next_tile_id: 0,
|
|
next_tile_attr: 0,
|
|
next_tile_lsb: 0,
|
|
next_tile_msb: 0,
|
|
sprite_indices: [0; 8],
|
|
sprite_count: 0,
|
|
next_sprite_indices: [0; 8],
|
|
next_sprite_count: 0,
|
|
}
|
|
}
|
|
|
|
pub fn begin_frame(&mut self) {
|
|
self.scroll_events.clear();
|
|
self.scroll_events.push(self.current_scroll_event(0, 0));
|
|
self.bg_shift_pattern_lo = 0;
|
|
self.bg_shift_pattern_hi = 0;
|
|
self.bg_shift_attr_lo = 0;
|
|
self.bg_shift_attr_hi = 0;
|
|
self.next_tile_id = 0;
|
|
self.next_tile_attr = 0;
|
|
self.next_tile_lsb = 0;
|
|
self.next_tile_msb = 0;
|
|
self.sprite_count = 0;
|
|
self.next_sprite_count = 0;
|
|
}
|
|
|
|
pub fn frame_buffer(&self) -> &[u8] {
|
|
&self.frame_rgba
|
|
}
|
|
|
|
pub fn render_dot(&mut self, mapper: &dyn Mapper, scanline: u32, dot: u32) {
|
|
if dot == 0 || dot > 340 {
|
|
return;
|
|
}
|
|
|
|
let rendering_active = self.rendering_enabled() && (scanline < 240 || scanline == 261);
|
|
let bg_fetch_cycle =
|
|
rendering_active && ((1..=256).contains(&dot) || (321..=336).contains(&dot));
|
|
|
|
let show_bg = (self.mask & 0x08) != 0;
|
|
let show_bg_left = (self.mask & 0x02) != 0;
|
|
let show_spr = (self.mask & 0x10) != 0;
|
|
let show_spr_left = (self.mask & 0x04) != 0;
|
|
|
|
if scanline < 240 && (1..=256).contains(&dot) {
|
|
let x = (dot - 1) as usize;
|
|
let y = scanline as usize;
|
|
let bg_layer_enabled = show_bg && (x >= 8 || show_bg_left);
|
|
let (bg_color_index, bg_opaque) = if bg_layer_enabled {
|
|
self.background_pixel_from_shifters()
|
|
} else {
|
|
(self.read_palette(0), false)
|
|
};
|
|
|
|
if !self.sprite0_hit_set() && self.sprite0_hit_at(mapper, y, dot) && bg_opaque {
|
|
self.set_sprite0_hit(true);
|
|
}
|
|
|
|
let mut final_color = bg_color_index & 0x3F;
|
|
let sprite_layer_enabled = show_spr && (x >= 8 || show_spr_left);
|
|
if sprite_layer_enabled
|
|
&& let Some((spr_color_index, behind_bg)) = self.sprite_pixel(mapper, x, y)
|
|
&& !(behind_bg && bg_opaque)
|
|
{
|
|
final_color = spr_color_index & 0x3F;
|
|
}
|
|
|
|
let (r, g, b) = apply_color_emphasis(nes_rgb(final_color), self.mask);
|
|
let i = (y * 256 + x) * 4;
|
|
self.frame_rgba[i] = r;
|
|
self.frame_rgba[i + 1] = g;
|
|
self.frame_rgba[i + 2] = b;
|
|
self.frame_rgba[i + 3] = 255;
|
|
}
|
|
|
|
if bg_fetch_cycle {
|
|
self.bg_shift_pattern_lo <<= 1;
|
|
self.bg_shift_pattern_hi <<= 1;
|
|
self.bg_shift_attr_lo <<= 1;
|
|
self.bg_shift_attr_hi <<= 1;
|
|
let phase = dot & 7;
|
|
match phase {
|
|
1 => {
|
|
let nt_addr = 0x2000 | (self.vram_addr & 0x0FFF);
|
|
self.next_tile_id = self.read_nt(nt_addr, mapper);
|
|
}
|
|
3 => {
|
|
let attr_addr = 0x23C0
|
|
| (self.vram_addr & 0x0C00)
|
|
| ((self.vram_addr >> 4) & 0x38)
|
|
| ((self.vram_addr >> 2) & 0x07);
|
|
let attr = self.read_nt(attr_addr, mapper);
|
|
let shift = (((self.vram_addr >> 4) & 4) | (self.vram_addr & 2)) as u8;
|
|
self.next_tile_attr = (attr >> shift) & 0x03;
|
|
}
|
|
5 => {
|
|
let table = if (self.ctrl & 0x10) != 0 {
|
|
0x1000
|
|
} else {
|
|
0x0000
|
|
};
|
|
let fine_y = (self.vram_addr >> 12) & 0x7;
|
|
let addr = table + ((self.next_tile_id as u16) << 4) + fine_y;
|
|
self.next_tile_lsb = mapper.ppu_read(addr);
|
|
}
|
|
7 => {
|
|
let table = if (self.ctrl & 0x10) != 0 {
|
|
0x1000
|
|
} else {
|
|
0x0000
|
|
};
|
|
let fine_y = (self.vram_addr >> 12) & 0x7;
|
|
let addr = table + ((self.next_tile_id as u16) << 4) + fine_y + 8;
|
|
self.next_tile_msb = mapper.ppu_read(addr);
|
|
}
|
|
0 => {
|
|
self.reload_bg_shifters();
|
|
self.increment_coarse_x();
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
if rendering_active {
|
|
// Transfer pre-evaluated sprite list at the start of each visible scanline,
|
|
// so dots 1-256 render with the correct sprites for *this* scanline.
|
|
if scanline < 240 && dot == 1 && self.sprites_enabled() {
|
|
self.sprite_count = self.next_sprite_count;
|
|
self.sprite_indices = self.next_sprite_indices;
|
|
}
|
|
|
|
if dot == 256 {
|
|
self.increment_fine_y();
|
|
} else if dot == 257 {
|
|
self.copy_horizontal_bits();
|
|
if scanline < 240 {
|
|
if self.sprites_enabled() {
|
|
let next_scanline = (scanline as usize + 1) % 240;
|
|
let (count, indices, overflow) =
|
|
self.evaluate_sprites_for_scanline(next_scanline);
|
|
self.next_sprite_count = count;
|
|
self.next_sprite_indices = indices;
|
|
if overflow {
|
|
self.set_sprite_overflow(true);
|
|
}
|
|
}
|
|
} else if scanline == 261 {
|
|
let (count, indices, _) = self.evaluate_sprites_for_scanline(0);
|
|
self.next_sprite_count = count;
|
|
self.next_sprite_indices = indices;
|
|
}
|
|
} else if scanline == 261 && (280..=304).contains(&dot) {
|
|
self.copy_vertical_bits();
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn note_scroll_register_write(&mut self, scanline: usize, dot: u32) {
|
|
self.note_scroll_register_write_legacy(scanline, dot);
|
|
}
|
|
|
|
pub(super) fn background_pixel_from_shifters(&self) -> (u8, bool) {
|
|
let bit = 0x8000u16 >> self.fine_x;
|
|
let p0 = u8::from((self.bg_shift_pattern_lo & bit) != 0);
|
|
let p1 = u8::from((self.bg_shift_pattern_hi & bit) != 0);
|
|
let pix = p0 | (p1 << 1);
|
|
if pix == 0 {
|
|
return (self.read_palette(0), false);
|
|
}
|
|
let a0 = u8::from((self.bg_shift_attr_lo & bit) != 0);
|
|
let a1 = u8::from((self.bg_shift_attr_hi & bit) != 0);
|
|
let pal = (a0 | (a1 << 1)) << 2 | pix;
|
|
(self.read_palette(pal as u16), true)
|
|
}
|
|
|
|
pub(super) fn reload_bg_shifters(&mut self) {
|
|
self.bg_shift_pattern_lo = (self.bg_shift_pattern_lo & 0xFF00) | self.next_tile_lsb as u16;
|
|
self.bg_shift_pattern_hi = (self.bg_shift_pattern_hi & 0xFF00) | self.next_tile_msb as u16;
|
|
let attr_lo = if (self.next_tile_attr & 0x01) != 0 {
|
|
0xFF
|
|
} else {
|
|
0x00
|
|
};
|
|
let attr_hi = if (self.next_tile_attr & 0x02) != 0 {
|
|
0xFF
|
|
} else {
|
|
0x00
|
|
};
|
|
self.bg_shift_attr_lo = (self.bg_shift_attr_lo & 0xFF00) | attr_lo;
|
|
self.bg_shift_attr_hi = (self.bg_shift_attr_hi & 0xFF00) | attr_hi;
|
|
}
|
|
|
|
pub(super) fn increment_coarse_x(&mut self) {
|
|
if (self.vram_addr & 0x001F) == 31 {
|
|
self.vram_addr &= !0x001F;
|
|
self.vram_addr ^= 0x0400;
|
|
} else {
|
|
self.vram_addr = self.vram_addr.wrapping_add(1);
|
|
}
|
|
}
|
|
|
|
pub(super) fn increment_fine_y(&mut self) {
|
|
if (self.vram_addr & 0x7000) != 0x7000 {
|
|
self.vram_addr = self.vram_addr.wrapping_add(0x1000);
|
|
return;
|
|
}
|
|
self.vram_addr &= !0x7000;
|
|
let mut y = (self.vram_addr & 0x03E0) >> 5;
|
|
if y == 29 {
|
|
y = 0;
|
|
self.vram_addr ^= 0x0800;
|
|
} else if y == 31 {
|
|
y = 0;
|
|
} else {
|
|
y += 1;
|
|
}
|
|
self.vram_addr = (self.vram_addr & !0x03E0) | (y << 5);
|
|
}
|
|
|
|
pub(super) fn copy_horizontal_bits(&mut self) {
|
|
self.vram_addr = (self.vram_addr & !0x041F) | (self.temp_addr & 0x041F);
|
|
}
|
|
|
|
pub(super) fn copy_vertical_bits(&mut self) {
|
|
self.vram_addr = (self.vram_addr & !0x7BE0) | (self.temp_addr & 0x7BE0);
|
|
}
|
|
|
|
pub(super) fn evaluate_sprites_for_scanline(&self, y: usize) -> (u8, [u8; 8], bool) {
|
|
let mut indices = [0u8; 8];
|
|
let mut count = 0u8;
|
|
let sprite_height = if (self.ctrl & 0x20) != 0 { 16i16 } else { 8i16 };
|
|
let y = y as i16;
|
|
for i in 0..64usize {
|
|
let oam_idx = i * 4;
|
|
let sprite_y = self.oam[oam_idx] as i16 + 1;
|
|
if y >= sprite_y && y < sprite_y + sprite_height {
|
|
if count < 8 {
|
|
indices[count as usize] = i as u8;
|
|
count += 1;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
let overflow = self.sprite_overflow_on_scanline(y as usize);
|
|
(count, indices, overflow)
|
|
}
|
|
|
|
pub fn note_scroll_register_write_legacy(&mut self, scanline: usize, dot: u32) {
|
|
let mut target_scanline = scanline;
|
|
let mut x_start = 0u8;
|
|
if dot <= 256 {
|
|
if dot > 0 {
|
|
x_start = (dot - 1) as u8;
|
|
}
|
|
} else {
|
|
target_scanline = target_scanline.saturating_add(1);
|
|
}
|
|
|
|
if target_scanline >= VISIBLE_SCANLINES {
|
|
return;
|
|
}
|
|
|
|
let clamped = target_scanline as u8;
|
|
let event = self.current_scroll_event(clamped, x_start);
|
|
if let Some(last) = self.scroll_events.last_mut()
|
|
&& last.scanline == clamped
|
|
&& last.x_start == x_start
|
|
{
|
|
*last = event;
|
|
return;
|
|
}
|
|
self.scroll_events.push(event);
|
|
}
|
|
|
|
pub fn set_vblank(&mut self, enabled: bool) {
|
|
if enabled {
|
|
self.status |= 0x80;
|
|
} else {
|
|
self.status &= !0x80;
|
|
}
|
|
}
|
|
|
|
pub fn set_sprite0_hit(&mut self, enabled: bool) {
|
|
if enabled {
|
|
self.status |= 0x40;
|
|
} else {
|
|
self.status &= !0x40;
|
|
}
|
|
}
|
|
|
|
pub fn set_sprite_overflow(&mut self, enabled: bool) {
|
|
if enabled {
|
|
self.status |= 0x20;
|
|
} else {
|
|
self.status &= !0x20;
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub fn sprite_overflow_set(&self) -> bool {
|
|
(self.status & 0x20) != 0
|
|
}
|
|
|
|
pub fn sprite0_hit_set(&self) -> bool {
|
|
(self.status & 0x40) != 0
|
|
}
|
|
|
|
pub fn rendering_enabled(&self) -> bool {
|
|
(self.mask & 0x18) != 0
|
|
}
|
|
|
|
pub fn sprites_enabled(&self) -> bool {
|
|
(self.mask & 0x10) != 0
|
|
}
|
|
|
|
pub fn nmi_enabled(&self) -> bool {
|
|
(self.ctrl & 0x80) != 0
|
|
}
|
|
|
|
pub fn vblank_flag_set(&self) -> bool {
|
|
(self.status & 0x80) != 0
|
|
}
|
|
|
|
pub fn mmc3_scanline_clock_active(&self) -> bool {
|
|
let bg_enabled = (self.mask & 0x08) != 0;
|
|
let spr_enabled = (self.mask & 0x10) != 0;
|
|
if !bg_enabled && !spr_enabled {
|
|
return false;
|
|
}
|
|
|
|
let bg_uses_1000 = bg_enabled && (self.ctrl & 0x10) != 0;
|
|
let sprite_uses_1000 = if !spr_enabled {
|
|
false
|
|
} else if (self.ctrl & 0x20) != 0 {
|
|
// 8x16 sprites select pattern table per-tile; A12 activity is possible.
|
|
true
|
|
} else {
|
|
(self.ctrl & 0x08) != 0
|
|
};
|
|
|
|
bg_uses_1000 || sprite_uses_1000
|
|
}
|
|
}
|
|
|
|
mod memory;
|
|
mod persistence;
|
|
mod registers;
|
|
mod render;
|