diff --git a/src/lib.rs b/src/lib.rs index 4263d00..e79f60c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -98,6 +98,9 @@ use prelude::{GbaCell, IrqFn}; mod macros; +#[cfg(test)] +mod test_harness; + #[cfg(feature = "on_gba")] mod asm_runtime; #[cfg(feature = "on_gba")] @@ -143,15 +146,8 @@ impl Align4<[u8; N]> { /// * If the number of bytes isn't a multiple of 4 #[inline] #[must_use] - pub fn as_u32_slice(&self) -> &[u32] { - assert!(self.0.len() % 4 == 0); - // Safety: our struct is aligned to 4, so the pointer will already be - // aligned, we only need to check the length - unsafe { - let data: *const u8 = self.0.as_ptr(); - let len: usize = self.0.len(); - core::slice::from_raw_parts(data.cast::(), len / 4) - } + pub const fn as_u32_slice(&self) -> &[u32] { + self.as_slice() } /// Views these bytes as a slice of `u16` @@ -159,15 +155,26 @@ impl Align4<[u8; N]> { /// * If the number of bytes isn't a multiple of 2 #[inline] #[must_use] - pub fn as_u16_slice(&self) -> &[u16] { - assert!(self.0.len() % 2 == 0); - // Safety: our struct is aligned to 4, so the pointer will already be - // aligned, we only need to check the length - unsafe { - let data: *const u8 = self.0.as_ptr(); - let len: usize = self.0.len(); - core::slice::from_raw_parts(data.cast::(), len / 2) + pub const fn as_u16_slice(&self) -> &[u16] { + self.as_slice() + } + + /// Views these bytes as a slice of `T` + /// ## Panics + /// * If the number of bytes isn't a multiple of T + /// * If the alignment of T isn't 4, 2, or 1 + #[inline] + #[must_use] + pub const fn as_slice(&self) -> &[T] { + const { + assert!(N % (size_of::() + (size_of::() % align_of::())) == 0); + assert!( + align_of::() == 4 || align_of::() == 2 || align_of::() == 1 + ); } + let data: *const u8 = self.0.as_ptr(); + let len = const { N / size_of::() }; + unsafe { core::slice::from_raw_parts(data.cast::(), len) } } } @@ -179,121 +186,6 @@ macro_rules! include_aligned_bytes { }}; } -#[cfg(test)] -mod test_harness { - use crate::prelude::*; - use crate::{bios, mem, mgba}; - use core::fmt::Write; - - #[panic_handler] - fn panic(info: &core::panic::PanicInfo) -> ! { - DISPSTAT.write(DisplayStatus::new().with_irq_vblank(true)); - BG_PALETTE.index(0).write(Color::from_rgb(25, 10, 5)); - IE.write(IrqBits::VBLANK); - IME.write(true); - VBlankIntrWait(); - VBlankIntrWait(); - VBlankIntrWait(); - - // the Fatal one kills emulation after one line / 256 bytes - // so emit all the information as Error first - if let Ok(mut log) = - mgba::MgbaBufferedLogger::try_new(mgba::MgbaMessageLevel::Error) - { - writeln!(log, "[failed]").ok(); - write!(log, "{}", info).ok(); - } - - if let Ok(mut log) = - mgba::MgbaBufferedLogger::try_new(mgba::MgbaMessageLevel::Fatal) - { - if let Some(loc) = info.location() { - write!(log, "panic at {loc}! see mgba error log for details.").ok(); - } else { - write!(log, "panic! see mgba error log for details.").ok(); - } - } - - IE.write(IrqBits::new()); - bios::IntrWait(true, IrqBits::new()); - loop {} - } - - pub(crate) trait UnitTest { - fn run(&self); - } - - impl UnitTest for T { - fn run(&self) { - if let Ok(mut log) = - mgba::MgbaBufferedLogger::try_new(mgba::MgbaMessageLevel::Info) - { - write!(log, "{}...", core::any::type_name::()).ok(); - } - - self(); - - if let Ok(mut log) = - mgba::MgbaBufferedLogger::try_new(mgba::MgbaMessageLevel::Info) - { - writeln!(log, "[ok]").ok(); - } - } - } - - pub(crate) fn test_runner(tests: &[&dyn UnitTest]) { - if let Ok(mut log) = - mgba::MgbaBufferedLogger::try_new(mgba::MgbaMessageLevel::Info) - { - write!(log, "Running {} tests", tests.len()).ok(); - } - - for test in tests { - test.run(); - } - if let Ok(mut log) = - mgba::MgbaBufferedLogger::try_new(mgba::MgbaMessageLevel::Info) - { - write!(log, "Tests finished successfully").ok(); - } - } - - #[no_mangle] - extern "C" fn main() { - DISPCNT.write(DisplayControl::new().with_video_mode(VideoMode::_0)); - BG_PALETTE.index(0).write(Color::new()); - - crate::test_main(); - - BG_PALETTE.index(0).write(Color::from_rgb(5, 15, 25)); - BG_PALETTE.index(1).write(Color::new()); - BG0CNT - .write(BackgroundControl::new().with_charblock(0).with_screenblock(31)); - DISPCNT.write( - DisplayControl::new().with_video_mode(VideoMode::_0).with_show_bg0(true), - ); - - // some niceties for people without mgba-test-runner - let tsb = TEXT_SCREENBLOCKS.get_frame(31).unwrap(); - unsafe { - mem::set_u32x80_unchecked( - tsb.into_block::<1024>().as_mut_ptr().cast(), - 0, - 12, - ); - } - Cga8x8Thick.bitunpack_4bpp(CHARBLOCK0_4BPP.as_region(), 0); - - let row = tsb.get_row(9).unwrap().iter().skip(6); - for (addr, ch) in row.zip(b"all tests passed!") { - addr.write(TextEntry::new().with_tile(*ch as u16)); - } - - DISPSTAT.write(DisplayStatus::new()); - bios::IntrWait(true, IrqBits::new()); - } -} - #[cfg(test)] mod test { use super::Align4; @@ -304,4 +196,34 @@ mod test { assert_eq!(a.as_u16_slice(), &[0x100_u16.to_le(), 0x302_u16.to_le()]); assert_eq!(a.as_u32_slice(), &[0x3020100_u32.to_le()]); } + + #[test_case] + fn align4_as_generic() { + // with padding + #[repr(C, align(4))] + #[derive(PartialEq, Debug)] + struct FiveByte([u8; 5]); + + assert_eq!( + Align4(*b"hello...world...").as_slice::(), + &[FiveByte(*b"hello"), FiveByte(*b"world")] + ); + + // and without + #[repr(C, align(2))] + #[derive(PartialEq, Debug)] + struct ThreeHalfWords(u16, u16, u16); + + assert_eq!( + Align4([ + 0x11u8, 0x11u8, 0x22u8, 0x22u8, 0x33u8, 0x33u8, 0x44u8, 0x44u8, 0x55u8, + 0x55u8, 0x66u8, 0x66u8 + ]) + .as_slice::(), + &[ + ThreeHalfWords(0x1111, 0x2222, 0x3333), + ThreeHalfWords(0x4444, 0x5555, 0x6666) + ] + ); + } } diff --git a/src/test_harness.rs b/src/test_harness.rs new file mode 100644 index 0000000..87cb464 --- /dev/null +++ b/src/test_harness.rs @@ -0,0 +1,109 @@ +use crate::{bios, mem, mgba, prelude::*}; +use core::fmt::Write; + +#[panic_handler] +fn panic(info: &core::panic::PanicInfo) -> ! { + DISPSTAT.write(DisplayStatus::new().with_irq_vblank(true)); + BG_PALETTE.index(0).write(Color::from_rgb(25, 10, 5)); + IE.write(IrqBits::VBLANK); + IME.write(true); + VBlankIntrWait(); + VBlankIntrWait(); + VBlankIntrWait(); + + // the Fatal one kills emulation after one line / 256 bytes + // so emit all the information as Error first + if let Ok(mut log) = + mgba::MgbaBufferedLogger::try_new(mgba::MgbaMessageLevel::Error) + { + writeln!(log, "[failed]").ok(); + write!(log, "{}", info).ok(); + } + + if let Ok(mut log) = + mgba::MgbaBufferedLogger::try_new(mgba::MgbaMessageLevel::Fatal) + { + if let Some(loc) = info.location() { + write!(log, "panic at {loc}! see mgba error log for details.").ok(); + } else { + write!(log, "panic! see mgba error log for details.").ok(); + } + } + + IE.write(IrqBits::new()); + bios::IntrWait(true, IrqBits::new()); + loop {} +} + +pub(crate) trait UnitTest { + fn run(&self); +} + +impl UnitTest for T { + fn run(&self) { + if let Ok(mut log) = + mgba::MgbaBufferedLogger::try_new(mgba::MgbaMessageLevel::Info) + { + write!(log, "{}...", core::any::type_name::()).ok(); + } + + self(); + + if let Ok(mut log) = + mgba::MgbaBufferedLogger::try_new(mgba::MgbaMessageLevel::Info) + { + writeln!(log, "[ok]").ok(); + } + } +} + +pub(crate) fn test_runner(tests: &[&dyn UnitTest]) { + if let Ok(mut log) = + mgba::MgbaBufferedLogger::try_new(mgba::MgbaMessageLevel::Info) + { + write!(log, "Running {} tests", tests.len()).ok(); + } + + for test in tests { + test.run(); + } + if let Ok(mut log) = + mgba::MgbaBufferedLogger::try_new(mgba::MgbaMessageLevel::Info) + { + write!(log, "Tests finished successfully").ok(); + } +} + +#[no_mangle] +extern "C" fn main() { + DISPCNT.write(DisplayControl::new().with_video_mode(VideoMode::_0)); + BG_PALETTE.index(0).write(Color::new()); + + crate::test_main(); + + BG_PALETTE.index(0).write(Color::from_rgb(5, 15, 25)); + BG_PALETTE.index(1).write(Color::new()); + BG0CNT.write(BackgroundControl::new().with_charblock(0).with_screenblock(31)); + DISPCNT.write( + DisplayControl::new().with_video_mode(VideoMode::_0).with_show_bg0(true), + ); + + // some niceties for people without mgba-test-runner + let tsb = TEXT_SCREENBLOCKS.get_frame(31).unwrap(); + unsafe { + mem::set_u32x80_unchecked( + tsb.into_block::<1024>().as_mut_ptr().cast(), + 0, + 12, + ); + } + Cga8x8Thick.bitunpack_4bpp(CHARBLOCK0_4BPP.as_region(), 0); + + let row = tsb.get_row(9).unwrap().iter().skip(6); + for (addr, ch) in row.zip(b"all tests passed!") { + addr.write(TextEntry::new().with_tile(*ch as u16)); + } + + DISPSTAT.write(DisplayStatus::new()); + bios::IntrWait(true, IrqBits::new()); +}