feat(demo): run CPU-driven bitmap tile and sprite scenes
Add original ARM graphics programs, input controls, DMA initialization, and a bounded frame runner.
This commit is contained in:
parent
0280563430
commit
169fab9bf6
3 changed files with 1143 additions and 0 deletions
499
src/graphics_demo.rs
Normal file
499
src/graphics_demo.rs
Normal file
|
|
@ -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<MachineError> 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, MemoryError> {
|
||||
Self::with_program(program())
|
||||
}
|
||||
|
||||
pub(crate) fn with_program(rom: Vec<u8>) -> Result<Self, MemoryError> {
|
||||
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<usize, GraphicsError> {
|
||||
self.frame_with_control(buttons, output, 0x403)
|
||||
}
|
||||
|
||||
pub(crate) fn frame_with_control(
|
||||
&mut self,
|
||||
buttons: Buttons,
|
||||
output: &mut Framebuffer,
|
||||
ready_control: u16,
|
||||
) -> Result<usize, GraphicsError> {
|
||||
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<usize, GraphicsError> {
|
||||
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<u8> {
|
||||
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<u32>, 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));
|
||||
}
|
||||
}
|
||||
347
src/tile_demo.rs
Normal file
347
src/tile_demo.rs
Normal file
|
|
@ -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<Self, MemoryError> {
|
||||
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<usize, GraphicsError> {
|
||||
self.runner.frame_with_control(buttons, output, 0x1340)
|
||||
}
|
||||
}
|
||||
|
||||
fn literal(code: &mut Vec<u32>, 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<u8> {
|
||||
program(Scene::Effects)
|
||||
}
|
||||
|
||||
pub(crate) fn mosaic_program() -> Vec<u8> {
|
||||
program(Scene::Mosaic)
|
||||
}
|
||||
|
||||
fn program(scene: Scene) -> Vec<u8> {
|
||||
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<u8> = 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
|
||||
}
|
||||
297
tests/graphics.rs
Normal file
297
tests/graphics.rs
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue