mirror of
https://github.com/italicsjenga/gba.git
synced 2024-12-23 19:01:30 +11:00
Clear some cruft
This commit is contained in:
parent
b9d965410e
commit
1fcfbb00e8
|
@ -1,115 +0,0 @@
|
|||
|
||||
|
||||
## A basic hello1 explanation
|
||||
|
||||
So, what just happened? Even if you're used to Rust that might look pretty
|
||||
strange. We'll go over most of the little parts right here, and then bigger
|
||||
parts will get their own sections.
|
||||
|
||||
```rust
|
||||
#![feature(start)]
|
||||
```
|
||||
|
||||
This enables the [start
|
||||
feature](https://doc.rust-lang.org/beta/unstable-book/language-features/start.html),
|
||||
which you would normally be able to read about in the unstable book, except that
|
||||
the book tells you nothing at all except to look at the [tracking
|
||||
issue](https://github.com/rust-lang/rust/issues/29633).
|
||||
|
||||
Basically, a GBA game is even more low-level than the _normal_ amount of
|
||||
low-level that you get from Rust, so we have to tell the compiler to account for
|
||||
that by specifying a `#[start]`, and we need this feature on to do that.
|
||||
|
||||
```rust
|
||||
#![no_std]
|
||||
```
|
||||
|
||||
There's no standard library available on the GBA, so we'll have to live a core
|
||||
only life.
|
||||
|
||||
```rust
|
||||
#[panic_handler]
|
||||
fn panic(_info: &core::panic::PanicInfo) -> ! {
|
||||
loop {}
|
||||
}
|
||||
```
|
||||
|
||||
This sets our [panic
|
||||
handler](https://doc.rust-lang.org/nightly/nomicon/panic-handler.html).
|
||||
Basically, if we somehow trigger a panic, this is where the program goes.
|
||||
However, right now we don't know how to get any sort of message out to the user
|
||||
so... we do nothing at all. We _can't even return_ from here, so we just sit in
|
||||
an infinite loop. The player will have to reset the universe from the outside.
|
||||
|
||||
```rust
|
||||
#[start]
|
||||
fn main(_argc: isize, _argv: *const *const u8) -> isize {
|
||||
```
|
||||
|
||||
This is our `#[start]`. We call it `main`, but it's not like a `main` that you'd
|
||||
see in a Rust program. It's _more like_ the sort of `main` that you'd see in a C
|
||||
program, but it's still **not** that either. If you compile a `#[start]` program
|
||||
for a target with an OS such as `arm-none-eabi-nm` you can open up the debug
|
||||
info and see that your result will have the symbol for the C `main` along side
|
||||
the symbol for the start `main` that we write here. Our start `main` is just its
|
||||
own unique thing, and the inputs and outputs have to be like that because that's
|
||||
how `#[start]` is specified to work in Rust.
|
||||
|
||||
If you think about it for a moment you'll probably realize that, those inputs
|
||||
and outputs are totally useless to us on a GBA. There's no OS on the GBA to call
|
||||
our program, and there's no place for our program to "return to" when it's done.
|
||||
|
||||
Side note: if you want to learn more about stuff "before main gets called" you
|
||||
can watch a great [CppCon talk](https://www.youtube.com/watch?v=dOfucXtyEsU) by
|
||||
Matt Godbolt (yes, that Godbolt) where he delves into quite a bit of it. The
|
||||
talk doesn't really apply to the GBA, but it's pretty good.
|
||||
|
||||
```rust
|
||||
unsafe {
|
||||
```
|
||||
|
||||
I hope you're all set for some `unsafe`, because there's a lot of it to be had.
|
||||
|
||||
```rust
|
||||
(0x04000000 as *mut u16).write_volatile(0x0403);
|
||||
```
|
||||
|
||||
Sure!
|
||||
|
||||
```rust
|
||||
(0x06000000 as *mut u16).offset(120 + 80 * 240).write_volatile(0x001F);
|
||||
(0x06000000 as *mut u16).offset(136 + 80 * 240).write_volatile(0x03E0);
|
||||
(0x06000000 as *mut u16).offset(120 + 96 * 240).write_volatile(0x7C00);
|
||||
```
|
||||
|
||||
Ah, of course.
|
||||
|
||||
```rust
|
||||
loop {}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
And, as mentioned above, there's no place for a GBA program to "return to", so
|
||||
we can't ever let `main` try to return there. Instead, we go into an infinite
|
||||
`loop` that does nothing. The fact that this doesn't ever return an `isize`
|
||||
value doesn't seem to bother Rust, because I guess we're at least not returning
|
||||
any other type of thing instead.
|
||||
|
||||
Fun fact: unlike in C++, an infinite loop with no side effects isn't Undefined
|
||||
Behavior for us rustaceans... _semantically_. In truth LLVM has a [known
|
||||
bug](https://github.com/rust-lang/rust/issues/28728) in this area, so we won't
|
||||
actually be relying on empty loops in any future programs.
|
||||
|
||||
## All Those Magic Numbers
|
||||
|
||||
Alright, I cheated quite a bit in the middle there. The program works, but I
|
||||
didn't really tell you why because I didn't really tell you what any of those
|
||||
magic numbers mean or do.
|
||||
|
||||
* `0x04000000` is the address of an IO Register called the Display Control.
|
||||
* `0x06000000` is the start of Video RAM.
|
||||
|
||||
So we write some magic to the display control register once, then we write some
|
||||
other magic to three magic locations in the Video RAM. Somehow that shows three
|
||||
dots. Gotta read on to find out why!
|
|
@ -1,132 +0,0 @@
|
|||
# hello2
|
||||
|
||||
Okay so let's have a look again:
|
||||
|
||||
`hello1`
|
||||
|
||||
```rust
|
||||
#![feature(start)]
|
||||
#![no_std]
|
||||
|
||||
#[panic_handler]
|
||||
fn panic(_info: &core::panic::PanicInfo) -> ! {
|
||||
loop {}
|
||||
}
|
||||
|
||||
#[start]
|
||||
fn main(_argc: isize, _argv: *const *const u8) -> isize {
|
||||
unsafe {
|
||||
(0x04000000 as *mut u16).write_volatile(0x0403);
|
||||
(0x06000000 as *mut u16).offset(120 + 80 * 240).write_volatile(0x001F);
|
||||
(0x06000000 as *mut u16).offset(136 + 80 * 240).write_volatile(0x03E0);
|
||||
(0x06000000 as *mut u16).offset(120 + 96 * 240).write_volatile(0x7C00);
|
||||
loop {}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Now let's clean this up so that it's clearer what's going on.
|
||||
|
||||
First we'll label that display control stuff, including using the `VolatilePtr`
|
||||
type from the volatile explanation:
|
||||
|
||||
```rust
|
||||
pub const DISPCNT: VolatilePtr<u16> = VolatilePtr(0x04000000 as *mut u16);
|
||||
pub const MODE3: u16 = 3;
|
||||
pub const BG2: u16 = 0b100_0000_0000;
|
||||
```
|
||||
|
||||
Next we make some const values for the actual pixel drawing
|
||||
|
||||
```rust
|
||||
pub const VRAM: usize = 0x06000000;
|
||||
pub const SCREEN_WIDTH: isize = 240;
|
||||
```
|
||||
|
||||
Note that VRAM has to be interpreted in different ways depending on mode, so we
|
||||
just leave it as `usize` and we'll cast it into the right form closer to the
|
||||
actual use.
|
||||
|
||||
Next we want a small helper function for putting together a color value.
|
||||
Happily, this one can even be declared as a `const` function. At the time of
|
||||
writing, we've got the "minimal const fn" support in nightly. It really is quite
|
||||
limited, but I'm happy to let rustc and LLVM pre-compute as much as they can
|
||||
when it comes to the GBA's tiny CPU.
|
||||
|
||||
```rust
|
||||
pub const fn rgb16(red: u16, green: u16, blue: u16) -> u16 {
|
||||
blue << 10 | green << 5 | red
|
||||
}
|
||||
```
|
||||
|
||||
Finally, we'll make a function for drawing a pixel in Mode 3. Even though it's
|
||||
just a one-liner, having the "important parts" be labeled as function arguments
|
||||
usually helps you think about it a lot better.
|
||||
|
||||
```rust
|
||||
pub unsafe fn mode3_pixel(col: isize, row: isize, color: u16) {
|
||||
VolatilePtr(VRAM as *mut u16).offset(col + row * SCREEN_WIDTH).write(color);
|
||||
}
|
||||
```
|
||||
|
||||
So now we've got this:
|
||||
|
||||
`hello2`
|
||||
|
||||
```rust
|
||||
#![feature(start)]
|
||||
#![no_std]
|
||||
|
||||
#[panic_handler]
|
||||
fn panic(_info: &core::panic::PanicInfo) -> ! {
|
||||
loop {}
|
||||
}
|
||||
|
||||
#[start]
|
||||
fn main(_argc: isize, _argv: *const *const u8) -> isize {
|
||||
unsafe {
|
||||
DISPCNT.write(MODE3 | BG2);
|
||||
mode3_pixel(120, 80, rgb16(31, 0, 0));
|
||||
mode3_pixel(136, 80, rgb16(0, 31, 0));
|
||||
mode3_pixel(120, 96, rgb16(0, 0, 31));
|
||||
loop {}
|
||||
}
|
||||
}
|
||||
|
||||
#[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 fn rgb16(red: u16, green: u16, blue: u16) -> u16 {
|
||||
blue << 10 | green << 5 | red
|
||||
}
|
||||
|
||||
pub unsafe fn mode3_pixel(col: isize, row: isize, color: u16) {
|
||||
VolatilePtr(VRAM as *mut u16).offset(col + row * SCREEN_WIDTH).write(color);
|
||||
}
|
||||
```
|
||||
|
||||
Exact same program that we started with, but much easier to read.
|
||||
|
||||
Of course, in the full `gba` crate that this book is a part of we have these and
|
||||
other elements all labeled and sorted out for you (not identically, but
|
||||
similarly). Still, for educational purposes it's often best to do it yourself at
|
||||
least once.
|
|
@ -1,10 +0,0 @@
|
|||
# Ch 1: Hello GBA
|
||||
|
||||
Traditionally a person writes a "hello, world" program so that they can test
|
||||
that their development environment is setup properly and to just get a feel for
|
||||
using the tools involved. To get an idea of what a small part of a source file
|
||||
will look like. All that stuff.
|
||||
|
||||
Normally, you write a program that prints "hello, world" to the terminal. The
|
||||
GBA has no terminal, but it does have a screen, so instead we're going to draw
|
||||
three dots to the screen.
|
|
@ -1,70 +0,0 @@
|
|||
# Volatile
|
||||
|
||||
Before we focus on what the numbers mean, first let's ask ourselves: Why are we
|
||||
doing _volatile_ writes? You've probably never used that keywords before at all.
|
||||
What _is_ volatile anyway?
|
||||
|
||||
Well, the optimizer is pretty aggressive, and so it'll skip reads and writes
|
||||
when it thinks can. Like if you write to a pointer once, and then again a moment
|
||||
later, and it didn't see any other reads in between, it'll think that it can
|
||||
just skip doing that first write since it'll get overwritten anyway. Sometimes
|
||||
that's correct, but sometimes it's not.
|
||||
|
||||
Marking a read or write as _volatile_ tells the compiler that it really must do
|
||||
that action, and in the exact order that we wrote it out. It says that there
|
||||
might even be special hardware side effects going on that the compiler isn't
|
||||
aware of. In this case, the write to the display control register sets a video
|
||||
mode, and the writes to the Video RAM set pixels that will show up on the
|
||||
screen.
|
||||
|
||||
Similar to "atomic" operations you might have heard about, all volatile
|
||||
operations are enforced to happen in the exact order that you specify them, but
|
||||
only relative to other volatile operations. So something like
|
||||
|
||||
```rust
|
||||
c.write_volatile(5);
|
||||
a += b;
|
||||
d.write_volatile(7);
|
||||
```
|
||||
|
||||
might end up changing `a` either before or after the change to `c` (since the
|
||||
value of `a` doesn't affect the write to `c`), but the write to `d` will
|
||||
_always_ happen after the write to `c`, even though the compiler doesn't see any
|
||||
direct data dependency there.
|
||||
|
||||
If you ever go on to use volatile stuff on other platforms it's important to
|
||||
note that volatile doesn't make things thread-safe, you still need atomic for
|
||||
that. However, the GBA doesn't have threads, so we don't have to worry about
|
||||
those sorts of thread safety concerns (there's interrupts, but that's another
|
||||
matter).
|
||||
|
||||
## Volatile by default
|
||||
|
||||
Of course, writing out `volatile_write` every time is more than we wanna do.
|
||||
There's clarity and then there's excessive. This is a chance to write our first
|
||||
[newtype](https://doc.rust-lang.org/1.0.0/style/features/types/newtype.html).
|
||||
Basically a type that's got the exact same binary representation as some other
|
||||
type, but new methods and trait implementations.
|
||||
|
||||
We want a `*mut T` that's volatile by default, and also when we offset it...
|
||||
well the verdict is slightly unclear on how `offset` vs `wrapping_offset` work
|
||||
when you're using pointers that you made up out of nowhere. I've asked the
|
||||
experts and they genuinely weren't sure, so we'll make an `offset` method that
|
||||
does a `wrapping_offset` just to be careful.
|
||||
|
||||
```rust
|
||||
#[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))
|
||||
}
|
||||
}
|
||||
```
|
|
@ -1,22 +0,0 @@
|
|||
# Ch 2: User Input
|
||||
|
||||
It's all well and good to draw three pixels, but they don't do anything yet. We
|
||||
want them to do something, and for that we need to get some input from the user.
|
||||
|
||||
The GBA, as I'm sure you know, has an arrow pad, A and B, L and R, Start and
|
||||
Select. That's a little more than the NES/GB/CGB had, and a little less than the
|
||||
SNES had. As you can guess, we get key state info from an IO register.
|
||||
|
||||
Also, we will need a way to keep the program from running "too fast". On a
|
||||
modern computer or console you do this with vsync info from the GPU and Monitor,
|
||||
and on the GBA we'll be using vsync info from an IO register that tracks what
|
||||
the display hardware is doing.
|
||||
|
||||
As a way to apply our knowledge We'll make a simple "light cycle" game where
|
||||
your dot leaves a trail behind them and you die if you go off the screen or if
|
||||
you touch your own trail. We just make a copy of `hello2.rs` named
|
||||
`light_cycle.rs` and then fill it in as we go through the chapter. Normally you
|
||||
might not place the entire program into a single source file, particularly as it
|
||||
grows over time, but since these are small examples it's much better to have
|
||||
them be completely self contained than it is to have them be "properly
|
||||
organized" for the long term.
|
Loading…
Reference in a new issue