mirror of
https://github.com/italicsjenga/gba.git
synced 2024-12-23 19:01:30 +11:00
bitmap video explanation
This commit is contained in:
parent
3b688b907c
commit
0f031e6435
|
@ -16,12 +16,199 @@ consider this memory to be in one of a few totally different formats.
|
||||||
|
|
||||||
### Mode 3
|
### 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
|
### 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
|
### 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
|
## Using Mode 3
|
||||||
|
|
||||||
TODO
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
|
@ -21,6 +21,22 @@ fn panic(info: &core::panic::PanicInfo) -> ! {
|
||||||
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 =
|
const SETTING: DisplayControlSetting =
|
||||||
|
@ -49,10 +65,9 @@ fn main(_argc: isize, _argv: *const *const u8) -> isize {
|
||||||
spin_until_vblank();
|
spin_until_vblank();
|
||||||
|
|
||||||
// draw the new game and wait until the next frame starts.
|
// draw the new game and wait until the next frame starts.
|
||||||
const BLACK: Color = Color::from_rgb(0, 0, 0);
|
|
||||||
if px >= Mode3::WIDTH || py >= Mode3::HEIGHT {
|
if px >= Mode3::WIDTH || py >= Mode3::HEIGHT {
|
||||||
// out of bounds, reset the screen and position.
|
// out of bounds, reset the screen and position.
|
||||||
Mode3::dma_clear_to(BLACK);
|
Mode3::dma_clear_to(Color::from_rgb(0, 0, 0));
|
||||||
px = Mode3::WIDTH / 2;
|
px = Mode3::WIDTH / 2;
|
||||||
py = Mode3::HEIGHT / 2;
|
py = Mode3::HEIGHT / 2;
|
||||||
} else {
|
} else {
|
||||||
|
@ -67,19 +82,3 @@ fn main(_argc: isize, _argv: *const *const u8) -> isize {
|
||||||
spin_until_vdraw();
|
spin_until_vdraw();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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 {}
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in a new issue