diff --git a/examples/hello_world.rs b/examples/hello_world.rs index 727c1f6..01ef803 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -14,11 +14,10 @@ use gba::{ #[panic_handler] fn panic(info: &core::panic::PanicInfo) -> ! { - // This kills the emulation with a message if we're running within mGBA. + // This kills the emulation with a message if we're running inside an + // emulator we support (mGBA or NO$GBA), or just crashes the game if we + // aren't. fatal!("{}", info); - // If we're _not_ running within mGBA then we still need to not return, so - // loop forever doing nothing. - loop {} } /// Performs a busy loop until VBlank starts. diff --git a/src/debug.rs b/src/debug.rs new file mode 100644 index 0000000..8e03309 --- /dev/null +++ b/src/debug.rs @@ -0,0 +1,131 @@ +//! Special utilities for debugging ROMs on various emulators. +//! +//! This is the underlying implementation behind the various print macros in +//! the gba crate. It currently supports the latest versions of mGBA and NO$GBA. + +use crate::{ + io::{ + dma::{DMAControlSetting, DMA0, DMA1, DMA2, DMA3}, + irq::{IrqEnableSetting, IME}, + }, + sync::{InitOnce, RawMutex, Static}, +}; +use core::fmt::{Arguments, Error}; +use voladdress::VolAddress; + +pub mod mgba; +pub mod nocash; + +/// A cross-emulator debug level. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[allow(missing_docs)] +pub enum DebugLevel { + /// This causes the emulator (or debug interface) to halt! + Fatal, + Error, + Warning, + Info, + Debug, +} + +/// An interface for debugging features. +pub trait DebugInterface { + /// Whether debugging is enabled. + fn device_attached(&self) -> bool; + + /// Prints a debug message to the emulator. + fn debug_print(&self, debug: DebugLevel, args: &Arguments<'_>) -> Result<(), Error>; +} + +/// An lock to ensure interface changes go smoothly. +static LOCK: RawMutex = RawMutex::new(); +/// An optimization to allow us to short circuit debugging early when there is no interface. +static NO_DEBUG: Static = Static::new(false); +/// The debugging interface in use. +static INTERFACE: Static> = Static::new(None); +/// Debug interface detection only happens once. +static DETECT_ONCE: InitOnce<()> = InitOnce::new(); + +/// Sets the debug interface in use manually. +pub fn set_debug_interface(interface: &'static dyn DebugInterface) { + let _lock = LOCK.lock(); + INTERFACE.write(Some(interface)); + NO_DEBUG.write(false); +} + +/// Disables debugging. +pub fn set_debug_disabled() { + let _lock = LOCK.lock(); + INTERFACE.write(None); + NO_DEBUG.write(true); +} + +/// Prints a line to the debug interface, if there is any. +#[inline(never)] +pub fn debug_print(debug: DebugLevel, args: &Arguments<'_>) -> Result<(), Error> { + if let Some(interface) = get_debug_interface() { + interface.debug_print(debug, args)?; + } + Ok(()) +} + +/// Returns the current active debugging interface if there is one, or `None` +/// if one isn't attached. +#[inline(never)] +pub fn get_debug_interface() -> Option<&'static dyn DebugInterface> { + let mut interface = INTERFACE.read(); + if interface.is_none() { + DETECT_ONCE.get(|| { + let mut new_value: Option<&'static dyn DebugInterface> = None; + if mgba::detect() { + new_value = Some(&mgba::MGBADebugInterface); + } else if nocash::detect() { + new_value = Some(&nocash::NoCashDebugInterface); + } + if new_value.is_some() { + INTERFACE.write(new_value); + interface = new_value; + } + }); + } + interface +} + +/// Whether debugging is disabled. +/// +/// This should only be relied on for correctness. If this is false, there is no +/// possible way any debugging calls will succeed, and it is better to simply +/// skip the entire routine. +#[inline(always)] +pub fn is_debugging_disabled() -> bool { + NO_DEBUG.read() +} + +/// Crashes the program by disabling interrupts and entering an infinite loop. +/// +/// This is used to implement fatal errors outside of mGBA. +#[inline(never)] +pub fn crash() -> ! { + #[cfg(all(target_vendor = "nintendo", target_env = "agb"))] + { + IME.write(IrqEnableSetting::IRQ_NO); + unsafe { + // Stop all ongoing DMAs just in case. + DMA0::set_control(DMAControlSetting::new()); + DMA1::set_control(DMAControlSetting::new()); + DMA2::set_control(DMAControlSetting::new()); + DMA3::set_control(DMAControlSetting::new()); + + // Writes the halt call back to memory + // + // we use an infinite loop in RAM just to make sure removing the + // Game Pak doesn't break this crash loop. + let target = VolAddress::::new(0x03000000); + target.write(0xe7fe); // assembly instruction: `loop: b loop` + core::mem::transmute::<_, extern "C" fn() -> !>(0x03000001)() + } + } + + #[cfg(not(all(target_vendor = "nintendo", target_env = "agb")))] + loop { } +} diff --git a/src/mgba.rs b/src/debug/mgba.rs similarity index 51% rename from src/mgba.rs rename to src/debug/mgba.rs index c6a50ad..ad381a5 100644 --- a/src/mgba.rs +++ b/src/debug/mgba.rs @@ -4,7 +4,10 @@ //! you've got some older version of things there might be any number of //! differences or problems. -use super::*; +use super::{DebugInterface, DebugLevel}; +use crate::sync::InitOnce; +use core::fmt::{Arguments, Write}; +use voladdress::VolAddress; #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u16)] @@ -18,29 +21,41 @@ pub enum MGBADebugLevel { Debug = 4, } +// MGBADebug related addresses. +const ENABLE_ADDRESS: VolAddress = unsafe { VolAddress::new(0x4fff780) }; +const ENABLE_ADDRESS_INPUT: u16 = 0xC0DE; +const ENABLE_ADDRESS_OUTPUT: u16 = 0x1DEA; + +const OUTPUT_BASE: VolAddress = unsafe { VolAddress::new(0x4fff600) }; + +const SEND_ADDRESS: VolAddress = unsafe { VolAddress::new(0x4fff700) }; +const SEND_FLAG: u16 = 0x100; + +// Only enable MGBA debugging once. +static MGBA_DEBUGGING: InitOnce = InitOnce::new(); + +/// Returns whether we are running in mGBA. +#[inline(never)] +pub fn detect() -> bool { + *MGBA_DEBUGGING.get(|| { + ENABLE_ADDRESS.write(ENABLE_ADDRESS_INPUT); + ENABLE_ADDRESS.read() == ENABLE_ADDRESS_OUTPUT + }) +} + /// Allows writing to the `mGBA` debug output. #[derive(Debug, PartialEq, Eq)] pub struct MGBADebug { bytes_written: u8, } impl MGBADebug { - const ENABLE_ADDRESS: VolAddress = unsafe { VolAddress::new(0x4fff780) }; - const ENABLE_ADDRESS_INPUT: u16 = 0xC0DE; - const ENABLE_ADDRESS_OUTPUT: u16 = 0x1DEA; - - const OUTPUT_BASE: VolAddress = unsafe { VolAddress::new(0x4fff600) }; - - const SEND_ADDRESS: VolAddress = unsafe { VolAddress::new(0x4fff700) }; - const SEND_FLAG: u16 = 0x100; - /// Gives a new MGBADebug, if running within `mGBA` /// /// # Fails /// /// If you're not running in the `mGBA` emulator. pub fn new() -> Option { - Self::ENABLE_ADDRESS.write(Self::ENABLE_ADDRESS_INPUT); - if Self::ENABLE_ADDRESS.read() == Self::ENABLE_ADDRESS_OUTPUT { + if detect() { Some(MGBADebug { bytes_written: 0 }) } else { None @@ -56,9 +71,9 @@ impl MGBADebug { pub fn send(&mut self, level: MGBADebugLevel) { if level == MGBADebugLevel::Fatal { // Note(Lokathor): A Fatal send causes the emulator to halt! - Self::SEND_ADDRESS.write(Self::SEND_FLAG | MGBADebugLevel::Fatal as u16); + SEND_ADDRESS.write(SEND_FLAG | MGBADebugLevel::Fatal as u16); } else { - Self::SEND_ADDRESS.write(Self::SEND_FLAG | level as u16); + SEND_ADDRESS.write(SEND_FLAG | level as u16); self.bytes_written = 0; } } @@ -67,7 +82,7 @@ impl MGBADebug { impl core::fmt::Write for MGBADebug { fn write_str(&mut self, s: &str) -> Result<(), core::fmt::Error> { unsafe { - let mut current = Self::OUTPUT_BASE.offset(self.bytes_written as isize); + let mut current = OUTPUT_BASE.offset(self.bytes_written as isize); let mut str_iter = s.bytes(); while self.bytes_written < 255 { match str_iter.next() { @@ -79,7 +94,32 @@ impl core::fmt::Write for MGBADebug { None => return Ok(()), } } - Err(core::fmt::Error) + Ok(()) } } } + +/// The [`DebugInterface`] for MGBA. +pub struct MGBADebugInterface; +impl DebugInterface for MGBADebugInterface { + fn device_attached(&self) -> bool { + detect() + } + + fn debug_print(&self, debug: DebugLevel, args: &Arguments<'_>) -> Result<(), core::fmt::Error> { + if let Some(mut out) = MGBADebug::new() { + write!(out, "{}", args)?; + out.send(match debug { + DebugLevel::Fatal => MGBADebugLevel::Fatal, + DebugLevel::Error => MGBADebugLevel::Error, + DebugLevel::Warning => MGBADebugLevel::Warning, + DebugLevel::Info => MGBADebugLevel::Info, + DebugLevel::Debug => MGBADebugLevel::Debug, + }); + if debug == DebugLevel::Fatal { + super::crash(); + } + } + Ok(()) + } +} diff --git a/src/debug/nocash.rs b/src/debug/nocash.rs new file mode 100644 index 0000000..42214a6 --- /dev/null +++ b/src/debug/nocash.rs @@ -0,0 +1,76 @@ +//! Special utils for if you're running on the NO$GBA emulator. +//! +//! Note that this assumes that you're using the very latest version (3.03). If +//! you've got some older version of things there might be any number of +//! differences or problems. + +use crate::{ + debug::{DebugInterface, DebugLevel}, + sync::InitOnce, +}; +use core::fmt::{Arguments, Write}; +use typenum::consts::U16; +use voladdress::{VolAddress, VolBlock}; + +const CHAR_OUT: VolAddress = unsafe { VolAddress::new(0x04FFFA1C) }; +const SIGNATURE_ADDR: VolBlock = unsafe { VolBlock::new(0x04FFFA00) }; + +const SIGNATURE: [u8; 7] = *b"no$gba "; +static NO_CASH_DEBUGGING: InitOnce = InitOnce::new(); + +/// Returns whether we are running in `NO$GBA`. +#[inline(never)] +pub fn detect() -> bool { + *NO_CASH_DEBUGGING.get(|| { + for i in 0..7 { + if SIGNATURE_ADDR.index(i).read() != SIGNATURE[i] { + return false; + } + } + true + }) +} + +/// Allows writing to the `NO$GBA` debug output. +#[derive(Debug, PartialEq, Eq)] +pub struct NoCashDebug(()); +impl NoCashDebug { + /// Gives a new NoCashDebug, if running within `NO$GBA` + /// + /// # Fails + /// + /// If you're not running in the `NO$GBA` emulator. + pub fn new() -> Option { + if detect() { + Some(NoCashDebug(())) + } else { + None + } + } +} +impl core::fmt::Write for NoCashDebug { + fn write_str(&mut self, s: &str) -> Result<(), core::fmt::Error> { + for b in s.bytes() { + CHAR_OUT.write(b); + } + Ok(()) + } +} + +/// The [`DebugInterface`] for `NO$GBA`. +pub struct NoCashDebugInterface; +impl DebugInterface for NoCashDebugInterface { + fn device_attached(&self) -> bool { + detect() + } + + fn debug_print(&self, debug: DebugLevel, args: &Arguments<'_>) -> Result<(), core::fmt::Error> { + if let Some(mut out) = NoCashDebug::new() { + write!(out, "User: [{:?}] {}\n", debug, args)?; + if debug == DebugLevel::Fatal { + super::crash(); + } + } + Ok(()) + } +} diff --git a/src/lib.rs b/src/lib.rs index 217b946..ee159e0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -42,10 +42,10 @@ pub mod rom; pub mod sram; -pub mod mgba; - pub mod sync; +pub mod debug; + extern "C" { /// This marks the end of the `.data` and `.bss` sections in IWRAM. /// diff --git a/src/macros.rs b/src/macros.rs index 5cbc981..bfb5237 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -87,97 +87,84 @@ macro_rules! newtype_enum { }; } -/// Delivers a fatal message to the mGBA output, halting emulation. +/// Delivers a fatal message to the emulator debug output, and crashes +/// the the game. /// -/// This works basically like `println`. mGBA is a C program and all, so you -/// should only attempt to print non-null ASCII values through this. There's -/// also a maximum length of 255 bytes per message. +/// This works basically like `println`. You should avoid null ASCII values. +/// Furthermore on mGBA, there is a maximum length of 255 bytes per message. /// -/// This has no effect if you're not using mGBA. +/// This has no effect outside of a supported emulator. #[macro_export] macro_rules! fatal { ($($arg:tt)*) => {{ - use $crate::mgba::{MGBADebug, MGBADebugLevel}; - use core::fmt::Write; - if let Some(mut mgba) = MGBADebug::new() { - let _ = write!(mgba, $($arg)*); - mgba.send(MGBADebugLevel::Fatal); + use $crate::debug; + if !debug::is_debugging_disabled() { + debug::debug_print(debug::DebugLevel::Fatal, &format_args!($($arg)*)).ok(); } + debug::crash() }}; } -/// Delivers an error message to the mGBA output. +/// Delivers a error message to the emulator debug output. /// -/// This works basically like `println`. mGBA is a C program and all, so you -/// should only attempt to print non-null ASCII values through this. There's -/// also a maximum length of 255 bytes per message. +/// This works basically like `println`. You should avoid null ASCII values. +/// Furthermore on mGBA, there is a maximum length of 255 bytes per message. /// -/// This has no effect if you're not using mGBA. +/// This has no effect outside of a supported emulator. #[macro_export] macro_rules! error { ($($arg:tt)*) => {{ - use $crate::mgba::{MGBADebug, MGBADebugLevel}; - use core::fmt::Write; - if let Some(mut mgba) = MGBADebug::new() { - let _ = write!(mgba, $($arg)*); - mgba.send(MGBADebugLevel::Error); + use $crate::debug; + if !debug::is_debugging_disabled() { + debug::debug_print(debug::DebugLevel::Error, &format_args!($($arg)*)).ok(); } }}; } -/// Delivers a warning message to the mGBA output. +/// Delivers a warning message to the emulator debug output. /// -/// This works basically like `println`. mGBA is a C program and all, so you -/// should only attempt to print non-null ASCII values through this. There's -/// also a maximum length of 255 bytes per message. +/// This works basically like `println`. You should avoid null ASCII values. +/// Furthermore on mGBA, there is a maximum length of 255 bytes per message. /// -/// This has no effect if you're not using mGBA. +/// This has no effect outside of a supported emulator. #[macro_export] macro_rules! warn { ($($arg:tt)*) => {{ - use $crate::mgba::{MGBADebug, MGBADebugLevel}; - use core::fmt::Write; - if let Some(mut mgba) = MGBADebug::new() { - let _ = write!(mgba, $($arg)*); - mgba.send(MGBADebugLevel::Warning); + use $crate::debug; + if !debug::is_debugging_disabled() { + debug::debug_print(debug::DebugLevel::Warning, &format_args!($($arg)*)).ok(); } }}; } -/// Delivers an info message to the mGBA output. +/// Delivers an info message to the emulator debug output. /// -/// This works basically like `println`. mGBA is a C program and all, so you -/// should only attempt to print non-null ASCII values through this. There's -/// also a maximum length of 255 bytes per message. +/// This works basically like `println`. You should avoid null ASCII values. +/// Furthermore on mGBA, there is a maximum length of 255 bytes per message. /// -/// This has no effect if you're not using mGBA. +/// This has no effect outside of a supported emulator. #[macro_export] macro_rules! info { ($($arg:tt)*) => {{ - use $crate::mgba::{MGBADebug, MGBADebugLevel}; - use core::fmt::Write; - if let Some(mut mgba) = MGBADebug::new() { - let _ = write!(mgba, $($arg)*); - mgba.send(MGBADebugLevel::Info); + use $crate::debug; + if !debug::is_debugging_disabled() { + debug::debug_print(debug::DebugLevel::Info, &format_args!($($arg)*)).ok(); } }}; } -/// Delivers a debug message to the mGBA output. +/// Delivers a debug message to the emulator debug output. /// -/// This works basically like `println`. mGBA is a C program and all, so you -/// should only attempt to print non-null ASCII values through this. There's -/// also a maximum length of 255 bytes per message. +/// This works basically like `println`. You should avoid null ASCII values. +/// Furthermore on mGBA, there is a maximum length of 255 bytes per message. /// -/// This has no effect if you're not using mGBA. +/// This has no effect outside of a supported emulator. #[macro_export] macro_rules! debug { ($($arg:tt)*) => {{ - use $crate::mgba::{MGBADebug, MGBADebugLevel}; - use core::fmt::Write; - if let Some(mut mgba) = MGBADebug::new() { - let _ = write!(mgba, $($arg)*); - mgba.send(MGBADebugLevel::Debug); + use $crate::debug; + if !debug::is_debugging_disabled() { + debug::debug_print(debug::DebugLevel::Debug, &format_args!($($arg)*)).ok(); } }}; }