diff --git a/src/graphics_demo.rs b/src/graphics_demo.rs new file mode 100644 index 0000000..2ca215d --- /dev/null +++ b/src/graphics_demo.rs @@ -0,0 +1,499 @@ +//! Original ARM program for CPU-driven Mode 3 graphics and polled GBA input. +//! ARM code uses DMA3 for the background fill, then redraws during VBlank. +//! The optional original BIOS supplies VBlankIntrWait and IRQ dispatch. +//! The host runs between display events, not RAM update-counter changes. + +use std::{error::Error, fmt}; + +use crate::{ + bios, + cpu::CpuError, + dma::DmaError, + input::Buttons, + io::DISPCNT, + machine::{FrameRunError, Machine, MachineError}, + memory::{MemoryError, ROM_START}, + video::{Framebuffer, VideoError}, +}; + +pub const DEMO_STATE: u32 = 0x0200_0000; // completed updates, x, y, color (debug state) +pub const SQUARE_SIZE: usize = 16; +pub const BACKGROUND: u16 = 0x4000; +const FRAME_STEP_LIMIT: usize = 200_000; + +#[derive(Debug)] +pub enum GraphicsError { + Cpu(CpuError), + Dma(DmaError), + Memory(MemoryError), + Video(VideoError), + FrameTimeout, + FrameUnavailable, +} + +impl fmt::Display for GraphicsError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Cpu(error) => write!(f, "{error}"), + Self::Dma(error) => write!(f, "{error}"), + Self::Memory(error) => write!(f, "{error}"), + Self::Video(error) => write!(f, "{error}"), + Self::FrameUnavailable => write!(f, "no complete scanline frame is available"), + Self::FrameTimeout => write!( + f, + "graphics demo exceeded {FRAME_STEP_LIMIT} steps without completing a frame" + ), + } + } +} + +impl Error for GraphicsError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + Self::Cpu(error) => Some(error), + Self::Dma(error) => Some(error), + Self::Memory(error) => Some(error), + Self::Video(error) => Some(error), + Self::FrameTimeout | Self::FrameUnavailable => None, + } + } +} + +impl From for GraphicsError { + fn from(error: MachineError) -> Self { + match error { + MachineError::Cpu(error) => Self::Cpu(error), + MachineError::Dma(error) => Self::Dma(error), + } + } +} + +pub struct GraphicsDemo { + machine: Machine, + primed: bool, +} + +impl GraphicsDemo { + pub fn new() -> Result { + Self::with_program(program()) + } + + pub(crate) fn with_program(rom: Vec) -> Result { + Ok(Self { + machine: bios::boot(rom)?, + primed: false, + }) + } + + pub fn machine(&self) -> &Machine { + &self.machine + } + + /// Supply input, run to the next VBlank entry, then present the captured image. + /// The first call also runs startup under forced blank and primes the VBlank wait. + /// Emulated CPU code and DMA write display registers, VRAM, and demo state. + /// Errors retain completed machine steps; the output buffer is not updated. + pub fn frame( + &mut self, + buttons: Buttons, + output: &mut Framebuffer, + ) -> Result { + self.frame_with_control(buttons, output, 0x403) + } + + pub(crate) fn frame_with_control( + &mut self, + buttons: Buttons, + output: &mut Framebuffer, + ready_control: u16, + ) -> Result { + self.machine.memory_mut().set_scanline_rendering(true); + self.machine.memory_mut().set_buttons(buttons); + let mut steps = 0; + if !self.primed { + // Suppress incomplete startup images. This watches a real display + // register, not a software mailbox or an instruction-address breakpoint. + while self + .machine + .memory() + .read16(DISPCNT) + .map_err(GraphicsError::Memory)? + != ready_control + { + if steps == FRAME_STEP_LIMIT { + return Err(GraphicsError::FrameTimeout); + } + self.machine.step().map_err(GraphicsError::from)?; + steps += 1; + } + steps += self.wait_vblank(FRAME_STEP_LIMIT - steps)?; + self.primed = true; + } + // Drawing occurs in the preceding VBlank. At this next VBlank entry + // the completed visible frame is ready; the new update has not begun. + steps += self.wait_vblank(FRAME_STEP_LIMIT - steps)?; + if !self + .machine + .memory() + .present_frame(output) + .map_err(GraphicsError::Video)? + { + return Err(GraphicsError::FrameUnavailable); + } + Ok(steps) + } + + fn wait_vblank(&mut self, max_steps: usize) -> Result { + self.machine + .run_until_vblank(max_steps) + .map_err(|error| match error { + FrameRunError::Cpu(error) => GraphicsError::Cpu(error), + FrameRunError::Dma(error) => GraphicsError::Dma(error), + FrameRunError::StepLimit(_) => GraphicsError::FrameTimeout, + }) + } +} + +fn program() -> Vec { + let mut code = vec![ + 0xe3a0_8406, // MOV r8, #0x06000000 (VRAM) + 0xe3a0_9301, // MOV r9, #0x04000000 (DISPCNT) + 0xe289_ac01, // ADD r10, r9, #0x100 + 0xe28a_a030, // ADD r10, r10, #0x30 (KEYINPUT) + 0xe3a0_b402, // MOV r11, #0x02000000 (demo state) + 0xe3a0_0b01, // MOV r0, #0x400 + 0xe380_0083, // ORR r0, r0, #0x83 (Mode 3, BG2, forced blank) + 0xe1c9_00b0, // STRH r0, [r9] + 0xe3a0_0c01, // MOV r0,#256 (identity BG2 transform) + 0xe1c9_02b0, // STRH r0,[r9,#0x20] (BG2PA) + 0xe1c9_02b6, // STRH r0,[r9,#0x26] (BG2PD) + 0xe3a0_6901, // MOV r6, #0x4000 (blue background) + 0xe1cb_61b0, // STRH r6, [r11, #16] (DMA fill source) + 0xe289_00d4, // ADD r0, r9, #0xd4 (DMA3SAD) + 0xe28b_1010, // ADD r1, r11, #16 + 0xe580_1000, // STR r1, [r0] (source) + 0xe580_8004, // STR r8, [r0, #4] (VRAM destination) + 0xe3a0_1c96, // MOV r1, #38400 (halfwords) + 0xe3a0_2481, // MOV r2, #0x81000000 (enable, fixed source) + 0xe181_1002, // ORR r1, r1, r2 + 0xe580_1008, // STR r1, [r0, #8] (count/control; CPU pauses for DMA) + ]; + let irq_pointer_load = code.len(); + code.extend([ + 0, // LDR r0,=irq_handler (patched below) + 0xe3a0_1403, // MOV r1,#0x03000000 + 0xe281_1c7f, // ADD r1,r1,#0x7f00 + 0xe581_00fc, // STR r0,[r1,#0xfc] (BIOS IRQ callback pointer) + 0xe3a0_0008, // MOV r0, #8 (VBlank request enable) + 0xe1c9_00b4, // STRH r0, [r9, #4] (DISPSTAT) + 0xe289_1c02, // ADD r1, r9, #0x200 (IE) + 0xe3a0_0001, // MOV r0, #1 + 0xe1c1_00b0, // STRH r0, [r1] (IE=VBlank; wait service will enable IME) + 0xe3a0_4070, // MOV r4, #112 (x) + 0xe3a0_5048, // MOV r5, #72 (y) + 0xe3a0_7000, // MOV r7, #0 (completed frames) + 0xe3a0_0b01, // MOV r0, #0x400 + 0xe380_0003, // ORR r0, r0, #3 (Mode 3 + BG2, no forced blank) + 0xe1c9_00b0, // STRH r0, [r9] + ]); + // VBlankIntrWait discards the old BIOS flag and waits for a new IRQ callback. + let frame = code.len(); + code.push(0xef05_0000); // ARM SWI #0x050000 (VBlankIntrWait) + code.push(0xe3a0_6901); // MOV r6, #0x4000 (erase old square) + let erase_call = code.len(); + code.push(0); // BL draw_square, patched below + code.extend([ + 0xe1da_30b0, // LDRH r3, [r10] (sample active-low KEYINPUT once) + 0xe313_0010, // TST r3, #Right + 0x0284_4002, // ADDEQ r4, r4, #2 + 0xe313_0020, // TST r3, #Left + 0x0244_4002, // SUBEQ r4, r4, #2 + 0xe354_0000, // CMP r4, #0 + 0xb3a0_4000, // MOVLT r4, #0 + 0xe354_00e0, // CMP r4, #224 + 0xc3a0_40e0, // MOVGT r4, #224 + 0xe313_0040, // TST r3, #Up + 0x0245_5002, // SUBEQ r5, r5, #2 + 0xe313_0080, // TST r3, #Down + 0x0285_5002, // ADDEQ r5, r5, #2 + 0xe355_0000, // CMP r5, #0 + 0xb3a0_5000, // MOVLT r5, #0 + 0xe355_0090, // CMP r5, #144 + 0xc3a0_5090, // MOVGT r5, #144 + 0xe313_0008, // TST r3, #Start + 0x03a0_4070, // MOVEQ r4, #112 + 0x03a0_5048, // MOVEQ r5, #72 + 0xe3a0_6c7f, // MOV r6, #0x7f00 + 0xe386_60ff, // ORR r6, r6, #0xff (white) + 0xe313_0002, // TST r3, #B + 0x03a0_6e3e, // MOVEQ r6, #0x03e0 (green) + 0xe313_0001, // TST r3, #A + 0x03a0_601f, // MOVEQ r6, #0x001f (red, A takes priority) + ]); + let draw_call = code.len(); + code.push(0); // BL draw_square + code.extend([ + 0xe58b_4004, // STR r4, [r11, #4] + 0xe58b_5008, // STR r5, [r11, #8] + 0xe58b_600c, // STR r6, [r11, #12] + 0xe287_7001, // ADD r7, r7, #1 + 0xe58b_7000, // STR r7, [r11] (debug counter only; host does not wait on it) + ]); + branch(&mut code, frame, 14, false); + + // Draw a 16x16 square. r4=x, r5=y, r6=color. Clobbers r0-r2 only. + let draw_square = code.len(); + code.extend([ + 0xe065_0205, // RSB r0, r5, r5, LSL #4 (y * 15) + 0xe1a0_0200, // MOV r0, r0, LSL #4 (y * 240) + 0xe080_0004, // ADD r0, r0, r4 + 0xe088_0080, // ADD r0, r8, r0, LSL #1 (pixel byte address) + 0xe3a0_1010, // MOV r1, #16 (rows) + ]); + let row = code.len(); + code.push(0xe3a0_2010); // MOV r2, #16 (columns) + let pixel = code.len(); + code.extend([ + 0xe0c0_60b2, // STRH r6, [r0], #2 + 0xe252_2001, // SUBS r2, r2, #1 + ]); + branch(&mut code, pixel, 1, false); + code.extend([ + 0xe280_0d07, // ADD r0, r0, #448 (remaining bytes in row) + 0xe251_1001, // SUBS r1, r1, #1 + ]); + branch(&mut code, row, 1, false); + code.push(0xe12f_ff1e); // BX lr + patch_branch(&mut code, erase_call, draw_square, 14, true); + patch_branch(&mut code, draw_call, draw_square, 14, true); + + // BIOS dispatch saves r0-r3/r12/lr. The callback must acknowledge IF and + // record its requests in BIOS IRQ_FLAGS, then return to the dispatcher. + let irq_handler = code.len(); + code.extend([ + 0xe280_0c02, // ADD r0,r0,#0x200 (dispatcher supplies I/O base) + 0xe1d0_10b2, // LDRH r1,[r0,#2] (IF) + 0xe1c0_10b2, // STRH r1,[r0,#2] (acknowledge) + 0xe3a0_0403, // MOV r0,#0x03000000 + 0xe280_0c7f, // ADD r0,r0,#0x7f00 + 0xe1d0_2fb8, // LDRH r2,[r0,#0xf8] (BIOS IRQ_FLAGS) + 0xe182_2001, // ORR r2,r2,r1 + 0xe1c0_2fb8, // STRH r2,[r0,#0xf8] + 0xe12f_ff1e, // BX lr + ]); + let offset = (code.len() - irq_pointer_load - 2) * 4; + assert!(offset <= 0xfff); + code[irq_pointer_load] = 0xe59f_0000 | offset as u32; + code.push(ROM_START + irq_handler as u32 * 4); + code.into_iter().flat_map(u32::to_le_bytes).collect() +} + +fn branch(code: &mut Vec, target: usize, condition: u32, link: bool) { + let index = code.len(); + code.push(0); + patch_branch(code, index, target, condition, link); +} + +fn patch_branch(code: &mut [u32], index: usize, target: usize, condition: u32, link: bool) { + let displacement = target as i32 - index as i32 - 2; + assert!((-0x80_0000..0x80_0000).contains(&displacement)); + code[index] = (condition << 28) + | 0x0a00_0000 + | (u32::from(link) << 24) + | (displacement as u32 & 0x00ff_ffff); +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{cpu::Cpu, io::DISPSTAT, memory::Memory}; + + #[test] + fn frame_runner_does_not_require_a_ram_counter_update() { + let mut memory = Memory::new(0xeaff_fffe_u32.to_le_bytes().to_vec()).unwrap(); + memory.write16(DISPCNT, 0x403).unwrap(); + let mut demo = GraphicsDemo { + machine: Machine::new(Cpu::new(ROM_START), memory), + primed: true, + }; + let mut frame = Framebuffer::default(); + for expected in 1..=2 { + demo.frame(Buttons::default(), &mut frame).unwrap(); + assert_eq!(demo.machine().memory().read32(DEMO_STATE).unwrap(), 0); + assert_eq!(demo.machine().memory().display_position().vblanks, expected); + assert_eq!(demo.machine().memory().display_position().scanline, 160); + } + } + + #[test] + fn arm_startup_uses_dma3_for_every_background_pixel() { + use crate::{ + dma::DMA_BASE, + machine::StepKind, + memory::VRAM_START, + video::{HEIGHT, WIDTH}, + }; + + let mut demo = GraphicsDemo::new().unwrap(); + let mut transfers = 0; + let mut ready = false; + for _ in 0..50_000 { + let before = demo.machine.cpu().clone(); + if let StepKind::Dma { channel } = demo.machine.step().unwrap() { + assert_eq!(channel, 3); + assert_eq!(demo.machine.cpu(), &before); + assert_eq!(demo.machine.memory().read16(DISPCNT).unwrap(), 0x483); + transfers += 1; + } + if demo.machine.memory().read16(DISPCNT).unwrap() == 0x403 { + ready = true; + break; + } + } + assert!(ready); + assert_eq!(transfers, WIDTH * HEIGHT); + assert_eq!( + demo.machine + .memory() + .read16(DMA_BASE + 3 * 12 + 10) + .unwrap() + & 0x8000, + 0 + ); + for index in 0..WIDTH * HEIGHT { + assert_eq!( + demo.machine + .memory() + .read16(VRAM_START + index as u32 * 2) + .unwrap(), + BACKGROUND + ); + } + } + + #[test] + fn dma_error_keeps_output_unchanged() { + use crate::dma::DMA_BASE; + + let mut demo = GraphicsDemo::new().unwrap(); + // An unsupported special-mode request fails before any CPU instruction. + demo.machine + .memory_mut() + .write16(DMA_BASE + 10, 0xb000) + .unwrap(); + let mut frame = Framebuffer::default(); + frame.clear(0x1f); + assert!(matches!( + demo.frame(Buttons::default(), &mut frame), + Err(GraphicsError::Dma(_)) + )); + assert_eq!(demo.machine().cycles(), 0); + assert!(frame.pixels().iter().all(|&pixel| pixel == 0xff0000)); + } + + #[test] + fn graphics_uses_bios_wait_and_irq_dispatch_before_each_redraw() { + use crate::{ + cpu::Mode, + io::{IE, IF, IME}, + machine::StepKind, + }; + + let mut demo = GraphicsDemo::new().unwrap(); + let mut frame = Framebuffer::default(); + for expected in 1..=3 { + demo.frame(Buttons::default(), &mut frame).unwrap(); + assert_eq!( + demo.machine().memory().read32(DEMO_STATE).unwrap(), + expected + ); + assert_eq!(demo.machine().memory().display_position().line_cycle, 0); + assert_eq!(demo.machine().memory().read16(IE).unwrap(), 1); + assert_eq!(demo.machine().memory().read16(IME).unwrap(), 0); + assert_eq!(demo.machine().memory().read16(IF).unwrap(), 1); + assert!(demo.machine().last_timing().idle_cycles > 0); + assert_eq!(demo.machine().last_timing().code_cycles, 0); + assert_eq!(demo.machine().cpu().mode(), Mode::Supervisor); + assert_eq!(demo.machine().cpu().cpsr() & 0x80, 0); + assert!(!demo.machine().halted()); // VBlank wakes HALT while the wait service has IME=0. + let mut entries = 0; + for _ in 0..200 { + if demo.machine().cpu().mode() == Mode::System { + break; + } + if demo.machine.step().unwrap() == StepKind::IrqEntry { + entries += 1; + } + } + assert_eq!(entries, 1); + assert_eq!(demo.machine().memory().read16(IF).unwrap(), 0); + assert_eq!(demo.machine().memory().read16(bios::IRQ_FLAGS).unwrap(), 0); + assert_eq!(demo.machine().memory().read16(IME).unwrap(), 1); + assert_eq!(demo.machine().cpu().mode(), Mode::System); + assert_eq!(demo.machine().cpu().cpsr() & 0x80, 0); + assert!(demo.machine().cpu().pc() >= ROM_START); + } + } + + #[test] + fn arm_program_completes_one_update_per_vblank_while_status_is_set() { + let mut demo = GraphicsDemo::new().unwrap(); + let mut updates = 0; + let mut previous_vblank = None; + for _ in 0..300_000 { + demo.machine.step().unwrap(); + let counter = demo.machine.memory().read32(DEMO_STATE).unwrap(); + if counter != updates { + assert_eq!(counter, updates + 1); + assert_eq!(demo.machine.memory().read16(DISPSTAT).unwrap() & 1, 1); + let vblank = demo.machine.memory().display_position().vblanks; + if let Some(previous) = previous_vblank { + assert_eq!(vblank, previous + 1); + } + previous_vblank = Some(vblank); + updates = counter; + if updates == 3 { + break; + } + } + } + assert_eq!(updates, 3); + } + + #[test] + fn instruction_error_keeps_output_unchanged() { + let mut demo = GraphicsDemo { + machine: Machine::new( + Cpu::new(ROM_START), + Memory::new(0xf000_0000_u32.to_le_bytes().to_vec()).unwrap(), + ), + primed: false, + }; + let mut frame = Framebuffer::default(); + frame.clear(0x1f); + assert!(matches!( + demo.frame(Buttons::default(), &mut frame), + Err(GraphicsError::Cpu(_)) + )); + assert_eq!(demo.machine().cycles(), 0); + assert!(frame.pixels().iter().all(|&pixel| pixel == 0xff0000)); + } + + #[test] + fn stalled_program_returns_timeout_instead_of_hanging_the_window() { + let mut demo = GraphicsDemo { + machine: Machine::new( + Cpu::new(ROM_START), + Memory::new(0xeaff_fffe_u32.to_le_bytes().to_vec()).unwrap(), + ), + primed: false, + }; + let mut frame = Framebuffer::default(); + assert!(matches!( + demo.frame(Buttons::default(), &mut frame), + Err(GraphicsError::FrameTimeout) + )); + assert!(demo.machine().cycles() > 0); + assert!(frame.pixels().iter().all(|&pixel| pixel == 0)); + } +} diff --git a/src/tile_demo.rs b/src/tile_demo.rs new file mode 100644 index 0000000..2dcf0a8 --- /dev/null +++ b/src/tile_demo.rs @@ -0,0 +1,347 @@ +//! Original Mode 0 scene. ARM code copies assets through BIOS services, polls +//! KEYINPUT, and updates scroll registers and a sprite once per VBlank. +//! The host does not write VRAM or object attribute memory (OAM). + +use crate::{ + graphics_demo::{GraphicsDemo, GraphicsError}, + input::Buttons, + machine::Machine, + memory::{MemoryError, ROM_START}, + video::Framebuffer, +}; + +/// Completed updates, horizontal scroll, vertical scroll (three debug words). +pub const TILE_STATE: u32 = 0x0200_0000; + +pub struct TileDemo { + runner: GraphicsDemo, +} + +impl TileDemo { + pub fn new() -> Result { + Ok(Self { + runner: GraphicsDemo::with_program(program(Scene::Tiles))?, + }) + } + + pub fn machine(&self) -> &Machine { + self.runner.machine() + } + + /// Run startup if needed, then present one completed VBlank-synchronized frame. + pub fn frame( + &mut self, + buttons: Buttons, + output: &mut Framebuffer, + ) -> Result { + self.runner.frame_with_control(buttons, output, 0x1340) + } +} + +fn literal(code: &mut Vec, patches: &mut Vec<(usize, u32, u32)>, reg: u32, value: u32) { + patches.push((code.len(), reg, value)); + code.push(0); +} + +enum Scene { + Tiles, + Effects, + Mosaic, +} + +pub(crate) fn effects_program() -> Vec { + program(Scene::Effects) +} + +pub(crate) fn mosaic_program() -> Vec { + program(Scene::Mosaic) +} + +fn program(scene: Scene) -> Vec { + let effects = matches!(scene, Scene::Effects); + let mosaic = matches!(scene, Scene::Mosaic); + let mut code = vec![ + 0xe3a0_9301, // MOV r9,#0x04000000 + 0xe3a0_0080, // MOV r0,#0x80 (forced blank) + 0xe1c9_00b0, // STRH r0,[r9] + ]; + let mut patches = Vec::new(); + let palette_load = code.len(); + literal(&mut code, &mut patches, 0, 0); // LDR r0,=palette (patched after layout) + code.extend([ + 0xe3a0_1405, // MOV r1,#0x05000000 + 0xe3a0_2c02, // MOV r2,#512 halfwords (BG and OBJ palettes) + 0xef0b_0000, // CpuSet + ]); + let vram_load = code.len(); + literal(&mut code, &mut patches, 0, 0); // LDR r0,=VRAM image + code.extend([ + 0xe3a0_1406, // MOV r1,#0x06000000 + 0xe3a0_2a06, // MOV r2,#24576 words (96 KiB VRAM) + 0xef0c_0000, // CpuFastSet + ]); + let oam_load = code.len(); + literal(&mut code, &mut patches, 0, 0); // LDR r0,=OAM image + code.extend([ + 0xe3a0_1407, // MOV r1,#0x07000000 + 0xe3a0_2c02, // MOV r2,#512 halfwords + 0xef0b_0000, // CpuSet; disables unused sprites + 0xe3a0_6407, // MOV r6,#0x07000000 (keep OAM base) + ]); + let irq_load = code.len(); + literal(&mut code, &mut patches, 0, 0); + code.extend([ + 0xe3a0_1403, // MOV r1,#0x03000000 + 0xe281_1c7f, // ADD r1,r1,#0x7f00 + 0xe581_00fc, // STR r0,[r1,#0xfc] (callback) + 0xe3a0_0008, // MOV r0,#8 + 0xe1c9_00b4, // STRH r0,[r9,#4] (VBlank IRQ request) + 0xe289_1c02, // ADD r1,r9,#0x200 + 0xe3a0_0001, // MOV r0,#1 + 0xe1c1_00b0, // STRH r0,[r1] (IE) + ]); + literal( + &mut code, + &mut patches, + 0, + if mosaic { 0x1844 } else { 0x1804 }, + ); // BG0: char1, map24 + code.push(0xe1c9_00b8); // STRH r0,[r9,#8] + literal( + &mut code, + &mut patches, + 0, + if mosaic { 0xd0c1 } else { 0xd081 }, + ); // BG1: 8bpp, 512x512 + code.extend([ + 0xe1c9_00ba, // STRH r0,[r9,#10] + 0xe289_ac01, // ADD r10,r9,#0x100 + 0xe28a_a030, // ADD r10,r10,#0x30 (KEYINPUT) + 0xe3a0_b402, // MOV r11,#0x02000000 + 0xe3a0_4000, // MOV r4,#0 (scroll x) + 0xe3a0_5000, // MOV r5,#0 (scroll y) + 0xe3a0_7000, // MOV r7,#0 (updates) + if effects { 0xe3a0_0c33 } else { 0xe3a0_0c13 }, // WIN0 for effects; BG0+BG1+OBJ + 0xe380_0040, // ORR r0,r0,#0x40 (1D OBJ mapping) + 0xe1c9_00b0, // STRH r0,[r9] + ]); + let frame = code.len(); + code.extend([ + 0xef05_0000, // VBlankIntrWait + 0xe1da_30b0, // LDRH r3,[r10] + 0xe313_0010, // TST r3,#Right + 0x0284_4002, // ADDEQ r4,r4,#2 + 0xe313_0020, // TST r3,#Left + 0x0244_4002, // SUBEQ r4,r4,#2 + 0xe313_0040, // TST r3,#Up + 0x0245_5002, // SUBEQ r5,r5,#2 + 0xe313_0080, // TST r3,#Down + 0x0285_5002, // ADDEQ r5,r5,#2 + 0xe313_0008, // TST r3,#Start + 0x03a0_4000, // MOVEQ r4,#0 + 0x03a0_5000, // MOVEQ r5,#0 + 0xe1a0_4b84, // MOV r4,r4,LSL #23 + 0xe1a0_4ba4, // MOV r4,r4,LSR #23 (mask to 9 bits) + 0xe1a0_5b85, // MOV r5,r5,LSL #23 + 0xe1a0_5ba5, // MOV r5,r5,LSR #23 + 0xe1c9_41b4, // STRH r4,[r9,#0x14] (BG1HOFS) + 0xe1c9_51b6, // STRH r5,[r9,#0x16] (BG1VOFS) + 0xe1a0_00a4, // MOV r0,r4,LSR #1 + 0xe1a0_10a5, // MOV r1,r5,LSR #1 + 0xe1c9_01b0, // STRH r0,[r9,#0x10] (BG0HOFS, half speed) + 0xe1c9_11b2, // STRH r1,[r9,#0x12] (BG0VOFS) + 0xe3a0_0048, // MOV r0,#72 (regular Y) + 0xe3a0_1901, // MOV r1,#0x4000 (16x16 sprite) + 0xe381_1070, // ORR r1,r1,#112 (regular X) + 0xe313_0001, // TST r3,#A (Z) + if mosaic { 0xe1a0_0000 } else { 0x0381_1a01 }, // Z flips except in mosaic demo + 0xe203_2c03, // AND r2,r3,#0x300 (L/R are active-low) + 0xe352_0c03, // CMP r2,#0x300 + 0x13a0_0c03, // MOVNE r0,#0x300 (affine + double-size) + 0x1380_0040, // ORRNE r0,r0,#64 (32-pixel canvas centered at y=80) + 0x13a0_1901, // MOVNE r1,#0x4000 (16x16 texture, matrix0; no flip) + 0x1381_1068, // ORRNE r1,r1,#104 (32-pixel canvas centered at x=120) + 0xe1c6_00b0, // STRH r0,[r6] (OBJ0 attribute0) + 0xe1c6_10b2, // STRH r1,[r6,#2] (OBJ0 attribute1) + 0xe3a0_0c01, // MOV r0,#256 (PA, identity) + 0xe3a0_1000, // MOV r1,#0 (PB) + 0xe3a0_cc01, // MOV r12,#256 (PD) + 0xe313_0c02, // TST r3,#L (Q: 45-degree clockwise rotation) + 0x03a0_00b5, // MOVEQ r0,#181 + 0x03a0_10b5, // MOVEQ r1,#181 + 0x03a0_c0b5, // MOVEQ r12,#181 + 0xe313_0c01, // TST r3,#R (W: approximately 2x scale) + 0x01a0_00a0, // MOVEQ r0,r0,LSR #1 + 0x01a0_10a1, // MOVEQ r1,r1,LSR #1 + 0x01a0_c0ac, // MOVEQ r12,r12,LSR #1 + 0xe261_2000, // RSB r2,r1,#0 (PC=-PB, after scale quantization) + 0xe1c6_00b6, // STRH r0,[r6,#6] (matrix0 PA) + 0xe1c6_10be, // STRH r1,[r6,#14] (matrix0 PB) + 0xe1c6_21b6, // STRH r2,[r6,#22] (matrix0 PC) + 0xe1c6_c1be, // STRH r12,[r6,#30] (matrix0 PD) + 0xe3a0_1000, // MOV r1,#0 (priority0, tile0) + 0xe313_0002, // TST r3,#B (X) + if mosaic { 0xe1a0_0000 } else { 0x03a0_1b01 }, // X lowers priority except in mosaic demo + 0xe1c6_10b4, // STRH r1,[r6,#4] (OBJ0 attribute2) + 0xe287_7001, // ADD r7,r7,#1 + 0xe58b_7000, // STR r7,[r11] + 0xe58b_4004, // STR r4,[r11,#4] + 0xe58b_5008, // STR r5,[r11,#8] + ]); + if mosaic { + code.extend([ + 0xe1a0_01a7, // MOV r0,r7,LSR #3 (advance size every eight updates) + 0xe200_000f, // AND r0,r0,#15 (encoded size minus one) + 0xe180_0200, // ORR r0,r0,r0,LSL #4 + 0xe180_0400, // ORR r0,r0,r0,LSL #8 (same size for all fields) + 0xe313_0001, // TST r3,#A (Z: BG1x1) + 0x03c0_00ff, // BICEQ r0,r0,#0xff + 0xe313_0002, // TST r3,#B (X: OBJ1x1) + 0x03c0_0cff, // BICEQ r0,r0,#0xff00 + 0xe1c9_04bc, // STRH r0,[r9,#0x4c] (MOSAIC) + 0xe1d6_10b0, // LDRH r1,[r6] (OBJ attribute0) + 0xe381_1a01, // ORR r1,r1,#0x1000 (mosaic, regular or affine) + 0xe1c6_10b0, // STRH r1,[r6] + ]); + } + if effects { + code.extend([ + 0xe284_0040, // ADD r0,r4,#64 (window left, follows horizontal scroll) + 0xe200_00ff, // AND r0,r0,#255 + 0xe280_1070, // ADD r1,r0,#112 (window width) + 0xe201_10ff, // AND r1,r1,#255 + 0xe181_1400, // ORR r1,r1,r0,LSL #8 + 0xe1c9_14b0, // STRH r1,[r9,#0x40] (WIN0H) + 0xe285_0028, // ADD r0,r5,#40 (window top) + 0xe200_00ff, // AND r0,r0,#255 + 0xe280_1050, // ADD r1,r0,#80 (window height) + 0xe201_10ff, // AND r1,r1,#255 + 0xe181_1400, // ORR r1,r1,r0,LSL #8 + 0xe1c9_14b4, // STRH r1,[r9,#0x44] (WIN0V) + 0xe3a0_001f, // MOV r0,#0x1f (all layers, no effects inside) + 0xe1c9_04b8, // STRH r0,[r9,#0x48] (WININ) + 0xe3a0_003f, // MOV r0,#0x3f (all layers and effects outside) + 0xe1c9_04ba, // STRH r0,[r9,#0x4a] (WINOUT) + 0xe3a0_00bf, // MOV r0,#0xbf (brighten all layers) + 0xe313_0002, // TST r3,#B (X: darken) + 0x03a0_00ff, // MOVEQ r0,#0xff + 0xe313_0001, // TST r3,#A (Z: alpha BG0/OBJ over BG1) + 0x03a0_0c02, // MOVEQ r0,#0x200 + 0x0380_0051, // ORREQ r0,r0,#0x51 + 0xe1c9_05b0, // STRH r0,[r9,#0x50] (BLDCNT) + 0xe3a0_0c08, // MOV r0,#0x800 + 0xe380_0008, // ORR r0,r0,#8 + 0xe1c9_05b2, // STRH r0,[r9,#0x52] (BLDALPHA: half of each target) + 0xe3a0_0008, // MOV r0,#8 + 0xe1c9_05b4, // STRH r0,[r9,#0x54] (BLDY: half brightness change) + ]); + } + let offset = frame as i32 - code.len() as i32 - 2; + code.push(0xea00_0000 | (offset as u32 & 0x00ff_ffff)); + let irq_address = ROM_START + code.len() as u32 * 4; + code.extend([ + 0xe280_0c02, // ADD r0,r0,#0x200 (BIOS supplies I/O base) + 0xe1d0_10b2, // LDRH r1,[r0,#2] + 0xe1c0_10b2, // STRH r1,[r0,#2] (ack IF) + 0xe3a0_0403, // MOV r0,#0x03000000 + 0xe280_0c7f, // ADD r0,r0,#0x7f00 + 0xe1d0_2fb8, // LDRH r2,[r0,#0xf8] + 0xe182_2001, // ORR r2,r2,r1 + 0xe1c0_2fb8, // STRH r2,[r0,#0xf8] (BIOS flags) + 0xe12f_ff1e, // BX lr + ]); + let palette_address = ROM_START + (code.len() + patches.len()) as u32 * 4; + for (index, reg, mut value) in patches { + if index == palette_load { + value = palette_address; + } + if index == vram_load { + value = palette_address + 1024; + } + if index == oam_load { + value = palette_address + 1024 + 0x18000; + } + if index == irq_load { + value = irq_address; + } + let offset = (code.len() - index - 2) * 4; + assert!(offset < 4096); + code[index] = 0xe59f_0000 | (reg << 12) | offset as u32; + code.push(value); + } + let mut rom: Vec = code.into_iter().flat_map(u32::to_le_bytes).collect(); + let mut palette = [0_u16; 512]; + palette[0] = 0x4000; + palette[1] = 0x0260; // Grass + palette[2] = 0x03a0; // Light grass + palette[3] = 0x7d20; // Water + palette[4] = 0x7e80; // Light water + palette[17] = 0x03ff; // Foreground gold (bank1) + palette[257] = 0x001f; // Sprite red + palette[258] = 0x7fff; // Sprite white + palette[259] = 0; // Sprite opaque black eye + palette[260] = 0x03ff; // Sprite gold arm + rom.extend(palette.into_iter().flat_map(u16::to_le_bytes)); + let mut vram = vec![0_u8; 0x18000]; + for tile in 0..2 { + for y in 0..8 { + for x in 0..8 { + vram[tile * 64 + y * 8 + x] = (tile * 2 + 1 + (x / 2 + y / 2) % 2) as u8; + } + } + } + // Sparse foreground crosses. Index zero leaves the terrain visible. + for y in 0..8 { + for x in 0..8 { + if x == 3 || y == 3 { + vram[0x4020 + y * 4 + x / 2] |= 1 << ((x % 2) * 4); + } + } + } + for y in 0..64 { + for x in 0..64 { + let block = x / 32 + y / 32 * 2; + let address = 0x8000 + block * 0x800 + (y % 32 * 32 + x % 32) * 2; + let entry = ((x / 4 + y / 4) % 2) as u16; + vram[address..address + 2].copy_from_slice(&entry.to_le_bytes()); + } + } + for y in 0..32 { + for x in 0..32 { + let entry = if x % 4 == 1 && y % 4 == 1 { + 0x1001_u16 + } else { + 0 + }; + let address = 0xc000 + (y * 32 + x) * 2; + vram[address..address + 2].copy_from_slice(&entry.to_le_bytes()); + } + } + // Original 16x16 character, stored as four consecutive 4bpp tiles. + for y in 1..15 { + for x in 2..14 { + let color = if x == 10 && y == 7 { + 3 + } else if x == 12 && (10..13).contains(&y) { + 4 + } else if (5..10).contains(&y) { + 2 + } else { + 1 + }; + let tile = y / 8 * 2 + x / 8; + let address = 0x10000 + tile * 32 + (y % 8) * 4 + (x % 8) / 2; + vram[address] |= color << ((x & 1) * 4); + } + } + rom.extend(vram); + let mut oam = [0_u16; 512]; + for object in 0..128 { + oam[object * 4] = 0x200; + } + oam[0] = 72; // Y + oam[1] = 0x4070; // 16x16, X=112 + oam[2] = 0; // Tile0, priority0, palette0 + rom.extend(oam.into_iter().flat_map(u16::to_le_bytes)); + rom +} diff --git a/tests/graphics.rs b/tests/graphics.rs new file mode 100644 index 0000000..09975ae --- /dev/null +++ b/tests/graphics.rs @@ -0,0 +1,297 @@ +use gba_rust::{ + graphics_demo::{GraphicsDemo, BACKGROUND, DEMO_STATE, SQUARE_SIZE}, + input::{Button, Buttons}, + io::{BG2PA, BG2PD, DISPCNT, GREENSWAP, KEYINPUT}, + memory::{Memory, MemoryError, PALETTE_START, VRAM_START}, + timing::{bus_cycles, AccessKind, AccessWidth}, + video::{rgb555_to_rgb888, Framebuffer, VideoError, HEIGHT, WIDTH}, +}; + +fn mode3() -> Memory { + let mut memory = Memory::new(vec![]).unwrap(); + memory.write16(DISPCNT, 0x403).unwrap(); + memory.write16(BG2PA, 256).unwrap(); + memory.write16(BG2PD, 256).unwrap(); + memory +} + +fn assert_demo_frame(demo: &GraphicsDemo, frame: &Framebuffer, x: usize, y: usize, color: u16) { + let memory = demo.machine().memory(); + assert_eq!(memory.read32(DEMO_STATE + 4).unwrap(), x as u32); + assert_eq!(memory.read32(DEMO_STATE + 8).unwrap(), y as u32); + assert_eq!(memory.read32(DEMO_STATE + 12).unwrap(), u32::from(color)); + assert_eq!(memory.read16(DISPCNT).unwrap(), 0x403); + for py in 0..HEIGHT { + for px in 0..WIDTH { + let expected = + if (x..x + SQUARE_SIZE).contains(&px) && (y..y + SQUARE_SIZE).contains(&py) { + color + } else { + BACKGROUND + }; + assert_eq!( + frame.pixels()[py * WIDTH + px], + rgb555_to_rgb888(expected), + "pixel {px},{py}" + ); + } + } +} + +#[test] +fn cpu_program_draws_mode3_and_moves_from_keyinput_without_host_vram_writes() { + let mut demo = GraphicsDemo::new().unwrap(); + let mut frame = Framebuffer::default(); + assert_eq!(demo.machine().memory().read16(DISPCNT).unwrap(), 0); + assert_eq!(demo.machine().memory().read16(VRAM_START).unwrap(), 0); + let startup_steps = demo.frame(Buttons::default(), &mut frame).unwrap(); + assert!(startup_steps > WIDTH * HEIGHT); + assert_demo_frame(&demo, &frame, 112, 72, 0x7fff); + let previous = demo.machine().memory().display_position(); + let previous_cycles = demo.machine().cycles(); + assert_eq!(previous.scanline, 160); + let steps = demo + .frame( + Buttons::default() + .with(Button::Right, true) + .with(Button::Up, true), + &mut frame, + ) + .unwrap(); + assert!(steps < startup_steps); + let position = demo.machine().memory().display_position(); + assert_eq!(position.scanline, 160); + assert_eq!(position.vblanks, previous.vblanks + 1); + assert_eq!(position.frames, previous.frames + 1); + assert_eq!( + demo.machine().cycles() - previous_cycles, + u64::from(gba_rust::display::CYCLES_PER_FRAME) + u64::from(position.line_cycle) + - u64::from(previous.line_cycle) + ); + assert_demo_frame(&demo, &frame, 114, 70, 0x7fff); + assert_eq!(demo.machine().memory().read32(DEMO_STATE).unwrap(), 2); +} + +#[test] +fn cpu_program_clamps_edges_cancels_opposites_changes_color_and_resets() { + let mut demo = GraphicsDemo::new().unwrap(); + let mut frame = Framebuffer::default(); + let upper_left = Buttons::default() + .with(Button::Left, true) + .with(Button::Up, true); + for _ in 0..65 { + demo.frame(upper_left, &mut frame).unwrap(); + } + assert_demo_frame(&demo, &frame, 0, 0, 0x7fff); + let all_directions = Buttons::from_bits(0xf0); + demo.frame(all_directions, &mut frame).unwrap(); + assert_demo_frame(&demo, &frame, 0, 0, 0x7fff); + let lower_right = Buttons::default() + .with(Button::Right, true) + .with(Button::Down, true); + for _ in 0..120 { + demo.frame(lower_right, &mut frame).unwrap(); + } + assert_demo_frame(&demo, &frame, 224, 144, 0x7fff); + demo.frame(all_directions, &mut frame).unwrap(); + assert_demo_frame(&demo, &frame, 224, 144, 0x7fff); + for (buttons, color) in [ + (Buttons::from_bits(2), 0x3e0), + (Buttons::from_bits(3), 0x1f), + (Buttons::default(), 0x7fff), + ] { + demo.frame(buttons, &mut frame).unwrap(); + assert_demo_frame(&demo, &frame, 224, 144, color); + } + demo.frame(Buttons::default().with(Button::Start, true), &mut frame) + .unwrap(); + assert_demo_frame(&demo, &frame, 112, 72, 0x7fff); +} + +#[test] +fn mode3_renders_row_major_colors_and_ignores_page_and_pixel_top_bit() { + let mut memory = mode3(); + let mut frame = Framebuffer::default(); + for (index, color) in [ + (0, 0x801f), + (1, 0x03e0), + (WIDTH, 0x7c00), + (WIDTH * HEIGHT - 1, 0x7fff), + ] { + memory + .write16(VRAM_START + index as u32 * 2, color) + .unwrap(); + } + memory.write16(DISPCNT, 0x413).unwrap(); // Page bit has no effect in Mode 3. + memory.render_frame(&mut frame).unwrap(); + assert_eq!(frame.pixels()[0], 0xff0000); + assert_eq!(frame.pixels()[1], 0x00ff00); + assert_eq!(frame.pixels()[WIDTH], 0x0000ff); + assert_eq!(frame.pixels()[WIDTH * HEIGHT - 1], 0xffffff); + assert_eq!(frame.pixels()[2], 0); + assert_eq!(memory.cycles(), 0); +} + +#[test] +fn forced_blank_is_white_and_disabled_bg2_uses_palette_backdrop() { + let mut memory = mode3(); + let mut frame = Framebuffer::default(); + memory.write16(PALETTE_START, 0x3e0).unwrap(); + memory.write16(DISPCNT, 3).unwrap(); + memory.render_frame(&mut frame).unwrap(); + assert!(frame.pixels().iter().all(|&pixel| pixel == 0x00ff00)); + memory.write16(DISPCNT, 0x403).unwrap(); + memory.render_frame(&mut frame).unwrap(); + assert!(frame.pixels().iter().all(|&pixel| pixel == 0)); // Mode 3 black is opaque. + memory.write16(DISPCNT, 0xffff).unwrap(); + memory.render_frame(&mut frame).unwrap(); + assert!(frame.pixels().iter().all(|&pixel| pixel == 0xffffff)); +} + +#[test] +fn unsupported_modes_and_layers_report_errors_without_changing_output() { + let mut memory = mode3(); + let mut frame = Framebuffer::default(); + frame.clear(0x001f); + for mode in [6, 7] { + memory.write16(DISPCNT, 0x400 | mode).unwrap(); + assert_eq!( + memory.render_frame(&mut frame), + Err(VideoError::UnsupportedMode(mode as u8)) + ); + } + for layer in [0x100, 0x200, 0x800] { + memory.write16(DISPCNT, 0x403 | layer).unwrap(); + assert_eq!( + memory.render_frame(&mut frame), + Err(VideoError::UnsupportedLayers(0x403 | layer)) + ); + } + assert!(frame.pixels().iter().all(|&pixel| pixel == 0xff0000)); +} + +#[test] +fn display_control_masks_cgb_bit_and_green_swap_exchanges_only_green() { + let mut memory = mode3(); + memory.write32(DISPCNT, u32::MAX).unwrap(); + assert_eq!(memory.read32(DISPCNT).unwrap(), 0x0001_fff7); + memory.write8(DISPCNT, 3).unwrap(); + memory.write8(DISPCNT + 1, 4).unwrap(); + memory.write8(GREENSWAP + 1, 0xff).unwrap(); + assert_eq!(memory.read32(DISPCNT).unwrap(), 0x0001_0403); + memory.write16(VRAM_START, 0x001f).unwrap(); + memory.write16(VRAM_START + 2, 0x7fe0).unwrap(); // Green + blue + let mut frame = Framebuffer::default(); + memory.render_frame(&mut frame).unwrap(); + assert_eq!(frame.pixels()[0], 0xffff00); + assert_eq!(frame.pixels()[1], 0x0000ff); + memory.write8(GREENSWAP, 0).unwrap(); + memory.render_frame(&mut frame).unwrap(); + assert_eq!(frame.pixels()[0], 0xff0000); + assert_eq!(frame.pixels()[1], 0x00ffff); +} + +#[test] +fn palette_and_vram_mirrors_preserve_word_and_halfword_layouts() { + let mut memory = mode3(); + memory.write32(PALETTE_START + 0x400, 0x1234_5678).unwrap(); + assert_eq!(memory.read32(PALETTE_START).unwrap(), 0x1234_5678); + memory.write16(0x05ff_fffe, 0xabcd).unwrap(); + assert_eq!(memory.read16(PALETTE_START + 0x3fe).unwrap(), 0xabcd); + for (mirror, physical) in [ + (0x0602_0000, VRAM_START), + (0x0601_8000, VRAM_START + 0x10000), + (0x06ff_fffc, VRAM_START + 0x17ffc), + ] { + memory.write32(mirror, 0x1234_5678).unwrap(); + assert_eq!(memory.read32(physical).unwrap(), 0x1234_5678); + assert_eq!(memory.read8(mirror).unwrap(), 0x78); + assert_eq!(memory.read16(mirror + 2).unwrap(), 0x1234); + } + memory.write32(VRAM_START + 0x17ffc, 0xabcd_1234).unwrap(); + assert_eq!(memory.read32(VRAM_START + 0x1fffc).unwrap(), 0xabcd_1234); + assert_eq!(memory.read32(0x0700_0000).unwrap(), 0); // OAM starts cleared. +} + +#[test] +fn video_byte_writes_duplicate_bg_and_palette_but_ignore_obj_area() { + let mut memory = mode3(); + for address in [ + PALETTE_START, + PALETTE_START + 0x3ff, + VRAM_START, + VRAM_START + 1, + VRAM_START + 0x13fff, + ] { + memory.write8(address, 0x5a).unwrap(); + assert_eq!(memory.read16(address & !1).unwrap(), 0x5a5a); + } + for address in [ + VRAM_START + 0x14000, + VRAM_START + 0x17fff, + VRAM_START + 0x1ffff, + ] { + memory.write16(address & !1, 0x1234).unwrap(); + memory.write8(address, 0xff).unwrap(); + assert_eq!(memory.read16(address & !1).unwrap(), 0x1234); + } + memory.write16(DISPCNT, 0).unwrap(); // Tile-mode OBJ area starts at 64 KiB. + memory.write16(VRAM_START + 0x10000, 0xabcd).unwrap(); + memory.write8(VRAM_START + 0x18000, 0).unwrap(); + assert_eq!(memory.read16(VRAM_START + 0x10000).unwrap(), 0xabcd); +} + +#[test] +fn misaligned_video_writes_leave_all_bytes_unchanged() { + let mut memory = mode3(); + for address in [PALETTE_START, VRAM_START] { + memory.write32(address, 0x1234_5678).unwrap(); + assert_eq!( + memory.write16(address + 1, 0), + Err(MemoryError::Unaligned(address + 1)) + ); + assert_eq!( + memory.write32(address + 2, 0), + Err(MemoryError::Unaligned(address + 2)) + ); + assert_eq!(memory.read32(address).unwrap(), 0x1234_5678); + } +} + +#[test] +fn keyinput_exposes_all_ten_buttons_active_low_and_ignores_writes() { + let mut memory = mode3(); + assert_eq!(memory.read16(KEYINPUT).unwrap(), 0x3ff); + for bits in 0..=0x3ff { + memory.set_buttons(Buttons::from_bits(bits)); + assert_eq!(memory.read16(KEYINPUT).unwrap(), !bits & 0x3ff); + memory.write16(KEYINPUT, 0).unwrap(); + assert_eq!(memory.read16(KEYINPUT).unwrap(), !bits & 0x3ff); + } + let buttons = Buttons::from_bits(0xffff) + .with(Button::A, false) + .with(Button::L, false); + assert_eq!(buttons.bits(), 0x1fe); + memory.set_buttons(buttons); + memory.write8(KEYINPUT + 1, 0).unwrap(); + assert_eq!(memory.read8(KEYINPUT).unwrap(), 1); + assert_eq!(memory.read8(KEYINPUT + 1).unwrap(), 2); + memory.set_buttons(Buttons::default()); + assert_eq!(memory.read16(KEYINPUT).unwrap(), 0x3ff); + assert_eq!(memory.cycles(), 0); + assert_eq!( + memory.read16(KEYINPUT + 2), + Err(MemoryError::Unmapped(KEYINPUT + 2)) + ); // KEYCNT deferred. +} + +#[test] +fn video_bus_widths_have_nominal_16_bit_bus_costs() { + for address in [PALETTE_START, VRAM_START, 0x06ff_fffc] { + for kind in [AccessKind::Sequential, AccessKind::NonSequential] { + assert_eq!(bus_cycles(0, address, AccessWidth::Byte, kind), 1); + assert_eq!(bus_cycles(0, address, AccessWidth::Halfword, kind), 1); + assert_eq!(bus_cycles(0, address, AccessWidth::Word, kind), 2); + } + } +}