Initial commit: NES emulator with GTK4 desktop frontend
Some checks failed
CI / rust (push) Has been cancelled
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.
This commit is contained in:
387
src/native_core/ppu/api.rs
Normal file
387
src/native_core/ppu/api.rs
Normal file
@@ -0,0 +1,387 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user