Working Sprite implementation
Some checks failed
Cargo Build & Test / Rust project - latest (stable) (push) Failing after 9s

This commit is contained in:
2026-02-10 22:17:51 -06:00
parent 22c586f15a
commit 3372559c19
8 changed files with 1585 additions and 26 deletions

View File

@@ -35,7 +35,10 @@ use tracing_subscriber::EnvFilter;
// const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "int_nmi_exit_timing.nes");
// const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "render-updating.nes");
// const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "ppu_palette_shared.nes");
const ROM_FILE: &str = "./Super Mario Bros. (World).nes";
// const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "input_test.nes");
// const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "scrolling.nes");
const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "sprites.nes");
// const ROM_FILE: &str = "./Super Mario Bros. (World).nes";
// const ROM_FILE: &str = "./cpu_timing_test.nes";
// const ROM_FILE: &str = "../nes-test-roms/instr_test-v5/official_only.nes";
@@ -178,7 +181,7 @@ impl Emulator {
nes,
windows: HashMap::from_iter([
(win, WindowType::Main),
(win_2, WindowType::Memory(MemoryTy::OAM, HexView {}))
(win_2, WindowType::Debugger)
]),
debugger: DebuggerState::new(),
main_win_size: Size::new(0., 0.),

View File

@@ -88,11 +88,205 @@ pub enum PPUMMRegisters {
Palette,
}
bitfield::bitfield! {
#[derive(Default, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SpriteAttrs(u8);
impl Debug;
vflip, set_vflip: 7;
hflip, set_hflip: 6;
priority, set_priority: 5;
palette, set_palette: 1, 0;
}
#[derive(Debug, Clone, Default)]
struct SpriteOutputUnit {
high: u8,
low: u8,
x_pos: u8,
y_pos: u8,
tile: u8,
attrs: SpriteAttrs,
}
#[derive(Debug, Clone)]
enum OamState {
ReadY,
ReadTile,
ReadAttrs,
ReadX,
OverflowScan,
Wait,
}
#[derive(Debug, Clone)]
pub struct OAM {
mem: Vec<u8>,
secondary: Vec<u8>,
addr: u8,
count: u8,
state: OamState,
oam_read_buffer: u8,
edit_ver: usize,
sprite_output_units: Vec<SpriteOutputUnit>,
overflow: bool,
sprite_offset_0x1000: bool,
}
impl OAM {
fn new() -> Self {
Self {
mem: vec![0u8; 256],
addr: 0,
count: 0,
edit_ver: 0,
secondary: vec![0xFF; 32],
sprite_output_units: vec![SpriteOutputUnit::default(); 8],
state: OamState::ReadY,
oam_read_buffer: 0,
overflow: false,
sprite_offset_0x1000: false,
}
}
fn ppu_cycle(&mut self, pixel: usize, line: usize, mem: &mut PpuMem<'_>) {
if pixel < 64 {
} else if pixel == 64 {
self.secondary.fill(0xFF);
self.state = OamState::ReadY;
self.addr = 0;
self.count = 0;
} else if pixel <= 256 {
// Fetch/evalute sprites for next line
if pixel % 2 == 1 && !matches!(self.state, OamState::Wait) {
self.oam_read_buffer = self.mem[self.addr as usize];
if self.addr == 255 {
self.addr = 0;
} else {
self.addr += 1;
}
} else {
match self.state {
OamState::ReadY => {
let l = self.oam_read_buffer as usize;
if self.count < 8 {
if l <= line && line < l + 8 {
self.secondary[self.count as usize * 4] = self.oam_read_buffer;
self.state = OamState::ReadTile;
} else {
if self.addr < 0xFD {
self.addr += 3;
} else {
self.state = OamState::Wait; // Should be Overflow scan...
}
}
} else {
self.state = OamState::Wait; // Should be Overflow scan...
}
}
OamState::ReadTile => {
self.secondary[self.count as usize * 4 + 1] = self.oam_read_buffer;
self.state = OamState::ReadAttrs;
}
OamState::ReadAttrs => {
self.secondary[self.count as usize * 4 + 2] = self.oam_read_buffer;
self.state = OamState::ReadX;
}
OamState::ReadX => {
self.secondary[self.count as usize * 4 + 3] = self.oam_read_buffer;
self.state = OamState::ReadY;
self.count += 1;
if self.count == 8 {
self.state = OamState::Wait; // Should be Overflow scan...
}
}
OamState::OverflowScan => todo!(),
OamState::Wait => (),
}
}
} else if pixel <= 320 {
if pixel == 257 {
// Reset output units
self.sprite_output_units.fill(SpriteOutputUnit::default());
}
let run = pixel - 257;
if run / 8 < self.count as usize {
if run % 8 == 0 {
self.sprite_output_units[run / 8].y_pos = self.secondary[(run / 8) * 4];
} else if run % 8 == 1 {
self.sprite_output_units[run / 8].tile = self.secondary[(run / 8) * 4 + 1];
} else if run % 8 == 2 {
self.sprite_output_units[run / 8].attrs.0 = self.secondary[(run / 8) * 4 + 2];
} else if run % 8 == 3 {
self.sprite_output_units[run / 8].x_pos = self.secondary[(run / 8) * 4 + 3];
} else if run % 8 == 4 {
} else if run % 8 == 5 {
let off = line - self.sprite_output_units[run / 8].y_pos as usize;
let off = if self.sprite_output_units[run / 8].attrs.vflip() {
7 - off
} else {
off
};
let addr = if self.sprite_offset_0x1000 {
0x1000
} else {
0x0000
} + 16 * self.sprite_output_units[run / 8].tile as u16
+ off as u16;
self.sprite_output_units[run / 8].low = mem.read(addr).reg_map(|_, _| todo!());
} else if run % 8 == 6 {
} else if run % 8 == 7 {
let off = line - self.sprite_output_units[run / 8].y_pos as usize;
let off = if self.sprite_output_units[run / 8].attrs.vflip() {
7 - off
} else {
off
};
let addr = if self.sprite_offset_0x1000 {
0x1000
} else {
0x0000
} + 16 * self.sprite_output_units[run / 8].tile as u16
+ off as u16
+ 8;
self.sprite_output_units[run / 8].high = mem.read(addr).reg_map(|_, _| todo!());
}
}
}
}
/// Returns Some((palette index, above background))
fn pixel(&mut self, pixel: usize, _line: usize) -> Option<(u8, bool, bool)> {
self.sprite_output_units
.iter()
.enumerate()
.filter_map(|(i, s)| {
if pixel >= s.x_pos as usize && pixel < s.x_pos as usize + 8 && s.x_pos < 0xFF {
let bit_pos = pixel - s.x_pos as usize;
let bit_pos = if s.attrs.hflip() {
bit_pos
} else {
7 - bit_pos
};
// println!("Shifting: 0b{:08b} by {}", s.high, 8 - bit_pos);
let hi = (s.high >> bit_pos) & 1;
// println!("Shifting: 0b{:08b} by {}", s.low, bit_pos);
let lo = (s.low >> bit_pos) & 1;
let idx = (hi << 1) | lo;
Some((
if idx != 0 {
idx + s.attrs.palette() * 4 + 0x10
} else {
0
},
!s.attrs.priority(),
i == 0,
))
} else {
None
}
})
.next()
}
}
#[derive(Debug, Clone)]
@@ -463,9 +657,9 @@ pub struct Palette {
}
impl Palette {
pub fn color(&self, idx: u8, palette: u8) -> Color {
pub fn color(&self, idx: u8) -> Color {
debug_assert!(idx < 0x20, "Palette index out of range");
self.colors[(self.ram[idx as usize + palette as usize * 4] & 0x3F) as usize]
self.colors[(self.ram[idx as usize] & 0x3F) as usize]
}
pub fn ram(&self, offset: u8) -> u8 {
@@ -487,6 +681,7 @@ pub struct PPU {
pub mask: Mask,
pub vblank: bool,
sprite_zero_hit: bool,
pub palette: Palette,
pub background: Background,
@@ -542,6 +737,7 @@ impl PPU {
],
},
vblank: false,
sprite_zero_hit: false,
frame_count: 0,
nmi_enabled: false,
// nmi_waiting: false,
@@ -566,11 +762,7 @@ impl PPU {
next_attr: 0,
next_attr_2: 0,
},
oam: OAM {
mem: vec![0u8; 256],
addr: 0,
edit_ver: 0,
},
oam: OAM::new(),
vram_buffer: 0,
}
}
@@ -587,7 +779,9 @@ impl PPU {
0 => panic!("ppuctrl is write-only"),
1 => panic!("ppumask is write-only"),
2 => {
let tmp = if self.vblank { 0b1000_0000 } else { 0 };
let tmp = if self.vblank { 0b1000_0000 } else { 0 }
| if self.sprite_zero_hit { 0b0100_0000 } else { 0 }
| if self.oam.overflow { 0b0010_0000 } else { 0 };
self.vblank = false;
self.background.w = false;
tmp
@@ -605,10 +799,11 @@ impl PPU {
let val = self.vram_buffer;
self.vram_buffer = v;
val
},
Value::Register { reg: PPUMMRegisters::Palette, offset } => {
self.palette.ram[offset as usize]
},
}
Value::Register {
reg: PPUMMRegisters::Palette,
offset,
} => self.palette.ram[offset as usize],
};
// .reg_map(|a, off| match a {
// PPUMMRegisters::Palette => self.palette.ram[off as usize],
@@ -626,9 +821,10 @@ impl PPU {
0x00 => {
self.nmi_enabled = val & 0b1000_0000 != 0;
self.background.t =
(self.background.t & 0b0001_1000_0000_0000) | (((val & 0b11) as u16) << 10);
(self.background.t & !0b0001_1000_0000_0000) | (((val & 0b11) as u16) << 10);
self.background.vram_column = val & 0b0000_0100 != 0;
self.background.second_pattern = val & 0b0001_0000 != 0;
self.oam.sprite_offset_0x1000 = val & 0b0000_1000 != 0;
// TODO: other control fields
}
0x01 => {
@@ -752,31 +948,54 @@ impl PPU {
self.background.cur_shift_high <<= 1;
self.background.cur_shift_low <<= 1;
}
if self.scanline != 261 {
self.oam.ppu_cycle(self.pixel, self.scanline, mem);
}
// TODO
if self.pixel == 0 {
if self.scanline < 9 {
// dbg!((self.scanline, &self.oam.sprite_output_units[0]));
// dbg!(&self.oam.secondary);
}
// self.dbg_int = true;
// idle cycle
} else if self.pixel < 257 || self.pixel > 320 {
// self.dbg_int = true;
const POS: u32 = 1 << 15;
// const POS: u32 = 1 << 15;
let bit_pos = 15 - self.background.x;
// let pos: u32 = 1 << (15 + self.background.x*0); // TODO: handle this correctly
// Determine background color
let a = self.background.cur_shift_high & POS;
let b = self.background.cur_shift_low & POS;
let val = (a >> 14) | (b >> 15);
let a = (self.background.cur_shift_high >> bit_pos) & 1;
let b = (self.background.cur_shift_low >> bit_pos) & 1;
let val = (a << 1) | b;
debug_assert!(val < 4);
let h_off = ((self.pixel - 1) / 16) % 2;
let v_off = (self.scanline / 16) % 2;
let off = v_off * 4 + h_off * 2;
let palette = (self.background.cur_attr >> off) & 0x3;
let color = val as u8 + if val != 0 { palette * 4 } else { 0 };
if self.scanline < 240 && self.pixel < 257 {
let color = if let Some((sp_color, above, is_sprite_zero)) =
self.oam.pixel(self.pixel, self.scanline)
{
if is_sprite_zero && sp_color != 0 && color != 0 {
self.sprite_zero_hit = true;
}
if sp_color == 0 || (color != 0 && !above) {
color
} else {
sp_color
}
} else {
color
};
// Write to screen
self.render_buffer.write(
self.scanline,
self.pixel - 1,
self.palette
.color(val as u8, if val != 0 { palette } else { 0 }), // self.palette.colors[val as usize],
self.palette.color(color), // self.palette.colors[val as usize],
); // TODO: this should come from shift registers
}
if self.pixel < 337 {
@@ -868,7 +1087,8 @@ impl PPU {
}
if self.scanline == 261 && self.pixel == 1 {
self.vblank = false;
// TODO: clear sprite 0 & sprite overflow
self.sprite_zero_hit = false;
self.oam.overflow = false;
}
if self.scanline == 241 && self.pixel == 1 {
self.vblank = true;
@@ -930,10 +1150,10 @@ impl PPU {
),
Size::new(1., 1.),
match (low & (1 << bit) != 0, high & (1 << bit) != 0) {
(false, false) => self.palette.color(0, 0),
(true, false) => self.palette.color(1, palette),
(false, true) => self.palette.color(2, palette),
(true, true) => self.palette.color(3, palette),
(false, false) => self.palette.color(0),
(true, false) => self.palette.color(1 + 4 * palette),
(false, true) => self.palette.color(2 + 4 * palette),
(true, true) => self.palette.color(3 + 4 * palette),
// (false, false) => Color { r: 0, g: 0, b: 0 },
// (true, false) => Color {
// r: 64,

335
src/test_roms/input_test.s Normal file
View File

@@ -0,0 +1,335 @@
; Verifies that reset doesn't alter any RAM.
.include "testing.s"
; patterns_bin "pat.bin"
.macro chr b,s
.repeat s
.byte b
.endrepeat
.endmacro
.pushseg
.segment "CHARS"
T_EMPTY = 0
.repeat 2 ; Empty 0
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.endrepeat
T_TOP_LEFT = 1
.repeat 2 ; Top Left 1
.byte %11111111
.byte %11111111
.byte %11000000
.byte %11000000
.byte %11000000
.byte %11000000
.byte %11000000
.byte %11000000
.endrepeat
T_TOP_RIGHT = 2
.repeat 2 ; Top Right 2
.byte %11111111
.byte %11111111
.byte %00000011
.byte %00000011
.byte %00000011
.byte %00000011
.byte %00000011
.byte %00000011
.endrepeat
T_BOTTOM_RIGHT = 3
.repeat 2 ; Bottom Right 3
.byte %00000011
.byte %00000011
.byte %00000011
.byte %00000011
.byte %00000011
.byte %00000011
.byte %11111111
.byte %11111111
.endrepeat
T_BOTTOM_LEFT = 4
.repeat 2 ; Bottom Left 4
.byte %11000000
.byte %11000000
.byte %11000000
.byte %11000000
.byte %11000000
.byte %11000000
.byte %11111111
.byte %11111111
.endrepeat
T_FORWARD = 5
.repeat 2 ; FW 5
.byte %00000011
.byte %00000111
.byte %00001110
.byte %00011100
.byte %00111000
.byte %01110000
.byte %11100000
.byte %11000000
.endrepeat
T_BACKWARD = 6
.repeat 2 ; BW 6
.byte %11000000
.byte %11100000
.byte %01110000
.byte %00111000
.byte %00011100
.byte %00001110
.byte %00000111
.byte %00000011
.endrepeat
T_PILL_L = 7
.repeat 2 ; Pill L 7
.byte %00111111
.byte %01111111
.byte %11100000
.byte %11000000
.byte %11000000
.byte %11100000
.byte %01111111
.byte %00111111
.endrepeat
T_PILL_R = 8
.repeat 2 ; Pill R 8
.byte %11111100
.byte %11111110
.byte %00000111
.byte %00000011
.byte %00000011
.byte %00000111
.byte %11111110
.byte %11111100
.endrepeat
.popseg
zp_res TEMP
.macro load_ppu_addr addr
lda #.hibyte(addr)
sta PPUADDR
lda #.lobyte(addr)
sta PPUADDR ; Load $2000
.endmacro
reset:
sei
cld
ldx #$FF
txs
; Init PPU
bit PPUSTATUS
vwait1:
bit PPUSTATUS
bpl vwait1
vwait2:
bit PPUSTATUS
bpl vwait2
lda #$00
sta PPUCTRL ; NMI off, PPU slave, Small sprites, 0 addresses
; Fill NT0
load_ppu_addr $2000
ldx #$00
ldy #$04
lda #$00
fill_loop:
sta PPUDATA
dex
bne fill_loop
dey
bne fill_loop
; Set specific tiles
load_ppu_addr $2184
lda #T_TOP_LEFT
sta PPUDATA
lda #T_TOP_RIGHT
sta PPUDATA
load_ppu_addr $21A4
lda #T_BACKWARD
sta PPUDATA
lda #T_FORWARD
sta PPUDATA
load_ppu_addr $21C2
lda #T_TOP_LEFT
sta PPUDATA
lda #T_BACKWARD
sta PPUDATA
load_ppu_addr $21E2
lda #T_BOTTOM_LEFT
sta PPUDATA
lda #T_FORWARD
sta PPUDATA
load_ppu_addr $21C6
lda #T_FORWARD
sta PPUDATA
lda #T_TOP_RIGHT
sta PPUDATA
load_ppu_addr $21E6
lda #T_BACKWARD
sta PPUDATA
lda #T_BOTTOM_RIGHT
sta PPUDATA
load_ppu_addr $2204
lda #T_FORWARD
sta PPUDATA
lda #T_BACKWARD
sta PPUDATA
load_ppu_addr $2224
lda #T_BOTTOM_LEFT
sta PPUDATA
lda #T_BOTTOM_RIGHT
sta PPUDATA
load_ppu_addr $21EA
lda #T_PILL_L
sta PPUDATA
lda #T_PILL_R
sta PPUDATA
lda #$00
sta PPUDATA
sta PPUDATA
lda #T_PILL_L
sta PPUDATA
lda #T_PILL_R
sta PPUDATA
load_ppu_addr $21D2
lda #T_FORWARD
sta PPUDATA
lda #T_BACKWARD
sta PPUDATA
load_ppu_addr $21F2
lda #T_TOP_LEFT
sta PPUDATA
lda #T_TOP_RIGHT
sta PPUDATA
load_ppu_addr $21D6
lda #T_TOP_LEFT
sta PPUDATA
lda #T_PILL_R
sta PPUDATA
load_ppu_addr $21F6
lda #T_TOP_LEFT
sta PPUDATA
lda #T_PILL_R
sta PPUDATA
load_ppu_addr $3F00
lda #$0f
sta PPUDATA
lda #$20
sta PPUDATA
sta PPUDATA
sta PPUDATA
lda #$0f
sta PPUDATA
lda #$09
sta PPUDATA
sta PPUDATA
sta PPUDATA
lda #$00
sta PPUSCROLL
sta PPUSCROLL
lda #%00001110
sta PPUMASK
lda #$80
sta PPUCTRL ; NMI on, PPU slave, Small sprites, 0 addresses
loop:
jmp loop
.macro shl_a count
.scope
ldx #count
a_l:asl A
dex
bne a_l
.endscope
.endmacro
nmi:
lda PPUSTATUS
lda #%00000110
sta PPUMASK ; Force blank
lda #$01
sta JOY1
lda #$00
sta JOY1
load_ppu_addr $23DC ; A
lda JOY1
and #$01
shl_a 6
sta PPUDATA
load_ppu_addr $23DD ; B
lda JOY1
and #$01
shl_a 6
sta PPUDATA
load_ppu_addr $23DA ; Select
lda JOY1
and #$01
shl_a 6
sta PPUDATA
load_ppu_addr $23DB ; Start
lda JOY1
and #$01
shl_a 6
sta PPUDATA
lda JOY1 ; Up
and #$01
sta TEMP
load_ppu_addr $23E1 ; Down
lda JOY1
and #$01
sta PPUDATA
load_ppu_addr $23D8 ; Left
lda JOY1
and #$01
shl_a 6
sta PPUDATA
load_ppu_addr $23D9 ; Right
lda JOY1
and #$01
shl_a 6
ora TEMP
sta PPUDATA
lda #$00
sta PPUSCROLL
sta PPUSCROLL
lda #%00001110
sta PPUMASK ; Enable rendering
rti
exit:
stp
irq:
stp

View File

@@ -36,6 +36,8 @@ macro_rules! rom_test {
pub(crate) use rom_test;
use crate::{Break, NES};
rom_test!(basic_cpu, "basic-cpu.nes", |nes| {
assert_eq!(nes.last_instruction(), "0x8001 HLT :2 []");
assert_eq!(nes.cpu_cycle(), 10);
@@ -136,6 +138,67 @@ rom_test!(even_odd, "even_odd.nes", |nes| {
assert_eq!(nes.cpu.y, 0x00);
});
fn run_frame(nes: &mut NES) {
nes.run_one_clock_cycle(&Break::default());
while nes.ppu().scanline != 241 || nes.ppu().pixel != 0 {
nes.run_one_clock_cycle(&Break::default());
}
}
rom_test!(input_test, "input_test.nes", timeout = 86964*3, |nes| {
const A: u16 = 0x23DC; // 0 || 0b01000000
const B: u16 = 0x23DD; // 0 || 0b01000000
const SELECT: u16 = 0x23DA; // 0 || 0b01000000
const START: u16 = 0x23DB; // 0 || 0b01000000
const DOWN: u16 = 0x23E1; // 0 || 1
const LEFT: u16 = 0x23D8; // 0 || 0b01000000
const UP_RIGHT: u16 = 0x23D9; // 0 || 0b01000000 | 1
run_frame(&mut nes);
assert_eq!(nes.mapped.peek_ppu(A).unwrap(), 0);
assert_eq!(nes.mapped.peek_ppu(B).unwrap(), 0);
assert_eq!(nes.mapped.peek_ppu(SELECT).unwrap(), 0);
assert_eq!(nes.mapped.peek_ppu(START).unwrap(), 0);
assert_eq!(nes.mapped.peek_ppu(DOWN).unwrap(), 0);
assert_eq!(nes.mapped.peek_ppu(LEFT).unwrap(), 0);
assert_eq!(nes.mapped.peek_ppu(UP_RIGHT).unwrap(), 0);
nes.controller_1().set_a(true);
run_frame(&mut nes);
assert_eq!(nes.mapped.peek_ppu(A).unwrap(), 0x40);
assert_eq!(nes.mapped.peek_ppu(B).unwrap(), 0);
assert_eq!(nes.mapped.peek_ppu(SELECT).unwrap(), 0);
assert_eq!(nes.mapped.peek_ppu(START).unwrap(), 0);
assert_eq!(nes.mapped.peek_ppu(DOWN).unwrap(), 0);
assert_eq!(nes.mapped.peek_ppu(LEFT).unwrap(), 0);
assert_eq!(nes.mapped.peek_ppu(UP_RIGHT).unwrap(), 0);
nes.controller_1().set_b(true);
run_frame(&mut nes);
assert_eq!(nes.mapped.peek_ppu(A).unwrap(), 0x40);
assert_eq!(nes.mapped.peek_ppu(B).unwrap(), 0x40);
assert_eq!(nes.mapped.peek_ppu(SELECT).unwrap(), 0);
assert_eq!(nes.mapped.peek_ppu(START).unwrap(), 0);
assert_eq!(nes.mapped.peek_ppu(DOWN).unwrap(), 0);
assert_eq!(nes.mapped.peek_ppu(LEFT).unwrap(), 0);
assert_eq!(nes.mapped.peek_ppu(UP_RIGHT).unwrap(), 0);
nes.controller_1().0 = 0xFF;
run_frame(&mut nes);
assert_eq!(nes.mapped.peek_ppu(A).unwrap(), 0x40);
assert_eq!(nes.mapped.peek_ppu(B).unwrap(), 0x40);
assert_eq!(nes.mapped.peek_ppu(SELECT).unwrap(), 0x40);
assert_eq!(nes.mapped.peek_ppu(START).unwrap(), 0x40);
assert_eq!(nes.mapped.peek_ppu(DOWN).unwrap(), 0x01);
assert_eq!(nes.mapped.peek_ppu(LEFT).unwrap(), 0x40);
assert_eq!(nes.mapped.peek_ppu(UP_RIGHT).unwrap(), 0x41);
nes.controller_1().0 = 0;
run_frame(&mut nes);
assert_eq!(nes.mapped.peek_ppu(A).unwrap(), 0);
assert_eq!(nes.mapped.peek_ppu(B).unwrap(), 0);
assert_eq!(nes.mapped.peek_ppu(SELECT).unwrap(), 0);
assert_eq!(nes.mapped.peek_ppu(START).unwrap(), 0);
assert_eq!(nes.mapped.peek_ppu(DOWN).unwrap(), 0);
assert_eq!(nes.mapped.peek_ppu(LEFT).unwrap(), 0);
assert_eq!(nes.mapped.peek_ppu(UP_RIGHT).unwrap(), 0);
});
// rom_test!(even_odd, "even_odd.nes", |nes| {
// assert_eq!(nes.last_instruction(), "0x8023 HLT :2 []");
// assert_eq!(nes.cpu_cycle(), 57182);

View File

@@ -94,3 +94,8 @@ rom_test!(ppu_palette_shared, "ppu_palette_shared.nes", |nes| {
});
// Sets up an image, and scrolls a specific number of pixels over
rom_test!(ppu_scrolling, "ppu_fine_x_scrolling.nes", timeout = 86964*4, |nes| {
assert_eq!(nes.image().read(0, 0), Color { r: 0xFF, g: 0xFE, b: 0xFF });
});

View File

@@ -0,0 +1,306 @@
.include "testing.s"
; patterns_bin "pat.bin"
.macro chr b,s
.repeat s
.byte b
.endrepeat
.endmacro
.pushseg
.segment "CHARS"
T_EMPTY = 0
.repeat 2 ; Empty 0
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.endrepeat
T_TOP_LEFT = 1
.repeat 2 ; Top Left 1
.byte %11111111
.byte %11111111
.byte %11000000
.byte %11000000
.byte %11000000
.byte %11000000
.byte %11000000
.byte %11000000
.endrepeat
T_TOP_RIGHT = 2
.repeat 2 ; Top Right 2
.byte %11111111
.byte %11111111
.byte %00000011
.byte %00000011
.byte %00000011
.byte %00000011
.byte %00000011
.byte %00000011
.endrepeat
T_BOTTOM_RIGHT = 3
.repeat 2 ; Bottom Right 3
.byte %00000011
.byte %00000011
.byte %00000011
.byte %00000011
.byte %00000011
.byte %00000011
.byte %11111111
.byte %11111111
.endrepeat
T_BOTTOM_LEFT = 4
.repeat 2 ; Bottom Left 4
.byte %11000000
.byte %11000000
.byte %11000000
.byte %11000000
.byte %11000000
.byte %11000000
.byte %11111111
.byte %11111111
.endrepeat
T_FORWARD = 5
.repeat 2 ; FW 5
.byte %00000011
.byte %00000111
.byte %00001110
.byte %00011100
.byte %00111000
.byte %01110000
.byte %11100000
.byte %11000000
.endrepeat
T_BACKWARD = 6
.repeat 2 ; BW 6
.byte %11000000
.byte %11100000
.byte %01110000
.byte %00111000
.byte %00011100
.byte %00001110
.byte %00000111
.byte %00000011
.endrepeat
T_PILL_L = 7
.repeat 2 ; Pill L 7
.byte %00111111
.byte %01111111
.byte %11100000
.byte %11000000
.byte %11000000
.byte %11100000
.byte %01111111
.byte %00111111
.endrepeat
T_PILL_R = 8
.repeat 2 ; Pill R 8
.byte %11111100
.byte %11111110
.byte %00000111
.byte %00000011
.byte %00000011
.byte %00000111
.byte %11111110
.byte %11111100
.endrepeat
T_LINE = 9
.repeat 2 ; Empty 0
.byte %00100000
.byte %00100000
.byte %00100000
.byte %00100000
.byte %00100000
.byte %00100000
.byte %00100000
.byte %00100000
.endrepeat
.popseg
zp_res Y_SCROLL
zp_res X_SCROLL
zp_res CTRL_BYTE
.macro load_ppu_addr addr
lda #.hibyte(addr)
sta PPUADDR
lda #.lobyte(addr)
sta PPUADDR ; Load $2000
.endmacro
reset:
sei
cld
ldx #$FF
txs
; Init PPU
bit PPUSTATUS
vwait1:
bit PPUSTATUS
bpl vwait1
vwait2:
bit PPUSTATUS
bpl vwait2
lda #$00
sta PPUCTRL ; NMI off, PPU slave, Small sprites, 0 addresses
; Fill NT0
load_ppu_addr $2000
ldx #$00
ldy #$04
lda #$00
fill_loop:
sta PPUDATA
dex
bne fill_loop
dey
bne fill_loop
; Set specific tiles
load_ppu_addr $2184
lda #T_TOP_LEFT
sta PPUDATA
lda #T_TOP_RIGHT
sta PPUDATA
load_ppu_addr $21A4
lda #T_BACKWARD
sta PPUDATA
lda #T_FORWARD
sta PPUDATA
load_ppu_addr $21C2
lda #T_TOP_LEFT
sta PPUDATA
lda #T_BACKWARD
sta PPUDATA
load_ppu_addr $21E2
lda #T_BOTTOM_LEFT
sta PPUDATA
lda #T_FORWARD
sta PPUDATA
load_ppu_addr $21C6
lda #T_FORWARD
sta PPUDATA
lda #T_TOP_RIGHT
sta PPUDATA
load_ppu_addr $21E6
lda #T_BACKWARD
sta PPUDATA
lda #T_BOTTOM_RIGHT
sta PPUDATA
load_ppu_addr $2204
lda #T_FORWARD
sta PPUDATA
lda #T_BACKWARD
sta PPUDATA
load_ppu_addr $2224
lda #T_BOTTOM_LEFT
sta PPUDATA
lda #T_BOTTOM_RIGHT
sta PPUDATA
load_ppu_addr $21EA
lda #T_PILL_L
sta PPUDATA
lda #T_PILL_R
sta PPUDATA
lda #$00
sta PPUDATA
sta PPUDATA
lda #T_PILL_L
sta PPUDATA
lda #T_PILL_R
sta PPUDATA
load_ppu_addr $21D2
lda #T_FORWARD
sta PPUDATA
lda #T_BACKWARD
sta PPUDATA
load_ppu_addr $21F2
lda #T_TOP_LEFT
sta PPUDATA
lda #T_TOP_RIGHT
sta PPUDATA
load_ppu_addr $21D6
lda #T_TOP_LEFT
sta PPUDATA
lda #T_PILL_R
sta PPUDATA
load_ppu_addr $21F6
lda #T_TOP_LEFT
sta PPUDATA
lda #T_PILL_R
sta PPUDATA
load_ppu_addr $3F00
lda #$0f
sta PPUDATA
lda #$20
sta PPUDATA
sta PPUDATA
sta PPUDATA
lda #$0f
sta PPUDATA
lda #$09
sta PPUDATA
sta PPUDATA
sta PPUDATA
load_ppu_addr $2001
lda #T_LINE
sta PPUDATA
lda #$00
sta PPUSCROLL
sta PPUSCROLL
lda #%00001110
sta PPUMASK
lda #$80
sta PPUCTRL ; NMI on, PPU slave, Small sprites, 0 addresses
sta CTRL_BYTE
loop:
jmp loop
nmi:
lda PPUSTATUS
lda #%00000110
sta PPUMASK ; Force blank
lda #$0A
; adc X_SCROLL
; sta X_SCROLL
sta PPUSCROLL
; bcc y_scroll
; lda #$01
; eor CTRL_BYTE
; sta CTRL_BYTE
; sta PPUCTRL
; y_scroll:
; clc
lda #$00
; adc Y_SCROLL
; sta Y_SCROLL
sta PPUSCROLL
lda CTRL_BYTE
sta PPUCTRL
lda #%00001110
sta PPUMASK ; Enable rendering
rti
exit:
stp
irq:
stp

306
src/test_roms/scrolling.s Normal file
View File

@@ -0,0 +1,306 @@
.include "testing.s"
; patterns_bin "pat.bin"
.macro chr b,s
.repeat s
.byte b
.endrepeat
.endmacro
.pushseg
.segment "CHARS"
T_EMPTY = 0
.repeat 2 ; Empty 0
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.endrepeat
T_TOP_LEFT = 1
.repeat 2 ; Top Left 1
.byte %11111111
.byte %11111111
.byte %11000000
.byte %11000000
.byte %11000000
.byte %11000000
.byte %11000000
.byte %11000000
.endrepeat
T_TOP_RIGHT = 2
.repeat 2 ; Top Right 2
.byte %11111111
.byte %11111111
.byte %00000011
.byte %00000011
.byte %00000011
.byte %00000011
.byte %00000011
.byte %00000011
.endrepeat
T_BOTTOM_RIGHT = 3
.repeat 2 ; Bottom Right 3
.byte %00000011
.byte %00000011
.byte %00000011
.byte %00000011
.byte %00000011
.byte %00000011
.byte %11111111
.byte %11111111
.endrepeat
T_BOTTOM_LEFT = 4
.repeat 2 ; Bottom Left 4
.byte %11000000
.byte %11000000
.byte %11000000
.byte %11000000
.byte %11000000
.byte %11000000
.byte %11111111
.byte %11111111
.endrepeat
T_FORWARD = 5
.repeat 2 ; FW 5
.byte %00000011
.byte %00000111
.byte %00001110
.byte %00011100
.byte %00111000
.byte %01110000
.byte %11100000
.byte %11000000
.endrepeat
T_BACKWARD = 6
.repeat 2 ; BW 6
.byte %11000000
.byte %11100000
.byte %01110000
.byte %00111000
.byte %00011100
.byte %00001110
.byte %00000111
.byte %00000011
.endrepeat
T_PILL_L = 7
.repeat 2 ; Pill L 7
.byte %00111111
.byte %01111111
.byte %11100000
.byte %11000000
.byte %11000000
.byte %11100000
.byte %01111111
.byte %00111111
.endrepeat
T_PILL_R = 8
.repeat 2 ; Pill R 8
.byte %11111100
.byte %11111110
.byte %00000111
.byte %00000011
.byte %00000011
.byte %00000111
.byte %11111110
.byte %11111100
.endrepeat
T_LINE = 9
.repeat 2 ; Empty 0
.byte %00100000
.byte %00100000
.byte %00100000
.byte %00100000
.byte %00100000
.byte %00100000
.byte %00100000
.byte %00100000
.endrepeat
.popseg
zp_res Y_SCROLL
zp_res X_SCROLL
zp_res CTRL_BYTE
.macro load_ppu_addr addr
lda #.hibyte(addr)
sta PPUADDR
lda #.lobyte(addr)
sta PPUADDR ; Load $2000
.endmacro
reset:
sei
cld
ldx #$FF
txs
; Init PPU
bit PPUSTATUS
vwait1:
bit PPUSTATUS
bpl vwait1
vwait2:
bit PPUSTATUS
bpl vwait2
lda #$00
sta PPUCTRL ; NMI off, PPU slave, Small sprites, 0 addresses
; Fill NT0
load_ppu_addr $2000
ldx #$00
ldy #$04
lda #$00
fill_loop:
sta PPUDATA
dex
bne fill_loop
dey
bne fill_loop
; Set specific tiles
load_ppu_addr $2184
lda #T_TOP_LEFT
sta PPUDATA
lda #T_TOP_RIGHT
sta PPUDATA
load_ppu_addr $21A4
lda #T_BACKWARD
sta PPUDATA
lda #T_FORWARD
sta PPUDATA
load_ppu_addr $21C2
lda #T_TOP_LEFT
sta PPUDATA
lda #T_BACKWARD
sta PPUDATA
load_ppu_addr $21E2
lda #T_BOTTOM_LEFT
sta PPUDATA
lda #T_FORWARD
sta PPUDATA
load_ppu_addr $21C6
lda #T_FORWARD
sta PPUDATA
lda #T_TOP_RIGHT
sta PPUDATA
load_ppu_addr $21E6
lda #T_BACKWARD
sta PPUDATA
lda #T_BOTTOM_RIGHT
sta PPUDATA
load_ppu_addr $2204
lda #T_FORWARD
sta PPUDATA
lda #T_BACKWARD
sta PPUDATA
load_ppu_addr $2224
lda #T_BOTTOM_LEFT
sta PPUDATA
lda #T_BOTTOM_RIGHT
sta PPUDATA
load_ppu_addr $21EA
lda #T_PILL_L
sta PPUDATA
lda #T_PILL_R
sta PPUDATA
lda #$00
sta PPUDATA
sta PPUDATA
lda #T_PILL_L
sta PPUDATA
lda #T_PILL_R
sta PPUDATA
load_ppu_addr $21D2
lda #T_FORWARD
sta PPUDATA
lda #T_BACKWARD
sta PPUDATA
load_ppu_addr $21F2
lda #T_TOP_LEFT
sta PPUDATA
lda #T_TOP_RIGHT
sta PPUDATA
load_ppu_addr $21D6
lda #T_TOP_LEFT
sta PPUDATA
lda #T_PILL_R
sta PPUDATA
load_ppu_addr $21F6
lda #T_TOP_LEFT
sta PPUDATA
lda #T_PILL_R
sta PPUDATA
load_ppu_addr $3F00
lda #$0f
sta PPUDATA
lda #$20
sta PPUDATA
sta PPUDATA
sta PPUDATA
lda #$0f
sta PPUDATA
lda #$09
sta PPUDATA
sta PPUDATA
sta PPUDATA
load_ppu_addr $2001
lda #T_LINE
sta PPUDATA
lda #$00
sta PPUSCROLL
sta PPUSCROLL
lda #%00001110
sta PPUMASK
lda #$80
sta PPUCTRL ; NMI on, PPU slave, Small sprites, 0 addresses
sta CTRL_BYTE
loop:
jmp loop
nmi:
lda PPUSTATUS
lda #%00000110
sta PPUMASK ; Force blank
lda #$0A
adc X_SCROLL
sta X_SCROLL
sta PPUSCROLL
bcc y_scroll
lda #$01
eor CTRL_BYTE
sta CTRL_BYTE
sta PPUCTRL
y_scroll:
clc
lda #$00
adc Y_SCROLL
sta Y_SCROLL
sta PPUSCROLL
lda CTRL_BYTE
sta PPUCTRL
lda #%00001110
sta PPUMASK ; Enable rendering
rti
exit:
stp
irq:
stp

321
src/test_roms/sprites.s Normal file
View File

@@ -0,0 +1,321 @@
.include "testing.s"
.pushseg
.segment "CHARS"
T_EMPTY = 0
.repeat 2 ; Empty 0
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.endrepeat
T_TOP_LEFT = 1
.repeat 2 ; Top Left 1
.byte %11111111
.byte %11111111
.byte %11000000
.byte %11000000
.byte %11000000
.byte %11000000
.byte %11000000
.byte %11000000
.endrepeat
T_TOP_RIGHT = 2
.repeat 2 ; Top Right 2
.byte %11111111
.byte %11111111
.byte %00000011
.byte %00000011
.byte %00000011
.byte %00000011
.byte %00000011
.byte %00000011
.endrepeat
T_BOTTOM_RIGHT = 3
.repeat 2 ; Bottom Right 3
.byte %00000011
.byte %00000011
.byte %00000011
.byte %00000011
.byte %00000011
.byte %00000011
.byte %11111111
.byte %11111111
.endrepeat
T_BOTTOM_LEFT = 4
.repeat 2 ; Bottom Left 4
.byte %11000000
.byte %11000000
.byte %11000000
.byte %11000000
.byte %11000000
.byte %11000000
.byte %11111111
.byte %11111111
.endrepeat
T_FORWARD = 5
.repeat 2 ; FW 5
.byte %00000011
.byte %00000111
.byte %00001110
.byte %00011100
.byte %00111000
.byte %01110000
.byte %11100000
.byte %11000000
.endrepeat
T_BACKWARD = 6
.repeat 2 ; BW 6
.byte %11000000
.byte %11100000
.byte %01110000
.byte %00111000
.byte %00011100
.byte %00001110
.byte %00000111
.byte %00000011
.endrepeat
T_PILL_L = 7
.repeat 2 ; Pill L 7
.byte %00111111
.byte %01111111
.byte %11100000
.byte %11000000
.byte %11000000
.byte %11100000
.byte %01111111
.byte %00111111
.endrepeat
T_PILL_R = 8
.repeat 2 ; Pill R 8
.byte %11111100
.byte %11111110
.byte %00000111
.byte %00000011
.byte %00000011
.byte %00000111
.byte %11111110
.byte %11111100
.endrepeat
T_LINE = 9
.repeat 2 ; Empty 0
.byte %00100000
.byte %00100000
.byte %00100000
.byte %00100000
.byte %00100000
.byte %00100000
.byte %00100000
.byte %00100000
.endrepeat
.popseg
zp_res Y_SCROLL
zp_res X_SCROLL
zp_res CTRL_BYTE
.macro load_ppu_addr addr
lda #.hibyte(addr)
sta PPUADDR
lda #.lobyte(addr)
sta PPUADDR ; Load $2000
.endmacro
reset:
sei
cld
ldx #$FF
txs
; Init PPU
bit PPUSTATUS
vwait1:
bit PPUSTATUS
bpl vwait1
vwait2:
bit PPUSTATUS
bpl vwait2
lda #$00
sta PPUCTRL ; NMI off, PPU slave, Small sprites, 0 addresses
; Fill NT0
load_ppu_addr $2000
ldx #$00
ldy #$04
lda #$00
fill_loop:
sta PPUDATA
dex
bne fill_loop
dey
bne fill_loop
; Set specific tiles
load_ppu_addr $2184
lda #T_TOP_LEFT
sta PPUDATA
lda #T_TOP_RIGHT
sta PPUDATA
load_ppu_addr $21A4
lda #T_BACKWARD
sta PPUDATA
lda #T_FORWARD
sta PPUDATA
load_ppu_addr $21C2
lda #T_TOP_LEFT
sta PPUDATA
lda #T_BACKWARD
sta PPUDATA
load_ppu_addr $21E2
lda #T_BOTTOM_LEFT
sta PPUDATA
lda #T_FORWARD
sta PPUDATA
load_ppu_addr $21C6
lda #T_FORWARD
sta PPUDATA
lda #T_TOP_RIGHT
sta PPUDATA
load_ppu_addr $21E6
lda #T_BACKWARD
sta PPUDATA
lda #T_BOTTOM_RIGHT
sta PPUDATA
load_ppu_addr $2204
lda #T_FORWARD
sta PPUDATA
lda #T_BACKWARD
sta PPUDATA
load_ppu_addr $2224
lda #T_BOTTOM_LEFT
sta PPUDATA
lda #T_BOTTOM_RIGHT
sta PPUDATA
load_ppu_addr $21EA
lda #T_PILL_L
sta PPUDATA
lda #T_PILL_R
sta PPUDATA
lda #$00
sta PPUDATA
sta PPUDATA
lda #T_PILL_L
sta PPUDATA
lda #T_PILL_R
sta PPUDATA
load_ppu_addr $21D2
lda #T_FORWARD
sta PPUDATA
lda #T_BACKWARD
sta PPUDATA
load_ppu_addr $21F2
lda #T_TOP_LEFT
sta PPUDATA
lda #T_TOP_RIGHT
sta PPUDATA
load_ppu_addr $21D6
lda #T_TOP_LEFT
sta PPUDATA
lda #T_PILL_R
sta PPUDATA
load_ppu_addr $21F6
lda #T_TOP_LEFT
sta PPUDATA
lda #T_PILL_R
sta PPUDATA
load_ppu_addr $3F00
lda #$0f
sta PPUDATA
lda #$20
sta PPUDATA
sta PPUDATA
sta PPUDATA
lda #$0f
sta PPUDATA
lda #$09
sta PPUDATA
sta PPUDATA
sta PPUDATA
load_ppu_addr $2001
lda #T_LINE
sta PPUDATA
ldx #$00
sprite_loop:
lda sprite_data,x
sta $300,x
inx
cpx #(sprite_data_end - sprite_data)
bne sprite_loop
lda #$00
sta PPUSCROLL
sta PPUSCROLL
lda #%00001110
sta PPUMASK
lda #$80
sta PPUCTRL ; NMI on, PPU slave, Small sprites, 0 addresses
sta CTRL_BYTE
loop:
jmp loop
nmi:
lda PPUSTATUS
lda #%00000110
sta PPUMASK ; Force blank
lda #$01
adc X_SCROLL
sta X_SCROLL
sta PPUSCROLL
bcc y_scroll
lda #$01
eor CTRL_BYTE
sta CTRL_BYTE
sta PPUCTRL
y_scroll:
clc
lda #$00
adc Y_SCROLL
sta Y_SCROLL
sta PPUSCROLL
lda #$00
sta SPRADDR
lda #$03
sta SPRDMA
lda CTRL_BYTE
sta PPUCTRL
lda #%00011110
sta PPUMASK ; Enable rendering
rti
exit:
stp
irq:
stp
sprite_data:
.byte $00
.byte T_PILL_R
.byte $00
.byte $00
sprite_data_end: