mirror of
https://github.com/italicsjenga/gba.git
synced 2024-12-23 19:01:30 +11:00
Merge pull request #38 from rust-console/lokathor
Hello World is 100% Safe
This commit is contained in:
commit
9b7751f3d0
|
@ -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
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<T>` 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<VolAddress<T>>` 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<T>(pub *mut T);
|
||||
```
|
||||
|
||||
Obviously we want to be able to read and write:
|
||||
|
||||
```rust
|
||||
impl<T> VolatilePtr<T> {
|
||||
/// 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<T>(address: usize, index: usize) -> usize {
|
||||
address + (index * std::mem::size_of::<T>())
|
||||
pub struct VolAddress<T> {
|
||||
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<Z>(self) -> VolatilePtr<Z> {
|
||||
VolatilePtr(self.0 as *mut Z)
|
||||
impl<T> Clone for VolAddress<T> {
|
||||
fn clone(&self) -> Self {
|
||||
*self
|
||||
}
|
||||
}
|
||||
impl<T> Copy for VolAddress<T> {}
|
||||
impl<T> PartialEq for VolAddress<T> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.address == other.address
|
||||
}
|
||||
}
|
||||
impl<T> Eq for VolAddress<T> {}
|
||||
impl<T> PartialOrd for VolAddress<T> {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.address.cmp(&other.address))
|
||||
}
|
||||
}
|
||||
impl<T> Ord for VolAddress<T> {
|
||||
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<T>`, then we can safely _use_ the value once it's been properly
|
||||
created.
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
|
||||
pub struct VolatilePtrIter<T> {
|
||||
vol_ptr: VolatilePtr<T>,
|
||||
// you'll need these features enabled and a recent nightly
|
||||
#![feature(const_int_wrapping)]
|
||||
#![feature(min_const_unsafe_fn)]
|
||||
|
||||
impl<T> VolAddress<T> {
|
||||
pub const unsafe fn new_unchecked(address: usize) -> Self {
|
||||
VolAddress {
|
||||
address: NonZeroUsize::new_unchecked(address),
|
||||
marker: PhantomData,
|
||||
}
|
||||
}
|
||||
pub const unsafe fn cast<Z>(self) -> VolAddress<Z> {
|
||||
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::<T>())),
|
||||
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<u16> = 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<T> VolAddress<T> {
|
||||
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<T> VolAddress<T> {
|
||||
pub const unsafe fn iter_slots(self, slots: usize) -> VolAddressIter<T> {
|
||||
VolAddressIter { vol_address: self, slots }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct VolAddressIter<T> {
|
||||
vol_address: VolAddress<T>,
|
||||
slots: usize,
|
||||
}
|
||||
```
|
||||
impl<T> Clone for VolAddressIter<T> {
|
||||
fn clone(&self) -> Self {
|
||||
VolAddressIter {
|
||||
vol_address: self.vol_address,
|
||||
slots: self.slots,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<T> PartialEq for VolAddressIter<T> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.vol_address == other.vol_address && self.slots == other.slots
|
||||
}
|
||||
}
|
||||
impl<T> Eq for VolAddressIter<T> {}
|
||||
impl<T> Iterator for VolAddressIter<T> {
|
||||
type Item = VolAddress<T>;
|
||||
|
||||
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<T> Iterator for VolatilePtrIter<T> {
|
||||
type Item = VolatilePtr<T>;
|
||||
|
||||
fn next(&mut self) -> Option<VolatilePtr<T>> {
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
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<T> FusedIterator for VolAddressIter<T> {}
|
||||
```
|
||||
|
||||
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<T>` 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 `<T:Clone>`, 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<T>` 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<T: Clone> Iterator for VolatilePtrIter<T> {
|
||||
type Item = VolatilePtr<T>;
|
||||
|
||||
fn next(&mut self) -> Option<VolatilePtr<T>> {
|
||||
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<T> {
|
||||
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<T> core::fmt::Pointer for VolatilePtr<T> {
|
||||
/// 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<T>(pub *mut T);
|
||||
impl<T> VolatilePtr<T> {
|
||||
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<Z>(self) -> VolatilePtr<Z> {
|
||||
VolatilePtr(self.0 as *mut Z)
|
||||
}
|
||||
pub unsafe fn iter_slots(self, slots: usize) -> VolatilePtrIter<T> {
|
||||
VolatilePtrIter {
|
||||
vol_ptr: self,
|
||||
slots,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<T> core::fmt::Pointer for VolatilePtr<T> {
|
||||
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<T> {
|
||||
vol_ptr: VolatilePtr<T>,
|
||||
#[derive(Debug)]
|
||||
pub struct VolAddressBlock<T> {
|
||||
vol_address: VolAddress<T>,
|
||||
slots: usize,
|
||||
}
|
||||
impl<T: Clone> Iterator for VolatilePtrIter<T> {
|
||||
type Item = VolatilePtr<T>;
|
||||
fn next(&mut self) -> Option<VolatilePtr<T>> {
|
||||
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<T> Clone for VolAddressBlock<T> {
|
||||
fn clone(&self) -> Self {
|
||||
VolAddressBlock {
|
||||
vol_address: self.vol_address,
|
||||
slots: self.slots,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<T> PartialEq for VolAddressBlock<T> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.vol_address == other.vol_address && self.slots == other.slots
|
||||
}
|
||||
}
|
||||
impl<T> Eq for VolAddressBlock<T> {}
|
||||
|
||||
impl<T> VolAddressBlock<T> {
|
||||
pub const unsafe fn new_unchecked(vol_address: VolAddress<T>, slots: usize) -> Self {
|
||||
VolAddressBlock { vol_address, slots }
|
||||
}
|
||||
pub const fn iter(self) -> VolAddressIter<T> {
|
||||
VolAddressIter {
|
||||
vol_address: self.vol_address,
|
||||
slots: self.slots,
|
||||
}
|
||||
}
|
||||
pub unsafe fn index_unchecked(self, slot: usize) -> VolAddress<T> {
|
||||
self.vol_address.offset(slot as isize)
|
||||
}
|
||||
pub fn index(self, slot: usize) -> VolAddress<T> {
|
||||
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<VolAddress<T>> {
|
||||
if slot < self.slots {
|
||||
unsafe { Some(self.vol_address.offset(slot as isize)) }
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
@ -323,6 +295,26 @@ impl<T: Clone> Iterator for VolatilePtrIter<T> {
|
|||
}
|
||||
```
|
||||
|
||||
Now we can have something like:
|
||||
|
||||
```rust
|
||||
const OTHER_MAGIC: VolAddressBlock<u16> = 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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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").
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
# hello_magic
|
|
@ -1 +0,0 @@
|
|||
# hello_world
|
|
@ -1 +0,0 @@
|
|||
# light_cycle
|
|
@ -1 +0,0 @@
|
|||
# bg_demo
|
|
@ -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)
|
||||
|
|
|
@ -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<T>(pub *mut T);
|
||||
impl<T> VolatilePtr<T> {
|
||||
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<DisplayControlSetting> = 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<Color> = 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 {}
|
||||
}
|
||||
|
|
|
@ -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<T>(pub *mut T);
|
||||
impl<T> VolatilePtr<T> {
|
||||
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<u16> = 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<u16> = 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<u16> = 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 {}
|
||||
}
|
||||
|
|
|
@ -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!");
|
||||
}
|
||||
|
|
10
src/base.rs
Normal file
10
src/base.rs
Normal file
|
@ -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::*;
|
|
@ -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
|
|
@ -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};
|
||||
|
|
@ -10,4 +10,5 @@ use super::*;
|
|||
|
||||
use gba_proc_macro::register_bit;
|
||||
|
||||
pub mod display;
|
||||
pub mod keypad;
|
||||
|
|
|
@ -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<u16> = 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 {}
|
||||
}
|
40
src/lib.rs
40
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)> {
|
||||
|
|
79
src/mgba.rs
Normal file
79
src/mgba.rs
Normal file
|
@ -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<u16> = unsafe { VolAddress::new_unchecked(0x4fff780) };
|
||||
const ENABLE_ADDRESS_INPUT: u16 = 0xC0DE;
|
||||
const ENABLE_ADDRESS_OUTPUT: u16 = 0x1DEA;
|
||||
|
||||
const OUTPUT_BASE: VolAddress<u8> = unsafe { VolAddress::new_unchecked(0x4fff600) };
|
||||
|
||||
const SEND_ADDRESS: VolAddress<u16> = 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> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
83
src/video.rs
Normal file
83
src/video.rs
Normal file
|
@ -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<Color> =
|
||||
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<u32> =
|
||||
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<Color> {
|
||||
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?
|
||||
}
|
|
@ -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<u16> = 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<u16> {
|
||||
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<u32> = VolAddressBlock::new_unchecked(MODE3_VRAM.cast::<u32>(), (SCREEN_HEIGHT * SCREEN_WIDTH / 2) as usize);
|
||||
for b in block.iter() {
|
||||
b.write(bulk_color);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue