#![no_std]
#![cfg_attr(any(test, feature = "testing"), no_main)]
#![cfg_attr(any(test, feature = "testing"), feature(custom_test_frameworks))]
#![cfg_attr(
any(test, feature = "testing"),
test_runner(crate::test_runner::test_runner)
)]
#![cfg_attr(
any(test, feature = "testing"),
reexport_test_harness_main = "test_main"
)]
#![feature(allocator_api)]
#![feature(asm_const)]
#![warn(clippy::all)]
#![allow(clippy::needless_pass_by_ref_mut)]
#![deny(clippy::must_use_candidate)]
#![deny(clippy::trivially_copy_pass_by_ref)]
#![deny(clippy::semicolon_if_nothing_returned)]
#![deny(clippy::map_unwrap_or)]
#![deny(clippy::needless_pass_by_value)]
#![deny(clippy::redundant_closure_for_method_calls)]
#![deny(clippy::cloned_instead_of_copied)]
#![deny(rustdoc::broken_intra_doc_links)]
#![deny(rustdoc::private_intra_doc_links)]
#![deny(rustdoc::invalid_html_tags)]
pub use agb_image_converter::include_background_gfx;
#[doc(hidden)]
pub use agb_image_converter::include_aseprite_inner;
#[doc(hidden)]
pub use agb_image_converter::include_font as include_font_inner;
#[doc(hidden)]
pub use agb_image_converter::include_colours_inner;
#[macro_export]
macro_rules! include_font {
($font_path: literal, $font_size: literal) => {{
use $crate::display;
$crate::include_font_inner!($font_path, $font_size)
}};
}
pub use agb_macros::entry;
pub use agb_sound_converter::include_wav;
extern crate alloc;
mod agb_alloc;
mod agbabi;
mod bitarray;
pub mod display;
mod dma;
pub mod input;
pub mod interrupt;
mod memory_mapped;
pub mod mgba;
#[doc(inline)]
pub use agb_fixnum as fixnum;
pub use agb_hashmap as hash_map;
pub mod rng;
pub mod save;
mod single;
pub mod sound;
pub mod sync;
pub mod syscall;
pub mod timer;
mod no_game;
pub use no_game::no_game;
pub(crate) mod arena;
mod global_asm;
pub use {agb_alloc::ExternalAllocator, agb_alloc::InternalAllocator};
#[cfg(not(any(test, feature = "testing")))]
#[panic_handler]
#[allow(unused_must_use)]
fn panic_implementation(info: &core::panic::PanicInfo) -> ! {
use core::fmt::Write;
if let Some(mut mgba) = mgba::Mgba::new() {
write!(mgba, "{}", info);
mgba.set_level(mgba::DebugLevel::Fatal);
}
#[allow(clippy::empty_loop)]
loop {}
}
#[non_exhaustive]
pub struct Gba {
pub display: display::Display,
pub sound: sound::dmg::Sound,
pub mixer: sound::mixer::MixerController,
pub save: save::SaveManager,
pub timers: timer::TimerController,
}
impl Gba {
#[doc(hidden)]
#[must_use]
pub unsafe fn new_in_entry() -> Self {
Self::single_new()
}
const unsafe fn single_new() -> Self {
Self {
display: display::Display::new(),
sound: sound::dmg::Sound::new(),
mixer: sound::mixer::MixerController::new(),
save: save::SaveManager::new(),
timers: timer::TimerController::new(),
}
}
}
#[cfg(any(test, feature = "testing"))]
pub mod test_runner {
use super::*;
#[doc(hidden)]
pub trait Testable {
fn run(&self, gba: &mut Gba);
}
impl<T> Testable for T
where
T: Fn(&mut Gba),
{
fn run(&self, gba: &mut Gba) {
let mut mgba = mgba::Mgba::new().unwrap();
mgba.print(
format_args!("{}...", core::any::type_name::<T>()),
mgba::DebugLevel::Info,
)
.unwrap();
mgba::test_runner_measure_cycles();
self(gba);
mgba::test_runner_measure_cycles();
assert!(
unsafe { agb_alloc::number_of_blocks() } < 2,
"memory is being leaked, there are {} blocks",
unsafe { agb_alloc::number_of_blocks() }
);
mgba.print(format_args!("[ok]"), mgba::DebugLevel::Info)
.unwrap();
}
}
#[panic_handler]
fn panic_implementation(info: &core::panic::PanicInfo) -> ! {
if let Some(mut mgba) = mgba::Mgba::new() {
mgba.print(format_args!("[failed]"), mgba::DebugLevel::Error)
.unwrap();
mgba.print(format_args!("Error: {info}"), mgba::DebugLevel::Fatal)
.unwrap();
}
loop {}
}
static mut TEST_GBA: Option<Gba> = None;
#[doc(hidden)]
pub fn test_runner(tests: &[&dyn Testable]) {
let mut mgba = mgba::Mgba::new().unwrap();
mgba.print(
format_args!("Running {} tests", tests.len()),
mgba::DebugLevel::Info,
)
.unwrap();
let gba = unsafe { TEST_GBA.as_mut() }.unwrap();
for test in tests {
test.run(gba);
}
mgba.print(
format_args!("Tests finished successfully"),
mgba::DebugLevel::Info,
)
.unwrap();
}
#[cfg(test)]
mod agb {
pub mod test_runner {
pub use super::super::agb_start_tests;
}
}
#[cfg(test)]
#[entry]
fn agb_test_main(gba: Gba) -> ! {
#[allow(clippy::empty_loop)]
loop {} }
#[doc(hidden)]
pub fn agb_start_tests(gba: Gba, test_main: impl Fn()) -> ! {
unsafe { TEST_GBA = Some(gba) };
test_main();
#[allow(clippy::empty_loop)]
loop {}
}
pub fn assert_image_output(image: &str) {
display::busy_wait_for_vblank();
display::busy_wait_for_vblank();
let mut mgba = crate::mgba::Mgba::new().unwrap();
mgba.print(format_args!("image:{image}"), crate::mgba::DebugLevel::Info)
.unwrap();
display::busy_wait_for_vblank();
}
}
#[inline(never)]
pub(crate) fn program_counter_before_interrupt() -> u32 {
extern "C" {
static mut agb_rs__program_counter: u32;
}
unsafe { agb_rs__program_counter }
}
#[cfg(test)]
mod test {
use core::ptr::addr_of_mut;
use super::Gba;
#[test_case]
#[allow(clippy::eq_op)]
fn trivial_test(_gba: &mut Gba) {
assert_eq!(1, 1);
}
#[test_case]
fn gba_struct_is_zero_sized(_gba: &mut Gba) {
use core::mem;
assert_eq!(mem::size_of::<Gba>(), 0);
}
#[test_case]
fn wait_30_frames(_gba: &mut Gba) {
let vblank = crate::interrupt::VBlank::get();
let mut counter = 0;
loop {
if counter > 30 {
break;
}
vblank.wait_for_vblank();
counter += 1;
}
}
#[link_section = ".ewram"]
static mut EWRAM_TEST: u32 = 5;
#[test_case]
fn ewram_static_test(_gba: &mut Gba) {
unsafe {
let ewram_ptr = addr_of_mut!(EWRAM_TEST);
let content = ewram_ptr.read_volatile();
assert_eq!(content, 5, "expected data in ewram to be 5");
ewram_ptr.write_volatile(content + 1);
let content = ewram_ptr.read_volatile();
assert_eq!(content, 6, "expected data to have increased by one");
let address = ewram_ptr as usize;
assert!(
(0x0200_0000..0x0204_0000).contains(&address),
"ewram is located between 0x0200_0000 and 0x0204_0000, address was actually found to be {address:#010X}",
);
}
}
#[link_section = ".iwram"]
static mut IWRAM_EXPLICIT: u32 = 9;
#[test_case]
fn iwram_explicit_test(_gba: &mut Gba) {
unsafe {
let iwram_ptr = addr_of_mut!(IWRAM_EXPLICIT);
let address = iwram_ptr as usize;
assert!(
(0x0300_0000..0x0300_8000).contains(&address),
"iwram is located between 0x0300_0000 and 0x0300_8000, but was actually found to be at {address:#010X}"
);
let c = iwram_ptr.read_volatile();
assert_eq!(c, 9, "expected content to be 9");
iwram_ptr.write_volatile(u32::MAX);
let c = iwram_ptr.read_volatile();
assert_eq!(c, u32::MAX, "expected content to be {}", u32::MAX);
}
}
static mut IMPLICIT_STORAGE: u32 = 9;
#[test_case]
fn implicit_data_test(_gba: &mut Gba) {
unsafe {
let iwram_ptr = addr_of_mut!(IMPLICIT_STORAGE);
let address = iwram_ptr as usize;
assert!(
(0x0200_0000..0x0204_0000).contains(&address),
"implicit data storage is expected to be in ewram, which is between 0x0300_0000 and 0x0300_8000, but was actually found to be at {address:#010X}"
);
let c = iwram_ptr.read_volatile();
assert_eq!(c, 9, "expected content to be 9");
iwram_ptr.write_volatile(u32::MAX);
let c = iwram_ptr.read_volatile();
assert_eq!(c, u32::MAX, "expected content to be {}", u32::MAX);
}
}
}