mirror of
https://github.com/italicsjenga/gba.git
synced 2024-12-24 03:11:29 +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