refactor: split into gameboy-rust workspace with core, demos, and desktop crates

Move the emulator into crates/gba-core, the original demo programs into
crates/gba-demos, and the minifb frontend into crates/desktop. The binary
is now gameboy-rust. Imports change from gba_rust to gba_core/gba_demos;
no behavior changes.

gba-core has no dependencies. Its integration tests use gba-demos as a
development dependency.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
0xrsydn 2026-10-05 16:07:32 +07:00
commit f8a0676748
113 changed files with 304 additions and 282 deletions

View file

@ -0,0 +1,8 @@
[package]
name = "gba-demos"
version.workspace = true
edition.workspace = true
publish.workspace = true
[dependencies]
gba-core = { path = "../gba-core" }

View file

@ -0,0 +1,242 @@
//! Original ARM Mode 2 program. The CPU copies assets through BIOS services
//! and writes affine coefficients/reference points during VBlank.
//! The bitmap demos reuse this instruction builder with different assets and page control.
use crate::graphics_demo::{GraphicsDemo, GraphicsError};
use gba_core::{
input::Buttons,
machine::Machine,
memory::{MemoryError, ROM_START},
video::Framebuffer,
};
/// Debug words: completed updates, signed pan X, signed pan Y.
pub const AFFINE_STATE: u32 = 0x0200_0000;
pub struct AffineDemo {
runner: GraphicsDemo,
}
impl AffineDemo {
pub fn new() -> Result<Self, MemoryError> {
Ok(Self {
runner: GraphicsDemo::with_program(program(2, false))?,
})
}
pub fn machine(&self) -> &Machine {
self.runner.machine()
}
pub fn frame(
&mut self,
buttons: Buttons,
output: &mut Framebuffer,
) -> Result<usize, GraphicsError> {
self.runner.frame_with_control(buttons, output, 0x402)
}
}
pub(crate) fn bitmap_program(mode: u16) -> Vec<u8> {
assert!(matches!(mode, 4 | 5));
program(mode, false)
}
pub(crate) fn raster_program() -> Vec<u8> {
program(2, true)
}
fn program(mode: u16, raster: bool) -> Vec<u8> {
let bitmap = mode != 2;
let mut code = vec![
0xe3a0_9301, // MOV r9,#0x04000000
0xe3a0_0080, // MOV r0,#128 (forced blank)
0xe1c9_00b0, // STRH r0,[r9]
0, // LDR r0,=palette
0xe3a0_1405, // MOV r1,#0x05000000
0xe3a0_2c01, // MOV r2,#256 halfwords
0xef0b_0000, // CpuSet
0, // LDR r0,=VRAM image
0xe3a0_1406, // MOV r1,#0x06000000
if bitmap { 0xe3a0_2a05 } else { 0xe3a0_2901 }, // MOV r2,#20480 or #16384 words
0xef0c_0000, // CpuFastSet
0, // LDR r0,=IRQ callback
0xe3a0_1403, // MOV r1,#0x03000000
0xe281_1c7f, // ADD r1,r1,#0x7f00
0xe581_00fc, // STR r0,[r1,#252]
0xe3a0_0008, // MOV r0,#8
0xe1c9_00b4, // STRH r0,[r9,#4] (VBlank request)
0xe289_1c02, // ADD r1,r9,#0x200
0xe3a0_0001, // MOV r0,#1
0xe1c1_00b0, // STRH r0,[r1] (IE)
0xe289_ac01, // ADD r10,r9,#0x100
0xe28a_a030, // ADD r10,r10,#0x30 (KEYINPUT)
0xe289_6020, // ADD r6,r9,#0x20 (BG2 affine registers)
0xe3a0_b402, // MOV r11,#0x02000000
0xe3a0_4000, // MOV r4,#0 (signed pan X)
0xe3a0_5000, // MOV r5,#0 (signed pan Y)
0xe3a0_7000, // MOV r7,#0 (updates)
0xe3a0_0b01, // MOV r0,#0x400
0xe380_0000 | u32::from(mode), // ORR r0,r0,#mode (BG2 enabled)
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
if bitmap { 0xe3a0_0000 } else { 0xe3a0_0a07 }, // MOV r0,#0 or #0x7000 (tile wrap)
0xe313_0001, // TST r3,#A (Z: disable tile wrapping; bitmap page handled below)
if raster { 0xe1a0_0000 } else { 0x03c0_0a02 }, // Raster Z bypasses distortion, not wrapping.
0xe1c9_00bc, // STRH r0,[r9,#12] (BG2CNT)
0xe3a0_0c01, // MOV r0,#256 (PA)
0xe3a0_1000, // MOV r1,#0 (PB)
0xe3a0_cc01, // MOV r12,#256 (PD)
0xe313_0c02, // TST r3,#L (Q: 45-degree rotation)
0x03a0_00b5, // MOVEQ r0,#181
0x03a0_10b5, // MOVEQ r1,#181
0x03a0_c0b5, // MOVEQ r12,#181
0xe313_0c01, // TST r3,#R (W: 2x zoom)
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)
0xe1c6_00b0, // STRH r0,[r6] (PA)
0xe1c6_10b2, // STRH r1,[r6,#2] (PB)
0xe1c6_20b4, // STRH r2,[r6,#4] (PC)
0xe1c6_c0b6, // STRH r12,[r6,#6] (PD)
// Reference = (map center + pan)*256 - matrix*(screen center).
0xe3a0_8078, // MOV r8,#120
0xe000_0098, // MUL r0,r8,r0 (120*PA)
0xe3a0_8050, // MOV r8,#80
0xe020_0198, // MLA r0,r8,r1,r0 (+80*PB)
0xe260_0000, // RSB r0,r0,#0
0xe080_0404, // ADD r0,r0,r4,LSL #8
match mode {
4 => 0xe280_0c78,
5 => 0xe280_0a05,
_ => 0xe280_0902,
}, // ADD map center X * 256
0xe586_0008, // STR r0,[r6,#8] (BG2X)
0xe3a0_8078, // MOV r8,#120
0xe002_0298, // MUL r2,r8,r2 (120*PC)
0xe3a0_8050, // MOV r8,#80
0xe022_2c98, // MLA r2,r8,r12,r2 (+80*PD)
0xe262_2000, // RSB r2,r2,#0
0xe082_2405, // ADD r2,r2,r5,LSL #8
match mode {
4 => 0xe282_2c50,
5 => 0xe282_2901,
_ => 0xe282_2902,
}, // ADD map center Y * 256
0xe586_200c, // STR r2,[r6,#12] (BG2Y)
0xe287_7001, // ADD r7,r7,#1
]);
if bitmap {
code.extend([
0xe3a0_0b01, // MOV r0,#0x400
0xe380_0000 | u32::from(mode), // ORR r0,r0,#mode
0xe317_0020, // TST r7,#32 (alternate pages every32 updates)
0x1380_0010, // ORRNE r0,r0,#16
0xe313_0001, // TST r3,#A (Z: force page1)
0x0380_0010, // ORREQ r0,r0,#16
0xe1c9_00b0, // STRH r0,[r9] (page switch during VBlank)
]);
}
let mut raster_dma_load = None;
if raster {
code.extend([
0xe3a0_2402, // MOV r2,#0x02000000
0xe282_2a01, // ADD r2,r2,#0x1000 (PB table)
0xe3a0_0000, // MOV r0,#0 (table row)
]);
let row = code.len();
code.extend([
0xe310_0010, // TST r0,#16
0x0281_8c02, // ADDEQ r8,r1,#512 (base PB + two pixels)
0x1241_8c02, // SUBNE r8,r1,#512 (base PB - two pixels)
0xe313_0001, // TST r3,#A (Z: bypass distortion)
0x01a0_8001, // MOVEQ r8,r1
0xe0c2_80b2, // STRH r8,[r2],#2
0xe280_0001, // ADD r0,r0,#1
0xe350_00a0, // CMP r0,#160
]);
let displacement = row as i32 - code.len() as i32 - 2;
code.push(0x1a00_0000 | (displacement as u32 & 0x00ff_ffff));
code.extend([
0xe3a0_0000, // MOV r0,#0
0xe1c9_0bba, // STRH r0,[r9,#0xba] (disable DMA0)
0xe3a0_0402, // MOV r0,#0x02000000
0xe280_0a01, // ADD r0,r0,#0x1000
0xe589_00b0, // STR r0,[r9,#0xb0] (source)
0xe289_0022, // ADD r0,r9,#0x22 (BG2PB)
0xe589_00b4, // STR r0,[r9,#0xb4] (fixed destination)
]);
raster_dma_load = Some(code.len());
code.push(0); // LDR r0,=DMA repeat/HBlank configuration
code.push(0xe589_00b8); // STR r0,[r9,#0xb8] (arm DMA0)
}
code.extend([
0xe58b_7000, // STR r7,[r11]
0xe58b_4004, // STR r4,[r11,#4]
0xe58b_5008, // STR r5,[r11,#8]
]);
let offset = frame as i32 - code.len() as i32 - 2;
code.push(0xea00_0000 | (offset as u32 & 0xffffff));
let irq = 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,#248]
0xe182_2001, // ORR r2,r2,r1
0xe1c0_2fb8, // STRH r2,[r0,#248] (BIOS flags)
0xe12f_ff1e, // BX lr
]);
let palette_address = ROM_START + (code.len() + 3 + usize::from(raster)) as u32 * 4;
let mut literals = vec![(3, palette_address), (7, palette_address + 512), (11, irq)];
if let Some(index) = raster_dma_load {
literals.push((index, 0xa240_0001));
}
for (index, value) in literals {
let offset = (code.len() - index - 2) * 4;
assert!(offset < 4096);
code[index] = 0xe59f_0000 | offset as u32;
code.push(value);
}
let mut rom: Vec<u8> = code.into_iter().flat_map(u32::to_le_bytes).collect();
if bitmap {
rom.extend(crate::bitmap_demo::assets(mode));
return rom;
}
let mut palette = [0_u16; 256];
palette[..9].copy_from_slice(&[
0x4000, 0x0260, 0x03a0, 0x7d20, 0x7e80, 0x001f, 0x421f, 0x03ff, 0x7fff,
]);
rom.extend(palette.into_iter().flat_map(u16::to_le_bytes));
let mut vram = vec![0_u8; 0x10000];
for t in 0..4 {
for y in 0..8 {
for x in 0..8 {
vram[t * 64 + y * 8 + x] = (t * 2 + 1 + (x / 2 + y / 2) % 2) as u8;
}
}
}
for y in 0..32 {
for x in 0..32 {
vram[0x8000 + y * 32 + x] = ((x / 4 + 2 * (y / 4)) % 4) as u8;
}
}
rom.extend(vram);
rom
}

View file

@ -0,0 +1,35 @@
//! CPU-driven affine raster distortion. HBlank DMA changes BG2PB for the next
//! line transition; internal origins accumulate the resulting horizontal shifts.
use crate::graphics_demo::{GraphicsDemo, GraphicsError};
use gba_core::{input::Buttons, machine::Machine, memory::MemoryError, video::Framebuffer};
/// Completed updates and signed pan X/Y, as three debug words.
pub const AFFINE_RASTER_STATE: u32 = crate::affine_demo::AFFINE_STATE;
/// 160 halfwords generated by ARM code during each VBlank.
pub const AFFINE_RASTER_TABLE: u32 = 0x0200_1000;
pub struct AffineRasterDemo {
runner: GraphicsDemo,
}
impl AffineRasterDemo {
pub fn new() -> Result<Self, MemoryError> {
Ok(Self {
runner: GraphicsDemo::with_program(crate::affine_demo::raster_program())?,
})
}
pub fn machine(&self) -> &Machine {
self.runner.machine()
}
/// Arrows pan; Q rotates; W zooms; Z bypasses raster distortion; Enter resets pan.
pub fn frame(
&mut self,
buttons: Buttons,
output: &mut Framebuffer,
) -> Result<usize, GraphicsError> {
self.runner.frame_with_control(buttons, output, 0x402)
}
}

View file

@ -0,0 +1,72 @@
//! Original Mode 4/5 page-flipping demos. ARM code copies both images through
//! BIOS services, samples input, and switches the displayed page during VBlank.
use crate::{
affine_demo,
graphics_demo::{GraphicsDemo, GraphicsError},
};
use gba_core::{input::Buttons, machine::Machine, memory::MemoryError, video::Framebuffer};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u16)]
pub enum BitmapMode {
Mode4 = 4,
Mode5 = 5,
}
/// Debug words: completed updates, signed pan X, signed pan Y.
pub const BITMAP_STATE: u32 = 0x0200_0000;
pub struct BitmapDemo {
runner: GraphicsDemo,
mode: BitmapMode,
}
impl BitmapDemo {
pub fn new(mode: BitmapMode) -> Result<Self, MemoryError> {
Ok(Self {
runner: GraphicsDemo::with_program(affine_demo::bitmap_program(mode as u16))?,
mode,
})
}
pub fn machine(&self) -> &Machine {
self.runner.machine()
}
pub fn frame(
&mut self,
buttons: Buttons,
output: &mut Framebuffer,
) -> Result<usize, GraphicsError> {
self.runner
.frame_with_control(buttons, output, 0x400 | self.mode as u16)
}
}
pub(crate) fn assets(mode: u16) -> Vec<u8> {
let mut palette = [0_u16; 256];
palette[0] = 0x4000;
for (i, color) in palette.iter_mut().enumerate().skip(1) {
*color = ((i & 31) | (((i >> 3) & 31) << 5) | (((i * 3) & 31) << 10)) as u16;
}
let mut data: Vec<u8> = palette.into_iter().flat_map(u16::to_le_bytes).collect();
let mut vram = vec![0_u8; 0x14000];
let (width, height) = if mode == 4 { (240, 160) } else { (160, 128) };
for page in 0..2 {
for y in 0..height {
for x in 0..width {
let offset = y * width + x;
if mode == 4 {
vram[page * 0xa000 + offset] = ((x / 8 + 3 * (y / 8) + page * 64) & 255) as u8;
} else {
let color = (((x / 5 + page * 11) & 31)
| (((y / 4 + page * 7) & 31) << 5)
| (((x / 8 + y / 8 + page * 5) & 31) << 10))
as u16;
let address = page * 0xa000 + offset * 2;
vram[address..address + 2].copy_from_slice(&color.to_le_bytes());
}
}
}
}
data.extend(vram);
data
}

View file

@ -0,0 +1,35 @@
//! Original CPU-driven window and color-effects scene, based on the tile demo.
//! The CPU writes window bounds and effect registers during each VBlank.
use crate::graphics_demo::{GraphicsDemo, GraphicsError};
use gba_core::{input::Buttons, machine::Machine, memory::MemoryError, video::Framebuffer};
/// Completed updates and nine-bit horizontal/vertical scroll, as three words.
pub const EFFECTS_STATE: u32 = crate::tile_demo::TILE_STATE;
pub struct EffectsDemo {
runner: GraphicsDemo,
}
impl EffectsDemo {
pub fn new() -> Result<Self, MemoryError> {
Ok(Self {
runner: GraphicsDemo::with_program(crate::tile_demo::effects_program())?,
})
}
pub fn machine(&self) -> &Machine {
self.runner.machine()
}
/// Arrows scroll and move WIN0. Z selects alpha; X selects darkening.
/// Otherwise the area outside WIN0 is brightened. Enter resets scrolling.
/// Q/W retain the tile demo's sprite rotation and zoom controls.
pub fn frame(
&mut self,
buttons: Buttons,
output: &mut Framebuffer,
) -> Result<usize, GraphicsError> {
self.runner.frame_with_control(buttons, output, 0x3340)
}
}

View 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 gba_core::{
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 gba_core::{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 gba_core::{
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 gba_core::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 gba_core::{
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));
}
}

View file

@ -0,0 +1,93 @@
//! Original GBA test programs that drive `gba-core`. No game or Nintendo code.
pub mod affine_demo;
pub mod affine_raster_demo;
pub mod bitmap_demo;
pub mod effects_demo;
pub mod graphics_demo;
pub mod mosaic_demo;
pub mod raster_demo;
pub mod tile_demo;
pub mod timer_demo;
/// Steps needed to reach and execute the demo's final self-branch.
pub const DEMO_STEPS: usize = 40;
/// Steps needed to execute both software interrupts and the final self-branch.
pub const EXCEPTION_DEMO_STEPS: usize = 11;
/// Original test image, not a Nintendo BIOS or a BIOS-service replacement.
/// The SWI vector branches to a handler that increments r10 and returns.
pub fn demo_bios() -> Vec<u8> {
let mut bytes = vec![0; gba_core::memory::BIOS_SIZE];
bytes[8..12].copy_from_slice(&0xea00_000c_u32.to_le_bytes()); // B 0x40
bytes[0x40..0x44].copy_from_slice(&0xe28a_a001_u32.to_le_bytes()); // ADD r10, r10, #1
bytes[0x44..0x48].copy_from_slice(&0xe1b0_f00e_u32.to_le_bytes()); // MOVS pc, lr
bytes
}
/// Original ARM and Thumb software-interrupt test code.
pub fn exception_demo_program() -> Vec<u8> {
let mut bytes: Vec<u8> = [
0xef00_0001_u32, // ARM SWI #1
0xe28f_0001, // ADD r0, pc, #1 (Thumb offset 12, bit zero set)
0xe12f_ff10, // BX r0
]
.into_iter()
.flat_map(u32::to_le_bytes)
.collect();
bytes.extend(0xdf02_u16.to_le_bytes()); // Thumb SWI #2
bytes.extend(0xe7fe_u16.to_le_bytes()); // B .
bytes
}
/// Original ARM/Thumb, RAM, stack, and swap test code, not a Nintendo ROM.
pub fn demo_program() -> Vec<u8> {
let mut bytes: Vec<u8> = [
0xe3a0_0003_u32, // MOV r0, #3
0xe3a0_1000, // MOV r1, #0
0xe281_1001, // loop: ADD r1, r1, #1
0xe250_0001, // SUBS r0, r0, #1
0x1aff_fffc, // BNE loop
0xe351_0003, // CMP r1, #3
0x03a0_202a, // MOVEQ r2, #42
0xe3a0_3402, // MOV r3, #0x02000000 (work RAM)
0xe583_2000, // STR r2, [r3]
0xe593_4000, // LDR r4, [r3]
0xe3a0_d403, // MOV sp, #0x03000000 (internal work RAM)
0xe28d_dc01, // ADD sp, sp, #0x100
0xeb00_0000, // BL subroutine at offset 56
0xea00_0005, // B Thumb setup at offset 80
0xe92d_4010, // subroutine: STMDB sp!, {r4, lr}
0xe1a0_5084, // MOV r5, r4, LSL #1
0xe006_0594, // MUL r6, r4, r5
0xe103_7096, // SWP r7, r6, [r3] (old RAM value into r7)
0xe3a0_4000, // MOV r4, #0 (demonstrate restoration from the stack)
0xe8bd_8010, // LDMIA sp!, {r4, pc} (restore r4 and return)
0xe28f_800d, // ADD r8, pc, #13 (Thumb entry at offset 100, bit 0 set)
0xe28f_9004, // ADD r9, pc, #4 (ARM return at offset 96)
0xe12f_ff18, // BX r8
0xe1a0_0000, // NOP, skipped
0xeaff_fffe, // ARM return: B .
]
.into_iter()
.flat_map(u32::to_le_bytes)
.collect();
bytes.extend(
[
0x2006_u16, // Thumb entry: MOV r0, #6
0x2107, // MOV r1, #7
0xf000, // BL prefix
0xf803, // BL suffix: target offset 114
0x6058, // STR r0, [r3, #4]
0x282a, // CMP r0, #42
0x4748, // BX r9 (return to ARM)
0xb500, // Thumb subroutine: PUSH {lr}
0x4348, // MUL r0, r1
0xbd00, // POP {pc} (stays in Thumb)
]
.into_iter()
.flat_map(u16::to_le_bytes),
);
bytes
}

View file

@ -0,0 +1,34 @@
//! Original tile scene with CPU-controlled background and sprite mosaic sizes.
use crate::graphics_demo::{GraphicsDemo, GraphicsError};
use gba_core::{input::Buttons, machine::Machine, memory::MemoryError, video::Framebuffer};
/// Completed updates and nine-bit horizontal/vertical scroll, as three words.
pub const MOSAIC_STATE: u32 = crate::tile_demo::TILE_STATE;
pub struct MosaicDemo {
runner: GraphicsDemo,
}
impl MosaicDemo {
pub fn new() -> Result<Self, MemoryError> {
Ok(Self {
runner: GraphicsDemo::with_program(crate::tile_demo::mosaic_program())?,
})
}
pub fn machine(&self) -> &Machine {
self.runner.machine()
}
/// Block dimensions cycle from 1 to 16 pixels, changing every eight updates.
/// Z bypasses BG mosaic; X bypasses OBJ mosaic. Q/W rotate/zoom the sprite.
/// Arrows scroll; Enter resets scrolling. The host only supplies input.
pub fn frame(
&mut self,
buttons: Buttons,
output: &mut Framebuffer,
) -> Result<usize, GraphicsError> {
self.runner.frame_with_control(buttons, output, 0x1340)
}
}

View file

@ -0,0 +1,146 @@
//! Original ARM raster program. Repeated HBlank DMA changes the palette backdrop
//! after each captured row. A whole-frame snapshot cannot reproduce these bands.
use crate::graphics_demo::{GraphicsDemo, GraphicsError};
use gba_core::{
input::Buttons,
machine::Machine,
memory::{MemoryError, ROM_START},
video::Framebuffer,
};
/// Completed updates and five-bit band phase, stored as two debug words.
pub const RASTER_STATE: u32 = 0x0200_0000;
pub const RASTER_TABLE: u32 = 0x0200_1000;
pub struct RasterDemo {
runner: GraphicsDemo,
}
impl RasterDemo {
pub fn new() -> Result<Self, MemoryError> {
Ok(Self {
runner: GraphicsDemo::with_program(program())?,
})
}
pub fn machine(&self) -> &Machine {
self.runner.machine()
}
/// Left/right move the color bands. Enter resets their phase.
pub fn frame(
&mut self,
buttons: Buttons,
output: &mut Framebuffer,
) -> Result<usize, GraphicsError> {
self.runner.frame_with_control(buttons, output, 0x400)
}
}
fn literal(code: &mut Vec<u32>, pool: &mut Vec<(usize, u32, u32)>, register: u32, value: u32) {
pool.push((code.len(), register, value));
code.push(0);
}
fn program() -> Vec<u8> {
let mut code = vec![
0xe3a0_9301, // MOV r9,#0x04000000
0xe3a0_0080, // MOV r0,#128 (forced blank)
0xe1c9_00b0, // STRH r0,[r9]
];
let mut pool = Vec::new();
let table_load = code.len();
literal(&mut code, &mut pool, 0, 0);
code.extend([
0xe3a0_1402, // MOV r1,#0x02000000
0xe281_1a01, // ADD r1,r1,#0x1000
0xe3a0_2c01, // MOV r2,#256 halfwords
0xef0b_0000, // CpuSet: copy original color table to EWRAM
]);
let irq_load = code.len();
literal(&mut code, &mut pool, 0, 0);
code.extend([
0xe3a0_1403, // MOV r1,#0x03000000
0xe281_1c7f, // ADD r1,r1,#0x7f00
0xe581_00fc, // STR r0,[r1,#0xfc] (IRQ callback)
0xe3a0_0008, // MOV r0,#8
0xe1c9_00b4, // STRH r0,[r9,#4] (VBlank IRQ)
0xe289_1c02, // ADD r1,r9,#0x200
0xe3a0_0001, // MOV r0,#1
0xe1c1_00b0, // STRH r0,[r1] (IE)
0xe3a0_8402, // MOV r8,#0x02000000
0xe288_8a01, // ADD r8,r8,#0x1000 (table base)
0xe3a0_6405, // MOV r6,#0x05000000 (backdrop palette)
0xe289_50b0, // ADD r5,r9,#0xb0 (DMA0 registers)
0xe289_ac01, // ADD r10,r9,#0x100
0xe28a_a030, // ADD r10,r10,#0x30 (KEYINPUT)
0xe3a0_b402, // MOV r11,#0x02000000 (debug state)
0xe3a0_4000, // MOV r4,#0 (phase)
0xe3a0_7000, // MOV r7,#0 (updates)
0xe3a0_0b01, // MOV r0,#0x400 (Mode0, transparent BG2)
0xe1c9_00b0, // STRH r0,[r9] (startup complete)
]);
let frame = code.len();
code.extend([
0xef05_0000, // VBlankIntrWait
0xe1da_30b0, // LDRH r3,[r10]
0xe313_0010, // TST r3,#Right
0x0284_4001, // ADDEQ r4,r4,#1
0xe313_0020, // TST r3,#Left
0x0244_4001, // SUBEQ r4,r4,#1
0xe204_401f, // AND r4,r4,#31
0xe313_0008, // TST r3,#Start
0x03a0_4000, // MOVEQ r4,#0
0xe3a0_0000, // MOV r0,#0
0xe1c5_00ba, // STRH r0,[r5,#10] (disable before rearming DMA)
0xe088_0084, // ADD r0,r8,r4,LSL #1
0xe1d0_10b0, // LDRH r1,[r0] (color for row0)
0xe1c6_10b0, // STRH r1,[r6]
0xe280_0002, // ADD r0,r0,#2 (row1 color starts HBlank stream)
0xe585_0000, // STR r0,[r5] (source)
0xe585_6004, // STR r6,[r5,#4] (fixed destination)
]);
literal(&mut code, &mut pool, 0, 0xa240_0001); // HBlank, repeat, fixed destination, one halfword.
code.extend([
0xe585_0008, // STR r0,[r5,#8] (arm DMA0)
0xe287_7001, // ADD r7,r7,#1
0xe58b_7000, // STR r7,[r11]
0xe58b_4004, // STR r4,[r11,#4]
]);
let displacement = frame as i32 - code.len() as i32 - 2;
code.push(0xea00_0000 | (displacement as u32 & 0x00ff_ffff));
let irq = 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 table = ROM_START + (code.len() + pool.len()) as u32 * 4;
for (index, register, value) in pool {
let value = if index == table_load {
table
} else if index == irq_load {
irq
} else {
value
};
let offset = (code.len() - index - 2) * 4;
assert!(offset < 4096);
code[index] = 0xe59f_0000 | register << 12 | offset as u32;
code.push(value);
}
let mut rom: Vec<u8> = code.into_iter().flat_map(u32::to_le_bytes).collect();
for i in 0..256u16 {
let n = i & 31;
let color = n | ((31 - n) << 5) | ((n / 2) << 10);
rom.extend(color.to_le_bytes());
}
rom
}

View 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};
use gba_core::{
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
}

View file

@ -0,0 +1,53 @@
//! Original code that configures a timer, handles one IRQ, and returns.
//! Uses nominal instruction/bus costs; device updates occur at instruction boundaries.
use gba_core::{
cpu::Cpu,
machine::Machine,
memory::{Memory, MemoryError, BIOS_SIZE, ROM_START},
};
pub const TIMER_DEMO_STEPS: usize = 21;
pub fn timer_demo() -> Result<Machine, MemoryError> {
let rom = [
0xe3a0_0301_u32, // MOV r0, #0x04000000
0xe280_0c01, // ADD r0, r0, #0x100 (Timer 0)
0xe3e0_100f, // MVN r1, #15 (low half = 0xfff0)
0xe1c0_10b0, // STRH r1, [r0] (reload)
0xe3a0_10c0, // MOV r1, #0xc0 (enable + local IRQ)
0xe3a0_2008, // MOV r2, #8 (Timer 0 IRQ bit)
0xe280_3c01, // ADD r3, r0, #0x100 (IE)
0xe1c3_20b0, // STRH r2, [r3] (IE)
0xe3a0_2001, // MOV r2, #1
0xe583_2008, // STR r2, [r3, #8] (IME)
0xe1c0_10b2, // STRH r1, [r0, #2] (start timer)
0xeaff_fffe, // B . (waiting for IRQ, not hardware HALT)
]
.into_iter()
.flat_map(u32::to_le_bytes)
.collect();
// A test vector and handler, not a Nintendo BIOS. The handler deliberately
// changes shared r2 and r10; it does not implement the GBA BIOS calling convention.
let mut bios = vec![0; BIOS_SIZE];
bios[0x18..0x1c].copy_from_slice(&0xea00_0010_u32.to_le_bytes()); // B 0x60
for (index, instruction) in [
0xe3a0_2000_u32, // MOV r2, #0
0xe1c0_20b2, // STRH r2, [r0, #2] (stop timer)
0xe3a0_2008, // MOV r2, #8
0xe1c3_20b2, // STRH r2, [r3, #2] (acknowledge IF)
0xe28a_a001, // ADD r10, r10, #1
0xe25e_f004, // SUBS pc, lr, #4 (return from IRQ)
]
.into_iter()
.enumerate()
{
let offset = 0x60 + index * 4;
bios[offset..offset + 4].copy_from_slice(&instruction.to_le_bytes());
}
Ok(Machine::new(
Cpu::new(ROM_START),
Memory::with_bios(rom, bios)?,
))
}