Adds support for NO$GBA's debugging API. (#108)

* Implement a debugging interface that allows the use of debugging on multiple emulators.

* Implement NO$GBA debugging interface.

* Run rustfmt on new debug code.

* Fix the debug module not compiling on non-ARM systems.

* Don't error (and just silently truncate) on messages that are too long.
This commit is contained in:
Alissa Rao 2021-02-22 22:19:14 -08:00 committed by GitHub
parent 2aa59bb341
commit 8bfa1a228a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 305 additions and 72 deletions

View file

@ -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.

131
src/debug.rs Normal file
View file

@ -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<bool> = Static::new(false);
/// The debugging interface in use.
static INTERFACE: Static<Option<&'static dyn DebugInterface>> = 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::<u16>::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 { }
}

View file

@ -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<u16> = unsafe { VolAddress::new(0x4fff780) };
const ENABLE_ADDRESS_INPUT: u16 = 0xC0DE;
const ENABLE_ADDRESS_OUTPUT: u16 = 0x1DEA;
const OUTPUT_BASE: VolAddress<u8> = unsafe { VolAddress::new(0x4fff600) };
const SEND_ADDRESS: VolAddress<u16> = unsafe { VolAddress::new(0x4fff700) };
const SEND_FLAG: u16 = 0x100;
// Only enable MGBA debugging once.
static MGBA_DEBUGGING: InitOnce<bool> = 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<u16> = unsafe { VolAddress::new(0x4fff780) };
const ENABLE_ADDRESS_INPUT: u16 = 0xC0DE;
const ENABLE_ADDRESS_OUTPUT: u16 = 0x1DEA;
const OUTPUT_BASE: VolAddress<u8> = unsafe { VolAddress::new(0x4fff600) };
const SEND_ADDRESS: VolAddress<u16> = 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> {
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(())
}
}

76
src/debug/nocash.rs Normal file
View file

@ -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<u8> = unsafe { VolAddress::new(0x04FFFA1C) };
const SIGNATURE_ADDR: VolBlock<u8, U16> = unsafe { VolBlock::new(0x04FFFA00) };
const SIGNATURE: [u8; 7] = *b"no$gba ";
static NO_CASH_DEBUGGING: InitOnce<bool> = 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<Self> {
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(())
}
}

View file

@ -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.
///

View file

@ -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();
}
}};
}