mirror of
https://github.com/italicsjenga/gba.git
synced 2024-12-23 19:01:30 +11:00
Merge pull request #35 from rust-console/lokathor
Moderate updates to Introduction and Limitations
This commit is contained in:
commit
8eec5399d8
|
@ -43,10 +43,10 @@ fn main() -> std::io::Result<()> {
|
|||
std::process::Command::new("arm-none-eabi-objcopy").args(
|
||||
&["-O", "binary",
|
||||
&format!("target/thumbv4-none-agb/release/examples/{}",name),
|
||||
&format!("target/example-{}.gba",name)])
|
||||
&format!("target/{}.gba",name)])
|
||||
.output().expect("failed to objcopy!");
|
||||
std::process::Command::new("gbafix").args(
|
||||
&[&format!("target/example-{}.gba",name)])
|
||||
&[&format!("target/{}.gba",name)])
|
||||
.output().expect("failed to gbafix!");
|
||||
}
|
||||
}
|
||||
|
|
18
README.md
18
README.md
|
@ -8,17 +8,19 @@
|
|||
|
||||
# gba
|
||||
|
||||
A crate that helps you make GameBoy Advance (GBA) games
|
||||
This repository is both a [Tutorial Book](https://rust-console.github.io/gba/)
|
||||
that teaches you what you need to know to write Rust games for the GameBoy
|
||||
Advance (GBA), and also a [crate](https://crates.io/crates/gba) that you can
|
||||
use to do the same.
|
||||
|
||||
# First Time Setup
|
||||
## First Time Setup
|
||||
|
||||
This crate requires a fair amount of special setup. All of the steps are
|
||||
detailed for you [in the 0th chapter of the
|
||||
book](https://rust-console.github.io/gba/00-introduction/03-development-setup.html) that goes with this
|
||||
crate.
|
||||
Writing a Rust program for the GBA requires a fair amount of special setup. All
|
||||
of the steps are detailed for you [in the Introduction chapter of the
|
||||
book](https://rust-console.github.io/gba/00-introduction/03-development-setup.html).
|
||||
|
||||
If you've done the global setup once before and just want to get a new project
|
||||
started quickly we got you covered:
|
||||
If you've done the described global setup once before and just want to get a new
|
||||
project started quickly we got you covered:
|
||||
|
||||
```sh
|
||||
curl https://raw.githubusercontent.com/rust-console/gba/master/init.sh -sSf | bash -s APP_NAME
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# Introduction
|
||||
|
||||
This is the book for learning how to write GBA games in Rust.
|
||||
This is the book for learning how to write GameBoy Advance (GBA) games in Rust.
|
||||
|
||||
I'm **Lokathor**, the main author of the book. There's also **Ketsuban** who
|
||||
provides the technical advisement, reviews the PRs, and keeps my crazy in check.
|
||||
|
@ -10,7 +10,12 @@ of the pages listed in the Table Of Contents.
|
|||
|
||||
## Feedback
|
||||
|
||||
It's also often hard to tell when you've explained something properly to someone
|
||||
who doesn't understand the concept yet. Please, if things don't make sense then
|
||||
[file an issue](https://github.com/rust-console/gba/issues) about it so I know
|
||||
where things need to improve.
|
||||
It's very often hard to tell when you've explained something properly. In the
|
||||
same way that your brain will read over small misspellings and correct things
|
||||
into the right word, if an explanation for something you already understand
|
||||
accidentally skips over some small detail then your brain can fill in the gaps
|
||||
without you realizing it.
|
||||
|
||||
**Please**, if things don't make sense then [file an
|
||||
issue](https://github.com/rust-console/gba/issues) about it so I know where
|
||||
things need to improve.
|
||||
|
|
|
@ -21,8 +21,8 @@ goes:
|
|||
`hello_magic.rs`:
|
||||
|
||||
```rust
|
||||
#![feature(start)]
|
||||
#![no_std]
|
||||
#![feature(start)]
|
||||
|
||||
#[panic_handler]
|
||||
fn panic(_info: &core::panic::PanicInfo) -> ! {
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
# GBA Limitations
|
|
@ -1 +0,0 @@
|
|||
# No Floats
|
|
@ -1 +0,0 @@
|
|||
# Core Only
|
|
@ -1 +0,0 @@
|
|||
# Volatile Destination
|
9
book/src/01-quirks/00-index.md
Normal file
9
book/src/01-quirks/00-index.md
Normal file
|
@ -0,0 +1,9 @@
|
|||
# Quirks
|
||||
|
||||
The GBA supports a lot of totally normal Rust code exactly like you'd think.
|
||||
|
||||
However, it also is missing a lot of what you might expect, and sometimes we
|
||||
have to do things in slightly weird ways.
|
||||
|
||||
We start the book by covering the quirks our code will have, just to avoid too
|
||||
many surprises later.
|
116
book/src/01-quirks/01-no_std.md
Normal file
116
book/src/01-quirks/01-no_std.md
Normal file
|
@ -0,0 +1,116 @@
|
|||
# No Std
|
||||
|
||||
First up, as you already saw in the `hello_magic` code, we have to use the
|
||||
`#![no_std]` outer attribute on our program when we target the GBA. You can find
|
||||
some info about `no_std` in two official sources:
|
||||
|
||||
* [unstable
|
||||
book section](https://doc.rust-lang.org/unstable-book/language-features/lang-items.html#writing-an-executable-without-stdlib)
|
||||
* [embedded
|
||||
book section](https://rust-embedded.github.io/book/intro/no-std.html?highlight=no_std#a--no_std--rust-environment)
|
||||
|
||||
The unstable book is borderline useless here because it's describing too many
|
||||
things in too many words. The embedded book is much better, but still fairly
|
||||
terse.
|
||||
|
||||
## Bare Metal
|
||||
|
||||
The GBA falls under what the Embedded Book calls "Bare Metal Environments".
|
||||
Basically, the machine powers on and immediately begins executing some ASM code.
|
||||
Our ASM startup was provided by `Ketsuban` (check the `crt0.s` file). We'll go
|
||||
over _how_ it works much later on, for now it's enough to know that it does
|
||||
work, and eventually control passes into Rust code.
|
||||
|
||||
On the rust code side of things, we determine our starting point with the
|
||||
`#[start]` attribute on our `main` function. The `main` function also has a
|
||||
specific type signature that's different from the usual `main` that you'd see in
|
||||
Rust. I'd tell you to read the unstable-book entry on `#[start]` but they
|
||||
[literally](https://doc.rust-lang.org/unstable-book/language-features/start.html)
|
||||
just tell you to look at the [tracking issue for
|
||||
it](https://github.com/rust-lang/rust/issues/29633) instead, and that's not very
|
||||
helpful either. Basically it just _has_ to be declared the way it is, even
|
||||
though there's nothing passing in the arguments and there's no place that the
|
||||
return value will go. The compiler won't accept it any other way.
|
||||
|
||||
## No Standard Library
|
||||
|
||||
The Embedded Book tells us that we can't use the standard library, but we get
|
||||
access to something called "libcore", which sounds kinda funny. What they're
|
||||
talking about is just [the core
|
||||
crate](https://doc.rust-lang.org/core/index.html), which is called `libcore`
|
||||
within the rust repository for historical reasons.
|
||||
|
||||
The `core` crate is actually still a really big portion of Rust. The standard
|
||||
library doesn't actually hold too much code (relatively speaking), instead it
|
||||
just takes code form other crates and then re-exports it in an organized way. So
|
||||
with just `core` instead of `std`, what are we missing?
|
||||
|
||||
In no particular order:
|
||||
|
||||
* Allocation
|
||||
* Clock
|
||||
* Network
|
||||
* File System
|
||||
|
||||
The allocation system and all the types that you can use if you have a global
|
||||
allocator are neatly packaged up in the
|
||||
[alloc](https://doc.rust-lang.org/alloc/index.html) crate. The rest isn't as
|
||||
nicely organized.
|
||||
|
||||
It's _possible_ to implement a fair portion of the entire standard library
|
||||
within a GBA context and make the rest just panic if you try to use it. However,
|
||||
do you really need all that? Eh... probably not?
|
||||
|
||||
* We don't need a file system, because all of our data is just sitting there in
|
||||
the ROM for us to use. When programming we can organize our `const` data into
|
||||
modules and such to keep it organized, but once the game is compiled it's just
|
||||
one huge flat address space. TODO: Parasyte says that a FS can be handy even
|
||||
if it's all just ReadOnly, so we'll eventually talk about how you might set up
|
||||
such a thing I guess, since we'll already be talking about replacements for
|
||||
three of the other four things we "lost". Maybe we'll make Parasyte write that
|
||||
section.
|
||||
* Networking, well, the GBA has a Link Cable you can use to communicate with
|
||||
another GBA, but it's not really like a unix socket with TCP, so the standard
|
||||
Rust networking isn't a very good match.
|
||||
* Clock is actually two different things at once. One is the ability to store
|
||||
the time long term, which is a bit of hardware that some gamepaks have in them
|
||||
(eg: pokemon ruby/sapphire/emerald). The GBA itself can't keep time while
|
||||
power is off. However, the second part is just tracking time moment to moment,
|
||||
which the GBA can totally do. We'll see how to access the timers soon enough.
|
||||
|
||||
Which just leaves us with allocation. Do we need an allocator? Depends on your
|
||||
game. For demos and small games you probably don't need one. For bigger games
|
||||
you'll maybe want to get an allocator going eventually. It's in some sense a
|
||||
crutch, but it's a very useful one.
|
||||
|
||||
So I promise that at some point we'll cover how to get an allocator going.
|
||||
Either a Rust Global Allocator (if practical), which would allow for a lot of
|
||||
the standard library types to be used "for free" once it was set up, or just a
|
||||
custom allocator that's GBA specific if Rust's global allocator style isn't a
|
||||
good fit for the GBA (I honestly haven't looked into it).
|
||||
|
||||
## LLVM Intrinsics
|
||||
|
||||
TODO: explain that we'll occasionally have to provide some intrinsics.
|
||||
|
||||
## Bare Metal Panic
|
||||
|
||||
TODO: expand this
|
||||
|
||||
* Write `0xC0DE` to `0x4fff780` (`u16`) to enable mGBA logging. Write any other
|
||||
value to disable it.
|
||||
* Read `0x4fff780` (`u16`) to check mGBA logging status.
|
||||
* You get `0x1DEA` if debugging is active.
|
||||
* Otherwise you get standard open bus nonsense values.
|
||||
* Write your message into the virtual `[u8; 255]` array starting at `0x4fff600`.
|
||||
mGBA will interpret these bytes as a CString value.
|
||||
* Write `0x100` PLUS the message level to `0x4fff700` (`u16`) when you're ready
|
||||
to send a message line:
|
||||
* 0: Fatal (halts execution with a popup)
|
||||
* 1: Error
|
||||
* 2: Warning
|
||||
* 3: Info
|
||||
* 4: Debug
|
||||
* Sending the message also automatically zeroes the output buffer.
|
||||
* View the output within the "Tools" menu, "View Logs...". Note that the Fatal
|
||||
message, if any doesn't get logged.
|
13
book/src/01-quirks/02-fixed_only.md
Normal file
13
book/src/01-quirks/02-fixed_only.md
Normal file
|
@ -0,0 +1,13 @@
|
|||
# Fixed Only
|
||||
|
||||
In addition to not having the standard library available, we don't even have a
|
||||
floating point unit available! We can't do floating point math in hardware! We
|
||||
could still do floating point math as software computations if we wanted, but
|
||||
that's a slow, slow thing to do.
|
||||
|
||||
Instead let's learn about another way to have fractional values called "Fixed
|
||||
Point"
|
||||
|
||||
## Fixed Point
|
||||
|
||||
TODO: describe fixed point, make some types, do the impls, all that.
|
345
book/src/01-quirks/03-volatile_destination.md
Normal file
345
book/src/01-quirks/03-volatile_destination.md
Normal file
|
@ -0,0 +1,345 @@
|
|||
# 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".
|
||||
|
||||
## 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.
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[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>())
|
||||
}
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
```rust
|
||||
/// Performs a cast into some new pointer type.
|
||||
pub fn cast<Z>(self) -> VolatilePtr<Z> {
|
||||
VolatilePtr(self.0 as *mut Z)
|
||||
}
|
||||
```
|
||||
|
||||
### Volatile Iterating
|
||||
|
||||
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.
|
||||
|
||||
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:
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
|
||||
pub struct VolatilePtrIter<T> {
|
||||
vol_ptr: VolatilePtr<T>,
|
||||
slots: usize,
|
||||
}
|
||||
```
|
||||
|
||||
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>> {
|
||||
if self.slots > 0 {
|
||||
let out = Some(self.vol_ptr);
|
||||
self.slots -= 1;
|
||||
self.vol_ptr = unsafe { self.vol_ptr.offset(1) };
|
||||
out
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
* 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`:
|
||||
|
||||
```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>,
|
||||
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
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Volatile ASM
|
||||
|
||||
In addition to some memory locations being volatile, it's also possible for
|
||||
inline assembly to be declared volatile. This is basically the same idea, "hey
|
||||
just do what I'm telling you, don't get smart about it".
|
||||
|
||||
Normally when you have some `asm!` it's basically treated like a function,
|
||||
there's inputs and outputs and the compiler will try to optimize it so that if
|
||||
you don't actually use the outputs it won't bother with doing those
|
||||
instructions. However, `asm!` is basically a pure black box, so the compiler
|
||||
doesn't know what's happening inside at all, and it can't see if there's any
|
||||
important side effects going on.
|
||||
|
||||
An example of an important side effect that doesn't have output values would be
|
||||
putting the CPU into a low power state while we want for the next VBlank. This
|
||||
lets us save quite a bit of battery power. It requires some setup to be done
|
||||
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".
|
|
@ -1,9 +1,7 @@
|
|||
# Newtype
|
||||
|
||||
There's one thing I want to get out of the way near the start of the book and it
|
||||
didn't really have a good fit anywhere else in the book so it goes right here.
|
||||
|
||||
We're talking about the "Newtype Pattern"!
|
||||
There's a great Zero Cost abstraction that we'll be using a lot that you might
|
||||
not already be familiar with: we're talking about the "Newtype Pattern"!
|
||||
|
||||
Now, I told you to read the Rust Book before you read this book, and I'm sure
|
||||
you're all good students who wouldn't sneak into this book without doing the
|
||||
|
@ -111,13 +109,31 @@ macro_rules! newtype {
|
|||
```
|
||||
|
||||
That seems like enough for all of our examples, so we'll stop there. We could
|
||||
add more things, such as making the `From` impl optional (because what if you
|
||||
shouldn't unwrap it for some weird reason?), allowing for more precise
|
||||
visibility controls (on both the newtype overall and the inner field), and maybe
|
||||
even other things I can't think of right now. We won't really need those in our
|
||||
example code for this book, so it's probably nicer to just keep the macro
|
||||
simpler and quit while we're ahead.
|
||||
add more things:
|
||||
|
||||
**As a reminder:** remember that macros have to appear _before_ they're invoked in
|
||||
your source, so the `newtype` macro will always have to be at the very top of
|
||||
your file, or in a module that's declared before other modules and code.
|
||||
* Making the `From` impl being optional. We'd have to make the newtype
|
||||
invocation be more complicated somehow, the user puts ", no-unwrap" after the
|
||||
inner type declaration or something, or something like that.
|
||||
* Allowing for more precise visibility controls on the wrapping type and on the
|
||||
inner field. This would add a lot of line noise, so we'll just always have our
|
||||
newtypes be `pub`.
|
||||
* Allowing for generic newtypes, which might sound silly but that we'll actually
|
||||
see an example of soon enough. To do this you might _think_ that we can change
|
||||
the `:ident` declarations to `:ty`, but since we're declaring a fresh type not
|
||||
using an existing type we have to accept it as an `:ident`. The way you get
|
||||
around this is with a proc-macro, which is a lot more powerful but which also
|
||||
requires that you write the proc-macro in an entirely other crate that gets
|
||||
compiled first. We don't need that much power, so for our examples we'll go
|
||||
with the macro_rules version and just do it by hand in the few cases where we
|
||||
need a generic newtype.
|
||||
* Allowing for `Deref` and `DerefMut`, which usually defeats the point of doing
|
||||
the newtype, but maybe sometimes it's the right thing, so if you were going
|
||||
for the full industrial strength version with a proc-macro and all you might
|
||||
want to make that part of your optional add-ons as well the same way you might
|
||||
want optional `From`. You'd probably want `From` to be "on by default" and
|
||||
`Deref`/`DerefMut` to be "off by default", but whatever.
|
||||
|
||||
**As a reminder:** remember that `macro_rules` macros have to appear _before_
|
||||
they're invoked in your source, so the `newtype` macro will always have to be at
|
||||
the very top of your file, or if you put it in a module within your project
|
||||
you'll need to declare the module before anything that uses it.
|
|
@ -6,12 +6,12 @@
|
|||
* [Book Goals and Style](00-introduction/02-goals_and_style.md)
|
||||
* [Development Setup](00-introduction/03-development-setup.md)
|
||||
* [Hello, Magic](00-introduction/04-hello-magic.md)
|
||||
* [Newtype](00-introduction/05-newtype.md)
|
||||
* [Help and Resources](00-introduction/06-help_and_resources.md)
|
||||
* [Limitations](01-limitations/00-index.md)
|
||||
* [No Floats](01-limitations/01-no_floats.md)
|
||||
* [Core Only](01-limitations/02-core_only.md)
|
||||
* [Volatile Destination](01-limitations/03-volatile_destination.md)
|
||||
* [Help and Resources](00-introduction/05-help_and_resources.md)
|
||||
* [Quirks](01-quirks/00-index.md)
|
||||
* [No Std](01-quirks/01-no_std.md)
|
||||
* [Fixed Only](01-quirks/02-fixed_only.md)
|
||||
* [Volatile Destination](01-quirks/03-volatile_destination.md)
|
||||
* [Newtype](01-quirks/04-newtype.md)
|
||||
* [Concepts](02-concepts/00-index.md)
|
||||
* [CPU](02-concepts/01-cpu.md)
|
||||
* [BIOS](02-concepts/02-bios.md)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#![feature(start)]
|
||||
#![no_std]
|
||||
#![feature(start)]
|
||||
|
||||
#[panic_handler]
|
||||
fn panic(_info: &core::panic::PanicInfo) -> ! {
|
||||
|
|
109
examples/mgba_panic_handler.rs
Normal file
109
examples/mgba_panic_handler.rs
Normal file
|
@ -0,0 +1,109 @@
|
|||
#![no_std]
|
||||
#![feature(start)]
|
||||
|
||||
#[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
|
||||
}
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
||||
}
|
||||
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!");
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue