Merge pull request #65 from rust-console/lokathor

IO Registers + Bitmap book section
This commit is contained in:
Lokathor 2019-02-14 21:23:28 -07:00 committed by GitHub
commit 98a9eefaf5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 1120 additions and 438 deletions

View file

@ -1,7 +1,7 @@
[package]
name = "gba"
description = "A crate (and book) for making GBA games with Rust."
version = "0.4.0"
version = "0.4.0-pre"
authors = ["Lokathor <zefria@gmail.com>", "Thomas Winwood <twwinwood@gmail.com>"]
repository = "https://github.com/rust-console/gba"
readme = "README.md"

View file

@ -1 +1,214 @@
# Bitmap Video
Our first video modes to talk about are the bitmap video modes.
It's not because they're the best and fastest, it's because they're the
_simplest_. You can get going and practice with them really quickly. Usually
after that you end up wanting to move on to the other video modes because they
have better hardware support, so you can draw more complex things with the small
number of cycles that the GBA allows.
## The Three Bitmap Modes
As I said in the Hardware Memory Map section, the Video RAM lives in the address
space at `0x600_0000`. Depending on our video mode the display controller will
consider this memory to be in one of a few totally different formats.
### Mode 3
The screen is 160 rows, each 240 pixels long, of `u16` color values.
This is "full" resolution, and "full" color. It adds up to 76,800 bytes. VRAM is
only 96,304 bytes total though. There's enough space left over after the bitmap
for some object tile data if you want to use objects, but basically Mode3 is
using all of VRAM as one huge canvas.
### Mode 4
The screen is 160 rows, each 240 pixels long, of `u8` palette values.
This has half as much space per pixel. What's a palette value? That's an index
into the background PALRAM which says what the color of that pixel should be. We
still have the full color space available, but we can only use 256 colors at the
same time.
What did we get in exchange for this? Well, now there's a second "page". The
second page starts `0xA000` bytes into VRAM (in both Mode 4 and Mode 5). It's an
entire second set of pixel data. You determine if Page 0 or Page 1 is shown
using bit 4 of DISPCNT. When you swap which page is being displayed it's called
page flipping or flipping the page, or something like that.
Having two pages is cool, but Mode 4 has a big drawback: it's part of VRAM so
that "can't write 1 byte at a time" rule applies. This means that to set a
single byte we need to read a `u16`, adjust just one side of it, and then write
that `u16` back. We can hide the complication behind a method call, but it
simply takes longer to do all that, so editing pixels ends up being
unfortunately slow compared to the other bitmap modes.
### Mode 5
The screen is 128 rows, each 160 pixels long, of `u16` color values.
Mode 5 has two pages like Mode 4 does, but instead of keeping full resolution we
keep full color. The pixels are displayed in the top left and it's just black on
the right and bottom edges. You can use the background control registers to
shift it around, maybe center it, but there's no way to get around the fact that
not having full resolution is kinda awkward.
## Using Mode 3
Let's have a look at how this comes together. We'll call this one
`hello_world.rs`, since it's our first real program.
### Module Attributes and Imports
At the top of our file we're still `no_std` and we're still using
`feature(start)`, but now we're using the `gba` crate so we're 100% safe code!
Often enough we'll need a little `unsafe`, but for just bitmap drawing we don't
need it.
```rust
#![no_std]
#![feature(start)]
#![forbid(unsafe_code)]
use gba::{
fatal,
io::{
display::{DisplayControlSetting, DisplayMode, DISPCNT, VBLANK_SCANLINE, VCOUNT},
keypad::read_key_input,
},
vram::bitmap::Mode3,
Color,
};
```
### Panic Handler
Before we had a panic handler that just looped forever. Now that we're using the
`gba` crate we can rely on the debug output channel from `mGBA` to get a message
into the real world. There's macros setup for each message severity, and they
all accept a format string and arguments, like how `println` works. The catch is
that a given message is capped at a length of 255 bytes, and it should probably
be ASCII only.
In the case of the `fatal` message level, it also halts the emulator.
Of course, if the program is run on real hardware then the `fatal` message won't
stop the program, so we still need the infinite loop there too.
(not that this program _can_ panic, but `rustc` doesn't know that so it demands
we have a `panic_handler`)
```rust
#[panic_handler]
fn panic(info: &core::panic::PanicInfo) -> ! {
// This kills the emulation with a message if we're running within mGBA.
fatal!("{}", info);
// If we're _not_ running within mGBA then we still need to not return, so
// loop forever doing nothing.
loop {}
}
```
### Waiting Around
Like I talked about before, sometimes we need to wait around a bit for the right
moment to start doing work. However, we don't know how to do the good version of
waiting for VBlank and VDraw to start, so we'll use the really bad version of it
for now.
```rust
/// Performs a busy loop until VBlank starts.
///
/// This is very inefficient, and please keep following the lessons until we
/// cover how interrupts work!
pub fn spin_until_vblank() {
while VCOUNT.read() < VBLANK_SCANLINE {}
}
/// Performs a busy loop until VDraw starts.
///
/// This is very inefficient, and please keep following the lessons until we
/// cover how interrupts work!
pub fn spin_until_vdraw() {
while VCOUNT.read() >= VBLANK_SCANLINE {}
}
```
### Setup in `main`
In main we set the display control value we want and declare a few variables
we're going to use in our primary loop.
```rust
#[start]
fn main(_argc: isize, _argv: *const *const u8) -> isize {
const SETTING: DisplayControlSetting =
DisplayControlSetting::new().with_mode(DisplayMode::Mode3).with_bg2(true);
DISPCNT.write(SETTING);
let mut px = Mode3::WIDTH / 2;
let mut py = Mode3::HEIGHT / 2;
let mut color = Color::from_rgb(31, 0, 0);
```
### Stuff During VDraw
When a frame starts we want to read the keys, then adjust as much of the game
state as we can without touching VRAM.
Once we're ready, we do our spin loop until VBlank starts.
In this case, we're going to adjust `px` and `py` depending on the arrow pad
input, and also we'll cycle around the color depending on L and R being pressed.
```rust
loop {
// read our keys for this frame
let this_frame_keys = read_key_input();
// adjust game state and wait for vblank
px = px.wrapping_add(2 * this_frame_keys.x_tribool() as usize);
py = py.wrapping_add(2 * this_frame_keys.y_tribool() as usize);
if this_frame_keys.l() {
color = Color(color.0.rotate_left(5));
}
if this_frame_keys.r() {
color = Color(color.0.rotate_right(5));
}
// now we wait
spin_until_vblank();
```
### Stuff During VBlank
When VBlank starts we want want to update video memory to display the new
frame's situation.
In our case, we're going to paint a little square of the current color, but also
if you go off the map it resets the screen.
At the end, we spin until VDraw starts so we can do the whole thing again.
```rust
// draw the new game and wait until the next frame starts.
if px >= Mode3::WIDTH || py >= Mode3::HEIGHT {
// out of bounds, reset the screen and position.
Mode3::dma_clear_to(Color::from_rgb(0, 0, 0));
px = Mode3::WIDTH / 2;
py = Mode3::HEIGHT / 2;
} else {
// draw the new part of the line
Mode3::write(px, py, color);
Mode3::write(px, py + 1, color);
Mode3::write(px + 1, py, color);
Mode3::write(px + 1, py + 1, color);
}
// now we wait again
spin_until_vdraw();
}
}
```

View file

@ -1 +1,237 @@
# IO Registers
As I said before, the IO registers are how you tell the GBA to do all the things
you want it to do. If you want a hint at what's available, they're all listed
out in the [GBA I/O Map](https://problemkaputt.de/gbatek.htm#gbaiomap) section
of GBATEK. Go have a quick look.
Each individual IO register has a particular address just like we talked about
in the Hardware Memory Map section. They also have a size (listed in bytes), and
a note on if they're read only, write only, or read-write. Finally, each
register has a name and a one line summary. Unfortunately for us, the names are
all C style names with heavy shorthand. I'm not normally a fan of shorthand
names, but the `gba` crate uses the register names from GBATEK as much as
possible, since they're the most commonly used set of names among GBA
programmers. That way, if you're reading other guides and they say to set the
`BG2CNT` register, then you know exactly what register to look for within the
`gba` docs.
## Register Bits
There's only about 100 registers, but there's a lot more than 100 details we
want to have control over on the GBA. How does that work? Well, let's use a
particular register to talk about it. The first one on the list is `DISPCNT`,
the "Display Control" register. It's one of the most important IO registers, so
this is a "two birds with one stone" situation.
Naturally there's a whole lot of things involved in the LCD that we want to
control, and it's all "one" value, but that value is actually many "fields"
packed into one value. When learning about an IO register, you have to look at
its bit pattern breakdown. For `DISPCNT` the GBATEK entry looks like this:
```txt
4000000h - DISPCNT - LCD Control (Read/Write)
Bit Expl.
0-2 BG Mode (0-5=Video Mode 0-5, 6-7=Prohibited)
3 Reserved / CGB Mode (0=GBA, 1=CGB; can be set only by BIOS opcodes)
4 Display Frame Select (0-1=Frame 0-1) (for BG Modes 4,5 only)
5 H-Blank Interval Free (1=Allow access to OAM during H-Blank)
6 OBJ Character VRAM Mapping (0=Two dimensional, 1=One dimensional)
7 Forced Blank (1=Allow FAST access to VRAM,Palette,OAM)
8 Screen Display BG0 (0=Off, 1=On)
9 Screen Display BG1 (0=Off, 1=On)
10 Screen Display BG2 (0=Off, 1=On)
11 Screen Display BG3 (0=Off, 1=On)
12 Screen Display OBJ (0=Off, 1=On)
13 Window 0 Display Flag (0=Off, 1=On)
14 Window 1 Display Flag (0=Off, 1=On)
15 OBJ Window Display Flag (0=Off, 1=On)
```
So what we're supposed to understand here is that we've got a `u16`, and then we
set the individual bits for the things that we want. In the `hello_magic`
example you might recall that we set this register to the value `0x0403`. That
was a bit of a trick on my part because hex numbers usually look far more
mysterious than decimal or binary numbers. If we converted it to binary it'd
look like this:
```rust
0b100_0000_0011
```
And then you can just go down the list of settings to see what bits are what:
* Bits 0-2 (BG Mode) are `0b011`, so that's Video Mode 3
* Bit 10 (Display BG2) is enabled
* Everything else is disabled
Naturally, trying to remember exactly what bit does what can be difficult. In
the `gba` crate we attempt as much as possible to make types that wrap over a
`u16` or `u32` and then have getters and setters _as if_ all the inner bits were
different fields.
* If it's a single bit then the getter/setter will use `bool`.
* If it's more than one bit and each pattern has some non-numeric meaning then
it'll use an `enum`.
* If it's more than one bit and numeric in nature then it'll just use the
wrapped integer type. Note that you generally won't get the full range of the
inner number type, and any excess gets truncated down to fit in the bits
available.
All the getters and setters are defined as `const` functions, so you can make
constant declarations for the exact setting combinations that you want.
## Some Important IO Registers
It's not easy to automatically see what registers will be important for getting
started and what registers can be saved to learn about later.
We'll go over three IO registers here that will help us the most to get started,
then next lesson we'll cover how that Video Mode 3 bitmap drawing works, and
then by the end of the next lesson we'll be able to put it all together into
something interactive.
### DISPCNT: Display Control
The [DISPCNT](https://problemkaputt.de/gbatek.htm#lcdiodisplaycontrol) register
lets us affect the major details of our video output. There's a lot of other
registers involved too, but it all starts here.
```rust
pub const DISPCNT: VolAddress<DisplayControlSetting> = unsafe { VolAddress::new(0x400_0000) };
```
As you can see, the display control register is, like most registers,
complicated enough that we make it a dedicated type with getters and setters for
the "phantom" fields. In this case it's mostly a bunch of `bool` values we can
set, and also the video mode is an `enum`.
We already looked at the bit listing above, let's go over what's important right
now and skip the other bits:
* BG Mode sets how the whole screen is going to work and even how the display
adapter is going to interpret the bit layout of video memory for pixel
processing. We'll start with Mode 3, which is the simplest to learn.
* The "Forced Blank" bit is one of the very few bits that starts _on_ at the
start of the main program. When it's enabled it prevents the display adapter
from displaying anything at all. You use this bit when you need to do a very
long change to video memory and you don't want the user to see the
intermediate states being partly drawn.
* The "Screen Display" bits let us enable different display layers. We care
about BG2 right now because the bitmap modes (3, 4, and 5) are all treated as
if they were drawing into BG2 (even though it's the only BG layer available in
those modes).
There's a bunch of other stuff, but we'll get to those things later. They're not
relevent right now, and there's enough to learn already. Already we can see that
when the `hello_magic` demo says
```rust
(0x400_0000 as *mut u16).write_volatile(0x0403);
```
We could re-write that more sensibly like this
```rust
const SETTING: DisplayControlSetting =
DisplayControlSetting::new().with_mode(DisplayMode::Mode3).with_bg2(true);
DISPCNT.write(SETTING);
```
### VCOUNT: Vertical Display Counter
The [VCOUNT](https://problemkaputt.de/gbatek.htm#lcdiointerruptsandstatus)
register lets us find out what row of pixels (called a **scanline**) is
currently being processed.
```rust
pub const VCOUNT: ROVolAddress<u16> = unsafe { ROVolAddress::new(0x400_0006) };
```
You see, the display adapter is constantly running its own loop, along side the
CPU. It starts at the very first pixel of the very first scanline, takes 4
cycles to determine what color that pixel is, and then processes the next
pixel. Each scanline is 240 pixels long, followed by 68 "virtual" pixels so that
you have just a moment to setup for the next scanline to be drawn if you need
it. 272 cycles (68*4) is not a lot of time, but it's enough that you could
change some palette colors or move some objects around if you need to.
* Horizontal pixel value `0..240`: "HDraw"
* Horizontal pixel value `240..308`: "HBlank"
There's no way to check the current horizontal counter, but there is a way to
have the CPU interrupt the normal code when the HBlank period starts, which
we'll learn about later.
Once a complete scanline has been processed (including the blank period), the
display adapter keeps going with the next scanline. Similar to how the
horizontal processing works, there's 160 scanlines in the real display, and then
it's followed by 68 "virtual" scanlines to give you time for adjusting video
memory between the frames of the game.
* Vertical Count `0..160`: "VDraw"
* Vertical Count `160..228`: "VBlank"
Once every scanline has been processed (including the vblank period), the
display adapter starts the whole loop over again with scanline 0. A total of
280,896 cycles per display loop (4 * 308 * 228), and about 59.59ns per CPU
cycle, gives us a full speed display rate of 59.73fps. That's close enough to
60fps that I think we can just round up a bit whenever we're not counting it
down to the exact cycle timings.
However, there's a bit of a snag. If we change video memory during the middle of
a scanline the display will _immediately_ start processing using the new state
of video memory. The picture before the change and after the change won't look
like a single, clean picture. Instead you'll get what's called "[screen
tearing](https://en.wikipedia.org/wiki/Screen_tearing)", which is usually
considered to be the mark of a badly programmed game.
To avoid this we just need to only adjust video memory during one of the blank
periods. If you're really cool you can adjust things during HBlank, but we're
not that cool yet. Starting out our general program flow will be:
1) Gather input for the frame (next part of this lesson) and update the game
state, getting everything ready for when VBlank actually starts.
2) Once VBlank starts we update all of the video memory as fast as we can.
3) Once we're done drawing we again wait for the VDraw period to begin and then
do it all again.
Now, it's not the most efficient way, but to get our timings right we can just
read from `VCOUNT` over and over in a "busy loop". Once we read a value of 160
we know that we've entered VBlank. Once it goes back to 0 we know that we're
back in VDraw.
Doing a busy loop like this actually drains the batteries way more than
necessary. It keeps the CPU active constantly, which is what uses a fair amount
of the power. Normally you're supposed to put the CPU to sleep if you're just
waiting around for something to happen. However, that also requires learning
about some more concepts to get right. So to keep things easier starting out
we'll do the bad/lazy version and then upgrade our technique later.
### KEYINPUT: Key Input Reading
The [KEYINPUT](https://problemkaputt.de/gbatek.htm#gbakeypadinput) register is
the last one we've got to learn about this lesson. It lets you check the status
of all 10 buttons on the GBA.
```rust
pub const KEYINPUT: ROVolAddress<u16> = unsafe { ROVolAddress::new(0x400_0130) };
```
There's little to say here. It's a read only register, and the data just
contains one bit per button. The only thing that's a little weird about it is
that the bits follow a "low active" convention, so if the button is pressed then
the bit is 0, and if the button is released the bit is 1.
You _could_ work with that directly, but I think it's a lot easier to think
about having `true` for pressed and `false` for not pressed. So the `gba` crate
flips the bits when you read the keys:
```rust
/// Gets the current state of the keys
pub fn read_key_input() -> KeyInput {
KeyInput(KEYINPUT.read() ^ 0b0000_0011_1111_1111)
}
```
Now we can treat the KeyInput values like a totally normal bitset.

View file

@ -3,22 +3,82 @@
#![forbid(unsafe_code)]
use gba::{
io::display::{DisplayControlSetting, DisplayMode, DISPCNT},
fatal,
io::{
display::{DisplayControlSetting, DisplayMode, DISPCNT, VBLANK_SCANLINE, VCOUNT},
keypad::read_key_input,
},
vram::bitmap::Mode3,
Color,
};
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
fn panic(info: &core::panic::PanicInfo) -> ! {
// This kills the emulation with a message if we're running within mGBA.
fatal!("{}", info);
// If we're _not_ running within mGBA then we still need to not return, so
// loop forever doing nothing.
loop {}
}
/// Performs a busy loop until VBlank starts.
///
/// This is very inefficient, and please keep following the lessons until we
/// cover how interrupts work!
pub fn spin_until_vblank() {
while VCOUNT.read() < VBLANK_SCANLINE {}
}
/// Performs a busy loop until VDraw starts.
///
/// This is very inefficient, and please keep following the lessons until we
/// cover how interrupts work!
pub fn spin_until_vdraw() {
while VCOUNT.read() >= VBLANK_SCANLINE {}
}
#[start]
fn main(_argc: isize, _argv: *const *const u8) -> isize {
const SETTING: DisplayControlSetting = DisplayControlSetting::new().with_mode(DisplayMode::Mode3).with_bg2(true);
const SETTING: DisplayControlSetting =
DisplayControlSetting::new().with_mode(DisplayMode::Mode3).with_bg2(true);
DISPCNT.write(SETTING);
Mode3::write_pixel(120, 80, Color::from_rgb(31, 0, 0));
Mode3::write_pixel(136, 80, Color::from_rgb(0, 31, 0));
Mode3::write_pixel(120, 96, Color::from_rgb(0, 0, 31));
loop {}
let mut px = Mode3::WIDTH / 2;
let mut py = Mode3::HEIGHT / 2;
let mut color = Color::from_rgb(31, 0, 0);
loop {
// read our keys for this frame
let this_frame_keys = read_key_input();
// adjust game state and wait for vblank
px = px.wrapping_add(2 * this_frame_keys.x_tribool() as usize);
py = py.wrapping_add(2 * this_frame_keys.y_tribool() as usize);
if this_frame_keys.l() {
color = Color(color.0.rotate_left(5));
}
if this_frame_keys.r() {
color = Color(color.0.rotate_right(5));
}
// now we wait
spin_until_vblank();
// draw the new game and wait until the next frame starts.
if px >= Mode3::WIDTH || py >= Mode3::HEIGHT {
// out of bounds, reset the screen and position.
Mode3::dma_clear_to(Color::from_rgb(0, 0, 0));
px = Mode3::WIDTH / 2;
py = Mode3::HEIGHT / 2;
} else {
// draw the new part of the line
Mode3::write(px, py, color);
Mode3::write(px, py + 1, color);
Mode3::write(px + 1, py, color);
Mode3::write(px + 1, py + 1, color);
}
// now we wait again
spin_until_vdraw();
}
}

View file

@ -26,7 +26,8 @@ fn panic(_info: &core::panic::PanicInfo) -> ! {
fn start_timers() {
let init_val: u16 = u32::wrapping_sub(0x1_0000, 64) as u16;
const TIMER_SETTINGS: TimerControlSetting = TimerControlSetting::new().with_overflow_irq(true).with_enabled(true);
const TIMER_SETTINGS: TimerControlSetting =
TimerControlSetting::new().with_overflow_irq(true).with_enabled(true);
TM0CNT_L.write(init_val);
TM0CNT_H.write(TIMER_SETTINGS.with_tick_rate(TimerTickRate::CPU1024));
@ -90,8 +91,8 @@ static mut PIXEL: usize = 0;
fn write_pixel(color: Color) {
unsafe {
Mode3::write_pixel(PIXEL, 0, color);
PIXEL = (PIXEL + 1) % Mode3::SCREEN_PIXEL_COUNT;
Mode3::write(PIXEL, 0, color);
PIXEL = (PIXEL + 1) % (Mode3::WIDTH * Mode3::HEIGHT);
}
}

View file

@ -1,61 +0,0 @@
#![no_std]
#![feature(start)]
#![forbid(unsafe_code)]
use gba::{
io::{
display::{spin_until_vblank, spin_until_vdraw, DisplayControlSetting, DisplayMode, DISPCNT},
keypad::read_key_input,
},
vram::bitmap::Mode3,
Color,
};
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
loop {}
}
#[start]
fn main(_argc: isize, _argv: *const *const u8) -> isize {
const SETTING: DisplayControlSetting = DisplayControlSetting::new().with_mode(DisplayMode::Mode3).with_bg2(true);
DISPCNT.write(SETTING);
let mut px = Mode3::SCREEN_WIDTH / 2;
let mut py = Mode3::SCREEN_HEIGHT / 2;
let mut color = Color::from_rgb(31, 0, 0);
loop {
// read the input for this frame
let this_frame_keys = read_key_input();
// adjust game state and wait for vblank
px = px.wrapping_add(2 * this_frame_keys.column_direction() as usize);
py = py.wrapping_add(2 * this_frame_keys.row_direction() as usize);
spin_until_vblank();
// draw the new game and wait until the next frame starts.
const BLACK: Color = Color::from_rgb(0, 0, 0);
if px >= Mode3::SCREEN_WIDTH || py >= Mode3::SCREEN_HEIGHT {
// out of bounds, reset the screen and position.
Mode3::clear_to(BLACK);
color = color.rotate_left(5);
px = Mode3::SCREEN_WIDTH / 2;
py = Mode3::SCREEN_HEIGHT / 2;
} else {
let color_here = Mode3::read_pixel(px, py);
if color_here != Some(BLACK) {
// crashed into our own line, reset the screen
Mode3::dma_clear_to(BLACK);
color = color.rotate_left(5);
} else {
// draw the new part of the line
Mode3::write_pixel(px, py, color);
Mode3::write_pixel(px, py + 1, color);
Mode3::write_pixel(px + 1, py, color);
Mode3::write_pixel(px + 1, py + 1, color);
}
}
spin_until_vdraw();
}
}

View file

@ -1,8 +1,9 @@
color = "Never"
error_on_line_overflow = false
fn_args_density = "Compressed"
merge_imports = true
reorder_imports = true
use_try_shorthand = true
tab_spaces = 2
max_width = 150
color = "Never"
max_width = 100
use_small_heuristics = "Max"

View file

@ -19,10 +19,7 @@ pub struct Fx<T, F: Unsigned> {
impl<T, F: Unsigned> Fx<T, F> {
/// Uses the provided value directly.
pub fn from_raw(r: T) -> Self {
Fx {
num: r,
phantom: PhantomData,
}
Fx { num: r, phantom: PhantomData }
}
/// Unwraps the inner value.
@ -32,60 +29,42 @@ impl<T, F: Unsigned> Fx<T, F> {
/// Casts the base type, keeping the fractional bit quantity the same.
pub fn cast_inner<Z, C: Fn(T) -> Z>(self, op: C) -> Fx<Z, F> {
Fx {
num: op(self.num),
phantom: PhantomData,
}
Fx { num: op(self.num), phantom: PhantomData }
}
}
impl<T: Add<Output = T>, F: Unsigned> Add for Fx<T, F> {
type Output = Self;
fn add(self, rhs: Fx<T, F>) -> Self::Output {
Fx {
num: self.num + rhs.num,
phantom: PhantomData,
}
Fx { num: self.num + rhs.num, phantom: PhantomData }
}
}
impl<T: Sub<Output = T>, F: Unsigned> Sub for Fx<T, F> {
type Output = Self;
fn sub(self, rhs: Fx<T, F>) -> Self::Output {
Fx {
num: self.num - rhs.num,
phantom: PhantomData,
}
Fx { num: self.num - rhs.num, phantom: PhantomData }
}
}
impl<T: Shl<u32, Output = T>, F: Unsigned> Shl<u32> for Fx<T, F> {
type Output = Self;
fn shl(self, rhs: u32) -> Self::Output {
Fx {
num: self.num << rhs,
phantom: PhantomData,
}
Fx { num: self.num << rhs, phantom: PhantomData }
}
}
impl<T: Shr<u32, Output = T>, F: Unsigned> Shr<u32> for Fx<T, F> {
type Output = Self;
fn shr(self, rhs: u32) -> Self::Output {
Fx {
num: self.num >> rhs,
phantom: PhantomData,
}
Fx { num: self.num >> rhs, phantom: PhantomData }
}
}
impl<T: Neg<Output = T>, F: Unsigned> Neg for Fx<T, F> {
type Output = Self;
fn neg(self) -> Self::Output {
Fx {
num: -self.num,
phantom: PhantomData,
}
Fx { num: -self.num, phantom: PhantomData }
}
}
@ -94,18 +73,12 @@ macro_rules! fixed_point_methods {
impl<F: Unsigned> Fx<$t, F> {
/// Gives the smallest positive non-zero value.
pub fn precision() -> Self {
Fx {
num: 1,
phantom: PhantomData,
}
Fx { num: 1, phantom: PhantomData }
}
/// Makes a value with the integer part shifted into place.
pub fn from_int_part(i: $t) -> Self {
Fx {
num: i << F::U8,
phantom: PhantomData,
}
Fx { num: i << F::U8, phantom: PhantomData }
}
/// Changes the fractional bit quantity, keeping the base type the same.
@ -140,21 +113,12 @@ macro_rules! fixed_point_signed_multiply {
let pre_shift = (self.num as i32).wrapping_mul(rhs.num as i32);
if pre_shift < 0 {
if pre_shift == core::i32::MIN {
Fx {
num: core::$t::MIN,
phantom: PhantomData,
}
Fx { num: core::$t::MIN, phantom: PhantomData }
} else {
Fx {
num: (-((-pre_shift) >> F::U8)) as $t,
phantom: PhantomData,
}
Fx { num: (-((-pre_shift) >> F::U8)) as $t, phantom: PhantomData }
}
} else {
Fx {
num: (pre_shift >> F::U8) as $t,
phantom: PhantomData,
}
Fx { num: (pre_shift >> F::U8) as $t, phantom: PhantomData }
}
}
}
@ -192,10 +156,7 @@ macro_rules! fixed_point_signed_division {
fn div(self, rhs: Fx<$t, F>) -> Self::Output {
let mul_output: i32 = (self.num as i32).wrapping_mul(1 << F::U8);
let divide_result: i32 = crate::bios::div(mul_output, rhs.num as i32);
Fx {
num: divide_result as $t,
phantom: PhantomData,
}
Fx { num: divide_result as $t, phantom: PhantomData }
}
}
};
@ -213,10 +174,7 @@ macro_rules! fixed_point_unsigned_division {
fn div(self, rhs: Fx<$t, F>) -> Self::Output {
let mul_output: i32 = (self.num as i32).wrapping_mul(1 << F::U8);
let divide_result: i32 = crate::bios::div(mul_output, rhs.num as i32);
Fx {
num: divide_result as $t,
phantom: PhantomData,
}
Fx { num: divide_result as $t, phantom: PhantomData }
}
}
};

View file

@ -6,6 +6,7 @@ use super::*;
pub const BLDCNT: VolAddress<ColorEffectSetting> = unsafe { VolAddress::new(0x400_0050) };
newtype! {
/// TODO: docs
ColorEffectSetting, u16
}
@ -29,10 +30,15 @@ impl ColorEffectSetting {
}
newtype_enum! {
/// TODO: docs
ColorSpecialEffect = u16,
/// TODO: docs
None = 0,
/// TODO: docs
AlphaBlending = 1,
/// TODO: docs
BrightnessIncrease = 2,
/// TODO: docs
BrightnessDecrease = 3,
}
@ -40,6 +46,7 @@ newtype_enum! {
pub const BLDALPHA: VolAddress<AlphaBlendingSetting> = unsafe { VolAddress::new(0x400_0052) };
newtype! {
/// TODO: docs
AlphaBlendingSetting, u16
}
@ -55,6 +62,7 @@ impl AlphaBlendingSetting {
pub const BLDY: VolAddress<BrightnessSetting> = unsafe { VolAddress::new(0x400_0054) };
newtype! {
/// TODO: docs
BrightnessSetting, u32
}

View file

@ -122,42 +122,11 @@ impl DisplayStatusSetting {
/// Gives the current scanline that the display controller is working on. If
/// this is at or above the `VBLANK_SCANLINE` value then the display controller
/// is in a "vertical blank" period.
pub const VCOUNT: VolAddress<u16> = unsafe { VolAddress::new(0x400_0006) };
pub const VCOUNT: ROVolAddress<u16> = unsafe { ROVolAddress::new(0x400_0006) };
/// If the `VCOUNT` register reads equal to or above this then you're in vblank.
pub const VBLANK_SCANLINE: u16 = 160;
/// Obtains the current `VCOUNT` value.
pub fn vcount() -> u16 {
VCOUNT.read()
}
/// Performs a busy loop until VBlank starts.
///
/// NOTE: This method isn't very power efficient, since it is equivalent to
/// calling "halt" repeatedly. The recommended way to wait for a VBlank or VDraw
/// is to set an IRQ handler with
/// [`io::irq::set_irq_handler`](`io::irq::set_irq_handler`) and using
/// [`bios::vblank_intr_wait`](bios::vblank_interrupt_wait) to sleep the CPU
/// until a VBlank IRQ is generated. See the [`io::irq`](io::irq) module for
/// more details.
pub fn spin_until_vblank() {
while vcount() < VBLANK_SCANLINE {}
}
/// Performs a busy loop until VDraw starts.
///
/// NOTE: This method isn't very power efficient, since it is equivalent to
/// calling "halt" repeatedly. The recommended way to wait for a VBlank or VDraw
/// is to set an IRQ handler with
/// [`io::irq::set_irq_handler`](`io::irq::set_irq_handler`) and using
/// [`bios::vblank_intr_wait`](bios::vblank_interrupt_wait) to sleep the CPU
/// until a VBlank IRQ is generated. See the [`io::irq`](io::irq) module for
/// more details.
pub fn spin_until_vdraw() {
while vcount() >= VBLANK_SCANLINE {}
}
/// Global mosaic effect control. Write-only.
pub const MOSAIC: VolAddress<MosaicSetting> = unsafe { VolAddress::new(0x400_004C) };

View file

@ -8,7 +8,7 @@ use super::*;
/// follow the "high-active" convention (hint: you probably do, it's far easier
/// to work with) then call `read_key_input()` rather than reading this register
/// directly. It will perform the necessary bit flip operation for you.
pub const KEYINPUT: VolAddress<u16> = unsafe { VolAddress::new(0x400_0130) };
pub const KEYINPUT: ROVolAddress<u16> = unsafe { ROVolAddress::new(0x400_0130) };
/// A "tribool" value helps us interpret the arrow pad.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@ -50,9 +50,10 @@ impl KeyInput {
KeyInput(self.0 ^ other.0)
}
/// Gives the arrow pad value as a tribool, with Plus being increased column
/// value (right).
pub fn column_direction(self) -> TriBool {
/// Right/left tribool.
///
/// Right is Plus and Left is Minus
pub fn x_tribool(self) -> TriBool {
if self.right() {
TriBool::Plus
} else if self.left() {
@ -62,9 +63,10 @@ impl KeyInput {
}
}
/// Gives the arrow pad value as a tribool, with Plus being increased row
/// value (down).
pub fn row_direction(self) -> TriBool {
/// Up/down tribool.
///
/// Down is Plus and Up is Minus
pub fn y_tribool(self) -> TriBool {
if self.down() {
TriBool::Plus
} else if self.up() {

View file

@ -1,4 +1,5 @@
///! Module for sound registers.
//! Module for sound registers.
use super::*;
//TODO within these "read/write" registers only some bits are actually read/write!
@ -7,6 +8,7 @@ use super::*;
pub const SOUND1CNT_L: VolAddress<SweepRegisterSetting> = unsafe { VolAddress::new(0x400_0060) };
newtype! {
/// TODO: docs
SweepRegisterSetting, u16
}
@ -23,6 +25,7 @@ impl SweepRegisterSetting {
pub const SOUND1CNT_H: VolAddress<DutyLenEnvelopeSetting> = unsafe { VolAddress::new(0x400_0062) };
newtype! {
/// TODO: docs
DutyLenEnvelopeSetting, u16
}
@ -41,6 +44,7 @@ impl DutyLenEnvelopeSetting {
pub const SOUND1CNT_X: VolAddress<FrequencyControlSetting> = unsafe { VolAddress::new(0x400_0064) };
newtype! {
/// TODO: docs
FrequencyControlSetting, u32 // TODO: u16 or u32?
}
@ -60,9 +64,11 @@ pub const SOUND2CNT_L: VolAddress<DutyLenEnvelopeSetting> = unsafe { VolAddress:
pub const SOUND2CNT_H: VolAddress<FrequencyControlSetting> = unsafe { VolAddress::new(0x400_006C) };
/// Sound Channel 3 Stop/Wave RAM select (`NR23`, `NR24`). Read/Write.
pub const SOUND3CNT_L: VolAddress<StopWaveRAMSelectSetting> = unsafe { VolAddress::new(0x400_0070) };
pub const SOUND3CNT_L: VolAddress<StopWaveRAMSelectSetting> =
unsafe { VolAddress::new(0x400_0070) };
newtype! {
/// TODO: docs
StopWaveRAMSelectSetting, u16
}
@ -79,6 +85,7 @@ impl StopWaveRAMSelectSetting {
pub const SOUND3CNT_H: VolAddress<LengthVolumeSetting> = unsafe { VolAddress::new(0x400_0072) };
newtype! {
/// TODO: docs
LengthVolumeSetting, u16
}
@ -115,6 +122,7 @@ pub const WAVE_RAM3_H: VolAddress<u16> = unsafe { VolAddress::new(0x400_009E) };
pub const SOUND4CNT_L: VolAddress<LengthEnvelopeSetting> = unsafe { VolAddress::new(0x400_0078) };
newtype! {
/// TODO: docs
LengthEnvelopeSetting, u32 // TODO: is this u32?
}
@ -132,6 +140,7 @@ impl LengthEnvelopeSetting {
pub const SOUND4CNT_H: VolAddress<NoiseFrequencySetting> = unsafe { VolAddress::new(0x400_007C) };
newtype! {
/// TODO: docs
NoiseFrequencySetting, u32 // TODO: is this u32?
}
@ -158,9 +167,11 @@ pub const FIFO_B_L: VolAddress<u16> = unsafe { VolAddress::new(0x400_00A4) };
pub const FIFO_B_H: VolAddress<u16> = unsafe { VolAddress::new(0x400_00A6) };
/// Channel L/R Volume/Enable (`NR50`, `NR51`). Read/Write.
pub const SOUNDCNT_L: VolAddress<NonWaveVolumeEnableSetting> = unsafe { VolAddress::new(0x400_0080) };
pub const SOUNDCNT_L: VolAddress<NonWaveVolumeEnableSetting> =
unsafe { VolAddress::new(0x400_0080) };
newtype! {
/// TODO: docs
NonWaveVolumeEnableSetting, u16
}
@ -178,6 +189,7 @@ impl NonWaveVolumeEnableSetting {
pub const SOUNDCNT_H: VolAddress<WaveVolumeEnableSetting> = unsafe { VolAddress::new(0x400_0082) };
newtype! {
/// TODO: docs
WaveVolumeEnableSetting, u16
}
@ -199,9 +211,13 @@ impl WaveVolumeEnableSetting {
}
newtype_enum! {
/// TODO: docs
NumberSoundVolume = u16,
/// TODO: docs
Quarter = 0,
/// TODO: docs
Half = 1,
/// TODO: docs
Full = 2,
}
@ -209,6 +225,7 @@ newtype_enum! {
pub const SOUNDCNT_X: VolAddress<SoundMasterSetting> = unsafe { VolAddress::new(0x400_0084) };
newtype! {
/// TODO: docs
SoundMasterSetting, u16
}
@ -227,6 +244,7 @@ impl SoundMasterSetting {
pub const SOUNDBIAS: VolAddress<SoundPWMSetting> = unsafe { VolAddress::new(0x400_0088) };
newtype! {
/// TODO: docs
SoundPWMSetting, u16
}

View file

@ -9,6 +9,7 @@ pub const WIN0H: VolAddress<HorizontalWindowSetting> = unsafe { VolAddress::new(
pub const WIN1H: VolAddress<HorizontalWindowSetting> = unsafe { VolAddress::new(0x400_0042) };
newtype! {
/// TODO: docs
HorizontalWindowSetting, u16
}
@ -27,6 +28,7 @@ pub const WIN0V: VolAddress<VerticalWindowSetting> = unsafe { VolAddress::new(0x
pub const WIN1V: VolAddress<VerticalWindowSetting> = unsafe { VolAddress::new(0x400_0046) };
newtype! {
/// TODO: docs
VerticalWindowSetting, u16
}
@ -42,6 +44,7 @@ impl VerticalWindowSetting {
pub const WININ: VolAddress<InsideWindowSetting> = unsafe { VolAddress::new(0x400_0048) };
newtype! {
/// TODO: docs
InsideWindowSetting, u16
}
@ -67,6 +70,7 @@ impl InsideWindowSetting {
pub const WINOUT: VolAddress<OutsideWindowSetting> = unsafe { VolAddress::new(0x400_004A) };
newtype! {
/// TODO: docs
OutsideWindowSetting, u16
}

View file

@ -3,7 +3,7 @@
#![feature(cfg_target_vendor)]
#![allow(clippy::cast_lossless)]
#![deny(clippy::float_arithmetic)]
//#![warn(missing_docs)]
#![warn(missing_docs)]
//! This crate helps you write GBA ROMs.
//!
@ -19,91 +19,9 @@
//! do, it's a giant bag of Undefined Behavior.
pub(crate) use gba_proc_macro::phantom_fields;
pub(crate) use voladdress::{VolAddress, VolBlock};
pub(crate) use voladdress::{read_only::ROVolAddress, VolAddress, VolBlock};
/// Assists in defining a newtype wrapper over some base type.
///
/// Note that rustdoc and derives are all the "meta" stuff, so you can write all
/// of your docs and derives in front of your newtype in the same way you would
/// for a normal struct. Then the inner type to be wrapped it name.
///
/// The macro _assumes_ that you'll be using it to wrap numeric types and that
/// it's safe to have a `0` value, so it automatically provides a `const fn`
/// method for `new` that just wraps `0`. Also, it derives Debug, Clone, Copy,
/// Default, PartialEq, and Eq. If all this is not desired you can add `, no
/// frills` to the invocation.
///
/// Example:
/// ```
/// newtype! {
/// /// Records a particular key press combination.
/// KeyInput, u16
/// }
/// newtype! {
/// /// You can't derive most stuff above array size 32, so we add
/// /// the `, no frills` modifier to this one.
/// BigArray, [u8; 200], no frills
/// }
/// ```
#[macro_export]
macro_rules! newtype {
($(#[$attr:meta])* $new_name:ident, $v:vis $old_name:ty) => {
$(#[$attr])*
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
#[repr(transparent)]
pub struct $new_name($v $old_name);
impl $new_name {
/// A `const` "zero value" constructor
pub const fn new() -> Self {
$new_name(0)
}
}
};
($(#[$attr:meta])* $new_name:ident, $v:vis $old_name:ty, no frills) => {
$(#[$attr])*
#[repr(transparent)]
pub struct $new_name($v $old_name);
};
}
/// Assists in defining a newtype that's an enum.
///
/// First give `NewType = OldType,`, then define the tags and their explicit
/// values with zero or more entries of `TagName = base_value,`. In both cases
/// you can place doc comments or other attributes directly on to the type
/// declaration or the tag declaration.
///
/// The generated enum will get an appropriate `repr` attribute as well as Debug, Clone, Copy,
///
/// Example:
/// ```
/// newtype_enum! {
/// /// The Foo
/// Foo = u16,
/// /// The Bar
/// Bar = 0,
/// /// The Zap
/// Zap = 1,
/// }
/// ```
#[macro_export]
macro_rules! newtype_enum {
(
$(#[$struct_attr:meta])*
$new_name:ident = $old_name:ident,
$($(#[$tag_attr:meta])* $tag_name:ident = $base_value:expr,)*
) => {
$(#[$struct_attr])*
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr($old_name)]
pub enum $new_name {
$(
$(#[$tag_attr])*
$tag_name = $base_value,
)*
}
};
}
pub mod macros;
pub mod base;
@ -142,7 +60,7 @@ extern "C" {
newtype! {
/// A color on the GBA is an RGB 5.5.5 within a `u16`
#[derive(PartialOrd, Ord, Hash)]
Color, u16
Color, pub u16
}
impl Color {
@ -153,13 +71,6 @@ impl Color {
pub const fn from_rgb(r: u16, g: u16, b: u16) -> Color {
Color(b << 10 | g << 5 | r)
}
/// Does a left rotate of the bits.
///
/// This has no particular meaning but is a wild way to cycle colors.
pub const fn rotate_left(self, n: u32) -> Color {
Color(self.0.rotate_left(n))
}
}
//

203
src/macros.rs Normal file
View file

@ -0,0 +1,203 @@
//! Contains the macros for the crate.
//!
//! Because (unlike everything else in Rust) a macro has to be declared before
//! use, we place them in their own module and then declare that module at the
//! start of the crate.
/// Assists in defining a newtype wrapper over some base type.
///
/// Note that rustdoc and derives are all the "meta" stuff, so you can write all
/// of your docs and derives in front of your newtype in the same way you would
/// for a normal struct. Then the inner type to be wrapped it name.
///
/// The macro _assumes_ that you'll be using it to wrap numeric types and that
/// it's safe to have a `0` value, so it automatically provides a `const fn`
/// method for `new` that just wraps `0`. Also, it derives Debug, Clone, Copy,
/// Default, PartialEq, and Eq. If all this is not desired you can add `, no
/// frills` to the invocation.
///
/// ```no_run
/// newtype! {
/// /// Records a particular key press combination.
/// KeyInput, u16
/// }
/// newtype! {
/// /// You can't derive most stuff above array size 32, so we add
/// /// the `, no frills` modifier to this one.
/// BigArray, [u8; 200], no frills
/// }
/// ```
#[macro_export]
macro_rules! newtype {
($(#[$attr:meta])* $new_name:ident, $v:vis $old_name:ty) => {
$(#[$attr])*
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
#[repr(transparent)]
pub struct $new_name($v $old_name);
impl $new_name {
/// A `const` "zero value" constructor
pub const fn new() -> Self {
$new_name(0)
}
}
};
($(#[$attr:meta])* $new_name:ident, $v:vis $old_name:ty, no frills) => {
$(#[$attr])*
#[repr(transparent)]
pub struct $new_name($v $old_name);
};
}
/// Assists in defining a newtype that's an enum.
///
/// First give `NewType = OldType,`, then define the tags and their explicit
/// values with zero or more entries of `TagName = base_value,`. In both cases
/// you can place doc comments or other attributes directly on to the type
/// declaration or the tag declaration.
///
/// The generated enum will get an appropriate `repr` attribute as well as
/// Debug, Clone, Copy, PartialEq, and Eq
///
/// ```no_run
/// newtype_enum! {
/// /// The Foo
/// Foo = u16,
/// /// The Bar
/// Bar = 0,
/// /// The Zap
/// Zap = 1,
/// }
/// ```
#[macro_export]
macro_rules! newtype_enum {
(
$(#[$struct_attr:meta])*
$new_name:ident = $old_name:ident,
$($(#[$tag_attr:meta])* $tag_name:ident = $base_value:expr,)*
) => {
$(#[$struct_attr])*
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr($old_name)]
pub enum $new_name {
$(
$(#[$tag_attr])*
$tag_name = $base_value,
)*
}
};
}
/// Delivers a fatal message to the mGBA output, halting emulation.
///
/// This works basically like `println`. mGBA is a C program and all, so you
/// should only attempt to print non-null ASCII values through this. There's
/// also a maximum length of 255 bytes per message.
///
/// This has no effect if you're not using mGBA.
#[macro_export]
macro_rules! fatal {
($($arg:tt)*) => {{
use $crate::mgba::{MGBADebug, MGBADebugLevel};
use core::fmt::Write;
if let Some(mut mgba) = MGBADebug::new() {
let _ = write!(mgba, $($arg)*);
mgba.send(MGBADebugLevel::Fatal);
}
}};
}
/// Delivers an error message to the mGBA output.
///
/// This works basically like `println`. mGBA is a C program and all, so you
/// should only attempt to print non-null ASCII values through this. There's
/// also a maximum length of 255 bytes per message.
///
/// This has no effect if you're not using mGBA.
#[macro_export]
macro_rules! error {
($($arg:tt)*) => {{
use $crate::mgba::{MGBADebug, MGBADebugLevel};
use core::fmt::Write;
if let Some(mut mgba) = MGBADebug::new() {
let _ = write!(mgba, $($arg)*);
mgba.send(MGBADebugLevel::Error);
}
}};
}
/// Delivers a warning message to the mGBA output.
///
/// This works basically like `println`. mGBA is a C program and all, so you
/// should only attempt to print non-null ASCII values through this. There's
/// also a maximum length of 255 bytes per message.
///
/// This has no effect if you're not using mGBA.
#[macro_export]
macro_rules! warn {
($($arg:tt)*) => {{
use $crate::mgba::{MGBADebug, MGBADebugLevel};
use core::fmt::Write;
if let Some(mut mgba) = MGBADebug::new() {
let _ = write!(mgba, $($arg)*);
mgba.send(MGBADebugLevel::Warning);
}
}};
}
/// Delivers an info message to the mGBA output.
///
/// This works basically like `println`. mGBA is a C program and all, so you
/// should only attempt to print non-null ASCII values through this. There's
/// also a maximum length of 255 bytes per message.
///
/// This has no effect if you're not using mGBA.
#[macro_export]
macro_rules! info {
($($arg:tt)*) => {{
use $crate::mgba::{MGBADebug, MGBADebugLevel};
use core::fmt::Write;
if let Some(mut mgba) = MGBADebug::new() {
let _ = write!(mgba, $($arg)*);
mgba.send(MGBADebugLevel::Info);
}
}};
}
/// Delivers a debug message to the mGBA output.
///
/// This works basically like `println`. mGBA is a C program and all, so you
/// should only attempt to print non-null ASCII values through this. There's
/// also a maximum length of 255 bytes per message.
///
/// This has no effect if you're not using mGBA.
#[macro_export]
macro_rules! debug {
($($arg:tt)*) => {{
use $crate::mgba::{MGBADebug, MGBADebugLevel};
use core::fmt::Write;
if let Some(mut mgba) = MGBADebug::new() {
let _ = write!(mgba, $($arg)*);
mgba.send(MGBADebugLevel::Debug);
}
}};
}
/// Using timers 0 and 1, performs a crude timing of the expression given.
#[macro_export]
macro_rules! time_this01 {
($x:expr) => {{
use $crate::io::timers::*;
const NORMAL_ON: TimerControlSetting = TimerControlSetting::new().with_enabled(true);
const CASCADE_ON: TimerControlSetting =
TimerControlSetting::new().with_enabled(true).with_tick_rate(TimerTickRate::Cascade);
const OFF: TimerControlSetting = TimerControlSetting::new();
TM1CNT_H.write(CASCADE_ON);
TM0CNT_H.write(NORMAL_ON);
$x;
TM0CNT_H.write(OFF);
TM1CNT_H.write(OFF);
let end_low = TM0CNT_L.read() as u32;
let end_high = TM1CNT_L.read() as u32;
end_high << 16 | end_low
}};
}

View file

@ -55,8 +55,6 @@ impl MGBADebug {
/// it might accidentally be discarded.
pub fn send(&mut self, level: MGBADebugLevel) {
if level == MGBADebugLevel::Fatal {
Self::SEND_ADDRESS.write(Self::SEND_FLAG | MGBADebugLevel::Error as u16);
// Note(Lokathor): A Fatal send causes the emulator to halt!
Self::SEND_ADDRESS.write(Self::SEND_FLAG | MGBADebugLevel::Fatal as u16);
} else {

View file

@ -29,11 +29,14 @@ pub mod text;
/// being the correct thing.
pub const VRAM_BASE_USIZE: usize = 0x600_0000;
pub const PAGE1_OFFSET: usize = 0xA000;
/// The character base blocks.
pub const CHAR_BASE_BLOCKS: VolBlock<[u8; 0x4000], U6> = unsafe { VolBlock::new(VRAM_BASE_USIZE) };
/// The screen entry base blocks.
pub const SCREEN_BASE_BLOCKS: VolBlock<[u8; 0x800], U32> = unsafe { VolBlock::new(VRAM_BASE_USIZE) };
pub const SCREEN_BASE_BLOCKS: VolBlock<[u8; 0x800], U32> =
unsafe { VolBlock::new(VRAM_BASE_USIZE) };
newtype! {
/// An 8x8 tile with 4bpp, packed as `u32` values for proper alignment.

View file

@ -1,155 +1,213 @@
//! Module for the Bitmap video modes.
use super::*;
use core::ops::{Div, Mul};
use typenum::consts::{U128, U160, U2, U256, U4};
/// Mode 3 is a bitmap mode with full color and full resolution.
/// A bitmap video mode with full color and full resolution.
///
/// * **Width:** 240
/// * **Height:** 160
///
/// Because the memory requirements are so large, there's only a single page
/// available instead of two pages like the other video modes have.
/// Because it takes so much space to have full color and full resolution at the
/// same time, there's no alternate page available when using mode 3.
///
/// As with all bitmap modes, the image itself utilizes BG2 for display, so you
/// must have BG2 enabled in addition to being within Mode 3.
/// As with all the bitmap video modes, the bitmap is considered to be BG2, so
/// you have to enable BG2 as well if you want to see the bitmap.
pub struct Mode3;
impl Mode3 {
/// The physical width in pixels of the GBA screen.
pub const SCREEN_WIDTH: usize = 240;
/// The screen's width in this mode.
pub const WIDTH: usize = 240;
/// The physical height in pixels of the GBA screen.
pub const SCREEN_HEIGHT: usize = 160;
/// The screen's height in this mode.
pub const HEIGHT: usize = 160;
/// The number of pixels on the screen.
pub const SCREEN_PIXEL_COUNT: usize = Self::SCREEN_WIDTH * Self::SCREEN_HEIGHT;
const VRAM: VolBlock<Color, <U256 as Mul<U160>>::Output> =
unsafe { VolBlock::new(VRAM_BASE_USIZE) };
/// The Mode 3 VRAM.
const WORDS_BLOCK: VolBlock<u32, <<U256 as Mul<U160>>::Output as Div<U2>>::Output> =
unsafe { VolBlock::new(VRAM_BASE_USIZE) };
/// Gets the address of the pixel specified.
///
/// Use `col + row * SCREEN_WIDTH` to get the address of an individual pixel,
/// or use the helpers provided in this module.
pub const VRAM: VolBlock<Color, <U256 as Mul<U160>>::Output> = unsafe { VolBlock::new(VRAM_BASE_USIZE) };
/// private iterator over the pixels, two at a time
const VRAM_BULK: VolBlock<u32, <<U256 as Mul<U160>>::Output as Div<U2>>::Output> = unsafe { VolBlock::new(VRAM_BASE_USIZE) };
/// Reads the pixel at the given (col,row).
/// ## Failure
///
/// # Failure
///
/// Gives `None` if out of bounds.
pub fn read_pixel(col: usize, row: usize) -> Option<Color> {
Self::VRAM.get(col + row * Self::SCREEN_WIDTH).map(VolAddress::read)
/// Gives `None` if out of bounds
fn get(col: usize, row: usize) -> Option<VolAddress<Color>> {
Self::VRAM.get(col + row * Self::WIDTH)
}
/// Writes the pixel at the given (col,row).
/// Reads the color of the pixel specified.
///
/// # Failure
/// ## Failure
///
/// Gives `None` if out of bounds.
pub fn write_pixel(col: usize, row: usize, color: Color) -> Option<()> {
Self::VRAM.get(col + row * Self::SCREEN_WIDTH).map(|va| va.write(color))
/// Gives `None` if out of bounds
pub fn read(col: usize, row: usize) -> Option<Color> {
Self::get(col, row).map(VolAddress::read)
}
/// Clears the whole screen to the desired color.
/// Writes a color to the pixel specified.
///
/// ## Failure
///
/// Gives `None` if out of bounds
pub fn write(col: usize, row: usize, color: Color) -> Option<()> {
Self::get(col, row).map(|va| va.write(color))
}
/// Clear the screen to the color specified.
///
/// Takes ~430,000 cycles (~1.5 frames).
pub fn clear_to(color: Color) {
let color32 = color.0 as u32;
let bulk_color = color32 << 16 | color32;
for va in Self::VRAM_BULK.iter() {
for va in Self::WORDS_BLOCK.iter() {
va.write(bulk_color)
}
}
/// Clears the whole screen to the desired color using DMA3.
/// Clears the screen to the color specified using DMA3.
///
/// Takes ~61,500 frames (~73% of VBlank)
pub fn dma_clear_to(color: Color) {
use crate::io::dma::DMA3;
let color32 = color.0 as u32;
let bulk_color = color32 << 16 | color32;
unsafe { DMA3::fill32(&bulk_color, VRAM_BASE_USIZE as *mut u32, (Self::SCREEN_PIXEL_COUNT / 2) as u16) };
unsafe {
DMA3::fill32(&bulk_color, VRAM_BASE_USIZE as *mut u32, Self::WORDS_BLOCK.len() as u16)
};
}
/// Draws a line between the two points given `(c1,r1,c2,r2,color)`.
///
/// Works fine with out of bounds points. It only draws to in bounds
/// locations.
pub fn draw_line(c1: isize, r1: isize, c2: isize, r2: isize, color: Color) {
let mut col = c1;
let mut row = r1;
let w = c2 - c1;
let h = r2 - r1;
let mut dx1 = 0;
let mut dx2 = 0;
let mut dy1 = 0;
let mut dy2 = 0;
let mut longest = w.abs();
let mut shortest = h.abs();
if w < 0 {
dx1 = -1;
} else if w > 0 {
dx1 = 1;
};
if h < 0 {
dy1 = -1;
} else if h > 0 {
dy1 = 1;
};
if w < 0 {
dx2 = -1;
} else if w > 0 {
dx2 = 1;
};
if !(longest > shortest) {
core::mem::swap(&mut longest, &mut shortest);
if h < 0 {
dy2 = -1;
} else if h > 0 {
dy2 = 1
};
dx2 = 0;
}
let mut numerator = longest >> 1;
(0..(longest + 1)).for_each(|_| {
Self::write(col as usize, row as usize, color);
numerator += shortest;
if !(numerator < longest) {
numerator -= longest;
col += dx1;
row += dy1;
} else {
col += dx2;
row += dy2;
}
});
}
}
//TODO: Mode3 Iter Scanlines / Pixels?
//TODO: Mode3 Line Drawing?
/// Used to select what page to read from or write to in Mode 4 and Mode 5.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Page {
/// Page 0
Zero,
/// Page 1
One,
}
/// Mode 4 is a bitmap mode with 8bpp paletted color.
/// A bitmap video mode with full resolution and paletted color.
///
/// * **Width:** 240
/// * **Height:** 160
/// * **Pages:** 2
///
/// VRAM has a minimum write size of 2 bytes at a time, so writing individual
/// palette entries for the pixels is more costly than with the other bitmap
/// modes.
/// Because the pixels use palette indexes there's enough space to have two
/// pages.
///
/// As with all bitmap modes, the image itself utilizes BG2 for display, so you
/// must have BG2 enabled in addition to being within Mode 4.
/// As with all the bitmap video modes, the bitmap is considered to be BG2, so
/// you have to enable BG2 as well if you want to see the bitmap.
pub struct Mode4;
impl Mode4 {
/// The physical width in pixels of the GBA screen.
pub const SCREEN_WIDTH: usize = 240;
/// The screen's width in this mode.
pub const WIDTH: usize = 240;
/// The physical height in pixels of the GBA screen.
pub const SCREEN_HEIGHT: usize = 160;
/// The screen's height in this mode.
pub const HEIGHT: usize = 160;
/// The number of pixels on the screen.
pub const SCREEN_PIXEL_COUNT: usize = Self::SCREEN_WIDTH * Self::SCREEN_HEIGHT;
const PAGE0_INDEXES: VolBlock<u8, <U256 as Mul<U160>>::Output> =
unsafe { VolBlock::new(VRAM_BASE_USIZE) };
/// Used for bulk clearing operations.
const SCREEN_U32_COUNT: usize = Self::SCREEN_PIXEL_COUNT / 4;
const PAGE1_INDEXES: VolBlock<u8, <U256 as Mul<U160>>::Output> =
unsafe { VolBlock::new(VRAM_BASE_USIZE + PAGE1_OFFSET) };
// TODO: newtype this?
const PAGE0_BLOCK8: VolBlock<u8, <U256 as Mul<U160>>::Output> = unsafe { VolBlock::new(VRAM_BASE_USIZE) };
const PAGE0_WORDS: VolBlock<u32, <<U256 as Mul<U160>>::Output as Div<U4>>::Output> =
unsafe { VolBlock::new(VRAM_BASE_USIZE) };
// TODO: newtype this?
const PAGE1_BLOCK8: VolBlock<u8, <U256 as Mul<U160>>::Output> = unsafe { VolBlock::new(VRAM_BASE_USIZE + 0xA000) };
const PAGE1_WORDS: VolBlock<u32, <<U256 as Mul<U160>>::Output as Div<U4>>::Output> =
unsafe { VolBlock::new(VRAM_BASE_USIZE + PAGE1_OFFSET) };
// TODO: newtype this?
const PAGE0_BLOCK16: VolBlock<u16, <<U256 as Mul<U160>>::Output as Div<U2>>::Output> = unsafe { VolBlock::new(VRAM_BASE_USIZE) };
// TODO: newtype this?
const PAGE1_BLOCK16: VolBlock<u16, <<U256 as Mul<U160>>::Output as Div<U2>>::Output> = unsafe { VolBlock::new(VRAM_BASE_USIZE + 0xA000) };
/// private iterator over the page0 pixels, four at a time
const PAGE0_BULK32: VolBlock<u32, <<U256 as Mul<U160>>::Output as Div<U4>>::Output> = unsafe { VolBlock::new(VRAM_BASE_USIZE) };
/// private iterator over the page1 pixels, four at a time
const PAGE1_BULK32: VolBlock<u32, <<U256 as Mul<U160>>::Output as Div<U4>>::Output> = unsafe { VolBlock::new(VRAM_BASE_USIZE + 0xA000) };
/// Reads the pixel at the given (col,row).
/// Reads the color of the pixel specified.
///
/// # Failure
/// ## Failure
///
/// Gives `None` if out of bounds.
pub fn read_pixel(page1: bool, col: usize, row: usize) -> Option<u8> {
// Note(Lokathor): byte _reads_ from VRAM are okay.
if page1 {
Self::PAGE1_BLOCK8.get(col + row * Self::SCREEN_WIDTH).map(VolAddress::read)
} else {
Self::PAGE0_BLOCK8.get(col + row * Self::SCREEN_WIDTH).map(VolAddress::read)
/// Gives `None` if out of bounds
pub fn read(page: Page, col: usize, row: usize) -> Option<u8> {
match page {
Page::Zero => Self::PAGE0_INDEXES,
Page::One => Self::PAGE1_INDEXES,
}
.get(col + row * Self::WIDTH)
.map(VolAddress::read)
}
/// Writes the pixel at the given (col,row).
/// Writes a color to the pixel specified.
///
/// # Failure
/// ## Failure
///
/// Gives `None` if out of bounds.
pub fn write_pixel(page1: bool, col: usize, row: usize, pal8bpp: u8) -> Option<()> {
// Note(Lokathor): byte _writes_ to VRAM are not permitted. We must jump
// through hoops when we attempt to write just a single byte.
if col < Self::SCREEN_WIDTH && row < Self::SCREEN_HEIGHT {
let real_index = col + row * Self::SCREEN_WIDTH;
/// Gives `None` if out of bounds
pub fn write(page: Page, col: usize, row: usize, pal8bpp: u8) -> Option<()> {
// Note(Lokathor): Byte writes to VRAM aren't permitted, we have to jump
// through some hoops.
if col < Self::WIDTH && row < Self::HEIGHT {
let real_index = col + row * Self::WIDTH;
let rounded_down_index = real_index & !1;
let address: VolAddress<u16> = unsafe {
if page1 {
Self::PAGE1_BLOCK8.index(rounded_down_index).cast()
} else {
Self::PAGE0_BLOCK8.index(rounded_down_index).cast()
match page {
Page::Zero => Self::PAGE0_INDEXES,
Page::One => Self::PAGE1_INDEXES,
}
.index_unchecked(rounded_down_index)
.cast::<u16>()
};
if real_index == rounded_down_index {
// even byte, change the high bits
@ -166,137 +224,237 @@ impl Mode4 {
}
}
/// Writes a "wide" pairing of palette entries to the location specified.
/// Clear the screen to the palette index specified.
///
/// The page is imagined to be a series of `u16` values rather than `u8`
/// values, allowing you to write two palette entries side by side as a single
/// write operation.
pub fn write_wide_pixel(page1: bool, wide_col: usize, row: usize, wide_pal8bpp: u16) -> Option<()> {
if wide_col < Self::SCREEN_WIDTH / 2 && row < Self::SCREEN_HEIGHT {
let wide_index = wide_col + row * Self::SCREEN_WIDTH / 2;
let address: VolAddress<u16> = if page1 {
Self::PAGE1_BLOCK16.index(wide_index)
} else {
Self::PAGE0_BLOCK16.index(wide_index)
};
Some(address.write(wide_pal8bpp))
} else {
None
}
}
/// Clears the page to the desired color.
pub fn clear_page_to(page1: bool, pal8bpp: u8) {
/// Takes ~215,000 cycles (~76% of a frame)
pub fn clear_to(page: Page, pal8bpp: u8) {
let pal8bpp_32 = pal8bpp as u32;
let bulk_color = (pal8bpp_32 << 24) | (pal8bpp_32 << 16) | (pal8bpp_32 << 8) | pal8bpp_32;
for va in (if page1 { Self::PAGE1_BULK32 } else { Self::PAGE0_BULK32 }).iter() {
let words = match page {
Page::Zero => Self::PAGE0_WORDS,
Page::One => Self::PAGE1_WORDS,
};
for va in words.iter() {
va.write(bulk_color)
}
}
/// Clears the page to the desired color using DMA3.
pub fn dma_clear_page_to(page1: bool, pal8bpp: u8) {
/// Clears the screen to the palette index specified using DMA3.
///
/// Takes ~30,800 frames (~37% of VBlank)
pub fn dma_clear_to(page: Page, pal8bpp: u8) {
use crate::io::dma::DMA3;
let pal8bpp_32 = pal8bpp as u32;
let bulk_color = (pal8bpp_32 << 24) | (pal8bpp_32 << 16) | (pal8bpp_32 << 8) | pal8bpp_32;
let write_target = if page1 {
VRAM_BASE_USIZE as *mut u32
} else {
(VRAM_BASE_USIZE + 0xA000) as *mut u32
let words_address = unsafe {
match page {
Page::Zero => Self::PAGE0_WORDS.index_unchecked(0).to_usize(),
Page::One => Self::PAGE1_WORDS.index_unchecked(0).to_usize(),
}
};
unsafe { DMA3::fill32(&bulk_color, write_target, Self::SCREEN_U32_COUNT as u16) };
unsafe { DMA3::fill32(&bulk_color, words_address as *mut u32, Self::PAGE0_WORDS.len() as u16) };
}
/// Draws a line between the two points given `(c1,r1,c2,r2,color)`.
///
/// Works fine with out of bounds points. It only draws to in bounds
/// locations.
pub fn draw_line(page: Page, c1: isize, r1: isize, c2: isize, r2: isize, pal8bpp: u8) {
let mut col = c1;
let mut row = r1;
let w = c2 - c1;
let h = r2 - r1;
let mut dx1 = 0;
let mut dx2 = 0;
let mut dy1 = 0;
let mut dy2 = 0;
let mut longest = w.abs();
let mut shortest = h.abs();
if w < 0 {
dx1 = -1;
} else if w > 0 {
dx1 = 1;
};
if h < 0 {
dy1 = -1;
} else if h > 0 {
dy1 = 1;
};
if w < 0 {
dx2 = -1;
} else if w > 0 {
dx2 = 1;
};
if !(longest > shortest) {
core::mem::swap(&mut longest, &mut shortest);
if h < 0 {
dy2 = -1;
} else if h > 0 {
dy2 = 1
};
dx2 = 0;
}
let mut numerator = longest >> 1;
(0..(longest + 1)).for_each(|_| {
Self::write(page, col as usize, row as usize, pal8bpp);
numerator += shortest;
if !(numerator < longest) {
numerator -= longest;
col += dx1;
row += dy1;
} else {
col += dx2;
row += dy2;
}
});
}
}
//TODO: Mode4 Iter Scanlines / Pixels?
//TODO: Mode4 Line Drawing?
/// Mode 5 is a bitmap mode with full color and reduced resolution.
///
/// * **Width:** 160
/// * **Height:** 128
/// * **Pages:** 2
///
/// Because of the reduced resolution, we're allowed two pages for display.
/// Because of the reduced resolutions there's enough space to have two pages.
///
/// As with all bitmap modes, the image itself utilizes BG2 for display, so you
/// must have BG2 enabled in addition to being within Mode 3.
/// As with all the bitmap video modes, the bitmap is considered to be BG2, so
/// you have to enable BG2 as well if you want to see the bitmap.
pub struct Mode5;
impl Mode5 {
/// The physical width in pixels of the GBA screen.
pub const SCREEN_WIDTH: usize = 160;
/// The screen's width in this mode.
pub const WIDTH: usize = 160;
/// The physical height in pixels of the GBA screen.
pub const SCREEN_HEIGHT: usize = 128;
/// The screen's height in this mode.
pub const HEIGHT: usize = 128;
/// The number of pixels on the screen.
pub const SCREEN_PIXEL_COUNT: usize = Self::SCREEN_WIDTH * Self::SCREEN_HEIGHT;
const PAGE0_PIXELS: VolBlock<Color, <U160 as Mul<U128>>::Output> =
unsafe { VolBlock::new(VRAM_BASE_USIZE) };
/// Used for bulk clearing operations.
const SCREEN_U32_COUNT: usize = Self::SCREEN_PIXEL_COUNT / 2;
const PAGE1_PIXELS: VolBlock<Color, <U160 as Mul<U128>>::Output> =
unsafe { VolBlock::new(VRAM_BASE_USIZE + PAGE1_OFFSET) };
// TODO: newtype this?
const PAGE0_BLOCK: VolBlock<Color, <U160 as Mul<U128>>::Output> = unsafe { VolBlock::new(VRAM_BASE_USIZE) };
const PAGE0_WORDS: VolBlock<u32, <<U160 as Mul<U128>>::Output as Div<U2>>::Output> =
unsafe { VolBlock::new(VRAM_BASE_USIZE) };
// TODO: newtype this?
const PAGE1_BLOCK: VolBlock<Color, <U160 as Mul<U128>>::Output> = unsafe { VolBlock::new(VRAM_BASE_USIZE + 0xA000) };
const PAGE1_WORDS: VolBlock<u32, <<U160 as Mul<U128>>::Output as Div<U2>>::Output> =
unsafe { VolBlock::new(VRAM_BASE_USIZE + PAGE1_OFFSET) };
/// private iterator over the page0 pixels, four at a time
const PAGE0_BULK32: VolBlock<u32, <<U160 as Mul<U128>>::Output as Div<U2>>::Output> = unsafe { VolBlock::new(VRAM_BASE_USIZE) };
/// private iterator over the page1 pixels, four at a time
const PAGE1_BULK32: VolBlock<u32, <<U160 as Mul<U128>>::Output as Div<U2>>::Output> = unsafe { VolBlock::new(VRAM_BASE_USIZE + 0xA000) };
/// Reads the pixel at the given (col,row).
/// Reads the color of the pixel specified.
///
/// # Failure
/// ## Failure
///
/// Gives `None` if out of bounds.
pub fn read_pixel(page1: bool, col: usize, row: usize) -> Option<Color> {
if page1 {
Self::PAGE1_BLOCK.get(col + row * Self::SCREEN_WIDTH).map(VolAddress::read)
} else {
Self::PAGE0_BLOCK.get(col + row * Self::SCREEN_WIDTH).map(VolAddress::read)
/// Gives `None` if out of bounds
pub fn read(page: Page, col: usize, row: usize) -> Option<Color> {
match page {
Page::Zero => Self::PAGE0_PIXELS,
Page::One => Self::PAGE1_PIXELS,
}
.get(col + row * Self::WIDTH)
.map(VolAddress::read)
}
/// Writes the pixel at the given (col,row).
/// Writes a color to the pixel specified.
///
/// # Failure
/// ## Failure
///
/// Gives `None` if out of bounds.
pub fn write_pixel(page1: bool, col: usize, row: usize, color: Color) -> Option<()> {
if page1 {
Self::PAGE1_BLOCK.get(col + row * Self::SCREEN_WIDTH).map(|va| va.write(color))
} else {
Self::PAGE0_BLOCK.get(col + row * Self::SCREEN_WIDTH).map(|va| va.write(color))
/// Gives `None` if out of bounds
pub fn write(page: Page, col: usize, row: usize, color: Color) -> Option<()> {
match page {
Page::Zero => Self::PAGE0_PIXELS,
Page::One => Self::PAGE1_PIXELS,
}
.get(col + row * Self::WIDTH)
.map(|va| va.write(color))
}
/// Clears the whole screen to the desired color.
pub fn clear_page_to(page1: bool, color: Color) {
/// Clear the screen to the color specified.
///
/// Takes ~215,000 cycles (~76% of a frame)
pub fn clear_to(page: Page, color: Color) {
let color32 = color.0 as u32;
let bulk_color = color32 << 16 | color32;
for va in (if page1 { Self::PAGE1_BULK32 } else { Self::PAGE0_BULK32 }).iter() {
let words = match page {
Page::Zero => Self::PAGE0_WORDS,
Page::One => Self::PAGE1_WORDS,
};
for va in words.iter() {
va.write(bulk_color)
}
}
/// Clears the whole screen to the desired color using DMA3.
pub fn dma_clear_page_to(page1: bool, color: Color) {
/// Clears the screen to the color specified using DMA3.
///
/// Takes ~30,800 frames (~37% of VBlank)
pub fn dma_clear_to(page: Page, color: Color) {
use crate::io::dma::DMA3;
let color32 = color.0 as u32;
let bulk_color = color32 << 16 | color32;
let write_target = if page1 {
VRAM_BASE_USIZE as *mut u32
} else {
(VRAM_BASE_USIZE + 0xA000) as *mut u32
let words_address = unsafe {
match page {
Page::Zero => Self::PAGE0_WORDS.index_unchecked(0).to_usize(),
Page::One => Self::PAGE1_WORDS.index_unchecked(0).to_usize(),
}
};
unsafe { DMA3::fill32(&bulk_color, write_target, Self::SCREEN_U32_COUNT as u16) };
unsafe { DMA3::fill32(&bulk_color, words_address as *mut u32, Self::PAGE0_WORDS.len() as u16) };
}
/// Draws a line between the two points given `(c1,r1,c2,r2,color)`.
///
/// Works fine with out of bounds points. It only draws to in bounds
/// locations.
pub fn draw_line(page: Page, c1: isize, r1: isize, c2: isize, r2: isize, color: Color) {
let mut col = c1;
let mut row = r1;
let w = c2 - c1;
let h = r2 - r1;
let mut dx1 = 0;
let mut dx2 = 0;
let mut dy1 = 0;
let mut dy2 = 0;
let mut longest = w.abs();
let mut shortest = h.abs();
if w < 0 {
dx1 = -1;
} else if w > 0 {
dx1 = 1;
};
if h < 0 {
dy1 = -1;
} else if h > 0 {
dy1 = 1;
};
if w < 0 {
dx2 = -1;
} else if w > 0 {
dx2 = 1;
};
if !(longest > shortest) {
core::mem::swap(&mut longest, &mut shortest);
if h < 0 {
dy2 = -1;
} else if h > 0 {
dy2 = 1
};
dx2 = 0;
}
let mut numerator = longest >> 1;
(0..(longest + 1)).for_each(|_| {
Self::write(page, col as usize, row as usize, color);
numerator += shortest;
if !(numerator < longest) {
numerator -= longest;
col += dx1;
row += dy1;
} else {
col += dx2;
row += dy2;
}
});
}
}
//TODO: Mode5 Iter Scanlines / Pixels?
//TODO: Mode5 Line Drawing?