diff --git a/.travis.yml b/.travis.yml index 2f5bf99..ec6794f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,7 @@ rust: before_script: - rustup component add rust-src - - rustup component add clippy + #- rustup component add clippy --toolchain=nightly - (test -x $HOME/.cargo/bin/cargo-install-update || cargo install cargo-update) - (test -x $HOME/.cargo/bin/cargo-xbuild || cargo install cargo-xbuild) - (test -x $HOME/.cargo/bin/cargo-make || cargo install cargo-make) @@ -29,14 +29,14 @@ script: - export PATH="$PATH:/opt/devkitpro/tools/bin" - cd .. # Run all verificaions, both debug and release - - cargo clippy - - cargo clippy --release + #- cargo clippy + #- cargo clippy --release - cargo test --no-fail-fast --lib - cargo test --no-fail-fast --lib --release - cargo test --no-fail-fast --tests - cargo test --no-fail-fast --tests --release # Let cargo make take over the rest - - cargo make build-all + - cargo make justrelease # Test build the book so that a failed book build kills this run - cd book && mdbook build diff --git a/Cargo.toml b/Cargo.toml index 400c908..d3d18c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ publish = false [dependencies] typenum = "1.10" -gba-proc-macro = "0.2.1" +gba-proc-macro = "0.3" #[dev-dependencies] #quickcheck="0.7" diff --git a/book/src/00-introduction/02-goals_and_style.md b/book/src/00-introduction/02-goals_and_style.md index 268afbe..eac5366 100644 --- a/book/src/00-introduction/02-goals_and_style.md +++ b/book/src/00-introduction/02-goals_and_style.md @@ -2,26 +2,17 @@ So, what's this book actually gonna teach you? -I'm _not_ gonna tell you how to use a crate that already exists. +My goal is certainly not just showing off the crate. Programming for the GBA is +weird enough that I'm trying to teach you all the rest of the stuff you need to +know along the way. If I do my job right then you'd be able to write your own +crate for GBA stuff just how you think it should all go by the end. -Don't get me wrong, there _is_ a [gba](https://crates.io/crates/gba) crate, and -it's on crates.io and all that jazz. - -However, unlike most crates that come with a tutorial book, I don't want to just -teach you how to use the crate. What I want is to teach you what you need to -know so that you could build the crate yourself, from scratch, if it didn't -already exist for you. Let's call it the [Handmade -Hero](https://handmadehero.org/) school of design. Much more than you might find -in other Rust crate books, I'll be attempting to show a lot of the _why_ in -addition to just the _how_. Once you know how to do it all on your own, you can -decide for yourself if the `gba` crate does it well, or if you think you can -come up with something that suits your needs better. - -Overall the book is sorted for easy review once you're trying to program -something, and the GBA has a few interconnected concepts, so some parts of the -book end up having to refer you to portions that you haven't read yet. The -chapters and sections are sorted so that _minimal_ future references are -required, but it's unavoidable. +Overall the book is sorted more for easy review once you're trying to program +something. The GBA has a few things that can stand on their own and many other +things are a mass of interconnected concepts, so some parts of the book end up +having to refer you to portions that you haven't read yet. The chapters and +sections are sorted so that _minimal_ future references are required, but it's +unavoidable that it'll happen sometimes. The actual "tutorial order" of the book is the [Examples](../05-examples/00-index.md) chapter. Each section of that chapter diff --git a/book/src/01-quirks/02-fixed_only.md b/book/src/01-quirks/02-fixed_only.md index bb0e401..e6ddf0f 100644 --- a/book/src/01-quirks/02-fixed_only.md +++ b/book/src/01-quirks/02-fixed_only.md @@ -402,9 +402,9 @@ silly but it's so much faster than doing an actual division). Also, again signed values can be annoying, because if the value _just happens_ to be `i32::MIN` then when you negate it you'll have... _still_ a negative value. I'm not 100% on this, but I think the correct thing to do at that point -is to give `$t::MIN` as out output num value. +is to give `$t::MIN` as the output num value. -Did you get all that? Good, because this is involves casting, we will need to +Did you get all that? Good, because this involves casting, so we will need to implement it three times, which calls for another macro. ```rust diff --git a/book/src/01-quirks/03-volatile_destination.md b/book/src/01-quirks/03-volatile_destination.md index f03c62c..ce50a98 100644 --- a/book/src/01-quirks/03-volatile_destination.md +++ b/book/src/01-quirks/03-volatile_destination.md @@ -1,321 +1,293 @@ # Volatile Destination -TODO: replace all this one "the rant" is finalized - -There's a reasonable chance that you've never heard of `volatile` before, so -what's that? Well, it's a term that can be used in more than one context, but -basically it means "get your grubby mitts off my stuff you over-eager compiler". +TODO: update this when we can make more stuff `const` ## Volatile Memory -The first, and most common, form of volatile thing is volatile memory. Volatile -memory can change without your program changing it, usually because it's not a -location in RAM, but instead some special location that represents an actual -hardware device, or part of a hardware device perhaps. The compiler doesn't know -what's going on in this situation, but when the program is actually run and the -CPU gets an instruction to read or write from that location, instead of just -accessing some place in RAM like with normal memory, it accesses whatever bit of -hardware and does _something_. The details of that something depend on the -hardware, but what's important is that we need to actually, definitely execute -that read or write instruction. - -This is not how normal memory works. Normally when the compiler -sees us write values into variables and read values from variables, it's free to -optimize those expressions and eliminate some of the reads and writes if it can, -and generally try to save us time. Maybe it even knows some stuff about the data -dependencies in our expressions and so it does some of the reads or writes out -of order from what the source says, because the compiler knows that it won't -actually make a difference to the operation of the program. A good and helpful -friend, that compiler. - -Volatile memory works almost the opposite way. With volatile memory we -need the compiler to _definitely_ emit an instruction to do a read or write and -they need to happen _exactly_ in the order that we say to do it. Each volatile -read or write might have any sort of side effect that the compiler -doesn't know about, and it shouldn't try to be clever about the optimization. Just do what we -say, please. - -In Rust, we don't mark volatile things as being a separate type of thing, -instead we use normal raw pointers and then call the -[read_volatile](https://doc.rust-lang.org/core/ptr/fn.read_volatile.html) and -[write_volatile](https://doc.rust-lang.org/core/ptr/fn.write_volatile.html) -functions (also available as methods, if you like), which then delegate to the -LLVM -[volatile_load](https://doc.rust-lang.org/core/intrinsics/fn.volatile_load.html) -and -[volatile_store](https://doc.rust-lang.org/core/intrinsics/fn.volatile_store.html) -intrinsics. In C and C++ you can tag a pointer as being volatile and then any -normal read and write with it becomes the volatile version, but in Rust we have -to remember to use the correct alternate function instead. - -I'm told by the experts that this makes for a cleaner and saner design from a -_language design_ perspective, but it really kinda screws us when doing low -level code. References, both mutable and shared, aren't volatile, so they -compile into normal reads and writes. This means we can't do anything we'd -normally do in Rust that utilizes references of any kind. Volatile blocks of -memory can't use normal `.iter()` or `.iter_mut()` based iteration (which give -`&T` or `&mut T`), and they also can't use normal `Index` and `IndexMut` sugar -like `a + x[i]` or `x[i] = 7`. - -Unlike with normal raw pointers, this pain point never goes away. There's no way -to abstract over the difference with Rust as it exists now, you'd need to -actually adjust the core language by adding an additional pointer type (`*vol -T`) and possibly a reference type to go with it (`&vol T`) to get the right -semantics. And then you'd need an `IndexVol` trait, and you'd need -`.iter_vol()`, and so on for every other little thing. It would be a lot of -work, and the Rust developers just aren't interested in doing all that for such -a limited portion of their user population. We'll just have to deal with not -having any syntax sugar. - -### VolatilePtr - -No syntax sugar doesn't mean we can't at least make things a little easier for -ourselves. Enter the `VolatilePtr` type, which is a newtype over a `*mut T`. -One of those "manual" newtypes I mentioned where we can't use our nice macro. +The compiler is an eager friend, so when it sees a read or a write that won't +have an effect, it eliminates that read or write. For example, if we write ```rust -#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)] +let mut x = 5; +x = 7; +``` + +The compiler won't actually ever put 5 into `x`. It'll skip straight to putting +7 in `x`, because we never read from `x` when it's 5, so that's a safe change to +make. Normally, values are stored in RAM, which has no side effects when you +read and write from it. RAM is purely for keeping notes about values you'll need +later on. + +However, what if we had a bit of hardware where we wanted to do a write and that +did something _other than_ keeping the value for us to look at later? As you saw +in the `hello_magic` example, we have to use a `write_volatile` operation. +Volatile means "just do it anyway". The compiler thinks that it's pointless, but +we know better, so we can force it to really do exactly what we say by using +`write_volatile` instead of `write`. + +This is kinda error prone though, right? Because it's just a raw pointer, so we +might forget to use `write_volatile` at some point. + +Instead, we want a type that's always going to use volatile reads and writes. +Also, we want a pointer type that lets our reads and writes to be as safe as +possible once we've unsafely constructed the initial value. + +### Constructing The VolAddress Type + +First, we want a type that stores a location within the address space. This can +be a pointer, or a `usize`, and we'll use a `usize` because that's easier to +work with in a `const` context (and we want to have `const` when we can get it). +We'll also have our type use `NonZeroUsize` instead of just `usize` so that +`Option>` stays as a single machine word. This helps quite a bit +when we want to iterate over the addresses of a block of memory (such as +locations within the palette memory). Hardware is never at the null address +anyway. Also, if we had _just_ an address number then we wouldn't be able to +track what type the address is for. We need some +[PhantomData](https://doc.rust-lang.org/core/marker/struct.PhantomData.html), +and specifically we need the phantom data to be for `*mut T`: + +* If we used `*const T` that'd have the wrong + [variance](https://doc.rust-lang.org/nomicon/subtyping.html). +* If we used `&mut T` then that's fusing in the ideas of _lifetime_ and + _exclusive access_ to our type. That's potentially important, but that's also + an abstraction we'll build _on top of_ this `VolAddress` type if we need it. + +One abstraction layer at a time, so we start with just a phantom pointer. This gives us a type that looks like this: + +```rust +#[derive(Debug)] #[repr(transparent)] -pub struct VolatilePtr(pub *mut T); -``` - -Obviously we want to be able to read and write: - -```rust -impl VolatilePtr { - /// Performs a `read_volatile`. - pub unsafe fn read(self) -> T { - self.0.read_volatile() - } - - /// Performs a `write_volatile`. - pub unsafe fn write(self, data: T) { - self.0.write_volatile(data); - } -``` - -And we want a way to jump around when we do have volatile memory that's in -blocks. This is where we can get ourselves into some trouble if we're not -careful. We have to decide between -[offset](https://doc.rust-lang.org/std/primitive.pointer.html#method.offset) and -[wrapping_offset](https://doc.rust-lang.org/std/primitive.pointer.html#method.wrapping_offset). -The difference is that `offset` optimizes better, but also it can be Undefined -Behavior if the result is not "in bounds or one byte past the end of the same -allocated object". I asked [ubsan](https://github.com/ubsan) (who is the expert -that you should always listen to on matters like this) what that means exactly -when memory mapped hardware is involved (since we never allocated anything), and -the answer was that you _can_ use an `offset` in statically memory mapped -situations like this as long as you don't use it to jump to the address of -something that Rust itself allocated at some point. Cool, we all like being able -to use the one that optimizes better. Unfortunately, the downside to using -`offset` instead of `wrapping_offset` is that with `offset`, it's Undefined -Behavior _simply to calculate the out of bounds result_ (with `wrapping_offset` -it's not Undefined Behavior until you _use_ the out of bounds result). We'll -have to be quite careful when we're using `offset`. - -```rust - /// Performs a normal `offset`. - pub unsafe fn offset(self, count: isize) -> Self { - VolatilePtr(self.0.offset(count)) - } -``` - -Now, one thing of note is that doing the `offset` isn't `const`. The math for it -is something that's possible to do in a `const` way of course, but Rust -basically doesn't allow you to fiddle raw pointers much during `const` right -now. Maybe in the future that will improve. - -If we did want to have a `const` function for finding the correct address within -a volatile block of memory we'd have to do all the math using `usize` values, -and then cast that value into being a pointer once we were done. It'd look -something like this: - -```rust -const fn address_index(address: usize, index: usize) -> usize { - address + (index * std::mem::size_of::()) +pub struct VolAddress { + address: NonZeroUsize, + marker: PhantomData<*mut T>, } ``` -But, back to methods for `VolatilePtr`, well we sometimes want to be able to -cast a `VolatilePtr` between pointer types. Since we won't be able to do that -with `as`, we'll have to write a method for it: +Now, because of how `derive` is specified, it derives traits _if the generic +parameter_ supports those traits. Since our type is like a pointer, the traits +it supports are distinct from whatever traits the target type supports. So we'll +provide those implementations manually. ```rust - /// Performs a cast into some new pointer type. - pub fn cast(self) -> VolatilePtr { - VolatilePtr(self.0 as *mut Z) +impl Clone for VolAddress { + fn clone(&self) -> Self { + *self } +} +impl Copy for VolAddress {} +impl PartialEq for VolAddress { + fn eq(&self, other: &Self) -> bool { + self.address == other.address + } +} +impl Eq for VolAddress {} +impl PartialOrd for VolAddress { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.address.cmp(&other.address)) + } +} +impl Ord for VolAddress { + fn cmp(&self, other: &Self) -> Ordering { + self.address.cmp(&other.address) + } +} ``` -### Volatile Iterating +Boilerplate junk, not interesting. There's a reason that you derive those traits +99% of the time in Rust. -How about that `Iterator` stuff I said we'd be missing? We can actually make -_an_ Iterator available, it's just not the normal "iterate by shared reference -or unique reference" Iterator. Instead, it's more like a "throw out a series of -`VolatilePtr` values" style Iterator. Other than that small difference it's -totally normal, and we'll be able to use map and skip and take and all those -neat methods. +### Constructing A VolAddress Value -So how do we make this thing we need? First we check out the [Implementing -Iterator](https://doc.rust-lang.org/core/iter/index.html#implementing-iterator) -section in the core documentation. It says we need a struct for holding the -iterator state. Right-o, probably something like this: +Okay so here's the next core concept: If we unsafely _construct_ a +`VolAddress`, then we can safely _use_ the value once it's been properly +created. ```rust -#[derive(Debug, Clone, Hash, PartialEq, Eq)] -pub struct VolatilePtrIter { - vol_ptr: VolatilePtr, +// you'll need these features enabled and a recent nightly +#![feature(const_int_wrapping)] +#![feature(min_const_unsafe_fn)] + +impl VolAddress { + pub const unsafe fn new_unchecked(address: usize) -> Self { + VolAddress { + address: NonZeroUsize::new_unchecked(address), + marker: PhantomData, + } + } + pub const unsafe fn cast(self) -> VolAddress { + VolAddress { + address: self.address, + marker: PhantomData, + } + } + pub unsafe fn offset(self, offset: isize) -> Self { + VolAddress { + address: NonZeroUsize::new_unchecked(self.address.get().wrapping_add(offset as usize * core::mem::size_of::())), + marker: PhantomData, + } + } +} +``` + +So what are the unsafety rules here? + +* Non-null, obviously. +* Must be aligned for `T` +* Must always produce valid bit patterns for `T` +* Must not be part of the address space that Rust's stack or allocator will ever + uses. + +So, again using the `hello_magic` example, we had + +```rust +(0x400_0000 as *mut u16).write_volatile(0x0403); +``` + +And instead we could declare + +```rust +const MAGIC_LOCATION: VolAddress = unsafe { VolAddress::new_unchecked(0x400_0000) }; +``` + +### Using A VolAddress Value + +Now that we've named the magic location, we want to write to it. + +```rust +impl VolAddress { + pub fn read(self) -> T + where + T: Copy, + { + unsafe { (self.address.get() as *mut T).read_volatile() } + } + pub unsafe fn read_non_copy(self) -> T { + (self.address.get() as *mut T).read_volatile() + } + pub fn write(self, val: T) { + unsafe { (self.address.get() as *mut T).write_volatile(val) } + } +} +``` + +So if the type is `Copy` we can `read` it as much as we want. If, somehow, the +type isn't `Copy`, then it might be `Drop`, and that means if we read out a +value over and over we could cause the `drop` method to trigger UB. Since the +end user might really know what they're doing, we provide an unsafe backup +`read_non_copy`. + +On the other hand, we can `write` to the location as much as we want. Even if +the type isn't `Copy`, _not running `Drop` is safe_, so a `write` is always +safe. + +Now we can write to our magical location. + +```rust +MAGIC_LOCATION.write(0x0403); +``` + +### VolAddress Iteration + +We've already seen that sometimes we want to have a base address of some sort +and then offset from that location to another. What if we wanted to iterate over +_all the locations_. That's not particularly hard. + +```rust +impl VolAddress { + pub const unsafe fn iter_slots(self, slots: usize) -> VolAddressIter { + VolAddressIter { vol_address: self, slots } + } +} + +#[derive(Debug)] +pub struct VolAddressIter { + vol_address: VolAddress, slots: usize, } -``` +impl Clone for VolAddressIter { + fn clone(&self) -> Self { + VolAddressIter { + vol_address: self.vol_address, + slots: self.slots, + } + } +} +impl PartialEq for VolAddressIter { + fn eq(&self, other: &Self) -> bool { + self.vol_address == other.vol_address && self.slots == other.slots + } +} +impl Eq for VolAddressIter {} +impl Iterator for VolAddressIter { + type Item = VolAddress; -And then we just implement -[core::iter::Iterator](https://doc.rust-lang.org/core/iter/trait.Iterator.html) -on that struct. Wow, that's quite the trait though! Don't worry, we only need to -implement two small things and then the rest of it comes free as a bunch of -default methods. - -So, the code that we _want_ to write looks like this: - -```rust -impl Iterator for VolatilePtrIter { - type Item = VolatilePtr; - - fn next(&mut self) -> Option> { + fn next(&mut self) -> Option { if self.slots > 0 { - let out = Some(self.vol_ptr); - self.slots -= 1; - self.vol_ptr = unsafe { self.vol_ptr.offset(1) }; - out + let out = self.vol_address; + unsafe { + self.slots -= 1; + self.vol_address = self.vol_address.offset(1); + } + Some(out) } else { None } } } +impl FusedIterator for VolAddressIter {} ``` -Except we _can't_ write that code. What? The problem is that we used -`derive(Clone, Copy` on `VolatilePtr`. Because of a quirk in how `derive` works, -this means `VolatilePtr` will only be `Copy` if the `T` is `Copy`, _even -though the pointer itself is always `Copy` regardless of what it points to_. -Ugh, terrible. We've got three basic ways to handle this: +### VolAddressBlock -* Make the `Iterator` implementation be for ``, and then hope that we - always have types that are `Clone`. -* Hand implement every trait we want `VolatilePtr` (and `VolatilePtrIter`) to - have so that we can override the fact that `derive` is basically broken in - this case. -* Make `VolatilePtr` store a `usize` value instead of a pointer, and then cast - it to `*mut T` when we actually need to read and write. This would require us - to also store a `PhantomData` so that the type of the address is tracked - properly, which would make it a lot more verbose to construct a `VolatilePtr` - value. - -None of those options are particularly appealing. I guess we'll do the first one -because it's the least amount of up front trouble, and I don't _think_ we'll -need to be iterating non-Clone values. All we do to pick that option is add the -bound to the very start of the `impl` block, where we introduce the `T`: +Obviously, having a base address and a length exist separately is error prone. +There's a good reason for slices to keep their pointer and their length +together. We want something like that, which we'll call a "block" because +"array" and "slice" are already things in Rust. ```rust -impl Iterator for VolatilePtrIter { - type Item = VolatilePtr; - - fn next(&mut self) -> Option> { - if self.slots > 0 { - let out = Some(self.vol_ptr.clone()); - self.slots -= 1; - self.vol_ptr = unsafe { self.vol_ptr.clone().offset(1) }; - out - } else { - None - } - } -} -``` - -What's going on here? Okay so our iterator has a number of slots that it'll go -over, and then when it's out of slots it starts producing `None` forever. That's -actually pretty simple. We're also masking some unsafety too. In this case, -we'll rely on the person who made the `VolatilePtrIter` to have selected the -correct number of slots. This gives us a new method for `VolatilePtr`: - -```rust - pub unsafe fn iter_slots(self, slots: usize) -> VolatilePtrIter { - VolatilePtrIter { - vol_ptr: self, - slots, - } - } -``` - -With this design, making the `VolatilePtrIter` at the start is `unsafe` (we have -to trust the caller that the right number of slots exists), and then using it -after that is totally safe (if the right number of slots was given we'll never -screw up our end of it). - -### VolatilePtr Formatting - -Also, just as a little bonus that we probably won't use, we could enable our new -pointer type to be formatted as a pointer value. - -```rust -impl core::fmt::Pointer for VolatilePtr { - /// Formats exactly like the inner `*mut T`. - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - write!(f, "{:p}", self.0) - } -} -``` - -Neat! - -### VolatilePtr Complete - -That was a lot of small code blocks, let's look at it all put together: - -```rust -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -#[repr(transparent)] -pub struct VolatilePtr(pub *mut T); -impl VolatilePtr { - pub unsafe fn read(self) -> T { - self.0.read_volatile() - } - pub unsafe fn write(self, data: T) { - self.0.write_volatile(data); - } - pub unsafe fn offset(self, count: isize) -> Self { - VolatilePtr(self.0.offset(count)) - } - pub fn cast(self) -> VolatilePtr { - VolatilePtr(self.0 as *mut Z) - } - pub unsafe fn iter_slots(self, slots: usize) -> VolatilePtrIter { - VolatilePtrIter { - vol_ptr: self, - slots, - } - } -} -impl core::fmt::Pointer for VolatilePtr { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - write!(f, "{:p}", self.0) - } -} - -#[derive(Debug, Clone, Hash, PartialEq, Eq)] -pub struct VolatilePtrIter { - vol_ptr: VolatilePtr, +#[derive(Debug)] +pub struct VolAddressBlock { + vol_address: VolAddress, slots: usize, } -impl Iterator for VolatilePtrIter { - type Item = VolatilePtr; - fn next(&mut self) -> Option> { - if self.slots > 0 { - let out = Some(self.vol_ptr.clone()); - self.slots -= 1; - self.vol_ptr = unsafe { self.vol_ptr.clone().offset(1) }; - out +impl Clone for VolAddressBlock { + fn clone(&self) -> Self { + VolAddressBlock { + vol_address: self.vol_address, + slots: self.slots, + } + } +} +impl PartialEq for VolAddressBlock { + fn eq(&self, other: &Self) -> bool { + self.vol_address == other.vol_address && self.slots == other.slots + } +} +impl Eq for VolAddressBlock {} + +impl VolAddressBlock { + pub const unsafe fn new_unchecked(vol_address: VolAddress, slots: usize) -> Self { + VolAddressBlock { vol_address, slots } + } + pub const fn iter(self) -> VolAddressIter { + VolAddressIter { + vol_address: self.vol_address, + slots: self.slots, + } + } + pub unsafe fn index_unchecked(self, slot: usize) -> VolAddress { + self.vol_address.offset(slot as isize) + } + pub fn index(self, slot: usize) -> VolAddress { + if slot < self.slots { + unsafe { self.vol_address.offset(slot as isize) } + } else { + panic!("Index Requested: {} >= Bound: {}", slot, self.slots) + } + } + pub fn get(self, slot: usize) -> Option> { + if slot < self.slots { + unsafe { Some(self.vol_address.offset(slot as isize)) } } else { None } @@ -323,6 +295,26 @@ impl Iterator for VolatilePtrIter { } ``` +Now we can have something like: + +```rust +const OTHER_MAGIC: VolAddressBlock = unsafe { + VolAddressBlock::new_unchecked( + VolAddress::new_unchecked(0x600_0000), + 240 * 160 + ) +}; + +OTHER_MAGIC.index(120 + 80 * 240).write_volatile(0x001F); +OTHER_MAGIC.index(136 + 80 * 240).write_volatile(0x03E0); +OTHER_MAGIC.index(120 + 96 * 240).write_volatile(0x7C00); +``` + +### Docs? + +If you wanna see these types and methods with a full docs write up you should +check the GBA crate's source. + ## Volatile ASM In addition to some memory locations being volatile, it's also possible for @@ -343,3 +335,9 @@ safely (otherwise the GBA won't ever actually wake back up from the low power state), but the `asm!` you use once you're ready is just a single instruction with no return value. The compiler can't tell what's going on, so you just have to say "do it anyway". + +Note that if you use a linker script to include any ASM with your Rust program +(eg: the `crt0.s` file that we setup in the "Development Setup" section), all of +that ASM is "volatile" for these purposes. Volatile isn't actually a _hardware_ +concept, it's just an LLVM concept, and the linker script runs after LLVM has +done its work. diff --git a/book/src/02-concepts/09-sram.md b/book/src/02-concepts/09-sram.md index 65ec4d2..fbb6202 100644 --- a/book/src/02-concepts/09-sram.md +++ b/book/src/02-concepts/09-sram.md @@ -14,3 +14,8 @@ for full details of how the `WAITCNT` register works. The game pak SRAM also has only an 8-bit bus, so have fun with that. The GBA Direct Memory Access (DMA) unit cannot access SRAM. + +Also, you [should not write to SRAM with code executing from +ROM](https://problemkaputt.de/gbatek.htm#gbacartbackupsramfram). Instead, you +should move the code to WRAM and execute the save code from there. We'll cover +how to handle that eventually. diff --git a/book/src/04-non-video/01-buttons.md b/book/src/04-non-video/01-buttons.md index 8eb4e80..586f6c3 100644 --- a/book/src/04-non-video/01-buttons.md +++ b/book/src/04-non-video/01-buttons.md @@ -3,3 +3,100 @@ It's all well and good to just show a picture, even to show an animation, but if we want a game we have to let the user interact with something. +## Key Input Register + +* KEYINPUT, `0x400_0130`, `u16`, read only + +This little `u16` stores the status of _all_ the buttons on the GBA, all at +once. There's only 10 of them, and we have 16 bits to work with, so that sounds +easy. However, there's a bit of a catch. The register follows a "low-active" +convention, where pressing a button _clears_ that bit until it's released. + +```rust +const NO_BUTTONS_PRESSED: u16 = 0b0000_0011_1111_1111; +``` + +The buttons are, going up in order from the 0th bit: + +* A +* B +* Select +* Start +* Right +* Left +* Up +* Down +* R +* L + +Bits above that are not used. However, since the left and right directions, as +well as the up and down directions, can never be pressed at the same time, the +`KEYINPUT` register should never read as zero. Of course, the register _might_ +read as zero if someone is using an emulator that allows for such inputs, so I +wouldn't go so far as to make it be `NonZeroU16` or anything like that. + +When programming, we usually are thinking of what buttons we want to have _be +pressed_ instead of buttons we want to have _not be pressed_. This means that we +need an inversion to happen somewhere along the line. The easiest moment of +inversion is immediately as you read in from the register and wrap the value up +in a newtype. + +```rust +pub fn read_key_input() -> KeyInput { + KeyInput(KEYINPUT.read() ^ 0b0000_0011_1111_1111) +} +``` + +Now the KeyInput you get can be checked for what buttons are pressed by checking +for a set bit like you'd do anywhere else. + +```rust +impl KeyInput { + pub fn a_pressed(self) -> bool { + (self.0 & A_BIT) > 0 + } +} +``` + +Note that the current `KEYINPUT` value changes in real time as the user presses +or releases the buttons. To account for this, it's best to read the value just +once per game frame and then use that single value as if it was the input across +the whole frame. If you've worked with polling input before that should sound +totally normal. If not, just remember to call `read_key_input` once per frame +and then use that `KeyInput` value across the whole frame. + +### Detecting New Presses + +The keypad only tells you what's _currently_ pressed, but if you want to check +what's _newly_ pressed it's not too much harder. + +All that you do is store the last frame's keys and compare them to the current +keys with an `XOR`. In the `gba` crate it's called `KeyInput::difference`. Once +you've got the difference between last frame and this frame, you know what +changes happened. + +* If something is in the difference and _not pressed_ in the last frame, that + means it was newly pressed. +* If something is in the difference and _pressed_ in the last frame that means + it was newly released. +* If something is not in the difference then there's no change between last + frame and this frame. + +## Key Interrupt Control + +* KEYCNT, `0x400_0132`, `u16`, read/write + +This lets you control what keys will trigger a keypad interrupt. Of course, for +the actual interrupt to fire you also need to set the `IME` and `IE` registers +properly. See the [Interrupts](05-interrupts.md) section for details there. + +The main thing to know about this register is that the keys are in _the exact +same order_ as the key input order. However, with this register they use a +high-active convention instead (eg: the bit is active when the button should be +pressed as part of the interrupt). + +In addition to simply having the bits for the buttons, bit 14 is a flag for +enabling keypad interrupts (in addition to the flag in the `IE` register), and +bit 15 decides how having more than one button works. If bit 15 is disabled, +it's an OR combination (eg: "press any key to continue"). If bit 15 is enabled +it's an AND combination (eg: "press A+B+Start+Select to reset"). diff --git a/book/src/05-examples/01-hello_magic.md b/book/src/05-examples/01-hello_magic.md deleted file mode 100644 index ba1a038..0000000 --- a/book/src/05-examples/01-hello_magic.md +++ /dev/null @@ -1 +0,0 @@ -# hello_magic diff --git a/book/src/05-examples/02-hello_world.md b/book/src/05-examples/02-hello_world.md deleted file mode 100644 index 115e729..0000000 --- a/book/src/05-examples/02-hello_world.md +++ /dev/null @@ -1 +0,0 @@ -# hello_world diff --git a/book/src/05-examples/03-light_cycle.md b/book/src/05-examples/03-light_cycle.md deleted file mode 100644 index f72194e..0000000 --- a/book/src/05-examples/03-light_cycle.md +++ /dev/null @@ -1 +0,0 @@ -# light_cycle diff --git a/book/src/05-examples/04-bg_demo.md b/book/src/05-examples/04-bg_demo.md deleted file mode 100644 index 8cc8680..0000000 --- a/book/src/05-examples/04-bg_demo.md +++ /dev/null @@ -1 +0,0 @@ -# bg_demo diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index 704ba7a..a697feb 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -35,7 +35,3 @@ * [Link Cable](04-non-video/06-link_cable.md) * [Game Pak](04-non-video/07-game_pak.md) * [Examples](05-examples/00-index.md) - * [hello_magic](05-examples/01-hello_magic.md) - * [hello_world](05-examples/02-hello_world.md) - * [light_cycle](05-examples/03-light_cycle.md) - * [bg_demo](05-examples/04-bg_demo.md) diff --git a/examples/hello_world.rs b/examples/hello_world.rs index 549569a..1f5a360 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -1,83 +1,12 @@ #![no_std] #![feature(start)] -#![feature(underscore_const_names)] +#![forbid(unsafe_code)] -#[macro_export] -macro_rules! newtype { - ($(#[$attr:meta])* $new_name:ident, $old_name:ident) => { - $(#[$attr])* - #[repr(transparent)] - pub struct $new_name($old_name); - }; -} - -#[macro_export] -macro_rules! const_assert { - ($condition:expr) => { - #[deny(const_err)] - #[allow(dead_code)] - const _: usize = 0 - !$condition as usize; - }; -} - -/// 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 <= 31); - const_assert!($g <= 31); - const_assert!($b <= 31); - Color::new($r, $g, $b) - }}; -} - -// TODO: kill this -#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)] -#[repr(transparent)] -pub struct VolatilePtr(pub *mut T); -impl VolatilePtr { - pub unsafe fn read(&self) -> T { - core::ptr::read_volatile(self.0) - } - pub unsafe fn write(&self, data: T) { - core::ptr::write_volatile(self.0, data); - } - pub unsafe fn offset(self, count: isize) -> Self { - VolatilePtr(self.0.wrapping_offset(count)) - } -} - -newtype! { - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - Color, u16 -} - -impl Color { - /// Combines the Red, Blue, and Green provided into a single color value. - pub const fn new(red: u16, green: u16, blue: u16) -> Color { - Color(blue << 10 | green << 5 | red) - } -} - -newtype! { - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - DisplayControlSetting, u16 -} - -pub const DISPLAY_CONTROL: VolatilePtr = VolatilePtr(0x0400_0000 as *mut DisplayControlSetting); -pub const JUST_MODE3: DisplayControlSetting = DisplayControlSetting(3); -pub const JUST_BG2: DisplayControlSetting = DisplayControlSetting(0b100_0000_0000); -pub const JUST_MODE3_AND_BG2: DisplayControlSetting = DisplayControlSetting(JUST_MODE3.0 | JUST_BG2.0); - -pub struct Mode3; -impl Mode3 { - const SCREEN_WIDTH: isize = 240; - const PIXELS: VolatilePtr = VolatilePtr(0x600_0000 as *mut Color); - - pub unsafe fn draw_pixel_unchecked(col: isize, row: isize, color: Color) { - Self::PIXELS.offset(col + row * Self::SCREEN_WIDTH).write(color); - } -} +use gba::{ + io::display::{DisplayControlMode, DisplayControlSetting, DISPCNT}, + video::Mode3, + Color, +}; #[panic_handler] fn panic(_info: &core::panic::PanicInfo) -> ! { @@ -86,11 +15,10 @@ fn panic(_info: &core::panic::PanicInfo) -> ! { #[start] fn main(_argc: isize, _argv: *const *const u8) -> isize { - unsafe { - DISPLAY_CONTROL.write(JUST_MODE3_AND_BG2); - Mode3::draw_pixel_unchecked(120, 80, const_rgb!(31, 0, 0)); - Mode3::draw_pixel_unchecked(136, 80, const_rgb!(0, 31, 0)); - Mode3::draw_pixel_unchecked(120, 96, const_rgb!(0, 0, 31)); - loop {} - } + const SETTING: DisplayControlSetting = DisplayControlSetting::new().with_mode(DisplayControlMode::Bitmap3).with_display_bg2(true); + DISPCNT.write(SETTING); + Mode3::write_pixel(120, 80, Color::from_rgb(31, 0, 0)); + Mode3::write_pixel(136, 80, Color::from_rgb(0, 31, 0)); + Mode3::write_pixel(120, 96, Color::from_rgb(0, 0, 31)); + loop {} } diff --git a/examples/light_cycle.rs b/examples/light_cycle.rs index c593c19..ee367f9 100644 --- a/examples/light_cycle.rs +++ b/examples/light_cycle.rs @@ -1,5 +1,15 @@ #![no_std] #![feature(start)] +#![forbid(unsafe_code)] + +use gba::{ + io::{ + display::{spin_until_vblank, spin_until_vdraw, DisplayControlMode, DisplayControlSetting, DISPCNT}, + keypad::read_key_input, + }, + video::Mode3, + Color, +}; #[panic_handler] fn panic(_info: &core::panic::PanicInfo) -> ! { @@ -8,168 +18,44 @@ fn panic(_info: &core::panic::PanicInfo) -> ! { #[start] fn main(_argc: isize, _argv: *const *const u8) -> isize { - unsafe { - DISPCNT.write(MODE3 | BG2); - } + const SETTING: DisplayControlSetting = DisplayControlSetting::new().with_mode(DisplayControlMode::Bitmap3).with_display_bg2(true); + DISPCNT.write(SETTING); - let mut px = SCREEN_WIDTH / 2; - let mut py = SCREEN_HEIGHT / 2; - let mut color = rgb16(31, 0, 0); + let mut px = Mode3::SCREEN_WIDTH / 2; + let mut py = Mode3::SCREEN_HEIGHT / 2; + let mut color = Color::from_rgb(31, 0, 0); loop { // read the input for this frame - let this_frame_keys = key_input(); + let this_frame_keys = read_key_input(); // adjust game state and wait for vblank - px += 2 * this_frame_keys.column_direction() as isize; - py += 2 * this_frame_keys.row_direction() as isize; - wait_until_vblank(); + px = px.wrapping_add(2 * this_frame_keys.column_direction() as usize); + py = py.wrapping_add(2 * this_frame_keys.row_direction() as usize); + spin_until_vblank(); // draw the new game and wait until the next frame starts. - unsafe { - if px < 0 || py < 0 || px == SCREEN_WIDTH || py == SCREEN_HEIGHT { - // out of bounds, reset the screen and position. - mode3_clear_screen(0); + const BLACK: Color = Color::from_rgb(0, 0, 0); + if px >= Mode3::SCREEN_WIDTH || py >= Mode3::SCREEN_HEIGHT { + // out of bounds, reset the screen and position. + Mode3::clear_to(BLACK); + color = color.rotate_left(5); + px = Mode3::SCREEN_WIDTH / 2; + py = Mode3::SCREEN_HEIGHT / 2; + } else { + let color_here = Mode3::read_pixel(px, py); + if color_here != Some(BLACK) { + // crashed into our own line, reset the screen + Mode3::clear_to(BLACK); color = color.rotate_left(5); - px = SCREEN_WIDTH / 2; - py = SCREEN_HEIGHT / 2; } else { - let color_here = mode3_read_pixel(px, py); - if color_here != 0 { - // crashed into our own line, reset the screen - mode3_clear_screen(0); - color = color.rotate_left(5); - } else { - // draw the new part of the line - mode3_draw_pixel(px, py, color); - mode3_draw_pixel(px, py + 1, color); - mode3_draw_pixel(px + 1, py, color); - mode3_draw_pixel(px + 1, py + 1, color); - } + // draw the new part of the line + Mode3::write_pixel(px, py, color); + Mode3::write_pixel(px, py + 1, color); + Mode3::write_pixel(px + 1, py, color); + Mode3::write_pixel(px + 1, py + 1, color); } } - wait_until_vdraw(); + spin_until_vdraw(); } } - -#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)] -#[repr(transparent)] -pub struct VolatilePtr(pub *mut T); -impl VolatilePtr { - pub unsafe fn read(&self) -> T { - core::ptr::read_volatile(self.0) - } - pub unsafe fn write(&self, data: T) { - core::ptr::write_volatile(self.0, data); - } - pub unsafe fn offset(self, count: isize) -> Self { - VolatilePtr(self.0.wrapping_offset(count)) - } -} - -pub const DISPCNT: VolatilePtr = VolatilePtr(0x04000000 as *mut u16); -pub const MODE3: u16 = 3; -pub const BG2: u16 = 0b100_0000_0000; - -pub const VRAM: usize = 0x06000000; -pub const SCREEN_WIDTH: isize = 240; -pub const SCREEN_HEIGHT: isize = 160; - -pub const fn rgb16(red: u16, green: u16, blue: u16) -> u16 { - blue << 10 | green << 5 | red -} - -pub unsafe fn mode3_clear_screen(color: u16) { - let color = color as u32; - let bulk_color = color << 16 | color; - let mut ptr = VolatilePtr(VRAM as *mut u32); - for _ in 0..SCREEN_HEIGHT { - for _ in 0..(SCREEN_WIDTH / 2) { - ptr.write(bulk_color); - ptr = ptr.offset(1); - } - } -} - -pub unsafe fn mode3_draw_pixel(col: isize, row: isize, color: u16) { - VolatilePtr(VRAM as *mut u16).offset(col + row * SCREEN_WIDTH).write(color); -} - -pub unsafe fn mode3_read_pixel(col: isize, row: isize) -> u16 { - VolatilePtr(VRAM as *mut u16).offset(col + row * SCREEN_WIDTH).read() -} - -pub const KEYINPUT: VolatilePtr = VolatilePtr(0x400_0130 as *mut u16); - -/// A newtype over the key input state of the GBA. -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] -#[repr(transparent)] -pub struct KeyInputSetting(u16); - -/// A "tribool" value helps us interpret the arrow pad. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[repr(i32)] -pub enum TriBool { - Minus = -1, - Neutral = 0, - Plus = 1, -} - -pub fn key_input() -> KeyInputSetting { - unsafe { KeyInputSetting(KEYINPUT.read() ^ 0b0000_0011_1111_1111) } -} - -pub const KEY_A: u16 = 1 << 0; -pub const KEY_B: u16 = 1 << 1; -pub const KEY_SELECT: u16 = 1 << 2; -pub const KEY_START: u16 = 1 << 3; -pub const KEY_RIGHT: u16 = 1 << 4; -pub const KEY_LEFT: u16 = 1 << 5; -pub const KEY_UP: u16 = 1 << 6; -pub const KEY_DOWN: u16 = 1 << 7; -pub const KEY_R: u16 = 1 << 8; -pub const KEY_L: u16 = 1 << 9; - -impl KeyInputSetting { - pub fn contains(&self, key: u16) -> bool { - (self.0 & key) != 0 - } - - pub fn difference(&self, other: KeyInputSetting) -> KeyInputSetting { - KeyInputSetting(self.0 ^ other.0) - } - - pub fn column_direction(&self) -> TriBool { - if self.contains(KEY_RIGHT) { - TriBool::Plus - } else if self.contains(KEY_LEFT) { - TriBool::Minus - } else { - TriBool::Neutral - } - } - - pub fn row_direction(&self) -> TriBool { - if self.contains(KEY_DOWN) { - TriBool::Plus - } else if self.contains(KEY_UP) { - TriBool::Minus - } else { - TriBool::Neutral - } - } -} - -pub const VCOUNT: VolatilePtr = VolatilePtr(0x0400_0006 as *mut u16); - -pub fn vcount() -> u16 { - unsafe { VCOUNT.read() } -} - -pub fn wait_until_vblank() { - while vcount() < SCREEN_HEIGHT as u16 {} -} - -pub fn wait_until_vdraw() { - while vcount() >= SCREEN_HEIGHT as u16 {} -} diff --git a/examples/mgba_panic_handler.rs b/examples/mgba_panic_handler.rs index 8620d26..17e7057 100644 --- a/examples/mgba_panic_handler.rs +++ b/examples/mgba_panic_handler.rs @@ -1,109 +1,30 @@ #![no_std] #![feature(start)] +#![forbid(unsafe_code)] -#[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 - } -} +use gba::{ + io::display::{DisplayControlMode, DisplayControlSetting, DISPCNT}, + video::Mode3, + Color, +}; #[panic_handler] fn panic(info: &core::panic::PanicInfo) -> ! { - unsafe { - const DEBUG_ENABLE_MGBA: *mut u16 = 0x4fff780 as *mut u16; - const DEBUG_OUTPUT_BASE: *mut u8 = 0x4fff600 as *mut u8; - const DEBUG_SEND_MGBA: *mut u16 = 0x4fff700 as *mut u16; - const DEBUG_SEND_FLAG: u16 = 0x100; - const DEBUG_FATAL: u16 = 0; - const DEBUG_ERROR: u16 = 1; - DEBUG_ENABLE_MGBA.write_volatile(0xC0DE); - if DEBUG_ENABLE_MGBA.read_volatile() == 0x1DEA { - // Give the location - if let Some(location) = info.location() { - let mut out_ptr = DEBUG_OUTPUT_BASE; - let line = location.line(); - let mut line_bytes = [ - (line / 10000) as u8, - ((line / 1000) % 10) as u8, - ((line / 1000) % 10) as u8, - ((line / 10) % 10) as u8, - (line % 10) as u8, - ]; - for line_bytes_mut in line_bytes.iter_mut() { - *line_bytes_mut += b'0'; - } - for b in "Panic: " - .bytes() - .chain(location.file().bytes()) - .chain(", Line ".bytes()) - .chain(line_bytes.iter().cloned()) - .take(255) - { - out_ptr.write_volatile(b); - out_ptr = out_ptr.offset(1); - } - } else { - let mut out_ptr = DEBUG_OUTPUT_BASE; - for b in "Panic with no location info:".bytes().take(255) { - out_ptr.write_volatile(b); - out_ptr = out_ptr.offset(1); - } - } - DEBUG_SEND_MGBA.write_volatile(DEBUG_SEND_FLAG + DEBUG_ERROR); - // Give the payload - if let Some(payload) = info.payload().downcast_ref::<&str>() { - let mut out_ptr = DEBUG_OUTPUT_BASE; - for b in payload.bytes().take(255) { - out_ptr.write_volatile(b); - out_ptr = out_ptr.offset(1); - } - } else { - let mut out_ptr = DEBUG_OUTPUT_BASE; - for b in "no payload".bytes().take(255) { - out_ptr.write_volatile(b); - out_ptr = out_ptr.offset(1); - } - } - DEBUG_SEND_MGBA.write_volatile(DEBUG_SEND_FLAG + DEBUG_ERROR); - DEBUG_SEND_MGBA.write_volatile(DEBUG_SEND_FLAG + DEBUG_FATAL); - } + use core::fmt::Write; + use gba::mgba::{MGBADebug, MGBADebugLevel}; + if let Some(mut mgba) = MGBADebug::new() { + let _ = write!(mgba, "{}", info); + mgba.send(MGBADebugLevel::Fatal); } loop {} } #[start] fn main(_argc: isize, _argv: *const *const u8) -> isize { - unsafe { - (0x400_0000 as *mut u16).write_volatile(0x0403); - (0x600_0000 as *mut u16).offset(120 + 80 * 240).write_volatile(0x001F); - (0x600_0000 as *mut u16).offset(136 + 80 * 240).write_volatile(0x03E0); - (0x600_0000 as *mut u16).offset(120 + 96 * 240).write_volatile(0x7C00); - panic!("fumoffu!"); - } + const SETTING: DisplayControlSetting = DisplayControlSetting::new().with_mode(DisplayControlMode::Bitmap3).with_display_bg2(true); + DISPCNT.write(SETTING); + Mode3::write_pixel(120, 80, Color::from_rgb(31, 0, 0)); + Mode3::write_pixel(136, 80, Color::from_rgb(0, 31, 0)); + Mode3::write_pixel(120, 96, Color::from_rgb(0, 0, 31)); + panic!("fumoffu!"); } diff --git a/src/base.rs b/src/base.rs new file mode 100644 index 0000000..4c7b530 --- /dev/null +++ b/src/base.rs @@ -0,0 +1,10 @@ +//! Holds fundamental types/ops which the rest of the crate it built on. + +pub mod fixed_point; +//pub(crate) use self::fixed_point::*; + +pub mod volatile; +pub(crate) use self::volatile::*; + +pub mod builtins; +//pub(crate) use self::builtins::*; diff --git a/src/builtins.rs b/src/base/builtins.rs similarity index 79% rename from src/builtins.rs rename to src/base/builtins.rs index db3615e..5966450 100644 --- a/src/builtins.rs +++ b/src/base/builtins.rs @@ -65,11 +65,21 @@ pub extern "C" fn __clzsi2(mut x: usize) -> usize { #[test] fn __clzsi2_test() { - let mut i = 1 << 63; + let mut i: usize = core::usize::MAX; while i > 0 { - assert_eq!(__clzsi2(i), i.leading_zeros() as usize); + assert_eq!(__clzsi2(i) as u32, i.leading_zeros()); i >>= 1; } + // check 0 also + i = 0; + assert_eq!(__clzsi2(i) as u32, i.leading_zeros()); + // double check for bit patterns that aren't solid 1s + i = 1; + for _ in 0 .. 63 { + assert_eq!(__clzsi2(i) as u32, i.leading_zeros()); + i <<= 2; + i += 1; + } } // TODO: add some shims diff --git a/src/fixed.rs b/src/base/fixed_point.rs similarity index 100% rename from src/fixed.rs rename to src/base/fixed_point.rs diff --git a/src/core_extras.rs b/src/base/volatile.rs similarity index 98% rename from src/core_extras.rs rename to src/base/volatile.rs index 679b42f..5e12e28 100644 --- a/src/core_extras.rs +++ b/src/base/volatile.rs @@ -1,6 +1,4 @@ -//! Things that I wish were in core, but aren't. - -//TODO(Lokathor): reorganize as gba::core_extras::fixed_point and gba::core_extras::volatile ? +//! Holds types for correct handling of volatile memory. use core::{cmp::Ordering, iter::FusedIterator, marker::PhantomData, num::NonZeroUsize}; diff --git a/src/io.rs b/src/io.rs index 5149b5a..c0f5424 100644 --- a/src/io.rs +++ b/src/io.rs @@ -10,4 +10,5 @@ use super::*; use gba_proc_macro::register_bit; +pub mod display; pub mod keypad; diff --git a/src/io_registers.rs b/src/io/display.rs similarity index 69% rename from src/io_registers.rs rename to src/io/display.rs index 4ba89dc..ce7cda6 100644 --- a/src/io_registers.rs +++ b/src/io/display.rs @@ -1,21 +1,4 @@ -//! The module for all things relating to the IO Register portion of the GBA's -//! memory map. -//! -//! Here we define many constants for the volatile pointers to the various IO -//! registers. Each raw register constant is named according to the name given -//! to it in GBATEK's [GBA I/O -//! Map](http://problemkaputt.de/gbatek.htm#gbaiomap). They program in C, and so -//! of course all the names terrible and missing as many vowels as possible. -//! However, being able to look it up online is the most important thing here, -//! so oh well. -//! -//! In addition to the const `VolatilePtr` values, we will over time be adding -//! safe wrappers around each register, including newtypes and such so that you -//! can easily work with whatever each specific register is doing. - -// TODO(lokathor): IO Register newtypes. - -use gba_proc_macro::register_bit; +//! Contains types and definitions for display related IO registers. use super::*; @@ -36,6 +19,7 @@ impl DisplayControlSetting { pub const BG_MODE_MASK: u16 = 0b111; pub fn mode(self) -> DisplayControlMode { + // TODO: constify match self.0 & Self::BG_MODE_MASK { 0 => DisplayControlMode::Tiled0, 1 => DisplayControlMode::Tiled1, @@ -46,16 +30,8 @@ impl DisplayControlSetting { _ => unreachable!(), } } - pub fn set_mode(&mut self, new_mode: DisplayControlMode) { - self.0 &= !Self::BG_MODE_MASK; - self.0 |= match new_mode { - DisplayControlMode::Tiled0 => 0, - DisplayControlMode::Tiled1 => 1, - DisplayControlMode::Tiled2 => 2, - DisplayControlMode::Bitmap3 => 3, - DisplayControlMode::Bitmap4 => 4, - DisplayControlMode::Bitmap5 => 5, - }; + pub const fn with_mode(self, new_mode: DisplayControlMode) -> Self { + Self((self.0 & !Self::BG_MODE_MASK) | (new_mode as u16)) } register_bit!(CGB_MODE_BIT, u16, 0b1000, cgb_mode); @@ -75,25 +51,26 @@ impl DisplayControlSetting { /// The six display modes available on the GBA. #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u16)] pub enum DisplayControlMode { /// This basically allows for the most different things at once (all layers, /// 1024 tiles, two palette modes, etc), but you can't do affine /// transformations. - Tiled0, + Tiled0 = 0, /// This is a mix of `Tile0` and `Tile2`: BG0 and BG1 run as if in `Tiled0`, /// and BG2 runs as if in `Tiled2`. - Tiled1, + Tiled1 = 1, /// This allows affine transformations, but only uses BG2 and BG3. - Tiled2, + Tiled2 = 2, /// This is the basic bitmap draw mode. The whole screen is a single bitmap. /// Uses BG2 only. - Bitmap3, + Bitmap3 = 3, /// This uses _paletted color_ so that there's enough space to have two pages /// at _full resolution_, allowing page flipping. Uses BG2 only. - Bitmap4, + Bitmap4 = 4, /// This uses _reduced resolution_ so that there's enough space to have two /// pages with _full color_, allowing page flipping. Uses BG2 only. - Bitmap5, + Bitmap5 = 5, } /// Assigns the given display control setting. @@ -105,22 +82,29 @@ pub fn display_control() -> DisplayControlSetting { DISPCNT.read() } -/// Vertical Counter (LY) +/// If the `VCOUNT` register reads equal to or above this then you're in vblank. +pub const VBLANK_SCANLINE: u16 = 160; + +/// Vertical Counter (LY). Read only. +/// +/// Gives the current scanline that the display controller is working on. If +/// this is at or above the `VBLANK_SCANLINE` value then the display controller +/// is in a "vertical blank" period. pub const VCOUNT: VolAddress = unsafe { VolAddress::new_unchecked(0x400_0006) }; -/// Obtains the current VCount value. +/// Obtains the current `VCOUNT` value. pub fn vcount() -> u16 { VCOUNT.read() } /// Performs a busy loop until VBlank starts. -pub fn wait_until_vblank() { +pub fn spin_until_vblank() { // TODO: make this the better version with BIOS and interrupts and such. - while vcount() < SCREEN_HEIGHT as u16 {} + while vcount() < VBLANK_SCANLINE {} } /// Performs a busy loop until VDraw starts. -pub fn wait_until_vdraw() { +pub fn spin_until_vdraw() { // TODO: make this the better version with BIOS and interrupts and such. - while vcount() >= SCREEN_HEIGHT as u16 {} + while vcount() >= VBLANK_SCANLINE {} } diff --git a/src/lib.rs b/src/lib.rs index be313d4..193139f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,7 @@ #![cfg_attr(not(test), no_std)] #![feature(asm)] #![feature(const_int_wrapping)] +#![feature(const_int_rotate)] #![feature(min_const_unsafe_fn)] #![warn(missing_docs)] #![allow(clippy::cast_lossless)] @@ -57,18 +58,39 @@ macro_rules! newtype { }; } -pub mod builtins; - -pub mod fixed; - +pub mod base; +pub(crate) use self::base::*; pub mod bios; - -pub mod core_extras; -pub(crate) use crate::core_extras::*; - pub mod io; +pub mod mgba; +pub mod video; -pub mod video_ram; +newtype! { + /// A color on the GBA is an RGB 5.5.5 within a `u16` + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + Color, u16 +} + +impl Color { + /// Constructs a color from the channel values provided (should be 0..=31). + /// + /// No actual checks are performed, so illegal channel values can overflow + /// into each other and produce an unintended color. + pub const fn from_rgb(r: u16, g: u16, b: u16) -> Color { + Color(b << 10 | g << 5 | r) + } + + /// Does a left rotate of the bits. + /// + /// This has no particular meaning but is a wild way to cycle colors. + pub const fn rotate_left(self, n: u32) -> Color { + Color(self.0.rotate_left(n)) + } +} + +// +// After here is totally unsorted nonsense +// /// Performs unsigned divide and remainder, gives None if dividing by 0. pub fn divrem_u32(numer: u32, denom: u32) -> Option<(u32, u32)> { diff --git a/src/mgba.rs b/src/mgba.rs new file mode 100644 index 0000000..24e6648 --- /dev/null +++ b/src/mgba.rs @@ -0,0 +1,79 @@ +//! Special utils for if you're running on the mGBA emulator. + +use super::*; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u16)] +#[allow(missing_docs)] +pub enum MGBADebugLevel { + Fatal = 0, + Error = 1, + Warning = 2, + Info = 3, + Debug = 4, +} + +/// Allows writing to the `mGBA` debug output. +#[derive(Debug, PartialEq, Eq)] +pub struct MGBADebug { + bytes_written: u8, +} +impl MGBADebug { + const ENABLE_ADDRESS: VolAddress = unsafe { VolAddress::new_unchecked(0x4fff780) }; + const ENABLE_ADDRESS_INPUT: u16 = 0xC0DE; + const ENABLE_ADDRESS_OUTPUT: u16 = 0x1DEA; + + const OUTPUT_BASE: VolAddress = unsafe { VolAddress::new_unchecked(0x4fff600) }; + + const SEND_ADDRESS: VolAddress = unsafe { VolAddress::new_unchecked(0x4fff700) }; + const SEND_FLAG: u16 = 0x100; + + /// Gives a new MGBADebug, if running within `mGBA` + /// + /// # Fails + /// + /// If you're not running in the `mGBA` emulator. + pub fn new() -> Option { + Self::ENABLE_ADDRESS.write(Self::ENABLE_ADDRESS_INPUT); + if Self::ENABLE_ADDRESS.read() == Self::ENABLE_ADDRESS_OUTPUT { + Some(MGBADebug { bytes_written: 0 }) + } else { + None + } + } + + /// Once output is buffered you must send it out with a level. + /// + /// If the `Fatal` level is selected, the buffer is sent out as `Error` + /// followed by a blank message being sent as `Error`. This is done because + /// the `Fatal` message appears in a popup without showing up in the log, so + /// it might accidentally be discarded. + pub fn send(&mut self, level: MGBADebugLevel) { + if level == MGBADebugLevel::Fatal { + Self::SEND_ADDRESS.write(Self::SEND_FLAG | MGBADebugLevel::Error as u16); + Self::SEND_ADDRESS.write(Self::SEND_FLAG | MGBADebugLevel::Fatal as u16); + } else { + Self::SEND_ADDRESS.write(Self::SEND_FLAG | level as u16); + } + } +} + +impl core::fmt::Write for MGBADebug { + fn write_str(&mut self, s: &str) -> Result<(), core::fmt::Error> { + unsafe { + let mut current = Self::OUTPUT_BASE.offset(self.bytes_written as isize); + let mut str_iter = s.bytes(); + while self.bytes_written < 255 { + match str_iter.next() { + Some(byte) => { + current.write(byte); + current = current.offset(1); + self.bytes_written += 1; + } + None => return Ok(()), + } + } + Err(core::fmt::Error) + } + } +} diff --git a/src/video.rs b/src/video.rs new file mode 100644 index 0000000..d6bb6c7 --- /dev/null +++ b/src/video.rs @@ -0,0 +1,83 @@ +//! Module for all things relating to the Video RAM. +//! +//! Note that the GBA has six different display modes available, and the +//! _meaning_ of Video RAM depends on which display mode is active. In all +//! cases, Video RAM is **96kb** from `0x0600_0000` to `0x0601_7FFF`. +//! +//! # Safety +//! +//! Note that all possible bit patterns are technically allowed within Video +//! Memory. If you write the "wrong" thing into video memory you don't crash the +//! GBA, instead you just get graphical glitches (or perhaps nothing at all). +//! Accordingly, the "safe" functions here will check that you're in bounds, but +//! they won't bother to check that you've set the video mode they're designed +//! for. + +pub use super::*; + +/// The start of VRAM. +/// +/// Depending on what display mode is currently set there's different ways that +/// your program should interpret the VRAM space. Accordingly, we give the raw +/// value as just being a `usize`. Specific video mode types then wrap this as +/// being the correct thing. +pub const VRAM_BASE_USIZE: usize = 0x600_0000; + +/// Mode 3 is a bitmap mode with full color and full resolution. +/// +/// * **Width:** 240 +/// * **Height:** 160 +/// +/// Because the memory requirements are so large, there's only a single page +/// available instead of two pages like the other video modes have. +/// +/// As with all bitmap modes, the bitmap itself utilizes BG2 for display, so you +/// must have that BG enabled in addition to being within Mode 3. +pub struct Mode3; +impl Mode3 { + /// The physical width in pixels of the GBA screen. + pub const SCREEN_WIDTH: usize = 240; + + /// The physical height in pixels of the GBA screen. + pub const SCREEN_HEIGHT: usize = 160; + + /// The Mode 3 VRAM. + /// + /// Use `col + row * SCREEN_WIDTH` to get the address of an individual pixel, + /// or use the helpers provided in this module. + pub const VRAM: VolAddressBlock = + unsafe { VolAddressBlock::new_unchecked(VolAddress::new_unchecked(VRAM_BASE_USIZE), Self::SCREEN_WIDTH * Self::SCREEN_HEIGHT) }; + + /// private iterator over the pixels, two at a time + const BULK_ITER: VolAddressIter = + unsafe { VolAddressBlock::new_unchecked(VolAddress::new_unchecked(VRAM_BASE_USIZE), Self::SCREEN_WIDTH * Self::SCREEN_HEIGHT / 2).iter() }; + + /// Reads the pixel at the given (col,row). + /// + /// # Failure + /// + /// Gives `None` if out of bounds. + pub fn read_pixel(col: usize, row: usize) -> Option { + Self::VRAM.get(col + row * Self::SCREEN_WIDTH).map(VolAddress::read) + } + + /// Writes the pixel at the given (col,row). + /// + /// # Failure + /// + /// Gives `None` if out of bounds. + pub fn write_pixel(col: usize, row: usize, color: Color) -> Option<()> { + Self::VRAM.get(col + row * Self::SCREEN_WIDTH).map(|va| va.write(color)) + } + + /// Clears the whole screen to the desired color. + pub fn clear_to(color: Color) { + let color32 = color.0 as u32; + let bulk_color = color32 << 16 | color32; + for va in Self::BULK_ITER { + va.write(bulk_color) + } + } + + // TODO: dma_clear_to? +} diff --git a/src/video_ram.rs b/src/video_ram.rs deleted file mode 100644 index 232249f..0000000 --- a/src/video_ram.rs +++ /dev/null @@ -1,83 +0,0 @@ -//! Module for all things relating to the Video RAM. -//! -//! Note that the GBA has six different display modes available, and the -//! _meaning_ of Video RAM depends on which display mode is active. In all -//! cases, Video RAM is **96kb** from `0x0600_0000` to `0x0601_7FFF`. -//! -//! # Safety -//! -//! Note that all possible bit patterns are technically allowed within Video -//! Memory. If you write the "wrong" thing into video memory you don't crash the -//! GBA, instead you just get graphical glitches (or perhaps nothing at all). -//! Accordingly, the "safe" functions here will check that you're in bounds, but -//! they won't bother to check that you've set the video mode they're designed -//! for. - -pub use super::*; - -// TODO: kill all this too - -/// The physical width in pixels of the GBA screen. -pub const SCREEN_WIDTH: isize = 240; - -/// The physical height in pixels of the GBA screen. -pub const SCREEN_HEIGHT: isize = 160; - -/// The start of VRAM. -/// -/// Depending on what display mode is currently set there's different ways that -/// your program should interpret the VRAM space. Accordingly, we give the raw -/// value as just being a `usize`. -pub const VRAM_BASE_ADDRESS: usize = 0x0600_0000; - -const MODE3_VRAM: VolAddress = unsafe { VolAddress::new_unchecked(VRAM_BASE_ADDRESS) }; - -/// Draws a pixel to the screen while in Display Mode 3, with bounds checks. -/// -/// # Panics -/// -/// If `col` or `row` are out of bounds this will panic. -pub fn mode3_draw_pixel(col: isize, row: isize, color: u16) { - assert!(col >= 0 && col < SCREEN_WIDTH); - assert!(row >= 0 && row < SCREEN_HEIGHT); - unsafe { mode3_draw_pixel_unchecked(col, row, color) } -} - -/// Draws a pixel to the screen while in Display Mode 3. -/// -/// Coordinates are relative to the top left corner. -/// -/// If you're in another mode you'll get something weird drawn, but it's memory -/// safe in the rust sense as long as you stay in bounds. -/// -/// # Safety -/// -/// * `col` must be in `0..SCREEN_WIDTH` -/// * `row` must be in `0..SCREEN_HEIGHT` -pub unsafe fn mode3_draw_pixel_unchecked(col: isize, row: isize, color: u16) { - MODE3_VRAM.offset(col + row * SCREEN_WIDTH).write(color); -} - -/// Reads the given pixel of video memory according to Mode 3 placement. -/// -/// # Failure -/// -/// If the location is out of bounds you get `None`. -pub fn mode3_read_pixel(col: isize, row: isize) -> Option { - if col >= 0 && col < SCREEN_WIDTH && row >= 0 && row < SCREEN_HEIGHT { - unsafe { Some(MODE3_VRAM.offset(col + row * SCREEN_WIDTH).read()) } - } else { - None - } -} - -/// Clears the entire screen to the color specified. -pub unsafe fn mode3_clear_screen(color: u16) { - // TODO: use DMA? - let color = color as u32; - let bulk_color = color << 16 | color; - let block: VolAddressBlock = VolAddressBlock::new_unchecked(MODE3_VRAM.cast::(), (SCREEN_HEIGHT * SCREEN_WIDTH / 2) as usize); - for b in block.iter() { - b.write(bulk_color); - } -}