mirror of
https://github.com/italicsjenga/gba.git
synced 2024-12-24 03:11:29 +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(
|
std::process::Command::new("arm-none-eabi-objcopy").args(
|
||||||
&["-O", "binary",
|
&["-O", "binary",
|
||||||
&format!("target/thumbv4-none-agb/release/examples/{}",name),
|
&format!("target/thumbv4-none-agb/release/examples/{}",name),
|
||||||
&format!("target/example-{}.gba",name)])
|
&format!("target/{}.gba",name)])
|
||||||
.output().expect("failed to objcopy!");
|
.output().expect("failed to objcopy!");
|
||||||
std::process::Command::new("gbafix").args(
|
std::process::Command::new("gbafix").args(
|
||||||
&[&format!("target/example-{}.gba",name)])
|
&[&format!("target/{}.gba",name)])
|
||||||
.output().expect("failed to gbafix!");
|
.output().expect("failed to gbafix!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
18
README.md
18
README.md
|
@ -8,17 +8,19 @@
|
||||||
|
|
||||||
# gba
|
# 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
|
Writing a Rust program for the GBA requires a fair amount of special setup. All
|
||||||
detailed for you [in the 0th chapter of the
|
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) that goes with this
|
book](https://rust-console.github.io/gba/00-introduction/03-development-setup.html).
|
||||||
crate.
|
|
||||||
|
|
||||||
If you've done the global setup once before and just want to get a new project
|
If you've done the described global setup once before and just want to get a new
|
||||||
started quickly we got you covered:
|
project started quickly we got you covered:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
curl https://raw.githubusercontent.com/rust-console/gba/master/init.sh -sSf | bash -s APP_NAME
|
curl https://raw.githubusercontent.com/rust-console/gba/master/init.sh -sSf | bash -s APP_NAME
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# Introduction
|
# 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
|
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.
|
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
|
## Feedback
|
||||||
|
|
||||||
It's also often hard to tell when you've explained something properly to someone
|
It's very often hard to tell when you've explained something properly. In the
|
||||||
who doesn't understand the concept yet. Please, if things don't make sense then
|
same way that your brain will read over small misspellings and correct things
|
||||||
[file an issue](https://github.com/rust-console/gba/issues) about it so I know
|
into the right word, if an explanation for something you already understand
|
||||||
where things need to improve.
|
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`:
|
`hello_magic.rs`:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
#![feature(start)]
|
|
||||||
#![no_std]
|
#![no_std]
|
||||||
|
#![feature(start)]
|
||||||
|
|
||||||
#[panic_handler]
|
#[panic_handler]
|
||||||
fn panic(_info: &core::panic::PanicInfo) -> ! {
|
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
|
# Newtype
|
||||||
|
|
||||||
There's one thing I want to get out of the way near the start of the book and it
|
There's a great Zero Cost abstraction that we'll be using a lot that you might
|
||||||
didn't really have a good fit anywhere else in the book so it goes right here.
|
not already be familiar with: we're talking about the "Newtype Pattern"!
|
||||||
|
|
||||||
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
|
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
|
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
|
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
|
add more things:
|
||||||
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.
|
|
||||||
|
|
||||||
**As a reminder:** remember that macros have to appear _before_ they're invoked in
|
* Making the `From` impl being optional. We'd have to make the newtype
|
||||||
your source, so the `newtype` macro will always have to be at the very top of
|
invocation be more complicated somehow, the user puts ", no-unwrap" after the
|
||||||
your file, or in a module that's declared before other modules and code.
|
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)
|
* [Book Goals and Style](00-introduction/02-goals_and_style.md)
|
||||||
* [Development Setup](00-introduction/03-development-setup.md)
|
* [Development Setup](00-introduction/03-development-setup.md)
|
||||||
* [Hello, Magic](00-introduction/04-hello-magic.md)
|
* [Hello, Magic](00-introduction/04-hello-magic.md)
|
||||||
* [Newtype](00-introduction/05-newtype.md)
|
* [Help and Resources](00-introduction/05-help_and_resources.md)
|
||||||
* [Help and Resources](00-introduction/06-help_and_resources.md)
|
* [Quirks](01-quirks/00-index.md)
|
||||||
* [Limitations](01-limitations/00-index.md)
|
* [No Std](01-quirks/01-no_std.md)
|
||||||
* [No Floats](01-limitations/01-no_floats.md)
|
* [Fixed Only](01-quirks/02-fixed_only.md)
|
||||||
* [Core Only](01-limitations/02-core_only.md)
|
* [Volatile Destination](01-quirks/03-volatile_destination.md)
|
||||||
* [Volatile Destination](01-limitations/03-volatile_destination.md)
|
* [Newtype](01-quirks/04-newtype.md)
|
||||||
* [Concepts](02-concepts/00-index.md)
|
* [Concepts](02-concepts/00-index.md)
|
||||||
* [CPU](02-concepts/01-cpu.md)
|
* [CPU](02-concepts/01-cpu.md)
|
||||||
* [BIOS](02-concepts/02-bios.md)
|
* [BIOS](02-concepts/02-bios.md)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
#![feature(start)]
|
|
||||||
#![no_std]
|
#![no_std]
|
||||||
|
#![feature(start)]
|
||||||
|
|
||||||
#[panic_handler]
|
#[panic_handler]
|
||||||
fn panic(_info: &core::panic::PanicInfo) -> ! {
|
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