1#![no_std]
2#![cfg_attr(any(test, feature = "testing"), no_main)]
4#![cfg_attr(any(test, feature = "testing"), feature(custom_test_frameworks))]
5#![cfg_attr(
6 any(test, feature = "testing"),
7 test_runner(crate::test_runner::test_runner)
8)]
9#![cfg_attr(
10 any(test, feature = "testing"),
11 reexport_test_harness_main = "test_main"
12)]
13#![feature(allocator_api)]
14#![warn(clippy::all)]
15#![allow(clippy::needless_pass_by_ref_mut)]
16#![deny(clippy::must_use_candidate)]
17#![deny(clippy::trivially_copy_pass_by_ref)]
18#![deny(clippy::semicolon_if_nothing_returned)]
19#![deny(clippy::map_unwrap_or)]
20#![deny(clippy::needless_pass_by_value)]
21#![deny(clippy::redundant_closure_for_method_calls)]
22#![deny(clippy::cloned_instead_of_copied)]
23#![deny(rustdoc::broken_intra_doc_links)]
24#![deny(rustdoc::private_intra_doc_links)]
25#![deny(rustdoc::invalid_html_tags)]
26
27pub use agb_image_converter::include_background_gfx;
118
119#[doc(hidden)]
120pub use agb_image_converter::include_aseprite_inner;
121
122#[doc(hidden)]
123pub use agb_image_converter::include_font as include_font_inner;
124
125#[doc(hidden)]
126pub use agb_image_converter::include_colours_inner;
127
128#[macro_export]
129macro_rules! include_font {
130 ($font_path: literal, $font_size: literal) => {{
131 use $crate::display;
132 $crate::include_font_inner!($font_path, $font_size)
133 }};
134}
135
136pub use agb_macros::entry;
154
155pub use agb_sound_converter::include_wav;
156
157extern crate alloc;
158mod agb_alloc;
159
160mod agbabi;
161#[cfg(feature = "backtrace")]
162mod backtrace;
163mod bitarray;
164pub mod display;
166pub mod dma;
168pub mod input;
170pub mod interrupt;
172mod memory_mapped;
173pub mod mgba;
175#[doc(inline)]
176pub use agb_fixnum as fixnum;
177pub use agb_hashmap as hash_map;
179#[cfg(feature = "backtrace")]
180mod panics_render;
181pub mod rng;
183pub mod save;
184mod single;
185pub mod sound;
187mod sync;
189pub mod syscall;
191pub mod timer;
193pub(crate) mod util;
194
195mod no_game;
196
197pub use no_game::no_game;
199
200pub(crate) mod arena;
201mod global_asm;
202
203pub mod external {
204 pub use critical_section;
205 pub use once_cell;
206 pub use portable_atomic;
207}
208
209pub use {agb_alloc::ExternalAllocator, agb_alloc::InternalAllocator};
210
211#[cfg(any(test, feature = "testing", feature = "backtrace"))]
212#[panic_handler]
213fn panic_implementation(info: &core::panic::PanicInfo) -> ! {
214 avoid_double_panic(info);
215
216 #[cfg(feature = "backtrace")]
217 let frames = backtrace::unwind_exception();
218
219 #[cfg(feature = "testing")]
220 if let Some(mut mgba) = mgba::Mgba::new() {
221 let _ = mgba.print(format_args!("[failed]"), mgba::DebugLevel::Error);
222 }
223
224 #[cfg(feature = "backtrace")]
225 crate::panics_render::render_backtrace(&frames, info);
226
227 #[cfg(not(feature = "backtrace"))]
228 if let Some(mut mgba) = mgba::Mgba::new() {
229 let _ = mgba.print(format_args!("{info}"), mgba::DebugLevel::Fatal);
230 }
231
232 #[cfg(not(feature = "backtrace"))]
233 loop {
234 syscall::halt();
235 }
236}
237
238fn avoid_double_panic(info: &core::panic::PanicInfo) {
241 static IS_PANICKING: portable_atomic::AtomicBool = portable_atomic::AtomicBool::new(false);
242
243 if IS_PANICKING.load(portable_atomic::Ordering::SeqCst) {
244 if let Some(mut mgba) = mgba::Mgba::new() {
245 let _ = mgba.print(
246 format_args!("Double panic: {info}"),
247 mgba::DebugLevel::Fatal,
248 );
249 }
250 loop {
251 syscall::halt();
252 }
253 } else {
254 IS_PANICKING.store(true, portable_atomic::Ordering::SeqCst);
255 }
256}
257
258#[non_exhaustive]
279pub struct Gba {
280 pub display: display::Display,
282 pub sound: sound::dmg::Sound,
285 pub mixer: sound::mixer::MixerController,
287 pub save: save::SaveManager,
289 pub timers: timer::TimerController,
291 pub dma: dma::DmaController,
293}
294
295impl Gba {
296 #[doc(hidden)]
297 #[must_use]
298 pub unsafe fn new_in_entry() -> Self {
303 Self::single_new()
304 }
305
306 const unsafe fn single_new() -> Self {
307 Self {
308 display: display::Display::new(),
309 sound: sound::dmg::Sound::new(),
310 mixer: sound::mixer::MixerController::new(),
311 save: save::SaveManager::new(),
312 timers: timer::TimerController::new(),
313 dma: dma::DmaController::new(),
314 }
315 }
316}
317
318#[cfg(any(test, feature = "testing"))]
319pub mod test_runner {
343 use util::SyncUnsafeCell;
344
345 use super::*;
346
347 #[doc(hidden)]
348 pub trait Testable {
349 fn run(&self, gba: &mut Gba);
350 }
351
352 impl<T> Testable for T
353 where
354 T: Fn(&mut Gba),
355 {
356 fn run(&self, gba: &mut Gba) {
357 let mut mgba = mgba::Mgba::new().unwrap();
358 mgba.print(
359 format_args!("{}...", core::any::type_name::<T>()),
360 mgba::DebugLevel::Info,
361 )
362 .unwrap();
363 mgba::test_runner_measure_cycles();
364 self(gba);
365 mgba::test_runner_measure_cycles();
366
367 mgba.print(format_args!("[ok]"), mgba::DebugLevel::Info)
368 .unwrap();
369 }
370 }
371
372 static TEST_GBA: SyncUnsafeCell<Option<Gba>> = SyncUnsafeCell::new(None);
373
374 #[doc(hidden)]
375 pub fn test_runner(tests: &[&dyn Testable]) {
376 let mut mgba = mgba::Mgba::new().unwrap();
377 mgba.print(
378 format_args!("Running {} tests", tests.len()),
379 mgba::DebugLevel::Info,
380 )
381 .unwrap();
382
383 let gba = unsafe { &mut *TEST_GBA.get() }.as_mut().unwrap();
384
385 for test in tests {
386 test.run(gba);
387 }
388
389 mgba.print(
390 format_args!("Tests finished successfully"),
391 mgba::DebugLevel::Info,
392 )
393 .unwrap();
394 }
395
396 #[cfg(test)]
398 mod agb {
399 pub mod test_runner {
400 pub use super::super::agb_start_tests;
401 }
402 }
403
404 #[cfg(test)]
405 #[entry]
406 fn agb_test_main(gba: Gba) -> ! {
407 #[allow(clippy::empty_loop)]
408 loop {} }
410
411 #[doc(hidden)]
412 pub fn agb_start_tests(gba: Gba, test_main: impl Fn()) -> ! {
413 *unsafe { &mut *TEST_GBA.get() } = Some(gba);
414 test_main();
415 #[allow(clippy::empty_loop)]
416 loop {}
417 }
418
419 pub fn assert_image_output(image: &str) {
420 display::busy_wait_for_vblank();
421 display::busy_wait_for_vblank();
422 let mut mgba = crate::mgba::Mgba::new().unwrap();
423 mgba.print(format_args!("image:{image}"), crate::mgba::DebugLevel::Info)
424 .unwrap();
425 display::busy_wait_for_vblank();
426 }
427}
428
429#[inline(never)]
430pub(crate) fn program_counter_before_interrupt() -> u32 {
431 extern "C" {
432 static mut agb_rs__program_counter: u32;
433 }
434 unsafe { agb_rs__program_counter }
435}
436
437#[cfg(test)]
438mod test {
439 use core::ptr::addr_of_mut;
440
441 use super::Gba;
442
443 #[test_case]
444 #[allow(clippy::eq_op)]
445 fn trivial_test(_gba: &mut Gba) {
446 assert_eq!(1, 1);
447 }
448
449 #[test_case]
450 fn gba_struct_is_zero_sized(_gba: &mut Gba) {
451 use core::mem;
452 assert_eq!(mem::size_of::<Gba>(), 0);
453 }
454
455 #[test_case]
456 fn wait_30_frames(_gba: &mut Gba) {
457 let vblank = crate::interrupt::VBlank::get();
458 let mut counter = 0;
459 loop {
460 if counter > 30 {
461 break;
462 }
463 vblank.wait_for_vblank();
464 counter += 1;
465 }
466 }
467
468 #[link_section = ".ewram"]
469 static mut EWRAM_TEST: u32 = 5;
470 #[test_case]
471 fn ewram_static_test(_gba: &mut Gba) {
472 unsafe {
473 let ewram_ptr = addr_of_mut!(EWRAM_TEST);
474 let content = ewram_ptr.read_volatile();
475 assert_eq!(content, 5, "expected data in ewram to be 5");
476 ewram_ptr.write_volatile(content + 1);
477 let content = ewram_ptr.read_volatile();
478 assert_eq!(content, 6, "expected data to have increased by one");
479 let address = ewram_ptr as usize;
480 assert!(
481 (0x0200_0000..0x0204_0000).contains(&address),
482 "ewram is located between 0x0200_0000 and 0x0204_0000, address was actually found to be {address:#010X}",
483 );
484 }
485 }
486
487 #[link_section = ".iwram"]
488 static mut IWRAM_EXPLICIT: u32 = 9;
489 #[test_case]
490 fn iwram_explicit_test(_gba: &mut Gba) {
491 unsafe {
492 let iwram_ptr = addr_of_mut!(IWRAM_EXPLICIT);
493 let address = iwram_ptr as usize;
494 assert!(
495 (0x0300_0000..0x0300_8000).contains(&address),
496 "iwram is located between 0x0300_0000 and 0x0300_8000, but was actually found to be at {address:#010X}"
497 );
498 let c = iwram_ptr.read_volatile();
499 assert_eq!(c, 9, "expected content to be 9");
500 iwram_ptr.write_volatile(u32::MAX);
501 let c = iwram_ptr.read_volatile();
502 assert_eq!(c, u32::MAX, "expected content to be {}", u32::MAX);
503 }
504 }
505
506 static mut IMPLICIT_STORAGE: u32 = 9;
507 #[test_case]
508 fn implicit_data_test(_gba: &mut Gba) {
509 unsafe {
510 let iwram_ptr = addr_of_mut!(IMPLICIT_STORAGE);
511 let address = iwram_ptr as usize;
512 assert!(
513 (0x0200_0000..0x0204_0000).contains(&address),
514 "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}"
515 );
516 let c = iwram_ptr.read_volatile();
517 assert_eq!(c, 9, "expected content to be 9");
518 iwram_ptr.write_volatile(u32::MAX);
519 let c = iwram_ptr.read_volatile();
520 assert_eq!(c, u32::MAX, "expected content to be {}", u32::MAX);
521 }
522 }
523}