From 58d739dd9e1826a5e0ec61799d6e456777f75934 Mon Sep 17 00:00:00 2001 From: Lokathor Date: Mon, 17 Dec 2018 17:00:22 -0700 Subject: [PATCH] fixed point and stuff --- Cargo.toml | 1 + book/src/01-quirks/01-no_std.md | 11 +- book/src/01-quirks/02-fixed_only.md | 261 +++++++++++++++++++++++++++- book/src/01-quirks/04-newtype.md | 13 +- examples/hello_world.rs | 7 +- src/builtins.rs | 38 ++++ src/core_extras.rs | 1 - src/fixed.rs | 86 +++++++++ src/io_registers.rs | 23 ++- src/lib.rs | 58 +++++-- 10 files changed, 457 insertions(+), 42 deletions(-) create mode 100644 src/builtins.rs create mode 100644 src/fixed.rs diff --git a/Cargo.toml b/Cargo.toml index 3c39e21..401f558 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ license = "Apache-2.0" publish = false [dependencies] +typenum = "1.10" gba-proc-macro = "0.2.1" [profile.release] diff --git a/book/src/01-quirks/01-no_std.md b/book/src/01-quirks/01-no_std.md index 13faa72..44fa757 100644 --- a/book/src/01-quirks/01-no_std.md +++ b/book/src/01-quirks/01-no_std.md @@ -89,10 +89,6 @@ the standard library types to be used "for free" once it was set up, or just a custom allocator that's GBA specific if Rust's global allocator style isn't a good fit for the GBA (I honestly haven't looked into it). -## LLVM Intrinsics - -TODO: explain that we'll occasionally have to provide some intrinsics. - ## Bare Metal Panic TODO: expand this @@ -114,3 +110,10 @@ TODO: expand this * Sending the message also automatically zeroes the output buffer. * View the output within the "Tools" menu, "View Logs...". Note that the Fatal message, if any doesn't get logged. + +TODO: this will probably fail without a `__clzsi2` implementation, which is a +good seg for the next section + +## LLVM Intrinsics + +TODO: explain that we'll occasionally have to provide some intrinsics. diff --git a/book/src/01-quirks/02-fixed_only.md b/book/src/01-quirks/02-fixed_only.md index 49e507a..c2c5c95 100644 --- a/book/src/01-quirks/02-fixed_only.md +++ b/book/src/01-quirks/02-fixed_only.md @@ -1,13 +1,258 @@ # Fixed Only -In addition to not having the standard library available, we don't even have a -floating point unit available! We can't do floating point math in hardware! We -could still do floating point math as software computations if we wanted, but -that's a slow, slow thing to do. +In addition to not having much of the standard library available, we don't even +have a floating point unit available! We can't do floating point math in +hardware! We _could_ still do floating point math as pure software computations +if we wanted, but that's a slow, slow thing to do. -Instead let's learn about another way to have fractional values called "Fixed -Point" +Are there faster ways? It's the same answer as always: "Yes, but not without a +tradeoff." -## Fixed Point +The faster way is to represent fractional values using a system called a [Fixed +Point Representation](https://en.wikipedia.org/wiki/Fixed-point_arithmetic). +What do we trade away? Numeric range. -TODO: describe fixed point, make some types, do the impls, all that. +* Floating point math stores bits for base value and for exponent all according + to a single [well defined](https://en.wikipedia.org/wiki/IEEE_754) standard + for how such a complicated thing works. +* Fixed point math takes a normal integer (either signed or unsigned) and then + just "mentally associates" it (so to speak) with a fractional value for its + "units". If you have 3 and it's in units of 1/2, then you have 3/2, or 1.5 + using decimal notation. If your number is 256 and it's in units of 1/256th + then the value is 1.0 in decimal notation. + +Floating point math requires dedicated hardware to perform quickly, but it can +"trade" precision when it needs to represent extremely large or small values. + +Fixed point math is just integral math, which our GBA is reasonably good at, but +because your number is associated with a fixed fraction your results can get out +of range very easily. + +## Representing A Fixed Point Value + +So we want to associate our numbers with a mental note of what units they're in: + +* [PhantomData](https://doc.rust-lang.org/core/marker/struct.PhantomData.html) + is a type that tells the compiler "please remember this extra type info" when + you add it as a field to a struct. It goes away at compile time, so it's + perfect for us to use as space for a note to ourselves without causing runtime + overhead. +* The [typenum](https://crates.io/crates/typenum) crate is the best way to + represent a number within a type in Rust. Since our values on the GBA are + always specified as a number of fractional bits to count the number as, we can + put `typenum` types such as `U8` or `U14` into our `PhantomData` to keep track + of what's going on. + +Now, those of you who know me, or perhaps just know my reputation, will of +course _immediately_ question what happened to the real Lokathor. I do not care +for most crates, and I particularly don't care for using a crate in teaching +situations. However, `typenum` has a number of factors on its side that let me +suggest it in this situation: + +* It's version 1.10 with a total of 21 versions and nearly 700k downloads, so we + can expect that the major troubles have been shaken out and that it will remain + fairly stable for quite some time to come. +* It has no further dependencies that it's going to drag into the compilation. +* It happens all at compile time, so it's not clogging up our actual game with + any nonsense. +* The (interesting) subject of "how do you do math inside Rust's trait system?" is + totally separate from the concern that we're trying to focus on here. + +Therefore, we will consider it acceptable to use this crate. + +Now the `typenum` crate defines a whole lot, but we'll focus down to just a +single type at the moment: +[UInt](https://docs.rs/typenum/1.10.0/typenum/uint/struct.UInt.html) is a +type-level unsigned value. It's like `u8` or `u16`, but while they're types that +then have values, each `UInt` construction statically equates to a specific +value. Like how the `()` type only has one value, which is also called `()`. In +this case, you wrap up `UInt` around smaller `UInt` values and a `B1` or `B0` +value to build up the binary number that you want at the type level. + +In other words, instead of writing + +```rust +let six = 0b110; +``` + +We write + +```rust +type U6 = UInt, B1>, B0>; +``` + +Wild, I know. If you look into the `typenum` crate you can do math and stuff +with these type level numbers, and we will a little bit below, but to start off +we _just_ need to store one in some `PhantomData`. + +### A struct For Fixed Point + +Our actual type for a fixed point value looks like this: + +```rust +use core::marker::PhantomData; +use typenum::marker_traits::Unsigned; + +/// Fixed point `T` value with `F` fractional bits. +#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord)] +#[repr(transparent)] +pub struct Fx { + bits: T, + _phantom: PhantomData, +} +``` + +This says that `Fx` is a generic type that holds some base number type `T` +and a `F` type that's marking off how many fractional bits we're using. We only +want people giving unsigned type-level values for the `PhantomData` type, so we +use the trait bound `F: Unsigned`. + +We use +[repr(transparent)](https://github.com/rust-lang/rfcs/blob/master/text/1758-repr-transparent.md) +here to ensure that `Fx` will always be treated just like the base type in the +final program (in terms of bit pattern and ABI). + +If you go and check, this is _basically_ how the existing general purpose crates +for fixed point math represent their numbers. They're a little fancier about it +because they have to cover every case, and we only have to cover our GBA case. + +That's quite a bit to type though. We probably want to make a few type aliases +for things to be easier to look at. Unfortunately there's [no standard +notation](https://en.wikipedia.org/wiki/Fixed-point_arithmetic#Notation) for how +you write a fixed point type. We also have to limit ourselves to what's valid +for use in a Rust type too. I like the `fx` thing, so we'll use that for signed +and then `fxu` if we need an unsigned value. + +```rust +/// Alias for an `i16` fixed point value with 8 fractional bits. +pub type fx8_8 = Fx; +``` + +Rust will complain about having `non_camel_case_types`, and you can shut that +warning up by putting an `#[allow(non_camel_case_types)]` attribute on the type +alias directly, or you can use `#![allow(non_camel_case_types)]` at the very top +of the module to shut up that warning for the whole module (which is what I +did). + +## Constructing A Fixed Point Value + +So how do we actually _make_ one of these values? Well, we can always just wrap or unwrap any value in our `Fx` type: + +```rust +impl Fx { + /// Uses the provided value directly. + pub fn from_raw(r: T) -> Self { + Fx { + num: r, + phantom: PhantomData, + } + } + /// Unwraps the inner value. + pub fn into_raw(self) -> T { + self.num + } +} +``` + +I'd like to use the `From` trait of course, but it was giving me some trouble, i +think because of the orphan rule. Oh well. + +If we want to be particular to the fact that these are supposed to be +_numbers_... that gets tricky. Rust is actually quite bad at being generic about +number types. You can use the [num](https://crates.io/crates/num) crate, or you +can just use a macro and invoke it once per type. Guess what we're gonna do. + +```rust +macro_rules! fixed_point_methods { + ($t:ident) => { + impl Fx<$t, F> { + /// Gives 0 for this type. + pub fn zero() -> Self { + Fx { + num: 0, + phantom: PhantomData, + } + } + + /// Gives the smallest positive non-zero value. + pub fn precision() -> Self { + Fx { + num: 1, + phantom: PhantomData, + } + } + + /// Makes a value with the integer part shifted into place. + pub fn from_int_part(i: $t) -> Self { + Fx { + num: i << F::to_u8(), + phantom: PhantomData, + } + } + } + }; +} + +fixed_point_methods! {u8} +fixed_point_methods! {i8} +fixed_point_methods! {i16} +fixed_point_methods! {u16} +fixed_point_methods! {i32} +fixed_point_methods! {u32} +``` + +Now _you'd think_ that those can all be `const`, but at the moment you can't +have a `const` function with a bound on any trait other than `Sized`, so they +have to be normal functions. + +Also, we're doing something a little interesting there with `from_int_part`. We +can take our `F` type and get it as a value instead of a type using `to_u8`. + +## Casting Values + +Next, once we have a value in one type, we need to be able to move it into +another type. A particular `Fx` type is a base number type and a fractional +count, so there's two ways we might want to move it. + +For casting the base type it's a little weird. Because there's so many number +types, and we can't be generic about them when using `as`, we'd have to make +like 30 functions (6 base number types we're using, times 5 target number types +you could cast to). Instead, we'll write it just once, and let the user pass a +closure that does the cast. + +We can put this as part of the basic impl block that `from_raw` and `into_raw` +are part of. If can avoid having code inside a macro we'll do it just because +macros are messy. + +```rust + /// Casts the base type, keeping the fractional bit quantity the same. + pub fn cast_inner Z>(self, op: C) -> Fx { + Fx { + num: op(self.num), + phantom: PhantomData, + } + } +``` + +It's... not the best to have to pass in the casting operation like that. +Hopefully we won't have to use it much. + +Also we might want to change the amount of fractional bits in a number. Oh, +gosh, this one is kinda complicated. + +## Addition / Subtraction + +## Multiplication / Division + +## Trigonometry + +## Just Using A Crate + +If you feel too intimidated by all of this then I'll suggest to you that the +[fixed](https://crates.io/crates/fixed) crate seems to be the best crate +available for fixed point math. + +_I have not tested its use on the GBA myself_. + +It's just my recommendation from looking at the docs of the various options +available. diff --git a/book/src/01-quirks/04-newtype.md b/book/src/01-quirks/04-newtype.md index 86b2916..f1c4be8 100644 --- a/book/src/01-quirks/04-newtype.md +++ b/book/src/01-quirks/04-newtype.md @@ -1,5 +1,8 @@ # Newtype +TODO: we've already used newtype twice by now (fixed point values and volatile +addresses), so we need to adjust how we start this section. + There's a great Zero Cost abstraction that we'll be using a lot that you might not already be familiar with: we're talking about the "Newtype Pattern"! @@ -27,13 +30,13 @@ cost at compile time. pub struct PixelColor(u16); ``` +TODO: we've already talked about repr(transparent) by now + Ah, except that, as I'm sure you remember from [The Rustonomicon](https://doc.rust-lang.org/nomicon/other-reprs.html#reprtransparent) -(and from [the -RFC](https://github.com/rust-lang/rfcs/blob/master/text/1758-repr-transparent.md) -too, of course), if we have a single field struct that's sometimes different -from having just the bare value, so we should be using `#[repr(transparent)]` -with our newtypes. +(and from the RFC too, of course), if we have a single field struct that's +sometimes different from having just the bare value, so we should be using +`#[repr(transparent)]` with our newtypes. ```rust #[repr(transparent)] diff --git a/examples/hello_world.rs b/examples/hello_world.rs index 3866d46..549569a 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -20,12 +20,13 @@ macro_rules! const_assert { }; } +/// Constructs an RGB value with a `const_assert!` that the input is in range. #[macro_export] macro_rules! const_rgb { ($r:expr, $g:expr, $b:expr) => {{ - const_assert!($r); - const_assert!($g); - const_assert!($b); + const_assert!($r <= 31); + const_assert!($g <= 31); + const_assert!($b <= 31); Color::new($r, $g, $b) }}; } diff --git a/src/builtins.rs b/src/builtins.rs new file mode 100644 index 0000000..048b5bf --- /dev/null +++ b/src/builtins.rs @@ -0,0 +1,38 @@ +#![allow(missing_docs)] + +//! The module to provide "builtin" functions that LLVM expects. +//! +//! You shouldn't need to call anything in here yourself, it just has to be in +//! the translation unit and LLVM will find it. + +#[no_mangle] +pub unsafe extern "C" fn __clzsi2(mut x: usize) -> usize { + let mut y: usize; + let mut n: usize = 32; + y = x >> 16; + if y != 0 { + n = n - 16; + x = y; + } + y = x >> 8; + if y != 0 { + n = n - 8; + x = y; + } + y = x >> 4; + if y != 0 { + n = n - 4; + x = y; + } + y = x >> 2; + if y != 0 { + n = n - 2; + x = y; + } + y = x >> 1; + if y != 0 { + n - 2 + } else { + n - x + } +} diff --git a/src/core_extras.rs b/src/core_extras.rs index 1eb96dd..38eaba4 100644 --- a/src/core_extras.rs +++ b/src/core_extras.rs @@ -39,4 +39,3 @@ impl VolatilePtr { } // TODO: kill all this with fire - diff --git a/src/fixed.rs b/src/fixed.rs new file mode 100644 index 0000000..98879c1 --- /dev/null +++ b/src/fixed.rs @@ -0,0 +1,86 @@ +#![allow(non_camel_case_types)] + +//! Module for fixed point math types and operations. + +use core::{convert::From, marker::PhantomData}; +use typenum::{marker_traits::Unsigned, U8}; + +/// Fixed point `T` value with `F` fractional bits. +#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord)] +#[repr(transparent)] +pub struct Fx { + num: T, + phantom: PhantomData, +} + +impl Fx { + /// Uses the provided value directly. + pub fn from_raw(r: T) -> Self { + Fx { + num: r, + phantom: PhantomData, + } + } + /// Unwraps the inner value. + pub fn into_raw(self) -> T { + self.num + } + + /// Casts the base type, keeping the fractional bit quantity the same. + pub fn cast_inner Z>(self, op: C) -> Fx { + Fx { + num: op(self.num), + phantom: PhantomData, + } + } +} + +macro_rules! fixed_point_methods { + ($t:ident) => { + impl Fx<$t, F> { + /// Gives 0 for this type. + pub fn zero() -> Self { + Fx { + num: 0, + phantom: PhantomData, + } + } + + /// Gives the smallest positive non-zero value. + pub fn precision() -> Self { + Fx { + num: 1, + phantom: PhantomData, + } + } + + /// Makes a value with the integer part shifted into place. + pub fn from_int_part(i: $t) -> Self { + Fx { + num: i << F::to_u8(), + phantom: PhantomData, + } + } + + /// Gives the raw inner value. + pub fn into_inner(&self) -> $t { + self.num + } + + /// Changes the fractional bit quantity, keeping the base type the same. + pub fn change_bit_quantity(&self) -> Fx<$t, N> { + unimplemented!() + } + } + }; +} + +fixed_point_methods! {u8} +fixed_point_methods! {i8} +fixed_point_methods! {i16} +fixed_point_methods! {u16} +fixed_point_methods! {i32} +fixed_point_methods! {u32} + +/// Alias for an `i16` fixed point value with 8 fractional bits. +pub type fx8_8 = Fx; diff --git a/src/io_registers.rs b/src/io_registers.rs index 7ea83d3..3439b87 100644 --- a/src/io_registers.rs +++ b/src/io_registers.rs @@ -15,7 +15,7 @@ // TODO(lokathor): IO Register newtypes. -use gba_proc_macro::{newtype, register_bit}; +use gba_proc_macro::register_bit; use super::*; @@ -25,9 +25,10 @@ use super::*; pub const DISPCNT: VolatilePtr = VolatilePtr(0x400_0000 as *mut u16); newtype!( + /// A newtype over the various display control options that you have on a GBA. + #[derive(Debug, Copy, Clone, Default, PartialEq, Eq)] DisplayControlSetting, - u16, - "A newtype over the various display control options that you have on a GBA." + u16 ); #[allow(missing_docs)] @@ -412,10 +413,14 @@ pub enum TriBool { Plus = 1, } -newtype!(KeyInputSetting, u16, "A newtype over the key input state of the GBA"); +newtype! { + /// Records a particular key press combination. + #[derive(Debug, Copy, Clone, Default, PartialEq, Eq)] + KeyInput, u16 +} #[allow(missing_docs)] -impl KeyInputSetting { +impl KeyInput { register_bit!(A_BIT, u16, 1, a_pressed); register_bit!(B_BIT, u16, 1 << 1, b_pressed); register_bit!(SELECT_BIT, u16, 1 << 2, select_pressed); @@ -428,8 +433,8 @@ impl KeyInputSetting { register_bit!(L_BIT, u16, 1 << 9, l_pressed); /// Takes the difference between these keys and another set of keys. - pub fn difference(self, other: KeyInputSetting) -> KeyInputSetting { - KeyInputSetting(self.0 ^ other.0) + pub fn difference(self, other: Self) -> Self { + KeyInput(self.0 ^ other.0) } /// Gives the arrow pad value as a tribool, with Plus being increased column @@ -458,11 +463,11 @@ impl KeyInputSetting { } /// Gets the current state of the keys -pub fn key_input() -> KeyInputSetting { +pub fn key_input() -> KeyInput { // Note(Lokathor): The 10 used bits are "low when pressed" style, but the 6 // unused bits are always low, so we XOR with this mask to get a result where // the only active bits are currently pressed keys. - unsafe { KeyInputSetting(KEYINPUT.read() ^ 0b0000_0011_1111_1111) } + unsafe { KeyInput(KEYINPUT.read() ^ 0b0000_0011_1111_1111) } } /// Key Interrupt Control diff --git a/src/lib.rs b/src/lib.rs index f00981f..c82a89b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,12 +1,12 @@ #![cfg_attr(not(test), no_std)] #![cfg_attr(not(test), feature(asm))] #![warn(missing_docs)] -//#![allow(clippy::cast_lossless)] +#![allow(clippy::cast_lossless)] #![deny(clippy::float_arithmetic)] //! This crate helps you write GBA ROMs. //! -//! # SAFETY POLICY +//! ## SAFETY POLICY //! //! Some parts of this crate are safe wrappers around unsafe operations. This is //! good, and what you'd expect from a Rust crate. @@ -16,21 +16,55 @@ //! //! **Do not** use this crate in programs that aren't running on the GBA. If you //! do, it's a giant bag of Undefined Behavior. -//! -//! # TESTING POLICY -//! -//! It is the intent of the crate authors that as much of the crate as possible -//! be written so that you can use `cargo test` for at least some parts of your -//! code without everything exploding instantly. To that end, where possible we -//! attempt to use `cfg` flags to make things safe for `cargo test`. Hopefully -//! we got it all. -pub mod core_extras; -pub(crate) use crate::core_extras::*; +/// Assists in defining a newtype wrapper over some base type. +/// +/// Note that rustdoc and derives are all the "meta" stuff, so you can write all +/// of your docs and derives in front of your newtype in the same way you would +/// for a normal struct. Then the inner type to be wrapped it name. +/// +/// The macro _assumes_ that you'll be using it to wrap zero safe numeric types, +/// so it automatically provides a `const fn` method for `new` that just wraps +/// `0`. If this is not desired you can add `, no frills` to the invocation. +/// +/// Example: +/// ``` +/// newtype! { +/// /// Records a particular key press combination. +/// #[derive(Debug, Copy, Clone, Default, PartialEq, Eq)] +/// KeyInput, u16 +/// } +/// ``` +#[macro_export] +macro_rules! newtype { + ($(#[$attr:meta])* $new_name:ident, $old_name:ident) => { + $(#[$attr])* + #[repr(transparent)] + pub struct $new_name($old_name); + impl $new_name { + /// A `const` "zero value" constructor + pub const fn new() -> Self { + $new_name(0) + } + } + }; + ($(#[$attr:meta])* $new_name:ident, $old_name:ident, no frills) => { + $(#[$attr])* + #[repr(transparent)] + pub struct $new_name($old_name); + }; +} + +pub mod builtins; + +pub mod fixed; #[cfg(not(test))] pub mod bios; +pub mod core_extras; +pub(crate) use crate::core_extras::*; + pub mod io_registers; pub mod video_ram;