diff --git a/crates/common/src/error.rs b/crates/common/src/error.rs
index 1c5ae65c145e5c06d0af9920ed57baf88e204164..b12da2cf7777f00b67f9f028b0d08b843adf41ce 100644
--- a/crates/common/src/error.rs
+++ b/crates/common/src/error.rs
@@ -1,3 +1,5 @@
+#![allow(clippy::uninlined_format_args)]
+
 //! Error related data structures to be shared and used.
 //!
 //! This module contains the [`Error`] enum, which is used to represent
@@ -14,16 +16,18 @@ pub enum Error {
     InvalidData,
     RomSize,
     IncompatibleBootRom,
+    InvalidParameter(String),
     CustomError(String),
 }
 
 impl Error {
-    pub fn description(&self) -> &str {
+    pub fn description(&self) -> String {
         match self {
-            Error::InvalidData => "Invalid data format",
-            Error::RomSize => "Invalid ROM size",
-            Error::IncompatibleBootRom => "Incompatible Boot ROM",
-            Error::CustomError(message) => message,
+            Error::InvalidData => String::from("Invalid data format"),
+            Error::RomSize => String::from("Invalid ROM size"),
+            Error::IncompatibleBootRom => String::from("Incompatible Boot ROM"),
+            Error::InvalidParameter(message) => format!("Invalid parameter: {}", message),
+            Error::CustomError(message) => String::from(message),
         }
     }
 }
diff --git a/frontends/libretro/src/lib.rs b/frontends/libretro/src/lib.rs
index d8003c913f22fc4a3f7b8cbf311b8811a06e62bc..233ba9c36678391808871a5d2377f2f0d1f3f99f 100644
--- a/frontends/libretro/src/lib.rs
+++ b/frontends/libretro/src/lib.rs
@@ -453,7 +453,10 @@ pub extern "C" fn retro_serialize(data: *mut c_void, size: usize) -> bool {
         Ok(state) => state,
         Err(err) => {
             warnln!("Failed to save state: {}", err);
-            return false;
+            #[allow(unreachable_code)]
+            {
+                return false;
+            }
         }
     };
     if state.len() > size {
@@ -462,7 +465,10 @@ pub extern "C" fn retro_serialize(data: *mut c_void, size: usize) -> bool {
             state.len(),
             size
         );
-        return false;
+        #[allow(unreachable_code)]
+        {
+            return false;
+        }
     }
     unsafe {
         ptr::copy_nonoverlapping(state.as_ptr(), data as *mut u8, state.len());
@@ -477,7 +483,10 @@ pub extern "C" fn retro_unserialize(data: *const c_void, size: usize) -> bool {
     let state = unsafe { from_raw_parts(data as *const u8, size) };
     if let Err(err) = StateManager::load(state, instance, None) {
         warnln!("Failed to load state: {}", err);
-        return false;
+        #[allow(unreachable_code)]
+        {
+            return false;
+        }
     }
     true
 }
diff --git a/frontends/sdl/README.md b/frontends/sdl/README.md
index fbcc32970ece9b2dbb49dd46a10d1318de2ae472..ad9cba6fc9f70523df2cd580b9e730ef70419b7d 100644
--- a/frontends/sdl/README.md
+++ b/frontends/sdl/README.md
@@ -106,4 +106,4 @@ cargo run -- --rom-path ../../res/roms/test/blargg/cpu/cpu_instrs.gb --cycles 10
 | `debug`    | Activates the base `debug` feature from Boytacean.                                                                                         |
 | `pedantic` | Additional safety instructions are executed to make sure the machine does no run "out of tracks", making sure to run many `panic()` calls. |
 | `slow`     | Runs the emulator at a very slow page 60x slower to allow visual debugging.                                                                |
-| `cpulog`   | Prints a log of the CPU instruction executed.                                                                                              |
+| `cpulog`   | Prints a log of the CPU instruction executed - will fill the stdout quickly.                                                               |
diff --git a/frontends/sdl/src/main.rs b/frontends/sdl/src/main.rs
index 34987e3b2e4cfd16f257897a9309e4299a3ae3ee..c2312203996dc205f8952c7ec4c0eb1239592901 100644
--- a/frontends/sdl/src/main.rs
+++ b/frontends/sdl/src/main.rs
@@ -258,12 +258,12 @@ impl Emulator {
         }
     }
 
-    #[cfg(not(feature = "slow"))]
-    pub fn start_base(&mut self) {}
-
-    #[cfg(feature = "slow")]
     pub fn start_base(&mut self) {
-        self.logic_frequency = 100;
+        self.system.set_diag();
+        #[cfg(feature = "slow")]
+        {
+            self.logic_frequency = 100;
+        }
     }
 
     pub fn start_graphics(&mut self, sdl: &Sdl, screen_scale: f32) {
@@ -420,6 +420,10 @@ impl Emulator {
         }
     }
 
+    pub fn print_debug(&mut self) {
+        println!("{}", self.system.description_debug());
+    }
+
     pub fn limited(&self) -> bool {
         !self.unlimited
     }
@@ -504,6 +508,10 @@ impl Emulator {
                         keycode: Some(Keycode::P),
                         ..
                     } => self.toggle_palette(),
+                    Event::KeyDown {
+                        keycode: Some(Keycode::C),
+                        ..
+                    } => self.print_debug(),
                     Event::KeyDown {
                         keycode: Some(Keycode::E),
                         keymod,
@@ -1030,7 +1038,7 @@ fn main() {
         let mode = Cartridge::from_file(&args.rom_path).unwrap().gb_mode();
         game_boy.set_mode(mode);
     }
-    let device: Box<dyn SerialDevice> = build_device(&args.device);
+    let device: Box<dyn SerialDevice> = build_device(&args.device).unwrap();
     game_boy.set_ppu_enabled(!args.no_ppu);
     game_boy.set_apu_enabled(!args.no_apu);
     game_boy.set_dma_enabled(!args.no_dma);
@@ -1070,10 +1078,10 @@ fn main() {
     run(args, &mut emulator);
 }
 
-fn build_device(device: &str) -> Box<dyn SerialDevice> {
+fn build_device(device: &str) -> Result<Box<dyn SerialDevice>, Error> {
     match device {
-        "null" => Box::<NullDevice>::default(),
-        "stdout" => Box::<StdoutDevice>::default(),
+        "null" => Ok(Box::<NullDevice>::default()),
+        "stdout" => Ok(Box::<StdoutDevice>::default()),
         "printer" => {
             let mut printer = Box::<PrinterDevice>::default();
             printer.set_callback(|image_buffer| {
@@ -1087,9 +1095,12 @@ fn build_device(device: &str) -> Box<dyn SerialDevice> {
                 )
                 .unwrap();
             });
-            printer
+            Ok(printer)
         }
-        _ => panic!("Unsupported device: {}", device),
+        _ => Err(Error::InvalidParameter(format!(
+            "Unsupported device: {}",
+            device
+        ))),
     }
 }
 
diff --git a/frontends/sdl/src/sdl.rs b/frontends/sdl/src/sdl.rs
index 3037a607a7dac04ee4e7cc5c670a8ab936e107e3..9df42a78cf4e1d168a29d6778ad8bee51c5d21ca 100644
--- a/frontends/sdl/src/sdl.rs
+++ b/frontends/sdl/src/sdl.rs
@@ -86,7 +86,7 @@ impl SdlSystem {
 }
 
 /// Creates an SDL2 Surface structure from the provided
-/// bytes that represent an image (eg: an PNG image buffer).
+/// bytes that represent an image (eg: a PNG image buffer).
 pub fn surface_from_bytes(bytes: &[u8]) -> Surface {
     unsafe {
         let rw_ops = RWops::from_bytes(bytes).unwrap();
diff --git a/src/apu.rs b/src/apu.rs
index 0402f31d21a39058df8dfde05c8b9f3107a9aaf9..810ed09e237077a3602b9a2104a1b78759730786 100644
--- a/src/apu.rs
+++ b/src/apu.rs
@@ -349,7 +349,7 @@ impl Apu {
         }
     }
 
-    pub fn read(&mut self, addr: u16) -> u8 {
+    pub fn read(&self, addr: u16) -> u8 {
         match addr {
             // 0xFF10 — NR10: Channel 1 sweep
             0xff10 => {
@@ -449,6 +449,7 @@ impl Apu {
 
             _ => {
                 warnln!("Reading from unknown APU location 0x{:04x}", addr);
+                #[allow(unreachable_code)]
                 0xff
             }
         }
@@ -658,11 +659,11 @@ impl Apu {
         }
     }
 
-    pub fn read_unsafe(&mut self, addr: u16) -> u8 {
+    pub fn read_raw(&mut self, addr: u16) -> u8 {
         self.read(addr)
     }
 
-    pub fn write_unsafe(&mut self, addr: u16, value: u8) {
+    pub fn write_raw(&mut self, addr: u16, value: u8) {
         self.write(addr, value);
     }
 
@@ -1079,7 +1080,7 @@ impl Apu {
 }
 
 impl BusComponent for Apu {
-    fn read(&mut self, addr: u16) -> u8 {
+    fn read(&self, addr: u16) -> u8 {
         self.read(addr)
     }
 
diff --git a/src/cpu.rs b/src/cpu.rs
index d1049cd7ba5dc9ddebdd146f256b4a1f51e2f2e4..53aef909a4a55279898fc14986fe0d285f932da4 100644
--- a/src/cpu.rs
+++ b/src/cpu.rs
@@ -5,10 +5,14 @@
 //!
 //! Most of the core CPU logic is implemented in the [`Cpu::clock`] method.
 
-use std::sync::Mutex;
+use std::{
+    fmt::{self, Display, Formatter},
+    sync::Mutex,
+};
 
 use crate::{
     apu::Apu,
+    assert_pedantic_gb,
     consts::LCDC_ADDR,
     debugln,
     dma::Dma,
@@ -24,6 +28,8 @@ use crate::{
 
 pub const PREFIX: u8 = 0xcb;
 
+pub type Instruction = &'static (fn(&mut Cpu), u8, &'static str);
+
 pub struct Cpu {
     pub pc: u16,
     pub sp: u16,
@@ -50,6 +56,11 @@ pub struct Cpu {
     /// taken by the current or last CPU operation.
     pub cycles: u8,
 
+    /// Reference to the PC (Program Counter) of the previous executed
+    /// instruction, used to provide a reference to the instruction
+    /// so that it can be logged or used for debugging purposes.
+    pub ppc: u16,
+
     /// The pointer to the parent configuration of the running
     /// Game Boy emulator, that can be used to control the behaviour
     /// of Game Boy emulation.
@@ -76,6 +87,7 @@ impl Cpu {
             halted: false,
             mmu,
             cycles: 0,
+            ppc: 0x0,
             gbc,
         }
     }
@@ -131,10 +143,18 @@ impl Cpu {
         // is going to be used in the fetching phase
         let pc = self.pc;
 
-        #[cfg(feature = "debug")]
-        if (0x8000..0x9fff).contains(&pc) {
-            panic!("Invalid PC area at 0x{:04x}", pc);
-        }
+        // runs a series of assertions to guarantee CPU execution
+        // state, only if pedantic mode is set
+        assert_pedantic_gb!(
+            !(0x8000..=0x9fff).contains(&pc),
+            "Invalid PC area at 0x{:04x}",
+            pc
+        );
+        assert_pedantic_gb!(
+            !self.mmu.boot_active() || pc <= 0x08ff,
+            "Invalid boot address: {:04x}",
+            pc
+        );
 
         // @TODO this is so bad, need to improve this by an order
         // of magnitude, to be able to have better performance
@@ -265,21 +285,13 @@ impl Cpu {
             return 4;
         }
 
-        // fetches the current instruction and increments
-        // the PC (program counter) accordingly
-        let mut opcode = self.mmu.read(self.pc);
-        self.pc = self.pc.wrapping_add(1);
-
-        let is_prefix = opcode == PREFIX;
-        let inst: &(fn(&mut Cpu), u8, &str);
-
-        if is_prefix {
-            opcode = self.mmu.read(self.pc);
-            self.pc = self.pc.wrapping_add(1);
-            inst = &EXTENDED[opcode as usize];
-        } else {
-            inst = &INSTRUCTIONS[opcode as usize];
-        }
+        // fetches the current instruction and updates the PC
+        // (Program Counter) according to the final value returned
+        // by the fetch operation (we may need to fetch instruction
+        // more than one byte of length)
+        let (inst, pc) = self.fetch(self.pc);
+        self.ppc = self.pc;
+        self.pc = pc;
 
         #[allow(unused_variables)]
         let (inst_fn, inst_time, inst_str) = inst;
@@ -300,19 +312,7 @@ impl Cpu {
 
         #[cfg(feature = "cpulog")]
         {
-            let title_str = format!("[0x{:04x}] {}", self.pc - 1, inst_str);
-            let inst_time_str = format!("({} cycles)", inst_time);
-            let registers_str = format!("[PC=0x{:04x} SP=0x{:04x}] [A=0x{:02x} B=0x{:02x} C=0x{:02x} D=0x{:02x} E=0x{:02x} H=0x{:02x} L=0x{:02x}]",
-            self.pc, self.sp, self.a, self.b, self.c, self.d, self.e, self.h, self.l);
-            println!(
-                "{0: <24} {1: <11} {2: <10}",
-                title_str, inst_time_str, registers_str
-            );
-        }
-
-        #[cfg(feature = "pedantic")]
-        if self.mmu.boot_active() && self.pc - 1 > 0x08ff {
-            panic!("Invalid boot address: {:04x}", self.pc - 1);
+            println!("{}", self.description(inst, self.ppc));
         }
 
         // calls the current instruction and increments the number of
@@ -327,6 +327,33 @@ impl Cpu {
         self.cycles
     }
 
+    #[inline(always)]
+    fn fetch(&self, pc: u16) -> (Instruction, u16) {
+        let mut pc = pc;
+
+        // fetches the current instruction and increments
+        // the PC (program counter) accordingly
+        let mut opcode = self.mmu.read(pc);
+        pc = pc.wrapping_add(1);
+
+        // checks if the current instruction is a prefix
+        // instruction, in case it is, fetches the next
+        // instruction and increments the PC accordingly
+        let inst: Instruction;
+        let is_prefix = opcode == PREFIX;
+        if is_prefix {
+            opcode = self.mmu.read(pc);
+            pc = pc.wrapping_add(1);
+            inst = &EXTENDED[opcode as usize];
+        } else {
+            inst = &INSTRUCTIONS[opcode as usize];
+        }
+
+        // returns both the fetched instruction and the
+        // updated PC (Program Counter) value
+        (inst, pc)
+    }
+
     #[inline(always)]
     pub fn mmu(&mut self) -> &mut Mmu {
         &mut self.mmu
@@ -617,6 +644,23 @@ impl Cpu {
     pub fn set_gbc(&mut self, value: SharedThread<GameBoyConfig>) {
         self.gbc = value;
     }
+
+    pub fn description(&self, inst: Instruction, inst_pc: u16) -> String {
+        let (_, inst_time, inst_str) = inst;
+        let title_str: String = format!("[0x{:04x}] {}", inst_pc, inst_str);
+        let inst_time_str = format!("({} cycles)", inst_time);
+        let registers_str = format!("[PC=0x{:04x} SP=0x{:04x}] [A=0x{:02x} B=0x{:02x} C=0x{:02x} D=0x{:02x} E=0x{:02x} H=0x{:02x} L=0x{:02x}]",
+        self.pc, self.sp, self.a, self.b, self.c, self.d, self.e, self.h, self.l);
+        format!(
+            "{0: <24} {1: <11} {2: <10}",
+            title_str, inst_time_str, registers_str
+        )
+    }
+
+    pub fn description_default(&self) -> String {
+        let (inst, _) = self.fetch(self.ppc);
+        self.description(inst, self.ppc)
+    }
 }
 
 impl Default for Cpu {
@@ -626,6 +670,12 @@ impl Default for Cpu {
     }
 }
 
+impl Display for Cpu {
+    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+        write!(f, "{}", self.description_default())
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::Cpu;
diff --git a/src/devices/printer.rs b/src/devices/printer.rs
index bdfe76f5b26544199fe49182160f512623498854..910b24d82a6d6af3403137acd12097252dc13cc0 100644
--- a/src/devices/printer.rs
+++ b/src/devices/printer.rs
@@ -266,7 +266,10 @@ impl SerialDevice for PrinterDevice {
             PrinterState::MagicBytes1 => {
                 if byte != 0x88 {
                     warnln!("Printer: Invalid magic byte 1: {:02x}", byte);
-                    return;
+                    #[allow(unreachable_code)]
+                    {
+                        return;
+                    }
                 }
                 self.command = PrinterCommand::Other;
                 self.command_length = 0;
@@ -277,7 +280,10 @@ impl SerialDevice for PrinterDevice {
                         self.state = PrinterState::MagicBytes1;
                     }
                     warnln!("Printer: Invalid magic byte 2: {:02x}", byte);
-                    return;
+                    #[allow(unreachable_code)]
+                    {
+                        return;
+                    }
                 }
             }
             PrinterState::Identification => self.command = PrinterCommand::from_u8(byte),
@@ -308,7 +314,10 @@ impl SerialDevice for PrinterDevice {
             }
             PrinterState::Other => {
                 warnln!("Printer: Invalid state: {:02x}", self.state as u8);
-                return;
+                #[allow(unreachable_code)]
+                {
+                    return;
+                }
             }
         }
 
diff --git a/src/diag.rs b/src/diag.rs
new file mode 100644
index 0000000000000000000000000000000000000000..b33d9c4ae631747468d9740ab3ec6fd118d3207d
--- /dev/null
+++ b/src/diag.rs
@@ -0,0 +1,103 @@
+use std::ptr::null;
+
+use crate::gb::GameBoy;
+
+/// Static mutable reference to the global instance of the
+/// Game Boy emulator, going to be used for global diagnostics.
+static mut GLOBAL_INSTANCE: *const GameBoy = null();
+
+impl GameBoy {
+    /// Sets the current instance as the one going to be used
+    /// in panic diagnostics.
+    pub fn set_diag(&self) {
+        self.set_global();
+    }
+
+    /// Dumps the diagnostics for the global instance of the
+    /// Boytacean emulator into stdout.
+    pub fn dump_diagnostics() {
+        if let Some(gb) = Self::get_global() {
+            gb.dump_diagnostics_s();
+        }
+    }
+
+    /// Sets the current instance as the global one going to
+    /// be used in panic diagnostics.
+    fn set_global(&self) {
+        unsafe {
+            GLOBAL_INSTANCE = self;
+        }
+    }
+
+    /// Obtains the global instance of the Game Boy emulator
+    /// ready to be used in diagnostics.
+    ///
+    /// If the global instance is not set, it will return `None`.
+    fn get_global() -> Option<&'static Self> {
+        unsafe {
+            if GLOBAL_INSTANCE.is_null() {
+                None
+            } else {
+                Some(&*GLOBAL_INSTANCE)
+            }
+        }
+    }
+
+    fn dump_diagnostics_s(&self) {
+        println!("Dumping Boytacean diagnostics:");
+        println!("{}", self.description_debug());
+    }
+}
+
+#[macro_export]
+macro_rules! panic_gb {
+    ($msg:expr) => {
+        {
+            $crate::gb::GameBoy::dump_diagnostics();
+            panic!($msg);
+        }
+    };
+    ($fmt:expr, $($arg:tt)*) => {
+        {
+            $crate::gb::GameBoy::dump_diagnostics();
+            panic!($fmt, $($arg)*);
+        }
+    };
+}
+
+#[macro_export]
+macro_rules! assert_gb {
+    ($cond:expr, $fmt:expr, $($arg:tt)*) => {
+        if !$cond {
+            if !$cond {
+                $crate::gb::GameBoy::dump_diagnostics();
+                panic!($fmt, $($arg)*);
+            }
+        }
+    };
+    ($cond:expr) => {
+        assert_gb!($cond, stringify!($cond));
+    };
+}
+
+#[cfg(feature = "pedantic")]
+#[macro_export]
+macro_rules! assert_pedantic_gb {
+    ($cond:expr, $fmt:expr, $($arg:tt)*) => {
+        $crate::assert_gb!($cond, $fmt, $($arg)*);
+    };
+    ($cond:expr) => {
+        $crate::assert_gb!($cond);
+    };
+}
+
+#[cfg(not(feature = "pedantic"))]
+#[macro_export]
+macro_rules! assert_pedantic_gb {
+    ($cond:expr, $fmt:expr, $($arg:tt)*) => {
+        ()
+    };
+    ($cond:expr) => {
+        ()
+    };
+}
diff --git a/src/dma.rs b/src/dma.rs
index c0bc385a3f05dc39b396f900f80fa3460a35c37a..ced235655f8ab5a004b43f2291de8807bc5270f8 100644
--- a/src/dma.rs
+++ b/src/dma.rs
@@ -1,9 +1,11 @@
 //! DMA (Direct Memory Access) functions and structures.
 
+use std::fmt::{self, Display, Formatter};
+
 use crate::{
     consts::{DMA_ADDR, HDMA1_ADDR, HDMA2_ADDR, HDMA3_ADDR, HDMA4_ADDR, HDMA5_ADDR},
     mmu::BusComponent,
-    warnln,
+    panic_gb, warnln,
 };
 
 #[derive(Clone, Copy, PartialEq, Eq, Debug)]
@@ -12,6 +14,35 @@ pub enum DmaMode {
     HBlank = 0x01,
 }
 
+impl DmaMode {
+    pub fn description(&self) -> &'static str {
+        match self {
+            DmaMode::General => "General-Purpose DMA",
+            DmaMode::HBlank => "HBlank DMA",
+        }
+    }
+
+    pub fn from_u8(value: u8) -> Self {
+        match value {
+            0x00 => DmaMode::General,
+            0x01 => DmaMode::HBlank,
+            _ => DmaMode::General,
+        }
+    }
+}
+
+impl Display for DmaMode {
+    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+        write!(f, "{}", self.description())
+    }
+}
+
+impl From<u8> for DmaMode {
+    fn from(value: u8) -> Self {
+        Self::from_u8(value)
+    }
+}
+
 pub struct Dma {
     source: u16,
     destination: u16,
@@ -53,7 +84,7 @@ impl Dma {
 
     pub fn clock(&mut self, _cycles: u16) {}
 
-    pub fn read(&mut self, addr: u16) -> u8 {
+    pub fn read(&self, addr: u16) -> u8 {
         match addr {
             // 0xFF46 — DMA: OAM DMA source address & start
             DMA_ADDR => self.value_dma,
@@ -63,6 +94,7 @@ impl Dma {
             }
             _ => {
                 warnln!("Reading from unknown DMA location 0x{:04x}", addr);
+                #[allow(unreachable_code)]
                 0xff
             }
         }
@@ -86,14 +118,27 @@ impl Dma {
             HDMA4_ADDR => self.destination = (self.destination & 0xff00) | ((value & 0xf0) as u16),
             // 0xFF55 — HDMA5: VRAM DMA length/mode/start (CGB only)
             HDMA5_ADDR => {
-                self.length = (((value & 0x7f) + 0x1) as u16) << 4;
-                self.mode = match (value & 80) >> 7 {
-                    0 => DmaMode::General,
-                    1 => DmaMode::HBlank,
-                    _ => DmaMode::General,
-                };
-                self.pending = self.length;
-                self.active_hdma = true;
+                // in case there's an active HDMA transfer and the
+                // bit 7 is set to 0, the transfer is stopped
+                if value & 0x80 == 0x00 && self.active_hdma && self.mode == DmaMode::HBlank {
+                    self.pending = 0;
+                    self.active_hdma = false;
+                } else {
+                    // ensures destination is set within VRAM range
+                    // required for compatibility with some games (know bug)
+                    self.destination = 0x8000 | (self.destination & 0x1fff);
+                    self.length = (((value & 0x7f) + 0x1) as u16) << 4;
+                    self.mode = ((value & 80) >> 7).into();
+                    self.pending = self.length;
+                    self.active_hdma = true;
+
+                    // @TODO: implement HBlank DMA using the proper timing
+                    // and during the HBlank period as described in the
+                    // https://gbdev.io/pandocs/CGB_Registers.html#lcd-vram-dma-transfers
+                    if self.mode == DmaMode::HBlank {
+                        panic_gb!("HBlank DMA not implemented");
+                    }
+                }
             }
             _ => warnln!("Writing to unknown DMA location 0x{:04x}", addr),
         }
@@ -174,10 +219,32 @@ impl Dma {
     pub fn active(&self) -> bool {
         self.active_dma || self.active_hdma
     }
+
+    pub fn description(&self) -> String {
+        format!(
+            "DMA: {}\nHDMA: {}",
+            self.description_dma(),
+            self.description_hdma()
+        )
+    }
+
+    pub fn description_dma(&self) -> String {
+        format!(
+            "active: {}, cycles: {}, value: 0x{:02x}",
+            self.active_dma, self.cycles_dma, self.value_dma
+        )
+    }
+
+    pub fn description_hdma(&self) -> String {
+        format!(
+            "active: {}, length: 0x{:04x}, mode: {}, source: 0x{:04x}, destination: 0x{:04x}",
+            self.active_hdma, self.length, self.mode, self.source, self.destination
+        )
+    }
 }
 
 impl BusComponent for Dma {
-    fn read(&mut self, addr: u16) -> u8 {
+    fn read(&self, addr: u16) -> u8 {
         self.read(addr)
     }
 
@@ -192,6 +259,12 @@ impl Default for Dma {
     }
 }
 
+impl Display for Dma {
+    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+        write!(f, "{}", self.description())
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::{Dma, DmaMode};
diff --git a/src/gb.rs b/src/gb.rs
index 5f86cf006fe6606cbfa0d955ea21237af198081d..5d18d316303cca24eb05b020b33c1a0294e69f7d 100644
--- a/src/gb.rs
+++ b/src/gb.rs
@@ -1022,6 +1022,15 @@ impl GameBoy {
             self.serial_i().device().description(),
         )
     }
+
+    pub fn description_debug(&self) -> String {
+        format!(
+            "{}\nCPU:\n{}\nDMA:\n{}",
+            self.description(12),
+            self.cpu_i().description_default(),
+            self.dma_i().description()
+        )
+    }
 }
 
 /// Gameboy implementations that are meant with performance
diff --git a/src/lib.rs b/src/lib.rs
index 318726e5fb984bc56d0caa6f4151056e4c9f5024..6538d3c3402c3d07fee1dcd1c4fe44ac15a1fba9 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -8,6 +8,7 @@ pub mod consts;
 pub mod cpu;
 pub mod data;
 pub mod devices;
+pub mod diag;
 pub mod dma;
 pub mod gb;
 pub mod gen;
diff --git a/src/macros.rs b/src/macros.rs
index 4fb08076a9b1296b8038d3e8822eaf6ab1bf0701..a9344317919e4922252ab1b30555ee8bf9bb4399 100644
--- a/src/macros.rs
+++ b/src/macros.rs
@@ -32,7 +32,7 @@ macro_rules! infoln {
 macro_rules! warnln {
     ($($rest:tt)*) => {
         {
-            panic!($($rest)*);
+            $crate::panic_gb!($($rest)*);
         }
     }
 }
diff --git a/src/mmu.rs b/src/mmu.rs
index cf8f41734490e82c21b007132474299bf305db25..d3cbd31301db115636dd831fb9f26983e68fa543 100644
--- a/src/mmu.rs
+++ b/src/mmu.rs
@@ -4,9 +4,11 @@ use std::sync::Mutex;
 
 use crate::{
     apu::Apu,
+    assert_pedantic_gb,
     dma::Dma,
     gb::{Components, GameBoyConfig, GameBoyMode, GameBoySpeed},
     pad::Pad,
+    panic_gb,
     ppu::Ppu,
     rom::Cartridge,
     serial::Serial,
@@ -22,9 +24,9 @@ pub const RAM_SIZE_DMG: usize = 8192;
 pub const RAM_SIZE_CGB: usize = 32768;
 
 pub trait BusComponent {
-    fn read(&mut self, addr: u16) -> u8;
+    fn read(&self, addr: u16) -> u8;
     fn write(&mut self, addr: u16, value: u8);
-    fn read_many(&mut self, addr: u16, count: usize) -> Vec<u8> {
+    fn read_many(&self, addr: u16, count: usize) -> Vec<u8> {
         (0..count)
             .map(|offset| self.read(addr + offset as u16))
             .collect()
@@ -196,7 +198,7 @@ impl Mmu {
                 match base_addr {
                     0xa000 => self.rom.ram_data_mut()[addr as usize] = value,
                     0xc000 => self.ram[addr as usize] = value,
-                    _ => panic!("Invalid base address for write: 0x{:04x}", base_addr),
+                    _ => panic_gb!("Invalid base address for write: 0x{:04x}", base_addr),
                 }
             }
         }
@@ -306,6 +308,20 @@ impl Mmu {
         //
         // we should also respect General-Purpose DMA vs HBlank DMA
         if self.dma.active_hdma() {
+            // runs a series of pre-validation on the HDMA transfer in
+            // pedantic mode is currently active (performance hit)
+            assert_pedantic_gb!(
+                (0x0000..=0x7ff0).contains(&self.dma.source())
+                    || (0xa000..=0xdff0).contains(&self.dma.source()),
+                "Invalid HDMA source start memory address 0x{:04x}",
+                self.dma.source()
+            );
+            assert_pedantic_gb!(
+                (0x8000..=0x9ff0).contains(&self.dma.destination()),
+                "Invalid HDMA destination start memory address 0x{:04x}",
+                self.dma.destination()
+            );
+
             // only runs the DMA transfer if the system is in CGB mode
             // this avoids issues when writing to DMG unmapped registers
             // that would otherwise cause the system to crash
@@ -318,19 +334,13 @@ impl Mmu {
         }
     }
 
-    pub fn read(&mut self, addr: u16) -> u8 {
+    pub fn read(&self, addr: u16) -> u8 {
         match addr {
             // 0x0000-0x0FFF - BOOT (256 B) + ROM0 (4 KB/16 KB)
             0x0000..=0x0fff => {
                 // in case the boot mode is active and the
                 // address is withing boot memory reads from it
-                if self.boot_active && addr <= 0x00fe {
-                    // if we're reading from this location we can
-                    // safely assume that we're exiting the boot
-                    // loading sequence and disable boot
-                    if addr == 0x00fe {
-                        self.boot_active = false;
-                    }
+                if self.boot_active && addr <= 0x00ff {
                     return self.boot[addr as usize];
                 }
                 if self.boot_active
@@ -428,6 +438,7 @@ impl Mmu {
 
             addr => {
                 warnln!("Reading from unknown location 0x{:04x}", addr);
+                #[allow(unreachable_code)]
                 0xff
             }
         }
@@ -535,9 +546,9 @@ impl Mmu {
 
     /// Reads a byte from a certain memory address, without the typical
     /// Game Boy verifications, allowing deep read of values.
-    pub fn read_unsafe(&mut self, addr: u16) -> u8 {
+    pub fn read_raw(&mut self, addr: u16) -> u8 {
         match addr {
-            0xff10..=0xff3f => self.apu.read_unsafe(addr),
+            0xff10..=0xff3f => self.apu.read_raw(addr),
             _ => self.read(addr),
         }
     }
@@ -546,9 +557,9 @@ impl Mmu {
     /// Game Boy verification process. This allows for faster memory
     /// access in registers and other memory areas that are typically
     /// inaccessible.
-    pub fn write_unsafe(&mut self, addr: u16, value: u8) {
+    pub fn write_raw(&mut self, addr: u16, value: u8) {
         match addr {
-            0xff10..=0xff3f => self.apu.write_unsafe(addr, value),
+            0xff10..=0xff3f => self.apu.write_raw(addr, value),
             _ => self.write(addr, value),
         }
     }
@@ -574,7 +585,7 @@ impl Mmu {
         let mut data: Vec<u8> = vec![];
 
         for index in 0..count {
-            let byte = self.read_unsafe(addr + index);
+            let byte = self.read_raw(addr + index);
             data.push(byte);
         }
 
@@ -583,7 +594,7 @@ impl Mmu {
 
     pub fn write_many_unsafe(&mut self, addr: u16, data: &[u8]) {
         for (index, byte) in data.iter().enumerate() {
-            self.write_unsafe(addr + index as u16, *byte)
+            self.write_raw(addr + index as u16, *byte)
         }
     }
 
diff --git a/src/pad.rs b/src/pad.rs
index aba8acc1c741f2e8b755c899eaf626a4d899b037..62dd6080da4b31a552b397817c090a5b8ab6e2b8 100644
--- a/src/pad.rs
+++ b/src/pad.rs
@@ -75,7 +75,7 @@ impl Pad {
         }
     }
 
-    pub fn read(&mut self, addr: u16) -> u8 {
+    pub fn read(&self, addr: u16) -> u8 {
         match addr {
             // 0xFF00 — P1/JOYP: Joypad
             0xff00 => {
@@ -107,6 +107,7 @@ impl Pad {
             }
             _ => {
                 warnln!("Reading from unknown Pad location 0x{:04x}", addr);
+                #[allow(unreachable_code)]
                 0xff
             }
         }
@@ -174,7 +175,7 @@ impl Pad {
 }
 
 impl BusComponent for Pad {
-    fn read(&mut self, addr: u16) -> u8 {
+    fn read(&self, addr: u16) -> u8 {
         self.read(addr)
     }
 
diff --git a/src/ppu.rs b/src/ppu.rs
index ac1a73be110eab8c3ee0da0de5c95d4f101c4c1f..ccb1ae61ca026e447f6634e0c59609401f1080e1 100644
--- a/src/ppu.rs
+++ b/src/ppu.rs
@@ -21,6 +21,7 @@ use crate::{
     },
     gb::{GameBoyConfig, GameBoyMode},
     mmu::BusComponent,
+    panic_gb,
     util::SharedThread,
     warnln,
 };
@@ -780,7 +781,7 @@ impl Ppu {
         }
     }
 
-    pub fn read(&mut self, addr: u16) -> u8 {
+    pub fn read(&self, addr: u16) -> u8 {
         match addr {
             0x8000..=0x9fff => self.vram[(self.vram_offset + (addr & 0x1fff)) as usize],
             0xfe00..=0xfe9f => self.oam[(addr & 0x009f) as usize],
@@ -837,6 +838,7 @@ impl Ppu {
             0xff6c => (if self.obj_priority { 0x01 } else { 0x00 }) | 0xfe,
             _ => {
                 warnln!("Reading from unknown PPU location 0x{:04x}", addr);
+                #[allow(unreachable_code)]
                 0xff
             }
         }
@@ -1777,7 +1779,7 @@ impl Ppu {
                     } else if obj.palette == 1 {
                         (&self.palette_obj_1, 0_u8)
                     } else {
-                        panic!("Invalid object palette: {:02x}", obj.palette);
+                        panic_gb!("Invalid object palette: {:02x}", obj.palette);
                     }
                 } else {
                     (&self.palettes_color_obj[obj.palette_cgb as usize], 0_u8)
@@ -1787,7 +1789,7 @@ impl Ppu {
             } else if obj.palette == 1 {
                 (&self.palette_obj_1, 2_u8)
             } else {
-                panic!("Invalid object palette: {:02x}", obj.palette);
+                panic_gb!("Invalid object palette: {:02x}", obj.palette);
             };
 
             // obtains the current integer value (raw) for the palette in use
@@ -1987,7 +1989,7 @@ impl Ppu {
             let color_index: usize = (value as usize >> (index * 2)) & 3;
             match color_index {
                 0..=3 => *palette_item = palette_colors[color_index],
-                color_index => panic!("Invalid palette color index {:04x}", color_index),
+                color_index => panic_gb!("Invalid palette color index {:04x}", color_index),
             }
         }
     }
@@ -2045,7 +2047,7 @@ impl Ppu {
 }
 
 impl BusComponent for Ppu {
-    fn read(&mut self, addr: u16) -> u8 {
+    fn read(&self, addr: u16) -> u8 {
         self.read(addr)
     }
 
diff --git a/src/rom.rs b/src/rom.rs
index 572c86ac1ee59f3973ab4b82e6a6ca81312e5d92..bc288768d01b748dcf121ea3e54dd41e7304a997 100644
--- a/src/rom.rs
+++ b/src/rom.rs
@@ -14,6 +14,7 @@ use crate::{
     gb::GameBoyMode,
     licensee::Licensee,
     mmu::BusComponent,
+    panic_gb,
     util::read_file,
     warnln,
 };
@@ -424,7 +425,7 @@ impl Cartridge {
         Self::from_data(&data)
     }
 
-    pub fn read(&mut self, addr: u16) -> u8 {
+    pub fn read(&self, addr: u16) -> u8 {
         match addr {
             // 0x0000-0x7FFF: 16 KiB ROM bank 00 & 16 KiB ROM Bank 01–NN
             0x0000..=0x7fff => (self.handler.read_rom)(self, addr),
@@ -930,7 +931,7 @@ impl Cartridge {
 }
 
 impl BusComponent for Cartridge {
-    fn read(&mut self, addr: u16) -> u8 {
+    fn read(&self, addr: u16) -> u8 {
         self.read(addr)
     }
 
@@ -968,7 +969,7 @@ pub static NO_MBC: Mbc = Mbc {
             // to this address for some reason (probably related to
             // some kind of MBC1 compatibility issue)
             0x2000 => (),
-            _ => panic!("Writing to unknown Cartridge ROM location 0x{:04x}", addr),
+            _ => panic_gb!("Writing to unknown Cartridge ROM location 0x{:04x}", addr),
         };
     },
     read_ram: |rom: &Cartridge, addr: u16| -> u8 { rom.ram_data[(addr - 0xa000) as usize] },
@@ -988,6 +989,7 @@ pub static MBC1: Mbc = Mbc {
                 .unwrap_or(&0x0),
             _ => {
                 warnln!("Reading from unknown Cartridge ROM location 0x{:04x}", addr);
+                #[allow(unreachable_code)]
                 0xff
             }
         }
@@ -1033,7 +1035,10 @@ pub static MBC1: Mbc = Mbc {
     write_ram: |rom: &mut Cartridge, addr: u16, value: u8| {
         if !rom.ram_enabled {
             warnln!("Attempt to write to ERAM while write protect is active");
-            return;
+            #[allow(unreachable_code)]
+            {
+                return;
+            }
         }
         rom.ram_data[rom.ram_offset + (addr - 0xa000) as usize] = value;
     },
@@ -1050,6 +1055,7 @@ pub static MBC3: Mbc = Mbc {
                 .unwrap_or(&0x0),
             _ => {
                 warnln!("Reading from unknown Cartridge ROM location 0x{:04x}", addr);
+                #[allow(unreachable_code)]
                 0xff
             }
         }
@@ -1089,7 +1095,10 @@ pub static MBC3: Mbc = Mbc {
     write_ram: |rom: &mut Cartridge, addr: u16, value: u8| {
         if !rom.ram_enabled {
             warnln!("Attempt to write to ERAM while write protect is active");
-            return;
+            #[allow(unreachable_code)]
+            {
+                return;
+            }
         }
         rom.ram_data[rom.ram_offset + (addr - 0xa000) as usize] = value;
     },
@@ -1106,6 +1115,7 @@ pub static MBC5: Mbc = Mbc {
                 .unwrap_or(&0x0),
             _ => {
                 warnln!("Reading from unknown Cartridge ROM location 0x{:04x}", addr);
+                #[allow(unreachable_code)]
                 0xff
             }
         }
@@ -1159,7 +1169,10 @@ pub static MBC5: Mbc = Mbc {
     write_ram: |rom: &mut Cartridge, addr: u16, value: u8| {
         if !rom.ram_enabled {
             warnln!("Attempt to write to ERAM while write protect is active");
-            return;
+            #[allow(unreachable_code)]
+            {
+                return;
+            }
         }
         rom.ram_data[rom.ram_offset + (addr - 0xa000) as usize] = value;
     },
diff --git a/src/serial.rs b/src/serial.rs
index 9c5ff0738aada1bdfbe8ded28813f55eab6d0b4d..ac0db54c9e235161962535459a892801440eefbd 100644
--- a/src/serial.rs
+++ b/src/serial.rs
@@ -90,7 +90,7 @@ impl Serial {
         }
     }
 
-    pub fn read(&mut self, addr: u16) -> u8 {
+    pub fn read(&self, addr: u16) -> u8 {
         match addr {
             // 0xFF01 — SB: Serial transfer data
             SB_ADDR => self.data,
@@ -104,6 +104,7 @@ impl Serial {
             }
             _ => {
                 warnln!("Reding from unknown Serial location 0x{:04x}", addr);
+                #[allow(unreachable_code)]
                 0xff
             }
         }
@@ -209,7 +210,7 @@ impl Serial {
 }
 
 impl BusComponent for Serial {
-    fn read(&mut self, addr: u16) -> u8 {
+    fn read(&self, addr: u16) -> u8 {
         self.read(addr)
     }
 
diff --git a/src/state.rs b/src/state.rs
index 8c4c917d0102fa113502b3b79db41577200450f6..e4f191200be8cf765ba05dbbf86913f2cbcae8ed 100644
--- a/src/state.rs
+++ b/src/state.rs
@@ -1659,7 +1659,7 @@ mod tests {
         assert_eq!(bess_core.de, 0x0000);
         assert_eq!(bess_core.hl, 0x0000);
         assert_eq!(bess_core.sp, 0x0000);
-        assert_eq!(bess_core.ime, false);
+        assert!(!bess_core.ime);
         assert_eq!(bess_core.ie, 0x00);
         assert_eq!(bess_core.execution_mode, 0);
         assert_eq!(bess_core.io_registers.len(), 128);
diff --git a/src/timer.rs b/src/timer.rs
index c70b8ff17e967f7da8c0881207ecebb855054aec..2fe4038f74142aad62afa6be2b8d9f8bd4a02724 100644
--- a/src/timer.rs
+++ b/src/timer.rs
@@ -3,7 +3,7 @@
 use crate::{
     consts::{DIV_ADDR, TAC_ADDR, TIMA_ADDR, TMA_ADDR},
     mmu::BusComponent,
-    warnln,
+    panic_gb, warnln,
 };
 
 pub struct Timer {
@@ -73,7 +73,7 @@ impl Timer {
         }
     }
 
-    pub fn read(&mut self, addr: u16) -> u8 {
+    pub fn read(&self, addr: u16) -> u8 {
         match addr {
             // 0xFF04 — DIV: Divider register
             DIV_ADDR => self.div,
@@ -85,6 +85,7 @@ impl Timer {
             TAC_ADDR => self.tac | 0xf8,
             _ => {
                 warnln!("Reding from unknown Timer location 0x{:04x}", addr);
+                #[allow(unreachable_code)]
                 0xff
             }
         }
@@ -106,7 +107,7 @@ impl Timer {
                     0x01 => self.tima_ratio = 16,
                     0x02 => self.tima_ratio = 64,
                     0x03 => self.tima_ratio = 256,
-                    value => panic!("Invalid TAC value 0x{:02x}", value),
+                    value => panic_gb!("Invalid TAC value 0x{:02x}", value),
                 }
                 self.tima_enabled = (value & 0x04) == 0x04;
             }
@@ -151,7 +152,7 @@ impl Timer {
 }
 
 impl BusComponent for Timer {
-    fn read(&mut self, addr: u16) -> u8 {
+    fn read(&self, addr: u16) -> u8 {
         self.read(addr)
     }