Files
nesemu/src/native_core/ppu/api.rs
se.cherkasov bdf23de8db
Some checks failed
CI / rust (push) Has been cancelled
Initial commit: NES emulator with GTK4 desktop frontend
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.
2026-03-13 11:48:45 +03:00

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;