mirror of
https://github.com/italicsjenga/gba.git
synced 2025-01-11 03:21:30 +11:00
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:
parent
2aa59bb341
commit
8bfa1a228a
|
@ -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
131
src/debug.rs
Normal 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 { }
|
||||
}
|
|
@ -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
76
src/debug/nocash.rs
Normal 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(())
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
///
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue