mirror of
https://github.com/koute/pinky.git
synced 2024-06-01 02:48:08 -04:00
1759 lines
65 KiB
Rust
1759 lines
65 KiB
Rust
use std::mem;
|
|
use std::default::Default;
|
|
use std::slice;
|
|
|
|
use emumisc::{WrappingExtra, BitExtra, HiLoAccess, PeekPoke, At, is_b5_set, is_b6_set, is_b7_set, reverse_bits};
|
|
|
|
pub trait Context: Sized {
|
|
fn state_mut( &mut self ) -> &mut State;
|
|
fn state( &self ) -> &State;
|
|
|
|
fn on_cycle( &mut self ) {}
|
|
fn on_frame_was_generated( &mut self );
|
|
fn set_vblank_nmi( &mut self, value: bool );
|
|
fn peek_video_memory( &self, offset: u16 ) -> u8;
|
|
fn poke_video_memory( &mut self, offset: u16, value: u8 );
|
|
}
|
|
|
|
pub trait Interface: Sized + Context {
|
|
#[inline]
|
|
fn framebuffer( &self ) -> &Framebuffer {
|
|
Private::framebuffer( self )
|
|
}
|
|
|
|
#[inline]
|
|
fn swap_framebuffer( &mut self, other: Framebuffer ) -> Framebuffer {
|
|
Private::swap_framebuffer( self, other )
|
|
}
|
|
|
|
#[inline]
|
|
fn execute( &mut self ) {
|
|
Private::execute( self )
|
|
}
|
|
|
|
#[inline]
|
|
fn peek_ppustatus( &mut self ) -> u8 {
|
|
Private::peek_ppustatus( self )
|
|
}
|
|
|
|
#[inline]
|
|
fn poke_ppuctrl( &mut self, value: u8 ) {
|
|
Private::poke_ppuctrl( self, value )
|
|
}
|
|
|
|
#[inline]
|
|
fn poke_ppumask( &mut self, value: u8 ) {
|
|
Private::poke_ppumask( self, value )
|
|
}
|
|
|
|
#[inline]
|
|
fn poke_ppuaddr( &mut self, value: u8 ) {
|
|
Private::poke_ppuaddr( self, value )
|
|
}
|
|
|
|
#[inline]
|
|
fn poke_ppuscroll( &mut self, value: u8 ) {
|
|
Private::poke_ppuscroll( self, value )
|
|
}
|
|
|
|
#[inline]
|
|
fn poke_ppudata( &mut self, value: u8 ) {
|
|
Private::poke_ppudata( self, value )
|
|
}
|
|
|
|
#[inline]
|
|
fn peek_ppudata( &mut self ) -> u8 {
|
|
Private::peek_ppudata( self )
|
|
}
|
|
|
|
#[inline]
|
|
fn poke_oamaddr( &mut self, value: u8 ) {
|
|
Private::poke_oamaddr( self, value )
|
|
}
|
|
|
|
#[inline]
|
|
fn poke_oamdata( &mut self, value: u8 ) {
|
|
Private::poke_oamdata( self, value )
|
|
}
|
|
|
|
#[inline]
|
|
fn peek_oamdata( &mut self ) -> u8 {
|
|
Private::peek_oamdata( self )
|
|
}
|
|
|
|
#[inline]
|
|
fn poke_sprite_list_ram( &mut self, index: u8, value: u8 ) {
|
|
Private::poke_sprite_list_ram( self, index, value )
|
|
}
|
|
}
|
|
|
|
impl< T: Context > Interface for T {}
|
|
impl< T: Context > Private for T {}
|
|
|
|
pub struct State {
|
|
palette_ram: [u8; 32],
|
|
sprite_list_ram: [u8; 256],
|
|
secondary_sprite_list_ram: [u8; 32],
|
|
framebuffer: Framebuffer,
|
|
odd_frame_flag: bool,
|
|
vblank_flag_was_cleared: bool,
|
|
|
|
// Internal registers.
|
|
current_address: u16, // 15-bit
|
|
temporary_address: u16, // 15-bit
|
|
fine_horizontal_scroll: u8, // 3-bit
|
|
write_toggle: bool, // 1-bit
|
|
|
|
n_pixel: u16,
|
|
n_scanline: u16,
|
|
n_dot: u16,
|
|
bg_pattern_lo_shift_register: u16,
|
|
bg_pattern_hi_shift_register: u16,
|
|
bg_palette_index_lo_shift_register: u16,
|
|
bg_palette_index_hi_shift_register: u16,
|
|
|
|
sprites: [Sprite; 8],
|
|
|
|
sprite_list_address: u8,
|
|
ppudata_read_buffer: u8,
|
|
|
|
// I/O registers.
|
|
ppuctrl: PpuCtrl,
|
|
ppumask: PpuMask,
|
|
ppustatus: PpuStatus,
|
|
|
|
residual_data: u8,
|
|
|
|
address: u16,
|
|
skip_cycle_flag: bool,
|
|
skip_next_cycle: bool,
|
|
odd_cycle_flag: bool,
|
|
background_pattern_index_latch: u8,
|
|
background_palette_index_latch: u8,
|
|
tile_lo_latch: u8,
|
|
tile_hi_latch: u8,
|
|
scanline_counter: u16,
|
|
scanline_index: u8,
|
|
chunk_counter: u16,
|
|
chunk_index: u8,
|
|
action_index: u16,
|
|
auxiliary_sprite_list_address: u8,
|
|
secondary_sprite_list_address: u8,
|
|
sprite_list_data_latch: u8,
|
|
sprite_evaluation_mode: SpriteEvaluationMode,
|
|
sprite_pattern_index_latch: u8,
|
|
sprite_vertical_position_latch: u8,
|
|
sprite_attributes_latch: u8,
|
|
sprite_index: u8,
|
|
first_sprite_is_sprite_zero_on_current_scanline: bool,
|
|
first_sprite_is_sprite_zero_on_next_scanline: bool
|
|
}
|
|
|
|
impl State {
|
|
pub fn new() -> State {
|
|
State {
|
|
palette_ram: [0; 32],
|
|
sprite_list_ram: [0; 64 * 4],
|
|
secondary_sprite_list_ram: [0; 8 * 4],
|
|
framebuffer: Framebuffer::new(),
|
|
odd_frame_flag: false,
|
|
vblank_flag_was_cleared: false,
|
|
|
|
current_address: 0,
|
|
temporary_address: 0,
|
|
fine_horizontal_scroll: 0,
|
|
write_toggle: false,
|
|
|
|
n_pixel: 0,
|
|
n_scanline: 261, // We start on the prerender scanline.
|
|
n_dot: 0,
|
|
bg_pattern_lo_shift_register: 0,
|
|
bg_pattern_hi_shift_register: 0,
|
|
bg_palette_index_lo_shift_register: 0,
|
|
bg_palette_index_hi_shift_register: 0,
|
|
|
|
sprites: [Sprite::default(); 8],
|
|
|
|
sprite_list_address: 0,
|
|
ppudata_read_buffer: 0,
|
|
|
|
ppuctrl: PpuCtrl::new(),
|
|
ppumask: PpuMask::new(),
|
|
ppustatus: PpuStatus::new(),
|
|
|
|
residual_data: 0,
|
|
|
|
address: 0,
|
|
skip_cycle_flag: false,
|
|
skip_next_cycle: false,
|
|
odd_cycle_flag: false,
|
|
background_pattern_index_latch: 0,
|
|
background_palette_index_latch: 0,
|
|
tile_lo_latch: 0,
|
|
tile_hi_latch: 0,
|
|
scanline_index: SCANLINES.len() as u8 - 1, // Point at the prerender scanline.
|
|
scanline_counter: 0,
|
|
chunk_index: SCANLINES.last().unwrap().first_chunk_index,
|
|
chunk_counter: 0,
|
|
action_index: CHUNKS[ SCANLINES.last().unwrap().first_chunk_index as usize ].first_action_index,
|
|
auxiliary_sprite_list_address: 0,
|
|
secondary_sprite_list_address: 0,
|
|
sprite_list_data_latch: 0,
|
|
sprite_evaluation_mode: SpriteEvaluationMode::Search,
|
|
sprite_pattern_index_latch: 0,
|
|
sprite_vertical_position_latch: 0,
|
|
sprite_attributes_latch: 0,
|
|
sprite_index: 0,
|
|
first_sprite_is_sprite_zero_on_next_scanline: false,
|
|
first_sprite_is_sprite_zero_on_current_scanline: false
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
pub fn framebuffer( &self ) -> &Framebuffer {
|
|
&self.framebuffer
|
|
}
|
|
}
|
|
|
|
#[derive(Copy, Clone, PartialEq, Eq)]
|
|
struct PpuCtrl(u8);
|
|
|
|
impl PpuCtrl {
|
|
fn new() -> PpuCtrl {
|
|
PpuCtrl(0)
|
|
}
|
|
|
|
/* Bits 0 and 1 determine the base background tilemap address. */
|
|
|
|
bit!( 2, increment_vram_address_by_32 ); /* Whenever the internal address register will get incremented by
|
|
1 or by 32 after a PPUDATA read/write. */
|
|
bit!( 3, use_second_pattern_table_for_sprites );
|
|
bit!( 4, use_second_pattern_table_for_background );
|
|
bit!( 5, big_sprite_mode ); /* Whenever we use 8x8 sprites (== 0) or 8x16 sprites (== 16). */
|
|
bit!( 6, slave_mode ); /* This bit is useless; setting it to 1 can even damage an unmodified NES. */
|
|
bit!( 7, should_generate_vblank_nmi );
|
|
|
|
fn base_background_tilemap_address( self ) -> u16 {
|
|
match self.0 & 0b11 {
|
|
0 => 0x2000,
|
|
1 => 0x2400,
|
|
2 => 0x2800,
|
|
3 => 0x2C00,
|
|
_ => unsafe { fast_unreachable!() }
|
|
}
|
|
}
|
|
|
|
fn vram_address_increment( self ) -> u16 {
|
|
if self.increment_vram_address_by_32() == false {
|
|
1
|
|
} else {
|
|
32
|
|
}
|
|
}
|
|
|
|
fn sprite_pattern_table_address( self ) -> u16 {
|
|
if self.use_second_pattern_table_for_sprites() == false {
|
|
0
|
|
} else {
|
|
0x1000
|
|
}
|
|
}
|
|
|
|
fn background_pattern_table_address( self ) -> u16 {
|
|
if self.use_second_pattern_table_for_background() == false {
|
|
0
|
|
} else {
|
|
0x1000
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Copy, Clone, PartialEq, Eq)]
|
|
struct PpuMask(u8);
|
|
|
|
impl PpuMask {
|
|
fn new() -> PpuMask {
|
|
PpuMask(0)
|
|
}
|
|
|
|
bit!( 0, grayscale_mode );
|
|
bit!( 1, show_background_in_leftmost_8_pixels );
|
|
bit!( 2, show_sprites_in_leftmost_8_pixels );
|
|
bit!( 3, show_background );
|
|
bit!( 4, show_sprites );
|
|
bit!( 5, emphasize_red );
|
|
bit!( 6, emphasize_green );
|
|
bit!( 7, emphasize_blue );
|
|
|
|
#[inline]
|
|
fn color_emphasize_bits( &self ) -> u8 {
|
|
self.0.get_bits( 0b11100000 )
|
|
}
|
|
}
|
|
|
|
#[derive(Copy, Clone, PartialEq, Eq)]
|
|
struct PpuStatus(u8);
|
|
|
|
impl PpuStatus {
|
|
fn new() -> PpuStatus {
|
|
PpuStatus(0)
|
|
}
|
|
|
|
bit!( 5, sprite_overflow ); /* Set when more than 8 sprites appear on a scanline. */
|
|
bit!( 6, sprite_0_hit ); /* Set when a nonzero pixel of sprite 0 overlaps a nonzero background pixel. */
|
|
bit_setter!( 6, modify_sprite_0_hit );
|
|
bit!( 7, vblank_has_occured );
|
|
bit_setter!( 7, modify_vblank_has_occured );
|
|
}
|
|
|
|
/*
|
|
See Wikipedia[1] for an introduction.
|
|
|
|
[1] -- https://en.wikipedia.org/wiki/Picture_Processing_Unit
|
|
|
|
The NES has a system palette of 64 colors; the PPU stores 2 sets of palettes, one for the background,
|
|
and another for the sprites. One palette set is composed of 4 palettes, where each palette is
|
|
composed of four colors, each an index into the system palette. The first color in the first palette
|
|
in the first palette set is a shared background color (used for transparency); the first color in
|
|
other three palettes is not used by the PPU when normally rendering. The first color in the palettes
|
|
in the second palette set are aliased to those from the first palette set.
|
|
|
|
The PPU has access to 256 tiles of raw pixel art (commonly called "pattern table", or CHR), where each
|
|
tile has 2 bit depth. The tiles are stored as two 1-bit images - the first 8 bytes store the low
|
|
bits of the 8x8 tile, and the next 8 bytes store the high bits. Each 2-bit pixel is an index into
|
|
the palette.
|
|
|
|
The background is composed of 32x30 tiles, each 8x8 pixels big; the PPU has access to a 960 byte long
|
|
tile map (commonly called "nametable") where each byte is an index into the pattern table. After each 960 byte
|
|
long background tilemap follows a 64 byte long table (commonly called "attribute table") of packed palette
|
|
indexes; this table controls which of the 4 palettes a given chunk of the background uses. Each 16x16 pixel area
|
|
has one palette assigned to it and one byte controls four 16x16 blocks.
|
|
|
|
Consider this 32x32 pixel big chunk (or four tiles, each 8x8 pixel big, in a 2x2 grid) of the background:
|
|
|
|
-------------------------------------------------------------------------
|
|
| A A A A A A A A | A A A A A A A A | B B B B B B B B | B B B B B B B B |
|
|
| A A A A A A A A | A A A A A A A A | B B B B B B B B | B B B B B B B B |
|
|
| A A A A A A A A | A A A A A A A A | B B B B B B B B | B B B B B B B B |
|
|
| A A A A A A A A | A A A A A A A A | B B B B B B B B | B B B B B B B B |
|
|
| A A A A A A A A | A A A A A A A A | B B B B B B B B | B B B B B B B B |
|
|
| A A A A A A A A | A A A A A A A A | B B B B B B B B | B B B B B B B B |
|
|
| A A A A A A A A | A A A A A A A A | B B B B B B B B | B B B B B B B B |
|
|
| A A A A A A A A | A A A A A A A A | B B B B B B B B | B B B B B B B B |
|
|
-------------------------------------------------------------------------
|
|
| A A A A A A A A | A A A A A A A A | B B B B B B B B | B B B B B B B B |
|
|
| A A A A A A A A | A A A A A A A A | B B B B B B B B | B B B B B B B B |
|
|
| A A A A A A A A | A A A A A A A A | B B B B B B B B | B B B B B B B B |
|
|
| A A A A A A A A | A A A A A A A A | B B B B B B B B | B B B B B B B B |
|
|
| A A A A A A A A | A A A A A A A A | B B B B B B B B | B B B B B B B B |
|
|
| A A A A A A A A | A A A A A A A A | B B B B B B B B | B B B B B B B B |
|
|
| A A A A A A A A | A A A A A A A A | B B B B B B B B | B B B B B B B B |
|
|
| A A A A A A A A | A A A A A A A A | B B B B B B B B | B B B B B B B B |
|
|
-------------------------------------------------------------------------
|
|
| C C C C C C C C | C C C C C C C C | D D D D D D D D | D D D D D D D D |
|
|
| C C C C C C C C | C C C C C C C C | D D D D D D D D | D D D D D D D D |
|
|
| C C C C C C C C | C C C C C C C C | D D D D D D D D | D D D D D D D D |
|
|
| C C C C C C C C | C C C C C C C C | D D D D D D D D | D D D D D D D D |
|
|
| C C C C C C C C | C C C C C C C C | D D D D D D D D | D D D D D D D D |
|
|
| C C C C C C C C | C C C C C C C C | D D D D D D D D | D D D D D D D D |
|
|
| C C C C C C C C | C C C C C C C C | D D D D D D D D | D D D D D D D D |
|
|
| C C C C C C C C | C C C C C C C C | D D D D D D D D | D D D D D D D D |
|
|
-------------------------------------------------------------------------
|
|
| C C C C C C C C | C C C C C C C C | D D D D D D D D | D D D D D D D D |
|
|
| C C C C C C C C | C C C C C C C C | D D D D D D D D | D D D D D D D D |
|
|
| C C C C C C C C | C C C C C C C C | D D D D D D D D | D D D D D D D D |
|
|
| C C C C C C C C | C C C C C C C C | D D D D D D D D | D D D D D D D D |
|
|
| C C C C C C C C | C C C C C C C C | D D D D D D D D | D D D D D D D D |
|
|
| C C C C C C C C | C C C C C C C C | D D D D D D D D | D D D D D D D D |
|
|
| C C C C C C C C | C C C C C C C C | D D D D D D D D | D D D D D D D D |
|
|
| C C C C C C C C | C C C C C C C C | D D D D D D D D | D D D D D D D D |
|
|
-------------------------------------------------------------------------
|
|
|
|
Every two bits of a byte from the packed palette indexes table control the palette of one 16x16 pixel region:
|
|
|
|
| Bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
|
|
| Area | D | D | C | C | B | B | A | A |
|
|
|
|
Assuming we know the X and Y coordinates of the current tile we can calculate:
|
|
packed_palette_indexes_address = tilemap_address + 960 + tile_y / 4 * (32 / 4) + tile_x / 4
|
|
where:
|
|
tilemap_address -> see the memory map for details
|
|
960 -> length of a tilemap
|
|
tile_x -> X coordinate of an 8x8 pixel tile, <0, 32)
|
|
tile_y -> Y coordinate of an 8x8 pixel tile, <0, 30)
|
|
tile_[x|y] / 4 -> since one byte of the packed palette index table encodes information
|
|
for a 4x4 block of tiles
|
|
(32 / 4) -> there are 32 tiles in a row and for every 4 of those tiles we have one byte of
|
|
data in the packed palette index table
|
|
|
|
We can also figure out which bits correspond to the current tile:
|
|
bits_to_shift = (tile_x & 2) + (tile_y & 2) * 2
|
|
palette_index = (packed_palette_indexes >> bits_to_shift) & 0b11
|
|
where:
|
|
tile_[x|y] & 2 -> For <0, inf) this produces: 0, 0, 2, 2, 0, 0, 2, 2, ...
|
|
|
|
The 2-bit pixel values from the pattern table are used as an index into the palette specified
|
|
by the packed palette index table to get the system palette index of the pixel.
|
|
|
|
8-bit index 2-bit pixel 4-bit index
|
|
Background Tile Index -> Pattern Table ----------------------------------------+-------------> System Palette -> Pixel Color
|
|
| |
|
|
| 2-bit palette index |
|
|
-> Packed Palette Index Table -> Background Palette Set -^
|
|
|
|
PPU memory map:
|
|
| | |
|
|
| RANGE | SIZE | CONTENTS
|
|
| | |
|
|
----------------------------------------------------------------------------
|
|
| 0x0000...0x0FFF | 4kb | Pattern table #0 (256 tiles, each 8x8 in size, each 16 bytes long)
|
|
| 0x1000...0x1FFF | 4kb | Pattern table #1
|
|
| 0x2000...0x23FF | 1kb | Background tilemap #0 (960 byte long tile map and 64 byte long packed palette index table)
|
|
| 0x2400...0x27FF | 1kb | Background tilemap #1
|
|
| 0x2800...0x2BFF | 1kb | Background tilemap #2
|
|
| 0x2C00...0x2FFF | 1kb | Background tilemap #3
|
|
| 0x3000...0x3EFF | 3840 | Mirror of 0x2000...0x2EFF
|
|
| 0x3F00...0x3F1F | 32b | Palette RAM (16 byte long background palettes and 16 byte long sprite palettes)
|
|
| 0x3F20...0x3FFF | 224b | Mirrors of palette RAM (7 in total)
|
|
| 0x4000...0xFFFF | | Mirrors of 0x0000...0x3FFF
|
|
|
|
0x0000...0x1FFF is mapped by the cartridge to either a VRAM or a VROM area on the cartridge itself.
|
|
A cartridge can have either of those, or even both, and can have multiple banks that it can switch
|
|
between. It can also map the 0x2000...0x2FFF area.
|
|
|
|
The PPU has only 2kb of RAM for the background tilemaps, so by default it can only hold two
|
|
background tilemaps simultaneously. The way the physical background tilemaps are mapped to logical
|
|
ones is determined by the cartridge; some of the possible configurations are:
|
|
- Horizontal
|
|
#0 and #1 point to the same background tilemap
|
|
#2 and #3 point to the same background tilemap
|
|
- Vertical
|
|
#0 and #2 point to the same background tilemap
|
|
#1 and #4 point to the same background tilemap
|
|
- Single screen
|
|
#0, #1, #2 and #3 all point to the same background tilemap
|
|
- No mirroring
|
|
All of the background tilemaps are distinct (requires extra RAM on the cartridge)
|
|
- Other
|
|
Diagonal, L-shaped, 3-screen vertical, 3-screen horizontal, etc.
|
|
|
|
The whole 0x0000...0x3EFF range can be mapped by the circuitry on the cartridge.
|
|
For more details see http://wiki.nesdev.com/w/index.php/Mirroring
|
|
|
|
(The following is strictly true for NTSC only.)
|
|
PPU drawing procedure:
|
|
1. PPU renders dummy scanline; on every second frame this scanline is 1 pixel shorter at the end of the scanline;
|
|
the VINT flag is cleared on the second tick of this scanline.
|
|
2. PPU renders 240 visible scanlines.
|
|
3. PPU waits for 1 scanline.
|
|
4. PPU waits for 20 scanlines; the VINT flag is set on the second tick of the first scanline.
|
|
|
|
The VINT flag is also cleared after 0x2002 (PPUSTATUS) is read.
|
|
|
|
The PPU renders 262 scanlines per frame in total.
|
|
Each scanline takes 341 PPU cycles, with each cycle producing one pixel.
|
|
*/
|
|
|
|
pub struct Framebuffer {
|
|
buffer: Box< [u16; 256 * 240] >
|
|
}
|
|
|
|
impl PartialEq for Framebuffer {
|
|
fn eq( &self, other: &Framebuffer ) -> bool {
|
|
&self.buffer[..] == &other.buffer[..]
|
|
}
|
|
}
|
|
|
|
impl Eq for Framebuffer {}
|
|
|
|
impl Framebuffer {
|
|
fn new() -> Framebuffer {
|
|
Framebuffer::default()
|
|
}
|
|
|
|
pub fn iter< 'a >( &'a self ) -> FramebufferIterator< 'a > {
|
|
FramebufferIterator {
|
|
iter: self.buffer.iter()
|
|
}
|
|
}
|
|
|
|
pub fn convert_to_abgr< T: AsMut< [u32] >>( &self, palette: &Palette, mut output: T ) {
|
|
let array = output.as_mut();
|
|
assert_eq!( array.len(), 256 * 240 );
|
|
|
|
for (pixel, out) in self.iter().zip( array.iter_mut() ) {
|
|
// TODO
|
|
debug_assert!( pixel.is_red_emphasized() == false );
|
|
debug_assert!( pixel.is_green_emphasized() == false );
|
|
debug_assert!( pixel.is_blue_emphasized() == false );
|
|
*out = palette.get_packed_abgr( pixel.color_in_system_palette_index() );
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Copy, Clone, PartialEq, Eq)]
|
|
pub struct FramebufferPixel( u16 );
|
|
|
|
impl FramebufferPixel {
|
|
#[inline]
|
|
pub fn color_in_system_palette_index( &self ) -> u8 {
|
|
(self.0 & 0xFF) as u8
|
|
}
|
|
|
|
#[inline]
|
|
pub fn is_red_emphasized( &self ) -> bool {
|
|
self.0.get_bits( 0b00000001_00000000 ) != 0
|
|
}
|
|
|
|
#[inline]
|
|
pub fn is_green_emphasized( &self ) -> bool {
|
|
self.0.get_bits( 0b00000010_00000000 ) != 0
|
|
}
|
|
|
|
#[inline]
|
|
pub fn is_blue_emphasized( &self ) -> bool {
|
|
self.0.get_bits( 0b00000100_00000000 ) != 0
|
|
}
|
|
}
|
|
|
|
pub struct FramebufferIterator< 'a > {
|
|
iter: slice::Iter< 'a, u16 >
|
|
}
|
|
|
|
impl< 'a > Iterator for FramebufferIterator< 'a > {
|
|
type Item = FramebufferPixel;
|
|
|
|
#[inline]
|
|
fn next( &mut self ) -> Option< Self::Item > {
|
|
self.iter.next().map( |&raw_pixel| FramebufferPixel( raw_pixel ) )
|
|
}
|
|
}
|
|
|
|
impl Default for Framebuffer {
|
|
fn default() -> Self {
|
|
Framebuffer {
|
|
buffer: Box::new( [0; 256 * 240] )
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
The NES doesn't output an RGB signal; it directly outputs analog video signal, hence
|
|
there is a multitude of ways of interpreting the colors it generates.
|
|
*/
|
|
pub struct Palette( [u32; 64] );
|
|
|
|
impl Palette {
|
|
pub fn new( data: &[u8] ) -> Palette {
|
|
assert_eq!( data.len(), 192 );
|
|
let mut output = [0; 64];
|
|
for (index, components) in data.chunks( 3 ).enumerate() {
|
|
let r = components[0] as u32;
|
|
let g = components[1] as u32;
|
|
let b = components[2] as u32;
|
|
output[ index ] = r | g << 8 | b << 16 | 0xFF << 24;
|
|
}
|
|
|
|
Palette( output )
|
|
}
|
|
|
|
pub fn get_packed_abgr( &self, index: u8 ) -> u32 {
|
|
debug_assert!( index < 64 );
|
|
self.0.peek( index )
|
|
}
|
|
|
|
pub fn get_rgb( &self, index: u8 ) -> (u8, u8, u8) {
|
|
let value = self.get_packed_abgr( index );
|
|
let r = (value & 0x000000FF) as u8;
|
|
let g = ((value & 0x0000FF00) >> 8) as u8;
|
|
let b = ((value & 0x00FF0000) >> 16) as u8;
|
|
|
|
(r, g, b)
|
|
}
|
|
}
|
|
|
|
// Source: http://forums.nesdev.com/viewtopic.php?p=150239#p150239
|
|
static DEFAULT_PALETTE: &'static [u8] = include_bytes!( "../data/FBX-Final.pal" );
|
|
|
|
impl Default for Palette {
|
|
fn default() -> Palette {
|
|
Palette::new( DEFAULT_PALETTE )
|
|
}
|
|
}
|
|
|
|
enum SpriteEvaluationMode {
|
|
Search,
|
|
Copy,
|
|
Idle
|
|
}
|
|
|
|
#[derive(Copy, Clone)]
|
|
struct Sprite {
|
|
pattern_lo_shift_register: u8,
|
|
pattern_hi_shift_register: u8,
|
|
attributes_latch: u8,
|
|
dots_until_is_displayed: u8
|
|
}
|
|
|
|
impl Sprite {
|
|
#[inline]
|
|
fn display_sprite_behind_background( &self ) -> bool {
|
|
is_b5_set( self.attributes_latch )
|
|
}
|
|
|
|
#[inline]
|
|
fn palette_index( &self ) -> u8 {
|
|
self.attributes_latch & 0b11
|
|
}
|
|
}
|
|
|
|
impl Default for Sprite {
|
|
fn default() -> Self {
|
|
Sprite {
|
|
pattern_lo_shift_register: 0,
|
|
pattern_hi_shift_register: 0,
|
|
attributes_latch: 0,
|
|
dots_until_is_displayed: 0
|
|
}
|
|
}
|
|
}
|
|
|
|
ppu_scheduling_logic!();
|
|
|
|
trait Private: Sized + Context {
|
|
fn fetch( &mut self ) -> u8 {
|
|
self.peek( self.state().address )
|
|
}
|
|
|
|
fn sprite_evaluation( &mut self ) {
|
|
if self.state().odd_cycle_flag {
|
|
// On odd cycles data is read from the primary sprite list.
|
|
self.state_mut().sprite_list_data_latch = self.state().sprite_list_ram.peek( self.state().auxiliary_sprite_list_address );
|
|
self.state_mut().auxiliary_sprite_list_address.wrapping_inc();
|
|
} else {
|
|
// On even cycles data is written to the secondary sprite list.
|
|
match self.state().sprite_evaluation_mode {
|
|
SpriteEvaluationMode::Search => {
|
|
{
|
|
let address = self.state().secondary_sprite_list_address;
|
|
let value = self.state().sprite_list_data_latch;
|
|
self.poke_secondary_sprite_list_ram( address, value );
|
|
}
|
|
|
|
let y = self.state().sprite_list_data_latch as u16;
|
|
let is_in_range = if !self.state().ppuctrl.big_sprite_mode() {
|
|
(self.state().n_scanline >= y) && (self.state().n_scanline < y + 8)
|
|
} else {
|
|
(self.state().n_scanline >= y) && (self.state().n_scanline < y + 16)
|
|
};
|
|
|
|
if is_in_range {
|
|
if self.state_mut().secondary_sprite_list_address == 0 {
|
|
self.state_mut().first_sprite_is_sprite_zero_on_next_scanline = self.state().auxiliary_sprite_list_address == 1;
|
|
}
|
|
|
|
self.state_mut().sprite_evaluation_mode = SpriteEvaluationMode::Copy;
|
|
self.state_mut().secondary_sprite_list_address += 1;
|
|
} else {
|
|
self.state_mut().auxiliary_sprite_list_address = self.state().auxiliary_sprite_list_address.wrapping_add( 3 );
|
|
//if self.state().auxiliary_sprite_list_address >= 255 {
|
|
// self.state_mut().sprite_evaluation_mode = SpriteEvaluationMode::Idle;
|
|
//}
|
|
}
|
|
},
|
|
SpriteEvaluationMode::Copy => {
|
|
{
|
|
let address = self.state().secondary_sprite_list_address;
|
|
let value = self.state().sprite_list_data_latch;
|
|
self.poke_secondary_sprite_list_ram( address, value );
|
|
}
|
|
|
|
self.state_mut().secondary_sprite_list_address += 1;
|
|
if self.state().secondary_sprite_list_address % 4 == 0 {
|
|
if self.state().secondary_sprite_list_address / 4 == 8 /*|| self.state().auxiliary_sprite_list_address >= 255*/ {
|
|
// TODO: This isn't accurate.
|
|
self.state_mut().sprite_evaluation_mode = SpriteEvaluationMode::Idle;
|
|
} else {
|
|
self.state_mut().sprite_evaluation_mode = SpriteEvaluationMode::Search;
|
|
}
|
|
}
|
|
},
|
|
SpriteEvaluationMode::Idle => {}
|
|
}
|
|
}
|
|
self.state_mut().odd_cycle_flag = !self.state().odd_cycle_flag;
|
|
}
|
|
|
|
fn current_sprite_mut( &mut self ) -> &mut Sprite {
|
|
let index = self.state().sprite_index;
|
|
self.state_mut().sprites.at_mut( index )
|
|
}
|
|
|
|
fn clear_secondary_sprite_ram_cell( &mut self ) {
|
|
if self.state().odd_cycle_flag {
|
|
|
|
} else {
|
|
{
|
|
let address = self.state().secondary_sprite_list_address;
|
|
self.state_mut().secondary_sprite_list_ram.poke( address, 0xFF );
|
|
}
|
|
self.state_mut().secondary_sprite_list_address += 1;
|
|
if self.state().secondary_sprite_list_address == 32 {
|
|
for i in 0..32 {
|
|
let value = self.state().secondary_sprite_list_ram[i];
|
|
debug_assert_eq!( value, 0xFF );
|
|
}
|
|
}
|
|
self.state_mut().secondary_sprite_list_address = self.state().secondary_sprite_list_address & (32 - 1);
|
|
}
|
|
|
|
self.state_mut().odd_cycle_flag = !self.state().odd_cycle_flag;
|
|
}
|
|
|
|
fn shift_sprite_registers( &mut self ) {
|
|
for sprite in self.state_mut().sprites.iter_mut() {
|
|
if sprite.dots_until_is_displayed == 0 {
|
|
sprite.pattern_lo_shift_register <<= 1;
|
|
sprite.pattern_hi_shift_register <<= 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
fn decrement_sprite_horizontal_counters( &mut self ) {
|
|
for sprite in self.state_mut().sprites.iter_mut() {
|
|
if sprite.dots_until_is_displayed != 0 {
|
|
sprite.dots_until_is_displayed -= 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
fn update_sprite_registers( &mut self ) {
|
|
for sprite in self.state_mut().sprites.iter_mut() {
|
|
if sprite.dots_until_is_displayed == 0 {
|
|
sprite.pattern_lo_shift_register <<= 1;
|
|
sprite.pattern_hi_shift_register <<= 1;
|
|
} else {
|
|
sprite.dots_until_is_displayed -= 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
fn reload_background_shift_registers( &mut self ) {
|
|
self.state_mut().bg_pattern_lo_shift_register = (self.state().bg_pattern_lo_shift_register & 0xFF00) | (self.state().tile_lo_latch as u16);
|
|
self.state_mut().bg_pattern_hi_shift_register = (self.state().bg_pattern_hi_shift_register & 0xFF00) | (self.state().tile_hi_latch as u16);
|
|
self.state_mut().bg_palette_index_lo_shift_register = (self.state().bg_palette_index_lo_shift_register & 0xFF00) | (((self.state().background_palette_index_latch as u16 >> 0) & 1) * 0x00FF);
|
|
self.state_mut().bg_palette_index_hi_shift_register = (self.state().bg_palette_index_hi_shift_register & 0xFF00) | (((self.state().background_palette_index_latch as u16 >> 1) & 1) * 0x00FF);
|
|
}
|
|
|
|
#[inline]
|
|
fn sprite_tile_lo_address( &self ) -> u16 {
|
|
let flip_vertically = is_b7_set( self.state().sprite_attributes_latch );
|
|
|
|
if self.state().ppuctrl.big_sprite_mode() == false {
|
|
let index = self.state().sprite_pattern_index_latch;
|
|
let mut tile_y = (self.state().n_scanline as i16 - self.state().sprite_vertical_position_latch as i16) & 7;
|
|
debug_assert!( tile_y < 8 );
|
|
|
|
if flip_vertically {
|
|
tile_y = (8 - 1) - tile_y;
|
|
}
|
|
|
|
self.state().ppuctrl.sprite_pattern_table_address() + index as u16 * 16 + tile_y as u16
|
|
} else {
|
|
// In double height sprite mode the pattern table is not selected
|
|
// by the ppuctrl; it's taken from the least significant bit of
|
|
// the sprite's index attribute.
|
|
let pattern_table_address = if self.state().sprite_pattern_index_latch & 1 == 0 {
|
|
0x0000
|
|
} else {
|
|
0x1000
|
|
};
|
|
|
|
// The top and the bottom 8x8 sprite patterns that compose a single
|
|
// 8x16 sprite are laid out in memory interleaved, e.g.:
|
|
// sprite0_top, sprite0_bottom, sprite1_top, sprite1_bottom, ...
|
|
// so we mask out the least significant bit of the index to get
|
|
// the index of the top sprite.
|
|
let top_sprite_index = self.state().sprite_pattern_index_latch & (!1);
|
|
let bottom_sprite_index = top_sprite_index + 1;
|
|
|
|
let sprite_y = (self.state().n_scanline as i16 - self.state().sprite_vertical_position_latch as i16) & 15;
|
|
let mut is_upper_sprite = sprite_y < 8;
|
|
|
|
if flip_vertically {
|
|
// The top and the bottom tiles of the sprite are swapped.
|
|
is_upper_sprite = !is_upper_sprite;
|
|
}
|
|
|
|
let mut tile_y = if sprite_y < 8 {
|
|
sprite_y
|
|
} else {
|
|
sprite_y - 8
|
|
};
|
|
|
|
let index = if is_upper_sprite {
|
|
top_sprite_index
|
|
} else {
|
|
bottom_sprite_index
|
|
};
|
|
|
|
if flip_vertically {
|
|
tile_y = (8 - 1) - tile_y;
|
|
}
|
|
|
|
pattern_table_address + index as u16 * 16 + tile_y as u16
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
fn sprite_tile_hi_address( &self ) -> u16 {
|
|
self.sprite_tile_lo_address() + 8
|
|
}
|
|
|
|
#[inline]
|
|
fn sprite_tile_lo_address_lo( &self ) -> u8 {
|
|
self.sprite_tile_lo_address() as u8
|
|
}
|
|
|
|
#[inline]
|
|
fn sprite_tile_lo_address_hi( &self ) -> u8 {
|
|
(self.sprite_tile_lo_address() >> 8) as u8
|
|
}
|
|
|
|
#[inline]
|
|
fn sprite_tile_hi_address_lo( &self ) -> u8 {
|
|
self.sprite_tile_hi_address() as u8
|
|
}
|
|
|
|
#[inline]
|
|
fn sprite_tile_hi_address_hi( &self ) -> u8 {
|
|
(self.sprite_tile_hi_address() >> 8) as u8
|
|
}
|
|
|
|
fn framebuffer( &self ) -> &Framebuffer {
|
|
&self.state().framebuffer
|
|
}
|
|
|
|
fn swap_framebuffer( &mut self, mut other: Framebuffer ) -> Framebuffer {
|
|
mem::swap( &mut self.state_mut().framebuffer, &mut other );
|
|
other
|
|
}
|
|
|
|
fn is_rendering_enabled( &self ) -> bool {
|
|
self.state().ppumask.show_background() || self.state().ppumask.show_sprites()
|
|
}
|
|
|
|
fn local_pixel_coordinate_x( &self ) -> u8 {
|
|
self.state().fine_horizontal_scroll
|
|
}
|
|
|
|
fn local_pixel_coordinate_y( &self ) -> u8 {
|
|
debug_assert!( (self.state().current_address & (1 << 15)) == 0 ); // The register is only 15-bit.
|
|
self.state().current_address.get_bits( 0b0111000000000000 ) as u8
|
|
}
|
|
|
|
fn set_local_pixel_coordinate_y( &mut self, value: u8 ) {
|
|
self.state_mut().current_address.replace_bits( 0b0111000000000000, value as u16 );
|
|
}
|
|
|
|
fn increment_horizontal_counters( &mut self ) {
|
|
if !self.rendering_is_enabled() {
|
|
return;
|
|
}
|
|
if self.tile_x() == 0b11111 {
|
|
self.state_mut().current_address ^= 1 << 10; // Select next horizontal tilemap.
|
|
self.set_tile_x( 0 );
|
|
} else {
|
|
let value = self.tile_x() + 1;
|
|
self.set_tile_x( value );
|
|
}
|
|
}
|
|
|
|
fn increment_vertical_counters( &mut self ) {
|
|
if !self.rendering_is_enabled() {
|
|
return;
|
|
}
|
|
let local_y = self.local_pixel_coordinate_y();
|
|
if local_y != 0b111 {
|
|
self.set_local_pixel_coordinate_y( local_y + 1 );
|
|
} else {
|
|
self.set_local_pixel_coordinate_y( 0 );
|
|
let tile_y = self.tile_y();
|
|
if tile_y == 29 {
|
|
self.set_tile_y( 0 );
|
|
self.state_mut().current_address ^= 1 << 11; // Select next vertical tilemap.
|
|
} else if tile_y == 31 {
|
|
// The tile Y coordinate can be manually set to be out-of-bounds.
|
|
self.set_tile_y( 0 );
|
|
} else {
|
|
self.set_tile_y( tile_y + 1 );
|
|
}
|
|
}
|
|
}
|
|
|
|
fn reset_horizontal_counters( &mut self ) {
|
|
if self.rendering_is_enabled() {
|
|
let mask = 0b0000010000011111;
|
|
let value = self.state().temporary_address;
|
|
self.state_mut().current_address.replace_bits( mask, value );
|
|
}
|
|
}
|
|
|
|
fn reset_vertical_counters( &mut self ) {
|
|
if self.rendering_is_enabled() {
|
|
let mask = 0b0111101111100000;
|
|
let value = self.state().temporary_address;
|
|
self.state_mut().current_address.replace_bits( mask, value );
|
|
}
|
|
}
|
|
|
|
fn tile_x( &self ) -> u8 {
|
|
(self.state().current_address >> 0) as u8 & 0b11111
|
|
}
|
|
|
|
fn set_tile_x( &mut self, value: u8 ) {
|
|
self.state_mut().current_address.replace_bits( 0b11111, value as u16 );
|
|
}
|
|
|
|
fn tile_y( &self ) -> u8 {
|
|
(self.state().current_address >> 5) as u8 & 0b11111
|
|
}
|
|
|
|
fn set_tile_y( &mut self, value: u8 ) {
|
|
self.state_mut().current_address.replace_bits( 0b1111100000, value as u16 );
|
|
}
|
|
|
|
fn tilemap_address( &self ) -> u16 {
|
|
0x2000 + (self.state().current_address & 0b0000110000000000)
|
|
}
|
|
|
|
fn pattern_index_address( &self ) -> u16 {
|
|
(self.tilemap_address() + (self.tile_x() as u16 + self.tile_y() as u16 * 32)) & 0b0011111111111111
|
|
}
|
|
|
|
fn packed_palette_indexes_address( &self ) -> u16 {
|
|
let offset = if self.state().n_dot > 256 && self.state().n_dot < 321 {
|
|
// This is for the garbage fetches during the sprite fetching.
|
|
// Not sure if this is how it's supposed to be done, but it seems
|
|
// to match up with the real PPU behavior so far.
|
|
self.state().current_address & 0xFF
|
|
} else {
|
|
960
|
|
};
|
|
|
|
((self.tilemap_address() + offset) + (self.tile_y() as u16 / 4 * (32 / 4)) + (self.tile_x() as u16 / 4))
|
|
}
|
|
|
|
fn bg_tile_lo_address( &self, index: u8 ) -> u16 {
|
|
(self.state().ppuctrl.background_pattern_table_address() + index as u16 * 16 + self.local_pixel_coordinate_y() as u16)
|
|
}
|
|
|
|
fn bg_tile_hi_address( &self, index: u8 ) -> u16 {
|
|
(self.state().ppuctrl.background_pattern_table_address() + index as u16 * 16 + 8 + self.local_pixel_coordinate_y() as u16)
|
|
}
|
|
|
|
fn background_pixel( &self ) -> (u8, u8) {
|
|
if self.state().ppumask.show_background() && (self.state().ppumask.show_background_in_leftmost_8_pixels() || self.state().n_dot >= 8) {
|
|
let shift = 15 - self.local_pixel_coordinate_x();
|
|
let pattern_lo = (self.state().bg_pattern_lo_shift_register >> shift) & 1; // << (7 - self.local_pixel_coordinate_x())) & 1;
|
|
let pattern_hi = (self.state().bg_pattern_hi_shift_register >> shift) & 1; // (7 - self.local_pixel_coordinate_x())) & 1;
|
|
let palette_index_lo = (self.state().bg_palette_index_lo_shift_register >> shift) & 1;
|
|
let palette_index_hi = (self.state().bg_palette_index_hi_shift_register >> shift) & 1;
|
|
|
|
let color_in_palette_index = pattern_lo | (pattern_hi << 1);
|
|
let palette_index = palette_index_lo | (palette_index_hi << 1);
|
|
|
|
(palette_index as u8, color_in_palette_index as u8)
|
|
} else {
|
|
(0, 0)
|
|
}
|
|
}
|
|
|
|
fn sprite_pixel( &self ) -> (u8, u8, bool, bool) {
|
|
if self.state().ppumask.show_sprites() && (self.state().ppumask.show_sprites_in_leftmost_8_pixels() || self.state().n_dot >= 8) {
|
|
for (nth, sprite) in self.state().sprites.iter().enumerate() {
|
|
if sprite.dots_until_is_displayed != 0 {
|
|
continue;
|
|
}
|
|
|
|
let pattern_lo = (sprite.pattern_lo_shift_register >> 7) & 1;
|
|
let pattern_hi = (sprite.pattern_hi_shift_register >> 7) & 1;
|
|
if pattern_lo == 0 && pattern_hi == 0 {
|
|
continue;
|
|
}
|
|
|
|
let color_in_palette_index = pattern_lo | (pattern_hi << 1);
|
|
let palette_index = 4 + sprite.palette_index();
|
|
|
|
return (palette_index, color_in_palette_index, sprite.display_sprite_behind_background(), nth == 0);
|
|
}
|
|
}
|
|
|
|
(0, 0, true, false)
|
|
}
|
|
|
|
fn pixel( &mut self ) -> (u8, u8) {
|
|
let (background_palette_index, background_color_index) = self.background_pixel();
|
|
let (sprite_palette_index, sprite_color_index, display_sprite_behind_background, is_sprite_zero) = self.sprite_pixel();
|
|
|
|
// Since sprite 0 hit doesn't happen on dot 255 I'm also assuming
|
|
// that we're not supposed to draw any sprites that have their
|
|
// position set to 255. If we do draw sprites on dot 255 then we
|
|
// usually get this ugly vertical line there since the sprites'
|
|
// position is cleared to be 0xFF by default, so any unused sprites
|
|
// end up on dot 255.
|
|
if self.state().n_dot == 255 {
|
|
return (background_palette_index, background_color_index);
|
|
}
|
|
|
|
if self.state().first_sprite_is_sprite_zero_on_current_scanline && sprite_color_index != 0 && background_color_index != 0 && is_sprite_zero {
|
|
self.state_mut().ppustatus.modify_sprite_0_hit( true );
|
|
}
|
|
|
|
match (background_color_index, sprite_color_index, display_sprite_behind_background) {
|
|
(0, 0, _) => (0, 0),
|
|
(0, _, _) => (sprite_palette_index, sprite_color_index),
|
|
(_, 0, _) => (background_palette_index, background_color_index),
|
|
(_, _, false) => (sprite_palette_index, sprite_color_index),
|
|
(_, _, true) => (background_palette_index, background_color_index)
|
|
}
|
|
}
|
|
|
|
fn output_pixel( &mut self ) {
|
|
let (palette_index, color_in_palette_index) = self.pixel();
|
|
|
|
let color_in_system_palette_index = self.peek( 0x3F00 + (palette_index as u16 * 4) + color_in_palette_index as u16 );
|
|
// let color_in_system_palette_index = self.state_mut().palette_ram.peek( (palette_index as usize * 4) + color_in_palette_index as usize );
|
|
let nth = self.state().n_pixel;
|
|
let pixel = ((self.state().ppumask.color_emphasize_bits() as u16) << 8) | (color_in_system_palette_index as u16);
|
|
self.state_mut().framebuffer.buffer.poke( nth, pixel );
|
|
self.state_mut().n_pixel += 1;
|
|
}
|
|
|
|
fn shift_background_registers( &mut self ) {
|
|
self.state_mut().bg_pattern_lo_shift_register <<= 1;
|
|
self.state_mut().bg_pattern_hi_shift_register <<= 1;
|
|
self.state_mut().bg_palette_index_lo_shift_register <<= 1;
|
|
self.state_mut().bg_palette_index_hi_shift_register <<= 1;
|
|
}
|
|
|
|
fn rendering_is_enabled( &mut self ) -> bool {
|
|
self.state().ppumask.show_background() || self.state().ppumask.show_sprites()
|
|
}
|
|
|
|
fn execute( &mut self ) {
|
|
self.execute_next_action();
|
|
self.on_cycle();
|
|
|
|
if self.state().skip_next_cycle {
|
|
// Technically we're supposed to skip the next cycle,
|
|
// but we might just as well execute it.
|
|
self.state_mut().skip_next_cycle = false;
|
|
self.execute_next_action();
|
|
}
|
|
}
|
|
|
|
fn execute_next_action( &mut self ) {
|
|
let action = unsafe { get_action( self.state().action_index as usize ) };
|
|
(action)( self );
|
|
self.state_mut().action_index += 1;
|
|
self.on_next_action();
|
|
}
|
|
|
|
fn on_next_action( &mut self ) {
|
|
let chunk = CHUNKS.at( self.state().chunk_index );
|
|
|
|
if self.state().action_index > chunk.last_action_index {
|
|
self.state_mut().chunk_counter += 1;
|
|
if self.state().chunk_counter != chunk.times {
|
|
self.state_mut().action_index = chunk.first_action_index;
|
|
} else {
|
|
self.state_mut().chunk_counter = 0;
|
|
self.state_mut().chunk_index += 1;
|
|
|
|
let scanline = SCANLINES.at( self.state().scanline_index );
|
|
if self.state().chunk_index > scanline.last_chunk_index {
|
|
self.state_mut().scanline_counter += 1;
|
|
if self.state().scanline_counter != scanline.times {
|
|
self.state_mut().chunk_index = scanline.first_chunk_index;
|
|
let chunk = CHUNKS.at( self.state().chunk_index );
|
|
self.state_mut().action_index = chunk.first_action_index;
|
|
} else {
|
|
self.state_mut().scanline_index += 1;
|
|
self.state_mut().scanline_counter = 0;
|
|
if self.state().scanline_index >= (SCANLINES.len() as u8) {
|
|
self.state_mut().scanline_index = 0;
|
|
}
|
|
let scanline = SCANLINES.at( self.state().scanline_index );
|
|
self.state_mut().chunk_index = scanline.first_chunk_index;
|
|
let chunk = CHUNKS.at( self.state().chunk_index );
|
|
self.state_mut().action_index = chunk.first_action_index;
|
|
}
|
|
} else {
|
|
let chunk = CHUNKS.at( self.state().chunk_index );
|
|
self.state_mut().action_index = chunk.first_action_index;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn try_trigger_vblank_nmi( &mut self ) {
|
|
let value = self.state().ppuctrl.should_generate_vblank_nmi() && self.state().ppustatus.vblank_has_occured();
|
|
self.set_vblank_nmi( value );
|
|
}
|
|
|
|
fn peek_ppustatus( &mut self ) -> u8 {
|
|
let value = self.state().ppustatus.0;
|
|
self.state_mut().ppustatus.modify_vblank_has_occured( false );
|
|
self.state_mut().write_toggle = false;
|
|
self.state_mut().vblank_flag_was_cleared = true;
|
|
|
|
let result = (value & 0b11100000) | (self.state().residual_data & 0b00011111);
|
|
self.state_mut().residual_data = result;
|
|
result
|
|
}
|
|
|
|
fn poke_ppuctrl( &mut self, value: u8 ) {
|
|
self.state_mut().ppuctrl.0 = value;
|
|
self.state_mut().temporary_address = (self.state().temporary_address & 0b0111001111111111) | ((value as u16 & 0b11) << 10);
|
|
}
|
|
|
|
fn poke_ppumask( &mut self, value: u8 ) {
|
|
self.state_mut().ppumask.0 = value;
|
|
}
|
|
|
|
fn poke_ppuaddr( &mut self, value: u8 ) {
|
|
if self.state().write_toggle == false {
|
|
// Write the upper byte. The register is only 15-bit, and the 14-bit always get zero'd
|
|
// so we AND the value with 0x3F. The first write always goes to the temporary address register.
|
|
self.state_mut().temporary_address = (((value & 0x3F) as u16) << 8) | (self.state().temporary_address & 0x00FF);
|
|
self.state_mut().write_toggle = true;
|
|
} else {
|
|
// Write the lower byte.
|
|
// On the second write the current address register gets updated.
|
|
self.state_mut().temporary_address = (value as u16) | (self.state().temporary_address & 0xFF00);
|
|
self.state_mut().current_address = self.state().temporary_address;
|
|
self.state_mut().write_toggle = false;
|
|
}
|
|
}
|
|
|
|
fn poke_ppuscroll( &mut self, value: u8 ) {
|
|
// The scrolling IO register shares the same physical register as the address IO register.
|
|
if self.state().write_toggle == false {
|
|
self.state_mut().fine_horizontal_scroll = value & 0b111;
|
|
self.state_mut().temporary_address = (self.state().temporary_address & 0b0111111111100000) | ((value >> 3) as u16);
|
|
self.state_mut().write_toggle = true;
|
|
} else {
|
|
// VALUE -> TEMPORARY ADDR. REG.
|
|
// XXYYYZZZ -> 0ZZZ--XXYYY-----
|
|
//
|
|
// '0': always zero, since the register is 15-bit
|
|
// '-': the bit is unmodified
|
|
// 'X', 'Y', 'Z': that region of bits is copied from the input value
|
|
|
|
let tmp = ((((value & 0b00000111) as u16) >> 0) << 12) |
|
|
((((value & 0b00111000) as u16) >> 3) << 5) |
|
|
((((value & 0b11000000) as u16) >> 6) << 8);
|
|
|
|
self.state_mut().temporary_address = (self.state().temporary_address & 0b0000110000011111) | tmp;
|
|
self.state_mut().write_toggle = false;
|
|
}
|
|
}
|
|
|
|
fn poke_ppudata( &mut self, value: u8 ) {
|
|
let address = self.state().current_address & 0x3FFF; // Addresses >= 0x3FFF are mirrors of 0...0x3FFF.
|
|
|
|
self.poke( address, value );
|
|
self.increment_current_address();
|
|
}
|
|
|
|
fn peek_ppudata( &mut self ) -> u8 {
|
|
let address = self.state().current_address & 0x3FFF;
|
|
let mut value = self.peek( address );
|
|
|
|
self.increment_current_address();
|
|
if address <= 0x3EFF {
|
|
// Put the value into the read buffer; return the old value.
|
|
let old_value = self.state().ppudata_read_buffer;
|
|
self.state_mut().ppudata_read_buffer = value;
|
|
self.state_mut().residual_data = old_value;
|
|
old_value
|
|
} else {
|
|
// Data from the palette RAM is always immediately available, though
|
|
// when reading the pallete RAM the read buffer gets updated with
|
|
// a value from 0x3000...0x3EFF range just as if that mirror would be
|
|
// extended to cover whole 0x3000...0x3FFF.
|
|
self.state_mut().ppudata_read_buffer = self.peek_video_memory( address );
|
|
|
|
debug_assert!( (value & 0b11000000) == 0 );
|
|
if self.state().ppumask.grayscale_mode() {
|
|
// When the greyscale mode is on only the following colors will be returned:
|
|
// 0x00 - 0b000000
|
|
// 0x10 - 0b010000
|
|
// 0x20 - 0b100000
|
|
// 0x30 - 0b110000
|
|
value &= 0b110000;
|
|
}
|
|
self.state_mut().residual_data = (self.state().residual_data & 0b11000000) | value;
|
|
value
|
|
}
|
|
}
|
|
|
|
fn increment_current_address( &mut self ) {
|
|
let addr_increment = self.state().ppuctrl.vram_address_increment();
|
|
self.state_mut().current_address = self.state().current_address.wrapping_add( addr_increment ) & 0x7FFF;
|
|
}
|
|
|
|
fn poke_oamaddr( &mut self, value: u8 ) {
|
|
self.state_mut().sprite_list_address = value;
|
|
}
|
|
|
|
fn poke_oamdata( &mut self, value: u8 ) {
|
|
let index = self.state().sprite_list_address;
|
|
self.state_mut().sprite_list_ram.poke( index, value );
|
|
self.state_mut().sprite_list_address.wrapping_inc();
|
|
}
|
|
|
|
fn peek_oamdata( &mut self ) -> u8 {
|
|
let index = self.state().sprite_list_address;
|
|
self.state_mut().sprite_list_ram.peek( index )
|
|
}
|
|
|
|
fn poke_sprite_list_ram( &mut self, index: u8, value: u8 ) {
|
|
self.state_mut().sprite_list_ram.poke( index, value );
|
|
}
|
|
|
|
fn poke_secondary_sprite_list_ram( &mut self, index: u8, value: u8 ) {
|
|
self.state_mut().secondary_sprite_list_ram.poke( index, value );
|
|
}
|
|
|
|
fn peek( &self, mut address: u16 ) -> u8 {
|
|
if address <= 0x3EFF {
|
|
self.peek_video_memory( address )
|
|
} else {
|
|
address = address & (32 - 1);
|
|
debug_assert!( address < 32 );
|
|
|
|
self.state().palette_ram.peek( address )
|
|
}
|
|
}
|
|
|
|
fn poke( &mut self, mut address: u16, mut value: u8 ) {
|
|
if address <= 0x3EFF {
|
|
self.poke_video_memory( address, value );
|
|
} else {
|
|
address = address & (32 - 1);
|
|
debug_assert!( address < 32 );
|
|
|
|
// The memory cells containing palette data are limited to only 64 values.
|
|
value &= 64 - 1;
|
|
|
|
self.state_mut().palette_ram.poke( address, value );
|
|
|
|
if (address & 0b11) == 0 {
|
|
// Duplicate the write to the mirrored location. See the palette_access test for details.
|
|
if address >= 16 {
|
|
address -= 16;
|
|
} else {
|
|
address += 16;
|
|
}
|
|
self.state_mut().palette_ram.poke( address, value );
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::{State, Context, Private};
|
|
|
|
struct DummyPPU {
|
|
cycle: usize,
|
|
state: State,
|
|
memory: [u8; 0xFFFF + 1]
|
|
}
|
|
|
|
impl DummyPPU {
|
|
fn new() -> Self {
|
|
DummyPPU {
|
|
cycle: 0,
|
|
state: State::new(),
|
|
memory: [0; 0xFFFF + 1]
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Context for DummyPPU {
|
|
fn state_mut( &mut self ) -> &mut State {
|
|
&mut self.state
|
|
}
|
|
|
|
fn state( &self ) -> &State {
|
|
&self.state
|
|
}
|
|
|
|
fn on_cycle( &mut self ) {
|
|
self.cycle += 1;
|
|
}
|
|
|
|
fn on_frame_was_generated( &mut self ) {}
|
|
fn set_vblank_nmi( &mut self, _: bool ) {}
|
|
fn peek_video_memory( &self, offset: u16 ) -> u8 {
|
|
self.memory[ offset as usize ]
|
|
}
|
|
|
|
fn poke_video_memory( &mut self, offset: u16, value: u8 ) {
|
|
self.memory[ offset as usize ] = value
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn vblank_flag_clear() {
|
|
struct TestPPU {
|
|
cycle: usize,
|
|
nmi_set_cycle: Option< usize >,
|
|
nmi_cleared_cycle: Option< usize >,
|
|
state: State
|
|
}
|
|
|
|
impl TestPPU {
|
|
fn new() -> TestPPU {
|
|
TestPPU {
|
|
cycle: 0,
|
|
nmi_set_cycle: None,
|
|
nmi_cleared_cycle: None,
|
|
state: State::new()
|
|
}
|
|
}
|
|
|
|
fn execute_frame( &mut self ) {
|
|
for _ in 0..(341 * 262) {
|
|
self.execute();
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Context for TestPPU {
|
|
fn state_mut( &mut self ) -> &mut State {
|
|
&mut self.state
|
|
}
|
|
|
|
fn state( &self ) -> &State {
|
|
&self.state
|
|
}
|
|
|
|
fn on_cycle( &mut self ) {
|
|
self.cycle += 1;
|
|
let vblank = self.state_mut().ppustatus.vblank_has_occured();
|
|
|
|
if self.nmi_set_cycle.is_none() && vblank {
|
|
self.nmi_set_cycle = Some( self.cycle );
|
|
} else if self.nmi_set_cycle.is_some() && self.nmi_cleared_cycle.is_none() && !vblank {
|
|
self.nmi_cleared_cycle = Some( self.cycle );
|
|
}
|
|
}
|
|
|
|
fn on_frame_was_generated( &mut self ) {}
|
|
fn set_vblank_nmi( &mut self, _: bool ) {}
|
|
fn peek_video_memory( &self, _: u16 ) -> u8 { 0 }
|
|
fn poke_video_memory( &mut self, _: u16, _: u8 ) {}
|
|
}
|
|
|
|
let mut ppu = TestPPU::new();
|
|
|
|
ppu.execute_frame();
|
|
assert_eq!( ppu.cycle, 262 * 341 );
|
|
ppu.execute_frame();
|
|
|
|
// The vblank flag is cleared 6820 PPU clocks after it's set.
|
|
assert_eq!( ppu.nmi_cleared_cycle.unwrap() - ppu.nmi_set_cycle.unwrap(), 6820 );
|
|
}
|
|
|
|
#[test]
|
|
fn poke_ppuaddr_once() {
|
|
let mut ppu = DummyPPU::new();
|
|
|
|
ppu.state.current_address = 0xCCCC;
|
|
ppu.state.temporary_address = 0xFFFF;
|
|
ppu.poke_ppuaddr( 0x12 );
|
|
assert_eq!( ppu.state.temporary_address, 0x12FF ); // Writes into the high byte of the temporary address register.
|
|
assert_eq!( ppu.state.current_address, 0xCCCC ); // Current address register is unchanged.
|
|
}
|
|
|
|
#[test]
|
|
fn poke_ppuaddr_twice() {
|
|
let mut ppu = DummyPPU::new();
|
|
|
|
ppu.poke_ppuaddr( 0x12 );
|
|
ppu.poke_ppuaddr( 0x34 );
|
|
assert_eq!( ppu.state.temporary_address, 0x1234 ); // Writes into the low byte of the temporary address register.
|
|
assert_eq!( ppu.state.current_address, 0x1234 ); // The value is copied to the current address register.
|
|
}
|
|
|
|
#[test]
|
|
fn poke_ppuaddr_and_peek_status() {
|
|
let mut ppu = DummyPPU::new();
|
|
|
|
ppu.state.current_address = 0xCCCC;
|
|
ppu.state.temporary_address = 0xFFFF;
|
|
ppu.poke_ppuaddr( 0x12 );
|
|
ppu.peek_ppustatus(); // A read from the PPUSTATUS clears the high/low flag.
|
|
assert_eq!( ppu.state.temporary_address, 0x12FF ); // Only the upper byte was written.
|
|
assert_eq!( ppu.state.current_address, 0xCCCC ); // Current address register is unchanged.
|
|
}
|
|
|
|
#[test]
|
|
fn poke_ppuaddr_0xFF() {
|
|
let mut ppu = DummyPPU::new();
|
|
|
|
ppu.poke_ppuaddr( 0xFF );
|
|
assert_eq!( ppu.state.temporary_address, 0x3F00 ); // The upper two bits are masked out on the first write.
|
|
ppu.poke_ppuaddr( 0xFF );
|
|
assert_eq!( ppu.state.temporary_address, 0x3FFF );
|
|
assert_eq!( ppu.state.current_address, 0x3FFF );
|
|
}
|
|
|
|
#[test]
|
|
fn poke_ppuctrl_temporary_address_write() {
|
|
let mut ppu = DummyPPU::new();
|
|
|
|
ppu.poke_ppuctrl( 0b00000011 );
|
|
assert_eq!( ppu.state.temporary_address, 0b0000110000000000 );
|
|
ppu.poke_ppuctrl( 0b00000000 );
|
|
assert_eq!( ppu.state.temporary_address, 0b0000000000000000 );
|
|
ppu.poke_ppuctrl( 0b00000001 );
|
|
assert_eq!( ppu.state.temporary_address, 0b0000010000000000 );
|
|
ppu.poke_ppuctrl( 0b00000010 );
|
|
assert_eq!( ppu.state.temporary_address, 0b0000100000000000 );
|
|
ppu.poke_ppuctrl( 0b11111100 );
|
|
assert_eq!( ppu.state.temporary_address, 0b0000000000000000 );
|
|
}
|
|
|
|
#[test]
|
|
fn poke_ppuscroll_once() {
|
|
let mut ppu = DummyPPU::new();
|
|
|
|
ppu.poke_ppuscroll( 0b11111111 );
|
|
assert_eq!( ppu.state.temporary_address, 0b0000000000011111 ); // Writes into the low five bits of the temporary address register...
|
|
assert_eq!( ppu.state.fine_horizontal_scroll, 0b111 ); // ...and to the fine X scroll register.
|
|
assert_eq!( ppu.state.current_address, 0 ); // Unmodified.
|
|
ppu.poke_ppuscroll( 0 ); // To reset the high/low toggle.
|
|
|
|
// Try writing various patterns.
|
|
ppu.poke_ppuscroll( 0b00000111 );
|
|
ppu.poke_ppuscroll( 0 ); // To reset the high/low toggle.
|
|
assert_eq!( ppu.state.temporary_address, 0b0000000000000000 );
|
|
assert_eq!( ppu.state.fine_horizontal_scroll, 0b111 );
|
|
|
|
ppu.poke_ppuscroll( 0b11111000 );
|
|
ppu.poke_ppuscroll( 0 );
|
|
assert_eq!( ppu.state.temporary_address, 0b0000000000011111 );
|
|
assert_eq!( ppu.state.fine_horizontal_scroll, 0b000 );
|
|
|
|
ppu.poke_ppuscroll( 0b10101010 );
|
|
ppu.poke_ppuscroll( 0 );
|
|
assert_eq!( ppu.state.temporary_address, 0b0000000000010101 );
|
|
assert_eq!( ppu.state.fine_horizontal_scroll, 0b010 );
|
|
}
|
|
|
|
#[test]
|
|
fn poke_ppuscroll_twice() {
|
|
let mut ppu = DummyPPU::new();
|
|
|
|
ppu.poke_ppuscroll( 0b11111111 );
|
|
ppu.poke_ppuscroll( 0b11111111 ); // Writes into the five high bits and three low bits of the temporary address register...
|
|
assert_eq!( ppu.state.temporary_address, 0b0111001111111111 ); // ...the bits 11 and 10 are untouched.
|
|
assert_eq!( ppu.state.current_address, 0 ); // Unmodified.
|
|
|
|
ppu.state.temporary_address = 0b0111111111111111;
|
|
ppu.poke_ppuscroll( 0b11111111 );
|
|
ppu.poke_ppuscroll( 0b11111111 );
|
|
assert_eq!( ppu.state.temporary_address, 0b0111111111111111 ); // Again, bits 11 and 10 are untouched.
|
|
ppu.state.temporary_address = 0;
|
|
|
|
// Write to the register with successively different patterns turning off bits one by one.
|
|
ppu.poke_ppuscroll( 0b11111111 );
|
|
ppu.poke_ppuscroll( 0b11111110 );
|
|
assert_eq!( ppu.state.temporary_address, 0b0110001111111111 );
|
|
|
|
ppu.poke_ppuscroll( 0b11111111 );
|
|
ppu.poke_ppuscroll( 0b11111100 );
|
|
assert_eq!( ppu.state.temporary_address, 0b0100001111111111 );
|
|
|
|
ppu.poke_ppuscroll( 0b11111111 );
|
|
ppu.poke_ppuscroll( 0b11111000 );
|
|
assert_eq!( ppu.state.temporary_address, 0b0000001111111111 );
|
|
|
|
ppu.poke_ppuscroll( 0b11111111 );
|
|
ppu.poke_ppuscroll( 0b01111000 );
|
|
assert_eq!( ppu.state.temporary_address, 0b0000000111111111 );
|
|
|
|
ppu.poke_ppuscroll( 0b11111111 );
|
|
ppu.poke_ppuscroll( 0b00111000 );
|
|
assert_eq!( ppu.state.temporary_address, 0b0000000011111111 );
|
|
|
|
ppu.poke_ppuscroll( 0b11111111 );
|
|
ppu.poke_ppuscroll( 0b00011000 );
|
|
assert_eq!( ppu.state.temporary_address, 0b0000000001111111 );
|
|
|
|
ppu.poke_ppuscroll( 0b11111111 );
|
|
ppu.poke_ppuscroll( 0b00001000 );
|
|
assert_eq!( ppu.state.temporary_address, 0b0000000000111111 );
|
|
|
|
ppu.poke_ppuscroll( 0b11111111 );
|
|
ppu.poke_ppuscroll( 0b00000000 );
|
|
assert_eq!( ppu.state.temporary_address, 0b0000000000011111 );
|
|
}
|
|
|
|
#[test]
|
|
fn poke_ppuaddr_and_ppuscroll() {
|
|
let mut ppu = DummyPPU::new();
|
|
|
|
// Both IO registers share the same high/low toggle and the same address registers.
|
|
ppu.poke_ppuaddr( 0b11111111 );
|
|
ppu.poke_ppuscroll( 0b11111111 );
|
|
assert_eq!( ppu.state.temporary_address, 0b0111111111100000 );
|
|
}
|
|
|
|
#[test]
|
|
fn palette_access() {
|
|
let mut ppu = DummyPPU::new();
|
|
|
|
ppu.poke( 0x3F00, 255 );
|
|
assert_eq!( ppu.peek( 0x3F00 ), 63 ); // The cells are limited to 6 bits.
|
|
|
|
// Clear the memory.
|
|
for i in 0..32 {
|
|
ppu.poke( 0x3F00 + i, 63 );
|
|
}
|
|
|
|
// Write unique value to every cell in the first palette set.
|
|
for i in 0..16 {
|
|
ppu.poke( 0x3F00 + i, i as u8 );
|
|
}
|
|
|
|
// First palette set.
|
|
assert_eq!( ppu.peek( 0x3F00 + 0 ), 0 );
|
|
assert_eq!( ppu.peek( 0x3F00 + 1 ), 1 );
|
|
assert_eq!( ppu.peek( 0x3F00 + 2 ), 2 );
|
|
assert_eq!( ppu.peek( 0x3F00 + 3 ), 3 );
|
|
assert_eq!( ppu.peek( 0x3F00 + 4 ), 4 );
|
|
assert_eq!( ppu.peek( 0x3F00 + 5 ), 5 );
|
|
assert_eq!( ppu.peek( 0x3F00 + 6 ), 6 );
|
|
assert_eq!( ppu.peek( 0x3F00 + 7 ), 7 );
|
|
assert_eq!( ppu.peek( 0x3F00 + 8 ), 8 );
|
|
assert_eq!( ppu.peek( 0x3F00 + 9 ), 9 );
|
|
assert_eq!( ppu.peek( 0x3F00 + 10 ), 10 );
|
|
assert_eq!( ppu.peek( 0x3F00 + 11 ), 11 );
|
|
assert_eq!( ppu.peek( 0x3F00 + 12 ), 12 );
|
|
assert_eq!( ppu.peek( 0x3F00 + 13 ), 13 );
|
|
assert_eq!( ppu.peek( 0x3F00 + 14 ), 14 );
|
|
assert_eq!( ppu.peek( 0x3F00 + 15 ), 15 );
|
|
|
|
// Second palette set.
|
|
assert_eq!( ppu.peek( 0x3F00 + 16 ), 0 ); // Mirrored from 0x3F00.
|
|
assert_eq!( ppu.peek( 0x3F00 + 17 ), 63 ); // Untouched.
|
|
assert_eq!( ppu.peek( 0x3F00 + 18 ), 63 );
|
|
assert_eq!( ppu.peek( 0x3F00 + 19 ), 63 );
|
|
assert_eq!( ppu.peek( 0x3F00 + 20 ), 4 ); // Mirrored from 0x3F04.
|
|
assert_eq!( ppu.peek( 0x3F00 + 21 ), 63 );
|
|
assert_eq!( ppu.peek( 0x3F00 + 22 ), 63 );
|
|
assert_eq!( ppu.peek( 0x3F00 + 23 ), 63 );
|
|
assert_eq!( ppu.peek( 0x3F00 + 24 ), 8 ); // Mirrored from 0x3F08.
|
|
assert_eq!( ppu.peek( 0x3F00 + 25 ), 63 );
|
|
assert_eq!( ppu.peek( 0x3F00 + 26 ), 63 );
|
|
assert_eq!( ppu.peek( 0x3F00 + 27 ), 63 );
|
|
assert_eq!( ppu.peek( 0x3F00 + 28 ), 12 ); // Mirrored from 0x3F0C.
|
|
assert_eq!( ppu.peek( 0x3F00 + 29 ), 63 );
|
|
assert_eq!( ppu.peek( 0x3F00 + 30 ), 63 );
|
|
assert_eq!( ppu.peek( 0x3F00 + 31 ), 63 );
|
|
|
|
// Write unique value to every cell in the second palette set.
|
|
for i in 16..32 {
|
|
ppu.poke( 0x3F00 + i, i as u8 );
|
|
}
|
|
|
|
// First palette set.
|
|
assert_eq!( ppu.peek( 0x3F00 + 0 ), 16 ); // Mirrored from 0x3F10.
|
|
assert_eq!( ppu.peek( 0x3F00 + 1 ), 1 ); // Untouched.
|
|
assert_eq!( ppu.peek( 0x3F00 + 2 ), 2 );
|
|
assert_eq!( ppu.peek( 0x3F00 + 3 ), 3 );
|
|
assert_eq!( ppu.peek( 0x3F00 + 4 ), 20 ); // Mirrored from 0x3F14.
|
|
assert_eq!( ppu.peek( 0x3F00 + 5 ), 5 );
|
|
assert_eq!( ppu.peek( 0x3F00 + 6 ), 6 );
|
|
assert_eq!( ppu.peek( 0x3F00 + 7 ), 7 );
|
|
assert_eq!( ppu.peek( 0x3F00 + 8 ), 24 ); // Mirrored from 0x3F18.
|
|
assert_eq!( ppu.peek( 0x3F00 + 9 ), 9 );
|
|
assert_eq!( ppu.peek( 0x3F00 + 10 ), 10 );
|
|
assert_eq!( ppu.peek( 0x3F00 + 11 ), 11 );
|
|
assert_eq!( ppu.peek( 0x3F00 + 12 ), 28 ); // Mirrored from 0x3F1C.
|
|
assert_eq!( ppu.peek( 0x3F00 + 13 ), 13 );
|
|
assert_eq!( ppu.peek( 0x3F00 + 14 ), 14 );
|
|
assert_eq!( ppu.peek( 0x3F00 + 15 ), 15 );
|
|
|
|
// Second palette set.
|
|
assert_eq!( ppu.peek( 0x3F00 + 16 ), 16 );
|
|
assert_eq!( ppu.peek( 0x3F00 + 17 ), 17 );
|
|
assert_eq!( ppu.peek( 0x3F00 + 18 ), 18 );
|
|
assert_eq!( ppu.peek( 0x3F00 + 19 ), 19 );
|
|
assert_eq!( ppu.peek( 0x3F00 + 20 ), 20 );
|
|
assert_eq!( ppu.peek( 0x3F00 + 21 ), 21 );
|
|
assert_eq!( ppu.peek( 0x3F00 + 22 ), 22 );
|
|
assert_eq!( ppu.peek( 0x3F00 + 23 ), 23 );
|
|
assert_eq!( ppu.peek( 0x3F00 + 24 ), 24 );
|
|
assert_eq!( ppu.peek( 0x3F00 + 25 ), 25 );
|
|
assert_eq!( ppu.peek( 0x3F00 + 26 ), 26 );
|
|
assert_eq!( ppu.peek( 0x3F00 + 27 ), 27 );
|
|
assert_eq!( ppu.peek( 0x3F00 + 28 ), 28 );
|
|
assert_eq!( ppu.peek( 0x3F00 + 29 ), 29 );
|
|
assert_eq!( ppu.peek( 0x3F00 + 30 ), 30 );
|
|
assert_eq!( ppu.peek( 0x3F00 + 31 ), 31 );
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test_ppu {
|
|
use super::{Context, State, Private};
|
|
use rp2c02_testsuite::TestPPU;
|
|
use std::cell::Cell;
|
|
use std::fmt;
|
|
|
|
#[derive(Copy, Clone, PartialEq, Eq)]
|
|
pub struct MemoryOperation {
|
|
pub address: u16,
|
|
pub value: u8
|
|
}
|
|
|
|
impl fmt::Debug for MemoryOperation {
|
|
fn fmt( &self, fmt: &mut fmt::Formatter ) -> fmt::Result {
|
|
write!( fmt, "{{ address: 0x{:04X}, value: 0x{:02X} }}", self.address, self.value )
|
|
}
|
|
}
|
|
|
|
struct Instance {
|
|
state: State,
|
|
memory: [u8; 0x2400],
|
|
last_vram_read: Cell< Option< MemoryOperation > >,
|
|
expected_vram_read: Option< Option< MemoryOperation > >
|
|
}
|
|
|
|
impl Context for Instance {
|
|
fn state_mut( &mut self ) -> &mut State {
|
|
&mut self.state
|
|
}
|
|
|
|
fn state( &self ) -> &State {
|
|
&self.state
|
|
}
|
|
|
|
fn on_frame_was_generated( &mut self ) {}
|
|
fn set_vblank_nmi( &mut self, _: bool ) {}
|
|
|
|
fn peek_video_memory( &self, address: u16 ) -> u8 {
|
|
// The simulator uses one screen mirroring of the background tilemaps.
|
|
let value = self.memory[ ((address & 0x23FF) | (address & 0x2000)) as usize ];
|
|
|
|
self.last_vram_read.set( Some( MemoryOperation {
|
|
address: address,
|
|
value: value
|
|
}));
|
|
|
|
value
|
|
}
|
|
|
|
fn poke_video_memory( &mut self, _: u16, _: u8 ) {
|
|
unreachable!();
|
|
}
|
|
}
|
|
|
|
impl Instance {
|
|
fn new() -> Self {
|
|
Instance {
|
|
state: State::new(),
|
|
memory: [0; 0x2400],
|
|
last_vram_read: Cell::new( None ),
|
|
expected_vram_read: None
|
|
}
|
|
}
|
|
}
|
|
|
|
impl TestPPU for Instance {
|
|
fn expect_vram_read( &mut self, address: u16, value: u8 ) {
|
|
self.expected_vram_read = Some( Some( MemoryOperation {
|
|
address: address,
|
|
value: value
|
|
}));
|
|
}
|
|
|
|
fn expect_no_vram_read( &mut self ) {
|
|
self.expected_vram_read = Some( None );
|
|
}
|
|
|
|
fn get_current_address( &self ) -> u16 {
|
|
self.state.current_address
|
|
}
|
|
|
|
fn read_ioreg( &mut self, index: u8 ) -> u8 {
|
|
match index {
|
|
2 => self.peek_ppustatus(),
|
|
4 => self.peek_oamdata(),
|
|
7 => self.peek_ppudata(),
|
|
_ => unreachable!()
|
|
}
|
|
}
|
|
|
|
fn read_secondary_sprite_ram( &self, index: u8 ) -> u8 {
|
|
self.state.secondary_sprite_list_ram[ index as usize ]
|
|
}
|
|
|
|
fn write_ioreg( &mut self, index: u8, value: u8 ) {
|
|
match index {
|
|
0 => self.poke_ppuctrl( value ),
|
|
1 => self.poke_ppumask( value ),
|
|
3 => self.poke_oamaddr( value ),
|
|
4 => self.poke_oamdata( value ),
|
|
5 => self.poke_ppuscroll( value ),
|
|
6 => self.poke_ppuaddr( value ),
|
|
7 => self.poke_ppudata( value ),
|
|
_ => unreachable!()
|
|
}
|
|
}
|
|
|
|
fn write_vram( &mut self, address: u16, value: u8 ) {
|
|
self.memory[ address as usize ] = value;
|
|
}
|
|
|
|
fn write_palette_ram( &mut self, index: u8, value: u8 ) {
|
|
self.state.palette_ram[ index as usize ] = value;
|
|
}
|
|
|
|
fn write_sprite_ram( &mut self, index: u8, value: u8 ) {
|
|
self.state.sprite_list_ram[ index as usize ] = value;
|
|
}
|
|
|
|
fn write_secondary_sprite_ram( &mut self, index: u8, value: u8 ) {
|
|
self.state.secondary_sprite_list_ram[ index as usize] = value;
|
|
}
|
|
|
|
fn step_pixel( &mut self ) {
|
|
self.last_vram_read = Cell::new( None );
|
|
|
|
self.execute();
|
|
|
|
if let Some( expected_vram_read ) = self.expected_vram_read.take() {
|
|
assert_eq!( self.last_vram_read.get(), expected_vram_read );
|
|
}
|
|
}
|
|
|
|
fn scanline( &self ) -> u16 {
|
|
self.state.n_scanline
|
|
}
|
|
|
|
fn dot( &self ) -> u16 {
|
|
self.state.n_dot
|
|
}
|
|
}
|
|
|
|
rp2c02_testsuite!( Instance );
|
|
}
|