mirror of
https://github.com/italicsjenga/gba.git
synced 2024-12-23 19:01:30 +11:00
Merge pull request #65 from rust-console/lokathor
IO Registers + Bitmap book section
This commit is contained in:
commit
98a9eefaf5
|
@ -1,7 +1,7 @@
|
||||||
[package]
|
[package]
|
||||||
name = "gba"
|
name = "gba"
|
||||||
description = "A crate (and book) for making GBA games with Rust."
|
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>"]
|
authors = ["Lokathor <zefria@gmail.com>", "Thomas Winwood <twwinwood@gmail.com>"]
|
||||||
repository = "https://github.com/rust-console/gba"
|
repository = "https://github.com/rust-console/gba"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|
|
@ -1 +1,214 @@
|
||||||
# Bitmap Video
|
# 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
|
@ -1 +1,237 @@
|
||||||
# IO Registers
|
# 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.
|
||||||
|
|
|
@ -3,22 +3,82 @@
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
|
|
||||||
use gba::{
|
use gba::{
|
||||||
io::display::{DisplayControlSetting, DisplayMode, DISPCNT},
|
fatal,
|
||||||
|
io::{
|
||||||
|
display::{DisplayControlSetting, DisplayMode, DISPCNT, VBLANK_SCANLINE, VCOUNT},
|
||||||
|
keypad::read_key_input,
|
||||||
|
},
|
||||||
vram::bitmap::Mode3,
|
vram::bitmap::Mode3,
|
||||||
Color,
|
Color,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[panic_handler]
|
#[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 {}
|
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]
|
#[start]
|
||||||
fn main(_argc: isize, _argv: *const *const u8) -> isize {
|
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);
|
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));
|
let mut px = Mode3::WIDTH / 2;
|
||||||
Mode3::write_pixel(120, 96, Color::from_rgb(0, 0, 31));
|
let mut py = Mode3::HEIGHT / 2;
|
||||||
loop {}
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,8 @@ fn panic(_info: &core::panic::PanicInfo) -> ! {
|
||||||
|
|
||||||
fn start_timers() {
|
fn start_timers() {
|
||||||
let init_val: u16 = u32::wrapping_sub(0x1_0000, 64) as u16;
|
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_L.write(init_val);
|
||||||
TM0CNT_H.write(TIMER_SETTINGS.with_tick_rate(TimerTickRate::CPU1024));
|
TM0CNT_H.write(TIMER_SETTINGS.with_tick_rate(TimerTickRate::CPU1024));
|
||||||
|
@ -90,8 +91,8 @@ static mut PIXEL: usize = 0;
|
||||||
|
|
||||||
fn write_pixel(color: Color) {
|
fn write_pixel(color: Color) {
|
||||||
unsafe {
|
unsafe {
|
||||||
Mode3::write_pixel(PIXEL, 0, color);
|
Mode3::write(PIXEL, 0, color);
|
||||||
PIXEL = (PIXEL + 1) % Mode3::SCREEN_PIXEL_COUNT;
|
PIXEL = (PIXEL + 1) % (Mode3::WIDTH * Mode3::HEIGHT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,8 +1,9 @@
|
||||||
|
color = "Never"
|
||||||
error_on_line_overflow = false
|
error_on_line_overflow = false
|
||||||
fn_args_density = "Compressed"
|
fn_args_density = "Compressed"
|
||||||
merge_imports = true
|
merge_imports = true
|
||||||
reorder_imports = true
|
reorder_imports = true
|
||||||
use_try_shorthand = true
|
use_try_shorthand = true
|
||||||
tab_spaces = 2
|
tab_spaces = 2
|
||||||
max_width = 150
|
max_width = 100
|
||||||
color = "Never"
|
use_small_heuristics = "Max"
|
||||||
|
|
|
@ -19,10 +19,7 @@ pub struct Fx<T, F: Unsigned> {
|
||||||
impl<T, F: Unsigned> Fx<T, F> {
|
impl<T, F: Unsigned> Fx<T, F> {
|
||||||
/// Uses the provided value directly.
|
/// Uses the provided value directly.
|
||||||
pub fn from_raw(r: T) -> Self {
|
pub fn from_raw(r: T) -> Self {
|
||||||
Fx {
|
Fx { num: r, phantom: PhantomData }
|
||||||
num: r,
|
|
||||||
phantom: PhantomData,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Unwraps the inner value.
|
/// 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.
|
/// 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> {
|
pub fn cast_inner<Z, C: Fn(T) -> Z>(self, op: C) -> Fx<Z, F> {
|
||||||
Fx {
|
Fx { num: op(self.num), phantom: PhantomData }
|
||||||
num: op(self.num),
|
|
||||||
phantom: PhantomData,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Add<Output = T>, F: Unsigned> Add for Fx<T, F> {
|
impl<T: Add<Output = T>, F: Unsigned> Add for Fx<T, F> {
|
||||||
type Output = Self;
|
type Output = Self;
|
||||||
fn add(self, rhs: Fx<T, F>) -> Self::Output {
|
fn add(self, rhs: Fx<T, F>) -> Self::Output {
|
||||||
Fx {
|
Fx { num: self.num + rhs.num, phantom: PhantomData }
|
||||||
num: self.num + rhs.num,
|
|
||||||
phantom: PhantomData,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Sub<Output = T>, F: Unsigned> Sub for Fx<T, F> {
|
impl<T: Sub<Output = T>, F: Unsigned> Sub for Fx<T, F> {
|
||||||
type Output = Self;
|
type Output = Self;
|
||||||
fn sub(self, rhs: Fx<T, F>) -> Self::Output {
|
fn sub(self, rhs: Fx<T, F>) -> Self::Output {
|
||||||
Fx {
|
Fx { num: self.num - rhs.num, phantom: PhantomData }
|
||||||
num: self.num - rhs.num,
|
|
||||||
phantom: PhantomData,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Shl<u32, Output = T>, F: Unsigned> Shl<u32> for Fx<T, F> {
|
impl<T: Shl<u32, Output = T>, F: Unsigned> Shl<u32> for Fx<T, F> {
|
||||||
type Output = Self;
|
type Output = Self;
|
||||||
fn shl(self, rhs: u32) -> Self::Output {
|
fn shl(self, rhs: u32) -> Self::Output {
|
||||||
Fx {
|
Fx { num: self.num << rhs, phantom: PhantomData }
|
||||||
num: self.num << rhs,
|
|
||||||
phantom: PhantomData,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Shr<u32, Output = T>, F: Unsigned> Shr<u32> for Fx<T, F> {
|
impl<T: Shr<u32, Output = T>, F: Unsigned> Shr<u32> for Fx<T, F> {
|
||||||
type Output = Self;
|
type Output = Self;
|
||||||
fn shr(self, rhs: u32) -> Self::Output {
|
fn shr(self, rhs: u32) -> Self::Output {
|
||||||
Fx {
|
Fx { num: self.num >> rhs, phantom: PhantomData }
|
||||||
num: self.num >> rhs,
|
|
||||||
phantom: PhantomData,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Neg<Output = T>, F: Unsigned> Neg for Fx<T, F> {
|
impl<T: Neg<Output = T>, F: Unsigned> Neg for Fx<T, F> {
|
||||||
type Output = Self;
|
type Output = Self;
|
||||||
fn neg(self) -> Self::Output {
|
fn neg(self) -> Self::Output {
|
||||||
Fx {
|
Fx { num: -self.num, phantom: PhantomData }
|
||||||
num: -self.num,
|
|
||||||
phantom: PhantomData,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,18 +73,12 @@ macro_rules! fixed_point_methods {
|
||||||
impl<F: Unsigned> Fx<$t, F> {
|
impl<F: Unsigned> Fx<$t, F> {
|
||||||
/// Gives the smallest positive non-zero value.
|
/// Gives the smallest positive non-zero value.
|
||||||
pub fn precision() -> Self {
|
pub fn precision() -> Self {
|
||||||
Fx {
|
Fx { num: 1, phantom: PhantomData }
|
||||||
num: 1,
|
|
||||||
phantom: PhantomData,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Makes a value with the integer part shifted into place.
|
/// Makes a value with the integer part shifted into place.
|
||||||
pub fn from_int_part(i: $t) -> Self {
|
pub fn from_int_part(i: $t) -> Self {
|
||||||
Fx {
|
Fx { num: i << F::U8, phantom: PhantomData }
|
||||||
num: i << F::U8,
|
|
||||||
phantom: PhantomData,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Changes the fractional bit quantity, keeping the base type the same.
|
/// 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);
|
let pre_shift = (self.num as i32).wrapping_mul(rhs.num as i32);
|
||||||
if pre_shift < 0 {
|
if pre_shift < 0 {
|
||||||
if pre_shift == core::i32::MIN {
|
if pre_shift == core::i32::MIN {
|
||||||
Fx {
|
Fx { num: core::$t::MIN, phantom: PhantomData }
|
||||||
num: core::$t::MIN,
|
|
||||||
phantom: PhantomData,
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
Fx {
|
Fx { num: (-((-pre_shift) >> F::U8)) as $t, phantom: PhantomData }
|
||||||
num: (-((-pre_shift) >> F::U8)) as $t,
|
|
||||||
phantom: PhantomData,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Fx {
|
Fx { num: (pre_shift >> F::U8) as $t, phantom: PhantomData }
|
||||||
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 {
|
fn div(self, rhs: Fx<$t, F>) -> Self::Output {
|
||||||
let mul_output: i32 = (self.num as i32).wrapping_mul(1 << F::U8);
|
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);
|
let divide_result: i32 = crate::bios::div(mul_output, rhs.num as i32);
|
||||||
Fx {
|
Fx { num: divide_result as $t, phantom: PhantomData }
|
||||||
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 {
|
fn div(self, rhs: Fx<$t, F>) -> Self::Output {
|
||||||
let mul_output: i32 = (self.num as i32).wrapping_mul(1 << F::U8);
|
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);
|
let divide_result: i32 = crate::bios::div(mul_output, rhs.num as i32);
|
||||||
Fx {
|
Fx { num: divide_result as $t, phantom: PhantomData }
|
||||||
num: divide_result as $t,
|
|
||||||
phantom: PhantomData,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -6,6 +6,7 @@ use super::*;
|
||||||
pub const BLDCNT: VolAddress<ColorEffectSetting> = unsafe { VolAddress::new(0x400_0050) };
|
pub const BLDCNT: VolAddress<ColorEffectSetting> = unsafe { VolAddress::new(0x400_0050) };
|
||||||
|
|
||||||
newtype! {
|
newtype! {
|
||||||
|
/// TODO: docs
|
||||||
ColorEffectSetting, u16
|
ColorEffectSetting, u16
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,10 +30,15 @@ impl ColorEffectSetting {
|
||||||
}
|
}
|
||||||
|
|
||||||
newtype_enum! {
|
newtype_enum! {
|
||||||
|
/// TODO: docs
|
||||||
ColorSpecialEffect = u16,
|
ColorSpecialEffect = u16,
|
||||||
|
/// TODO: docs
|
||||||
None = 0,
|
None = 0,
|
||||||
|
/// TODO: docs
|
||||||
AlphaBlending = 1,
|
AlphaBlending = 1,
|
||||||
|
/// TODO: docs
|
||||||
BrightnessIncrease = 2,
|
BrightnessIncrease = 2,
|
||||||
|
/// TODO: docs
|
||||||
BrightnessDecrease = 3,
|
BrightnessDecrease = 3,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,6 +46,7 @@ newtype_enum! {
|
||||||
pub const BLDALPHA: VolAddress<AlphaBlendingSetting> = unsafe { VolAddress::new(0x400_0052) };
|
pub const BLDALPHA: VolAddress<AlphaBlendingSetting> = unsafe { VolAddress::new(0x400_0052) };
|
||||||
|
|
||||||
newtype! {
|
newtype! {
|
||||||
|
/// TODO: docs
|
||||||
AlphaBlendingSetting, u16
|
AlphaBlendingSetting, u16
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,6 +62,7 @@ impl AlphaBlendingSetting {
|
||||||
pub const BLDY: VolAddress<BrightnessSetting> = unsafe { VolAddress::new(0x400_0054) };
|
pub const BLDY: VolAddress<BrightnessSetting> = unsafe { VolAddress::new(0x400_0054) };
|
||||||
|
|
||||||
newtype! {
|
newtype! {
|
||||||
|
/// TODO: docs
|
||||||
BrightnessSetting, u32
|
BrightnessSetting, u32
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -122,42 +122,11 @@ impl DisplayStatusSetting {
|
||||||
/// Gives the current scanline that the display controller is working on. If
|
/// 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
|
/// this is at or above the `VBLANK_SCANLINE` value then the display controller
|
||||||
/// is in a "vertical blank" period.
|
/// 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.
|
/// If the `VCOUNT` register reads equal to or above this then you're in vblank.
|
||||||
pub const VBLANK_SCANLINE: u16 = 160;
|
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.
|
/// Global mosaic effect control. Write-only.
|
||||||
pub const MOSAIC: VolAddress<MosaicSetting> = unsafe { VolAddress::new(0x400_004C) };
|
pub const MOSAIC: VolAddress<MosaicSetting> = unsafe { VolAddress::new(0x400_004C) };
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ use super::*;
|
||||||
/// follow the "high-active" convention (hint: you probably do, it's far easier
|
/// 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
|
/// to work with) then call `read_key_input()` rather than reading this register
|
||||||
/// directly. It will perform the necessary bit flip operation for you.
|
/// 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.
|
/// A "tribool" value helps us interpret the arrow pad.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
@ -50,9 +50,10 @@ impl KeyInput {
|
||||||
KeyInput(self.0 ^ other.0)
|
KeyInput(self.0 ^ other.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gives the arrow pad value as a tribool, with Plus being increased column
|
/// Right/left tribool.
|
||||||
/// value (right).
|
///
|
||||||
pub fn column_direction(self) -> TriBool {
|
/// Right is Plus and Left is Minus
|
||||||
|
pub fn x_tribool(self) -> TriBool {
|
||||||
if self.right() {
|
if self.right() {
|
||||||
TriBool::Plus
|
TriBool::Plus
|
||||||
} else if self.left() {
|
} else if self.left() {
|
||||||
|
@ -62,9 +63,10 @@ impl KeyInput {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gives the arrow pad value as a tribool, with Plus being increased row
|
/// Up/down tribool.
|
||||||
/// value (down).
|
///
|
||||||
pub fn row_direction(self) -> TriBool {
|
/// Down is Plus and Up is Minus
|
||||||
|
pub fn y_tribool(self) -> TriBool {
|
||||||
if self.down() {
|
if self.down() {
|
||||||
TriBool::Plus
|
TriBool::Plus
|
||||||
} else if self.up() {
|
} else if self.up() {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
///! Module for sound registers.
|
//! Module for sound registers.
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
//TODO within these "read/write" registers only some bits are actually read/write!
|
//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) };
|
pub const SOUND1CNT_L: VolAddress<SweepRegisterSetting> = unsafe { VolAddress::new(0x400_0060) };
|
||||||
|
|
||||||
newtype! {
|
newtype! {
|
||||||
|
/// TODO: docs
|
||||||
SweepRegisterSetting, u16
|
SweepRegisterSetting, u16
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,6 +25,7 @@ impl SweepRegisterSetting {
|
||||||
pub const SOUND1CNT_H: VolAddress<DutyLenEnvelopeSetting> = unsafe { VolAddress::new(0x400_0062) };
|
pub const SOUND1CNT_H: VolAddress<DutyLenEnvelopeSetting> = unsafe { VolAddress::new(0x400_0062) };
|
||||||
|
|
||||||
newtype! {
|
newtype! {
|
||||||
|
/// TODO: docs
|
||||||
DutyLenEnvelopeSetting, u16
|
DutyLenEnvelopeSetting, u16
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,6 +44,7 @@ impl DutyLenEnvelopeSetting {
|
||||||
pub const SOUND1CNT_X: VolAddress<FrequencyControlSetting> = unsafe { VolAddress::new(0x400_0064) };
|
pub const SOUND1CNT_X: VolAddress<FrequencyControlSetting> = unsafe { VolAddress::new(0x400_0064) };
|
||||||
|
|
||||||
newtype! {
|
newtype! {
|
||||||
|
/// TODO: docs
|
||||||
FrequencyControlSetting, u32 // TODO: u16 or u32?
|
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) };
|
pub const SOUND2CNT_H: VolAddress<FrequencyControlSetting> = unsafe { VolAddress::new(0x400_006C) };
|
||||||
|
|
||||||
/// Sound Channel 3 Stop/Wave RAM select (`NR23`, `NR24`). Read/Write.
|
/// 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! {
|
newtype! {
|
||||||
|
/// TODO: docs
|
||||||
StopWaveRAMSelectSetting, u16
|
StopWaveRAMSelectSetting, u16
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,6 +85,7 @@ impl StopWaveRAMSelectSetting {
|
||||||
pub const SOUND3CNT_H: VolAddress<LengthVolumeSetting> = unsafe { VolAddress::new(0x400_0072) };
|
pub const SOUND3CNT_H: VolAddress<LengthVolumeSetting> = unsafe { VolAddress::new(0x400_0072) };
|
||||||
|
|
||||||
newtype! {
|
newtype! {
|
||||||
|
/// TODO: docs
|
||||||
LengthVolumeSetting, u16
|
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) };
|
pub const SOUND4CNT_L: VolAddress<LengthEnvelopeSetting> = unsafe { VolAddress::new(0x400_0078) };
|
||||||
|
|
||||||
newtype! {
|
newtype! {
|
||||||
|
/// TODO: docs
|
||||||
LengthEnvelopeSetting, u32 // TODO: is this u32?
|
LengthEnvelopeSetting, u32 // TODO: is this u32?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,6 +140,7 @@ impl LengthEnvelopeSetting {
|
||||||
pub const SOUND4CNT_H: VolAddress<NoiseFrequencySetting> = unsafe { VolAddress::new(0x400_007C) };
|
pub const SOUND4CNT_H: VolAddress<NoiseFrequencySetting> = unsafe { VolAddress::new(0x400_007C) };
|
||||||
|
|
||||||
newtype! {
|
newtype! {
|
||||||
|
/// TODO: docs
|
||||||
NoiseFrequencySetting, u32 // TODO: is this u32?
|
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) };
|
pub const FIFO_B_H: VolAddress<u16> = unsafe { VolAddress::new(0x400_00A6) };
|
||||||
|
|
||||||
/// Channel L/R Volume/Enable (`NR50`, `NR51`). Read/Write.
|
/// 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! {
|
newtype! {
|
||||||
|
/// TODO: docs
|
||||||
NonWaveVolumeEnableSetting, u16
|
NonWaveVolumeEnableSetting, u16
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -178,6 +189,7 @@ impl NonWaveVolumeEnableSetting {
|
||||||
pub const SOUNDCNT_H: VolAddress<WaveVolumeEnableSetting> = unsafe { VolAddress::new(0x400_0082) };
|
pub const SOUNDCNT_H: VolAddress<WaveVolumeEnableSetting> = unsafe { VolAddress::new(0x400_0082) };
|
||||||
|
|
||||||
newtype! {
|
newtype! {
|
||||||
|
/// TODO: docs
|
||||||
WaveVolumeEnableSetting, u16
|
WaveVolumeEnableSetting, u16
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -199,9 +211,13 @@ impl WaveVolumeEnableSetting {
|
||||||
}
|
}
|
||||||
|
|
||||||
newtype_enum! {
|
newtype_enum! {
|
||||||
|
/// TODO: docs
|
||||||
NumberSoundVolume = u16,
|
NumberSoundVolume = u16,
|
||||||
|
/// TODO: docs
|
||||||
Quarter = 0,
|
Quarter = 0,
|
||||||
|
/// TODO: docs
|
||||||
Half = 1,
|
Half = 1,
|
||||||
|
/// TODO: docs
|
||||||
Full = 2,
|
Full = 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -209,6 +225,7 @@ newtype_enum! {
|
||||||
pub const SOUNDCNT_X: VolAddress<SoundMasterSetting> = unsafe { VolAddress::new(0x400_0084) };
|
pub const SOUNDCNT_X: VolAddress<SoundMasterSetting> = unsafe { VolAddress::new(0x400_0084) };
|
||||||
|
|
||||||
newtype! {
|
newtype! {
|
||||||
|
/// TODO: docs
|
||||||
SoundMasterSetting, u16
|
SoundMasterSetting, u16
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -227,6 +244,7 @@ impl SoundMasterSetting {
|
||||||
pub const SOUNDBIAS: VolAddress<SoundPWMSetting> = unsafe { VolAddress::new(0x400_0088) };
|
pub const SOUNDBIAS: VolAddress<SoundPWMSetting> = unsafe { VolAddress::new(0x400_0088) };
|
||||||
|
|
||||||
newtype! {
|
newtype! {
|
||||||
|
/// TODO: docs
|
||||||
SoundPWMSetting, u16
|
SoundPWMSetting, u16
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ pub const WIN0H: VolAddress<HorizontalWindowSetting> = unsafe { VolAddress::new(
|
||||||
pub const WIN1H: VolAddress<HorizontalWindowSetting> = unsafe { VolAddress::new(0x400_0042) };
|
pub const WIN1H: VolAddress<HorizontalWindowSetting> = unsafe { VolAddress::new(0x400_0042) };
|
||||||
|
|
||||||
newtype! {
|
newtype! {
|
||||||
|
/// TODO: docs
|
||||||
HorizontalWindowSetting, u16
|
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) };
|
pub const WIN1V: VolAddress<VerticalWindowSetting> = unsafe { VolAddress::new(0x400_0046) };
|
||||||
|
|
||||||
newtype! {
|
newtype! {
|
||||||
|
/// TODO: docs
|
||||||
VerticalWindowSetting, u16
|
VerticalWindowSetting, u16
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,6 +44,7 @@ impl VerticalWindowSetting {
|
||||||
pub const WININ: VolAddress<InsideWindowSetting> = unsafe { VolAddress::new(0x400_0048) };
|
pub const WININ: VolAddress<InsideWindowSetting> = unsafe { VolAddress::new(0x400_0048) };
|
||||||
|
|
||||||
newtype! {
|
newtype! {
|
||||||
|
/// TODO: docs
|
||||||
InsideWindowSetting, u16
|
InsideWindowSetting, u16
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,6 +70,7 @@ impl InsideWindowSetting {
|
||||||
pub const WINOUT: VolAddress<OutsideWindowSetting> = unsafe { VolAddress::new(0x400_004A) };
|
pub const WINOUT: VolAddress<OutsideWindowSetting> = unsafe { VolAddress::new(0x400_004A) };
|
||||||
|
|
||||||
newtype! {
|
newtype! {
|
||||||
|
/// TODO: docs
|
||||||
OutsideWindowSetting, u16
|
OutsideWindowSetting, u16
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
97
src/lib.rs
97
src/lib.rs
|
@ -3,7 +3,7 @@
|
||||||
#![feature(cfg_target_vendor)]
|
#![feature(cfg_target_vendor)]
|
||||||
#![allow(clippy::cast_lossless)]
|
#![allow(clippy::cast_lossless)]
|
||||||
#![deny(clippy::float_arithmetic)]
|
#![deny(clippy::float_arithmetic)]
|
||||||
//#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
|
|
||||||
//! This crate helps you write GBA ROMs.
|
//! This crate helps you write GBA ROMs.
|
||||||
//!
|
//!
|
||||||
|
@ -19,91 +19,9 @@
|
||||||
//! do, it's a giant bag of Undefined Behavior.
|
//! do, it's a giant bag of Undefined Behavior.
|
||||||
|
|
||||||
pub(crate) use gba_proc_macro::phantom_fields;
|
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.
|
pub mod macros;
|
||||||
///
|
|
||||||
/// 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 base;
|
pub mod base;
|
||||||
|
|
||||||
|
@ -142,7 +60,7 @@ extern "C" {
|
||||||
newtype! {
|
newtype! {
|
||||||
/// A color on the GBA is an RGB 5.5.5 within a `u16`
|
/// A color on the GBA is an RGB 5.5.5 within a `u16`
|
||||||
#[derive(PartialOrd, Ord, Hash)]
|
#[derive(PartialOrd, Ord, Hash)]
|
||||||
Color, u16
|
Color, pub u16
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Color {
|
impl Color {
|
||||||
|
@ -153,13 +71,6 @@ impl Color {
|
||||||
pub const fn from_rgb(r: u16, g: u16, b: u16) -> Color {
|
pub const fn from_rgb(r: u16, g: u16, b: u16) -> Color {
|
||||||
Color(b << 10 | g << 5 | r)
|
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
203
src/macros.rs
Normal 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
|
||||||
|
}};
|
||||||
|
}
|
|
@ -55,8 +55,6 @@ impl MGBADebug {
|
||||||
/// it might accidentally be discarded.
|
/// it might accidentally be discarded.
|
||||||
pub fn send(&mut self, level: MGBADebugLevel) {
|
pub fn send(&mut self, level: MGBADebugLevel) {
|
||||||
if level == MGBADebugLevel::Fatal {
|
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!
|
// Note(Lokathor): A Fatal send causes the emulator to halt!
|
||||||
Self::SEND_ADDRESS.write(Self::SEND_FLAG | MGBADebugLevel::Fatal as u16);
|
Self::SEND_ADDRESS.write(Self::SEND_FLAG | MGBADebugLevel::Fatal as u16);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -29,11 +29,14 @@ pub mod text;
|
||||||
/// being the correct thing.
|
/// being the correct thing.
|
||||||
pub const VRAM_BASE_USIZE: usize = 0x600_0000;
|
pub const VRAM_BASE_USIZE: usize = 0x600_0000;
|
||||||
|
|
||||||
|
pub const PAGE1_OFFSET: usize = 0xA000;
|
||||||
|
|
||||||
/// The character base blocks.
|
/// The character base blocks.
|
||||||
pub const CHAR_BASE_BLOCKS: VolBlock<[u8; 0x4000], U6> = unsafe { VolBlock::new(VRAM_BASE_USIZE) };
|
pub const CHAR_BASE_BLOCKS: VolBlock<[u8; 0x4000], U6> = unsafe { VolBlock::new(VRAM_BASE_USIZE) };
|
||||||
|
|
||||||
/// The screen entry base blocks.
|
/// 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! {
|
newtype! {
|
||||||
/// An 8x8 tile with 4bpp, packed as `u32` values for proper alignment.
|
/// An 8x8 tile with 4bpp, packed as `u32` values for proper alignment.
|
||||||
|
|
|
@ -1,155 +1,213 @@
|
||||||
//! Module for the Bitmap video modes.
|
//! Module for the Bitmap video modes.
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
use core::ops::{Div, Mul};
|
use core::ops::{Div, Mul};
|
||||||
use typenum::consts::{U128, U160, U2, U256, U4};
|
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
|
/// * **Width:** 240
|
||||||
/// * **Height:** 160
|
/// * **Height:** 160
|
||||||
///
|
///
|
||||||
/// Because the memory requirements are so large, there's only a single page
|
/// Because it takes so much space to have full color and full resolution at the
|
||||||
/// available instead of two pages like the other video modes have.
|
/// 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
|
/// As with all the bitmap video modes, the bitmap is considered to be BG2, so
|
||||||
/// must have BG2 enabled in addition to being within Mode 3.
|
/// you have to enable BG2 as well if you want to see the bitmap.
|
||||||
pub struct Mode3;
|
pub struct Mode3;
|
||||||
|
|
||||||
impl Mode3 {
|
impl Mode3 {
|
||||||
/// The physical width in pixels of the GBA screen.
|
/// The screen's width in this mode.
|
||||||
pub const SCREEN_WIDTH: usize = 240;
|
pub const WIDTH: usize = 240;
|
||||||
|
|
||||||
/// The physical height in pixels of the GBA screen.
|
/// The screen's height in this mode.
|
||||||
pub const SCREEN_HEIGHT: usize = 160;
|
pub const HEIGHT: usize = 160;
|
||||||
|
|
||||||
/// The number of pixels on the screen.
|
const VRAM: VolBlock<Color, <U256 as Mul<U160>>::Output> =
|
||||||
pub const SCREEN_PIXEL_COUNT: usize = Self::SCREEN_WIDTH * Self::SCREEN_HEIGHT;
|
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,
|
/// ## Failure
|
||||||
/// 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
|
/// Gives `None` if out of bounds
|
||||||
///
|
fn get(col: usize, row: usize) -> Option<VolAddress<Color>> {
|
||||||
/// Gives `None` if out of bounds.
|
Self::VRAM.get(col + row * Self::WIDTH)
|
||||||
pub fn read_pixel(col: usize, row: usize) -> Option<Color> {
|
|
||||||
Self::VRAM.get(col + row * Self::SCREEN_WIDTH).map(VolAddress::read)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Writes the pixel at the given (col,row).
|
/// Reads the color of the pixel specified.
|
||||||
///
|
///
|
||||||
/// # Failure
|
/// ## Failure
|
||||||
///
|
///
|
||||||
/// Gives `None` if out of bounds.
|
/// Gives `None` if out of bounds
|
||||||
pub fn write_pixel(col: usize, row: usize, color: Color) -> Option<()> {
|
pub fn read(col: usize, row: usize) -> Option<Color> {
|
||||||
Self::VRAM.get(col + row * Self::SCREEN_WIDTH).map(|va| va.write(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) {
|
pub fn clear_to(color: Color) {
|
||||||
let color32 = color.0 as u32;
|
let color32 = color.0 as u32;
|
||||||
let bulk_color = color32 << 16 | color32;
|
let bulk_color = color32 << 16 | color32;
|
||||||
for va in Self::VRAM_BULK.iter() {
|
for va in Self::WORDS_BLOCK.iter() {
|
||||||
va.write(bulk_color)
|
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) {
|
pub fn dma_clear_to(color: Color) {
|
||||||
use crate::io::dma::DMA3;
|
use crate::io::dma::DMA3;
|
||||||
|
|
||||||
let color32 = color.0 as u32;
|
let color32 = color.0 as u32;
|
||||||
let bulk_color = color32 << 16 | color32;
|
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?
|
/// Used to select what page to read from or write to in Mode 4 and Mode 5.
|
||||||
//TODO: Mode3 Line Drawing?
|
#[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
|
/// * **Width:** 240
|
||||||
/// * **Height:** 160
|
/// * **Height:** 160
|
||||||
/// * **Pages:** 2
|
/// * **Pages:** 2
|
||||||
///
|
///
|
||||||
/// VRAM has a minimum write size of 2 bytes at a time, so writing individual
|
/// Because the pixels use palette indexes there's enough space to have two
|
||||||
/// palette entries for the pixels is more costly than with the other bitmap
|
/// pages.
|
||||||
/// modes.
|
|
||||||
///
|
///
|
||||||
/// As with all bitmap modes, the image itself utilizes BG2 for display, so you
|
/// As with all the bitmap video modes, the bitmap is considered to be BG2, so
|
||||||
/// must have BG2 enabled in addition to being within Mode 4.
|
/// you have to enable BG2 as well if you want to see the bitmap.
|
||||||
pub struct Mode4;
|
pub struct Mode4;
|
||||||
|
|
||||||
impl Mode4 {
|
impl Mode4 {
|
||||||
/// The physical width in pixels of the GBA screen.
|
/// The screen's width in this mode.
|
||||||
pub const SCREEN_WIDTH: usize = 240;
|
pub const WIDTH: usize = 240;
|
||||||
|
|
||||||
/// The physical height in pixels of the GBA screen.
|
/// The screen's height in this mode.
|
||||||
pub const SCREEN_HEIGHT: usize = 160;
|
pub const HEIGHT: usize = 160;
|
||||||
|
|
||||||
/// The number of pixels on the screen.
|
const PAGE0_INDEXES: VolBlock<u8, <U256 as Mul<U160>>::Output> =
|
||||||
pub const SCREEN_PIXEL_COUNT: usize = Self::SCREEN_WIDTH * Self::SCREEN_HEIGHT;
|
unsafe { VolBlock::new(VRAM_BASE_USIZE) };
|
||||||
|
|
||||||
/// Used for bulk clearing operations.
|
const PAGE1_INDEXES: VolBlock<u8, <U256 as Mul<U160>>::Output> =
|
||||||
const SCREEN_U32_COUNT: usize = Self::SCREEN_PIXEL_COUNT / 4;
|
unsafe { VolBlock::new(VRAM_BASE_USIZE + PAGE1_OFFSET) };
|
||||||
|
|
||||||
// TODO: newtype this?
|
const PAGE0_WORDS: VolBlock<u32, <<U256 as Mul<U160>>::Output as Div<U4>>::Output> =
|
||||||
const PAGE0_BLOCK8: VolBlock<u8, <U256 as Mul<U160>>::Output> = unsafe { VolBlock::new(VRAM_BASE_USIZE) };
|
unsafe { VolBlock::new(VRAM_BASE_USIZE) };
|
||||||
|
|
||||||
// TODO: newtype this?
|
const PAGE1_WORDS: VolBlock<u32, <<U256 as Mul<U160>>::Output as Div<U4>>::Output> =
|
||||||
const PAGE1_BLOCK8: VolBlock<u8, <U256 as Mul<U160>>::Output> = unsafe { VolBlock::new(VRAM_BASE_USIZE + 0xA000) };
|
unsafe { VolBlock::new(VRAM_BASE_USIZE + PAGE1_OFFSET) };
|
||||||
|
|
||||||
// TODO: newtype this?
|
/// Reads the color of the pixel specified.
|
||||||
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).
|
|
||||||
///
|
///
|
||||||
/// # Failure
|
/// ## Failure
|
||||||
///
|
///
|
||||||
/// Gives `None` if out of bounds.
|
/// Gives `None` if out of bounds
|
||||||
pub fn read_pixel(page1: bool, col: usize, row: usize) -> Option<u8> {
|
pub fn read(page: Page, col: usize, row: usize) -> Option<u8> {
|
||||||
// Note(Lokathor): byte _reads_ from VRAM are okay.
|
match page {
|
||||||
if page1 {
|
Page::Zero => Self::PAGE0_INDEXES,
|
||||||
Self::PAGE1_BLOCK8.get(col + row * Self::SCREEN_WIDTH).map(VolAddress::read)
|
Page::One => Self::PAGE1_INDEXES,
|
||||||
} else {
|
|
||||||
Self::PAGE0_BLOCK8.get(col + row * Self::SCREEN_WIDTH).map(VolAddress::read)
|
|
||||||
}
|
}
|
||||||
|
.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.
|
/// Gives `None` if out of bounds
|
||||||
pub fn write_pixel(page1: bool, col: usize, row: usize, pal8bpp: u8) -> Option<()> {
|
pub fn write(page: Page, col: usize, row: usize, pal8bpp: u8) -> Option<()> {
|
||||||
// Note(Lokathor): byte _writes_ to VRAM are not permitted. We must jump
|
// Note(Lokathor): Byte writes to VRAM aren't permitted, we have to jump
|
||||||
// through hoops when we attempt to write just a single byte.
|
// through some hoops.
|
||||||
if col < Self::SCREEN_WIDTH && row < Self::SCREEN_HEIGHT {
|
if col < Self::WIDTH && row < Self::HEIGHT {
|
||||||
let real_index = col + row * Self::SCREEN_WIDTH;
|
let real_index = col + row * Self::WIDTH;
|
||||||
let rounded_down_index = real_index & !1;
|
let rounded_down_index = real_index & !1;
|
||||||
let address: VolAddress<u16> = unsafe {
|
let address: VolAddress<u16> = unsafe {
|
||||||
if page1 {
|
match page {
|
||||||
Self::PAGE1_BLOCK8.index(rounded_down_index).cast()
|
Page::Zero => Self::PAGE0_INDEXES,
|
||||||
} else {
|
Page::One => Self::PAGE1_INDEXES,
|
||||||
Self::PAGE0_BLOCK8.index(rounded_down_index).cast()
|
|
||||||
}
|
}
|
||||||
|
.index_unchecked(rounded_down_index)
|
||||||
|
.cast::<u16>()
|
||||||
};
|
};
|
||||||
if real_index == rounded_down_index {
|
if real_index == rounded_down_index {
|
||||||
// even byte, change the high bits
|
// 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`
|
/// Takes ~215,000 cycles (~76% of a frame)
|
||||||
/// values, allowing you to write two palette entries side by side as a single
|
pub fn clear_to(page: Page, pal8bpp: u8) {
|
||||||
/// 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) {
|
|
||||||
let pal8bpp_32 = pal8bpp as u32;
|
let pal8bpp_32 = pal8bpp as u32;
|
||||||
let bulk_color = (pal8bpp_32 << 24) | (pal8bpp_32 << 16) | (pal8bpp_32 << 8) | pal8bpp_32;
|
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)
|
va.write(bulk_color)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clears the page to the desired color using DMA3.
|
/// Clears the screen to the palette index specified using DMA3.
|
||||||
pub fn dma_clear_page_to(page1: bool, pal8bpp: u8) {
|
///
|
||||||
|
/// Takes ~30,800 frames (~37% of VBlank)
|
||||||
|
pub fn dma_clear_to(page: Page, pal8bpp: u8) {
|
||||||
use crate::io::dma::DMA3;
|
use crate::io::dma::DMA3;
|
||||||
|
|
||||||
let pal8bpp_32 = pal8bpp as u32;
|
let pal8bpp_32 = pal8bpp as u32;
|
||||||
let bulk_color = (pal8bpp_32 << 24) | (pal8bpp_32 << 16) | (pal8bpp_32 << 8) | pal8bpp_32;
|
let bulk_color = (pal8bpp_32 << 24) | (pal8bpp_32 << 16) | (pal8bpp_32 << 8) | pal8bpp_32;
|
||||||
let write_target = if page1 {
|
let words_address = unsafe {
|
||||||
VRAM_BASE_USIZE as *mut u32
|
match page {
|
||||||
} else {
|
Page::Zero => Self::PAGE0_WORDS.index_unchecked(0).to_usize(),
|
||||||
(VRAM_BASE_USIZE + 0xA000) as *mut u32
|
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.
|
/// Mode 5 is a bitmap mode with full color and reduced resolution.
|
||||||
///
|
///
|
||||||
/// * **Width:** 160
|
/// * **Width:** 160
|
||||||
/// * **Height:** 128
|
/// * **Height:** 128
|
||||||
/// * **Pages:** 2
|
/// * **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
|
/// As with all the bitmap video modes, the bitmap is considered to be BG2, so
|
||||||
/// must have BG2 enabled in addition to being within Mode 3.
|
/// you have to enable BG2 as well if you want to see the bitmap.
|
||||||
pub struct Mode5;
|
pub struct Mode5;
|
||||||
|
|
||||||
impl Mode5 {
|
impl Mode5 {
|
||||||
/// The physical width in pixels of the GBA screen.
|
/// The screen's width in this mode.
|
||||||
pub const SCREEN_WIDTH: usize = 160;
|
pub const WIDTH: usize = 160;
|
||||||
|
|
||||||
/// The physical height in pixels of the GBA screen.
|
/// The screen's height in this mode.
|
||||||
pub const SCREEN_HEIGHT: usize = 128;
|
pub const HEIGHT: usize = 128;
|
||||||
|
|
||||||
/// The number of pixels on the screen.
|
const PAGE0_PIXELS: VolBlock<Color, <U160 as Mul<U128>>::Output> =
|
||||||
pub const SCREEN_PIXEL_COUNT: usize = Self::SCREEN_WIDTH * Self::SCREEN_HEIGHT;
|
unsafe { VolBlock::new(VRAM_BASE_USIZE) };
|
||||||
|
|
||||||
/// Used for bulk clearing operations.
|
const PAGE1_PIXELS: VolBlock<Color, <U160 as Mul<U128>>::Output> =
|
||||||
const SCREEN_U32_COUNT: usize = Self::SCREEN_PIXEL_COUNT / 2;
|
unsafe { VolBlock::new(VRAM_BASE_USIZE + PAGE1_OFFSET) };
|
||||||
|
|
||||||
// TODO: newtype this?
|
const PAGE0_WORDS: VolBlock<u32, <<U160 as Mul<U128>>::Output as Div<U2>>::Output> =
|
||||||
const PAGE0_BLOCK: VolBlock<Color, <U160 as Mul<U128>>::Output> = unsafe { VolBlock::new(VRAM_BASE_USIZE) };
|
unsafe { VolBlock::new(VRAM_BASE_USIZE) };
|
||||||
|
|
||||||
// TODO: newtype this?
|
const PAGE1_WORDS: VolBlock<u32, <<U160 as Mul<U128>>::Output as Div<U2>>::Output> =
|
||||||
const PAGE1_BLOCK: VolBlock<Color, <U160 as Mul<U128>>::Output> = unsafe { VolBlock::new(VRAM_BASE_USIZE + 0xA000) };
|
unsafe { VolBlock::new(VRAM_BASE_USIZE + PAGE1_OFFSET) };
|
||||||
|
|
||||||
/// private iterator over the page0 pixels, four at a time
|
/// Reads the color of the pixel specified.
|
||||||
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).
|
|
||||||
///
|
///
|
||||||
/// # Failure
|
/// ## Failure
|
||||||
///
|
///
|
||||||
/// Gives `None` if out of bounds.
|
/// Gives `None` if out of bounds
|
||||||
pub fn read_pixel(page1: bool, col: usize, row: usize) -> Option<Color> {
|
pub fn read(page: Page, col: usize, row: usize) -> Option<Color> {
|
||||||
if page1 {
|
match page {
|
||||||
Self::PAGE1_BLOCK.get(col + row * Self::SCREEN_WIDTH).map(VolAddress::read)
|
Page::Zero => Self::PAGE0_PIXELS,
|
||||||
} else {
|
Page::One => Self::PAGE1_PIXELS,
|
||||||
Self::PAGE0_BLOCK.get(col + row * Self::SCREEN_WIDTH).map(VolAddress::read)
|
|
||||||
}
|
}
|
||||||
|
.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.
|
/// Gives `None` if out of bounds
|
||||||
pub fn write_pixel(page1: bool, col: usize, row: usize, color: Color) -> Option<()> {
|
pub fn write(page: Page, col: usize, row: usize, color: Color) -> Option<()> {
|
||||||
if page1 {
|
match page {
|
||||||
Self::PAGE1_BLOCK.get(col + row * Self::SCREEN_WIDTH).map(|va| va.write(color))
|
Page::Zero => Self::PAGE0_PIXELS,
|
||||||
} else {
|
Page::One => Self::PAGE1_PIXELS,
|
||||||
Self::PAGE0_BLOCK.get(col + row * Self::SCREEN_WIDTH).map(|va| va.write(color))
|
|
||||||
}
|
}
|
||||||
|
.get(col + row * Self::WIDTH)
|
||||||
|
.map(|va| va.write(color))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clears the whole screen to the desired color.
|
/// Clear the screen to the color specified.
|
||||||
pub fn clear_page_to(page1: bool, color: Color) {
|
///
|
||||||
|
/// Takes ~215,000 cycles (~76% of a frame)
|
||||||
|
pub fn clear_to(page: Page, color: Color) {
|
||||||
let color32 = color.0 as u32;
|
let color32 = color.0 as u32;
|
||||||
let bulk_color = color32 << 16 | color32;
|
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)
|
va.write(bulk_color)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clears the whole screen to the desired color using DMA3.
|
/// Clears the screen to the color specified using DMA3.
|
||||||
pub fn dma_clear_page_to(page1: bool, color: Color) {
|
///
|
||||||
|
/// Takes ~30,800 frames (~37% of VBlank)
|
||||||
|
pub fn dma_clear_to(page: Page, color: Color) {
|
||||||
use crate::io::dma::DMA3;
|
use crate::io::dma::DMA3;
|
||||||
|
|
||||||
let color32 = color.0 as u32;
|
let color32 = color.0 as u32;
|
||||||
let bulk_color = color32 << 16 | color32;
|
let bulk_color = color32 << 16 | color32;
|
||||||
let write_target = if page1 {
|
let words_address = unsafe {
|
||||||
VRAM_BASE_USIZE as *mut u32
|
match page {
|
||||||
} else {
|
Page::Zero => Self::PAGE0_WORDS.index_unchecked(0).to_usize(),
|
||||||
(VRAM_BASE_USIZE + 0xA000) as *mut u32
|
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?
|
|
||||||
|
|
Loading…
Reference in a new issue