pub mod affine;
pub mod color;
pub mod obj;
use self::affine::BgAffine;
use super::bus::interrupt::{InterruptController, bits as irq_bits};
use serde::{Deserialize, Serialize};
pub const SCREEN_WIDTH: u32 = 240;
pub const SCREEN_HEIGHT: u32 = 160;
pub const BYTES_PER_PIXEL: usize = 3;
const OAM_BYTES: usize = 1024;
pub const FRAMEBUFFER_BYTES: usize =
(SCREEN_WIDTH as usize) * (SCREEN_HEIGHT as usize) * BYTES_PER_PIXEL;
const TRANSPARENT: u16 = 0x8000;
const MODE3_FRAME_BASE: usize = 0x0000;
const MODE5_WIDTH: usize = 160;
const MODE5_HEIGHT: usize = 128;
pub const CYCLES_PER_SCANLINE: u32 = 1232;
pub const HBLANK_START_CYCLE: u32 = 1004;
pub const VISIBLE_SCANLINES: u32 = 160;
pub const SCANLINES_PER_FRAME: u32 = 228;
pub const VBLANK_LAST_SCANLINE: u32 = 226;
pub mod dispcnt {
pub const MODE_MASK: u16 = 0x0007;
pub const FRAME_SELECT: u16 = 1 << 4;
pub const FORCED_BLANK: u16 = 1 << 7;
pub const BG0_ENABLE: u16 = 1 << 8;
pub const BG1_ENABLE: u16 = 1 << 9;
pub const BG2_ENABLE: u16 = 1 << 10;
pub const BG3_ENABLE: u16 = 1 << 11;
pub const OBJ_ENABLE: u16 = 1 << 12;
pub const OBJ_MAPPING_1D: u16 = 1 << 6;
pub const HBLANK_INTERVAL_FREE: u16 = 1 << 5;
pub const WIN0_ENABLE: u16 = 1 << 13;
pub const WIN1_ENABLE: u16 = 1 << 14;
pub const OBJ_WIN_ENABLE: u16 = 1 << 15;
}
pub mod dispstat {
pub const VBLANK_FLAG: u16 = 1 << 0;
pub const HBLANK_FLAG: u16 = 1 << 1;
pub const VCOUNT_FLAG: u16 = 1 << 2;
pub const STATUS_MASK: u16 = VBLANK_FLAG | HBLANK_FLAG | VCOUNT_FLAG;
pub const VBLANK_IRQ_ENABLE: u16 = 1 << 3;
pub const HBLANK_IRQ_ENABLE: u16 = 1 << 4;
pub const VCOUNT_IRQ_ENABLE: u16 = 1 << 5;
pub const VCOUNT_SETTING_MASK: u16 = 0xFF00;
pub const WRITE_MASK: u16 =
VBLANK_IRQ_ENABLE | HBLANK_IRQ_ENABLE | VCOUNT_IRQ_ENABLE | VCOUNT_SETTING_MASK;
}
pub const REG_DISPCNT: u32 = 0x0400_0000;
pub const REG_GREEN_SWAP: u32 = 0x0400_0002;
pub const REG_BG0CNT: u32 = 0x0400_0008;
pub const REG_BG1CNT: u32 = 0x0400_000A;
pub const REG_BG2CNT: u32 = 0x0400_000C;
pub const REG_BG3CNT: u32 = 0x0400_000E;
pub const REG_DISPSTAT: u32 = 0x0400_0004;
pub const REG_VCOUNT: u32 = 0x0400_0006;
pub const REG_BG0HOFS: u32 = 0x0400_0010;
pub const REG_BG0VOFS: u32 = 0x0400_0012;
pub const REG_BG1HOFS: u32 = 0x0400_0014;
pub const REG_BG1VOFS: u32 = 0x0400_0016;
pub const REG_BG2HOFS: u32 = 0x0400_0018;
pub const REG_BG2VOFS: u32 = 0x0400_001A;
pub const REG_BG3HOFS: u32 = 0x0400_001C;
pub const REG_BG3VOFS: u32 = 0x0400_001E;
pub const REG_BG2PA: u32 = 0x0400_0020;
pub const REG_BG2PB: u32 = 0x0400_0022;
pub const REG_BG2PC: u32 = 0x0400_0024;
pub const REG_BG2PD: u32 = 0x0400_0026;
pub const REG_BG2X_L: u32 = 0x0400_0028;
pub const REG_BG2X_H: u32 = 0x0400_002A;
pub const REG_BG2Y_L: u32 = 0x0400_002C;
pub const REG_BG2Y_H: u32 = 0x0400_002E;
pub const REG_BG3PA: u32 = 0x0400_0030;
pub const REG_BG3PB: u32 = 0x0400_0032;
pub const REG_BG3PC: u32 = 0x0400_0034;
pub const REG_BG3PD: u32 = 0x0400_0036;
pub const REG_BG3X_L: u32 = 0x0400_0038;
pub const REG_BG3X_H: u32 = 0x0400_003A;
pub const REG_BG3Y_L: u32 = 0x0400_003C;
pub const REG_BG3Y_H: u32 = 0x0400_003E;
pub const REG_WIN0H: u32 = 0x0400_0040;
pub const REG_WIN1H: u32 = 0x0400_0042;
pub const REG_WIN0V: u32 = 0x0400_0044;
pub const REG_WIN1V: u32 = 0x0400_0046;
pub const REG_WININ: u32 = 0x0400_0048;
pub const REG_WINOUT: u32 = 0x0400_004A;
pub const REG_MOSAIC: u32 = 0x0400_004C;
pub const REG_BLDCNT: u32 = 0x0400_0050;
pub const REG_BLDALPHA: u32 = 0x0400_0052;
pub const REG_BLDY: u32 = 0x0400_0054;
#[derive(Debug, Default, Clone, Copy)]
pub struct PpuStepEvents {
pub vblank_starts: u32,
pub hblank_starts: u32,
pub frames_completed: u32,
}
#[derive(Debug, Clone)]
pub struct Ppu {
dispcnt: u16,
dispstat: u16,
bg_cnt: [u16; 4],
vcount: u16,
line_cycle: u32,
framebuffer: Vec<u8>,
frame_ready: bool,
bg_affine: [BgAffine; 2],
bg_scroll: [(u16, u16); 4],
win_h: [u16; 2],
win_v: [u16; 2],
winin: u16,
winout: u16,
green_swap: bool,
bldcnt: u16,
bldalpha: u16,
bldy: u8,
mosaic: u16,
forced_blank_restart_lines: u32,
bg_enable_delays: [u8; 4],
bg_disable_hblank_cooldowns: [u8; 4],
bg_force_current_scanline: [bool; 4],
bg_force_current_scanline_x_offset: [i8; 4],
hblank_irq_raised_this_scanline: bool,
obj_render_oam: Vec<u8>,
obj_render_oam_initialized: bool,
color_correction: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PpuState {
dispcnt: u16,
dispstat: u16,
bg_cnt: [u16; 4],
vcount: u16,
line_cycle: u32,
framebuffer: Vec<u8>,
frame_ready: bool,
bg_affine: [BgAffine; 2],
bg_scroll: [(u16, u16); 4],
win_h: [u16; 2],
win_v: [u16; 2],
winin: u16,
winout: u16,
green_swap: bool,
bldcnt: u16,
bldalpha: u16,
bldy: u8,
mosaic: u16,
forced_blank_restart_lines: u32,
#[serde(default)]
bg_enable_delays: [u8; 4],
#[serde(default)]
bg_disable_hblank_cooldowns: [u8; 4],
#[serde(default)]
bg_force_current_scanline: [bool; 4],
#[serde(default)]
bg_force_current_scanline_x_offset: [i8; 4],
#[serde(default)]
hblank_irq_raised_this_scanline: bool,
#[serde(default = "default_obj_render_oam")]
obj_render_oam: Vec<u8>,
#[serde(default)]
obj_render_oam_initialized: bool,
color_correction: bool,
}
fn default_obj_render_oam() -> Vec<u8> {
vec![0; OAM_BYTES]
}
#[inline]
fn alpha_blend_bgr555(c1: u16, c2: u16, eva: u8, evb: u8) -> u16 {
let eva = eva as u32;
let evb = evb as u32;
let r = ((c1 & 0x1F) as u32 * eva + (c2 & 0x1F) as u32 * evb) >> 4;
let g = (((c1 >> 5) & 0x1F) as u32 * eva + ((c2 >> 5) & 0x1F) as u32 * evb) >> 4;
let b = (((c1 >> 10) & 0x1F) as u32 * eva + ((c2 >> 10) & 0x1F) as u32 * evb) >> 4;
(r.min(31) | (g.min(31) << 5) | (b.min(31) << 10)) as u16
}
#[inline]
fn brighten_bgr555(c: u16, evy: u8) -> u16 {
let evy = evy as u32;
let r = (c & 0x1F) as u32;
let g = ((c >> 5) & 0x1F) as u32;
let b = ((c >> 10) & 0x1F) as u32;
let r2 = r + (((31 - r) * evy) >> 4);
let g2 = g + (((31 - g) * evy) >> 4);
let b2 = b + (((31 - b) * evy) >> 4);
(r2 | (g2 << 5) | (b2 << 10)) as u16
}
#[inline]
fn darken_bgr555(c: u16, evy: u8) -> u16 {
let evy = evy as u32;
let r = (c & 0x1F) as u32;
let g = ((c >> 5) & 0x1F) as u32;
let b = ((c >> 10) & 0x1F) as u32;
let r2 = r - ((r * evy) >> 4);
let g2 = g - ((g * evy) >> 4);
let b2 = b - ((b * evy) >> 4);
(r2 | (g2 << 5) | (b2 << 10)) as u16
}
impl Default for Ppu {
fn default() -> Self {
Self::new()
}
}
const SORT_KEY_PRIORITY_SPACING: u16 = 10;
const SORT_KEY_BG_OFFSET: u16 = 5;
const BG_ENABLE_DELAY_LINES: u8 = 2;
const BG_ENABLE_HDRAW_DELAY_LINES: u8 = 3;
const BG_DISABLE_HBLANK_COOLDOWN_LINES: u8 = 2;
const HBLANK_IRQ_EARLY_CYCLES: u32 = 48;
#[inline]
fn mosaic_size(mosaic: u16, shift: u32) -> u32 {
(((mosaic >> shift) & 0x000F) + 1) as u32
}
fn mosaic_anchor(value: u32, block_size: u32) -> u32 {
debug_assert!(block_size > 0);
value - (value % block_size)
}
impl Ppu {
const BG_ENABLE_MASK: u16 =
dispcnt::BG0_ENABLE | dispcnt::BG1_ENABLE | dispcnt::BG2_ENABLE | dispcnt::BG3_ENABLE;
pub fn new() -> Self {
let mut ppu = Self {
dispcnt: 0,
dispstat: 0,
bg_cnt: [0; 4],
vcount: 0,
line_cycle: 0,
framebuffer: vec![0; FRAMEBUFFER_BYTES],
frame_ready: false,
bg_affine: [BgAffine::default(); 2],
bg_scroll: [(0, 0); 4],
win_h: [0; 2],
win_v: [0; 2],
winin: 0,
winout: 0,
green_swap: false,
bldcnt: 0,
bldalpha: 0,
bldy: 0,
mosaic: 0,
forced_blank_restart_lines: 0,
bg_enable_delays: [0; 4],
bg_disable_hblank_cooldowns: [0; 4],
bg_force_current_scanline: [false; 4],
bg_force_current_scanline_x_offset: [0; 4],
hblank_irq_raised_this_scanline: false,
obj_render_oam: default_obj_render_oam(),
obj_render_oam_initialized: false,
color_correction: false,
};
ppu.update_vcount_match_flag(None);
ppu
}
pub fn capture_state(&self) -> PpuState {
PpuState {
dispcnt: self.dispcnt,
dispstat: self.dispstat,
bg_cnt: self.bg_cnt,
vcount: self.vcount,
line_cycle: self.line_cycle,
framebuffer: self.framebuffer.clone(),
frame_ready: self.frame_ready,
bg_affine: self.bg_affine,
bg_scroll: self.bg_scroll,
win_h: self.win_h,
win_v: self.win_v,
winin: self.winin,
winout: self.winout,
green_swap: self.green_swap,
bldcnt: self.bldcnt,
bldalpha: self.bldalpha,
bldy: self.bldy,
mosaic: self.mosaic,
forced_blank_restart_lines: self.forced_blank_restart_lines,
bg_enable_delays: self.bg_enable_delays,
bg_disable_hblank_cooldowns: self.bg_disable_hblank_cooldowns,
bg_force_current_scanline: self.bg_force_current_scanline,
bg_force_current_scanline_x_offset: self.bg_force_current_scanline_x_offset,
hblank_irq_raised_this_scanline: self.hblank_irq_raised_this_scanline,
obj_render_oam: self.obj_render_oam.clone(),
obj_render_oam_initialized: self.obj_render_oam_initialized,
color_correction: self.color_correction,
}
}
pub fn restore_state(&mut self, state: &PpuState) {
self.dispcnt = state.dispcnt;
self.dispstat = state.dispstat;
self.bg_cnt = state.bg_cnt;
self.vcount = state.vcount;
self.line_cycle = state.line_cycle;
self.framebuffer.clone_from(&state.framebuffer);
self.frame_ready = state.frame_ready;
self.bg_affine = state.bg_affine;
self.bg_scroll = state.bg_scroll;
self.win_h = state.win_h;
self.win_v = state.win_v;
self.winin = state.winin;
self.winout = state.winout;
self.green_swap = state.green_swap;
self.bldcnt = state.bldcnt;
self.bldalpha = state.bldalpha;
self.bldy = state.bldy;
self.mosaic = state.mosaic;
self.forced_blank_restart_lines = state.forced_blank_restart_lines;
self.bg_enable_delays = state.bg_enable_delays;
self.bg_disable_hblank_cooldowns = state.bg_disable_hblank_cooldowns;
self.bg_force_current_scanline = state.bg_force_current_scanline;
self.bg_force_current_scanline_x_offset = state.bg_force_current_scanline_x_offset;
self.hblank_irq_raised_this_scanline = state.hblank_irq_raised_this_scanline;
self.obj_render_oam = state.obj_render_oam.clone();
self.obj_render_oam.resize(OAM_BYTES, 0);
self.obj_render_oam_initialized = state.obj_render_oam_initialized;
self.color_correction = state.color_correction;
}
pub fn read_dispcnt(&self) -> u16 {
self.dispcnt
}
pub fn write_dispcnt(&mut self, value: u16) {
let was_forced_blank = self.dispcnt & dispcnt::FORCED_BLANK != 0;
let is_forced_blank = value & dispcnt::FORCED_BLANK != 0;
let old_bg_enables = self.dispcnt & Self::BG_ENABLE_MASK;
let new_bg_enables = value & Self::BG_ENABLE_MASK;
self.dispcnt = value;
self.update_bg_enable_delays(old_bg_enables, new_bg_enables, was_forced_blank);
if was_forced_blank && !is_forced_blank {
self.forced_blank_restart_lines = 2;
} else if is_forced_blank {
self.forced_blank_restart_lines = 0;
self.bg_enable_delays = [0; 4];
self.bg_disable_hblank_cooldowns = [0; 4];
self.bg_force_current_scanline = [false; 4];
self.bg_force_current_scanline_x_offset = [0; 4];
self.obj_render_oam_initialized = false;
}
}
pub fn read_green_swap(&self) -> u16 {
self.green_swap as u16
}
pub fn write_green_swap(&mut self, value: u16) {
self.green_swap = value & 1 != 0;
}
pub fn set_color_correction(&mut self, enabled: bool) {
self.color_correction = enabled;
}
pub fn read_dispstat(&self) -> u16 {
self.dispstat
}
pub fn read_bg0cnt(&self) -> u16 {
self.bg_cnt[0]
}
pub fn write_dispstat(&mut self, value: u16, ic: &mut InterruptController) {
let status = self.dispstat & dispstat::STATUS_MASK;
self.dispstat = status | (value & dispstat::WRITE_MASK);
self.update_vcount_match_flag(Some(ic));
}
pub fn write_bg0cnt(&mut self, value: u16) {
self.bg_cnt[0] = value;
}
pub fn read_bg_cnt(&self, n: usize) -> u16 {
let mask = if n <= 1 { 0xDFFF } else { 0xFFFF };
self.bg_cnt[n] & mask
}
pub fn write_bg_cnt(&mut self, n: usize, value: u16) {
self.bg_cnt[n] = value;
}
pub fn read_vcount(&self) -> u16 {
self.vcount
}
pub fn mode(&self) -> u8 {
(self.dispcnt & dispcnt::MODE_MASK) as u8
}
pub fn forced_blank(&self) -> bool {
self.dispcnt & dispcnt::FORCED_BLANK != 0 || self.forced_blank_restart_lines > 0
}
pub fn vram_pram_active_wait(&self) -> bool {
if self.forced_blank() {
return false;
}
if (self.vcount as u32) >= VISIBLE_SCANLINES {
return false;
}
self.dispstat & dispstat::HBLANK_FLAG == 0
}
pub fn oam_active_wait(&self) -> bool {
if self.forced_blank() {
return false;
}
if (self.vcount as u32) >= VISIBLE_SCANLINES {
return false;
}
if self.dispstat & dispstat::HBLANK_FLAG != 0
&& self.dispcnt & dispcnt::HBLANK_INTERVAL_FREE != 0
{
return false;
}
true
}
pub fn bg2_enabled(&self) -> bool {
self.dispcnt & dispcnt::BG2_ENABLE != 0
}
pub fn bg0_enabled(&self) -> bool {
self.dispcnt & dispcnt::BG0_ENABLE != 0
}
fn bg_layer_enabled(&self, bg_idx: usize) -> bool {
let enable_bit = dispcnt::BG0_ENABLE << bg_idx;
self.dispcnt & enable_bit != 0
&& (self.bg_enable_delays[bg_idx] == 0 || self.bg_force_current_scanline[bg_idx])
}
fn update_bg_enable_delays(
&mut self,
old_bg_enables: u16,
new_bg_enables: u16,
was_forced_blank: bool,
) {
for bg_idx in 0..4 {
let enable_bit = dispcnt::BG0_ENABLE << bg_idx;
let was_enabled = old_bg_enables & enable_bit != 0;
let is_enabled = new_bg_enables & enable_bit != 0;
if !is_enabled {
if was_enabled && self.dispstat & dispstat::HBLANK_FLAG != 0 {
self.bg_disable_hblank_cooldowns[bg_idx] = BG_DISABLE_HBLANK_COOLDOWN_LINES;
}
self.bg_enable_delays[bg_idx] = 0;
self.bg_force_current_scanline[bg_idx] = false;
self.bg_force_current_scanline_x_offset[bg_idx] = 0;
} else if !was_enabled
&& self.bg_disable_hblank_cooldowns[bg_idx] != 0
&& self.dispstat & dispstat::HBLANK_FLAG == 0
&& self.in_active_display()
{
self.bg_enable_delays[bg_idx] = BG_ENABLE_HDRAW_DELAY_LINES;
self.bg_force_current_scanline[bg_idx] = true;
self.bg_force_current_scanline_x_offset[bg_idx] =
if self.line_cycle <= 44 { -2 } else { 2 };
self.bg_disable_hblank_cooldowns[bg_idx] = 0;
} else if !was_enabled && self.in_active_display() && !was_forced_blank {
self.bg_enable_delays[bg_idx] = if self.dispstat & dispstat::HBLANK_FLAG != 0 {
BG_ENABLE_DELAY_LINES
} else {
BG_ENABLE_HDRAW_DELAY_LINES
};
self.bg_force_current_scanline[bg_idx] = false;
self.bg_force_current_scanline_x_offset[bg_idx] = 0;
self.bg_disable_hblank_cooldowns[bg_idx] = 0;
}
}
}
fn in_active_display(&self) -> bool {
(self.vcount as u32) < VISIBLE_SCANLINES
&& (self.vcount != 0 || self.line_cycle != 0)
&& self.dispstat & dispstat::VBLANK_FLAG == 0
&& !self.forced_blank()
}
fn advance_bg_enable_delays_after_render(&mut self) {
for delay in &mut self.bg_enable_delays {
*delay = delay.saturating_sub(1);
}
self.bg_force_current_scanline = [false; 4];
self.bg_force_current_scanline_x_offset = [0; 4];
for cooldown in &mut self.bg_disable_hblank_cooldowns {
*cooldown = cooldown.saturating_sub(1);
}
}
fn forced_scanline_sample_x(&self, bg_idx: usize, x: usize) -> Option<usize> {
if !self.bg_force_current_scanline[bg_idx] || x >= 8 {
return Some(x);
}
(x as isize)
.checked_add(self.bg_force_current_scanline_x_offset[bg_idx] as isize)
.filter(|&adjusted| (0..SCREEN_WIDTH as isize).contains(&adjusted))
.map(|adjusted| adjusted as usize)
}
fn copy_live_oam_to_render_latch(&mut self, live_oam: &[u8]) {
let copy_len = live_oam.len().min(OAM_BYTES);
self.obj_render_oam[..copy_len].copy_from_slice(&live_oam[..copy_len]);
self.obj_render_oam[copy_len..].fill(0);
self.obj_render_oam_initialized = true;
}
fn obj_render_oam_for_scanline(&mut self, live_oam: &[u8]) -> Vec<u8> {
if !self.obj_render_oam_initialized {
self.copy_live_oam_to_render_latch(live_oam);
}
self.obj_render_oam.clone()
}
pub fn frame_select(&self) -> bool {
self.dispcnt & dispcnt::FRAME_SELECT != 0
}
pub fn bg_affine(&self, bg: usize) -> Option<&BgAffine> {
self.bg_affine.get(bg)
}
pub fn read_affine(&self, addr: u32) -> Option<u16> {
let bg = match addr {
0x0400_0020..=0x0400_002F => 0,
0x0400_0030..=0x0400_003F => 1,
_ => return None,
};
let a = &self.bg_affine[bg];
Some(match addr & 0x000F {
0x0 => a.pa as u16,
0x2 => a.pb as u16,
0x4 => a.pc as u16,
0x6 => a.pd as u16,
0x8 => (a.x as u32) as u16,
0xA => ((a.x as u32) >> 16) as u16,
0xC => (a.y as u32) as u16,
0xE => ((a.y as u32) >> 16) as u16,
_ => return None,
})
}
pub fn write_affine(&mut self, addr: u32, value: u16) -> bool {
let bg = match addr {
0x0400_0020..=0x0400_002F => 0,
0x0400_0030..=0x0400_003F => 1,
_ => return false,
};
let a = &mut self.bg_affine[bg];
match addr & 0x000F {
0x0 => a.pa = value as i16,
0x2 => a.pb = value as i16,
0x4 => a.pc = value as i16,
0x6 => a.pd = value as i16,
0x8 => a.write_x_low(value),
0xA => a.write_x_high(value),
0xC => a.write_y_low(value),
0xE => a.write_y_high(value),
_ => return false, }
true
}
pub fn write_bg_hofs(&mut self, n: usize, value: u16) {
self.bg_scroll[n].0 = value & 0x01FF;
}
pub fn read_bg_hofs(&self, n: usize) -> u16 {
self.bg_scroll[n].0
}
pub fn write_bg_vofs(&mut self, n: usize, value: u16) {
self.bg_scroll[n].1 = value & 0x01FF;
}
pub fn read_bg_vofs(&self, n: usize) -> u16 {
self.bg_scroll[n].1
}
pub fn write_win_h(&mut self, n: usize, value: u16) {
self.win_h[n] = value;
}
pub fn read_win_h(&self, n: usize) -> u16 {
self.win_h[n]
}
pub fn write_win_v(&mut self, n: usize, value: u16) {
self.win_v[n] = value;
}
pub fn read_win_v(&self, n: usize) -> u16 {
self.win_v[n]
}
pub fn write_winin(&mut self, value: u16) {
self.winin = value & 0x3F3F;
}
pub fn read_winin(&self) -> u16 {
self.winin
}
pub fn write_winout(&mut self, value: u16) {
self.winout = value & 0x3F3F;
}
pub fn read_winout(&self) -> u16 {
self.winout
}
pub fn write_bldcnt(&mut self, value: u16) {
self.bldcnt = value & 0x3FFF;
}
pub fn read_bldcnt(&self) -> u16 {
self.bldcnt
}
pub fn write_bldalpha(&mut self, value: u16) {
self.bldalpha = value & 0x1F1F;
}
pub fn read_bldalpha(&self) -> u16 {
self.bldalpha
}
pub fn write_bldy(&mut self, value: u16) {
self.bldy = (value & 0x1F) as u8;
}
pub fn read_bldy(&self) -> u16 {
self.bldy as u16
}
pub fn write_mosaic(&mut self, value: u16) {
self.mosaic = value;
}
pub fn read_mosaic(&self) -> u16 {
self.mosaic
}
pub fn frame_ready(&self) -> bool {
self.frame_ready
}
pub fn clear_frame_ready(&mut self) {
self.frame_ready = false;
}
pub fn framebuffer(&self) -> &[u8] {
&self.framebuffer
}
pub fn step(
&mut self,
cycles: u32,
ic: &mut InterruptController,
vram: &[u8],
pram: &[u8],
oam: &[u8],
) -> PpuStepEvents {
let mut events = PpuStepEvents::default();
let mut remaining = cycles;
while remaining > 0 {
let take = remaining.min(CYCLES_PER_SCANLINE - self.line_cycle);
let next_cycle = self.line_cycle + take;
let hblank_irq_cycle = HBLANK_START_CYCLE.saturating_sub(HBLANK_IRQ_EARLY_CYCLES);
if self.line_cycle < hblank_irq_cycle
&& next_cycle >= hblank_irq_cycle
&& self.dispstat & dispstat::HBLANK_IRQ_ENABLE != 0
&& self.dispcnt & dispcnt::OBJ_ENABLE != 0
&& !self.hblank_irq_raised_this_scanline
{
ic.raise_late(
irq_bits::HBLANK,
next_cycle.saturating_sub(hblank_irq_cycle),
);
self.hblank_irq_raised_this_scanline = true;
}
if self.line_cycle < HBLANK_START_CYCLE && next_cycle >= HBLANK_START_CYCLE {
self.dispstat |= dispstat::HBLANK_FLAG;
if (self.vcount as u32) < VISIBLE_SCANLINES {
self.render_scanline(self.vcount as u32, vram, pram, oam);
for aff in &mut self.bg_affine {
aff.increment_reference_points();
}
self.advance_bg_enable_delays_after_render();
}
if (self.vcount as u32) < VISIBLE_SCANLINES {
events.hblank_starts = events.hblank_starts.saturating_add(1);
}
if self.dispstat & dispstat::HBLANK_IRQ_ENABLE != 0
&& !self.hblank_irq_raised_this_scanline
{
ic.raise_late(
irq_bits::HBLANK,
next_cycle.saturating_sub(HBLANK_START_CYCLE),
);
self.hblank_irq_raised_this_scanline = true;
}
}
self.line_cycle = next_cycle;
remaining -= take;
if self.line_cycle >= CYCLES_PER_SCANLINE {
self.line_cycle -= CYCLES_PER_SCANLINE;
self.advance_scanline(ic, &mut events);
}
}
events
}
fn advance_scanline(&mut self, ic: &mut InterruptController, events: &mut PpuStepEvents) {
self.dispstat &= !dispstat::HBLANK_FLAG;
self.hblank_irq_raised_this_scanline = false;
if self.forced_blank_restart_lines > 0 {
self.forced_blank_restart_lines -= 1;
if self.forced_blank_restart_lines == 0 {
self.vcount = 0;
self.dispstat &= !dispstat::VBLANK_FLAG;
for aff in &mut self.bg_affine {
aff.latch_reference_points();
}
self.obj_render_oam_initialized = false;
self.update_vcount_match_flag(Some(ic));
return;
}
}
let next = (self.vcount as u32 + 1) % SCANLINES_PER_FRAME;
self.vcount = next as u16;
if next == 0 {
self.obj_render_oam_initialized = false;
}
if next == VISIBLE_SCANLINES {
self.dispstat |= dispstat::VBLANK_FLAG;
events.vblank_starts = events.vblank_starts.saturating_add(1);
events.frames_completed = events.frames_completed.saturating_add(1);
self.frame_ready = true;
for aff in &mut self.bg_affine {
aff.latch_reference_points();
}
if self.dispstat & dispstat::VBLANK_IRQ_ENABLE != 0 {
ic.raise(irq_bits::VBLANK);
}
} else if next > VBLANK_LAST_SCANLINE {
self.dispstat &= !dispstat::VBLANK_FLAG;
}
self.update_vcount_match_flag(Some(ic));
}
fn update_vcount_match_flag(&mut self, ic: Option<&mut InterruptController>) {
let lyc = (self.dispstat >> 8) as u32;
let prev = self.dispstat & dispstat::VCOUNT_FLAG != 0;
let now = (self.vcount as u32) == lyc;
if now {
self.dispstat |= dispstat::VCOUNT_FLAG;
} else {
self.dispstat &= !dispstat::VCOUNT_FLAG;
}
if !prev
&& now
&& self.dispstat & dispstat::VCOUNT_IRQ_ENABLE != 0
&& let Some(ic) = ic
{
ic.raise(irq_bits::VCOUNT);
}
}
fn render_scanline(&mut self, y: u32, vram: &[u8], pram: &[u8], oam: &[u8]) {
if self.forced_blank() {
let row_start = (y as usize) * (SCREEN_WIDTH as usize) * BYTES_PER_PIXEL;
let row_end = row_start + (SCREEN_WIDTH as usize) * BYTES_PER_PIXEL;
for byte in &mut self.framebuffer[row_start..row_end] {
*byte = 0xFF;
}
return;
}
let render_oam = self.obj_render_oam_for_scanline(oam);
match self.mode() {
0 => self.render_mode0_scanline(y, vram, pram, &render_oam),
1 => self.render_mode1_scanline(y, vram, pram, &render_oam),
2 => self.render_mode2_scanline(y, vram, pram, &render_oam),
3 => self.render_mode3_scanline(y, vram, pram, &render_oam),
4 => self.render_mode4_scanline(y, vram, pram, &render_oam),
5 => self.render_mode5_scanline(y, vram, pram, &render_oam),
6 | 7 => self.render_prohibited_mode_scanline(y, vram, pram, &render_oam),
_ => self.render_backdrop_scanline(y, pram),
}
self.copy_live_oam_to_render_latch(oam);
if self.green_swap {
let row_start = (y as usize) * (SCREEN_WIDTH as usize) * BYTES_PER_PIXEL;
for x in (0..SCREEN_WIDTH as usize).step_by(2) {
let i0 = row_start + x * BYTES_PER_PIXEL + 1;
let i1 = row_start + (x + 1) * BYTES_PER_PIXEL + 1;
self.framebuffer.swap(i0, i1);
}
}
}
fn render_mode0_scanline(&mut self, y: u32, vram: &[u8], pram: &[u8], oam: &[u8]) {
let bg_enables = [
self.bg_layer_enabled(0),
self.bg_layer_enabled(1),
self.bg_layer_enabled(2),
self.bg_layer_enabled(3),
];
if !bg_enables.iter().any(|&e| e) && self.dispcnt & dispcnt::OBJ_ENABLE == 0 {
self.render_no_layers_scanline(y, vram, pram, oam);
return;
}
let mut layers: Vec<(usize, u8, [u16; SCREEN_WIDTH as usize])> = Vec::new();
for (i, &enabled) in bg_enables.iter().enumerate() {
if enabled {
let mut buf = [TRANSPARENT; SCREEN_WIDTH as usize];
self.render_text_bg_layer(i, y, vram, pram, &mut buf);
let prio = (self.bg_cnt[i] & 3) as u8;
layers.push((i, prio, buf));
}
}
self.composite_scanline(y, pram, vram, oam, &layers);
}
#[allow(clippy::too_many_arguments, clippy::needless_range_loop)]
fn render_text_bg_layer(
&self,
bg_idx: usize,
y: u32,
vram: &[u8],
pram: &[u8],
buf: &mut [u16; SCREEN_WIDTH as usize],
) {
let bgcnt = self.bg_cnt[bg_idx];
let is_8bpp = bgcnt & (1 << 7) != 0;
let mosaic_enabled = bgcnt & (1 << 6) != 0;
let (mosaic_h, mosaic_v) = self.bg_mosaic_size();
let bg_size = (bgcnt >> 14) & 0x0003;
let (width_tiles, height_tiles) = match bg_size {
0 => (32usize, 32usize),
1 => (64usize, 32usize),
2 => (32usize, 64usize),
_ => (64usize, 64usize),
};
let width_mask = width_tiles * 8 - 1;
let height_mask = height_tiles * 8 - 1;
let screenblock_base = (((bgcnt >> 8) & 0x001F) as usize) * 0x800;
let charblock_base = (((bgcnt >> 2) & 0x0003) as usize) * 16 * 1024;
let (hofs, vofs) = self.bg_scroll[bg_idx];
let sample_y = if mosaic_enabled {
mosaic_anchor(y, mosaic_v as u32) as usize
} else {
y as usize
};
let screen_y = (sample_y + vofs as usize) & height_mask;
for x in 0..(SCREEN_WIDTH as usize) {
let Some(output_x) = self.forced_scanline_sample_x(bg_idx, x) else {
buf[x] = TRANSPARENT;
continue;
};
let sample_x = if mosaic_enabled {
mosaic_anchor(output_x as u32, mosaic_h as u32) as usize
} else {
output_x
};
let screen_x = (sample_x + hofs as usize) & width_mask;
let tile_x = screen_x >> 3;
let tile_y = screen_y >> 3;
let screenblock_x = tile_x >> 5;
let screenblock_y = tile_y >> 5;
let screenblock = screenblock_y * (width_tiles >> 5) + screenblock_x;
let local_tile_x = tile_x & 31;
let local_tile_y = tile_y & 31;
let map_off =
screenblock_base + screenblock * 0x800 + (local_tile_y * 32 + local_tile_x) * 2;
let entry = if map_off + 1 < vram.len() {
u16::from_le_bytes([vram[map_off], vram[map_off + 1]])
} else {
0
};
let tile_id = (entry & 0x03FF) as usize;
let hflip = (entry & (1 << 10)) != 0;
let vflip = (entry & (1 << 11)) != 0;
let pixel_x = if hflip {
7 - (screen_x & 7)
} else {
screen_x & 7
};
let pixel_y = if vflip {
7 - (screen_y & 7)
} else {
screen_y & 7
};
let palette_index = if is_8bpp {
let tile_addr = charblock_base + tile_id * 64 + pixel_y * 8 + pixel_x;
vram.get(tile_addr).copied().unwrap_or(0) as usize
} else {
let tile_addr = charblock_base + tile_id * 32 + pixel_y * 4 + (pixel_x >> 1);
vram.get(tile_addr)
.map(|byte| {
if pixel_x & 1 == 0 {
byte & 0x0F
} else {
byte >> 4
}
})
.unwrap_or(0) as usize
};
if palette_index == 0 {
buf[x] = TRANSPARENT;
continue;
}
let pram_index = if is_8bpp {
palette_index * 2
} else {
let palette_bank = ((entry >> 12) & 0x000F) as usize;
(palette_bank * 16 + palette_index) * 2
};
buf[x] = if pram_index + 1 < pram.len() {
u16::from_le_bytes([pram[pram_index], pram[pram_index + 1]]) & 0x7FFF
} else {
TRANSPARENT
};
}
}
#[allow(clippy::too_many_arguments, clippy::needless_range_loop)]
fn render_affine_bg_layer(
&self,
bg_idx: usize,
affine_idx: usize,
vram: &[u8],
pram: &[u8],
buf: &mut [u16; SCREEN_WIDTH as usize],
) {
let bgcnt = self.bg_cnt[bg_idx];
let aff = self.bg_affine[affine_idx];
let mosaic_enabled = bgcnt & (1 << 6) != 0;
let (mosaic_h, mosaic_v) = self.bg_mosaic_size();
let size_shift = ((bgcnt >> 14) & 3) as u32;
let tiles_wide = 16u32 << size_shift;
let map_pixels = tiles_wide * 8;
let wrapping = (bgcnt & (1 << 13)) != 0;
let screenblock_base = (((bgcnt >> 8) & 0x001F) as usize) * 0x800;
let charblock_base = (((bgcnt >> 2) & 0x0003) as usize) * 16 * 1024;
let pa = aff.pa as i32;
let pb = aff.pb as i32;
let pc = aff.pc as i32;
let pd = aff.pd as i32;
let mosaic_y_offset = if mosaic_enabled {
let y = self.vcount as u32;
(y - mosaic_anchor(y, mosaic_v as u32)) as usize
} else {
0
};
let line_anchor_x = aff
.internal_x
.wrapping_sub(pb.wrapping_mul(mosaic_y_offset as i32));
let line_anchor_y = aff
.internal_y
.wrapping_sub(pd.wrapping_mul(mosaic_y_offset as i32));
for x in 0..(SCREEN_WIDTH as usize) {
let Some(output_x) = self.forced_scanline_sample_x(bg_idx, x) else {
buf[x] = TRANSPARENT;
continue;
};
let sample_x = if mosaic_enabled {
mosaic_anchor(output_x as u32, mosaic_h as u32) as usize
} else {
output_x
};
let px = line_anchor_x.wrapping_add(pa.wrapping_mul(sample_x as i32)) >> 8;
let py = line_anchor_y.wrapping_add(pc.wrapping_mul(sample_x as i32)) >> 8;
let (fx, fy) = if wrapping {
(
(px as u32) & (map_pixels - 1),
(py as u32) & (map_pixels - 1),
)
} else {
if px < 0 || py < 0 || px >= map_pixels as i32 || py >= map_pixels as i32 {
buf[x] = TRANSPARENT;
continue;
}
(px as u32, py as u32)
};
let tile_x = (fx >> 3) as usize;
let tile_y = (fy >> 3) as usize;
let pixel_x = (fx & 7) as usize;
let pixel_y = (fy & 7) as usize;
let map_off = screenblock_base + tile_y * (tiles_wide as usize) + tile_x;
let tile_id = *vram.get(map_off).unwrap_or(&0) as usize;
let tile_addr = charblock_base + tile_id * 64 + pixel_y * 8 + pixel_x;
let palette_index = *vram.get(tile_addr).unwrap_or(&0) as usize;
if palette_index == 0 {
buf[x] = TRANSPARENT;
continue;
}
let pram_index = palette_index * 2;
buf[x] = if pram_index + 1 < pram.len() {
u16::from_le_bytes([pram[pram_index], pram[pram_index + 1]]) & 0x7FFF
} else {
TRANSPARENT
};
}
}
fn render_mode2_scanline(&mut self, y: u32, vram: &[u8], pram: &[u8], oam: &[u8]) {
let bg_enables = [self.bg_layer_enabled(2), self.bg_layer_enabled(3)];
if !bg_enables.iter().any(|&e| e) && self.dispcnt & dispcnt::OBJ_ENABLE == 0 {
self.render_no_layers_scanline(y, vram, pram, oam);
return;
}
let mut layers: Vec<(usize, u8, [u16; SCREEN_WIDTH as usize])> = Vec::new();
let bg_indices = [2usize, 3usize];
let affine_indices = [0usize, 1usize];
for (i, &bg_idx) in bg_indices.iter().enumerate() {
if bg_enables[i] {
let mut buf = [TRANSPARENT; SCREEN_WIDTH as usize];
self.render_affine_bg_layer(bg_idx, affine_indices[i], vram, pram, &mut buf);
let prio = (self.bg_cnt[bg_idx] & 3) as u8;
layers.push((bg_idx, prio, buf));
}
}
self.composite_scanline(y, pram, vram, oam, &layers);
}
fn render_mode1_scanline(&mut self, y: u32, vram: &[u8], pram: &[u8], oam: &[u8]) {
let bg_enables = [
self.bg_layer_enabled(0),
self.bg_layer_enabled(1),
self.bg_layer_enabled(2),
];
if !bg_enables.iter().any(|&e| e) && self.dispcnt & dispcnt::OBJ_ENABLE == 0 {
self.render_no_layers_scanline(y, vram, pram, oam);
return;
}
let mut layers: Vec<(usize, u8, [u16; SCREEN_WIDTH as usize])> = Vec::new();
for (i, &bg_idx) in [0usize, 1, 2].iter().enumerate() {
if bg_enables[i] {
let mut buf = [TRANSPARENT; SCREEN_WIDTH as usize];
if bg_idx == 2 {
self.render_affine_bg_layer(bg_idx, 0, vram, pram, &mut buf);
} else {
self.render_text_bg_layer(bg_idx, y, vram, pram, &mut buf);
}
let prio = (self.bg_cnt[bg_idx] & 3) as u8;
layers.push((bg_idx, prio, buf));
}
}
self.composite_scanline(y, pram, vram, oam, &layers);
}
fn render_affine_bitmap_layer(
&self,
y: u32,
vram: &[u8],
width: usize,
height: usize,
frame_base: usize,
) -> [u16; SCREEN_WIDTH as usize] {
let mut buf = [TRANSPARENT; SCREEN_WIDTH as usize];
let aff = self.bg_affine[0];
let pa = aff.pa as i32;
let pb = aff.pb as i32;
let pc = aff.pc as i32;
let pd = aff.pd as i32;
let mosaic_enabled = self.bg_cnt[2] & (1 << 6) != 0;
let (mosaic_h, mosaic_v) = self.bg_mosaic_size();
let mosaic_y_offset = if mosaic_enabled {
(y - mosaic_anchor(y, mosaic_v as u32)) as usize
} else {
0
};
let line_anchor_x = aff
.internal_x
.wrapping_sub(pb.wrapping_mul(mosaic_y_offset as i32));
let line_anchor_y = aff
.internal_y
.wrapping_sub(pd.wrapping_mul(mosaic_y_offset as i32));
for (x, out_pixel) in buf.iter_mut().enumerate() {
let sample_screen_x = if mosaic_enabled {
mosaic_anchor(x as u32, mosaic_h as u32) as usize
} else {
x
};
let sample_x = line_anchor_x.wrapping_add(pa.wrapping_mul(sample_screen_x as i32));
let sample_y = line_anchor_y.wrapping_add(pc.wrapping_mul(sample_screen_x as i32));
if sample_x < 0 || sample_y < 0 {
continue;
}
let px = (sample_x >> 8) as usize;
let py = (sample_y >> 8) as usize;
if px >= width || py >= height {
continue;
}
let src = frame_base + (py * width + px) * 2;
*out_pixel = u16::from_le_bytes([vram[src], vram[src + 1]]) & 0x7FFF;
}
buf
}
fn render_affine_paletted_bitmap_layer(
&self,
y: u32,
vram: &[u8],
pram: &[u8],
width: usize,
height: usize,
frame_base: usize,
) -> [u16; SCREEN_WIDTH as usize] {
let mut buf = [TRANSPARENT; SCREEN_WIDTH as usize];
let aff = self.bg_affine[0];
let pa = aff.pa as i32;
let pb = aff.pb as i32;
let pc = aff.pc as i32;
let pd = aff.pd as i32;
let mosaic_enabled = self.bg_cnt[2] & (1 << 6) != 0;
let (mosaic_h, mosaic_v) = self.bg_mosaic_size();
let mosaic_y_offset = if mosaic_enabled {
(y - mosaic_anchor(y, mosaic_v as u32)) as usize
} else {
0
};
let line_anchor_x = aff
.internal_x
.wrapping_sub(pb.wrapping_mul(mosaic_y_offset as i32));
let line_anchor_y = aff
.internal_y
.wrapping_sub(pd.wrapping_mul(mosaic_y_offset as i32));
for (x, out_pixel) in buf.iter_mut().enumerate() {
let sample_screen_x = if mosaic_enabled {
mosaic_anchor(x as u32, mosaic_h as u32) as usize
} else {
x
};
let sample_x = line_anchor_x.wrapping_add(pa.wrapping_mul(sample_screen_x as i32));
let sample_y = line_anchor_y.wrapping_add(pc.wrapping_mul(sample_screen_x as i32));
if sample_x < 0 || sample_y < 0 {
continue;
}
let px = (sample_x >> 8) as usize;
let py = (sample_y >> 8) as usize;
if px >= width || py >= height {
continue;
}
let src = frame_base + py * width + px;
if src >= vram.len() {
continue;
}
let pal_index = vram[src];
*out_pixel = if pal_index == 0 {
TRANSPARENT
} else {
let pal_offset = (pal_index as usize) * 2;
if pal_offset + 1 < pram.len() {
u16::from_le_bytes([pram[pal_offset], pram[pal_offset + 1]]) & 0x7FFF
} else {
TRANSPARENT
}
};
}
buf
}
fn render_mode3_scanline(&mut self, y: u32, vram: &[u8], pram: &[u8], oam: &[u8]) {
if !self.bg_layer_enabled(2) && self.dispcnt & dispcnt::OBJ_ENABLE == 0 {
self.render_no_layers_scanline(y, vram, pram, oam);
return;
}
let mut layers: Vec<(usize, u8, [u16; SCREEN_WIDTH as usize])> = Vec::new();
if self.bg_layer_enabled(2) {
let buf = self.render_affine_bitmap_layer(
y,
vram,
SCREEN_WIDTH as usize,
SCREEN_HEIGHT as usize,
MODE3_FRAME_BASE,
);
let prio = (self.bg_cnt[2] & 3) as u8;
layers.push((2, prio, buf));
}
self.composite_scanline(y, pram, vram, oam, &layers);
}
fn render_mode4_scanline(&mut self, y: u32, vram: &[u8], pram: &[u8], oam: &[u8]) {
if !self.bg_layer_enabled(2) && self.dispcnt & dispcnt::OBJ_ENABLE == 0 {
self.render_no_layers_scanline(y, vram, pram, oam);
return;
}
let mut layers: Vec<(usize, u8, [u16; SCREEN_WIDTH as usize])> = Vec::new();
if self.bg_layer_enabled(2) {
let frame_base = if self.frame_select() {
0xA000usize
} else {
0x0000usize
};
let buf = self.render_affine_paletted_bitmap_layer(
y,
vram,
pram,
SCREEN_WIDTH as usize,
SCREEN_HEIGHT as usize,
frame_base,
);
let prio = (self.bg_cnt[2] & 3) as u8;
layers.push((2, prio, buf));
}
self.composite_scanline(y, pram, vram, oam, &layers);
}
fn render_mode5_scanline(&mut self, y: u32, vram: &[u8], pram: &[u8], oam: &[u8]) {
if !self.bg_layer_enabled(2) && self.dispcnt & dispcnt::OBJ_ENABLE == 0 {
self.render_no_layers_scanline(y, vram, pram, oam);
return;
}
let mut layers: Vec<(usize, u8, [u16; SCREEN_WIDTH as usize])> = Vec::new();
if self.bg_layer_enabled(2) {
let frame_base = if self.frame_select() {
0xA000usize
} else {
0x0000usize
};
let buf =
self.render_affine_bitmap_layer(y, vram, MODE5_WIDTH, MODE5_HEIGHT, frame_base);
let prio = (self.bg_cnt[2] & 3) as u8;
layers.push((2, prio, buf));
}
self.composite_scanline(y, pram, vram, oam, &layers);
}
#[allow(clippy::too_many_arguments, clippy::needless_range_loop)]
fn composite_scanline(
&mut self,
y: u32,
pram: &[u8],
vram: &[u8],
oam: &[u8],
bg_layers: &[(usize, u8, [u16; SCREEN_WIDTH as usize])],
) {
let row_start = (y as usize) * (SCREEN_WIDTH as usize) * BYTES_PER_PIXEL;
let backdrop = self.backdrop_bgr555(pram);
let obj_enabled = self.dispcnt & dispcnt::OBJ_ENABLE != 0;
let obj_scanline = if obj_enabled {
let mapping_1d = self.dispcnt & dispcnt::OBJ_MAPPING_1D != 0;
let bitmap_mode = self.mode() >= 3;
let cycle_budget = if self.dispcnt & dispcnt::HBLANK_INTERVAL_FREE != 0 {
obj::OBJ_CYCLE_BUDGET_HBLANK_FREE
} else {
obj::OBJ_CYCLE_BUDGET_NORMAL
};
Some(obj::render_obj_scanline(
y,
oam,
vram,
pram,
mapping_1d,
bitmap_mode,
self.mosaic,
cycle_budget,
))
} else {
None
};
let any_window_active = self.dispcnt
& (dispcnt::WIN0_ENABLE | dispcnt::WIN1_ENABLE | dispcnt::OBJ_WIN_ENABLE)
!= 0;
let bld_mode = (self.bldcnt >> 6) & 3;
let first_target_mask = (self.bldcnt & 0x3F) as u8;
let second_target_mask = ((self.bldcnt >> 8) & 0x3F) as u8;
let eva = ((self.bldalpha & 0x1F) as u8).min(16);
let evb = (((self.bldalpha >> 8) & 0x1F) as u8).min(16);
let evy = self.bldy.min(16);
let write_pixel: fn(&mut [u8], usize, u16) = if self.color_correction {
color::write_pixel_corrected
} else {
color::write_pixel
};
for x in 0..(SCREEN_WIDTH as usize) {
let layer_mask = if any_window_active {
self.window_layer_mask(x as u32, y, obj_scanline.as_ref())
} else {
0x3F
};
let sfx_enabled = (layer_mask >> 5) & 1 != 0;
let obj_px = obj_scanline.as_ref().map(|s| &s.pixels[x]);
let obj_opaque = obj_px.is_some_and(|px| px.opaque);
let obj_visible = obj_opaque && ((layer_mask & (1 << 4)) != 0);
let obj_is_semi_transparent =
obj_visible && matches!(obj_px, Some(px) if px.semi_transparent);
let suppress_brightness = obj_is_semi_transparent && second_target_mask != 0;
let mut top_sort: u16 = 0xFFFF; let mut top_layer: u8 = 5; let mut top_color: u16 = backdrop;
let mut top_semi_transparent = false;
let mut sec_sort: u16 = 0xFFFF; let mut sec_layer: u8 = 5;
let mut sec_color: u16 = backdrop;
if obj_visible {
let px = obj_px.unwrap();
let sort_key = (px.priority as u16) * SORT_KEY_PRIORITY_SPACING;
sec_sort = top_sort;
sec_layer = top_layer;
sec_color = top_color;
top_sort = sort_key;
top_layer = 4;
top_color = px.color;
top_semi_transparent = px.semi_transparent;
}
for &(bg_idx, bg_prio, ref buf) in bg_layers {
if layer_mask & (1 << bg_idx) == 0 {
continue; }
if buf[x] == TRANSPARENT {
continue;
}
let sort_key = (bg_prio as u16) * SORT_KEY_PRIORITY_SPACING
+ SORT_KEY_BG_OFFSET
+ (bg_idx as u16);
if sort_key < top_sort {
sec_sort = top_sort;
sec_layer = top_layer;
sec_color = top_color;
top_sort = sort_key;
top_layer = bg_idx as u8;
top_color = buf[x];
top_semi_transparent = false;
} else if sort_key < sec_sort {
sec_sort = sort_key;
sec_layer = bg_idx as u8;
sec_color = buf[x];
}
}
let final_color = if top_semi_transparent && sfx_enabled {
if (second_target_mask >> sec_layer) & 1 != 0 {
alpha_blend_bgr555(top_color, sec_color, eva, evb)
} else {
top_color
}
} else if sfx_enabled {
match bld_mode {
1 => {
if (first_target_mask >> top_layer) & 1 != 0
&& (second_target_mask >> sec_layer) & 1 != 0
{
alpha_blend_bgr555(top_color, sec_color, eva, evb)
} else {
top_color
}
}
2 => {
if !suppress_brightness && (first_target_mask >> top_layer) & 1 != 0 {
brighten_bgr555(top_color, evy)
} else {
top_color
}
}
3 => {
if !suppress_brightness && (first_target_mask >> top_layer) & 1 != 0 {
darken_bgr555(top_color, evy)
} else {
top_color
}
}
_ => top_color, }
} else {
top_color
};
let dst = row_start + x * BYTES_PER_PIXEL;
write_pixel(&mut self.framebuffer, dst, final_color);
}
}
fn window_layer_mask(&self, x: u32, y: u32, obj_scanline: Option<&obj::ObjScanline>) -> u8 {
if self.dispcnt & dispcnt::WIN0_ENABLE != 0 && self.pixel_in_window(0, x, y) {
return (self.winin & 0x3F) as u8;
}
if self.dispcnt & dispcnt::WIN1_ENABLE != 0 && self.pixel_in_window(1, x, y) {
return ((self.winin >> 8) & 0x3F) as u8;
}
if self.dispcnt & dispcnt::OBJ_WIN_ENABLE != 0
&& obj_scanline.is_some_and(|s| s.obj_window[x as usize])
{
return ((self.winout >> 8) & 0x3F) as u8;
}
(self.winout & 0x3F) as u8
}
fn pixel_in_window(&self, n: usize, x: u32, y: u32) -> bool {
let h = self.win_h[n];
let v = self.win_v[n];
let x1 = (h >> 8) as u32;
let x2 = (h & 0xFF) as u32;
let y1 = (v >> 8) as u32;
let y2 = (v & 0xFF) as u32;
let in_x = if x2 > SCREEN_WIDTH {
x >= x1 && x < SCREEN_WIDTH
} else if x1 > x2 {
x >= x1 || x < x2
} else {
x >= x1 && x < x2
};
let in_y = if y1 >= SCANLINES_PER_FRAME {
false
} else if y2 >= SCANLINES_PER_FRAME {
true
} else if y2 > SCREEN_HEIGHT {
y >= y1 && y < SCREEN_HEIGHT
} else if y1 > y2 {
y >= y1 || y < y2
} else {
y >= y1 && y < y2
};
in_x && in_y
}
fn render_prohibited_mode_scanline(&mut self, y: u32, vram: &[u8], pram: &[u8], oam: &[u8]) {
if self.dispcnt & dispcnt::OBJ_ENABLE == 0 {
self.render_no_layers_scanline(y, vram, pram, oam);
return;
}
self.composite_scanline(y, pram, vram, oam, &[]);
}
fn render_no_layers_scanline(&mut self, y: u32, vram: &[u8], pram: &[u8], oam: &[u8]) {
let any_window_active = self.dispcnt
& (dispcnt::WIN0_ENABLE | dispcnt::WIN1_ENABLE | dispcnt::OBJ_WIN_ENABLE)
!= 0;
if any_window_active {
self.composite_scanline(y, pram, vram, oam, &[]);
} else {
self.render_backdrop_scanline(y, pram);
}
}
fn render_backdrop_scanline(&mut self, y: u32, pram: &[u8]) {
let backdrop = self.backdrop_bgr555(pram);
let any_window_active = self.dispcnt
& (dispcnt::WIN0_ENABLE | dispcnt::WIN1_ENABLE | dispcnt::OBJ_WIN_ENABLE)
!= 0;
let bld_mode = (self.bldcnt >> 6) & 3;
let backdrop_is_first_target = (self.bldcnt & (1 << 5)) != 0;
let evy = self.bldy.min(16);
let final_backdrop = if !any_window_active && backdrop_is_first_target {
match bld_mode {
2 => brighten_bgr555(backdrop, evy),
3 => darken_bgr555(backdrop, evy),
_ => backdrop,
}
} else {
backdrop
};
let (r, g, b) = if self.color_correction {
color::bgr555_to_rgb888_corrected(final_backdrop)
} else {
color::bgr555_to_rgb888(final_backdrop)
};
let row_start = (y as usize) * (SCREEN_WIDTH as usize) * BYTES_PER_PIXEL;
for x in 0..(SCREEN_WIDTH as usize) {
let dst = row_start + x * BYTES_PER_PIXEL;
self.framebuffer[dst] = r;
self.framebuffer[dst + 1] = g;
self.framebuffer[dst + 2] = b;
}
}
fn backdrop_bgr555(&self, pram: &[u8]) -> u16 {
if pram.len() >= 2 {
u16::from_le_bytes([pram[0], pram[1]])
} else {
0
}
}
fn bg_mosaic_size(&self) -> (usize, usize) {
(
mosaic_size(self.mosaic, 0) as usize,
mosaic_size(self.mosaic, 4) as usize,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_ic() -> InterruptController {
let mut ic = InterruptController::new();
ic.write_ie(0xFFFF);
ic.write_ime(1);
ic
}
fn make_vram() -> Vec<u8> {
vec![0; 96 * 1024]
}
fn make_pram() -> Vec<u8> {
vec![0; 1024]
}
fn make_oam() -> Vec<u8> {
let mut oam = vec![0u8; 1024];
for i in 0..128 {
let offset = i * 8;
let attr0 = 0x0200u16;
oam[offset] = attr0 as u8;
oam[offset + 1] = (attr0 >> 8) as u8;
}
oam
}
fn step_and_render_scanline0(
ppu: &mut Ppu,
ic: &mut InterruptController,
vram: &[u8],
pram: &[u8],
oam: &[u8],
) {
ppu.step(
CYCLES_PER_SCANLINE * SCANLINES_PER_FRAME,
ic,
vram,
pram,
oam,
);
ppu.step(CYCLES_PER_SCANLINE, ic, vram, pram, oam);
}
#[test]
fn save_state_restores_registers_and_affine_state() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
ppu.write_dispcnt(3 | dispcnt::BG2_ENABLE);
ppu.write_green_swap(1);
ppu.write_dispstat(dispstat::VBLANK_IRQ_ENABLE | (42 << 8), &mut ic);
ppu.write_bg_cnt(0, 0xDFFF);
ppu.write_bg_cnt(2, 0xE0C3);
ppu.write_bg_hofs(0, 0x0123);
ppu.write_bg_vofs(0, 0x01F0);
ppu.write_win_h(0, 0x1234);
ppu.write_win_v(0, 0x5678);
ppu.write_winin(0x3F3F);
ppu.write_winout(0x2A2A);
ppu.write_mosaic(0x4321);
ppu.write_bldcnt(0x3F1F);
ppu.write_bldalpha(0x100F);
ppu.write_bldy(0x0011);
ppu.write_affine(REG_BG2PA, 0x0200);
ppu.write_affine(REG_BG2PB, 0x0010);
ppu.write_affine(REG_BG2PC, 0xFFF0);
ppu.write_affine(REG_BG2PD, 0x0180);
ppu.write_affine(REG_BG2X_L, 0x3456);
ppu.write_affine(REG_BG2X_H, 0x0001);
ppu.write_affine(REG_BG2Y_L, 0x789A);
ppu.write_affine(REG_BG2Y_H, 0x0800);
ppu.set_color_correction(true);
let saved = ppu.capture_state();
ppu.write_dispcnt(0);
ppu.write_green_swap(0);
ppu.write_dispstat(0, &mut ic);
ppu.write_bg_cnt(0, 0);
ppu.write_bg_cnt(2, 0);
ppu.write_bg_hofs(0, 0);
ppu.write_bg_vofs(0, 0);
ppu.write_win_h(0, 0);
ppu.write_win_v(0, 0);
ppu.write_winin(0);
ppu.write_winout(0);
ppu.write_mosaic(0);
ppu.write_bldcnt(0);
ppu.write_bldalpha(0);
ppu.write_bldy(0);
ppu.write_affine(REG_BG2PA, 0);
ppu.write_affine(REG_BG2PB, 0);
ppu.write_affine(REG_BG2PC, 0);
ppu.write_affine(REG_BG2PD, 0);
ppu.write_affine(REG_BG2X_L, 0);
ppu.write_affine(REG_BG2X_H, 0);
ppu.write_affine(REG_BG2Y_L, 0);
ppu.write_affine(REG_BG2Y_H, 0);
ppu.set_color_correction(false);
ppu.restore_state(&saved);
assert_eq!(ppu.read_dispcnt(), 3 | dispcnt::BG2_ENABLE);
assert_eq!(ppu.read_green_swap(), 1);
assert_eq!(
ppu.read_dispstat() & dispstat::WRITE_MASK,
dispstat::VBLANK_IRQ_ENABLE | (42 << 8)
);
assert_eq!(ppu.read_bg_cnt(0), 0xDFFF & !0x2000);
assert_eq!(ppu.read_bg_cnt(2), 0xE0C3);
assert_eq!(ppu.read_bg_hofs(0), 0x0123);
assert_eq!(ppu.read_bg_vofs(0), 0x01F0);
assert_eq!(ppu.read_win_h(0), 0x1234);
assert_eq!(ppu.read_win_v(0), 0x5678);
assert_eq!(ppu.read_winin(), 0x3F3F);
assert_eq!(ppu.read_winout(), 0x2A2A);
assert_eq!(ppu.read_mosaic(), 0x4321);
assert_eq!(ppu.read_bldcnt(), 0x3F1F);
assert_eq!(ppu.read_bldalpha(), 0x100F);
assert_eq!(ppu.read_bldy(), 0x0011);
assert_eq!(ppu.read_affine(REG_BG2PA), Some(0x0200));
assert_eq!(ppu.read_affine(REG_BG2PB), Some(0x0010));
assert_eq!(ppu.read_affine(REG_BG2PC), Some(0xFFF0));
assert_eq!(ppu.read_affine(REG_BG2PD), Some(0x0180));
assert_eq!(ppu.read_affine(REG_BG2X_L), Some(0x3456));
assert_eq!(ppu.read_affine(REG_BG2X_H), Some(0x0001));
assert_eq!(ppu.read_affine(REG_BG2Y_L), Some(0x789A));
assert_eq!(ppu.read_affine(REG_BG2Y_H), Some(0xF800));
assert!(ppu.color_correction);
}
#[test]
fn save_state_restores_framebuffer_ready_and_timing_state() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let mut vram = make_vram();
let pram = make_pram();
let oam = make_oam();
ppu.write_dispcnt(3 | dispcnt::BG2_ENABLE);
for x in 0..(SCREEN_WIDTH as usize) {
let off = x * 2;
vram[off..off + 2].copy_from_slice(&0x001Fu16.to_le_bytes());
}
ppu.step(
CYCLES_PER_SCANLINE * VISIBLE_SCANLINES + 17,
&mut ic,
&vram,
&pram,
&oam,
);
assert!(ppu.frame_ready());
assert_eq!(ppu.read_vcount(), VISIBLE_SCANLINES as u16);
assert_eq!(ppu.line_cycle, 17);
let saved = ppu.capture_state();
let saved_pixel = ppu.framebuffer()[0..3].to_vec();
ppu.clear_frame_ready();
ppu.step(CYCLES_PER_SCANLINE * 3, &mut ic, &vram, &pram, &oam);
ppu.framebuffer[0..3].copy_from_slice(&[0, 0, 0]);
ppu.restore_state(&saved);
assert!(ppu.frame_ready());
assert_eq!(ppu.read_vcount(), VISIBLE_SCANLINES as u16);
assert_eq!(ppu.line_cycle, 17);
assert_eq!(&ppu.framebuffer()[0..3], saved_pixel.as_slice());
}
#[test]
fn save_state_roundtrips_through_json() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
ppu.write_dispcnt(5 | dispcnt::BG2_ENABLE | dispcnt::FRAME_SELECT);
ppu.write_dispstat(dispstat::HBLANK_IRQ_ENABLE | (7 << 8), &mut ic);
ppu.step(123, &mut ic, &make_vram(), &make_pram(), &make_oam());
let saved = ppu.capture_state();
let bytes = serde_json::to_vec(&saved).expect("serialize PPU state");
let decoded: PpuState = serde_json::from_slice(&bytes).expect("deserialize PPU state");
let mut restored = Ppu::new();
restored.restore_state(&decoded);
assert_eq!(restored.read_dispcnt(), ppu.read_dispcnt());
assert_eq!(restored.read_dispstat(), ppu.read_dispstat());
assert_eq!(restored.line_cycle, ppu.line_cycle);
}
#[test]
fn new_ppu_has_zeroed_state_and_blank_framebuffer() {
let ppu = Ppu::new();
assert_eq!(ppu.read_dispcnt(), 0);
assert_eq!(ppu.read_dispstat(), dispstat::VCOUNT_FLAG);
assert_eq!(ppu.read_vcount(), 0);
assert!(!ppu.frame_ready());
assert_eq!(ppu.framebuffer().len(), FRAMEBUFFER_BYTES);
assert!(ppu.framebuffer().iter().all(|&b| b == 0));
}
#[test]
fn dispstat_status_bits_are_read_only() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
ppu.write_dispstat(0x0007 | 0x0038 | 0xAB00, &mut ic);
assert_eq!(ppu.read_dispstat() & dispstat::HBLANK_FLAG, 0);
assert_eq!(ppu.read_dispstat() & dispstat::VBLANK_FLAG, 0);
assert_eq!(
ppu.read_dispstat() & !dispstat::STATUS_MASK,
0x0038 | 0xAB00
);
}
#[test]
fn step_advances_line_cycle_within_scanline() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
ppu.step(500, &mut ic, &make_vram(), &make_pram(), &make_oam());
assert_eq!(ppu.read_vcount(), 0);
assert_eq!(ppu.read_dispstat() & dispstat::HBLANK_FLAG, 0);
}
#[test]
fn hblank_flag_sets_at_cycle_1006() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
ppu.step(
HBLANK_START_CYCLE - 1,
&mut ic,
&make_vram(),
&make_pram(),
&make_oam(),
);
assert_eq!(ppu.read_dispstat() & dispstat::HBLANK_FLAG, 0);
ppu.step(1, &mut ic, &make_vram(), &make_pram(), &make_oam());
assert_ne!(ppu.read_dispstat() & dispstat::HBLANK_FLAG, 0);
}
#[test]
fn hblank_flag_clears_when_scanline_advances() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
ppu.step(
CYCLES_PER_SCANLINE,
&mut ic,
&make_vram(),
&make_pram(),
&make_oam(),
);
assert_eq!(ppu.read_vcount(), 1);
assert_eq!(ppu.read_dispstat() & dispstat::HBLANK_FLAG, 0);
}
#[test]
fn hblank_irq_fires_only_when_enabled() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
ppu.step(
HBLANK_START_CYCLE,
&mut ic,
&make_vram(),
&make_pram(),
&make_oam(),
);
assert_eq!(ic.if_flags & irq_bits::HBLANK, 0);
ppu.write_dispstat(dispstat::HBLANK_IRQ_ENABLE, &mut ic);
ppu.step(
CYCLES_PER_SCANLINE,
&mut ic,
&make_vram(),
&make_pram(),
&make_oam(),
);
assert_ne!(ic.if_flags & irq_bits::HBLANK, 0);
}
#[test]
fn hblank_irq_fires_during_vblank() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let vram = make_vram();
let pram = make_pram();
let oam = make_oam();
ppu.write_dispstat(dispstat::HBLANK_IRQ_ENABLE, &mut ic);
ppu.step(
CYCLES_PER_SCANLINE * VISIBLE_SCANLINES,
&mut ic,
&vram,
&pram,
&oam,
);
assert_eq!(ppu.read_vcount(), 160);
ic.if_flags = 0;
ppu.step(HBLANK_START_CYCLE, &mut ic, &vram, &pram, &oam);
assert_ne!(
ic.if_flags & irq_bits::HBLANK,
0,
"H-Blank IRQ should fire during V-Blank (scanline 160)"
);
}
#[test]
fn hblank_dma_skips_vblank_scanlines() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let vram = make_vram();
let pram = make_pram();
let cycles = CYCLES_PER_SCANLINE * VISIBLE_SCANLINES;
ppu.step(cycles, &mut ic, &vram, &pram, &make_oam());
assert_eq!(ppu.read_vcount(), 160);
let events = ppu.step(HBLANK_START_CYCLE, &mut ic, &vram, &pram, &make_oam());
assert_eq!(
events.hblank_starts, 0,
"H-Blank DMA should not fire on VBlank scanlines"
);
}
#[test]
fn step_full_frame_counts_every_hblank() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let vram = make_vram();
let pram = make_pram();
let events = ppu.step(
CYCLES_PER_SCANLINE * SCANLINES_PER_FRAME,
&mut ic,
&vram,
&pram,
&make_oam(),
);
assert_eq!(events.hblank_starts, VISIBLE_SCANLINES);
assert_eq!(events.vblank_starts, 1);
assert_eq!(events.frames_completed, 1);
}
#[test]
fn step_two_frames_counts_two_vblanks() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let vram = make_vram();
let pram = make_pram();
let events = ppu.step(
CYCLES_PER_SCANLINE * SCANLINES_PER_FRAME * 2,
&mut ic,
&vram,
&pram,
&make_oam(),
);
assert_eq!(events.vblank_starts, 2);
assert_eq!(events.frames_completed, 2);
assert_eq!(events.hblank_starts, VISIBLE_SCANLINES * 2);
}
#[test]
fn vblank_starts_at_scanline_160() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let vram = make_vram();
let pram = make_pram();
let cycles = CYCLES_PER_SCANLINE * VISIBLE_SCANLINES;
let events = ppu.step(cycles, &mut ic, &vram, &pram, &make_oam());
assert_eq!(ppu.read_vcount(), 160);
assert_ne!(ppu.read_dispstat() & dispstat::VBLANK_FLAG, 0);
assert_eq!(events.vblank_starts, 1);
assert_eq!(events.frames_completed, 1);
assert!(ppu.frame_ready());
}
#[test]
fn vblank_irq_fires_when_enabled() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
ppu.write_dispstat(dispstat::VBLANK_IRQ_ENABLE, &mut ic);
let cycles = CYCLES_PER_SCANLINE * VISIBLE_SCANLINES;
ppu.step(cycles, &mut ic, &make_vram(), &make_pram(), &make_oam());
assert_ne!(ic.if_flags & irq_bits::VBLANK, 0);
}
#[test]
fn vblank_flag_clears_on_scanline_227() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let vram = make_vram();
let pram = make_pram();
let cycles = CYCLES_PER_SCANLINE * (VBLANK_LAST_SCANLINE + 1);
ppu.step(cycles, &mut ic, &vram, &pram, &make_oam());
assert_eq!(ppu.read_vcount() as u32, VBLANK_LAST_SCANLINE + 1);
assert_eq!(ppu.read_dispstat() & dispstat::VBLANK_FLAG, 0);
}
#[test]
fn frame_completes_after_a_full_frame() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let vram = make_vram();
let pram = make_pram();
let total = CYCLES_PER_SCANLINE * SCANLINES_PER_FRAME;
ppu.step(total, &mut ic, &vram, &pram, &make_oam());
assert_eq!(ppu.read_vcount(), 0);
assert!(ppu.frame_ready());
ppu.clear_frame_ready();
assert!(!ppu.frame_ready());
}
#[test]
fn vcount_match_flag_set_at_reset_when_lyc_zero() {
let ppu = Ppu::new();
assert_ne!(ppu.read_dispstat() & dispstat::VCOUNT_FLAG, 0);
}
#[test]
fn vcount_match_flag_updates_on_dispstat_write() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
assert_ne!(ppu.read_dispstat() & dispstat::VCOUNT_FLAG, 0);
ppu.write_dispstat(5 << 8, &mut ic);
assert_eq!(ppu.read_dispstat() & dispstat::VCOUNT_FLAG, 0);
ppu.write_dispstat(0, &mut ic);
assert_ne!(ppu.read_dispstat() & dispstat::VCOUNT_FLAG, 0);
}
#[test]
fn vcount_match_irq_fires_when_lyc_written_to_match_current_vcount() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
ppu.step(
CYCLES_PER_SCANLINE * 7,
&mut ic,
&make_vram(),
&make_pram(),
&make_oam(),
);
assert_eq!(ppu.read_vcount(), 7);
ic.if_flags = 0;
ppu.write_dispstat(dispstat::VCOUNT_IRQ_ENABLE | (7 << 8), &mut ic);
assert_ne!(ppu.read_dispstat() & dispstat::VCOUNT_FLAG, 0);
assert_ne!(ic.if_flags & irq_bits::VCOUNT, 0);
}
#[test]
fn vcount_match_flag_and_irq() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
ppu.write_dispstat(dispstat::VCOUNT_IRQ_ENABLE | (5 << 8), &mut ic);
ppu.step(
CYCLES_PER_SCANLINE * 5,
&mut ic,
&make_vram(),
&make_pram(),
&make_oam(),
);
assert_eq!(ppu.read_vcount(), 5);
assert_ne!(ppu.read_dispstat() & dispstat::VCOUNT_FLAG, 0);
assert_ne!(ic.if_flags & irq_bits::VCOUNT, 0);
ppu.step(
CYCLES_PER_SCANLINE,
&mut ic,
&make_vram(),
&make_pram(),
&make_oam(),
);
assert_eq!(ppu.read_vcount(), 6);
assert_eq!(ppu.read_dispstat() & dispstat::VCOUNT_FLAG, 0);
}
#[test]
fn mode3_renders_bitmap_from_vram() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let mut vram = make_vram();
let pram = make_pram();
ppu.write_dispcnt(3 | dispcnt::BG2_ENABLE);
for x in 0..(SCREEN_WIDTH as usize) {
let off = x * 2;
vram[off] = 0x1F;
vram[off + 1] = 0x00;
}
ppu.step(
CYCLES_PER_SCANLINE * SCANLINES_PER_FRAME,
&mut ic,
&vram,
&pram,
&make_oam(),
);
let fb = ppu.framebuffer();
assert_eq!(fb[0], 0xFF, "R");
assert_eq!(fb[1], 0x00, "G");
assert_eq!(fb[2], 0x00, "B");
let last = ((SCREEN_WIDTH as usize) - 1) * BYTES_PER_PIXEL;
assert_eq!(&fb[last..last + 3], &[0xFF, 0, 0]);
}
#[test]
fn mode3_bg_mosaic_repeats_upper_left_anchor_pixel() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let mut vram = make_vram();
let pram = make_pram();
ppu.write_dispcnt(3 | dispcnt::BG2_ENABLE);
ppu.write_bg_cnt(2, 1 << 6);
ppu.write_mosaic(0x0011);
ppu.write_affine(REG_BG2PA, 0x0100);
ppu.write_affine(REG_BG2PD, 0x0100);
vram[0..2].copy_from_slice(&0x001Fu16.to_le_bytes());
vram[2..4].copy_from_slice(&0x03E0u16.to_le_bytes());
let row1 = SCREEN_WIDTH as usize * 2;
vram[row1..row1 + 2].copy_from_slice(&0x7C00u16.to_le_bytes());
vram[row1 + 2..row1 + 4].copy_from_slice(&0x7FFFu16.to_le_bytes());
let row2 = SCREEN_WIDTH as usize * 2 * 2;
vram[row2..row2 + 2].copy_from_slice(&0x03E0u16.to_le_bytes());
vram[row2 + 2..row2 + 4].copy_from_slice(&0x7C00u16.to_le_bytes());
ppu.step(CYCLES_PER_SCANLINE * 3, &mut ic, &vram, &pram, &make_oam());
let fb_row1 = SCREEN_WIDTH as usize * BYTES_PER_PIXEL;
assert_eq!(&ppu.framebuffer()[fb_row1..fb_row1 + 3], &[0xFF, 0, 0]);
assert_eq!(
&ppu.framebuffer()[fb_row1 + BYTES_PER_PIXEL..fb_row1 + BYTES_PER_PIXEL + 3],
&[0xFF, 0, 0]
);
let fb_row2 = fb_row1 * 2;
assert_eq!(&ppu.framebuffer()[fb_row2..fb_row2 + 3], &[0, 0xFF, 0]);
assert_eq!(
&ppu.framebuffer()[fb_row2 + BYTES_PER_PIXEL..fb_row2 + BYTES_PER_PIXEL + 3],
&[0, 0xFF, 0]
);
}
#[test]
fn mode3_with_bg2_disabled_renders_backdrop() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let mut vram = make_vram();
let mut pram = make_pram();
ppu.write_dispcnt(3);
pram[0] = 0x1F;
pram[1] = 0x00;
vram[0] = 0x00;
vram[1] = 0x7C;
ppu.step(
CYCLES_PER_SCANLINE * SCANLINES_PER_FRAME,
&mut ic,
&vram,
&pram,
&make_oam(),
);
assert_eq!(&ppu.framebuffer()[0..3], &[0xFF, 0, 0]);
}
#[test]
fn mode3_uses_bg2_affine_reference_point() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let mut vram = make_vram();
let pram = make_pram();
ppu.write_dispcnt(3 | dispcnt::BG2_ENABLE);
ppu.write_affine(REG_BG2PA, 0x0100);
ppu.write_affine(REG_BG2PD, 0x0100);
ppu.write_affine(REG_BG2X_L, 0x0100);
vram[0] = 0x1F;
vram[1] = 0x00;
vram[2] = 0xE0;
vram[3] = 0x03;
ppu.step(
CYCLES_PER_SCANLINE * SCANLINES_PER_FRAME,
&mut ic,
&vram,
&pram,
&make_oam(),
);
assert_eq!(&ppu.framebuffer()[0..3], &[0, 0xFF, 0]);
}
#[test]
fn mode3_outside_240x160_bitmap_is_transparent() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let mut vram = make_vram();
let mut pram = make_pram();
ppu.write_dispcnt(3 | dispcnt::BG2_ENABLE);
ppu.write_affine(REG_BG2PA, 0x0100);
ppu.write_affine(REG_BG2PD, 0x0100);
ppu.write_affine(REG_BG2X_L, 0xF000);
pram[0] = 0x00;
pram[1] = 0x7C;
for y in 0..160usize {
for x in 0..240usize {
let off = (y * 240 + x) * 2;
vram[off] = 0x1F;
vram[off + 1] = 0x00;
}
}
ppu.step(
CYCLES_PER_SCANLINE * SCANLINES_PER_FRAME,
&mut ic,
&vram,
&pram,
&make_oam(),
);
assert_eq!(&ppu.framebuffer()[0..3], &[0, 0, 0xFF]);
}
#[test]
fn mode3_bgcnt_wrap_bit_set_oob_is_still_transparent() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let mut vram = make_vram();
let mut pram = make_pram();
ppu.write_dispcnt(3 | dispcnt::BG2_ENABLE);
ppu.write_bg_cnt(2, 1 << 13);
ppu.write_affine(REG_BG2PA, 0x0100);
ppu.write_affine(REG_BG2PD, 0x0100);
ppu.write_affine(REG_BG2X_L, 0xF000);
pram[0] = 0x00;
pram[1] = 0x7C;
for y in 0..160usize {
for x in 0..240usize {
let off = (y * 240 + x) * 2;
vram[off] = 0x1F;
vram[off + 1] = 0x00;
}
}
ppu.step(
CYCLES_PER_SCANLINE * SCANLINES_PER_FRAME,
&mut ic,
&vram,
&pram,
&make_oam(),
);
assert_eq!(
&ppu.framebuffer()[0..3],
&[0, 0, 0xFF],
"Mode 3: BG2CNT wrap bit must be ignored; OOB pixels must show backdrop"
);
}
#[test]
fn forced_blank_outputs_white() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let vram = make_vram();
let pram = make_pram();
ppu.write_dispcnt(dispcnt::FORCED_BLANK);
ppu.step(
CYCLES_PER_SCANLINE * SCANLINES_PER_FRAME,
&mut ic,
&vram,
&pram,
&make_oam(),
);
assert!(ppu.framebuffer().iter().all(|&b| b == 0xFF));
}
#[test]
fn forced_blank_midframe_deassert_resets_vcount_after_two_scanlines() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let vram = make_vram();
let pram = make_pram();
let oam = make_oam();
ppu.write_dispcnt(dispcnt::FORCED_BLANK);
ppu.step(CYCLES_PER_SCANLINE * 80, &mut ic, &vram, &pram, &oam);
assert_eq!(ppu.read_vcount(), 80);
ppu.write_dispcnt(0);
ppu.step(CYCLES_PER_SCANLINE * 2, &mut ic, &vram, &pram, &oam);
assert_eq!(
ppu.read_vcount(),
0,
"After forced blank de-assert, vcount must restart from 0 after 2 scanlines"
);
}
#[test]
fn forced_blank_transition_scanlines_still_output_white() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let vram = make_vram();
let mut pram = make_pram();
let oam = make_oam();
pram[0] = 0x00;
pram[1] = 0x7C;
ppu.write_dispcnt(dispcnt::FORCED_BLANK);
ppu.step(CYCLES_PER_SCANLINE * 80, &mut ic, &vram, &pram, &oam);
ppu.write_dispcnt(0);
ppu.step(HBLANK_START_CYCLE, &mut ic, &vram, &pram, &oam);
let row_start = 80 * (SCREEN_WIDTH as usize) * BYTES_PER_PIXEL;
assert!(
ppu.framebuffer()[row_start..row_start + SCREEN_WIDTH as usize * BYTES_PER_PIXEL]
.iter()
.all(|&b| b == 0xFF),
"First transition scanline must output white while countdown is active"
);
}
#[test]
fn forced_blank_reassert_before_restart_cancels_countdown() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let vram = make_vram();
let pram = make_pram();
let oam = make_oam();
ppu.write_dispcnt(dispcnt::FORCED_BLANK);
ppu.step(CYCLES_PER_SCANLINE * 80, &mut ic, &vram, &pram, &oam);
assert_eq!(ppu.read_vcount(), 80);
ppu.write_dispcnt(0);
ppu.write_dispcnt(dispcnt::FORCED_BLANK);
ppu.step(CYCLES_PER_SCANLINE * 2, &mut ic, &vram, &pram, &oam);
assert_eq!(
ppu.read_vcount(),
82,
"Re-asserting forced blank must cancel the restart countdown"
);
}
#[test]
fn backdrop_fill_uses_pram_entry_0() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let vram = make_vram();
let mut pram = make_pram();
pram[0] = 0x00;
pram[1] = 0x7C;
ppu.write_dispcnt(0);
ppu.step(
CYCLES_PER_SCANLINE * SCANLINES_PER_FRAME,
&mut ic,
&vram,
&pram,
&make_oam(),
);
assert_eq!(&ppu.framebuffer()[0..3], &[0, 0, 0xFF]);
}
#[test]
fn mode0_bg0_4bpp_renders_first_tile_pixel() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let mut vram = make_vram();
let mut pram = make_pram();
ppu.write_dispcnt(dispcnt::BG0_ENABLE);
pram[2] = 0x1F;
pram[3] = 0x00;
vram[32] = 0x11;
vram[0x0000] = 0x01;
vram[0x0001] = 0x00;
ppu.step(
CYCLES_PER_SCANLINE * SCANLINES_PER_FRAME,
&mut ic,
&vram,
&pram,
&make_oam(),
);
assert_eq!(&ppu.framebuffer()[0..3], &[0xFF, 0, 0]);
}
#[test]
fn mode0_bg0_4bpp_hflip_mirrors_tile_pixels() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let mut vram = make_vram();
let mut pram = make_pram();
ppu.write_dispcnt(dispcnt::BG0_ENABLE);
pram[2] = 0x1F;
pram[3] = 0x00;
vram[32 + 3] = 0x10;
vram[0x0000] = 0x01;
vram[0x0001] = 0x04;
ppu.step(
CYCLES_PER_SCANLINE * SCANLINES_PER_FRAME,
&mut ic,
&vram,
&pram,
&make_oam(),
);
assert_eq!(&ppu.framebuffer()[0..3], &[0xFF, 0, 0]);
}
#[test]
fn mode0_bg0_4bpp_vflip_mirrors_tile_rows() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let mut vram = make_vram();
let mut pram = make_pram();
ppu.write_dispcnt(dispcnt::BG0_ENABLE);
pram[2] = 0x1F;
pram[3] = 0x00;
vram[32 + 28] = 0x01;
vram[0x0000] = 0x01;
vram[0x0001] = 0x08;
ppu.step(
CYCLES_PER_SCANLINE * SCANLINES_PER_FRAME,
&mut ic,
&vram,
&pram,
&make_oam(),
);
assert_eq!(&ppu.framebuffer()[0..3], &[0xFF, 0, 0]);
}
#[test]
fn mode0_bg0_8bpp_renders_direct_palette_index_and_ignores_palette_bank() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let mut vram = make_vram();
let mut pram = make_pram();
ppu.write_dispcnt(dispcnt::BG0_ENABLE);
ppu.write_bg0cnt(1 << 7);
pram[33 * 2] = 0x1F;
pram[33 * 2 + 1] = 0x00;
vram[64] = 33;
vram[0x0000] = 0x01;
vram[0x0001] = 0xF0;
ppu.step(
CYCLES_PER_SCANLINE * SCANLINES_PER_FRAME,
&mut ic,
&vram,
&pram,
&make_oam(),
);
assert_eq!(&ppu.framebuffer()[0..3], &[0xFF, 0, 0]);
}
#[test]
fn mode0_bg0_8bpp_palette_index_zero_is_transparent() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let mut vram = make_vram();
let mut pram = make_pram();
ppu.write_dispcnt(dispcnt::BG0_ENABLE);
ppu.write_bg0cnt(1 << 7);
pram[0] = 0x00;
pram[1] = 0x7C;
vram[0x0000] = 0x01;
ppu.step(
CYCLES_PER_SCANLINE * SCANLINES_PER_FRAME,
&mut ic,
&vram,
&pram,
&make_oam(),
);
assert_eq!(&ppu.framebuffer()[0..3], &[0, 0, 0xFF]);
}
#[test]
fn mode0_bg0_8bpp_hflip_and_vflip_mirror_tile_pixels() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let mut vram = make_vram();
let mut pram = make_pram();
ppu.write_dispcnt(dispcnt::BG0_ENABLE);
ppu.write_bg0cnt(1 << 7);
pram[5 * 2] = 0x1F;
pram[5 * 2 + 1] = 0x00;
vram[64 + 7 * 8 + 7] = 5;
vram[0x0000] = 0x01;
vram[0x0001] = 0x0C;
ppu.step(
CYCLES_PER_SCANLINE * SCANLINES_PER_FRAME,
&mut ic,
&vram,
&pram,
&make_oam(),
);
assert_eq!(&ppu.framebuffer()[0..3], &[0xFF, 0, 0]);
}
#[test]
fn mode1_bg1_8bpp_renders_text_layer() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let mut vram = make_vram();
let mut pram = make_pram();
ppu.write_dispcnt(1 | dispcnt::BG1_ENABLE);
ppu.write_bg_cnt(1, 1 << 7);
pram[7 * 2] = 0x1F;
pram[7 * 2 + 1] = 0x00;
vram[64] = 7;
vram[0x0000] = 0x01;
ppu.step(
CYCLES_PER_SCANLINE * SCANLINES_PER_FRAME,
&mut ic,
&vram,
&pram,
&make_oam(),
);
assert_eq!(&ppu.framebuffer()[0..3], &[0xFF, 0, 0]);
}
#[test]
fn write_affine_routes_pa_pb_pc_pd_for_bg2_and_bg3() {
let mut ppu = Ppu::new();
assert!(ppu.write_affine(REG_BG2PA, 0x0100));
assert!(ppu.write_affine(REG_BG2PB, 0xFF00)); assert!(ppu.write_affine(REG_BG2PC, 0x0080)); assert!(ppu.write_affine(REG_BG2PD, 0x0100));
assert!(ppu.write_affine(REG_BG3PA, 0x0040));
assert!(ppu.write_affine(REG_BG3PD, 0x0040));
let bg2 = ppu.bg_affine(0).expect("BG2 affine state must exist");
assert_eq!(bg2.pa, 0x0100);
assert_eq!(bg2.pb, -256);
assert_eq!(bg2.pc, 0x0080);
assert_eq!(bg2.pd, 0x0100);
let bg3 = ppu.bg_affine(1).expect("BG3 affine state must exist");
assert_eq!(bg3.pa, 0x0040);
assert_eq!(bg3.pd, 0x0040);
assert_eq!(bg3.pb, 0);
assert_eq!(bg3.pc, 0);
}
#[test]
fn bg_affine_returns_none_for_out_of_range_index() {
let ppu = Ppu::new();
assert!(ppu.bg_affine(2).is_none());
assert!(ppu.bg_affine(usize::MAX).is_none());
}
#[test]
fn write_affine_x_y_assembles_28_bit_signed_reference() {
let mut ppu = Ppu::new();
assert!(ppu.write_affine(REG_BG2X_L, 0x1234));
assert!(ppu.write_affine(REG_BG2X_H, 0x0005));
assert!(ppu.write_affine(REG_BG2Y_L, 0xFFFF));
assert!(ppu.write_affine(REG_BG2Y_H, 0x0FFF));
let bg2 = ppu.bg_affine(0).expect("BG2 affine state must exist");
assert_eq!(bg2.x, 0x0005_1234);
assert_eq!(bg2.y, -1);
}
#[test]
fn write_affine_returns_false_for_unrelated_address() {
let mut ppu = Ppu::new();
assert!(!ppu.write_affine(REG_DISPCNT, 0xFFFF));
let bg2 = ppu.bg_affine(0).expect("BG2 affine state must exist");
assert_eq!(bg2.pa, 0x0100);
assert_eq!(bg2.x, 0);
}
#[test]
fn mode2_renders_backdrop_color() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let vram = make_vram();
let mut pram = make_pram();
ppu.write_dispcnt(2);
pram[0] = 0xE0;
pram[1] = 0x03;
ppu.step(
CYCLES_PER_SCANLINE * SCANLINES_PER_FRAME,
&mut ic,
&vram,
&pram,
&make_oam(),
);
assert_eq!(&ppu.framebuffer()[0..3], &[0, 0xFF, 0]);
}
#[test]
fn mode4_renders_paletted_bitmap_from_vram() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let mut vram = make_vram();
let mut pram = make_pram();
ppu.write_dispcnt(4 | dispcnt::BG2_ENABLE);
pram[10] = 0x1F;
pram[11] = 0x00;
vram[0] = 5;
ppu.step(
CYCLES_PER_SCANLINE * SCANLINES_PER_FRAME,
&mut ic,
&vram,
&pram,
&make_oam(),
);
assert_eq!(&ppu.framebuffer()[0..3], &[0xFF, 0, 0]);
}
#[test]
fn mode4_palette_index_0_shows_backdrop() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let mut vram = make_vram();
let mut pram = make_pram();
ppu.write_dispcnt(4 | dispcnt::BG2_ENABLE);
pram[0] = 0x00;
pram[1] = 0x7C;
vram[0] = 0;
ppu.step(
CYCLES_PER_SCANLINE * SCANLINES_PER_FRAME,
&mut ic,
&vram,
&pram,
&make_oam(),
);
assert_eq!(&ppu.framebuffer()[0..3], &[0, 0, 0xFF]);
}
#[test]
fn mode4_palette_index_0_transparent_allows_obj_through() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let mut vram = make_vram();
let mut pram = make_pram();
let mut oam = make_oam();
ppu.write_dispcnt(4 | dispcnt::BG2_ENABLE | dispcnt::OBJ_ENABLE | dispcnt::OBJ_MAPPING_1D);
pram[0] = 0x00;
pram[1] = 0x7C;
vram[0] = 0;
setup_obj_in_oam(&mut oam, 0, 0, 0, 512, 1);
let obj_tile_base = 0x1_0000 + 512 * 32;
for byte in &mut vram[obj_tile_base..obj_tile_base + 32] {
*byte = 0x11; }
pram[0x202] = 0x1F;
pram[0x203] = 0x00;
ppu.step(
CYCLES_PER_SCANLINE * SCANLINES_PER_FRAME,
&mut ic,
&vram,
&pram,
&oam,
);
assert_eq!(
&ppu.framebuffer()[0..3],
&[0xFF, 0, 0],
"OBJ should show through Mode 4 palette index 0 (transparent) pixel"
);
}
#[test]
fn mode4_bg_mosaic_repeats_anchor_and_keeps_index_0_transparent() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let mut vram = make_vram();
let mut pram = make_pram();
ppu.write_dispcnt(4 | dispcnt::BG2_ENABLE);
ppu.write_bg_cnt(2, 1 << 6);
ppu.write_mosaic(0x0011);
ppu.write_affine(REG_BG2PA, 0x0100);
ppu.write_affine(REG_BG2PD, 0x0100);
pram[0] = 0x00;
pram[1] = 0x7C;
pram[2] = 0x1F;
pram[3] = 0x00;
vram[0] = 1;
vram[1] = 0;
vram[2] = 0;
vram[3] = 1;
vram[SCREEN_WIDTH as usize] = 0;
vram[SCREEN_WIDTH as usize + 1] = 1;
vram[SCREEN_WIDTH as usize + 2] = 1;
vram[SCREEN_WIDTH as usize + 3] = 1;
ppu.step(CYCLES_PER_SCANLINE * 3, &mut ic, &vram, &pram, &make_oam());
let row1 = SCREEN_WIDTH as usize * BYTES_PER_PIXEL;
assert_eq!(&ppu.framebuffer()[row1..row1 + 3], &[0xFF, 0, 0]);
assert_eq!(
&ppu.framebuffer()[row1 + BYTES_PER_PIXEL..row1 + BYTES_PER_PIXEL + 3],
&[0xFF, 0, 0]
);
assert_eq!(
&ppu.framebuffer()[row1 + BYTES_PER_PIXEL * 2..row1 + BYTES_PER_PIXEL * 2 + 3],
&[0, 0, 0xFF]
);
assert_eq!(
&ppu.framebuffer()[row1 + BYTES_PER_PIXEL * 3..row1 + BYTES_PER_PIXEL * 3 + 3],
&[0, 0, 0xFF]
);
}
#[test]
fn mode4_with_bg2_disabled_renders_backdrop() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let vram = make_vram();
let mut pram = make_pram();
ppu.write_dispcnt(4);
pram[0] = 0xE0;
pram[1] = 0x03;
ppu.step(
CYCLES_PER_SCANLINE * SCANLINES_PER_FRAME,
&mut ic,
&vram,
&pram,
&make_oam(),
);
assert_eq!(&ppu.framebuffer()[0..3], &[0, 0xFF, 0]);
}
#[test]
fn mode4_frame_select_uses_correct_frame_base() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let mut vram = make_vram();
let mut pram = make_pram();
ppu.write_dispcnt(4 | dispcnt::BG2_ENABLE | dispcnt::FRAME_SELECT);
pram[14] = 0xE0;
pram[15] = 0x03;
vram[0] = 1;
vram[0xA000] = 7;
ppu.step(
CYCLES_PER_SCANLINE * SCANLINES_PER_FRAME,
&mut ic,
&vram,
&pram,
&make_oam(),
);
assert_eq!(&ppu.framebuffer()[0..3], &[0, 0xFF, 0]);
}
#[test]
fn mode4_uses_bg2_affine_reference_point() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let mut vram = make_vram();
let mut pram = make_pram();
ppu.write_dispcnt(4 | dispcnt::BG2_ENABLE);
ppu.write_affine(REG_BG2PA, 0x0100);
ppu.write_affine(REG_BG2PD, 0x0100);
ppu.write_affine(REG_BG2X_L, 0x0100);
pram[2] = 0x1F;
pram[3] = 0x00;
pram[4] = 0xE0;
pram[5] = 0x03;
vram[0] = 1;
vram[1] = 2;
ppu.step(
CYCLES_PER_SCANLINE * SCANLINES_PER_FRAME,
&mut ic,
&vram,
&pram,
&make_oam(),
);
assert_eq!(&ppu.framebuffer()[0..3], &[0, 0xFF, 0]);
}
#[test]
fn mode4_outside_240x160_bitmap_is_transparent() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let mut vram = make_vram();
let mut pram = make_pram();
ppu.write_dispcnt(4 | dispcnt::BG2_ENABLE);
ppu.write_affine(REG_BG2PA, 0x0100);
ppu.write_affine(REG_BG2PD, 0x0100);
ppu.write_affine(REG_BG2X_L, 0xF000);
pram[0] = 0x00;
pram[1] = 0x7C;
pram[2] = 0x1F;
pram[3] = 0x00;
for vram_byte in vram
.iter_mut()
.take(SCREEN_WIDTH as usize * SCREEN_HEIGHT as usize)
{
*vram_byte = 1;
}
ppu.step(
CYCLES_PER_SCANLINE * SCANLINES_PER_FRAME,
&mut ic,
&vram,
&pram,
&make_oam(),
);
assert_eq!(&ppu.framebuffer()[0..3], &[0, 0, 0xFF]);
}
#[test]
fn mode4_bgcnt_wrap_bit_set_oob_is_still_transparent() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let mut vram = make_vram();
let mut pram = make_pram();
ppu.write_dispcnt(4 | dispcnt::BG2_ENABLE);
ppu.write_bg_cnt(2, 1 << 13);
ppu.write_affine(REG_BG2PA, 0x0100);
ppu.write_affine(REG_BG2PD, 0x0100);
ppu.write_affine(REG_BG2X_L, 0xF000);
pram[0] = 0x00;
pram[1] = 0x7C;
pram[2] = 0x1F;
pram[3] = 0x00;
for item in vram
.iter_mut()
.take(SCREEN_WIDTH as usize * SCREEN_HEIGHT as usize)
{
*item = 1;
}
ppu.step(
CYCLES_PER_SCANLINE * SCANLINES_PER_FRAME,
&mut ic,
&vram,
&pram,
&make_oam(),
);
assert_eq!(
&ppu.framebuffer()[0..3],
&[0, 0, 0xFF],
"Mode 4: BG2CNT wrap bit must be ignored; OOB pixels must show backdrop"
);
}
#[test]
fn mode5_renders_160x128_bgr555_bitmap_from_vram() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let mut vram = make_vram();
let mut pram = make_pram();
ppu.write_dispcnt(5 | dispcnt::BG2_ENABLE);
ppu.write_affine(REG_BG2PA, 0x0100);
ppu.write_affine(REG_BG2PD, 0x0100);
pram[0] = 0x00;
pram[1] = 0x7C;
vram[0] = 0x1F;
vram[1] = 0x80;
ppu.step(
CYCLES_PER_SCANLINE * SCANLINES_PER_FRAME,
&mut ic,
&vram,
&pram,
&make_oam(),
);
assert_eq!(&ppu.framebuffer()[0..3], &[0xFF, 0, 0]);
}
#[test]
fn mode5_frame_select_uses_frame_1_at_0xa000() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let mut vram = make_vram();
let pram = make_pram();
ppu.write_dispcnt(5 | dispcnt::BG2_ENABLE | dispcnt::FRAME_SELECT);
ppu.write_affine(REG_BG2PA, 0x0100);
ppu.write_affine(REG_BG2PD, 0x0100);
vram[0] = 0x1F;
vram[1] = 0x00;
vram[0xA000] = 0xE0;
vram[0xA001] = 0x03;
ppu.step(
CYCLES_PER_SCANLINE * SCANLINES_PER_FRAME,
&mut ic,
&vram,
&pram,
&make_oam(),
);
assert_eq!(&ppu.framebuffer()[0..3], &[0, 0xFF, 0]);
}
#[test]
fn mode5_outside_160x128_bitmap_is_transparent() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let mut vram = make_vram();
let mut pram = make_pram();
ppu.write_dispcnt(5 | dispcnt::BG2_ENABLE);
ppu.write_affine(REG_BG2PA, 0x0100);
ppu.write_affine(REG_BG2PD, 0x0100);
pram[0] = 0x00;
pram[1] = 0x7C;
for y in 0..MODE5_HEIGHT {
for x in 0..MODE5_WIDTH {
let off = (y * MODE5_WIDTH + x) * 2;
vram[off] = 0x1F;
vram[off + 1] = 0x00;
}
}
ppu.step(
CYCLES_PER_SCANLINE * SCANLINES_PER_FRAME,
&mut ic,
&vram,
&pram,
&make_oam(),
);
let x_outside = 160usize * BYTES_PER_PIXEL;
let y_outside = (128usize * SCREEN_WIDTH as usize) * BYTES_PER_PIXEL;
assert_eq!(&ppu.framebuffer()[0..3], &[0xFF, 0, 0]);
assert_eq!(&ppu.framebuffer()[x_outside..x_outside + 3], &[0, 0, 0xFF]);
assert_eq!(&ppu.framebuffer()[y_outside..y_outside + 3], &[0, 0, 0xFF]);
}
#[test]
fn mode5_bgcnt_wrap_bit_set_oob_is_still_transparent() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let mut vram = make_vram();
let mut pram = make_pram();
ppu.write_dispcnt(5 | dispcnt::BG2_ENABLE);
ppu.write_bg_cnt(2, 1 << 13);
ppu.write_affine(REG_BG2PA, 0x0100);
ppu.write_affine(REG_BG2PD, 0x0100);
ppu.write_affine(REG_BG2X_L, 0xA000);
pram[0] = 0x00;
pram[1] = 0x7C;
for y in 0..MODE5_HEIGHT {
for x in 0..MODE5_WIDTH {
let off = (y * MODE5_WIDTH + x) * 2;
vram[off] = 0x1F;
vram[off + 1] = 0x00;
}
}
ppu.step(
CYCLES_PER_SCANLINE * SCANLINES_PER_FRAME,
&mut ic,
&vram,
&pram,
&make_oam(),
);
assert_eq!(
&ppu.framebuffer()[0..3],
&[0, 0, 0xFF],
"Mode 5: BG2CNT wrap bit must be ignored; OOB pixels must show backdrop"
);
}
#[test]
fn mode5_uses_bg2_affine_reference_point() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let mut vram = make_vram();
let pram = make_pram();
ppu.write_dispcnt(5 | dispcnt::BG2_ENABLE);
ppu.write_affine(REG_BG2PA, 0x0100);
ppu.write_affine(REG_BG2PD, 0x0100);
ppu.write_affine(REG_BG2X_L, 0x0100);
vram[0] = 0x1F;
vram[1] = 0x00;
vram[2] = 0xE0;
vram[3] = 0x03;
ppu.step(
CYCLES_PER_SCANLINE * SCANLINES_PER_FRAME,
&mut ic,
&vram,
&pram,
&make_oam(),
);
assert_eq!(&ppu.framebuffer()[0..3], &[0, 0xFF, 0]);
}
#[test]
fn mode5_bg_mosaic_anchors_horizontally_and_vertically_with_affine() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let mut vram = make_vram();
let pram = make_pram();
ppu.write_dispcnt(5 | dispcnt::BG2_ENABLE);
ppu.write_bg_cnt(2, 1 << 6);
ppu.write_mosaic(0x0011);
ppu.write_affine(REG_BG2PA, 0x0100);
ppu.write_affine(REG_BG2PD, 0x0100);
let row0_col2 = 2 * 2;
vram[row0_col2..row0_col2 + 2].copy_from_slice(&0x001Fu16.to_le_bytes());
let row0_col3 = 3 * 2;
vram[row0_col3..row0_col3 + 2].copy_from_slice(&0x03E0u16.to_le_bytes());
let row0_col4 = 4 * 2;
vram[row0_col4..row0_col4 + 2].copy_from_slice(&0x7C00u16.to_le_bytes());
let row1_base = MODE5_WIDTH * 2;
let row1_col2 = row1_base + 2 * 2;
vram[row1_col2..row1_col2 + 2].copy_from_slice(&0x7FFFu16.to_le_bytes());
ppu.step(CYCLES_PER_SCANLINE * 3, &mut ic, &vram, &pram, &make_oam());
let fb_row1 = SCREEN_WIDTH as usize * BYTES_PER_PIXEL;
assert_eq!(
&ppu.framebuffer()[fb_row1 + 2 * BYTES_PER_PIXEL..fb_row1 + 2 * BYTES_PER_PIXEL + 3],
&[0xFF, 0, 0]
);
assert_eq!(
&ppu.framebuffer()[fb_row1 + 3 * BYTES_PER_PIXEL..fb_row1 + 3 * BYTES_PER_PIXEL + 3],
&[0xFF, 0, 0]
);
assert_eq!(
&ppu.framebuffer()[fb_row1 + 4 * BYTES_PER_PIXEL..fb_row1 + 4 * BYTES_PER_PIXEL + 3],
&[0, 0, 0xFF]
);
assert_eq!(
&ppu.framebuffer()[fb_row1 + 5 * BYTES_PER_PIXEL..fb_row1 + 5 * BYTES_PER_PIXEL + 3],
&[0, 0, 0xFF]
);
}
#[test]
fn mode0_bg1_renders_independently_of_bg0() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let mut vram = make_vram();
let mut pram = make_pram();
ppu.write_dispcnt(dispcnt::BG1_ENABLE);
ppu.write_bg_cnt(1, (1 << 2) | (8 << 8));
pram[4] = 0xE0;
pram[5] = 0x03;
vram[0x4000 + 32] = 0x02;
vram[0x4000] = 0x01;
vram[0x4001] = 0x00;
ppu.step(
CYCLES_PER_SCANLINE * SCANLINES_PER_FRAME,
&mut ic,
&vram,
&pram,
&make_oam(),
);
assert_eq!(&ppu.framebuffer()[0..3], &[0, 0xFF, 0]);
}
#[test]
fn mode0_bg_priority_higher_priority_layer_on_top() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let mut vram = make_vram();
let mut pram = make_pram();
ppu.write_dispcnt(dispcnt::BG0_ENABLE | dispcnt::BG1_ENABLE);
ppu.write_bg_cnt(0, 1);
ppu.write_bg_cnt(1, (1 << 2) | (8 << 8));
pram[2] = 0x1F;
pram[3] = 0x00;
pram[4] = 0xE0;
pram[5] = 0x03;
vram[32] = 0x01;
vram[0x0000] = 0x01;
vram[0x0001] = 0x00;
vram[0x4000 + 32] = 0x02;
vram[0x4000] = 0x01;
vram[0x4001] = 0x00;
ppu.step(
CYCLES_PER_SCANLINE * SCANLINES_PER_FRAME,
&mut ic,
&vram,
&pram,
&make_oam(),
);
assert_eq!(&ppu.framebuffer()[0..3], &[0, 0xFF, 0]);
}
#[test]
fn mode0_bg_transparent_pixel_shows_layer_below() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let mut vram = make_vram();
let mut pram = make_pram();
ppu.write_dispcnt(dispcnt::BG0_ENABLE | dispcnt::BG1_ENABLE);
ppu.write_bg_cnt(0, 0);
ppu.write_bg_cnt(1, 1 | (1 << 2) | (8 << 8));
pram[2] = 0x1F;
pram[3] = 0x00;
vram[32] = 0x00;
vram[0x0000] = 0x01;
vram[0x0001] = 0x00;
vram[0x4000 + 32] = 0x01;
vram[0x4000] = 0x01;
vram[0x4001] = 0x00;
ppu.step(
CYCLES_PER_SCANLINE * SCANLINES_PER_FRAME,
&mut ic,
&vram,
&pram,
&make_oam(),
);
assert_eq!(&ppu.framebuffer()[0..3], &[0xFF, 0, 0]);
}
#[test]
fn mode0_equal_priority_lower_bg_number_wins() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let mut vram = make_vram();
let mut pram = make_pram();
ppu.write_dispcnt(dispcnt::BG0_ENABLE | dispcnt::BG1_ENABLE);
ppu.write_bg_cnt(0, 0);
ppu.write_bg_cnt(1, (1 << 2) | (8 << 8));
pram[2] = 0x1F;
pram[3] = 0x00;
pram[4] = 0x00;
pram[5] = 0x7C;
vram[32] = 0x01;
vram[0x0000] = 0x01;
vram[0x0001] = 0x00;
vram[0x4000 + 32] = 0x02;
vram[0x4000] = 0x01;
vram[0x4001] = 0x00;
ppu.step(
CYCLES_PER_SCANLINE * SCANLINES_PER_FRAME,
&mut ic,
&vram,
&pram,
&make_oam(),
);
assert_eq!(&ppu.framebuffer()[0..3], &[0xFF, 0, 0]);
}
#[test]
fn text_bg_mosaic_repeats_upper_left_anchor_pixel() {
let mut ppu = Ppu::new();
let mut vram = make_vram();
let mut pram = make_pram();
let mut buf = [TRANSPARENT; SCREEN_WIDTH as usize];
ppu.write_bg_cnt(0, 1 << 6);
ppu.write_mosaic(0x0011);
pram[2] = 0x1F;
pram[3] = 0x00;
pram[4] = 0xE0;
pram[5] = 0x03;
pram[6] = 0x00;
pram[7] = 0x7C;
pram[8] = 0xFF;
pram[9] = 0x7F;
vram[0] = 1;
vram[32] = 0x21;
vram[32 + 4] = 0x43;
ppu.render_text_bg_layer(0, 1, &vram, &pram, &mut buf);
assert_eq!(buf[0], 0x001F);
assert_eq!(buf[1], 0x001F);
}
#[test]
fn bg0_bg1_cnt_mask_bit_13_on_read() {
let mut ppu = Ppu::new();
ppu.write_bg_cnt(0, 0xFFFF);
ppu.write_bg_cnt(1, 0xFFFF);
ppu.write_bg_cnt(2, 0xFFFF);
ppu.write_bg_cnt(3, 0xFFFF);
assert_eq!(ppu.read_bg_cnt(0), 0xDFFF, "BG0CNT should mask bit 13");
assert_eq!(ppu.read_bg_cnt(1), 0xDFFF, "BG1CNT should mask bit 13");
assert_eq!(ppu.read_bg_cnt(2), 0xFFFF, "BG2CNT should be unmasked");
assert_eq!(ppu.read_bg_cnt(3), 0xFFFF, "BG3CNT should be unmasked");
}
#[test]
fn affine_internal_ref_latches_at_vblank() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let vram = make_vram();
let pram = make_pram();
ppu.write_affine(REG_BG2X_L, 0x1000);
ppu.write_affine(REG_BG2Y_L, 0x2000);
ppu.step(
CYCLES_PER_SCANLINE * SCANLINES_PER_FRAME,
&mut ic,
&vram,
&pram,
&make_oam(),
);
let aff = ppu.bg_affine(0).unwrap();
assert_eq!(aff.internal_x, 0x1000, "internal_x should latch from x");
assert_eq!(aff.internal_y, 0x2000, "internal_y should latch from y");
}
#[test]
fn affine_internal_ref_increments_per_scanline() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let vram = make_vram();
let pram = make_pram();
ppu.write_affine(REG_BG2PA, 0x0100);
ppu.write_affine(REG_BG2PB, 0x0010);
ppu.write_affine(REG_BG2PC, 0x0000);
ppu.write_affine(REG_BG2PD, 0x0020);
ppu.step(
CYCLES_PER_SCANLINE * SCANLINES_PER_FRAME,
&mut ic,
&vram,
&pram,
&make_oam(),
);
ppu.step(CYCLES_PER_SCANLINE * 10, &mut ic, &vram, &pram, &make_oam());
let aff = ppu.bg_affine(0).unwrap();
assert_eq!(
aff.internal_x, 0x00A0,
"internal_x should increment by PB per scanline"
);
assert_eq!(
aff.internal_y, 0x0140,
"internal_y should increment by PD per scanline"
);
}
#[test]
fn bg2x_write_outside_vblank_applies_to_current_scanline() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let mut vram = make_vram();
let pram = make_pram();
ppu.write_dispcnt(3 | dispcnt::BG2_ENABLE);
ppu.write_affine(REG_BG2PA, 0x0100);
ppu.write_affine(REG_BG2PD, 0x0100);
vram[0] = 0x1F;
vram[1] = 0x00;
vram[2] = 0xE0;
vram[3] = 0x03;
ppu.step(
CYCLES_PER_SCANLINE * SCANLINES_PER_FRAME,
&mut ic,
&vram,
&pram,
&make_oam(),
);
ppu.write_affine(REG_BG2X_L, 0x0100);
ppu.step(CYCLES_PER_SCANLINE, &mut ic, &vram, &pram, &make_oam());
let (r, g, b) = color::bgr555_to_rgb888(0x03E0);
assert_eq!(
&ppu.framebuffer()[0..3],
&[r, g, b],
"mid-frame BG2X write must take effect on the current scanline"
);
}
#[test]
fn bg2y_write_outside_vblank_applies_to_current_scanline() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let mut vram = make_vram();
let pram = make_pram();
ppu.write_dispcnt(3 | dispcnt::BG2_ENABLE);
ppu.write_affine(REG_BG2PA, 0x0100);
ppu.write_affine(REG_BG2PD, 0x0100);
vram[0] = 0x1F;
vram[1] = 0x00; vram[480] = 0xE0;
vram[481] = 0x03;
ppu.step(
CYCLES_PER_SCANLINE * SCANLINES_PER_FRAME,
&mut ic,
&vram,
&pram,
&make_oam(),
);
ppu.write_affine(REG_BG2Y_L, 0x0100);
ppu.step(CYCLES_PER_SCANLINE, &mut ic, &vram, &pram, &make_oam());
let (r, g, b) = color::bgr555_to_rgb888(0x03E0);
assert_eq!(
&ppu.framebuffer()[0..3],
&[r, g, b],
"mid-frame BG2Y write must take effect on the current scanline"
);
}
#[test]
fn bg2x_midframe_write_resets_pb_accumulation() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let vram = make_vram();
let pram = make_pram();
ppu.write_affine(REG_BG2PA, 0x0100);
ppu.write_affine(REG_BG2PB, 0x0010);
ppu.write_affine(REG_BG2PC, 0x0000);
ppu.write_affine(REG_BG2PD, 0x0100);
ppu.step(
CYCLES_PER_SCANLINE * SCANLINES_PER_FRAME,
&mut ic,
&vram,
&pram,
&make_oam(),
);
ppu.step(CYCLES_PER_SCANLINE * 10, &mut ic, &vram, &pram, &make_oam());
ppu.write_affine(REG_BG2X_L, 0x0200);
let aff = ppu.bg_affine(0).unwrap();
assert_eq!(
aff.internal_x, 0x0200,
"mid-frame BG2X write must immediately set internal_x"
);
ppu.step(CYCLES_PER_SCANLINE * 5, &mut ic, &vram, &pram, &make_oam());
let aff = ppu.bg_affine(0).unwrap();
assert_eq!(
aff.internal_x, 0x0250,
"after mid-frame write, PB increments must count from the new value"
);
}
#[test]
fn bg2x_midframe_write_preserved_through_next_vblank() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let mut vram = make_vram();
let pram = make_pram();
ppu.write_dispcnt(3 | dispcnt::BG2_ENABLE);
ppu.write_affine(REG_BG2PA, 0x0100);
ppu.write_affine(REG_BG2PD, 0x0100);
vram[0] = 0x1F;
vram[1] = 0x00;
vram[2] = 0xE0;
vram[3] = 0x03;
ppu.step(
CYCLES_PER_SCANLINE * SCANLINES_PER_FRAME,
&mut ic,
&vram,
&pram,
&make_oam(),
);
ppu.write_affine(REG_BG2X_L, 0x0100);
ppu.step(
CYCLES_PER_SCANLINE * SCANLINES_PER_FRAME,
&mut ic,
&vram,
&pram,
&make_oam(),
);
ppu.step(CYCLES_PER_SCANLINE, &mut ic, &vram, &pram, &make_oam());
let (r, g, b) = color::bgr555_to_rgb888(0x03E0);
assert_eq!(
&ppu.framebuffer()[0..3],
&[r, g, b],
"mid-frame write must persist into the next frame via VBlank latch"
);
}
fn setup_affine_tile(
vram: &mut [u8],
pram: &mut [u8],
screenblock: usize,
charblock: usize,
map_x: usize,
map_y: usize,
color_bgr555: u16,
) {
let map_base = screenblock * 0x800;
let map_entry = map_base + map_y * 32 + map_x;
vram[map_entry] = 1;
let tile_base = charblock * 16 * 1024 + 64;
for i in 0..64 {
vram[tile_base + i] = 1;
}
let bytes = color_bgr555.to_le_bytes();
pram[2] = bytes[0]; pram[3] = bytes[1]; }
#[test]
fn mode2_affine_identity_renders_tile() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let mut vram = make_vram();
let mut pram = make_pram();
ppu.write_dispcnt(2 | dispcnt::BG2_ENABLE);
ppu.write_bg_cnt(2, (8 << 8) | (1 << 14));
ppu.write_affine(REG_BG2PA, 0x0100);
ppu.write_affine(REG_BG2PD, 0x0100);
let green = 0x03E0u16; setup_affine_tile(&mut vram, &mut pram, 8, 0, 0, 0, green);
pram[0] = 0;
pram[1] = 0;
ppu.step(
CYCLES_PER_SCANLINE * SCANLINES_PER_FRAME + CYCLES_PER_SCANLINE,
&mut ic,
&vram,
&pram,
&make_oam(),
);
assert_eq!(
&ppu.framebuffer()[0..3],
&[0, 0xFF, 0],
"pixel (0,0) should be green from affine tile"
);
}
#[test]
fn affine_bg_mosaic_repeats_upper_left_anchor_pixel() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let mut vram = make_vram();
let mut pram = make_pram();
ppu.write_dispcnt(2 | dispcnt::BG2_ENABLE);
ppu.write_bg_cnt(2, (1 << 6) | (8 << 8) | (1 << 14));
ppu.write_mosaic(0x0011);
ppu.write_affine(REG_BG2PA, 0x0100);
ppu.write_affine(REG_BG2PD, 0x0100);
vram[8 * 0x800] = 1;
pram[2] = 0x1F;
pram[3] = 0x00;
pram[4] = 0xE0;
pram[5] = 0x03;
pram[6] = 0x00;
pram[7] = 0x7C;
pram[8] = 0xFF;
pram[9] = 0x7F;
let tile_base = 64;
vram[tile_base] = 1;
vram[tile_base + 1] = 2;
vram[tile_base + 8] = 3;
vram[tile_base + 9] = 4;
vram[tile_base + 16] = 2;
vram[tile_base + 17] = 3;
ppu.step(CYCLES_PER_SCANLINE * 3, &mut ic, &vram, &pram, &make_oam());
let row1 = SCREEN_WIDTH as usize * BYTES_PER_PIXEL;
assert_eq!(&ppu.framebuffer()[row1..row1 + 3], &[0xFF, 0, 0]);
assert_eq!(
&ppu.framebuffer()[row1 + BYTES_PER_PIXEL..row1 + BYTES_PER_PIXEL + 3],
&[0xFF, 0, 0]
);
let row2 = row1 * 2;
assert_eq!(&ppu.framebuffer()[row2..row2 + 3], &[0, 0xFF, 0]);
assert_eq!(
&ppu.framebuffer()[row2 + BYTES_PER_PIXEL..row2 + BYTES_PER_PIXEL + 3],
&[0, 0xFF, 0]
);
}
#[test]
fn mode2_affine_palette_index_0_is_transparent() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let mut vram = make_vram();
let mut pram = make_pram();
ppu.write_dispcnt(2 | dispcnt::BG2_ENABLE);
ppu.write_bg_cnt(2, (8 << 8) | (1 << 14));
ppu.write_affine(REG_BG2PA, 0x0100);
ppu.write_affine(REG_BG2PD, 0x0100);
let map_base = 8 * 0x800;
vram[map_base] = 1;
let red_bgr = 0x001Fu16; pram[0] = red_bgr as u8;
pram[1] = (red_bgr >> 8) as u8;
ppu.step(
CYCLES_PER_SCANLINE * SCANLINES_PER_FRAME + CYCLES_PER_SCANLINE,
&mut ic,
&vram,
&pram,
&make_oam(),
);
assert_eq!(
&ppu.framebuffer()[0..3],
&[0xFF, 0, 0],
"transparent tile should show backdrop"
);
}
#[test]
fn mode2_affine_out_of_bounds_is_transparent_when_no_wrap() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let mut vram = make_vram();
let mut pram = make_pram();
ppu.write_dispcnt(2 | dispcnt::BG2_ENABLE);
ppu.write_bg_cnt(2, 8 << 8);
ppu.write_affine(REG_BG2PA, 0x0100);
ppu.write_affine(REG_BG2PD, 0x0100);
ppu.write_affine(REG_BG2X_L, 0xC800);
let green = 0x03E0u16;
setup_affine_tile(&mut vram, &mut pram, 8, 0, 0, 0, green);
let blue_bgr = 0x7C00u16;
pram[0] = blue_bgr as u8;
pram[1] = (blue_bgr >> 8) as u8;
ppu.step(
CYCLES_PER_SCANLINE * SCANLINES_PER_FRAME + CYCLES_PER_SCANLINE,
&mut ic,
&vram,
&pram,
&make_oam(),
);
assert_eq!(
&ppu.framebuffer()[0..3],
&[0, 0, 0xFF],
"out-of-bounds should show backdrop when no wrap"
);
}
#[test]
fn mode2_affine_wrapping_wraps_coordinates() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let mut vram = make_vram();
let mut pram = make_pram();
ppu.write_dispcnt(2 | dispcnt::BG2_ENABLE);
ppu.write_bg_cnt(2, (8 << 8) | (1 << 13));
ppu.write_affine(REG_BG2PA, 0x0100);
ppu.write_affine(REG_BG2PD, 0x0100);
ppu.write_affine(REG_BG2X_L, 0x8000);
let green = 0x03E0u16;
setup_affine_tile(&mut vram, &mut pram, 8, 0, 0, 0, green);
pram[0] = 0;
pram[1] = 0;
ppu.step(
CYCLES_PER_SCANLINE * SCANLINES_PER_FRAME + CYCLES_PER_SCANLINE,
&mut ic,
&vram,
&pram,
&make_oam(),
);
assert_eq!(
&ppu.framebuffer()[0..3],
&[0, 0xFF, 0],
"wrapping should show tile at wrapped position"
);
}
#[test]
fn mode2_affine_bg2_bg3_priority_compositing() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let mut vram = make_vram();
let mut pram = make_pram();
ppu.write_dispcnt(2 | dispcnt::BG2_ENABLE | dispcnt::BG3_ENABLE);
ppu.write_bg_cnt(2, 1 | (8 << 8) | (1 << 14));
ppu.write_bg_cnt(3, (16 << 8) | (1 << 14));
ppu.write_affine(REG_BG2PA, 0x0100);
ppu.write_affine(REG_BG2PD, 0x0100);
ppu.write_affine(REG_BG3PA, 0x0100);
ppu.write_affine(REG_BG3PD, 0x0100);
let red = 0x001Fu16;
let map2_base = 8 * 0x800;
vram[map2_base] = 1; let tile1_base = 64; for i in 0..64 {
vram[tile1_base + i] = 1;
}
pram[2] = red as u8;
pram[3] = (red >> 8) as u8;
let green = 0x03E0u16;
let map3_base = 16 * 0x800;
vram[map3_base] = 2; let tile2_base = 128; for i in 0..64 {
vram[tile2_base + i] = 2;
}
pram[4] = green as u8;
pram[5] = (green >> 8) as u8;
pram[0] = 0;
pram[1] = 0;
ppu.step(
CYCLES_PER_SCANLINE * SCANLINES_PER_FRAME + CYCLES_PER_SCANLINE,
&mut ic,
&vram,
&pram,
&make_oam(),
);
assert_eq!(
&ppu.framebuffer()[0..3],
&[0, 0xFF, 0],
"BG3 (priority 0) should be on top of BG2 (priority 1)"
);
}
#[test]
fn mode1_mixes_regular_and_affine_bgs() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let mut vram = make_vram();
let mut pram = make_pram();
ppu.write_dispcnt(1 | dispcnt::BG0_ENABLE | dispcnt::BG2_ENABLE);
ppu.write_bg_cnt(0, 1 | (4 << 8));
ppu.write_bg_cnt(2, (1 << 7) | (2 << 2) | (8 << 8) | (1 << 14));
ppu.write_affine(REG_BG2PA, 0x0100);
ppu.write_affine(REG_BG2PD, 0x0100);
let green = 0x03E0u16;
let map_base = 8 * 0x800;
vram[map_base] = 1; let charblock2_base = 2 * 16 * 1024;
let tile1_base = charblock2_base + 64;
for i in 0..64 {
vram[tile1_base + i] = 1;
}
pram[2] = green as u8;
pram[3] = (green >> 8) as u8;
let red = 0x001Fu16;
let sb4_base = 4 * 0x800;
vram[sb4_base] = 1;
vram[sb4_base + 1] = 0;
let text_tile_base = 32; for i in 0..32 {
vram[text_tile_base + i] = 0x11;
}
pram[2] = red as u8;
pram[3] = (red >> 8) as u8;
for i in 0..64 {
vram[tile1_base + i] = 2; }
pram[4] = green as u8;
pram[5] = (green >> 8) as u8;
let blue = 0x7C00u16;
pram[0] = blue as u8;
pram[1] = (blue >> 8) as u8;
ppu.step(
CYCLES_PER_SCANLINE * SCANLINES_PER_FRAME + CYCLES_PER_SCANLINE,
&mut ic,
&vram,
&pram,
&make_oam(),
);
assert_eq!(
&ppu.framebuffer()[0..3],
&[0, 0xFF, 0],
"Mode 1: BG2 affine (priority 0) on top of BG0 text (priority 1)"
);
}
fn setup_obj_in_oam(oam: &mut [u8], obj_idx: usize, x: u16, y: u8, tile: u16, priority: u8) {
let base = obj_idx * 8;
let attr0 = y as u16; let attr1 = x & 0x1FF; let attr2 = (tile & 0x3FF) | ((priority as u16 & 3) << 10);
oam[base] = attr0 as u8;
oam[base + 1] = (attr0 >> 8) as u8;
oam[base + 2] = attr1 as u8;
oam[base + 3] = (attr1 >> 8) as u8;
oam[base + 4] = attr2 as u8;
oam[base + 5] = (attr2 >> 8) as u8;
}
#[test]
fn obj_renders_when_enabled_in_mode0() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let vram = make_vram();
let pram = make_pram();
let mut oam = make_oam();
ppu.write_dispcnt(dispcnt::OBJ_ENABLE | dispcnt::OBJ_MAPPING_1D);
setup_obj_in_oam(&mut oam, 0, 100, 0, 1, 0);
let tile_base = 0x1_0000 + 32;
let mut vram = vram;
for byte in &mut vram[tile_base..tile_base + 32] {
*byte = 0x11; }
let mut pram = pram;
pram[0x202] = 0x1F; pram[0x203] = 0x00;
ppu.step(
CYCLES_PER_SCANLINE * SCANLINES_PER_FRAME,
&mut ic,
&vram,
&pram,
&oam,
);
let dst = 100 * BYTES_PER_PIXEL;
assert_eq!(
&ppu.framebuffer()[dst..dst + 3],
&[0xFF, 0, 0],
"OBJ should render red pixel at x=100"
);
}
#[test]
fn obj_disabled_does_not_render() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let mut vram = make_vram();
let mut pram = make_pram();
let mut oam = make_oam();
ppu.write_dispcnt(dispcnt::OBJ_MAPPING_1D);
setup_obj_in_oam(&mut oam, 0, 100, 0, 1, 0);
let tile_base = 0x1_0000 + 32;
for byte in &mut vram[tile_base..tile_base + 32] {
*byte = 0x11;
}
pram[0x202] = 0x1F;
pram[0x203] = 0x00;
ppu.step(
CYCLES_PER_SCANLINE * SCANLINES_PER_FRAME,
&mut ic,
&vram,
&pram,
&oam,
);
let dst = 100 * BYTES_PER_PIXEL;
assert_eq!(
&ppu.framebuffer()[dst..dst + 3],
&[0, 0, 0],
"OBJ should not render when OBJ_ENABLE is off"
);
}
#[test]
fn hblank_interval_free_uses_reduced_obj_cycle_budget() {
let mut vram = make_vram();
let mut pram = make_pram();
let mut oam = make_oam();
for obj_idx in 0..14 {
setup_obj_in_oam(&mut oam, obj_idx, 0, 0, 0, 0);
let base = obj_idx * 8;
let attr1 = (oam[base + 2] as u16) | ((oam[base + 3] as u16) << 8) | (3 << 14);
oam[base + 2] = attr1 as u8;
oam[base + 3] = (attr1 >> 8) as u8;
}
setup_obj_in_oam(&mut oam, 14, 100, 0, 1, 0);
let base = 14 * 8;
let attr1 = (oam[base + 2] as u16) | ((oam[base + 3] as u16) << 8) | (3 << 14);
oam[base + 2] = attr1 as u8;
oam[base + 3] = (attr1 >> 8) as u8;
let tile_base = 0x1_0000 + 32;
for byte in &mut vram[tile_base..tile_base + 32] {
*byte = 0x11;
}
pram[0x202] = 0x1F;
pram[0x203] = 0x00;
let mut ppu_normal = Ppu::new();
let mut ppu_hblank_free = Ppu::new();
let mut ic_normal = make_ic();
let mut ic_hblank_free = make_ic();
ppu_normal.write_dispcnt(dispcnt::OBJ_ENABLE | dispcnt::OBJ_MAPPING_1D);
ppu_hblank_free.write_dispcnt(
dispcnt::OBJ_ENABLE | dispcnt::OBJ_MAPPING_1D | dispcnt::HBLANK_INTERVAL_FREE,
);
ppu_normal.step(
CYCLES_PER_SCANLINE * SCANLINES_PER_FRAME,
&mut ic_normal,
&vram,
&pram,
&oam,
);
ppu_hblank_free.step(
CYCLES_PER_SCANLINE * SCANLINES_PER_FRAME,
&mut ic_hblank_free,
&vram,
&pram,
&oam,
);
let dst = 100 * BYTES_PER_PIXEL;
assert_eq!(
&ppu_normal.framebuffer()[dst..dst + 3],
&[0xFF, 0, 0],
"OBJ should render with normal cycle budget"
);
assert_eq!(
&ppu_hblank_free.framebuffer()[dst..dst + 3],
&[0, 0, 0],
"OBJ should be dropped with hblank-free reduced cycle budget"
);
}
#[test]
fn obj_priority_compositing_with_bg() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let mut vram = make_vram();
let mut pram = make_pram();
let mut oam = make_oam();
ppu.write_dispcnt(dispcnt::BG0_ENABLE | dispcnt::OBJ_ENABLE | dispcnt::OBJ_MAPPING_1D);
let bgcnt = 1 | (8 << 8); ppu.write_bg_cnt(0, bgcnt);
let tile_base = 32; for byte in &mut vram[tile_base..tile_base + 32] {
*byte = 0x22;
}
let sb_base = 8 * 0x800;
vram[sb_base] = 1; vram[sb_base + 1] = 0;
pram[4] = 0xE0; pram[5] = 0x03;
setup_obj_in_oam(&mut oam, 0, 0, 0, 1, 0);
let obj_tile_base = 0x1_0000 + 32;
for byte in &mut vram[obj_tile_base..obj_tile_base + 32] {
*byte = 0x11; }
pram[0x202] = 0x1F; pram[0x203] = 0x00;
ppu.step(
CYCLES_PER_SCANLINE * SCANLINES_PER_FRAME,
&mut ic,
&vram,
&pram,
&oam,
);
assert_eq!(
&ppu.framebuffer()[0..3],
&[0xFF, 0, 0],
"OBJ priority 0 should draw on top of BG priority 1"
);
let mut ppu2 = Ppu::new();
ppu2.write_dispcnt(dispcnt::BG0_ENABLE | dispcnt::OBJ_ENABLE | dispcnt::OBJ_MAPPING_1D);
let bgcnt2 = 8 << 8; ppu2.write_bg_cnt(0, bgcnt2);
setup_obj_in_oam(&mut oam, 0, 0, 0, 1, 2);
ppu2.step(
CYCLES_PER_SCANLINE * SCANLINES_PER_FRAME,
&mut ic,
&vram,
&pram,
&oam,
);
assert_eq!(
&ppu2.framebuffer()[0..3],
&[0, 0xFF, 0],
"BG priority 0 should be on top of OBJ priority 2"
);
}
#[test]
fn window_registers_default_to_zero() {
let ppu = Ppu::new();
assert_eq!(ppu.read_winin(), 0);
assert_eq!(ppu.read_winout(), 0);
}
#[test]
fn write_winin_masks_invalid_bits() {
let mut ppu = Ppu::new();
ppu.write_winin(0xFFFF);
assert_eq!(ppu.read_winin(), 0x3F3F);
}
#[test]
fn write_winout_masks_invalid_bits() {
let mut ppu = Ppu::new();
ppu.write_winout(0xFFFF);
assert_eq!(ppu.read_winout(), 0x3F3F);
}
#[test]
fn write_win_h_stores_value() {
let mut ppu = Ppu::new();
ppu.write_win_h(0, 0x1080); ppu.write_win_h(1, 0x20F0); assert_eq!(ppu.read_win_h(0), 0x1080);
assert_eq!(ppu.read_win_h(1), 0x20F0);
}
#[test]
fn write_win_v_stores_value() {
let mut ppu = Ppu::new();
ppu.write_win_v(0, 0x1060); ppu.write_win_v(1, 0x00A0); assert_eq!(ppu.read_win_v(0), 0x1060);
assert_eq!(ppu.read_win_v(1), 0x00A0);
}
#[test]
fn dispcnt_window_enable_bits() {
let mut ppu = Ppu::new();
ppu.write_dispcnt(dispcnt::WIN0_ENABLE | dispcnt::WIN1_ENABLE | dispcnt::OBJ_WIN_ENABLE);
assert!(ppu.dispcnt & dispcnt::WIN0_ENABLE != 0);
assert!(ppu.dispcnt & dispcnt::WIN1_ENABLE != 0);
assert!(ppu.dispcnt & dispcnt::OBJ_WIN_ENABLE != 0);
}
#[test]
fn window_classify_outside_when_no_windows_active() {
let mut ppu = Ppu::new();
ppu.write_dispcnt(dispcnt::BG0_ENABLE);
assert!(!ppu.pixel_in_window(0, 100, 80));
}
#[test]
fn pixel_in_window_basic_rect() {
let mut ppu = Ppu::new();
ppu.write_win_h(0, (10 << 8) | 50);
ppu.write_win_v(0, (20 << 8) | 100);
assert!(ppu.pixel_in_window(0, 10, 20)); assert!(ppu.pixel_in_window(0, 49, 99)); assert!(!ppu.pixel_in_window(0, 50, 50)); assert!(!ppu.pixel_in_window(0, 25, 100)); assert!(!ppu.pixel_in_window(0, 5, 50)); }
#[test]
fn pixel_in_window_wraparound_x() {
let mut ppu = Ppu::new();
ppu.write_win_h(0, (200 << 8) | 50);
ppu.write_win_v(0, 160);
assert!(ppu.pixel_in_window(0, 210, 80)); assert!(ppu.pixel_in_window(0, 30, 80)); assert!(!ppu.pixel_in_window(0, 100, 80)); }
#[test]
fn pixel_in_window_clamp_x2_out_of_range() {
let mut ppu = Ppu::new();
ppu.write_win_h(0, (10 << 8) | 250);
ppu.write_win_v(0, 160); assert!(ppu.pixel_in_window(0, 10, 80)); assert!(ppu.pixel_in_window(0, 239, 80)); assert!(!ppu.pixel_in_window(0, 5, 80)); }
#[test]
fn pixel_in_window_wraparound_y() {
let mut ppu = Ppu::new();
ppu.write_win_h(0, 240); ppu.write_win_v(0, (120 << 8) | 50);
assert!(ppu.pixel_in_window(0, 100, 130)); assert!(ppu.pixel_in_window(0, 100, 30)); assert!(!ppu.pixel_in_window(0, 100, 60)); }
#[test]
fn pixel_in_window_clamp_y2_out_of_range() {
let mut ppu = Ppu::new();
ppu.write_win_h(0, 240); ppu.write_win_v(0, (10 << 8) | 200);
assert!(ppu.pixel_in_window(0, 100, 10)); assert!(ppu.pixel_in_window(0, 100, 159)); assert!(!ppu.pixel_in_window(0, 100, 5)); }
#[test]
fn pixel_in_window_vertical_end_at_frame_end_carries_into_next_visible_frame() {
let mut ppu = Ppu::new();
ppu.write_win_h(0, (120 << 8) | 240);
ppu.write_win_v(0, (80 << 8) | (SCANLINES_PER_FRAME as u16));
assert!(ppu.pixel_in_window(0, 120, 0));
assert!(ppu.pixel_in_window(0, 120, 79));
assert!(ppu.pixel_in_window(0, 120, 80));
assert!(ppu.pixel_in_window(0, 120, 159));
}
#[test]
fn pixel_in_window_vertical_end_before_frame_end_resets_before_next_visible_frame() {
let mut ppu = Ppu::new();
ppu.write_win_h(0, (120 << 8) | 240);
ppu.write_win_v(0, (80 << 8) | ((SCANLINES_PER_FRAME - 1) as u16));
assert!(!ppu.pixel_in_window(0, 120, 0));
assert!(!ppu.pixel_in_window(0, 120, 79));
assert!(ppu.pixel_in_window(0, 120, 80));
assert!(ppu.pixel_in_window(0, 120, 159));
}
#[test]
fn pixel_in_window_wraparound_x_low_range_is_inside() {
let mut ppu = Ppu::new();
ppu.write_win_h(0, (200 << 8) | 50);
ppu.write_win_v(0, 160);
assert!(ppu.pixel_in_window(0, 0, 80)); assert!(ppu.pixel_in_window(0, 30, 80)); assert!(ppu.pixel_in_window(0, 49, 80));
assert!(!ppu.pixel_in_window(0, 50, 80)); assert!(!ppu.pixel_in_window(0, 100, 80)); assert!(!ppu.pixel_in_window(0, 199, 80));
assert!(ppu.pixel_in_window(0, 200, 80)); assert!(ppu.pixel_in_window(0, 239, 80)); }
#[test]
fn pixel_in_window_wraparound_y_low_range_is_inside() {
let mut ppu = Ppu::new();
ppu.write_win_h(0, 240); ppu.write_win_v(0, (120 << 8) | 50);
assert!(ppu.pixel_in_window(0, 100, 0)); assert!(ppu.pixel_in_window(0, 100, 30)); assert!(ppu.pixel_in_window(0, 100, 49));
assert!(!ppu.pixel_in_window(0, 100, 50)); assert!(!ppu.pixel_in_window(0, 100, 60)); assert!(!ppu.pixel_in_window(0, 100, 119));
assert!(ppu.pixel_in_window(0, 100, 120)); assert!(ppu.pixel_in_window(0, 100, 159)); }
#[test]
fn window_layer_mask_win0_takes_priority() {
let mut ppu = Ppu::new();
ppu.write_dispcnt(dispcnt::WIN0_ENABLE | dispcnt::WIN1_ENABLE | dispcnt::BG0_ENABLE);
ppu.write_win_h(0, 240); ppu.write_win_v(0, 160); ppu.write_win_h(1, 240); ppu.write_win_v(1, 160); ppu.write_winin(0x0201); let mask = ppu.window_layer_mask(100, 80, None);
assert_eq!(mask, 0x01);
}
#[test]
fn window_layer_mask_outside_when_not_in_any_window() {
let mut ppu = Ppu::new();
ppu.write_dispcnt(dispcnt::WIN0_ENABLE | dispcnt::BG0_ENABLE);
ppu.write_win_h(0, (10 << 8) | 50);
ppu.write_win_v(0, (10 << 8) | 50);
ppu.write_winin(0x003F); ppu.write_winout(0x0001); let mask = ppu.window_layer_mask(100, 80, None);
assert_eq!(mask, 0x01);
}
#[test]
fn window_obj_window_uses_winout_high_byte() {
let mut ppu = Ppu::new();
ppu.write_dispcnt(dispcnt::OBJ_WIN_ENABLE | dispcnt::OBJ_ENABLE);
ppu.write_winout(0x3F00); let mut obj_scanline = obj::ObjScanline::default();
obj_scanline.obj_window[50] = true;
let mask = ppu.window_layer_mask(50, 0, Some(&obj_scanline));
assert_eq!(mask, 0x3F);
let mask = ppu.window_layer_mask(51, 0, Some(&obj_scanline));
assert_eq!(mask, 0x00);
}
#[test]
fn composite_bios_like_obj_window_reveals_bg3() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let mut vram = make_vram();
let mut pram = make_pram();
let mut oam = make_oam();
ppu.write_dispcnt(
0x0002
| dispcnt::BG3_ENABLE
| dispcnt::OBJ_ENABLE
| dispcnt::OBJ_WIN_ENABLE
| dispcnt::OBJ_MAPPING_1D,
);
ppu.write_winout(0x3F27);
pram[2] = 0xE0;
pram[3] = 0x03;
ppu.write_bg_cnt(3, 16 << 8);
let sb_base = 16 * 0x800;
vram[sb_base] = 1; for byte in &mut vram[64..128] {
*byte = 1; }
ppu.write_affine(0x0400_0030, 0x0100); ppu.write_affine(0x0400_0036, 0x0100);
let attr0: u16 = 2 << 10; let attr1: u16 = 0; let attr2: u16 = 1; oam[0] = attr0 as u8;
oam[1] = (attr0 >> 8) as u8;
oam[2] = attr1 as u8;
oam[3] = (attr1 >> 8) as u8;
oam[4] = attr2 as u8;
oam[5] = (attr2 >> 8) as u8;
for byte in &mut vram[0x10020..0x10040] {
*byte = 0x11; }
pram[0x200 + 2] = 0x1F;
pram[0x200 + 3] = 0x00;
ppu.step(
CYCLES_PER_SCANLINE * SCANLINES_PER_FRAME,
&mut ic,
&vram,
&pram,
&oam,
);
ppu.step(CYCLES_PER_SCANLINE, &mut ic, &vram, &pram, &oam);
let fb = ppu.framebuffer();
let px4 = &fb[4 * 3..4 * 3 + 3];
let green_rgb = color::bgr555_to_rgb888(0x03E0);
assert_eq!(
px4,
&[green_rgb.0, green_rgb.1, green_rgb.2],
"Pixel 4 inside OBJ window should show BG3 (green)"
);
let px60 = &fb[60 * 3..60 * 3 + 3];
assert_eq!(
px60,
&[0, 0, 0],
"Pixel 60 outside OBJ window should show backdrop (BG3 disabled)"
);
}
#[test]
fn green_swap_defaults_to_off() {
let ppu = Ppu::new();
assert_eq!(ppu.read_green_swap(), 0);
}
#[test]
fn green_swap_register_round_trips() {
let mut ppu = Ppu::new();
ppu.write_green_swap(1);
assert_eq!(ppu.read_green_swap(), 1);
ppu.write_green_swap(0);
assert_eq!(ppu.read_green_swap(), 0);
}
#[test]
fn green_swap_only_bit0_is_stored() {
let mut ppu = Ppu::new();
ppu.write_green_swap(0xFFFF);
assert_eq!(ppu.read_green_swap(), 1);
ppu.write_green_swap(0xFFFE);
assert_eq!(ppu.read_green_swap(), 0);
}
#[test]
fn green_swap_swaps_green_channel_between_adjacent_pixels() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let mut vram = make_vram();
let pram = make_pram(); let oam = make_oam();
ppu.write_dispcnt(3 | dispcnt::BG2_ENABLE);
ppu.write_green_swap(1);
ppu.write_affine(REG_BG2PA, 0x0100);
ppu.write_affine(REG_BG2PD, 0x0100);
let color_a: u16 = (4u16 << 5) | 1;
let color_b: u16 = (8u16 << 5) | 2;
vram[0] = color_a as u8;
vram[1] = (color_a >> 8) as u8;
vram[2] = color_b as u8;
vram[3] = (color_b >> 8) as u8;
step_and_render_scanline0(&mut ppu, &mut ic, &vram, &pram, &oam);
let fb = ppu.framebuffer();
let (r_a, g_a, b_a) = color::bgr555_to_rgb888(color_a);
let (r_b, g_b, b_b) = color::bgr555_to_rgb888(color_b);
assert_eq!(fb[0], r_a, "pixel 0 R unchanged");
assert_eq!(fb[1], g_b, "pixel 0 G swapped from pixel 1");
assert_eq!(fb[2], b_a, "pixel 0 B unchanged");
assert_eq!(fb[3], r_b, "pixel 1 R unchanged");
assert_eq!(fb[4], g_a, "pixel 1 G swapped from pixel 0");
assert_eq!(fb[5], b_b, "pixel 1 B unchanged");
}
#[test]
fn green_swap_disabled_leaves_pixels_unchanged() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let mut vram = make_vram();
let pram = make_pram();
let oam = make_oam();
ppu.write_dispcnt(3 | dispcnt::BG2_ENABLE);
ppu.write_affine(REG_BG2PA, 0x0100);
ppu.write_affine(REG_BG2PD, 0x0100);
let color_a: u16 = (4u16 << 5) | 1;
let color_b: u16 = (8u16 << 5) | 2;
vram[0] = color_a as u8;
vram[1] = (color_a >> 8) as u8;
vram[2] = color_b as u8;
vram[3] = (color_b >> 8) as u8;
step_and_render_scanline0(&mut ppu, &mut ic, &vram, &pram, &oam);
let fb = ppu.framebuffer();
let (r_a, g_a, b_a) = color::bgr555_to_rgb888(color_a);
let (r_b, g_b, b_b) = color::bgr555_to_rgb888(color_b);
assert_eq!(
&fb[0..3],
&[r_a, g_a, b_a],
"pixel 0 unchanged without swap"
);
assert_eq!(
&fb[3..6],
&[r_b, g_b, b_b],
"pixel 1 unchanged without swap"
);
}
#[test]
fn bldcnt_write_masks_high_bits() {
let mut ppu = Ppu::new();
ppu.write_bldcnt(0xFFFF);
assert_eq!(
ppu.read_bldcnt(),
0x3FFF,
"BLDCNT: bits 14-15 must be discarded"
);
}
#[test]
fn bldcnt_initial_value_is_zero() {
let ppu = Ppu::new();
assert_eq!(ppu.read_bldcnt(), 0);
}
#[test]
fn bldalpha_write_masks_unused_bits() {
let mut ppu = Ppu::new();
ppu.write_bldalpha(0xFFFF);
assert_eq!(
ppu.read_bldalpha(),
0x1F1F,
"BLDALPHA: only bits 0-4 and 8-12 are valid"
);
}
#[test]
fn bldalpha_initial_value_is_zero() {
let ppu = Ppu::new();
assert_eq!(ppu.read_bldalpha(), 0);
}
#[test]
fn bldy_write_does_not_panic() {
let mut ppu = Ppu::new();
ppu.write_bldy(0x10);
ppu.write_bldy(0xFF); }
fn run_one_frame(
ppu: &mut Ppu,
ic: &mut InterruptController,
vram: &[u8],
pram: &[u8],
oam: &[u8],
) {
ppu.step(
CYCLES_PER_SCANLINE * SCANLINES_PER_FRAME,
ic,
vram,
pram,
oam,
);
}
#[test]
fn brightness_increase_mode2_full_evy_makes_pixel_white() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let mut vram = make_vram();
let mut pram = make_pram();
ppu.write_dispcnt(dispcnt::BG0_ENABLE);
pram[2] = 0x00;
pram[3] = 0x7C;
vram[32] = 0x11; vram[0x0000] = 0x01;
ppu.write_bldcnt((2 << 6) | 0x01);
ppu.write_bldy(16);
run_one_frame(&mut ppu, &mut ic, &vram, &pram, &make_oam());
assert_eq!(
&ppu.framebuffer()[0..3],
&[0xFF, 0xFF, 0xFF],
"Full brightness increase should output white"
);
}
#[test]
fn brightness_increase_no_effect_when_first_target_not_selected() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let mut vram = make_vram();
let mut pram = make_pram();
ppu.write_dispcnt(dispcnt::BG0_ENABLE);
pram[2] = 0x00;
pram[3] = 0x7C; vram[32] = 0x11;
vram[0x0000] = 0x01;
ppu.write_bldcnt(2 << 6);
ppu.write_bldy(16);
run_one_frame(&mut ppu, &mut ic, &vram, &pram, &make_oam());
let (r, g, b) = color::bgr555_to_rgb888(0x7C00);
assert_eq!(
&ppu.framebuffer()[0..3],
&[r, g, b],
"No effect when 1st target does not include the top pixel layer"
);
}
#[test]
fn brightness_decrease_mode3_full_evy_makes_pixel_black() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let mut vram = make_vram();
let mut pram = make_pram();
ppu.write_dispcnt(dispcnt::BG0_ENABLE);
pram[2] = 0xFF;
pram[3] = 0x7F;
vram[32] = 0x11;
vram[0x0000] = 0x01;
ppu.write_bldcnt((3 << 6) | 0x01);
ppu.write_bldy(16);
run_one_frame(&mut ppu, &mut ic, &vram, &pram, &make_oam());
assert_eq!(
&ppu.framebuffer()[0..3],
&[0, 0, 0],
"Full brightness decrease should output black"
);
}
#[test]
fn alpha_blend_mode1_bg_over_backdrop() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let mut vram = make_vram();
let mut pram = make_pram();
ppu.write_dispcnt(dispcnt::BG0_ENABLE);
pram[0] = 0x00;
pram[1] = 0x7C;
pram[2] = 0x1F;
pram[3] = 0x00;
vram[32] = 0x11; vram[0x0000] = 0x01;
ppu.write_bldcnt((1 << 6) | 0x01 | (1 << 13));
ppu.write_bldalpha(8 | (8 << 8));
run_one_frame(&mut ppu, &mut ic, &vram, &pram, &make_oam());
let (r, g, b) = color::bgr555_to_rgb888(0x3C0F);
assert_eq!(
&ppu.framebuffer()[0..3],
&[r, g, b],
"50/50 alpha blend of red BG0 over blue backdrop"
);
}
#[test]
fn alpha_blend_no_effect_when_second_target_not_selected() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let mut vram = make_vram();
let mut pram = make_pram();
ppu.write_dispcnt(dispcnt::BG0_ENABLE);
pram[0] = 0x00;
pram[1] = 0x7C; pram[2] = 0x1F;
pram[3] = 0x00; vram[32] = 0x11;
vram[0x0000] = 0x01;
ppu.write_bldcnt((1 << 6) | 0x01);
ppu.write_bldalpha(8 | (8 << 8));
run_one_frame(&mut ppu, &mut ic, &vram, &pram, &make_oam());
let (r, g, b) = color::bgr555_to_rgb888(0x001F);
assert_eq!(&ppu.framebuffer()[0..3], &[r, g, b]);
}
#[test]
fn no_effect_when_bldcnt_mode_is_zero() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let mut vram = make_vram();
let mut pram = make_pram();
ppu.write_dispcnt(dispcnt::BG0_ENABLE);
pram[2] = 0x00;
pram[3] = 0x7C; vram[32] = 0x11;
vram[0x0000] = 0x01;
ppu.write_bldcnt(0x003F);
ppu.write_bldy(16);
run_one_frame(&mut ppu, &mut ic, &vram, &pram, &make_oam());
let (r, g, b) = color::bgr555_to_rgb888(0x7C00);
assert_eq!(
&ppu.framebuffer()[0..3],
&[r, g, b],
"Mode 0 must not apply any color effect"
);
}
fn setup_semi_transparent_obj_in_oam(
oam: &mut [u8],
obj_idx: usize,
x: u16,
y: u8,
tile: u16,
priority: u8,
) {
let base = obj_idx * 8;
let attr0 = y as u16 | 0x0400;
let attr1 = x & 0x1FF;
let attr2 = (tile & 0x3FF) | ((priority as u16 & 3) << 10);
oam[base] = attr0 as u8;
oam[base + 1] = (attr0 >> 8) as u8;
oam[base + 2] = attr1 as u8;
oam[base + 3] = (attr1 >> 8) as u8;
oam[base + 4] = attr2 as u8;
oam[base + 5] = (attr2 >> 8) as u8;
}
#[test]
fn semi_transparent_obj_alpha_blends_with_bg_below() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let mut vram = make_vram();
let mut pram = make_pram();
let mut oam = make_oam();
ppu.write_dispcnt(dispcnt::BG0_ENABLE | dispcnt::OBJ_ENABLE | dispcnt::OBJ_MAPPING_1D);
ppu.write_bg_cnt(0, 1);
pram[2] = 0x00;
pram[3] = 0x7C;
vram[32] = 0x11; vram[0x0000] = 0x01;
let obj_tile_base = 0x1_0000 + 32;
for byte in &mut vram[obj_tile_base..obj_tile_base + 32] {
*byte = 0x11;
}
pram[0x202] = 0x1F;
pram[0x203] = 0x00;
setup_semi_transparent_obj_in_oam(&mut oam, 0, 0, 0, 1, 0);
ppu.write_bldcnt(1 << 8);
ppu.write_bldalpha(8 | (8 << 8));
run_one_frame(&mut ppu, &mut ic, &vram, &pram, &oam);
let (r, g, b) = color::bgr555_to_rgb888(0x3C0F);
assert_eq!(
&ppu.framebuffer()[0..3],
&[r, g, b],
"Semi-transparent OBJ should alpha blend with BG below"
);
}
#[test]
fn semi_transparent_obj_shows_normal_when_no_second_target() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let mut vram = make_vram();
let mut pram = make_pram();
let mut oam = make_oam();
ppu.write_dispcnt(dispcnt::BG0_ENABLE | dispcnt::OBJ_ENABLE | dispcnt::OBJ_MAPPING_1D);
ppu.write_bg_cnt(0, 1);
pram[2] = 0x00;
pram[3] = 0x7C; vram[32] = 0x11;
vram[0x0000] = 0x01;
let obj_tile_base = 0x1_0000 + 32;
for byte in &mut vram[obj_tile_base..obj_tile_base + 32] {
*byte = 0x11;
}
pram[0x202] = 0x1F;
pram[0x203] = 0x00;
setup_semi_transparent_obj_in_oam(&mut oam, 0, 0, 0, 1, 0);
ppu.write_bldcnt(0);
ppu.write_bldalpha(8 | (8 << 8));
run_one_frame(&mut ppu, &mut ic, &vram, &pram, &oam);
let (r, g, b) = color::bgr555_to_rgb888(0x001F);
assert_eq!(
&ppu.framebuffer()[0..3],
&[r, g, b],
"Semi-transparent OBJ with no 2nd target should display at normal intensity"
);
}
#[test]
fn semi_transparent_obj_on_top_in_brightness_mode_alpha_blends_when_second_target_exists() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let mut vram = make_vram();
let mut pram = make_pram();
let mut oam = make_oam();
ppu.write_dispcnt(dispcnt::BG0_ENABLE | dispcnt::OBJ_ENABLE | dispcnt::OBJ_MAPPING_1D);
ppu.write_bg_cnt(0, 1);
pram[2] = 0x00;
pram[3] = 0x7C; vram[32] = 0x11;
vram[0x0000] = 0x01;
let obj_tile_base = 0x1_0000 + 32;
for byte in &mut vram[obj_tile_base..obj_tile_base + 32] {
*byte = 0x11;
}
pram[0x202] = 0x1F;
pram[0x203] = 0x00;
setup_semi_transparent_obj_in_oam(&mut oam, 0, 0, 0, 1, 0);
ppu.write_bldcnt((2 << 6) | 0x10 | (1 << 8));
ppu.write_bldalpha(8 | (8 << 8)); ppu.write_bldy(16);
run_one_frame(&mut ppu, &mut ic, &vram, &pram, &oam);
let (r, g, b) = color::bgr555_to_rgb888(0x3C0F);
assert_eq!(
&ppu.framebuffer()[0..3],
&[r, g, b],
"Semi-transparent OBJ on top in brightness mode must alpha-blend with 2nd target"
);
}
#[test]
fn semi_transparent_obj_on_top_in_brightness_mode_shows_normal_when_no_second_target() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let mut vram = make_vram();
let mut pram = make_pram();
let mut oam = make_oam();
ppu.write_dispcnt(dispcnt::BG0_ENABLE | dispcnt::OBJ_ENABLE | dispcnt::OBJ_MAPPING_1D);
ppu.write_bg_cnt(0, 1);
pram[2] = 0x00;
pram[3] = 0x7C;
vram[32] = 0x11;
vram[0x0000] = 0x01;
let obj_tile_base = 0x1_0000 + 32;
for byte in &mut vram[obj_tile_base..obj_tile_base + 32] {
*byte = 0x11;
}
pram[0x202] = 0x1F;
pram[0x203] = 0x00;
setup_semi_transparent_obj_in_oam(&mut oam, 0, 0, 0, 1, 0);
ppu.write_bldcnt((2 << 6) | 0x10);
ppu.write_bldy(16);
run_one_frame(&mut ppu, &mut ic, &vram, &pram, &oam);
let (r, g, b) = color::bgr555_to_rgb888(0x001F);
assert_eq!(
&ppu.framebuffer()[0..3],
&[r, g, b],
"Semi-transparent OBJ on top in brightness mode with no 2nd target must show normally"
);
}
#[test]
fn semi_transparent_obj_below_top_layer_suppresses_brightness_increase() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let mut vram = make_vram();
let mut pram = make_pram();
let mut oam = make_oam();
ppu.write_dispcnt(dispcnt::BG0_ENABLE | dispcnt::OBJ_ENABLE | dispcnt::OBJ_MAPPING_1D);
ppu.write_bg_cnt(0, 0);
pram[2] = 0x00;
pram[3] = 0x7C; vram[32] = 0x11;
vram[0x0000] = 0x01;
let obj_tile_base = 0x1_0000 + 32;
for byte in &mut vram[obj_tile_base..obj_tile_base + 32] {
*byte = 0x11;
}
pram[0x202] = 0x1F;
pram[0x203] = 0x00;
setup_semi_transparent_obj_in_oam(&mut oam, 0, 0, 0, 1, 1);
ppu.write_bldcnt((2 << 6) | 0x01 | (1 << 13));
ppu.write_bldy(16);
run_one_frame(&mut ppu, &mut ic, &vram, &pram, &oam);
let (r, g, b) = color::bgr555_to_rgb888(0x7C00);
assert_eq!(
&ppu.framebuffer()[0..3],
&[r, g, b],
"Brightness increase must be suppressed when semi-transparent OBJ is below and 2nd target exists"
);
}
#[test]
fn semi_transparent_obj_below_top_layer_suppresses_brightness_decrease() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let mut vram = make_vram();
let mut pram = make_pram();
let mut oam = make_oam();
ppu.write_dispcnt(dispcnt::BG0_ENABLE | dispcnt::OBJ_ENABLE | dispcnt::OBJ_MAPPING_1D);
ppu.write_bg_cnt(0, 0);
pram[2] = 0x00;
pram[3] = 0x7C; vram[32] = 0x11;
vram[0x0000] = 0x01;
let obj_tile_base = 0x1_0000 + 32;
for byte in &mut vram[obj_tile_base..obj_tile_base + 32] {
*byte = 0x11;
}
pram[0x202] = 0x1F;
pram[0x203] = 0x00;
setup_semi_transparent_obj_in_oam(&mut oam, 0, 0, 0, 1, 1);
ppu.write_bldcnt((3 << 6) | 0x01 | (1 << 13));
ppu.write_bldy(16);
run_one_frame(&mut ppu, &mut ic, &vram, &pram, &oam);
let (r, g, b) = color::bgr555_to_rgb888(0x7C00);
assert_eq!(
&ppu.framebuffer()[0..3],
&[r, g, b],
"Brightness decrease must be suppressed when semi-transparent OBJ is below and 2nd target exists"
);
}
#[test]
fn semi_transparent_obj_below_does_not_suppress_brightness_when_no_second_target() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let mut vram = make_vram();
let mut pram = make_pram();
let mut oam = make_oam();
ppu.write_dispcnt(dispcnt::BG0_ENABLE | dispcnt::OBJ_ENABLE | dispcnt::OBJ_MAPPING_1D);
ppu.write_bg_cnt(0, 0);
pram[2] = 0x00;
pram[3] = 0x7C;
vram[32] = 0x11;
vram[0x0000] = 0x01;
let obj_tile_base = 0x1_0000 + 32;
for byte in &mut vram[obj_tile_base..obj_tile_base + 32] {
*byte = 0x11;
}
pram[0x202] = 0x1F;
pram[0x203] = 0x00;
setup_semi_transparent_obj_in_oam(&mut oam, 0, 0, 0, 1, 1);
ppu.write_bldcnt((2 << 6) | 0x01);
ppu.write_bldy(16);
run_one_frame(&mut ppu, &mut ic, &vram, &pram, &oam);
assert_eq!(
&ppu.framebuffer()[0..3],
&[0xFF, 0xFF, 0xFF],
"Brightness must apply normally when no 2nd target is configured"
);
}
#[test]
fn window_masked_obj_does_not_suppress_brightness() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let mut vram = make_vram();
let mut pram = make_pram();
let mut oam = make_oam();
ppu.write_dispcnt(
dispcnt::BG0_ENABLE
| dispcnt::OBJ_ENABLE
| dispcnt::OBJ_MAPPING_1D
| dispcnt::WIN0_ENABLE,
);
ppu.write_bg_cnt(0, 0);
pram[2] = 0x00;
pram[3] = 0x7C; vram[32] = 0x11;
vram[0x0000] = 0x01;
let obj_tile_base = 0x1_0000 + 32;
for byte in &mut vram[obj_tile_base..obj_tile_base + 32] {
*byte = 0x11;
}
pram[0x202] = 0x1F;
pram[0x203] = 0x00; setup_semi_transparent_obj_in_oam(&mut oam, 0, 0, 0, 1, 1);
ppu.write_win_h(0, 240);
ppu.write_win_v(0, 160);
ppu.write_winin(0x0021);
ppu.write_bldcnt((2 << 6) | 0x01 | (1 << 13));
ppu.write_bldy(16);
run_one_frame(&mut ppu, &mut ic, &vram, &pram, &oam);
assert_eq!(
&ppu.framebuffer()[0..3],
&[0xFF, 0xFF, 0xFF],
"Window-masked OBJ must not suppress brightness"
);
}
#[test]
fn window_sfx_bit_zero_disables_color_effects() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let mut vram = make_vram();
let mut pram = make_pram();
ppu.write_dispcnt(dispcnt::BG0_ENABLE | dispcnt::WIN0_ENABLE);
pram[2] = 0x00;
pram[3] = 0x7C;
vram[32] = 0x11;
vram[0x0000] = 0x01;
ppu.write_win_h(0, 240);
ppu.write_win_v(0, 160);
ppu.write_winin(0x0001);
ppu.write_bldcnt((2 << 6) | 0x01);
ppu.write_bldy(16);
run_one_frame(&mut ppu, &mut ic, &vram, &pram, &make_oam());
let (r, g, b) = color::bgr555_to_rgb888(0x7C00);
assert_eq!(
&ppu.framebuffer()[0..3],
&[r, g, b],
"Window SFX bit=0 should disable color effects"
);
}
#[test]
fn window_sfx_bit_one_enables_color_effects() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let mut vram = make_vram();
let mut pram = make_pram();
ppu.write_dispcnt(dispcnt::BG0_ENABLE | dispcnt::WIN0_ENABLE);
pram[2] = 0x00;
pram[3] = 0x7C; vram[32] = 0x11;
vram[0x0000] = 0x01;
ppu.write_win_h(0, 240);
ppu.write_win_v(0, 160);
ppu.write_winin(0x0021);
ppu.write_bldcnt((2 << 6) | 0x01);
ppu.write_bldy(16);
run_one_frame(&mut ppu, &mut ic, &vram, &pram, &make_oam());
assert_eq!(
&ppu.framebuffer()[0..3],
&[0xFF, 0xFF, 0xFF],
"Window SFX bit=1 should enable color effects"
);
}
#[test]
fn backdrop_brightness_increase_applies_when_all_layers_disabled() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let vram = make_vram();
let mut pram = make_pram();
ppu.write_dispcnt(0);
pram[0] = 0x00;
pram[1] = 0x7C;
ppu.write_bldcnt((2 << 6) | (1 << 5));
ppu.write_bldy(16);
run_one_frame(&mut ppu, &mut ic, &vram, &pram, &make_oam());
assert_eq!(
&ppu.framebuffer()[0..3],
&[0xFF, 0xFF, 0xFF],
"Backdrop brightness increase with EVY=16 should output white"
);
}
#[test]
fn backdrop_brightness_decrease_applies_when_all_layers_disabled() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let vram = make_vram();
let mut pram = make_pram();
ppu.write_dispcnt(0);
pram[0] = 0xFF;
pram[1] = 0x7F;
ppu.write_bldcnt((3 << 6) | (1 << 5));
ppu.write_bldy(16);
run_one_frame(&mut ppu, &mut ic, &vram, &pram, &make_oam());
assert_eq!(
&ppu.framebuffer()[0..3],
&[0, 0, 0],
"Backdrop brightness decrease with EVY=16 should output black"
);
}
#[test]
fn backdrop_sfx_applies_per_pixel_when_window_active_all_layers_disabled() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let vram = make_vram();
let mut pram = make_pram();
ppu.write_dispcnt(dispcnt::WIN0_ENABLE);
pram[0] = 0x00;
pram[1] = 0x7C;
ppu.write_win_h(0, 120);
ppu.write_win_v(0, 160);
ppu.write_winin(1 << 5); ppu.write_winout(0);
ppu.write_bldcnt((2 << 6) | (1 << 5));
ppu.write_bldy(16);
run_one_frame(&mut ppu, &mut ic, &vram, &pram, &make_oam());
assert_eq!(
&ppu.framebuffer()[0..3],
&[0xFF, 0xFF, 0xFF],
"Backdrop inside WIN0 with SFX=1 and EVY=16 should be white"
);
let (r, g, b) = color::bgr555_to_rgb888(0x7C00);
let pixel_start = 120 * BYTES_PER_PIXEL;
assert_eq!(
&ppu.framebuffer()[pixel_start..pixel_start + 3],
&[r, g, b],
"Backdrop outside WIN0 with SFX=0 should remain raw blue"
);
}
#[test]
fn bldy_read_bldy_returns_live_evy() {
let mut ppu = Ppu::new();
ppu.write_bldy(12);
assert_eq!(ppu.read_bldy(), 12, "read_bldy should return the live EVY");
ppu.write_bldy(0x1F); assert_eq!(ppu.read_bldy(), 0x1F);
ppu.write_bldy(0xFF); assert_eq!(ppu.read_bldy(), 0x1F);
}
fn fill_obj_tile_4bpp(vram: &mut [u8], tile_id: usize, pal_idx: u8) {
let base = 0x1_0000 + tile_id * 32;
let nibble_pair = pal_idx | (pal_idx << 4);
for b in &mut vram[base..base + 32] {
*b = nibble_pair;
}
}
#[test]
fn prohibited_mode_6_obj_renders_when_obj_enabled() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let mut vram = make_vram();
let mut pram = make_pram();
let mut oam = make_oam();
ppu.write_dispcnt(6 | dispcnt::OBJ_ENABLE | dispcnt::OBJ_MAPPING_1D);
setup_obj_in_oam(&mut oam, 0, 100, 0, 512, 0);
fill_obj_tile_4bpp(&mut vram, 512, 1);
pram[0x202] = 0x1F;
pram[0x203] = 0x00;
ppu.step(
CYCLES_PER_SCANLINE * SCANLINES_PER_FRAME,
&mut ic,
&vram,
&pram,
&oam,
);
let dst = 100 * BYTES_PER_PIXEL;
assert_eq!(
&ppu.framebuffer()[dst..dst + 3],
&[0xFF, 0, 0],
"mode 6: OBJ should render red pixel at x=100"
);
}
#[test]
fn prohibited_mode_7_obj_renders_when_obj_enabled() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let mut vram = make_vram();
let mut pram = make_pram();
let mut oam = make_oam();
ppu.write_dispcnt(7 | dispcnt::OBJ_ENABLE | dispcnt::OBJ_MAPPING_1D);
setup_obj_in_oam(&mut oam, 0, 100, 0, 512, 0);
fill_obj_tile_4bpp(&mut vram, 512, 1);
pram[0x202] = 0x1F;
pram[0x203] = 0x00;
ppu.step(
CYCLES_PER_SCANLINE * SCANLINES_PER_FRAME,
&mut ic,
&vram,
&pram,
&oam,
);
let dst = 100 * BYTES_PER_PIXEL;
assert_eq!(
&ppu.framebuffer()[dst..dst + 3],
&[0xFF, 0, 0],
"mode 7: OBJ should render red pixel at x=100"
);
}
#[test]
fn prohibited_mode_6_backdrop_when_obj_disabled() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let vram = make_vram();
let mut pram = make_pram();
let oam = make_oam();
ppu.write_dispcnt(6);
pram[0] = 0xE0;
pram[1] = 0x03;
ppu.step(
CYCLES_PER_SCANLINE * SCANLINES_PER_FRAME,
&mut ic,
&vram,
&pram,
&oam,
);
let dst = 0;
assert_eq!(
&ppu.framebuffer()[dst..dst + 3],
&[0, 0xFF, 0],
"mode 6: backdrop (green) should fill scanline when OBJ disabled"
);
}
#[test]
fn prohibited_mode_7_backdrop_when_obj_disabled() {
let mut ppu = Ppu::new();
let mut ic = make_ic();
let vram = make_vram();
let mut pram = make_pram();
let oam = make_oam();
ppu.write_dispcnt(7);
pram[0] = 0xE0;
pram[1] = 0x03;
ppu.step(
CYCLES_PER_SCANLINE * SCANLINES_PER_FRAME,
&mut ic,
&vram,
&pram,
&oam,
);
let dst = 0;
assert_eq!(
&ppu.framebuffer()[dst..dst + 3],
&[0, 0xFF, 0],
"mode 7: backdrop (green) should fill scanline when OBJ disabled"
);
}
#[test]
fn ppu_color_correction_defaults_to_false() {
let ppu = Ppu::new();
assert!(
!ppu.color_correction,
"color_correction should default to false"
);
}
#[test]
fn ppu_set_color_correction_enables_it() {
let mut ppu = Ppu::new();
ppu.set_color_correction(true);
assert!(
ppu.color_correction,
"set_color_correction(true) should enable it"
);
}
#[test]
fn ppu_set_color_correction_disables_it() {
let mut ppu = Ppu::new();
ppu.set_color_correction(true);
ppu.set_color_correction(false);
assert!(
!ppu.color_correction,
"set_color_correction(false) should disable it"
);
}
#[test]
fn ppu_color_correction_darkens_midrange_colors() {
let bgr555_mid_red: u16 = 16; let vram = make_vram();
let mut pram = make_pram();
let oam = make_oam();
let mut ic = make_ic();
pram[0] = bgr555_mid_red as u8;
pram[1] = (bgr555_mid_red >> 8) as u8;
let mut ppu_linear = Ppu::new();
ppu_linear.write_dispcnt(0x0000);
step_and_render_scanline0(&mut ppu_linear, &mut ic, &vram, &pram, &oam);
let r_linear = ppu_linear.framebuffer()[0];
let mut ppu_corrected = Ppu::new();
ppu_corrected.write_dispcnt(0x0000);
ppu_corrected.set_color_correction(true);
let mut ic2 = make_ic();
step_and_render_scanline0(&mut ppu_corrected, &mut ic2, &vram, &pram, &oam);
let r_corrected = ppu_corrected.framebuffer()[0];
assert!(
r_corrected < r_linear,
"corrected red ({r_corrected}) should be darker than linear ({r_linear}) for r5=16"
);
}
}