Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

build_const-friendly fmt::Debug and human-readable decimal fmt::Display #211

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion .github/workflows/ci-builds.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
- uses: actions/checkout@v2

- name: Install Apt Dependencies
run: sudo apt-get update && sudo apt-get install binutils-arm-none-eabi
run: sudo apt-get update && sudo apt-get install binutils-arm-none-eabi libelf-dev

- uses: actions-rs/toolchain@v1
with:
Expand All @@ -39,3 +39,19 @@ jobs:
toolchain: ${{ matrix.rust.toolchain }}
command: check
args: --no-default-features

- name: Install mgba-test-runner
uses: actions-rs/cargo@v1
with:
toolchain: ${{ matrix.rust.toolchain }}
command: install
# newer revisions don't build on aarch64, at least, because of a c_char mishap
args: --git https://github.com/agbrs/agb --rev a7f9fdf01118a7a77d4dcf72f2b74a1961458b36 mgba-test-runner

- name: Run unit tests
uses: actions-rs/cargo@v1
env:
CARGO_TARGET_THUMBV4T_NONE_EABI_RUNNER: mgba-test-runner
with:
toolchain: ${{ matrix.rust.toolchain }}
command: test
117 changes: 101 additions & 16 deletions src/fixed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,24 @@ macro_rules! impl_common_fixed_ops {
Self(self.0 >> rhs)
}
}

impl<const B: u32> core::fmt::Debug for Fixed<$t, B> {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
let raw: $t = self.to_bits();
write!(
f,
concat!(
"Fixed::<",
stringify!($t),
",{}>::from_bits({:#x}_u32 as ",
stringify!($t),
")"
),
B, raw
)
}
}

impl_trait_op_unit!($t, Not, not);
impl_trait_op_self_rhs!($t, Add, add);
impl_trait_op_self_rhs!($t, Sub, sub);
Expand Down Expand Up @@ -335,18 +353,13 @@ macro_rules! impl_signed_fixed_ops {
}
}
impl_trait_op_unit!($t, Neg, neg);
impl<const B: u32> core::fmt::Debug for Fixed<$t, B> {
#[inline]

impl<const B: u32> core::fmt::Display for Fixed<$t, B> {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
let whole: $t = self.trunc().to_bits() >> B;
let fract: $t = self.fract().to_bits();
let divisor: $t = 1 << B;
if self.is_negative() {
let whole = whole.unsigned_abs();
write!(f, "-({whole}+{fract}/{divisor})")
} else {
write!(f, "{whole}+{fract}/{divisor}")
if self.to_bits() < 0 {
f.write_str("-")?;
}
fixed_fmt_abs::<B>(f, self.to_bits().abs() as u32)
}
}
};
Expand Down Expand Up @@ -393,17 +406,89 @@ macro_rules! impl_unsigned_fixed_ops {
Self(self.0 & (<$t>::MAX << B))
}
}
impl<const B: u32> core::fmt::Debug for Fixed<$t, B> {
#[inline]

impl<const B: u32> core::fmt::Display for Fixed<$t, B> {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
let whole: $t = self.trunc().to_bits() >> B;
let fract: $t = self.fract().to_bits();
let divisor: $t = 1 << B;
write!(f, "{whole}+{fract}/{divisor}")
fixed_fmt_abs::<B>(f, self.to_bits() as u32)
}
}
};
}
impl_unsigned_fixed_ops!(u8);
impl_unsigned_fixed_ops!(u16);
impl_unsigned_fixed_ops!(u32);

fn fixed_fmt_abs<const B: u32>(
f: &mut core::fmt::Formatter, abs: u32,
) -> core::fmt::Result {
let width = f.width().unwrap_or(0);
let precision = f.precision().unwrap_or(const { ((B as usize) + 1) / 3 });
let fract = abs & ((1 << B) - 1);
let fract_dec = 10u32
.checked_pow(precision as u32)
.and_then(|digits| fract.checked_mul(digits))
.map(|x| (x >> B) as u64)
.unwrap_or_else(|| (fract as u64 * 10u64.pow(precision as u32) >> B));
write!(f, "{:width$}.{:0precision$}", abs >> B, fract_dec)
}

#[cfg(test)]
mod test {
use crate::fixed::{i16fx14, i32fx8};
use core::{fmt::Write, str};

struct WriteBuf<const N: usize>([u8; N], usize);
impl<'a, const N: usize> Default for WriteBuf<N> {
fn default() -> Self {
Self([0u8; N], 0)
}
}
impl<const N: usize> Write for WriteBuf<N> {
fn write_str(&mut self, s: &str) -> core::fmt::Result {
let src = s.as_bytes();
let len = (self.0.len() - self.1).min(src.len());
self.0[self.1..self.1 + len].copy_from_slice(&src[..len]);
self.1 += len;
if len < src.len() {
Err(core::fmt::Error)
} else {
Ok(())
}
}
}
impl<const N: usize> WriteBuf<N> {
fn take(&mut self) -> &str {
let len = self.1;
self.1 = 0;
str::from_utf8(&self.0[..len]).unwrap()
}
}

#[test_case]
fn decimal_display() {
let mut wbuf = WriteBuf::<16>::default();

let x = i32fx8::from_bits(0x12345678);

write!(&mut wbuf, "{x}").unwrap();
assert_eq!(wbuf.take(), "1193046.468");

write!(&mut wbuf, "{x:9.1}").unwrap();
assert_eq!(wbuf.take(), " 1193046.4");

write!(&mut wbuf, "{x:1.6}").unwrap();
assert_eq!(wbuf.take(), "1193046.468750");

let x = x.neg();
write!(&mut wbuf, "{x}").unwrap();
assert_eq!(wbuf.take(), "-1193046.468");

let x = i16fx14::from_bits(0x6544 as i16);
write!(&mut wbuf, "{x}").unwrap();
assert_eq!(wbuf.take(), "1.58227");

let x = x.neg();
write!(&mut wbuf, "{x:.10}").unwrap();
assert_eq!(wbuf.take(), "-1.5822753906");
}
}
131 changes: 131 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
#![allow(clippy::let_and_return)]
#![allow(clippy::result_unit_err)]
#![warn(clippy::missing_inline_in_public_items)]
#![cfg_attr(test, feature(custom_test_frameworks))]
#![cfg_attr(test, test_runner(test_harness::test_runner))]
#![cfg_attr(test, no_main)]
#![cfg_attr(test, reexport_test_harness_main = "test_main")]

//! A crate for GBA development.
//!
Expand Down Expand Up @@ -174,3 +178,130 @@ macro_rules! include_aligned_bytes {
Align4(*include_bytes!($file))
}};
}

#[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<T: Fn()> UnitTest for T {
fn run(&self) {
if let Ok(mut log) =
mgba::MgbaBufferedLogger::try_new(mgba::MgbaMessageLevel::Info)
{
write!(log, "{}...", core::any::type_name::<T>()).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;

#[test_case]
fn align4_as_u16_u32_slice() {
let a = Align4([0u8, 1u8, 2u8, 3u8]);
assert_eq!(a.as_u16_slice(), &[0x100_u16.to_le(), 0x302_u16.to_le()]);
assert_eq!(a.as_u32_slice(), &[0x3020100_u32.to_le()]);
}
}
2 changes: 2 additions & 0 deletions src/mem.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#![cfg_attr(not(feature = "on_gba"), allow(unused_variables))]

use crate::macros::on_gba_or_unimplemented;

/// Copies `u8` at a time between exclusive regions.
Expand Down
Loading