#![no_std]
#![warn(clippy::pedantic, missing_docs)]
#![allow(
// Clippy erroneously believes "mGBA" is an item that requires backticks.
clippy::doc_markdown,
)]
use core::{
convert::Into,
fmt,
fmt::{write, Display, Write},
sync::{atomic, atomic::compiler_fence},
};
use log::{LevelFilter, Log, Metadata, Record, SetLoggerError};
const MGBA_LOG_BUFFER: *mut u8 = 0x04FF_F600 as *mut u8;
const MGBA_LOG_SEND: *mut Level = 0x04FF_F700 as *mut Level;
const MGBA_LOG_ENABLE: *mut u16 = 0x04FF_F780 as *mut u16;
const IME: *mut bool = 0x0400_0208 as *mut bool;
#[derive(Clone, Copy, Debug)]
enum Level {
Fatal = 0x100,
Error = 0x101,
Warning = 0x102,
Info = 0x103,
Debug = 0x104,
}
impl TryFrom<log::Level> for Level {
type Error = ();
fn try_from(level: log::Level) -> Result<Self, <Self as TryFrom<log::Level>>::Error> {
match level {
log::Level::Error => Ok(Self::Error),
log::Level::Warn => Ok(Self::Warning),
log::Level::Info => Ok(Self::Info),
log::Level::Debug => Ok(Self::Debug),
log::Level::Trace => Err(()),
}
}
}
#[derive(Debug)]
struct Writer {
level: Level,
index: u8,
}
impl Writer {
fn new(level: Level) -> Self {
Self { level, index: 0 }
}
fn write_byte(&mut self, byte: u8) {
unsafe {
MGBA_LOG_BUFFER
.add(self.index as usize)
.write_volatile(byte);
}
let (index, overflowed) = self.index.overflowing_add(1);
self.index = index;
if overflowed {
unsafe {
MGBA_LOG_SEND.write_volatile(self.level);
}
}
}
fn send(&mut self) {
self.write_byte(b'\x00');
if self.index != 0 {
unsafe {
MGBA_LOG_SEND.write_volatile(self.level);
}
self.index = 0;
}
}
}
impl Write for Writer {
fn write_str(&mut self, s: &str) -> fmt::Result {
for &byte in s.as_bytes() {
match byte {
b'\n' => {
self.send();
}
b'\x00' => {
self.write_byte(b'\x1a');
}
_ => {
self.write_byte(byte);
}
}
}
Ok(())
}
}
impl Drop for Writer {
fn drop(&mut self) {
self.send();
}
}
#[derive(Debug)]
struct Logger;
impl Log for Logger {
fn enabled(&self, metadata: &Metadata) -> bool {
metadata.level() <= log::Level::Debug
}
fn log(&self, record: &Record) {
if let Ok(level) = Level::try_from(record.level()) {
let previous_ime = unsafe { IME.read_volatile() };
unsafe { IME.write_volatile(false) };
write(&mut &mut Writer::new(level), *record.args())
.unwrap_or_else(|error| panic!("write to mGBA log buffer failed: {}", error));
unsafe {
IME.write_volatile(previous_ime);
}
}
}
fn flush(&self) {}
}
#[macro_export]
macro_rules! fatal {
($($arg:tt)+) => ($crate::__fatal(format_args!($($arg)+)));
}
#[doc(hidden)]
pub fn __fatal(args: fmt::Arguments) {
if unsafe { MGBA_LOG_ENABLE.read_volatile() } == 0x1DEA {
unsafe { IME.write_volatile(false) };
#[allow(unused_must_use)]
{
write(&mut Writer::new(Level::Fatal), args);
}
}
}
#[derive(Debug)]
pub enum Error {
NotAcknowledgedByMgba,
SetLoggerError(SetLoggerError),
}
impl From<SetLoggerError> for Error {
fn from(error: SetLoggerError) -> Self {
Self::SetLoggerError(error)
}
}
impl Display for Error {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::NotAcknowledgedByMgba => fmt.write_str("mGBA did not acknowledge initialization"),
Self::SetLoggerError(error) => write!(fmt, "`log::set_logger()` error: {error}"),
}
}
}
static LOGGER: Logger = Logger;
pub fn init() -> Result<(), Error> {
unsafe {
MGBA_LOG_ENABLE.write(0xC0DE);
}
if unsafe { MGBA_LOG_ENABLE.read_volatile() } != 0x1DEA {
return Err(Error::NotAcknowledgedByMgba);
}
let previous_ime = unsafe { IME.read_volatile() };
unsafe { IME.write_volatile(false) };
compiler_fence(atomic::Ordering::Acquire);
let result = unsafe { log::set_logger_racy(&LOGGER) }
.map(|()| unsafe { log::set_max_level_racy(LevelFilter::Debug) })
.map_err(Into::into);
compiler_fence(atomic::Ordering::Release);
unsafe {
IME.write_volatile(previous_ime);
}
result
}